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