cat_dev/mion/proto/control/
announcement.rs

1//! Packets related to announcements, and "finding" bridges.
2//!
3//! Specifically the packet types:
4//!
5//! - [`crate::mion::proto::MionCommandByte::AnnounceYourselves`]
6//! - [`crate::mion::proto::MionCommandByte::AcknowledgeAnnouncement`]
7//!
8//! And all the associated structures necessary to parse those packets.
9
10use crate::{
11	errors::NetworkParseError,
12	mion::{
13		errors::MionAPIError,
14		proto::control::{MionCommandByte, MionControlProtocolError},
15	},
16};
17use bytes::{BufMut, Bytes, BytesMut};
18#[cfg(feature = "clients")]
19use mac_address::MacAddress;
20use std::{
21	fmt::{Display, Formatter, Result as FmtResult, Write},
22	net::Ipv4Addr,
23};
24use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
25
26/// The data to pass along in the "Announce Yourself" message.
27const ANNOUNCEMENT_MESSAGE: &str = "MULTI_I/O_NETWORK_BOARD";
28/// The flag to encode into the packet to request more detailed information.
29const DETAIL_FLAG_MESSAGE: &str = "enumV1";
30
31/// An announcement to ask all MION's to identify themselves.
32///
33/// Provide "detailed" to get more than the IP/Mac/Name/FPGA Version/FW Version
34/// fields.
35#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Valuable)]
36pub struct MionIdentityAnnouncement {
37	/// If we should query for extra information, this corresponds to the
38	/// `-detail` flag from `findbridge`, and in the actual protocol maps
39	/// to `enumV1`.
40	detailed: bool,
41}
42impl MionIdentityAnnouncement {
43	#[must_use]
44	pub const fn new(is_detailed: bool) -> Self {
45		Self {
46			detailed: is_detailed,
47		}
48	}
49
50	/// If we are going to ask the MIONs to include more information about
51	/// themselves.
52	#[must_use]
53	pub const fn is_detailed(&self) -> bool {
54		self.detailed
55	}
56}
57impl Display for MionIdentityAnnouncement {
58	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
59		write!(
60			fmt,
61			"{}",
62			if self.detailed {
63				"DetailedMionIdentityAnnouncement"
64			} else {
65				"MionIdentityAnnouncement"
66			}
67		)
68	}
69}
70impl TryFrom<Bytes> for MionIdentityAnnouncement {
71	type Error = NetworkParseError;
72
73	fn try_from(packet: Bytes) -> Result<Self, Self::Error> {
74		if packet.len() < 25 {
75			return Err(NetworkParseError::NotEnoughData(
76				"MionIdentityAnnouncement",
77				25,
78				packet.len(),
79				packet,
80			));
81		}
82		if packet.len() > 33 {
83			return Err(NetworkParseError::UnexpectedTrailer(
84				"MionIdentityAnnouncement",
85				packet.slice(33..),
86			));
87		}
88		let is_detailed = packet.len() > 25;
89
90		if packet[0] != u8::from(MionCommandByte::AnnounceYourselves) {
91			return Err(MionControlProtocolError::UnknownCommand(packet[0]).into());
92		}
93
94		if &packet[1..24] != ANNOUNCEMENT_MESSAGE.as_bytes() {
95			return Err(NetworkParseError::FieldEncodedIncorrectly(
96				"MionIdentityAnnouncement",
97				"buff",
98				"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
99			));
100		}
101		if packet[24] != 0 {
102			return Err(NetworkParseError::FieldEncodedIncorrectly(
103				"MionIdentityAnnouncement",
104				"buff",
105				"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
106			));
107		}
108		if is_detailed && &packet[25..] != b"enumV1\0\0" {
109			return Err(NetworkParseError::FieldEncodedIncorrectly(
110				"MionIdentityAnnouncement",
111				"buff",
112				"Only the static string `enumV1` followed by two NUL Terminators is allowed after `MULTI_I/O_NETWORK_BOARD`.",
113			));
114		}
115
116		Ok(Self {
117			detailed: is_detailed,
118		})
119	}
120}
121impl From<&MionIdentityAnnouncement> for Bytes {
122	fn from(this: &MionIdentityAnnouncement) -> Self {
123		let mut buff = BytesMut::with_capacity(if this.detailed { 33 } else { 25 });
124		buff.put_u8(u8::from(MionCommandByte::AnnounceYourselves));
125		buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
126		buff.put_u8(0);
127		if this.detailed {
128			buff.extend_from_slice(DETAIL_FLAG_MESSAGE.as_bytes());
129			buff.put_u16(0_u16);
130		}
131		buff.freeze()
132	}
133}
134impl From<MionIdentityAnnouncement> for Bytes {
135	fn from(value: MionIdentityAnnouncement) -> Self {
136		Self::from(&value)
137	}
138}
139
140/// The boot type the MION is actively configured to boot into.
141#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Valuable)]
142pub enum MionBootType {
143	/// Boot from the devices own internal NAND.
144	NAND,
145	/// Boot from the PC rather than from it's own internal device nand.
146	PCFS,
147	/// Dual, also sometimes called 'MULTI' (mostly in earlier SDKs), allows
148	/// using both PCFS + NAND at the same time.
149	DUAL,
150	/// An unknown boot type we don't know how to parse.
151	Unk(u8),
152}
153impl MionBootType {
154	/// If this boot type _requires_ using PCFS to not error.
155	#[must_use]
156	pub const fn needs_pcfs(&self) -> bool {
157		matches!(*self, MionBootType::DUAL | MionBootType::PCFS)
158	}
159}
160impl Display for MionBootType {
161	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
162		match *self {
163			Self::NAND => write!(fmt, "NAND"),
164			Self::PCFS => write!(fmt, "PCFS"),
165			Self::DUAL => write!(fmt, "DUAL"),
166			Self::Unk(val) => write!(fmt, "Unk({val})"),
167		}
168	}
169}
170impl From<u8> for MionBootType {
171	fn from(value: u8) -> Self {
172		match value {
173			0x1 => MionBootType::NAND,
174			0x2 => MionBootType::PCFS,
175			0x3 => MionBootType::DUAL,
176			num => MionBootType::Unk(num),
177		}
178	}
179}
180impl From<MionBootType> for u8 {
181	fn from(value: MionBootType) -> u8 {
182		match value {
183			MionBootType::NAND => 0x1,
184			MionBootType::PCFS => 0x2,
185			MionBootType::DUAL => 0x3,
186			MionBootType::Unk(num) => num,
187		}
188	}
189}
190
191/// An identity for a CAT-DEV that we received from the network.
192#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
193#[cfg(feature = "clients")]
194#[derive(Clone, Debug, Hash, PartialEq, Eq)]
195pub struct MionIdentity {
196	// All the extra detailed data, most of this is unknown, though some bytes
197	// are able to be read.
198	detailed_all: Option<Bytes>,
199	firmware_version: [u8; 4],
200	fpga_version: [u8; 4],
201	ip_address: Ipv4Addr,
202	mac: MacAddress,
203	name: String,
204}
205#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
206#[cfg(feature = "clients")]
207impl MionIdentity {
208	/// Create a new MION Identity from scratch.
209	///
210	/// ## Errors
211	///
212	/// - If the name is not ASCII.
213	/// - If the name is longer than 255 bytes.
214	/// - If the name is empty.
215	pub fn new(
216		detailed_data: Option<Bytes>,
217		firmware_version: [u8; 4],
218		fpga_version: [u8; 4],
219		ip_address: Ipv4Addr,
220		mac: MacAddress,
221		name: String,
222	) -> Result<Self, MionAPIError> {
223		if !name.is_ascii() {
224			return Err(MionAPIError::DeviceNameMustBeAscii);
225		}
226		if name.len() > 255 {
227			return Err(MionAPIError::DeviceNameTooLong(name.len()));
228		}
229		if name.is_empty() {
230			return Err(MionAPIError::DeviceNameCannotBeEmpty);
231		}
232
233		Ok(Self {
234			detailed_all: detailed_data,
235			firmware_version,
236			fpga_version,
237			ip_address,
238			mac,
239			name,
240		})
241	}
242
243	/// The firmware version of the current CAT-DEV, rendered as a string you'd
244	/// see displayed.
245	#[must_use]
246	pub fn firmware_version(&self) -> String {
247		format!(
248			"0.{}.{}.{}",
249			self.firmware_version[0], self.firmware_version[1], self.firmware_version[2],
250		)
251	}
252	/// The firmware version of the current CAT-DEV.
253	///
254	/// Each part is split into it's own byte. If you want a string
255	/// representation call [`MionIdentity::firmware_version`].
256	#[must_use]
257	pub const fn raw_firmware_version(&self) -> [u8; 4] {
258		self.firmware_version
259	}
260
261	/// The FPGA of the current CAT-DEV, rendered as a string you'd see
262	/// displayed in a list view.
263	#[must_use]
264	pub fn fpga_version(&self) -> String {
265		let mut fpga_version = String::with_capacity(8);
266		for byte in [
267			self.fpga_version[3],
268			self.fpga_version[2],
269			self.fpga_version[1],
270			self.fpga_version[0],
271		] {
272			_ = write!(&mut fpga_version, "{byte:x}");
273		}
274		fpga_version
275	}
276	/// The FPGA of the current CAT-DEV, rendered as a string you'd see
277	/// displayed in a detail view.
278	#[must_use]
279	pub fn detailed_fpga_version(&self) -> String {
280		let mut fpga_version = String::with_capacity(8);
281		for byte in [
282			self.fpga_version[3],
283			self.fpga_version[2],
284			self.fpga_version[1],
285			self.fpga_version[0],
286		] {
287			_ = write!(&mut fpga_version, "{byte:02x}");
288		}
289		fpga_version
290	}
291	/// The version of the FPGA on the CAT-DEV.
292	///
293	/// Each part is split into it's own byte. If you want a string
294	/// representation call [`MionIdentity::fpga_version`].
295	#[must_use]
296	pub const fn raw_fpga_version(&self) -> [u8; 4] {
297		self.fpga_version
298	}
299
300	/// The IP Address this identity belongs to.
301	#[must_use]
302	pub const fn ip_address(&self) -> Ipv4Addr {
303		self.ip_address
304	}
305
306	/// The Mac Address of the identity belongs to.
307	#[must_use]
308	pub const fn mac_address(&self) -> MacAddress {
309		self.mac
310	}
311
312	/// The name of this machine.
313	#[must_use]
314	pub fn name(&self) -> &str {
315		&self.name
316	}
317
318	/// If the data the client sent back to us was _detailed_, and contains extra
319	/// bits of information.
320	///
321	/// NOTE: for old enough firmwares, even if you ask for detailed data you may
322	/// not get it.
323	#[must_use]
324	pub const fn is_detailed(&self) -> bool {
325		self.detailed_all.is_some()
326	}
327
328	/// If you've asked for, and received detailed information this will be the
329	/// SDK version that the current dev-kit is running.
330	#[must_use]
331	pub fn detailed_sdk_version(&self) -> Option<String> {
332		self.detailed_all.as_ref().map(|extra_data| {
333			let bytes = [
334				extra_data[227],
335				extra_data[228],
336				extra_data[229],
337				extra_data[230],
338			];
339
340			// SDK versions may not display the fourth identifier.
341			if bytes[3] == 0 {
342				format!("{}.{}.{}", bytes[0], bytes[1], bytes[2])
343			} else {
344				format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])
345			}
346		})
347	}
348	/// If you've asked for, and received detailed information this will be the
349	/// SDK version that the current dev-kit is running.
350	///
351	/// These are the 4 raw bytes returned from the response. If you want to
352	/// display these as a string somewhere you should use the method:
353	/// [`MionIdentity::detailed_sdk_version`].
354	#[must_use]
355	pub fn detailed_raw_sdk_version(&self) -> Option<[u8; 4]> {
356		self.detailed_all.as_ref().map(|extra_data| {
357			[
358				extra_data[227],
359				extra_data[228],
360				extra_data[229],
361				extra_data[230],
362			]
363		})
364	}
365
366	/// If you've asked for, and received detailed information this will be the
367	/// boot-type that the device is configured to use.
368	#[must_use]
369	pub fn detailed_boot_type(&self) -> Option<MionBootType> {
370		self.detailed_all
371			.as_ref()
372			.map(|extra_data| MionBootType::from(extra_data[232]))
373	}
374
375	/// If you've asked for, and received detailed information this will be the
376	/// status of cafe being on/off.
377	#[must_use]
378	pub fn detailed_is_cafe_on(&self) -> Option<bool> {
379		self.detailed_all
380			.as_ref()
381			.map(|extra_data| extra_data[233] > 0)
382	}
383}
384#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
385#[cfg(feature = "clients")]
386impl Display for MionIdentity {
387	fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
388		if let Some(detailed) = self.detailed_all.as_ref() {
389			write!(
390				fmt,
391				"{} (aka {}) @ {} fpga-v{}.{}.{}.{} fw-v{}.{}.{}.{} sdk-v{}.{}.{}.{} boot-type:{} cafe:{}",
392				self.name,
393				self.ip_address,
394				self.mac,
395				self.fpga_version[0],
396				self.fpga_version[1],
397				self.fpga_version[2],
398				self.fpga_version[3],
399				self.firmware_version[0],
400				self.firmware_version[1],
401				self.firmware_version[2],
402				self.firmware_version[3],
403				detailed[227],
404				detailed[228],
405				detailed[229],
406				detailed[230],
407				MionBootType::from(detailed[232]),
408				detailed[233],
409			)
410		} else {
411			write!(
412				fmt,
413				"{} (aka {}) @ {} fpga-v{}.{}.{}.{} fw-v{}.{}.{}.{}",
414				self.name,
415				self.ip_address,
416				self.mac,
417				self.fpga_version[0],
418				self.fpga_version[1],
419				self.fpga_version[2],
420				self.fpga_version[3],
421				self.firmware_version[0],
422				self.firmware_version[1],
423				self.firmware_version[2],
424				self.firmware_version[3],
425			)
426		}
427	}
428}
429#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
430#[cfg(feature = "clients")]
431impl TryFrom<(Ipv4Addr, Bytes)> for MionIdentity {
432	type Error = NetworkParseError;
433
434	fn try_from((from_address, packet): (Ipv4Addr, Bytes)) -> Result<Self, Self::Error> {
435		// Packet must be at least 18 bytes.
436		//
437		// Name starts at the 17th byte, and must be at least one byte long.
438		if packet.len() < 17 {
439			return Err(NetworkParseError::NotEnoughData(
440				"MionIdentity",
441				17,
442				packet.len(),
443				packet,
444			));
445		}
446
447		if packet[0] != u8::from(MionCommandByte::AcknowledgeAnnouncement) {
448			return Err(MionControlProtocolError::UnknownCommand(packet[0]).into());
449		}
450		// Name is variable in size, so we need to make sure we need there is
451		// enough space. Name length is stored at index 7, and is just one byte
452		// long.
453		let name_length = usize::from(packet[7]);
454		if packet.len() < 16 + name_length {
455			return Err(NetworkParseError::NotEnoughData(
456				"MionIdentity",
457				16 + name_length,
458				packet.len(),
459				packet,
460			));
461		}
462		if name_length < 1 {
463			return Err(NetworkParseError::FieldNotLongEnough(
464				"MionIdentity",
465				"name",
466				1,
467				name_length,
468				packet,
469			));
470		}
471		if packet.len() > 16 + name_length + 239 {
472			return Err(NetworkParseError::UnexpectedTrailer(
473				"MionIdentity",
474				packet.slice(16 + name_length + 239..),
475			));
476		}
477		if packet.len() != 16 + name_length && packet.len() != 16 + name_length + 239 {
478			return Err(NetworkParseError::UnexpectedTrailer(
479				"MionIdentity",
480				packet.slice(16 + name_length..),
481			));
482		}
483		let is_detailed = packet.len() > 16 + name_length;
484
485		let mac = MacAddress::new([
486			packet[1], packet[2], packet[3], packet[4], packet[5], packet[6],
487		]);
488		let fpga_version = [packet[8], packet[9], packet[10], packet[11]];
489		let firmware_version = [packet[12], packet[13], packet[14], packet[15]];
490		let Ok(name) = String::from_utf8(Vec::from(&packet[16..16 + name_length])) else {
491			return Err(NetworkParseError::FieldEncodedIncorrectly(
492				"MionIdentity",
493				"name",
494				"ASCII",
495			));
496		};
497		if !name.is_ascii() {
498			return Err(NetworkParseError::FieldEncodedIncorrectly(
499				"MionIdentity",
500				"name",
501				"ASCII",
502			));
503		}
504
505		let detailed_all = if is_detailed {
506			Some(packet.slice(16 + name_length..))
507		} else {
508			None
509		};
510
511		Ok(Self {
512			detailed_all,
513			firmware_version,
514			fpga_version,
515			ip_address: from_address,
516			mac,
517			name,
518		})
519	}
520}
521#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
522#[cfg(feature = "clients")]
523impl From<&MionIdentity> for Bytes {
524	fn from(value: &MionIdentity) -> Self {
525		let mut buff = BytesMut::with_capacity(16 + value.name.len());
526		buff.put_u8(u8::from(MionCommandByte::AcknowledgeAnnouncement));
527		buff.extend_from_slice(&value.mac.bytes());
528		buff.put_u8(u8::try_from(value.name.len()).unwrap_or(u8::MAX));
529		buff.extend_from_slice(&[
530			value.fpga_version[0],
531			value.fpga_version[1],
532			value.fpga_version[2],
533			value.fpga_version[3],
534		]);
535		buff.extend_from_slice(&[
536			value.firmware_version[0],
537			value.firmware_version[1],
538			value.firmware_version[2],
539			value.firmware_version[3],
540		]);
541		buff.extend_from_slice(value.name.as_bytes());
542		buff.freeze()
543	}
544}
545#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
546#[cfg(feature = "clients")]
547impl From<MionIdentity> for Bytes {
548	fn from(value: MionIdentity) -> Self {
549		Self::from(&value)
550	}
551}
552
553#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
554#[cfg(feature = "clients")]
555const MION_IDENTITY_FIELDS: &[NamedField<'static>] = &[
556	NamedField::new("name"),
557	NamedField::new("ip_address"),
558	NamedField::new("mac"),
559	NamedField::new("fpga_version"),
560	NamedField::new("firmware_version"),
561	NamedField::new("detailed_sdk_version"),
562	NamedField::new("detailed_boot_mode"),
563	NamedField::new("detailed_power_status"),
564];
565#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
566#[cfg(feature = "clients")]
567impl Structable for MionIdentity {
568	fn definition(&self) -> StructDef<'_> {
569		StructDef::new_static("MionIdentity", Fields::Named(MION_IDENTITY_FIELDS))
570	}
571}
572#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
573#[cfg(feature = "clients")]
574impl Valuable for MionIdentity {
575	fn as_value(&self) -> Value<'_> {
576		Value::Structable(self)
577	}
578
579	fn visit(&self, visitor: &mut dyn Visit) {
580		let detailed_sdk_version = self
581			.detailed_sdk_version()
582			.unwrap_or("<missing data>".to_owned());
583		let detailed_boot_mode = self
584			.detailed_boot_type()
585			.map_or("<missing data>".to_owned(), |bt| format!("{bt}"));
586		let detailed_power_status = self
587			.detailed_sdk_version()
588			.unwrap_or("<missing data>".to_owned());
589
590		visitor.visit_named_fields(&NamedValues::new(
591			MION_IDENTITY_FIELDS,
592			&[
593				Valuable::as_value(&self.name),
594				Valuable::as_value(&format!("{}", self.ip_address)),
595				Valuable::as_value(&format!("{}", self.mac)),
596				Valuable::as_value(&self.detailed_fpga_version()),
597				Valuable::as_value(&self.firmware_version()),
598				Valuable::as_value(&detailed_sdk_version),
599				Valuable::as_value(&detailed_boot_mode),
600				Valuable::as_value(&detailed_power_status),
601			],
602		));
603	}
604}
605
606#[cfg(test)]
607mod unit_tests {
608	use super::*;
609	use crate::mion::errors::MionProtocolError;
610
611	#[test]
612	pub fn mion_command_byte_conversions() {
613		for command_byte in vec![
614			MionCommandByte::Search,
615			MionCommandByte::Broadcast,
616			MionCommandByte::AnnounceYourselves,
617			MionCommandByte::AcknowledgeAnnouncement,
618		] {
619			assert_eq!(
620				MionCommandByte::try_from(u8::from(command_byte))
621					.expect("Failed to turn command byte -> u8 -> command byte"),
622				command_byte,
623				"Mion Command Byte when serialized & deserialized was not the same: {}",
624				command_byte,
625			);
626		}
627	}
628
629	#[cfg(feature = "clients")]
630	#[test]
631	pub fn mion_identity_construction_tests() {
632		assert_eq!(
633			MionIdentity::new(
634				None,
635				[0, 0, 0, 0],
636				[0, 0, 0, 0],
637				Ipv4Addr::LOCALHOST,
638				MacAddress::new([0, 0, 0, 0, 0, 0]),
639				// Doesn't fall within the ASCII range.
640				"Ƙ".to_owned()
641			),
642			Err(MionAPIError::DeviceNameMustBeAscii),
643		);
644		assert_eq!(
645			MionIdentity::new(
646				None,
647				[0, 0, 0, 0],
648				[0, 0, 0, 0],
649				Ipv4Addr::LOCALHOST,
650				MacAddress::new([0, 0, 0, 0, 0, 0]),
651				// Device name cannot be more than 255 bytes.
652				"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()
653			),
654			Err(MionAPIError::DeviceNameTooLong(300)),
655		);
656		assert_eq!(
657			MionIdentity::new(
658				None,
659				[0, 0, 0, 0],
660				[0, 0, 0, 0],
661				Ipv4Addr::LOCALHOST,
662				MacAddress::new([0, 0, 0, 0, 0, 0]),
663				// Cannot be empty!
664				String::new(),
665			),
666			Err(MionAPIError::DeviceNameCannotBeEmpty),
667		);
668		// Success!
669		assert!(
670			MionIdentity::new(
671				None,
672				[0, 0, 0, 0],
673				[0, 0, 0, 0],
674				Ipv4Addr::LOCALHOST,
675				MacAddress::new([0, 0, 0, 0, 0, 0]),
676				"00-00-00-00-00-00".to_owned(),
677			)
678			.is_ok()
679		);
680	}
681
682	#[cfg(feature = "clients")]
683	#[test]
684	pub fn mion_identity_deser() {
685		// Successful Serialization & Deserialization.
686		{
687			let identity = MionIdentity::new(
688				None,
689				[1, 2, 3, 4],
690				[5, 6, 7, 8],
691				Ipv4Addr::new(9, 10, 11, 12),
692				MacAddress::new([13, 14, 15, 16, 17, 18]),
693				"Apples".to_owned(),
694			)
695			.expect("Failed to create identity to serialize & deserialize.");
696
697			assert_eq!(
698				identity,
699				MionIdentity::try_from((Ipv4Addr::new(9, 10, 11, 12), Bytes::from(&identity)))
700					.expect("Failed to deserialize MION Identity")
701			);
702		}
703
704		// Too short no actual name field.
705		{
706			let buff = Bytes::from(vec![
707				// Command Byte
708				u8::from(MionCommandByte::AcknowledgeAnnouncement),
709				// Mac
710				0x1,
711				0x2,
712				0x3,
713				0x4,
714				0x5,
715				0x6,
716				// Name length
717				0x1,
718				// FPGA Version
719				0x1,
720				0x2,
721				0x3,
722				0x4,
723				// Firmware Version
724				0x1,
725				0x2,
726				0x3,
727				0x4,
728			]);
729
730			assert!(matches!(
731				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
732				Err(NetworkParseError::NotEnoughData("MionIdentity", 17, 16, _)),
733			));
734		}
735
736		// Command Byte is not correct.
737		{
738			let buff = Bytes::from(vec![
739				// Command Byte
740				u8::from(MionCommandByte::Search),
741				// Mac
742				0x1,
743				0x2,
744				0x3,
745				0x4,
746				0x5,
747				0x6,
748				// Name length
749				0x1,
750				// FPGA Version
751				0x1,
752				0x2,
753				0x3,
754				0x4,
755				// Firmware Version
756				0x1,
757				0x2,
758				0x3,
759				0x4,
760				// Name
761				101,
762			]);
763
764			assert_eq!(
765				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
766				Err(NetworkParseError::Mion(MionProtocolError::Control(
767					MionControlProtocolError::UnknownCommand(0x3F)
768				))),
769			);
770		}
771
772		// Name is not long enough.
773		{
774			let buff = Bytes::from(vec![
775				// Command Byte
776				u8::from(MionCommandByte::AcknowledgeAnnouncement),
777				// Mac
778				0x1,
779				0x2,
780				0x3,
781				0x4,
782				0x5,
783				0x6,
784				// Name length -- too short.
785				0x0,
786				// FPGA Version
787				0x1,
788				0x2,
789				0x3,
790				0x4,
791				// Firmware Version
792				0x1,
793				0x2,
794				0x3,
795				0x4,
796				// Name still needs to be present to pass initial length check.
797				101,
798			]);
799
800			assert_eq!(
801				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
802				Err(NetworkParseError::FieldNotLongEnough(
803					"MionIdentity",
804					"name",
805					1,
806					0,
807					buff
808				)),
809			);
810		}
811
812		// Name not UTF-8.
813		{
814			let buff = Bytes::from(vec![
815				// Command Byte
816				u8::from(MionCommandByte::AcknowledgeAnnouncement),
817				// Mac
818				0x1,
819				0x2,
820				0x3,
821				0x4,
822				0x5,
823				0x6,
824				// Name length
825				0x6,
826				// FPGA Version
827				0x1,
828				0x2,
829				0x3,
830				0x4,
831				// Firmware Version
832				0x1,
833				0x2,
834				0x3,
835				0x4,
836				// Name, invalid UTF-8
837				0xFF,
838				0xFF,
839				0xFF,
840				0xFF,
841				0xFF,
842				0xFF,
843			]);
844
845			assert_eq!(
846				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
847				Err(NetworkParseError::FieldEncodedIncorrectly(
848					"MionIdentity",
849					"name",
850					"ASCII"
851				)),
852			);
853		}
854
855		// Name UTF-8 but not ascii.
856		{
857			let buff = Bytes::from(vec![
858				// Command Byte
859				u8::from(MionCommandByte::AcknowledgeAnnouncement),
860				// Mac
861				0x1,
862				0x2,
863				0x3,
864				0x4,
865				0x5,
866				0x6,
867				// Name length
868				0x2,
869				// FPGA Version
870				0x1,
871				0x2,
872				0x3,
873				0x4,
874				// Firmware Version
875				0x1,
876				0x2,
877				0x3,
878				0x4,
879				// Name, "Ƙ" UTF-8 not ascii.
880				0xC6,
881				0x98,
882			]);
883
884			assert_eq!(
885				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
886				Err(NetworkParseError::FieldEncodedIncorrectly(
887					"MionIdentity",
888					"name",
889					"ASCII"
890				)),
891			);
892		}
893
894		// Unexpected trailer that isn't detailed.
895		{
896			let mut buff = BytesMut::new();
897			buff.extend_from_slice(&[
898				// Command Byte
899				u8::from(MionCommandByte::AcknowledgeAnnouncement),
900				// Mac
901				0x1,
902				0x2,
903				0x3,
904				0x4,
905				0x5,
906				0x6,
907				// Name length
908				0x2,
909				// FPGA Version
910				0x1,
911				0x2,
912				0x3,
913				0x4,
914				// Firmware Version
915				0x1,
916				0x2,
917				0x3,
918				0x4,
919				// Name.
920				0x61,
921				0x61,
922			]);
923			// Unexpected trailers...
924			buff.extend_from_slice(b"abcd");
925
926			assert_eq!(
927				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.freeze())),
928				Err(NetworkParseError::UnexpectedTrailer(
929					"MionIdentity",
930					Bytes::from(b"abcd".iter().cloned().collect::<Vec<u8>>())
931				)),
932			);
933		}
934
935		// Unexpected trailing data on fully detailed packet.
936		{
937			let mut buff = BytesMut::new();
938			buff.extend_from_slice(&[
939				// Command Byte
940				u8::from(MionCommandByte::AcknowledgeAnnouncement),
941				// Mac
942				0x1,
943				0x2,
944				0x3,
945				0x4,
946				0x5,
947				0x6,
948				// Name length
949				0x2,
950				// FPGA Version
951				0x1,
952				0x2,
953				0x3,
954				0x4,
955				// Firmware Version
956				0x1,
957				0x2,
958				0x3,
959				0x4,
960				// Name.
961				0x61,
962				0x61,
963			]);
964			// Pad extra detailed data.
965			buff.extend_from_slice(&[0x0; 239]);
966			// Unexpected trailers...
967			buff.extend_from_slice(b"abcd");
968
969			assert_eq!(
970				MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.freeze())),
971				Err(NetworkParseError::UnexpectedTrailer(
972					"MionIdentity",
973					Bytes::from(b"abcd".iter().cloned().collect::<Vec<u8>>())
974				)),
975			);
976		}
977	}
978
979	#[cfg(feature = "clients")]
980	#[test]
981	pub fn test_real_life_detailed_announcements() {
982		const OFF_ANNOUNCEMENT: [u8; 272] = [
983			0x20, 0x00, 0x25, 0x5c, 0xba, 0x5a, 0x00, 0x11, 0x71, 0x20, 0x05, 0x13, 0x00, 0x0e,
984			0x50, 0x01, 0x30, 0x30, 0x2d, 0x32, 0x35, 0x2d, 0x35, 0x43, 0x2d, 0x42, 0x41, 0x2d,
985			0x35, 0x41, 0x2d, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
986			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
987			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
988			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
989			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
990			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
991			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
992			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
993			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
994			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
995			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
996			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
997			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
998			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
999			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1000			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1001			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c, 0x0d, 0x00, 0x01, 0x02,
1002			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1003		];
1004		const ON_ANNOUNCEMENT: [u8; 272] = [
1005			0x20, 0x00, 0x25, 0x5c, 0xba, 0x5a, 0x00, 0x11, 0x71, 0x20, 0x05, 0x13, 0x00, 0x0e,
1006			0x50, 0x01, 0x30, 0x30, 0x2d, 0x32, 0x35, 0x2d, 0x35, 0x43, 0x2d, 0x42, 0x41, 0x2d,
1007			0x35, 0x41, 0x2d, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1008			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1009			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1010			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1011			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1012			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1013			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1014			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1015			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1016			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1017			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1018			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1019			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1020			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1021			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1022			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1023			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c, 0x0d, 0x00, 0x01, 0x02,
1024			0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
1025		];
1026
1027		let off_identity = MionIdentity::try_from((
1028			Ipv4Addr::LOCALHOST,
1029			Bytes::from(Vec::from(OFF_ANNOUNCEMENT)),
1030		))
1031		.expect("Failed to parse `OFF_ANNOUNCEMENT` from an actual data packet. Parser is broken.");
1032		let on_identity =
1033			MionIdentity::try_from((Ipv4Addr::LOCALHOST, Bytes::from(Vec::from(ON_ANNOUNCEMENT))))
1034				.expect(
1035					"Failed to parse `ON_ANNOUNCEMENT` from an actual data packet. Parser is broken.",
1036				);
1037
1038		assert_eq!(
1039			off_identity.detailed_sdk_version(),
1040			Some("2.12.13".to_owned())
1041		);
1042		assert_eq!(off_identity.detailed_boot_type(), Some(MionBootType::PCFS));
1043		assert_eq!(off_identity.detailed_is_cafe_on(), Some(false));
1044
1045		assert_eq!(
1046			on_identity.detailed_sdk_version(),
1047			Some("2.12.13".to_owned())
1048		);
1049		assert_eq!(on_identity.detailed_boot_type(), Some(MionBootType::PCFS));
1050		assert_eq!(on_identity.detailed_is_cafe_on(), Some(true));
1051	}
1052
1053	#[cfg(feature = "clients")]
1054	#[test]
1055	pub fn mion_announcement_ser_deser() {
1056		// Successes.
1057		{
1058			let announcement = MionIdentityAnnouncement { detailed: false };
1059			let serialized = Bytes::from(&announcement);
1060			let deser = MionIdentityAnnouncement::try_from(serialized);
1061			assert!(
1062				deser.is_ok(),
1063				"Failed to deserialize serialized MionIdentityAnnouncement!"
1064			);
1065			assert_eq!(
1066				announcement,
1067				deser.unwrap(),
1068				"MionIdentityAnnouncement was not the same after being serialized, and deserialized!",
1069			);
1070		}
1071		{
1072			let announcement = MionIdentityAnnouncement { detailed: true };
1073			let serialized = Bytes::from(&announcement);
1074			let deser = MionIdentityAnnouncement::try_from(serialized);
1075			assert!(
1076				deser.is_ok(),
1077				"Failed to deserialize serialized MionIdentityAnnouncement!"
1078			);
1079			assert_eq!(
1080				announcement,
1081				deser.unwrap(),
1082				"MionIdentityAnnouncement was not the same after being serialized, and deserialized!",
1083			);
1084		}
1085
1086		// Packet not long enough.
1087		{
1088			let packet = Bytes::from(vec![
1089				// Command Byte
1090				u8::from(MionCommandByte::AnnounceYourselves),
1091				// Should be Message Buff.
1092				0xA,
1093				// NUL terminator.
1094				0x0,
1095			]);
1096
1097			assert_eq!(
1098				MionIdentity::try_from((Ipv4Addr::LOCALHOST, packet.clone())),
1099				Err(NetworkParseError::NotEnoughData(
1100					"MionIdentity",
1101					17,
1102					3,
1103					packet
1104				)),
1105			);
1106		}
1107
1108		// Packet too long.
1109		{
1110			let packet = Bytes::from(vec![
1111				// Command Byte
1112				u8::from(MionCommandByte::AnnounceYourselves),
1113				// Should be Message Buff.
1114				0xA,
1115				0xA,
1116				0xA,
1117				0xA,
1118				0xA,
1119				0xA,
1120				0xA,
1121				0xA,
1122				0xA,
1123				0xA,
1124				0xA,
1125				0xA,
1126				0xA,
1127				0xA,
1128				0xA,
1129				0xA,
1130				0xA,
1131				0xA,
1132				0xA,
1133				0xA,
1134				0xA,
1135				0xA,
1136				0xA,
1137				0xA,
1138				0xA,
1139				0xA,
1140				0xA,
1141				0xA,
1142				0xA,
1143				0xA,
1144				0xA,
1145				0xA,
1146				0xA,
1147				0xA,
1148				0xA,
1149				0xA,
1150				0xA,
1151				0xA,
1152				0xA,
1153				0xA,
1154				0xA,
1155				// NUL terminator.
1156				0x0,
1157			]);
1158
1159			assert_eq!(
1160				MionIdentityAnnouncement::try_from(packet.clone()),
1161				Err(NetworkParseError::UnexpectedTrailer(
1162					"MionIdentityAnnouncement",
1163					packet.slice(33..),
1164				)),
1165			);
1166		}
1167
1168		// Command Byte Incorrect.
1169		{
1170			let mut buff = Vec::new();
1171			// Command byte incorrect
1172			buff.push(u8::from(MionCommandByte::Search));
1173			buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
1174			buff.push(0x0);
1175			let packet = Bytes::from(buff);
1176
1177			assert_eq!(
1178				MionIdentityAnnouncement::try_from(packet),
1179				Err(NetworkParseError::Mion(MionProtocolError::Control(
1180					MionControlProtocolError::UnknownCommand(u8::from(MionCommandByte::Search))
1181				))),
1182			);
1183		}
1184
1185		// Packet Data incorrect data.
1186		{
1187			let packet = Bytes::from(vec![
1188				// The Command Byte.
1189				u8::from(MionCommandByte::AnnounceYourselves),
1190				// 23 bytes of bad data.
1191				0xA,
1192				0xA,
1193				0xA,
1194				0xA,
1195				0xA,
1196				0xA,
1197				0xA,
1198				0xA,
1199				0xA,
1200				0xA,
1201				0xA,
1202				0xA,
1203				0xA,
1204				0xA,
1205				0xA,
1206				0xA,
1207				0xA,
1208				0xA,
1209				0xA,
1210				0xA,
1211				0xA,
1212				0xA,
1213				0xA,
1214				// NUL terminator.
1215				0x0,
1216			]);
1217
1218			assert_eq!(
1219				MionIdentityAnnouncement::try_from(packet),
1220				Err(NetworkParseError::FieldEncodedIncorrectly(
1221					"MionIdentityAnnouncement",
1222					"buff",
1223					"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
1224				)),
1225			);
1226		}
1227
1228		// Not ending with a NUL terminator.
1229		{
1230			let mut buff = Vec::new();
1231			// Command byte incorrect
1232			buff.push(u8::from(MionCommandByte::AnnounceYourselves));
1233			buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
1234			// Not NUL byte.
1235			buff.push(0x1);
1236			let packet = Bytes::from(buff);
1237
1238			assert_eq!(
1239				MionIdentityAnnouncement::try_from(packet),
1240				Err(NetworkParseError::FieldEncodedIncorrectly(
1241					"MionIdentityAnnouncement",
1242					"buff",
1243					"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
1244				)),
1245			);
1246		}
1247
1248		// `enumV1` tag is incorrect.
1249		{
1250			let mut buff = Vec::new();
1251			// Command byte incorrect
1252			buff.push(u8::from(MionCommandByte::AnnounceYourselves));
1253			buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
1254			buff.push(0x0);
1255			buff.extend_from_slice(DETAIL_FLAG_MESSAGE.as_bytes());
1256			// not null terminators.
1257			buff.push(0x1);
1258			buff.push(0x2);
1259			let packet = Bytes::from(buff);
1260
1261			assert_eq!(
1262				MionIdentityAnnouncement::try_from(packet),
1263				Err(NetworkParseError::FieldEncodedIncorrectly(
1264					"MionIdentityAnnouncement",
1265					"buff",
1266					"Only the static string `enumV1` followed by two NUL Terminators is allowed after `MULTI_I/O_NETWORK_BOARD`.",
1267				)),
1268			);
1269		}
1270	}
1271
1272	#[test]
1273	pub fn conversion_boot_type() {
1274		for boot_type in vec![
1275			MionBootType::NAND,
1276			MionBootType::PCFS,
1277			MionBootType::DUAL,
1278			MionBootType::Unk(0),
1279		] {
1280			assert_eq!(
1281				MionBootType::from(u8::from(boot_type)),
1282				boot_type,
1283				"`MIONBootType` : {boot_type} was not converted successfully!"
1284			);
1285		}
1286	}
1287}