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
10#[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/// MionBootType is used by SDIO server, and thus we allow all of these protos
22/// to be used on the server side.
23#[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
38/// The environment variable name to fetch what version of [`CAFE_HARDWARE`]
39/// we're using.
40const HARDWARE_ENV_NAME: &str = "CAFE_HARDWARE";
41/// The prefix prepended to the bridge name keys in the host env file to
42/// prevent collisions.
43const BRIDGE_NAME_KEY_PREFIX: &str = "BRIDGE_NAME_";
44/// The section name in the ini file we store a list of the host bridges are
45/// stored in.
46const HOST_BRIDGES_SECTION: &str = "HOST_BRIDGES";
47/// The key that contains that stores which bridge is marked as the default.
48const DEFAULT_BRIDGE_KEY: &str = "BRIDGE_DEFAULT_NAME";
49
50/// As far as I can derive from the sources available that we can cleanly read
51/// (e.g. shell scripts) there are two types of CAT-DEV units. This enum
52/// describes those.
53#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
54pub enum BridgeType {
55	/// "MION" is the 'standard' cat-dev unit that most people probably have (and
56	/// is in fact the only one the developers have).
57	///
58	/// MIONs are also refered to as "v3", and "v4". If you have a tan colored
59	/// box you almost assuredly have a MION.
60	Mion,
61	/// A toucan is the other type of cat-dev unit referenced in some of the
62	/// shell scripts.
63	///
64	/// We don't have a ton of documentation on this particular bridge type
65	/// mainly because we don't actually own any of these bridge types. They
66	/// generally be refered to as "v2" bridges.
67	Toucan,
68}
69impl BridgeType {
70	/// Attempt to get the bridge type from the environment.
71	///
72	/// This will only return `Some()` when the environment variable
73	/// `CAFE_HARDWARE` is set to a proper value. If it's not set to a value
74	/// then we will always return `None`.
75	///
76	/// A proper way to always get a bridge type would be doing something similar
77	/// to what the scripts do which is fallback to a default:
78	///
79	/// ```rust
80	/// let ty = cat_dev::mion::BridgeType::fetch_bridge_type().unwrap_or_default();
81	/// ```
82	#[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	/// Convert a known hardware type to a potential Bridge Type.
88	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	/// Shell scripts default to using MION if all things are the same.
115	///
116	/// In general you probably want to use [`BridgeType::fetch_bridge_type`],
117	/// and should only use this default when using something like
118	/// `unwrap_or_default` in case the bridge type isn't in the environment.
119	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/// The state of bridges that are actively known on this host.
133///
134/// This is effectively just a list of bridges along with one of those being
135/// set as the "default" bridge. A lot of this configuration is stored within
136/// a system level directory, see [`BridgeHostState::get_default_host_path`].
137#[derive(Clone, Debug, PartialEq, Eq)]
138pub struct BridgeHostState {
139	/// The fully existing configuration as we know it.
140	configuration: Ini,
141	/// The path we originally loaded ourselves from.
142	loaded_from_path: PathBuf,
143}
144impl BridgeHostState {
145	/// Attempt to load the bridge host state from the filesystem.
146	///
147	/// This is commonly referred to as `bridge_env.ini`, stored normally in
148	/// Windows under the `%APPDATA%\bridge_env.ini` file. This is where tools
149	/// like `getbridge`/`hostdisplayversion`/`setbridge` store which bridges
150	/// you actually use, and which one you've set as the default bridge.
151	///
152	/// ## Errors
153	///
154	/// - If we cannot get the default host path for your OS.
155	/// - Any error case from [`BridgeHostState::load_explicit_path`].
156	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	/// Attempt to load the bridge host state file from the filesystem.
163	///
164	/// This is commonly referred to as `bridge_env.ini`, and is a small
165	/// Windows (so UTF8) ini file, separated by newlines being `\r\n`.
166	///
167	/// ## Errors
168	///
169	/// - If we cannot read from the file on the file system.
170	/// - If we cannot parse the data in the file as UTF8.
171	/// - If we cannot parse the data as an INI file.
172	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	/// Grab the currently configured default host bridge.
195	///
196	/// This returns an option (representing if any default has been configured),
197	/// and then will return an option for the ip address as the name could
198	/// potentially point to a name that doesn't have an ip address set, or one
199	/// that doesn't have a valid IPv4 address (as bridges are required to be
200	/// IPv4).
201	#[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	/// Get an actively configured bridge.
224	///
225	/// Returns `(BridgeIP, IsDefault)`, if a bridge is actively configured.
226	#[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	/// List all the bridges that are actively configured.
240	///
241	/// Returns a map of `<BridgeName, (BridgeIP, IsDefault)>`. The Bridge IP
242	/// will be an empty option if we could not parse the value as an IPv4
243	/// Address (i.e. the value is invalid), or the key did not have a value.
244	#[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	/// Insert a new bridge, or update it's value.
278	///
279	/// *note: this will be visible in memory immediately, but in order to
280	/// persist it, or have it seen in another process you need to call
281	/// [`BridgeHostState::write_to_disk`].*
282	///
283	/// ## Errors
284	///
285	/// If the bridge name is not ascii.
286	/// If the bridge name is empty.
287	/// If the bridge name is too long.
288	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	/// Remove a bridge from the configuration file.
312	///
313	/// *note: this will be visible in memory immediately, but in order to
314	/// persist it, or have it seen in another process you need to call
315	/// [`BridgeHostState::write_to_disk`].*
316	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	/// Remove the default bridge key from the configuration file.
324	///
325	/// *note: this will be visible in memory immediately, but in order to
326	/// persist it, or have it seen in another process you need to call
327	/// [`BridgeHostState::write_to_disk`].*
328	pub fn remove_default_bridge(&mut self) {
329		self.configuration
330			.remove_key(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
331	}
332
333	/// Set the default bridge for your host.
334	///
335	/// ## Errors
336	///
337	/// If you try setting the default bridge to a bridge that does not exist.
338	/// If your device name is not ascii.
339	/// If your device name is empty.
340	/// If your device name is too long.
341	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	/// Write the current configuration to disk as a Windows INI file.
368	///
369	/// We always write the file with carriage returns `\r\n` (windows line
370	/// endings), and in UTF-8. So we can always copy-paste the file onto
371	/// a windows host and have it be read by the official tools without issue.
372	///
373	/// ## Errors
374	///
375	/// If we run into a system error when writing the file to the disk.
376	pub async fn write_to_disk(&self) -> Result<(), FSError> {
377		let mut serialized_configuration = self.configuration.writes();
378		// Multiline is disabled -- so this is safe to check if we have actual carriage returns.
379		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	/// Get the path the Bridge Host State file was being loaded from.
400	#[must_use]
401	pub fn get_path(&self) -> &PathBuf {
402		&self.loaded_from_path
403	}
404
405	/// Get the default path that the bridge host state is supposed to be stored
406	/// in.
407	///
408	/// NOTE: this directory is not necissarily guaranteed to exist.
409	///
410	/// Returns none when we can't find an appropriate path to store bridge host
411	/// state in.
412	#[allow(
413		// We explicitly use cfg blocks to block all escape.
414		//
415		// However, if you're on a non explicitly mentioned OS, we still want the
416		// fallback.
417		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		// No values get mapped to none.
480		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		// Static Toucan & MION values.
487		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		// Parsed Toucan & MION Values.
499		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		// Check that we don't just do an endswith mp:
511		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		// First test loading the actual real configuration file completely not
538		// touched by our tools, and just rsync'd into the source tree from the
539		// real machine.
540		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		// Ordering isn't guaranteed has to be one of these!
726		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}