1pub mod cgis;
11pub mod discovery;
12pub mod errors;
13pub mod firmware;
14pub mod parameter;
15pub mod proto;
16
17use crate::errors::FSError;
18use configparser::ini::Ini;
19use errors::MIONAPIError;
20use fnv::FnvHashMap;
21use std::{
22 fmt::{Display, Formatter, Result as FmtResult},
23 hash::BuildHasherDefault,
24 net::Ipv4Addr,
25 path::PathBuf,
26};
27
28const HARDWARE_ENV_NAME: &str = "CAFE_HARDWARE";
31const BRIDGE_NAME_KEY_PREFIX: &str = "BRIDGE_NAME_";
34const HOST_BRIDGES_SECTION: &str = "HOST_BRIDGES";
37const DEFAULT_BRIDGE_KEY: &str = "BRIDGE_DEFAULT_NAME";
39
40#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
44pub enum BridgeType {
45 Mion,
51 Toucan,
58}
59impl BridgeType {
60 #[must_use]
73 pub fn fetch_bridge_type() -> Option<Self> {
74 Self::hardware_type_to_value(std::env::var(HARDWARE_ENV_NAME).as_deref().ok())
75 }
76
77 fn hardware_type_to_value(hardware_type: Option<&str>) -> Option<Self> {
79 match hardware_type {
80 Some("ev") => Some(Self::Toucan),
81 Some("ev_x4") => Some(Self::Mion),
82 Some(val) => {
83 if val.chars().skip(6).collect::<String>() == *"mp" {
84 Some(Self::Mion)
85 } else if let Some(num) = val
86 .chars()
87 .nth(6)
88 .and_then(|character| char::to_digit(character, 10))
89 {
90 if num <= 2 {
91 Some(Self::Toucan)
92 } else {
93 Some(Self::Mion)
94 }
95 } else {
96 None
97 }
98 }
99 _ => None,
100 }
101 }
102}
103impl Default for BridgeType {
104 fn default() -> Self {
110 BridgeType::Mion
111 }
112}
113impl Display for BridgeType {
114 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
115 match *self {
116 Self::Mion => write!(fmt, "Mion"),
117 Self::Toucan => write!(fmt, "Toucan"),
118 }
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct BridgeHostState {
129 configuration: Ini,
131 loaded_from_path: PathBuf,
133}
134impl BridgeHostState {
135 pub async fn load() -> Result<Self, FSError> {
147 let default_host_path =
148 Self::get_default_host_path().ok_or(FSError::CantFindHostEnvPath)?;
149 Self::load_explicit_path(default_host_path).await
150 }
151
152 pub async fn load_explicit_path(path: PathBuf) -> Result<Self, FSError> {
163 if path.exists() {
164 let as_bytes = tokio::fs::read(&path).await?;
165 let as_string = String::from_utf8(as_bytes)?;
166
167 let mut ini_contents = Ini::new_cs();
168 ini_contents
169 .read(as_string)
170 .map_err(|ini_error| FSError::InvalidDataNeedsToBeINI(format!("{ini_error:?}")))?;
171
172 Ok(Self {
173 configuration: ini_contents,
174 loaded_from_path: path,
175 })
176 } else {
177 Ok(Self {
178 configuration: Ini::new_cs(),
179 loaded_from_path: path,
180 })
181 }
182 }
183
184 #[must_use]
192 pub fn get_default_bridge(&self) -> Option<(String, Option<Ipv4Addr>)> {
193 if let Some(host_key) = self
194 .configuration
195 .get(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY)
196 {
197 let host_name = host_key
198 .as_str()
199 .trim_start_matches(BRIDGE_NAME_KEY_PREFIX)
200 .to_owned();
201
202 Some((
203 host_name,
204 self.configuration
205 .get(HOST_BRIDGES_SECTION, &host_key)
206 .and_then(|value| value.parse::<Ipv4Addr>().ok()),
207 ))
208 } else {
209 None
210 }
211 }
212
213 #[must_use]
217 pub fn get_bridge(&self, bridge_name: &str) -> Option<(Option<Ipv4Addr>, bool)> {
218 let default_key = self
219 .configuration
220 .get(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
221 let key = format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}");
222 let is_default = default_key.as_deref() == Some(key.as_str());
223
224 self.configuration
225 .get(HOST_BRIDGES_SECTION, &key)
226 .map(|value| (value.parse::<Ipv4Addr>().ok(), is_default))
227 }
228
229 #[must_use]
235 pub fn list_bridges(&self) -> FnvHashMap<String, (Option<Ipv4Addr>, bool)> {
236 let ini_data = self.configuration.get_map_ref();
237 let Some(host_bridge_section) = ini_data.get(HOST_BRIDGES_SECTION) else {
238 return FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default());
239 };
240
241 let default_key = if let Some(Some(value)) = host_bridge_section.get(DEFAULT_BRIDGE_KEY) {
242 Some(value)
243 } else {
244 None
245 };
246
247 let mut bridges = FnvHashMap::default();
248 for (key, value) in host_bridge_section {
249 if key.as_str() == DEFAULT_BRIDGE_KEY
250 || !key.as_str().starts_with(BRIDGE_NAME_KEY_PREFIX)
251 {
252 continue;
253 }
254
255 let is_default = Some(key) == default_key;
256 bridges.insert(
257 key.trim_start_matches(BRIDGE_NAME_KEY_PREFIX).to_owned(),
258 (
259 value.as_ref().and_then(|val| val.parse::<Ipv4Addr>().ok()),
260 is_default,
261 ),
262 );
263 }
264 bridges
265 }
266
267 pub fn upsert_bridge(
279 &mut self,
280 bridge_name: &str,
281 bridge_ip: Ipv4Addr,
282 ) -> Result<(), MIONAPIError> {
283 if !bridge_name.is_ascii() {
284 return Err(MIONAPIError::DeviceNameMustBeAscii);
285 }
286 if bridge_name.is_empty() {
287 return Err(MIONAPIError::DeviceNameCannotBeEmpty);
288 }
289 if bridge_name.len() > 255 {
290 return Err(MIONAPIError::DeviceNameTooLong(bridge_name.len()));
291 }
292
293 self.configuration.set(
294 HOST_BRIDGES_SECTION,
295 &format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}"),
296 Some(format!("{bridge_ip}")),
297 );
298 Ok(())
299 }
300
301 pub fn remove_bridge(&mut self, bridge_name: &str) {
307 self.configuration.remove_key(
308 HOST_BRIDGES_SECTION,
309 &format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}"),
310 );
311 }
312
313 pub fn remove_default_bridge(&mut self) {
319 self.configuration
320 .remove_key(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
321 }
322
323 pub fn set_default_bridge(&mut self, bridge_name: &str) -> Result<(), MIONAPIError> {
332 if !bridge_name.is_ascii() {
333 return Err(MIONAPIError::DeviceNameMustBeAscii);
334 }
335 if bridge_name.is_empty() {
336 return Err(MIONAPIError::DeviceNameCannotBeEmpty);
337 }
338 if bridge_name.len() > 255 {
339 return Err(MIONAPIError::DeviceNameTooLong(bridge_name.len()));
340 }
341
342 let bridge_key = format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}");
343 if self
344 .configuration
345 .get(HOST_BRIDGES_SECTION, &bridge_key)
346 .is_none()
347 {
348 return Err(MIONAPIError::DefaultDeviceMustExist);
349 }
350
351 self.configuration
352 .set(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY, Some(bridge_key));
353
354 Ok(())
355 }
356
357 pub async fn write_to_disk(&self) -> Result<(), FSError> {
367 let mut serialized_configuration = self.configuration.writes();
368 if !serialized_configuration.contains("\r\n") {
370 serialized_configuration = serialized_configuration.replace('\n', "\r\n");
371 }
372
373 let parent_dir = {
374 let mut path = self.loaded_from_path.clone();
375 path.pop();
376 path
377 };
378 tokio::fs::create_dir_all(&parent_dir).await?;
379
380 tokio::fs::write(
381 &self.loaded_from_path,
382 serialized_configuration.into_bytes(),
383 )
384 .await?;
385
386 Ok(())
387 }
388
389 #[must_use]
391 pub fn get_path(&self) -> &PathBuf {
392 &self.loaded_from_path
393 }
394
395 #[allow(
403 unreachable_code,
408 )]
409 #[must_use]
410 pub fn get_default_host_path() -> Option<PathBuf> {
411 #[cfg(target_os = "windows")]
412 {
413 use std::env::var as env_var;
414 if let Ok(appdata_dir) = env_var("APPDATA") {
415 let mut path = PathBuf::from(appdata_dir);
416 path.push("bridge_env.ini");
417 return Some(path);
418 }
419
420 return None;
421 }
422
423 #[cfg(target_os = "macos")]
424 {
425 use std::env::var as env_var;
426 if let Ok(home_dir) = env_var("HOME") {
427 let mut path = PathBuf::from(home_dir);
428 path.push("Library");
429 path.push("Application Support");
430 path.push("bridge_env.ini");
431 return Some(path);
432 }
433
434 return None;
435 }
436
437 #[cfg(any(
438 target_os = "linux",
439 target_os = "freebsd",
440 target_os = "openbsd",
441 target_os = "netbsd"
442 ))]
443 {
444 use std::env::var as env_var;
445 if let Ok(xdg_config_dir) = env_var("XDG_CONFIG_HOME") {
446 let mut path = PathBuf::from(xdg_config_dir);
447 path.push("bridge_env.ini");
448 return Some(path);
449 } else if let Ok(home_dir) = env_var("HOME") {
450 let mut path = PathBuf::from(home_dir);
451 path.push(".config");
452 path.push("bridge_env.ini");
453 return Some(path);
454 }
455
456 return None;
457 }
458
459 None
460 }
461}
462
463#[cfg(test)]
464mod unit_tests {
465 use super::*;
466
467 #[test]
468 pub fn bridge_type_parsing() {
469 assert_eq!(
471 BridgeType::hardware_type_to_value(None),
472 None,
473 "Empty hardware type did not map to a null bridge type?"
474 );
475
476 assert_eq!(
478 BridgeType::hardware_type_to_value(Some("ev")),
479 Some(BridgeType::Toucan),
480 "Hardware type `ev` was not a `Toucan` bridge!"
481 );
482 assert_eq!(
483 BridgeType::hardware_type_to_value(Some("ev_x4")),
484 Some(BridgeType::Mion),
485 "Hardware type `ev_x4` was not a `Mion` bridge!",
486 );
487
488 assert_eq!(
490 BridgeType::hardware_type_to_value(Some("catdevmp")),
491 Some(BridgeType::Mion),
492 "Hardware type `catdevmp` was not a `Mion` bridge!"
493 );
494 assert_eq!(
495 BridgeType::hardware_type_to_value(Some("catdev200")),
496 Some(BridgeType::Toucan),
497 "Hardware type `catdev200` was not a `Toucan` bridge!"
498 );
499
500 assert_eq!(
502 BridgeType::hardware_type_to_value(Some("catdevdevmp")),
503 None,
504 "Invalid hardware type did not get mapped to an empty bridge!",
505 );
506 }
507
508 #[test]
509 pub fn bridge_type_default_is_mion() {
510 assert_eq!(
511 BridgeType::default(),
512 BridgeType::Mion,
513 "Default bridge type was not mion!"
514 );
515 }
516
517 #[test]
518 pub fn can_find_host_env() {
519 assert!(
520 BridgeHostState::get_default_host_path().is_some(),
521 "Failed to find the host state path for your particular OS, please file an issue!",
522 );
523 }
524
525 #[tokio::test]
526 pub async fn can_load_ini_files() {
527 let mut test_data_dir = PathBuf::from(
531 std::env::var("CARGO_MANIFEST_DIR")
532 .expect("Failed to read `CARGO_MANIFEST_DIR` to locate test files!"),
533 );
534 test_data_dir.push("src");
535 test_data_dir.push("mion");
536 test_data_dir.push("test-data");
537
538 {
539 let mut real_config_path = test_data_dir.clone();
540 real_config_path.push("real-bridge-env.ini");
541 let real_config = BridgeHostState::load_explicit_path(real_config_path)
542 .await
543 .expect("Failed to load a real `bridge_env.ini`!");
544
545 let all_bridges = real_config.list_bridges();
546 assert_eq!(
547 all_bridges.len(),
548 1,
549 "Didn't find the single bridge that should've been in our real life `bridge_env.ini`!",
550 );
551 assert_eq!(
552 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
553 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true)),
554 "Failed to find a default bridge returned through listed bridges.",
555 );
556 assert_eq!(
557 real_config.get_default_bridge(),
558 Some((
559 "00-25-5C-BA-5A-00".to_owned(),
560 Some(Ipv4Addr::new(192, 168, 7, 40)),
561 )),
562 "Failed to get default bridge"
563 );
564 assert_eq!(
565 real_config.get_bridge("00-25-5C-BA-5A-00"),
566 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true))
567 );
568 }
569
570 {
571 let mut real_config_path = test_data_dir.clone();
572 real_config_path.push("fake-valid-bridge-env.ini");
573 let real_config = BridgeHostState::load_explicit_path(real_config_path)
574 .await
575 .expect("Failed to load a real `bridge_env.ini`!");
576
577 assert_eq!(
578 real_config.get_default_bridge(),
579 Some((
580 "00-25-5C-BA-5A-00".to_owned(),
581 Some(Ipv4Addr::new(192, 168, 7, 40))
582 )),
583 );
584 let all_bridges = real_config.list_bridges();
585 assert_eq!(
586 all_bridges.len(),
587 3,
588 "Didn't find the three bridge that should've been in our fake but valid `bridge_env.ini`!",
589 );
590 assert_eq!(
591 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
592 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true)),
593 "Failed to find a default bridge returned through listed bridges in fake but valid bridge env.",
594 );
595 assert_eq!(
596 all_bridges.get("00-25-5C-BA-5A-01").cloned(),
597 Some((Some(Ipv4Addr::new(192, 168, 7, 41)), false)),
598 "Failed to find a non-default bridge returned through listed bridges in fake but valid bridge env.",
599 );
600 assert_eq!(
601 all_bridges.get("00-25-5C-BA-5A-02").cloned(),
602 Some((None, false)),
603 "Failed to find a non-default bridge returned through listed bridges in fake but valid bridge env.",
604 );
605 }
606
607 {
608 let mut real_config_path = test_data_dir.clone();
609 real_config_path.push("default-but-no-value.ini");
610 let real_config = BridgeHostState::load_explicit_path(real_config_path)
611 .await
612 .expect("Failed to load a real `bridge_env.ini`!");
613
614 assert_eq!(
615 real_config.get_default_bridge(),
616 Some(("00-25-5C-BA-5A-01".to_owned(), None)),
617 );
618 let all_bridges = real_config.list_bridges();
619 assert_eq!(
620 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
621 Some((Some(Ipv4Addr::new(192, 168, 7, 40)), false)),
622 "Failed to find a default bridge returned through listed bridges in bridge env with default but no value.",
623 );
624 }
625
626 {
627 let mut real_config_path = test_data_dir.clone();
628 real_config_path.push("default-but-invalid-value.ini");
629 let real_config = BridgeHostState::load_explicit_path(real_config_path)
630 .await
631 .expect("Failed to load a real `bridge_env.ini`!");
632
633 assert_eq!(
634 real_config.get_default_bridge(),
635 Some(("00-25-5C-BA-5A-00".to_owned(), None)),
636 );
637 let all_bridges = real_config.list_bridges();
638 assert_eq!(
639 all_bridges.get("00-25-5C-BA-5A-00").cloned(),
640 Some((None, true)),
641 );
642 }
643
644 {
645 let mut real_config_path = test_data_dir.clone();
646 real_config_path.push("invalid-ini-file.ini");
647
648 assert!(matches!(
649 BridgeHostState::load_explicit_path(real_config_path).await,
650 Err(FSError::InvalidDataNeedsToBeINI(_)),
651 ));
652 }
653 }
654
655 #[tokio::test]
656 pub async fn can_set_and_write_to_file() {
657 use tempfile::tempdir;
658 use tokio::fs::File;
659
660 let temporary_directory =
661 tempdir().expect("Failed to create temporary directory for tests!");
662 let mut path = PathBuf::from(temporary_directory.path());
663 path.push("bridge_env_custom_made.ini");
664 {
665 File::create(&path)
666 .await
667 .expect("Failed to create test file to write too!");
668 }
669 let mut host_env = BridgeHostState::load_explicit_path(path.clone())
670 .await
671 .expect("Failed to load empty file to write too!");
672
673 assert_eq!(
674 host_env.set_default_bridge("00-25-5C-BA-5A-00"),
675 Err(MIONAPIError::DefaultDeviceMustExist),
676 );
677 assert_eq!(
678 host_env.upsert_bridge("", Ipv4Addr::new(192, 168, 1, 1)),
679 Err(MIONAPIError::DeviceNameCannotBeEmpty),
680 );
681 assert_eq!(
682 host_env.upsert_bridge("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Ipv4Addr::new(192, 168, 1, 1)),
683 Err(MIONAPIError::DeviceNameTooLong(256)),
684 );
685 assert_eq!(
686 host_env.upsert_bridge("ð’€€", Ipv4Addr::new(192, 168, 1, 1)),
687 Err(MIONAPIError::DeviceNameMustBeAscii),
688 );
689
690 assert!(host_env
691 .upsert_bridge(" with spaces ", Ipv4Addr::new(192, 168, 1, 1))
692 .is_ok());
693 assert!(host_env.set_default_bridge(" with spaces ").is_ok());
694 assert!(host_env
695 .upsert_bridge("00-25-5C-BA-5A-00", Ipv4Addr::new(192, 168, 1, 2))
696 .is_ok());
697 assert!(host_env
698 .upsert_bridge(" with spaces ", Ipv4Addr::new(192, 168, 1, 3))
699 .is_ok());
700 assert!(host_env.set_default_bridge("00-25-5C-BA-5A-00").is_ok());
701 assert!(host_env.write_to_disk().await.is_ok());
702
703 let read_data = String::from_utf8(
704 tokio::fs::read(path)
705 .await
706 .expect("Failed to read written data!"),
707 )
708 .expect("Written INI file wasn't UTF8?");
709 let choices = [
711 "[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(),
712 "[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(),
713 "[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(),
714 "[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(),
715 "[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(),
716 "[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(),
717 ];
718
719 if !choices.contains(&read_data) {
720 panic!("Unexpected host bridges ini file:\n{read_data}");
721 }
722 }
723}