1#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
11#[cfg(feature = "clients")]
12pub mod cgis;
13#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
14#[cfg(feature = "clients")]
15pub mod discovery;
16pub mod errors;
17pub mod firmware;
18#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
19#[cfg(feature = "clients")]
20pub mod parameter;
21#[cfg_attr(docsrs, doc(cfg(any(feature = "clients", feature = "servers"))))]
24#[cfg(any(feature = "clients", feature = "servers"))]
25pub mod proto;
26
27use crate::errors::FSError;
28use configparser::ini::Ini;
29use errors::MionAPIError;
30use fnv::FnvHashMap;
31use std::{
32 fmt::{Display, Formatter, Result as FmtResult},
33 hash::BuildHasherDefault,
34 net::Ipv4Addr,
35 path::PathBuf,
36};
37
38const HARDWARE_ENV_NAME: &str = "CAFE_HARDWARE";
41const BRIDGE_NAME_KEY_PREFIX: &str = "BRIDGE_NAME_";
44const HOST_BRIDGES_SECTION: &str = "HOST_BRIDGES";
47const DEFAULT_BRIDGE_KEY: &str = "BRIDGE_DEFAULT_NAME";
49
50#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
54pub enum BridgeType {
55 Mion,
61 Toucan,
68}
69impl BridgeType {
70 #[must_use]
83 pub fn fetch_bridge_type() -> Option<Self> {
84 Self::hardware_type_to_value(std::env::var(HARDWARE_ENV_NAME).as_deref().ok())
85 }
86
87 fn hardware_type_to_value(hardware_type: Option<&str>) -> Option<Self> {
89 match hardware_type {
90 Some("ev") => Some(Self::Toucan),
91 Some("ev_x4") => Some(Self::Mion),
92 Some(val) => {
93 if val.chars().skip(6).collect::<String>() == *"mp" {
94 Some(Self::Mion)
95 } else if let Some(num) = val
96 .chars()
97 .nth(6)
98 .and_then(|character| char::to_digit(character, 10))
99 {
100 if num <= 2 {
101 Some(Self::Toucan)
102 } else {
103 Some(Self::Mion)
104 }
105 } else {
106 None
107 }
108 }
109 _ => None,
110 }
111 }
112}
113impl Default for BridgeType {
114 fn default() -> Self {
120 BridgeType::Mion
121 }
122}
123impl Display for BridgeType {
124 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
125 match *self {
126 Self::Mion => write!(fmt, "Mion"),
127 Self::Toucan => write!(fmt, "Toucan"),
128 }
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
138pub struct BridgeHostState {
139 configuration: Ini,
141 loaded_from_path: PathBuf,
143}
144impl BridgeHostState {
145 pub async fn load() -> Result<Self, FSError> {
157 let default_host_path =
158 Self::get_default_host_path().ok_or(FSError::CantFindHostEnvPath)?;
159 Self::load_explicit_path(default_host_path).await
160 }
161
162 pub async fn load_explicit_path(path: PathBuf) -> Result<Self, FSError> {
173 if path.exists() {
174 let as_bytes = tokio::fs::read(&path).await?;
175 let as_string = String::from_utf8(as_bytes)?;
176
177 let mut ini_contents = Ini::new_cs();
178 ini_contents
179 .read(as_string)
180 .map_err(|ini_error| FSError::InvalidDataNeedsToBeINI(format!("{ini_error:?}")))?;
181
182 Ok(Self {
183 configuration: ini_contents,
184 loaded_from_path: path,
185 })
186 } else {
187 Ok(Self {
188 configuration: Ini::new_cs(),
189 loaded_from_path: path,
190 })
191 }
192 }
193
194 #[must_use]
202 pub fn get_default_bridge(&self) -> Option<(String, Option<Ipv4Addr>)> {
203 if let Some(host_key) = self
204 .configuration
205 .get(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY)
206 {
207 let host_name = host_key
208 .as_str()
209 .trim_start_matches(BRIDGE_NAME_KEY_PREFIX)
210 .to_owned();
211
212 Some((
213 host_name,
214 self.configuration
215 .get(HOST_BRIDGES_SECTION, &host_key)
216 .and_then(|value| value.parse::<Ipv4Addr>().ok()),
217 ))
218 } else {
219 None
220 }
221 }
222
223 #[must_use]
227 pub fn get_bridge(&self, bridge_name: &str) -> Option<(Option<Ipv4Addr>, bool)> {
228 let default_key = self
229 .configuration
230 .get(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
231 let key = format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}");
232 let is_default = default_key.as_deref() == Some(key.as_str());
233
234 self.configuration
235 .get(HOST_BRIDGES_SECTION, &key)
236 .map(|value| (value.parse::<Ipv4Addr>().ok(), is_default))
237 }
238
239 #[must_use]
245 pub fn list_bridges(&self) -> FnvHashMap<String, (Option<Ipv4Addr>, bool)> {
246 let ini_data = self.configuration.get_map_ref();
247 let Some(host_bridge_section) = ini_data.get(HOST_BRIDGES_SECTION) else {
248 return FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default());
249 };
250
251 let default_key = if let Some(Some(value)) = host_bridge_section.get(DEFAULT_BRIDGE_KEY) {
252 Some(value)
253 } else {
254 None
255 };
256
257 let mut bridges = FnvHashMap::default();
258 for (key, value) in host_bridge_section {
259 if key.as_str() == DEFAULT_BRIDGE_KEY
260 || !key.as_str().starts_with(BRIDGE_NAME_KEY_PREFIX)
261 {
262 continue;
263 }
264
265 let is_default = Some(key) == default_key;
266 bridges.insert(
267 key.trim_start_matches(BRIDGE_NAME_KEY_PREFIX).to_owned(),
268 (
269 value.as_ref().and_then(|val| val.parse::<Ipv4Addr>().ok()),
270 is_default,
271 ),
272 );
273 }
274 bridges
275 }
276
277 pub fn upsert_bridge(
289 &mut self,
290 bridge_name: &str,
291 bridge_ip: Ipv4Addr,
292 ) -> Result<(), MionAPIError> {
293 if !bridge_name.is_ascii() {
294 return Err(MionAPIError::DeviceNameMustBeAscii);
295 }
296 if bridge_name.is_empty() {
297 return Err(MionAPIError::DeviceNameCannotBeEmpty);
298 }
299 if bridge_name.len() > 255 {
300 return Err(MionAPIError::DeviceNameTooLong(bridge_name.len()));
301 }
302
303 self.configuration.set(
304 HOST_BRIDGES_SECTION,
305 &format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}"),
306 Some(format!("{bridge_ip}")),
307 );
308 Ok(())
309 }
310
311 pub fn remove_bridge(&mut self, bridge_name: &str) {
317 self.configuration.remove_key(
318 HOST_BRIDGES_SECTION,
319 &format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}"),
320 );
321 }
322
323 pub fn remove_default_bridge(&mut self) {
329 self.configuration
330 .remove_key(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
331 }
332
333 pub fn set_default_bridge(&mut self, bridge_name: &str) -> Result<(), MionAPIError> {
342 if !bridge_name.is_ascii() {
343 return Err(MionAPIError::DeviceNameMustBeAscii);
344 }
345 if bridge_name.is_empty() {
346 return Err(MionAPIError::DeviceNameCannotBeEmpty);
347 }
348 if bridge_name.len() > 255 {
349 return Err(MionAPIError::DeviceNameTooLong(bridge_name.len()));
350 }
351
352 let bridge_key = format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}");
353 if self
354 .configuration
355 .get(HOST_BRIDGES_SECTION, &bridge_key)
356 .is_none()
357 {
358 return Err(MionAPIError::DefaultDeviceMustExist);
359 }
360
361 self.configuration
362 .set(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY, Some(bridge_key));
363
364 Ok(())
365 }
366
367 pub async fn write_to_disk(&self) -> Result<(), FSError> {
377 let mut serialized_configuration = self.configuration.writes();
378 if !serialized_configuration.contains("\r\n") {
380 serialized_configuration = serialized_configuration.replace('\n', "\r\n");
381 }
382
383 let parent_dir = {
384 let mut path = self.loaded_from_path.clone();
385 path.pop();
386 path
387 };
388 tokio::fs::create_dir_all(&parent_dir).await?;
389
390 tokio::fs::write(
391 &self.loaded_from_path,
392 serialized_configuration.into_bytes(),
393 )
394 .await?;
395
396 Ok(())
397 }
398
399 #[must_use]
401 pub fn get_path(&self) -> &PathBuf {
402 &self.loaded_from_path
403 }
404
405 #[allow(
413 unreachable_code,
418 )]
419 #[must_use]
420 pub fn get_default_host_path() -> Option<PathBuf> {
421 #[cfg(target_os = "windows")]
422 {
423 use std::env::var as env_var;
424 if let Ok(appdata_dir) = env_var("APPDATA") {
425 let mut path = PathBuf::from(appdata_dir);
426 path.push("bridge_env.ini");
427 return Some(path);
428 }
429
430 return None;
431 }
432
433 #[cfg(target_os = "macos")]
434 {
435 use std::env::var as env_var;
436 if let Ok(home_dir) = env_var("HOME") {
437 let mut path = PathBuf::from(home_dir);
438 path.push("Library");
439 path.push("Application Support");
440 path.push("bridge_env.ini");
441 return Some(path);
442 }
443
444 return None;
445 }
446
447 #[cfg(any(
448 target_os = "linux",
449 target_os = "freebsd",
450 target_os = "openbsd",
451 target_os = "netbsd"
452 ))]
453 {
454 use std::env::var as env_var;
455 if let Ok(xdg_config_dir) = env_var("XDG_CONFIG_HOME") {
456 let mut path = PathBuf::from(xdg_config_dir);
457 path.push("bridge_env.ini");
458 return Some(path);
459 } else if let Ok(home_dir) = env_var("HOME") {
460 let mut path = PathBuf::from(home_dir);
461 path.push(".config");
462 path.push("bridge_env.ini");
463 return Some(path);
464 }
465
466 return None;
467 }
468
469 None
470 }
471}
472
473#[cfg(test)]
474mod unit_tests {
475 use super::*;
476
477 #[test]
478 pub fn bridge_type_parsing() {
479 assert_eq!(
481 BridgeType::hardware_type_to_value(None),
482 None,
483 "Empty hardware type did not map to a null bridge type?"
484 );
485
486 assert_eq!(
488 BridgeType::hardware_type_to_value(Some("ev")),
489 Some(BridgeType::Toucan),
490 "Hardware type `ev` was not a `Toucan` bridge!"
491 );
492 assert_eq!(
493 BridgeType::hardware_type_to_value(Some("ev_x4")),
494 Some(BridgeType::Mion),
495 "Hardware type `ev_x4` was not a `Mion` bridge!",
496 );
497
498 assert_eq!(
500 BridgeType::hardware_type_to_value(Some("catdevmp")),
501 Some(BridgeType::Mion),
502 "Hardware type `catdevmp` was not a `Mion` bridge!"
503 );
504 assert_eq!(
505 BridgeType::hardware_type_to_value(Some("catdev200")),
506 Some(BridgeType::Toucan),
507 "Hardware type `catdev200` was not a `Toucan` bridge!"
508 );
509
510 assert_eq!(
512 BridgeType::hardware_type_to_value(Some("catdevdevmp")),
513 None,
514 "Invalid hardware type did not get mapped to an empty bridge!",
515 );
516 }
517
518 #[test]
519 pub fn bridge_type_default_is_mion() {
520 assert_eq!(
521 BridgeType::default(),
522 BridgeType::Mion,
523 "Default bridge type was not mion!"
524 );
525 }
526
527 #[test]
528 pub fn can_find_host_env() {
529 assert!(
530 BridgeHostState::get_default_host_path().is_some(),
531 "Failed to find the host state path for your particular OS, please file an issue!",
532 );
533 }
534
535 #[tokio::test]
536 pub async fn can_load_ini_files() {
537 let mut test_data_dir = PathBuf::from(
541 std::env::var("CARGO_MANIFEST_DIR")
542 .expect("Failed to read `CARGO_MANIFEST_DIR` to locate test files!"),
543 );
544 test_data_dir.push("src");
545 test_data_dir.push("mion");
546 test_data_dir.push("test-data");
547
548 {
549 let mut real_config_path = test_data_dir.clone();
550 real_config_path.push("real-bridge-env.ini");
551 let real_config = BridgeHostState::load_explicit_path(real_config_path)
552 .await
553 .expect("Failed to load a real `bridge_env.ini`!");
554
555 let all_bridges = real_config.list_bridges();
556 assert_eq!(
557 all_bridges.len(),
558 1,
559 "Didn't find the single bridge that should've been in our real life `bridge_env.ini`!",
560 );
561 assert_eq!(
562 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
563 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true)),
564 "Failed to find a default bridge returned through listed bridges.",
565 );
566 assert_eq!(
567 real_config.get_default_bridge(),
568 Some((
569 "00-25-5C-BA-5A-00".to_owned(),
570 Some(Ipv4Addr::new(192, 168, 7, 40)),
571 )),
572 "Failed to get default bridge"
573 );
574 assert_eq!(
575 real_config.get_bridge("00-25-5C-BA-5A-00"),
576 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true))
577 );
578 }
579
580 {
581 let mut real_config_path = test_data_dir.clone();
582 real_config_path.push("fake-valid-bridge-env.ini");
583 let real_config = BridgeHostState::load_explicit_path(real_config_path)
584 .await
585 .expect("Failed to load a real `bridge_env.ini`!");
586
587 assert_eq!(
588 real_config.get_default_bridge(),
589 Some((
590 "00-25-5C-BA-5A-00".to_owned(),
591 Some(Ipv4Addr::new(192, 168, 7, 40))
592 )),
593 );
594 let all_bridges = real_config.list_bridges();
595 assert_eq!(
596 all_bridges.len(),
597 3,
598 "Didn't find the three bridge that should've been in our fake but valid `bridge_env.ini`!",
599 );
600 assert_eq!(
601 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
602 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true)),
603 "Failed to find a default bridge returned through listed bridges in fake but valid bridge env.",
604 );
605 assert_eq!(
606 all_bridges.get("00-25-5C-BA-5A-01").cloned(),
607 Some((Some(Ipv4Addr::new(192, 168, 7, 41)), false)),
608 "Failed to find a non-default bridge returned through listed bridges in fake but valid bridge env.",
609 );
610 assert_eq!(
611 all_bridges.get("00-25-5C-BA-5A-02").cloned(),
612 Some((None, false)),
613 "Failed to find a non-default bridge returned through listed bridges in fake but valid bridge env.",
614 );
615 }
616
617 {
618 let mut real_config_path = test_data_dir.clone();
619 real_config_path.push("default-but-no-value.ini");
620 let real_config = BridgeHostState::load_explicit_path(real_config_path)
621 .await
622 .expect("Failed to load a real `bridge_env.ini`!");
623
624 assert_eq!(
625 real_config.get_default_bridge(),
626 Some(("00-25-5C-BA-5A-01".to_owned(), None)),
627 );
628 let all_bridges = real_config.list_bridges();
629 assert_eq!(
630 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
631 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), false)),
632 "Failed to find a default bridge returned through listed bridges in bridge env with default but no value.",
633 );
634 }
635
636 {
637 let mut real_config_path = test_data_dir.clone();
638 real_config_path.push("default-but-invalid-value.ini");
639 let real_config = BridgeHostState::load_explicit_path(real_config_path)
640 .await
641 .expect("Failed to load a real `bridge_env.ini`!");
642
643 assert_eq!(
644 real_config.get_default_bridge(),
645 Some(("00-25-5C-BA-5A-00".to_owned(), None)),
646 );
647 let all_bridges = real_config.list_bridges();
648 assert_eq!(
649 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
650 Some((None, true)),
651 );
652 }
653
654 {
655 let mut real_config_path = test_data_dir.clone();
656 real_config_path.push("invalid-ini-file.ini");
657
658 assert!(matches!(
659 BridgeHostState::load_explicit_path(real_config_path).await,
660 Err(FSError::InvalidDataNeedsToBeINI(_)),
661 ));
662 }
663 }
664
665 #[tokio::test]
666 pub async fn can_set_and_write_to_file() {
667 use tempfile::tempdir;
668 use tokio::fs::File;
669
670 let temporary_directory =
671 tempdir().expect("Failed to create temporary directory for tests!");
672 let mut path = PathBuf::from(temporary_directory.path());
673 path.push("bridge_env_custom_made.ini");
674 {
675 File::create(&path)
676 .await
677 .expect("Failed to create test file to write too!");
678 }
679 let mut host_env = BridgeHostState::load_explicit_path(path.clone())
680 .await
681 .expect("Failed to load empty file to write too!");
682
683 assert_eq!(
684 host_env.set_default_bridge("00-25-5C-BA-5A-00"),
685 Err(MionAPIError::DefaultDeviceMustExist),
686 );
687 assert_eq!(
688 host_env.upsert_bridge("", Ipv4Addr::new(192, 168, 1, 1)),
689 Err(MionAPIError::DeviceNameCannotBeEmpty),
690 );
691 assert_eq!(
692 host_env.upsert_bridge("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Ipv4Addr::new(192, 168, 1, 1)),
693 Err(MionAPIError::DeviceNameTooLong(256)),
694 );
695 assert_eq!(
696 host_env.upsert_bridge("ð’€€", Ipv4Addr::new(192, 168, 1, 1)),
697 Err(MionAPIError::DeviceNameMustBeAscii),
698 );
699
700 assert!(
701 host_env
702 .upsert_bridge(" with spaces ", Ipv4Addr::new(192, 168, 1, 1))
703 .is_ok()
704 );
705 assert!(host_env.set_default_bridge(" with spaces ").is_ok());
706 assert!(
707 host_env
708 .upsert_bridge("00-25-5C-BA-5A-00", Ipv4Addr::new(192, 168, 1, 2))
709 .is_ok()
710 );
711 assert!(
712 host_env
713 .upsert_bridge(" with spaces ", Ipv4Addr::new(192, 168, 1, 3))
714 .is_ok()
715 );
716 assert!(host_env.set_default_bridge("00-25-5C-BA-5A-00").is_ok());
717 assert!(host_env.write_to_disk().await.is_ok());
718
719 let read_data = String::from_utf8(
720 tokio::fs::read(path)
721 .await
722 .expect("Failed to read written data!"),
723 )
724 .expect("Written INI file wasn't UTF8?");
725 let choices = [
727 "[HOST_BRIDGES]\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\n".to_owned(),
728 "[HOST_BRIDGES]\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\n".to_owned(),
729 "[HOST_BRIDGES]\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\n".to_owned(),
730 "[HOST_BRIDGES]\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\n".to_owned(),
731 "[HOST_BRIDGES]\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\n".to_owned(),
732 "[HOST_BRIDGES]\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\n".to_owned(),
733 ];
734
735 if !choices.contains(&read_data) {
736 panic!("Unexpected host bridges ini file:\n{read_data}");
737 }
738 }
739}