1mod commands;
2mod hardware;
3mod model;
4mod traits;
5
6use anyhow::Result;
7use futures::future::FutureExt;
8use futures::{Stream, StreamExt, pin_mut, stream};
9use ipnet::IpNet;
10use rand::seq::SliceRandom;
11use reqwest::StatusCode;
12use reqwest::header::HeaderMap;
13use std::collections::HashSet;
14use std::net::IpAddr;
15use std::net::Ipv4Addr;
16use std::str::FromStr;
17use std::time::Duration;
18use tokio::net::TcpStream;
19use tokio::task::JoinSet;
20use tokio::time::timeout;
21
22use super::commands::MinerCommand;
23use super::util::{send_rpc_command, send_web_command};
24use crate::data::device::{MinerFirmware, MinerMake, MinerModel};
25use crate::miners::backends::antminer::AntMiner;
26use crate::miners::backends::avalonminer::AvalonMiner;
27use crate::miners::backends::bitaxe::Bitaxe;
28use crate::miners::backends::braiins::Braiins;
29use crate::miners::backends::epic::PowerPlay;
30use crate::miners::backends::luxminer::LuxMiner;
31use crate::miners::backends::marathon::Marathon;
32use crate::miners::backends::traits::*;
33use crate::miners::backends::vnish::Vnish;
34use crate::miners::backends::whatsminer::WhatsMiner;
35use crate::miners::factory::traits::VersionSelection;
36use std::net::SocketAddr;
37use std::pin::Pin;
38use std::sync::Arc;
39use traits::{DiscoveryCommands, ModelSelection};
40
41const IDENTIFICATION_TIMEOUT: Duration = Duration::from_secs(10);
42const CONNECTIVITY_TIMEOUT: Duration = Duration::from_secs(1);
43const CONNECTIVITY_RETRIES: u32 = 3;
44
45fn calculate_optimal_concurrency(ip_count: usize) -> usize {
46 match ip_count {
48 0..=1000 => 1000, 1001..=5000 => 2500, 5001..=10000 => 5000, _ => 10000, }
53}
54
55async fn check_port_open(ip: IpAddr, port: u16, connectivity_timeout: Duration) -> bool {
57 let addr: SocketAddr = (ip, port).into();
58
59 let stream = match timeout(connectivity_timeout, TcpStream::connect(addr)).await {
60 Ok(Ok(stream)) => stream,
61 _ => return false,
62 };
63
64 let _ = stream.set_nodelay(true);
66
67 let _ = stream.set_linger(Some(Duration::from_secs(0)));
69
70 true
71}
72
73async fn get_miner_type_from_command(
74 ip: IpAddr,
75 command: MinerCommand,
76) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
77 match command {
78 MinerCommand::RPC {
79 command,
80 parameters: _,
81 } => {
82 let response = send_rpc_command(&ip, command).await?;
83 parse_type_from_socket(response)
84 }
85 MinerCommand::WebAPI {
86 command,
87 parameters: _,
88 } => {
89 let response = send_web_command(&ip, command).await?;
90 parse_type_from_web(response)
91 }
92 _ => None,
93 }
94}
95
96fn parse_type_from_socket(
97 response: serde_json::Value,
98) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
99 let json_string = response.to_string().to_uppercase();
100 match () {
101 _ if json_string.contains("BOSMINER") || json_string.contains("BOSER") => {
102 Some((None, Some(MinerFirmware::BraiinsOS)))
103 }
104 _ if json_string.contains("LUXMINER") => Some((None, Some(MinerFirmware::LuxOS))),
105 _ if json_string.contains("MARAFW") || json_string.contains("KAONSU") => {
106 Some((None, Some(MinerFirmware::Marathon)))
107 }
108 _ if json_string.contains("VNISH") => Some((None, Some(MinerFirmware::VNish))),
109 _ if json_string.contains("BITMICRO") || json_string.contains("BTMINER") => {
110 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
111 }
112 _ if json_string.contains("ANTMINER") => {
113 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
114 }
115 _ if json_string.contains("AVALON") => {
116 Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
117 }
118 _ => None,
119 }
120}
121
122fn parse_type_from_web(
123 response: (String, HeaderMap, StatusCode),
124) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
125 let (resp_text, resp_headers, resp_status) = response;
126 let auth_header = match resp_headers.get("www-authenticate") {
127 Some(header) => header.to_str().unwrap(),
128 None => "",
129 };
130 let algo_header = match resp_headers.get("algorithm") {
131 Some(header) => header.to_str().unwrap(),
132 None => "",
133 };
134 let redirect_header = match resp_headers.get("location") {
135 Some(header) => header.to_str().unwrap(),
136 None => "",
137 };
138 match () {
139 _ if resp_status == 401 && algo_header.contains("MD5") => {
140 Some((None, Some(MinerFirmware::Marathon)))
141 }
142 _ if resp_status == 401 && auth_header.contains("realm=\"antMiner") => {
143 Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
144 }
145 _ if resp_text.contains("Braiins OS") => Some((None, Some(MinerFirmware::BraiinsOS))),
146 _ if resp_text.contains("Luxor Firmware") => Some((None, Some(MinerFirmware::LuxOS))),
147 _ if resp_text.contains("AxeOS") => {
148 Some((Some(MinerMake::Bitaxe), Some(MinerFirmware::Stock)))
149 }
150 _ if resp_text.contains("Miner Web Dashboard") => Some((None, Some(MinerFirmware::EPic))),
151 _ if resp_text.contains("Avalon") => {
152 Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
153 }
154 _ if resp_text.contains("AnthillOS") => Some((None, Some(MinerFirmware::VNish))),
155 _ if redirect_header.contains("https://") && resp_status == 307
156 || resp_text.contains("/cgi-bin/luci") =>
157 {
158 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
159 }
160 _ => None,
161 }
162}
163
164fn select_backend(
165 ip: IpAddr,
166 model: MinerModel,
167 firmware: Option<MinerFirmware>,
168 version: Option<semver::Version>,
169) -> Option<Box<dyn Miner>> {
170 match (model, firmware) {
171 (MinerModel::WhatsMiner(_), Some(MinerFirmware::Stock)) => {
172 Some(WhatsMiner::new(ip, model, version))
173 }
174 (MinerModel::Bitaxe(_), Some(MinerFirmware::Stock)) => {
175 Some(Bitaxe::new(ip, model, version))
176 }
177 (MinerModel::AvalonMiner(_), Some(MinerFirmware::Stock)) => {
178 Some(AvalonMiner::new(ip, model, version))
179 }
180 (MinerModel::AntMiner(_), Some(MinerFirmware::Stock)) => {
181 Some(AntMiner::new(ip, model, version))
182 }
183 (_, Some(MinerFirmware::VNish)) => Some(Vnish::new(ip, model, version)),
184 (_, Some(MinerFirmware::EPic)) => Some(PowerPlay::new(ip, model, version)),
185 (_, Some(MinerFirmware::Marathon)) => Some(Marathon::new(ip, model, version)),
186 (_, Some(MinerFirmware::LuxOS)) => Some(LuxMiner::new(ip, model, version)),
187 (_, Some(MinerFirmware::BraiinsOS)) => Some(Braiins::new(ip, model, version)),
188 _ => None,
189 }
190}
191
192#[derive(Debug, Clone)]
193pub struct MinerFactory {
194 search_makes: Option<Vec<MinerMake>>,
195 search_firmwares: Option<Vec<MinerFirmware>>,
196 ips: Vec<IpAddr>,
197 identification_timeout: Duration,
198 connectivity_timeout: Duration,
199 connectivity_retries: u32,
200 concurrent: Option<usize>,
201 check_port: bool,
202}
203
204impl Default for MinerFactory {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl MinerFactory {
211 pub async fn scan_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn Miner>>> {
212 if (1..self.connectivity_retries).next().is_some() {
214 if !self.check_port {
215 return self.get_miner(ip).await;
216 }
217 if check_port_open(ip, 80, self.connectivity_timeout).await {
219 return self.get_miner(ip).await;
220 }
221 if check_port_open(ip, 4028, self.connectivity_timeout).await {
223 return self.get_miner(ip).await;
224 }
225 if check_port_open(ip, 4029, self.connectivity_timeout).await {
227 return self.get_miner(ip).await;
228 }
229 if check_port_open(ip, 8889, self.connectivity_timeout).await {
231 return self.get_miner(ip).await;
232 }
233 }
234 Ok(None)
235 }
236
237 pub async fn get_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn Miner>>> {
238 let search_makes = self.search_makes.clone().unwrap_or(vec![
239 MinerMake::AntMiner,
240 MinerMake::WhatsMiner,
241 MinerMake::AvalonMiner,
242 MinerMake::EPic,
243 MinerMake::Braiins,
244 MinerMake::Bitaxe,
245 ]);
246 let search_firmwares = self.search_firmwares.clone().unwrap_or(vec![
247 MinerFirmware::Stock,
248 MinerFirmware::BraiinsOS,
249 MinerFirmware::VNish,
250 MinerFirmware::EPic,
251 MinerFirmware::HiveOS,
252 MinerFirmware::LuxOS,
253 MinerFirmware::Marathon,
254 ]);
255 let mut commands: HashSet<MinerCommand> = HashSet::new();
256
257 for make in search_makes {
258 for command in make.get_discovery_commands() {
259 commands.insert(command);
260 }
261 }
262 for firmware in search_firmwares {
263 for command in firmware.get_discovery_commands() {
264 commands.insert(command);
265 }
266 }
267
268 let mut discovery_tasks = JoinSet::new();
269 for command in commands {
270 let _ = discovery_tasks.spawn(get_miner_type_from_command(ip, command));
271 }
272
273 let timeout = tokio::time::sleep(self.identification_timeout).fuse();
274 let tasks = tokio::spawn(async move {
275 loop {
276 if discovery_tasks.is_empty() {
277 return None;
278 };
279 match discovery_tasks.join_next().await.unwrap_or(Ok(None)) {
280 Ok(Some(result)) => {
281 return Some(result);
282 }
283 _ => continue,
284 };
285 }
286 });
287
288 pin_mut!(timeout, tasks);
289
290 let miner_info = tokio::select!(
291 Ok(miner_info) = &mut tasks => {
292 miner_info
293 },
294 _ = &mut timeout => {
295 None
296 }
297 );
298
299 match miner_info {
300 Some((Some(make), Some(MinerFirmware::Stock))) => {
301 let model = make.get_model(ip).await?;
302 let version = make.get_version(ip).await;
303
304 Ok(select_backend(
305 ip,
306 model,
307 Some(MinerFirmware::Stock),
308 version,
309 ))
310 }
311 Some((_, Some(firmware))) => {
312 let model = firmware.get_model(ip).await?;
313 let version = firmware.get_version(ip).await;
314
315 Ok(select_backend(ip, model, Some(firmware), version))
316 }
317 Some((Some(make), firmware)) => {
318 let model = make.get_model(ip).await?;
319 let version = make.get_version(ip).await;
320
321 Ok(select_backend(ip, model, firmware, version))
322 }
323 _ => Ok(None),
324 }
325 }
326
327 pub fn new() -> MinerFactory {
328 MinerFactory {
329 search_makes: None,
330 search_firmwares: None,
331 ips: Vec::new(),
332 identification_timeout: IDENTIFICATION_TIMEOUT,
333 connectivity_timeout: CONNECTIVITY_TIMEOUT,
334 connectivity_retries: CONNECTIVITY_RETRIES,
335 concurrent: None,
336 check_port: true, }
338 }
339
340 pub fn with_port_check(mut self, enabled: bool) -> Self {
342 self.check_port = enabled;
343 self
344 }
345
346 pub fn with_concurrent_limit(mut self, limit: usize) -> Self {
348 self.concurrent = Some(limit);
349 self
350 }
351
352 pub fn with_adaptive_concurrency(mut self) -> Self {
353 self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
354 self
355 }
356
357 pub fn update_adaptive_concurrency(&mut self) {
358 if self.concurrent.is_none() {
359 self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
360 }
361 }
362
363 pub fn with_identification_timeout(mut self, timeout: Duration) -> Self {
365 self.identification_timeout = timeout;
366 self
367 }
368
369 pub fn with_identification_timeout_secs(mut self, timeout_secs: u64) -> Self {
370 self.identification_timeout = Duration::from_secs(timeout_secs);
371 self
372 }
373
374 pub fn with_connectivity_timeout(mut self, timeout: Duration) -> Self {
375 self.connectivity_timeout = timeout;
376 self
377 }
378
379 pub fn with_connectivity_timeout_secs(mut self, timeout_secs: u64) -> Self {
380 self.connectivity_timeout = Duration::from_secs(timeout_secs);
381 self
382 }
383
384 pub fn with_connectivity_retries(mut self, retries: u32) -> Self {
385 self.connectivity_retries = retries;
386 self
387 }
388
389 pub fn with_search_makes(mut self, search_makes: Vec<MinerMake>) -> Self {
391 self.search_makes = Some(search_makes);
392 self
393 }
394
395 pub fn with_makes(mut self, makes: Vec<MinerMake>) -> Self {
396 self.search_makes = Some(makes);
397 self
398 }
399
400 pub fn add_search_make(mut self, search_make: MinerMake) -> Self {
401 if self.search_makes.is_none() {
402 self.search_makes = Some(vec![search_make]);
403 } else {
404 self.search_makes.as_mut().unwrap().push(search_make);
405 }
406 self
407 }
408
409 pub fn remove_search_make(mut self, search_make: MinerMake) -> Self {
410 if let Some(makes) = self.search_makes.as_mut() {
411 makes.retain(|val| *val != search_make);
412 }
413 self
414 }
415
416 pub fn with_search_firmwares(mut self, search_firmwares: Vec<MinerFirmware>) -> Self {
418 self.search_firmwares = Some(search_firmwares);
419 self
420 }
421
422 pub fn with_firmwares(mut self, firmwares: Vec<MinerFirmware>) -> Self {
423 self.search_firmwares = Some(firmwares);
424 self
425 }
426
427 pub fn add_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
428 if self.search_firmwares.is_none() {
429 self.search_firmwares = Some(vec![search_firmware]);
430 } else {
431 self.search_firmwares
432 .as_mut()
433 .unwrap()
434 .push(search_firmware);
435 }
436 self
437 }
438
439 pub fn remove_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
440 if let Some(firmwares) = self.search_firmwares.as_mut() {
441 firmwares.retain(|val| *val != search_firmware);
442 }
443 self
444 }
445
446 pub fn from_subnet(subnet: &str) -> Result<Self> {
449 Self::new().with_subnet(subnet)
450 }
451
452 pub fn with_subnet(mut self, subnet: &str) -> Result<Self> {
454 let ips = self.hosts_from_subnet(subnet)?;
455 self.ips.extend(ips);
456 self.shuffle_ips();
457 Ok(self)
458 }
459
460 pub fn set_subnet(&mut self, subnet: &str) -> Result<&Self> {
462 let ips = self.hosts_from_subnet(subnet)?;
463 self.ips = ips;
464 self.shuffle_ips();
465 Ok(self)
466 }
467
468 fn hosts_from_subnet(&self, subnet: &str) -> Result<Vec<IpAddr>> {
469 let network = IpNet::from_str(subnet)?;
470 Ok(network.hosts().collect())
471 }
472
473 fn shuffle_ips(&mut self) {
475 let mut rng = rand::rng();
476 self.ips.shuffle(&mut rng);
477 }
478
479 pub fn from_octets(octet1: &str, octet2: &str, octet3: &str, octet4: &str) -> Result<Self> {
482 Self::new().with_octets(octet1, octet2, octet3, octet4)
483 }
484
485 pub fn with_octets(
487 mut self,
488 octet1: &str,
489 octet2: &str,
490 octet3: &str,
491 octet4: &str,
492 ) -> Result<Self> {
493 let ips = self.hosts_from_octets(octet1, octet2, octet3, octet4)?;
494 self.ips.extend(ips);
495 self.shuffle_ips();
496 Ok(self)
497 }
498
499 pub fn set_octets(
501 &mut self,
502 octet1: &str,
503 octet2: &str,
504 octet3: &str,
505 octet4: &str,
506 ) -> Result<&Self> {
507 let ips = self.hosts_from_octets(octet1, octet2, octet3, octet4)?;
508 self.ips = ips;
509 self.shuffle_ips();
510 Ok(self)
511 }
512
513 fn hosts_from_octets(
514 &self,
515 octet1: &str,
516 octet2: &str,
517 octet3: &str,
518 octet4: &str,
519 ) -> Result<Vec<IpAddr>> {
520 let octet1_range = parse_octet_range(octet1)?;
521 let octet2_range = parse_octet_range(octet2)?;
522 let octet3_range = parse_octet_range(octet3)?;
523 let octet4_range = parse_octet_range(octet4)?;
524
525 Ok(generate_ips_from_ranges(
526 &octet1_range,
527 &octet2_range,
528 &octet3_range,
529 &octet4_range,
530 ))
531 }
532
533 pub fn from_range(range_str: &str) -> Result<Self> {
536 Self::new().with_range(range_str)
537 }
538
539 pub fn with_range(mut self, range_str: &str) -> Result<Self> {
541 let ips = self.hosts_from_range(range_str)?;
542 self.ips.extend(ips);
543 self.shuffle_ips();
544 Ok(self)
545 }
546
547 pub fn set_range(&mut self, range_str: &str) -> Result<&Self> {
549 let ips = self.hosts_from_range(range_str)?;
550 self.ips = ips;
551 self.shuffle_ips();
552 Ok(self)
553 }
554
555 fn hosts_from_range(&self, range_str: &str) -> Result<Vec<IpAddr>> {
556 let parts: Vec<&str> = range_str.split('.').collect();
557 if parts.len() != 4 {
558 return Err(anyhow::anyhow!(
559 "Invalid IP range format. Expected format: 10.1-199.0.1-199"
560 ));
561 }
562
563 let octet1_range = parse_octet_range(parts[0])?;
564 let octet2_range = parse_octet_range(parts[1])?;
565 let octet3_range = parse_octet_range(parts[2])?;
566 let octet4_range = parse_octet_range(parts[3])?;
567
568 Ok(generate_ips_from_ranges(
569 &octet1_range,
570 &octet2_range,
571 &octet3_range,
572 &octet4_range,
573 ))
574 }
575
576 pub fn hosts(&self) -> Vec<IpAddr> {
578 self.ips.clone()
579 }
580
581 pub fn len(&self) -> usize {
583 self.ips.len()
584 }
585
586 pub fn is_empty(&self) -> bool {
588 self.ips.is_empty()
589 }
590
591 pub async fn scan(&self) -> Result<Vec<Box<dyn Miner>>> {
593 if self.ips.is_empty() {
594 return Err(anyhow::anyhow!(
595 "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
596 ));
597 }
598
599 let concurrency = self
600 .concurrent
601 .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
602
603 let miners: Vec<Box<dyn Miner>> = stream::iter(self.ips.iter().copied())
604 .map(|ip| async move { self.scan_miner(ip).await.ok().flatten() })
605 .buffer_unordered(concurrency)
606 .filter_map(|miner_opt| async move { miner_opt })
607 .collect()
608 .await;
609
610 Ok(miners)
611 }
612
613 pub fn scan_stream(&self) -> Pin<Box<impl Stream<Item = Box<dyn Miner>> + Send + use<>>> {
614 let concurrency = self
615 .concurrent
616 .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
617
618 let factory = Arc::new(self.clone());
619 let ips: Arc<[IpAddr]> = Arc::from(self.ips.as_slice());
620
621 let ip_count = ips.len();
622 let stream = stream::iter(0..ip_count)
623 .map(move |i| {
624 let factory = Arc::clone(&factory);
625 let ips = Arc::clone(&ips);
626 async move { factory.scan_miner(ips[i]).await.ok().flatten() }
627 })
628 .buffer_unordered(concurrency)
629 .filter_map(|miner_opt| async move { miner_opt });
630
631 Box::pin(stream)
632 }
633
634 pub fn scan_stream_with_ip(
635 &self,
636 ) -> Pin<Box<impl Stream<Item = (IpAddr, Option<Box<dyn Miner>>)> + Send + use<>>> {
637 let concurrency = self
638 .concurrent
639 .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
640
641 let factory = Arc::new(self.clone());
642 let ips: Arc<[IpAddr]> = Arc::from(self.ips.as_slice());
643
644 let ip_count = ips.len();
645 let stream = stream::iter(0..ip_count)
646 .map(move |i| {
647 let factory = Arc::clone(&factory);
648 let ips = Arc::clone(&ips);
649 async move { (ips[i], factory.scan_miner(ips[i]).await.ok().flatten()) }
650 })
651 .buffer_unordered(concurrency);
652
653 Box::pin(stream)
654 }
655
656 pub async fn scan_by_octets(
658 self,
659 octet1: &str,
660 octet2: &str,
661 octet3: &str,
662 octet4: &str,
663 ) -> Result<Vec<Box<dyn Miner>>> {
664 self.with_octets(octet1, octet2, octet3, octet4)?
665 .scan()
666 .await
667 }
668
669 pub async fn scan_by_range(self, range_str: &str) -> Result<Vec<Box<dyn Miner>>> {
671 self.with_range(range_str)?.scan().await
672 }
673}
674
675fn parse_octet_range(range_str: &str) -> Result<Vec<u8>> {
677 if range_str.contains('-') {
678 let parts: Vec<&str> = range_str.split('-').collect();
679 if parts.len() != 2 {
680 return Err(anyhow::anyhow!("Invalid range format: {}", range_str));
681 }
682
683 let start: u8 = parts[0].parse()?;
684 let end: u8 = parts[1].parse()?;
685
686 if start > end {
687 return Err(anyhow::anyhow!(
688 "Invalid range: start > end in {}",
689 range_str
690 ));
691 }
692
693 Ok((start..=end).collect())
694 } else {
695 let value: u8 = range_str.parse()?;
697 Ok(vec![value])
698 }
699}
700
701fn generate_ips_from_ranges(
703 octet1_range: &[u8],
704 octet2_range: &[u8],
705 octet3_range: &[u8],
706 octet4_range: &[u8],
707) -> Vec<IpAddr> {
708 let mut ips = Vec::new();
709
710 for &o1 in octet1_range {
711 for &o2 in octet2_range {
712 for &o3 in octet3_range {
713 for &o4 in octet4_range {
714 ips.push(IpAddr::V4(Ipv4Addr::new(o1, o2, o3, o4)));
715 }
716 }
717 }
718 }
719
720 ips
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 #[test]
728 fn test_parse_type_from_socket_whatsminer_2024_09_30() {
729 const RAW_DATA: &str = r#"{"STATUS": [{"STATUS": "S", "Msg": "Device Details"}], "DEVDETAILS": [{"DEVDETAILS": 0, "Name": "SM", "ID": 0, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}, {"DEVDETAILS": 1, "Name": "SM", "ID": 1, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}, {"DEVDETAILS": 2, "Name": "SM", "ID": 2, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}], "id": 1}"#;
730 let parsed_data = serde_json::from_str(RAW_DATA).unwrap();
731 let result = parse_type_from_socket(parsed_data);
732 assert_eq!(
733 result,
734 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
735 )
736 }
737
738 #[test]
739 fn test_parse_type_from_web_whatsminer_2024_09_30() {
740 let mut headers = HeaderMap::new();
741 headers.insert("location", "https://example.com/".parse().unwrap());
742
743 let response_data = (String::from(""), headers, StatusCode::TEMPORARY_REDIRECT);
744
745 let result = parse_type_from_web(response_data);
746 assert_eq!(
747 result,
748 Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
749 )
750 }
751
752 #[test]
753 fn test_parse_octet_range() {
754 let result = parse_octet_range("10").unwrap();
756 assert_eq!(result, vec![10]);
757
758 let result = parse_octet_range("1-5").unwrap();
760 assert_eq!(result, vec![1, 2, 3, 4, 5]);
761
762 let result = parse_octet_range("200-255").unwrap();
764 assert_eq!(result, (200..=255).collect::<Vec<u8>>());
765
766 let result = parse_octet_range("200-100");
768 assert!(result.is_err());
769
770 let result = parse_octet_range("1-5-10");
772 assert!(result.is_err());
773
774 let result = parse_octet_range("300");
776 assert!(result.is_err());
777 }
778
779 #[test]
780 fn test_generate_ips_from_ranges() {
781 let octet1 = vec![192];
782 let octet2 = vec![168];
783 let octet3 = vec![1];
784 let octet4 = vec![1, 2];
785
786 let ips = generate_ips_from_ranges(&octet1, &octet2, &octet3, &octet4);
787
788 assert_eq!(ips.len(), 2);
789 assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
790 assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))));
791 }
792}