cat_dev/mion/
mod.rs

1//! MION is the Sata Device present on the CAT-DEV Bridge.
2//!
3//! In general MION's are pretty much the "host bridge" most of the host bridge
4//! software _actually_ talks too. `hostdisplayversion` calls it the
5//! "Bridge Type", but I don't think we  know of any other bridge types.
6//!
7//! In general if you're trying to look for things relating to the bridge as a
8//! whole, you're _probably_ really actually talking to the MION.
9
10pub 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
28/// The environment variable name to fetch what version of [`CAFE_HARDWARE`]
29/// we're using.
30const HARDWARE_ENV_NAME: &str = "CAFE_HARDWARE";
31/// The prefix prepended to the bridge name keys in the host env file to
32/// prevent collisions.
33const BRIDGE_NAME_KEY_PREFIX: &str = "BRIDGE_NAME_";
34/// The section name in the ini file we store a list of the host bridges are
35/// stored in.
36const HOST_BRIDGES_SECTION: &str = "HOST_BRIDGES";
37/// The key that contains that stores which bridge is marked as the default.
38const DEFAULT_BRIDGE_KEY: &str = "BRIDGE_DEFAULT_NAME";
39
40/// As far as I can derive from the sources available that we can cleanly read
41/// (e.g. shell scripts) there are two types of CAT-DEV units. This enum
42/// describes those.
43#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
44pub enum BridgeType {
45	/// "MION" is the 'standard' cat-dev unit that most people probably have (and
46	/// is in fact the only one the developers have).
47	///
48	/// MIONs are also refered to as "v3", and "v4". If you have a tan colored
49	/// box you almost assuredly have a MION.
50	Mion,
51	/// A toucan is the other type of cat-dev unit referenced in some of the
52	/// shell scripts.
53	///
54	/// We don't have a ton of documentation on this particular bridge type
55	/// mainly because we don't actually own any of these bridge types. They
56	/// generally be refered to as "v2" bridges.
57	Toucan,
58}
59impl BridgeType {
60	/// Attempt to get the bridge type from the environment.
61	///
62	/// This will only return `Some()` when the environment variable
63	/// `CAFE_HARDWARE` is set to a proper value. If it's not set to a value
64	/// then we will always return `None`.
65	///
66	/// A proper way to always get a bridge type would be doing something similar
67	/// to what the scripts do which is fallback to a default:
68	///
69	/// ```rust
70	/// let ty = cat_dev::mion::BridgeType::fetch_bridge_type().unwrap_or_default();
71	/// ```
72	#[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	/// Convert a known hardware type to a potential Bridge Type.
78	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	/// Shell scripts default to using MION if all things are the same.
105	///
106	/// In general you probably want to use [`BridgeType::fetch_bridge_type`],
107	/// and should only use this default when using something like
108	/// `unwrap_or_default` in case the bridge type isn't in the environment.
109	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/// The state of bridges that are actively known on this host.
123///
124/// This is effectively just a list of bridges along with one of those being
125/// set as the "default" bridge. A lot of this configuration is stored within
126/// a system level directory, see [`BridgeHostState::get_default_host_path`].
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct BridgeHostState {
129	/// The fully existing configuration as we know it.
130	configuration: Ini,
131	/// The path we originally loaded ourselves from.
132	loaded_from_path: PathBuf,
133}
134impl BridgeHostState {
135	/// Attempt to load the bridge host state from the filesystem.
136	///
137	/// This is commonly referred to as `bridge_env.ini`, stored normally in
138	/// Windows under the `%APPDATA%\bridge_env.ini` file. This is where tools
139	/// like `getbridge`/`hostdisplayversion`/`setbridge` store which bridges
140	/// you actually use, and which one you've set as the default bridge.
141	///
142	/// ## Errors
143	///
144	/// - If we cannot get the default host path for your OS.
145	/// - Any error case from [`BridgeHostState::load_explicit_path`].
146	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	/// Attempt to load the bridge host state file from the filesystem.
153	///
154	/// This is commonly referred to as `bridge_env.ini`, and is a small
155	/// Windows (so UTF8) ini file, separated by newlines being `\r\n`.
156	///
157	/// ## Errors
158	///
159	/// - If we cannot read from the file on the file system.
160	/// - If we cannot parse the data in the file as UTF8.
161	/// - If we cannot parse the data as an INI file.
162	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	/// Grab the currently configured default host bridge.
185	///
186	/// This returns an option (representing if any default has been configured),
187	/// and then will return an option for the ip address as the name could
188	/// potentially point to a name that doesn't have an ip address set, or one
189	/// that doesn't have a valid IPv4 address (as bridges are required to be
190	/// IPv4).
191	#[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	/// Get an actively configured bridge.
214	///
215	/// Returns `(BridgeIP, IsDefault)`, if a bridge is actively configured.
216	#[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	/// List all the bridges that are actively configured.
230	///
231	/// Returns a map of `<BridgeName, (BridgeIP, IsDefault)>`. The Bridge IP
232	/// will be an empty option if we could not parse the value as an IPv4
233	/// Address (i.e. the value is invalid), or the key did not have a value.
234	#[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	/// Insert a new bridge, or update it's value.
268	///
269	/// *note: this will be visible in memory immediately, but in order to
270	/// persist it, or have it seen in another process you need to call
271	/// [`BridgeHostState::write_to_disk`].*
272	///
273	/// ## Errors
274	///
275	/// If the bridge name is not ascii.
276	/// If the bridge name is empty.
277	/// If the bridge name is too long.
278	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	/// Remove a bridge from the configuration file.
302	///
303	/// *note: this will be visible in memory immediately, but in order to
304	/// persist it, or have it seen in another process you need to call
305	/// [`BridgeHostState::write_to_disk`].*
306	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	/// Remove the default bridge key from the configuration file.
314	///
315	/// *note: this will be visible in memory immediately, but in order to
316	/// persist it, or have it seen in another process you need to call
317	/// [`BridgeHostState::write_to_disk`].*
318	pub fn remove_default_bridge(&mut self) {
319		self.configuration
320			.remove_key(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
321	}
322
323	/// Set the default bridge for your host.
324	///
325	/// ## Errors
326	///
327	/// If you try setting the default bridge to a bridge that does not exist.
328	/// If your device name is not ascii.
329	/// If your device name is empty.
330	/// If your device name is too long.
331	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	/// Write the current configuration to disk as a Windows INI file.
358	///
359	/// We always write the file with carriage returns `\r\n` (windows line
360	/// endings), and in UTF-8. So we can always copy-paste the file onto
361	/// a windows host and have it be read by the official tools without issue.
362	///
363	/// ## Errors
364	///
365	/// If we run into a system error when writing the file to the disk.
366	pub async fn write_to_disk(&self) -> Result<(), FSError> {
367		let mut serialized_configuration = self.configuration.writes();
368		// Multiline is disabled -- so this is safe to check if we have actual carriage returns.
369		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	/// Get the path the Bridge Host State file was being loaded from.
390	#[must_use]
391	pub fn get_path(&self) -> &PathBuf {
392		&self.loaded_from_path
393	}
394
395	/// Get the default path that the bridge host state is supposed to be stored
396	/// in.
397	///
398	/// NOTE: this directory is not necissarily guaranteed to exist.
399	///
400	/// Returns none when we can't find an appropriate path to store bridge host
401	/// state in.
402	#[allow(
403		// We explicitly use cfg blocks to block all escape.
404		//
405		// However, if you're on a non explicitly mentioned OS, we still want the
406		// fallback.
407		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		// No values get mapped to none.
470		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		// Static Toucan & MION values.
477		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		// Parsed Toucan & MION Values.
489		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		// Check that we don't just do an endswith mp:
501		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		// First test loading the actual real configuration file completely not
528		// touched by our tools, and just rsync'd into the source tree from the
529		// real machine.
530		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		// Ordering isn't guaranteed has to be one of these!
710		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}