use crate::{
errors::NetworkParseError,
mion::{
errors::MionAPIError,
proto::control::{MionCommandByte, MionControlProtocolError},
},
};
use bytes::{BufMut, Bytes, BytesMut};
#[cfg(feature = "clients")]
use mac_address::MacAddress;
use std::{
fmt::{Display, Formatter, Result as FmtResult, Write},
net::Ipv4Addr,
};
use valuable::{Fields, NamedField, NamedValues, StructDef, Structable, Valuable, Value, Visit};
const ANNOUNCEMENT_MESSAGE: &str = "MULTI_I/O_NETWORK_BOARD";
const DETAIL_FLAG_MESSAGE: &str = "enumV1";
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Valuable)]
pub struct MionIdentityAnnouncement {
detailed: bool,
}
impl MionIdentityAnnouncement {
#[must_use]
pub const fn new(is_detailed: bool) -> Self {
Self {
detailed: is_detailed,
}
}
#[must_use]
pub const fn is_detailed(&self) -> bool {
self.detailed
}
}
impl Display for MionIdentityAnnouncement {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
write!(
fmt,
"{}",
if self.detailed {
"DetailedMionIdentityAnnouncement"
} else {
"MionIdentityAnnouncement"
}
)
}
}
impl TryFrom<Bytes> for MionIdentityAnnouncement {
type Error = NetworkParseError;
fn try_from(packet: Bytes) -> Result<Self, Self::Error> {
if packet.len() < 25 {
return Err(NetworkParseError::NotEnoughData(
"MionIdentityAnnouncement",
25,
packet.len(),
packet,
));
}
if packet.len() > 33 {
return Err(NetworkParseError::UnexpectedTrailer(
"MionIdentityAnnouncement",
packet.slice(33..),
));
}
let is_detailed = packet.len() > 25;
if packet[0] != u8::from(MionCommandByte::AnnounceYourselves) {
return Err(MionControlProtocolError::UnknownCommand(packet[0]).into());
}
if &packet[1..24] != ANNOUNCEMENT_MESSAGE.as_bytes() {
return Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentityAnnouncement",
"buff",
"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
));
}
if packet[24] != 0 {
return Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentityAnnouncement",
"buff",
"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
));
}
if is_detailed && &packet[25..] != b"enumV1\0\0" {
return Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentityAnnouncement",
"buff",
"Only the static string `enumV1` followed by two NUL Terminators is allowed after `MULTI_I/O_NETWORK_BOARD`.",
));
}
Ok(Self {
detailed: is_detailed,
})
}
}
impl From<&MionIdentityAnnouncement> for Bytes {
fn from(this: &MionIdentityAnnouncement) -> Self {
let mut buff = BytesMut::with_capacity(if this.detailed { 33 } else { 25 });
buff.put_u8(u8::from(MionCommandByte::AnnounceYourselves));
buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
buff.put_u8(0);
if this.detailed {
buff.extend_from_slice(DETAIL_FLAG_MESSAGE.as_bytes());
buff.put_u16(0_u16);
}
buff.freeze()
}
}
impl From<MionIdentityAnnouncement> for Bytes {
fn from(value: MionIdentityAnnouncement) -> Self {
Self::from(&value)
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Valuable)]
pub enum MionBootType {
NAND,
PCFS,
DUAL,
Unk(u8),
}
impl MionBootType {
#[must_use]
pub const fn needs_pcfs(&self) -> bool {
matches!(*self, MionBootType::DUAL | MionBootType::PCFS)
}
}
impl Display for MionBootType {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match *self {
Self::NAND => write!(fmt, "NAND"),
Self::PCFS => write!(fmt, "PCFS"),
Self::DUAL => write!(fmt, "DUAL"),
Self::Unk(val) => write!(fmt, "Unk({val})"),
}
}
}
impl From<u8> for MionBootType {
fn from(value: u8) -> Self {
match value {
0x1 => MionBootType::NAND,
0x2 => MionBootType::PCFS,
0x3 => MionBootType::DUAL,
num => MionBootType::Unk(num),
}
}
}
impl From<MionBootType> for u8 {
fn from(value: MionBootType) -> u8 {
match value {
MionBootType::NAND => 0x1,
MionBootType::PCFS => 0x2,
MionBootType::DUAL => 0x3,
MionBootType::Unk(num) => num,
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct MionIdentity {
detailed_all: Option<Bytes>,
firmware_version: [u8; 4],
fpga_version: [u8; 4],
ip_address: Ipv4Addr,
mac: MacAddress,
name: String,
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl MionIdentity {
pub fn new(
detailed_data: Option<Bytes>,
firmware_version: [u8; 4],
fpga_version: [u8; 4],
ip_address: Ipv4Addr,
mac: MacAddress,
name: String,
) -> Result<Self, MionAPIError> {
if !name.is_ascii() {
return Err(MionAPIError::DeviceNameMustBeAscii);
}
if name.len() > 255 {
return Err(MionAPIError::DeviceNameTooLong(name.len()));
}
if name.is_empty() {
return Err(MionAPIError::DeviceNameCannotBeEmpty);
}
Ok(Self {
detailed_all: detailed_data,
firmware_version,
fpga_version,
ip_address,
mac,
name,
})
}
#[must_use]
pub fn firmware_version(&self) -> String {
format!(
"0.{}.{}.{}",
self.firmware_version[0], self.firmware_version[1], self.firmware_version[2],
)
}
#[must_use]
pub const fn raw_firmware_version(&self) -> [u8; 4] {
self.firmware_version
}
#[must_use]
pub fn fpga_version(&self) -> String {
let mut fpga_version = String::with_capacity(8);
for byte in [
self.fpga_version[3],
self.fpga_version[2],
self.fpga_version[1],
self.fpga_version[0],
] {
_ = write!(&mut fpga_version, "{byte:x}");
}
fpga_version
}
#[must_use]
pub fn detailed_fpga_version(&self) -> String {
let mut fpga_version = String::with_capacity(8);
for byte in [
self.fpga_version[3],
self.fpga_version[2],
self.fpga_version[1],
self.fpga_version[0],
] {
_ = write!(&mut fpga_version, "{byte:02x}");
}
fpga_version
}
#[must_use]
pub const fn raw_fpga_version(&self) -> [u8; 4] {
self.fpga_version
}
#[must_use]
pub const fn ip_address(&self) -> Ipv4Addr {
self.ip_address
}
#[must_use]
pub const fn mac_address(&self) -> MacAddress {
self.mac
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn is_detailed(&self) -> bool {
self.detailed_all.is_some()
}
#[must_use]
pub fn detailed_sdk_version(&self) -> Option<String> {
self.detailed_all.as_ref().map(|extra_data| {
let bytes = [
extra_data[227],
extra_data[228],
extra_data[229],
extra_data[230],
];
if bytes[3] == 0 {
format!("{}.{}.{}", bytes[0], bytes[1], bytes[2])
} else {
format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3])
}
})
}
#[must_use]
pub fn detailed_raw_sdk_version(&self) -> Option<[u8; 4]> {
self.detailed_all.as_ref().map(|extra_data| {
[
extra_data[227],
extra_data[228],
extra_data[229],
extra_data[230],
]
})
}
#[must_use]
pub fn detailed_boot_type(&self) -> Option<MionBootType> {
self.detailed_all
.as_ref()
.map(|extra_data| MionBootType::from(extra_data[232]))
}
#[must_use]
pub fn detailed_is_cafe_on(&self) -> Option<bool> {
self.detailed_all
.as_ref()
.map(|extra_data| extra_data[233] > 0)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl Display for MionIdentity {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
if let Some(detailed) = self.detailed_all.as_ref() {
write!(
fmt,
"{} (aka {}) @ {} fpga-v{}.{}.{}.{} fw-v{}.{}.{}.{} sdk-v{}.{}.{}.{} boot-type:{} cafe:{}",
self.name,
self.ip_address,
self.mac,
self.fpga_version[0],
self.fpga_version[1],
self.fpga_version[2],
self.fpga_version[3],
self.firmware_version[0],
self.firmware_version[1],
self.firmware_version[2],
self.firmware_version[3],
detailed[227],
detailed[228],
detailed[229],
detailed[230],
MionBootType::from(detailed[232]),
detailed[233],
)
} else {
write!(
fmt,
"{} (aka {}) @ {} fpga-v{}.{}.{}.{} fw-v{}.{}.{}.{}",
self.name,
self.ip_address,
self.mac,
self.fpga_version[0],
self.fpga_version[1],
self.fpga_version[2],
self.fpga_version[3],
self.firmware_version[0],
self.firmware_version[1],
self.firmware_version[2],
self.firmware_version[3],
)
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl TryFrom<(Ipv4Addr, Bytes)> for MionIdentity {
type Error = NetworkParseError;
fn try_from((from_address, packet): (Ipv4Addr, Bytes)) -> Result<Self, Self::Error> {
if packet.len() < 17 {
return Err(NetworkParseError::NotEnoughData(
"MionIdentity",
17,
packet.len(),
packet,
));
}
if packet[0] != u8::from(MionCommandByte::AcknowledgeAnnouncement) {
return Err(MionControlProtocolError::UnknownCommand(packet[0]).into());
}
let name_length = usize::from(packet[7]);
if packet.len() < 16 + name_length {
return Err(NetworkParseError::NotEnoughData(
"MionIdentity",
16 + name_length,
packet.len(),
packet,
));
}
if name_length < 1 {
return Err(NetworkParseError::FieldNotLongEnough(
"MionIdentity",
"name",
1,
name_length,
packet,
));
}
if packet.len() > 16 + name_length + 239 {
return Err(NetworkParseError::UnexpectedTrailer(
"MionIdentity",
packet.slice(16 + name_length + 239..),
));
}
if packet.len() != 16 + name_length && packet.len() != 16 + name_length + 239 {
return Err(NetworkParseError::UnexpectedTrailer(
"MionIdentity",
packet.slice(16 + name_length..),
));
}
let is_detailed = packet.len() > 16 + name_length;
let mac = MacAddress::new([
packet[1], packet[2], packet[3], packet[4], packet[5], packet[6],
]);
let fpga_version = [packet[8], packet[9], packet[10], packet[11]];
let firmware_version = [packet[12], packet[13], packet[14], packet[15]];
let Ok(name) = String::from_utf8(Vec::from(&packet[16..16 + name_length])) else {
return Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentity",
"name",
"ASCII",
));
};
if !name.is_ascii() {
return Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentity",
"name",
"ASCII",
));
}
let detailed_all = if is_detailed {
Some(packet.slice(16 + name_length..))
} else {
None
};
Ok(Self {
detailed_all,
firmware_version,
fpga_version,
ip_address: from_address,
mac,
name,
})
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl From<&MionIdentity> for Bytes {
fn from(value: &MionIdentity) -> Self {
let mut buff = BytesMut::with_capacity(16 + value.name.len());
buff.put_u8(u8::from(MionCommandByte::AcknowledgeAnnouncement));
buff.extend_from_slice(&value.mac.bytes());
buff.put_u8(u8::try_from(value.name.len()).unwrap_or(u8::MAX));
buff.extend_from_slice(&[
value.fpga_version[0],
value.fpga_version[1],
value.fpga_version[2],
value.fpga_version[3],
]);
buff.extend_from_slice(&[
value.firmware_version[0],
value.firmware_version[1],
value.firmware_version[2],
value.firmware_version[3],
]);
buff.extend_from_slice(value.name.as_bytes());
buff.freeze()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl From<MionIdentity> for Bytes {
fn from(value: MionIdentity) -> Self {
Self::from(&value)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
const MION_IDENTITY_FIELDS: &[NamedField<'static>] = &[
NamedField::new("name"),
NamedField::new("ip_address"),
NamedField::new("mac"),
NamedField::new("fpga_version"),
NamedField::new("firmware_version"),
NamedField::new("detailed_sdk_version"),
NamedField::new("detailed_boot_mode"),
NamedField::new("detailed_power_status"),
];
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl Structable for MionIdentity {
fn definition(&self) -> StructDef<'_> {
StructDef::new_static("MionIdentity", Fields::Named(MION_IDENTITY_FIELDS))
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
impl Valuable for MionIdentity {
fn as_value(&self) -> Value<'_> {
Value::Structable(self)
}
fn visit(&self, visitor: &mut dyn Visit) {
let detailed_sdk_version = self
.detailed_sdk_version()
.unwrap_or("<missing data>".to_owned());
let detailed_boot_mode = self
.detailed_boot_type()
.map_or("<missing data>".to_owned(), |bt| format!("{bt}"));
let detailed_power_status = self
.detailed_sdk_version()
.unwrap_or("<missing data>".to_owned());
visitor.visit_named_fields(&NamedValues::new(
MION_IDENTITY_FIELDS,
&[
Valuable::as_value(&self.name),
Valuable::as_value(&format!("{}", self.ip_address)),
Valuable::as_value(&format!("{}", self.mac)),
Valuable::as_value(&self.detailed_fpga_version()),
Valuable::as_value(&self.firmware_version()),
Valuable::as_value(&detailed_sdk_version),
Valuable::as_value(&detailed_boot_mode),
Valuable::as_value(&detailed_power_status),
],
));
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
use crate::mion::errors::MionProtocolError;
#[test]
pub fn mion_command_byte_conversions() {
for command_byte in vec![
MionCommandByte::Search,
MionCommandByte::Broadcast,
MionCommandByte::AnnounceYourselves,
MionCommandByte::AcknowledgeAnnouncement,
] {
assert_eq!(
MionCommandByte::try_from(u8::from(command_byte))
.expect("Failed to turn command byte -> u8 -> command byte"),
command_byte,
"Mion Command Byte when serialized & deserialized was not the same: {}",
command_byte,
);
}
}
#[cfg(feature = "clients")]
#[test]
pub fn mion_identity_construction_tests() {
assert_eq!(
MionIdentity::new(
None,
[0, 0, 0, 0],
[0, 0, 0, 0],
Ipv4Addr::LOCALHOST,
MacAddress::new([0, 0, 0, 0, 0, 0]),
"Ƙ".to_owned()
),
Err(MionAPIError::DeviceNameMustBeAscii),
);
assert_eq!(
MionIdentity::new(
None,
[0, 0, 0, 0],
[0, 0, 0, 0],
Ipv4Addr::LOCALHOST,
MacAddress::new([0, 0, 0, 0, 0, 0]),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()
),
Err(MionAPIError::DeviceNameTooLong(300)),
);
assert_eq!(
MionIdentity::new(
None,
[0, 0, 0, 0],
[0, 0, 0, 0],
Ipv4Addr::LOCALHOST,
MacAddress::new([0, 0, 0, 0, 0, 0]),
String::new(),
),
Err(MionAPIError::DeviceNameCannotBeEmpty),
);
assert!(
MionIdentity::new(
None,
[0, 0, 0, 0],
[0, 0, 0, 0],
Ipv4Addr::LOCALHOST,
MacAddress::new([0, 0, 0, 0, 0, 0]),
"00-00-00-00-00-00".to_owned(),
)
.is_ok()
);
}
#[cfg(feature = "clients")]
#[test]
pub fn mion_identity_deser() {
{
let identity = MionIdentity::new(
None,
[1, 2, 3, 4],
[5, 6, 7, 8],
Ipv4Addr::new(9, 10, 11, 12),
MacAddress::new([13, 14, 15, 16, 17, 18]),
"Apples".to_owned(),
)
.expect("Failed to create identity to serialize & deserialize.");
assert_eq!(
identity,
MionIdentity::try_from((Ipv4Addr::new(9, 10, 11, 12), Bytes::from(&identity)))
.expect("Failed to deserialize MION Identity")
);
}
{
let buff = Bytes::from(vec![
u8::from(MionCommandByte::AcknowledgeAnnouncement),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x1,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
]);
assert!(matches!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
Err(NetworkParseError::NotEnoughData("MionIdentity", 17, 16, _)),
));
}
{
let buff = Bytes::from(vec![
u8::from(MionCommandByte::Search),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x1,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
101,
]);
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
Err(NetworkParseError::Mion(MionProtocolError::Control(
MionControlProtocolError::UnknownCommand(0x3F)
))),
);
}
{
let buff = Bytes::from(vec![
u8::from(MionCommandByte::AcknowledgeAnnouncement),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x0,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
101,
]);
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
Err(NetworkParseError::FieldNotLongEnough(
"MionIdentity",
"name",
1,
0,
buff
)),
);
}
{
let buff = Bytes::from(vec![
u8::from(MionCommandByte::AcknowledgeAnnouncement),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x6,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
0xFF,
]);
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentity",
"name",
"ASCII"
)),
);
}
{
let buff = Bytes::from(vec![
u8::from(MionCommandByte::AcknowledgeAnnouncement),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x2,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
0xC6,
0x98,
]);
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.clone())),
Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentity",
"name",
"ASCII"
)),
);
}
{
let mut buff = BytesMut::new();
buff.extend_from_slice(&[
u8::from(MionCommandByte::AcknowledgeAnnouncement),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x2,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
0x61,
0x61,
]);
buff.extend_from_slice(b"abcd");
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.freeze())),
Err(NetworkParseError::UnexpectedTrailer(
"MionIdentity",
Bytes::from(b"abcd".iter().cloned().collect::<Vec<u8>>())
)),
);
}
{
let mut buff = BytesMut::new();
buff.extend_from_slice(&[
u8::from(MionCommandByte::AcknowledgeAnnouncement),
0x1,
0x2,
0x3,
0x4,
0x5,
0x6,
0x2,
0x1,
0x2,
0x3,
0x4,
0x1,
0x2,
0x3,
0x4,
0x61,
0x61,
]);
buff.extend_from_slice(&[0x0; 239]);
buff.extend_from_slice(b"abcd");
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, buff.freeze())),
Err(NetworkParseError::UnexpectedTrailer(
"MionIdentity",
Bytes::from(b"abcd".iter().cloned().collect::<Vec<u8>>())
)),
);
}
}
#[cfg(feature = "clients")]
#[test]
pub fn test_real_life_detailed_announcements() {
const OFF_ANNOUNCEMENT: [u8; 272] = [
0x20, 0x00, 0x25, 0x5c, 0xba, 0x5a, 0x00, 0x11, 0x71, 0x20, 0x05, 0x13, 0x00, 0x0e,
0x50, 0x01, 0x30, 0x30, 0x2d, 0x32, 0x35, 0x2d, 0x35, 0x43, 0x2d, 0x42, 0x41, 0x2d,
0x35, 0x41, 0x2d, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c, 0x0d, 0x00, 0x01, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
const ON_ANNOUNCEMENT: [u8; 272] = [
0x20, 0x00, 0x25, 0x5c, 0xba, 0x5a, 0x00, 0x11, 0x71, 0x20, 0x05, 0x13, 0x00, 0x0e,
0x50, 0x01, 0x30, 0x30, 0x2d, 0x32, 0x35, 0x2d, 0x35, 0x43, 0x2d, 0x42, 0x41, 0x2d,
0x35, 0x41, 0x2d, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0c, 0x0d, 0x00, 0x01, 0x02,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let off_identity = MionIdentity::try_from((
Ipv4Addr::LOCALHOST,
Bytes::from(Vec::from(OFF_ANNOUNCEMENT)),
))
.expect("Failed to parse `OFF_ANNOUNCEMENT` from an actual data packet. Parser is broken.");
let on_identity =
MionIdentity::try_from((Ipv4Addr::LOCALHOST, Bytes::from(Vec::from(ON_ANNOUNCEMENT))))
.expect(
"Failed to parse `ON_ANNOUNCEMENT` from an actual data packet. Parser is broken.",
);
assert_eq!(
off_identity.detailed_sdk_version(),
Some("2.12.13".to_owned())
);
assert_eq!(off_identity.detailed_boot_type(), Some(MionBootType::PCFS));
assert_eq!(off_identity.detailed_is_cafe_on(), Some(false));
assert_eq!(
on_identity.detailed_sdk_version(),
Some("2.12.13".to_owned())
);
assert_eq!(on_identity.detailed_boot_type(), Some(MionBootType::PCFS));
assert_eq!(on_identity.detailed_is_cafe_on(), Some(true));
}
#[cfg(feature = "clients")]
#[test]
pub fn mion_announcement_ser_deser() {
{
let announcement = MionIdentityAnnouncement { detailed: false };
let serialized = Bytes::from(&announcement);
let deser = MionIdentityAnnouncement::try_from(serialized);
assert!(
deser.is_ok(),
"Failed to deserialize serialized MionIdentityAnnouncement!"
);
assert_eq!(
announcement,
deser.unwrap(),
"MionIdentityAnnouncement was not the same after being serialized, and deserialized!",
);
}
{
let announcement = MionIdentityAnnouncement { detailed: true };
let serialized = Bytes::from(&announcement);
let deser = MionIdentityAnnouncement::try_from(serialized);
assert!(
deser.is_ok(),
"Failed to deserialize serialized MionIdentityAnnouncement!"
);
assert_eq!(
announcement,
deser.unwrap(),
"MionIdentityAnnouncement was not the same after being serialized, and deserialized!",
);
}
{
let packet = Bytes::from(vec![
u8::from(MionCommandByte::AnnounceYourselves),
0xA,
0x0,
]);
assert_eq!(
MionIdentity::try_from((Ipv4Addr::LOCALHOST, packet.clone())),
Err(NetworkParseError::NotEnoughData(
"MionIdentity",
17,
3,
packet
)),
);
}
{
let packet = Bytes::from(vec![
u8::from(MionCommandByte::AnnounceYourselves),
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0x0,
]);
assert_eq!(
MionIdentityAnnouncement::try_from(packet.clone()),
Err(NetworkParseError::UnexpectedTrailer(
"MionIdentityAnnouncement",
packet.slice(33..),
)),
);
}
{
let mut buff = Vec::new();
buff.push(u8::from(MionCommandByte::Search));
buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
buff.push(0x0);
let packet = Bytes::from(buff);
assert_eq!(
MionIdentityAnnouncement::try_from(packet),
Err(NetworkParseError::Mion(MionProtocolError::Control(
MionControlProtocolError::UnknownCommand(u8::from(MionCommandByte::Search))
))),
);
}
{
let packet = Bytes::from(vec![
u8::from(MionCommandByte::AnnounceYourselves),
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0xA,
0x0,
]);
assert_eq!(
MionIdentityAnnouncement::try_from(packet),
Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentityAnnouncement",
"buff",
"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
)),
);
}
{
let mut buff = Vec::new();
buff.push(u8::from(MionCommandByte::AnnounceYourselves));
buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
buff.push(0x1);
let packet = Bytes::from(buff);
assert_eq!(
MionIdentityAnnouncement::try_from(packet),
Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentityAnnouncement",
"buff",
"Must start with static message: `MULTI_I/O_NETWORK_BOARD` with a NUL Terminator",
)),
);
}
{
let mut buff = Vec::new();
buff.push(u8::from(MionCommandByte::AnnounceYourselves));
buff.extend_from_slice(ANNOUNCEMENT_MESSAGE.as_bytes());
buff.push(0x0);
buff.extend_from_slice(DETAIL_FLAG_MESSAGE.as_bytes());
buff.push(0x1);
buff.push(0x2);
let packet = Bytes::from(buff);
assert_eq!(
MionIdentityAnnouncement::try_from(packet),
Err(NetworkParseError::FieldEncodedIncorrectly(
"MionIdentityAnnouncement",
"buff",
"Only the static string `enumV1` followed by two NUL Terminators is allowed after `MULTI_I/O_NETWORK_BOARD`.",
)),
);
}
}
#[test]
pub fn conversion_boot_type() {
for boot_type in vec![
MionBootType::NAND,
MionBootType::PCFS,
MionBootType::DUAL,
MionBootType::Unk(0),
] {
assert_eq!(
MionBootType::from(u8::from(boot_type)),
boot_type,
"`MIONBootType` : {boot_type} was not converted successfully!"
);
}
}
}