#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
pub mod cgis;
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
pub mod discovery;
pub mod errors;
pub mod firmware;
#[cfg_attr(docsrs, doc(cfg(feature = "clients")))]
#[cfg(feature = "clients")]
pub mod parameter;
#[cfg_attr(docsrs, doc(cfg(any(feature = "clients", feature = "servers"))))]
#[cfg(any(feature = "clients", feature = "servers"))]
pub mod proto;
use crate::errors::FSError;
use configparser::ini::Ini;
use errors::MionAPIError;
use fnv::FnvHashMap;
use std::{
fmt::{Display, Formatter, Result as FmtResult},
hash::BuildHasherDefault,
net::Ipv4Addr,
path::PathBuf,
};
const HARDWARE_ENV_NAME: &str = "CAFE_HARDWARE";
const BRIDGE_NAME_KEY_PREFIX: &str = "BRIDGE_NAME_";
const HOST_BRIDGES_SECTION: &str = "HOST_BRIDGES";
const DEFAULT_BRIDGE_KEY: &str = "BRIDGE_DEFAULT_NAME";
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum BridgeType {
Mion,
Toucan,
}
impl BridgeType {
#[must_use]
pub fn fetch_bridge_type() -> Option<Self> {
Self::hardware_type_to_value(std::env::var(HARDWARE_ENV_NAME).as_deref().ok())
}
fn hardware_type_to_value(hardware_type: Option<&str>) -> Option<Self> {
match hardware_type {
Some("ev") => Some(Self::Toucan),
Some("ev_x4") => Some(Self::Mion),
Some(val) => {
if val.chars().skip(6).collect::<String>() == *"mp" {
Some(Self::Mion)
} else if let Some(num) = val
.chars()
.nth(6)
.and_then(|character| char::to_digit(character, 10))
{
if num <= 2 {
Some(Self::Toucan)
} else {
Some(Self::Mion)
}
} else {
None
}
}
_ => None,
}
}
}
impl Default for BridgeType {
fn default() -> Self {
BridgeType::Mion
}
}
impl Display for BridgeType {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match *self {
Self::Mion => write!(fmt, "Mion"),
Self::Toucan => write!(fmt, "Toucan"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BridgeHostState {
configuration: Ini,
loaded_from_path: PathBuf,
}
impl BridgeHostState {
pub async fn load() -> Result<Self, FSError> {
let default_host_path =
Self::get_default_host_path().ok_or(FSError::CantFindHostEnvPath)?;
Self::load_explicit_path(default_host_path).await
}
pub async fn load_explicit_path(path: PathBuf) -> Result<Self, FSError> {
if path.exists() {
let as_bytes = tokio::fs::read(&path).await?;
let as_string = String::from_utf8(as_bytes)?;
let mut ini_contents = Ini::new_cs();
ini_contents
.read(as_string)
.map_err(|ini_error| FSError::InvalidDataNeedsToBeINI(format!("{ini_error:?}")))?;
Ok(Self {
configuration: ini_contents,
loaded_from_path: path,
})
} else {
Ok(Self {
configuration: Ini::new_cs(),
loaded_from_path: path,
})
}
}
#[must_use]
pub fn get_default_bridge(&self) -> Option<(String, Option<Ipv4Addr>)> {
if let Some(host_key) = self
.configuration
.get(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY)
{
let host_name = host_key
.as_str()
.trim_start_matches(BRIDGE_NAME_KEY_PREFIX)
.to_owned();
Some((
host_name,
self.configuration
.get(HOST_BRIDGES_SECTION, &host_key)
.and_then(|value| value.parse::<Ipv4Addr>().ok()),
))
} else {
None
}
}
#[must_use]
pub fn get_bridge(&self, bridge_name: &str) -> Option<(Option<Ipv4Addr>, bool)> {
let default_key = self
.configuration
.get(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
let key = format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}");
let is_default = default_key.as_deref() == Some(key.as_str());
self.configuration
.get(HOST_BRIDGES_SECTION, &key)
.map(|value| (value.parse::<Ipv4Addr>().ok(), is_default))
}
#[must_use]
pub fn list_bridges(&self) -> FnvHashMap<String, (Option<Ipv4Addr>, bool)> {
let ini_data = self.configuration.get_map_ref();
let Some(host_bridge_section) = ini_data.get(HOST_BRIDGES_SECTION) else {
return FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default());
};
let default_key = if let Some(Some(value)) = host_bridge_section.get(DEFAULT_BRIDGE_KEY) {
Some(value)
} else {
None
};
let mut bridges = FnvHashMap::default();
for (key, value) in host_bridge_section {
if key.as_str() == DEFAULT_BRIDGE_KEY
|| !key.as_str().starts_with(BRIDGE_NAME_KEY_PREFIX)
{
continue;
}
let is_default = Some(key) == default_key;
bridges.insert(
key.trim_start_matches(BRIDGE_NAME_KEY_PREFIX).to_owned(),
(
value.as_ref().and_then(|val| val.parse::<Ipv4Addr>().ok()),
is_default,
),
);
}
bridges
}
pub fn upsert_bridge(
&mut self,
bridge_name: &str,
bridge_ip: Ipv4Addr,
) -> Result<(), MionAPIError> {
if !bridge_name.is_ascii() {
return Err(MionAPIError::DeviceNameMustBeAscii);
}
if bridge_name.is_empty() {
return Err(MionAPIError::DeviceNameCannotBeEmpty);
}
if bridge_name.len() > 255 {
return Err(MionAPIError::DeviceNameTooLong(bridge_name.len()));
}
self.configuration.set(
HOST_BRIDGES_SECTION,
&format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}"),
Some(format!("{bridge_ip}")),
);
Ok(())
}
pub fn remove_bridge(&mut self, bridge_name: &str) {
self.configuration.remove_key(
HOST_BRIDGES_SECTION,
&format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}"),
);
}
pub fn remove_default_bridge(&mut self) {
self.configuration
.remove_key(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY);
}
pub fn set_default_bridge(&mut self, bridge_name: &str) -> Result<(), MionAPIError> {
if !bridge_name.is_ascii() {
return Err(MionAPIError::DeviceNameMustBeAscii);
}
if bridge_name.is_empty() {
return Err(MionAPIError::DeviceNameCannotBeEmpty);
}
if bridge_name.len() > 255 {
return Err(MionAPIError::DeviceNameTooLong(bridge_name.len()));
}
let bridge_key = format!("{BRIDGE_NAME_KEY_PREFIX}{bridge_name}");
if self
.configuration
.get(HOST_BRIDGES_SECTION, &bridge_key)
.is_none()
{
return Err(MionAPIError::DefaultDeviceMustExist);
}
self.configuration
.set(HOST_BRIDGES_SECTION, DEFAULT_BRIDGE_KEY, Some(bridge_key));
Ok(())
}
pub async fn write_to_disk(&self) -> Result<(), FSError> {
let mut serialized_configuration = self.configuration.writes();
if !serialized_configuration.contains("\r\n") {
serialized_configuration = serialized_configuration.replace('\n', "\r\n");
}
let parent_dir = {
let mut path = self.loaded_from_path.clone();
path.pop();
path
};
tokio::fs::create_dir_all(&parent_dir).await?;
tokio::fs::write(
&self.loaded_from_path,
serialized_configuration.into_bytes(),
)
.await?;
Ok(())
}
#[must_use]
pub fn get_path(&self) -> &PathBuf {
&self.loaded_from_path
}
#[allow(
// We explicitly use cfg blocks to block all escape.
//
// However, if you're on a non explicitly mentioned OS, we still want the
// fallback.
unreachable_code,
)]
#[must_use]
pub fn get_default_host_path() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
use std::env::var as env_var;
if let Ok(appdata_dir) = env_var("APPDATA") {
let mut path = PathBuf::from(appdata_dir);
path.push("bridge_env.ini");
return Some(path);
}
return None;
}
#[cfg(target_os = "macos")]
{
use std::env::var as env_var;
if let Ok(home_dir) = env_var("HOME") {
let mut path = PathBuf::from(home_dir);
path.push("Library");
path.push("Application Support");
path.push("bridge_env.ini");
return Some(path);
}
return None;
}
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
{
use std::env::var as env_var;
if let Ok(xdg_config_dir) = env_var("XDG_CONFIG_HOME") {
let mut path = PathBuf::from(xdg_config_dir);
path.push("bridge_env.ini");
return Some(path);
} else if let Ok(home_dir) = env_var("HOME") {
let mut path = PathBuf::from(home_dir);
path.push(".config");
path.push("bridge_env.ini");
return Some(path);
}
return None;
}
None
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
pub fn bridge_type_parsing() {
assert_eq!(
BridgeType::hardware_type_to_value(None),
None,
"Empty hardware type did not map to a null bridge type?"
);
assert_eq!(
BridgeType::hardware_type_to_value(Some("ev")),
Some(BridgeType::Toucan),
"Hardware type `ev` was not a `Toucan` bridge!"
);
assert_eq!(
BridgeType::hardware_type_to_value(Some("ev_x4")),
Some(BridgeType::Mion),
"Hardware type `ev_x4` was not a `Mion` bridge!",
);
assert_eq!(
BridgeType::hardware_type_to_value(Some("catdevmp")),
Some(BridgeType::Mion),
"Hardware type `catdevmp` was not a `Mion` bridge!"
);
assert_eq!(
BridgeType::hardware_type_to_value(Some("catdev200")),
Some(BridgeType::Toucan),
"Hardware type `catdev200` was not a `Toucan` bridge!"
);
assert_eq!(
BridgeType::hardware_type_to_value(Some("catdevdevmp")),
None,
"Invalid hardware type did not get mapped to an empty bridge!",
);
}
#[test]
pub fn bridge_type_default_is_mion() {
assert_eq!(
BridgeType::default(),
BridgeType::Mion,
"Default bridge type was not mion!"
);
}
#[test]
pub fn can_find_host_env() {
assert!(
BridgeHostState::get_default_host_path().is_some(),
"Failed to find the host state path for your particular OS, please file an issue!",
);
}
#[tokio::test]
pub async fn can_load_ini_files() {
let mut test_data_dir = PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR")
.expect("Failed to read `CARGO_MANIFEST_DIR` to locate test files!"),
);
test_data_dir.push("src");
test_data_dir.push("mion");
test_data_dir.push("test-data");
{
let mut real_config_path = test_data_dir.clone();
real_config_path.push("real-bridge-env.ini");
let real_config = BridgeHostState::load_explicit_path(real_config_path)
.await
.expect("Failed to load a real `bridge_env.ini`!");
let all_bridges = real_config.list_bridges();
assert_eq!(
all_bridges.len(),
1,
"Didn't find the single bridge that should've been in our real life `bridge_env.ini`!",
);
assert_eq!(
all_bridges.get("00-25-5C-BA-5A-00").cloned(),
Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true)),
"Failed to find a default bridge returned through listed bridges.",
);
assert_eq!(
real_config.get_default_bridge(),
Some((
"00-25-5C-BA-5A-00".to_owned(),
Some(Ipv4Addr::new(192, 168, 7, 40)),
)),
"Failed to get default bridge"
);
assert_eq!(
real_config.get_bridge("00-25-5C-BA-5A-00"),
Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true))
);
}
{
let mut real_config_path = test_data_dir.clone();
real_config_path.push("fake-valid-bridge-env.ini");
let real_config = BridgeHostState::load_explicit_path(real_config_path)
.await
.expect("Failed to load a real `bridge_env.ini`!");
assert_eq!(
real_config.get_default_bridge(),
Some((
"00-25-5C-BA-5A-00".to_owned(),
Some(Ipv4Addr::new(192, 168, 7, 40))
)),
);
let all_bridges = real_config.list_bridges();
assert_eq!(
all_bridges.len(),
3,
"Didn't find the three bridge that should've been in our fake but valid `bridge_env.ini`!",
);
assert_eq!(
all_bridges.get("00-25-5C-BA-5A-00").cloned(),
Some((Some(Ipv4Addr::new(192, 168, 7, 40)), true)),
"Failed to find a default bridge returned through listed bridges in fake but valid bridge env.",
);
assert_eq!(
all_bridges.get("00-25-5C-BA-5A-01").cloned(),
Some((Some(Ipv4Addr::new(192, 168, 7, 41)), false)),
"Failed to find a non-default bridge returned through listed bridges in fake but valid bridge env.",
);
assert_eq!(
all_bridges.get("00-25-5C-BA-5A-02").cloned(),
Some((None, false)),
"Failed to find a non-default bridge returned through listed bridges in fake but valid bridge env.",
);
}
{
let mut real_config_path = test_data_dir.clone();
real_config_path.push("default-but-no-value.ini");
let real_config = BridgeHostState::load_explicit_path(real_config_path)
.await
.expect("Failed to load a real `bridge_env.ini`!");
assert_eq!(
real_config.get_default_bridge(),
Some(("00-25-5C-BA-5A-01".to_owned(), None)),
);
let all_bridges = real_config.list_bridges();
assert_eq!(
all_bridges.get("00-25-5C-BA-5A-00").cloned(),
Some((Some(Ipv4Addr::new(192, 168, 7, 40)), false)),
"Failed to find a default bridge returned through listed bridges in bridge env with default but no value.",
);
}
{
let mut real_config_path = test_data_dir.clone();
real_config_path.push("default-but-invalid-value.ini");
let real_config = BridgeHostState::load_explicit_path(real_config_path)
.await
.expect("Failed to load a real `bridge_env.ini`!");
assert_eq!(
real_config.get_default_bridge(),
Some(("00-25-5C-BA-5A-00".to_owned(), None)),
);
let all_bridges = real_config.list_bridges();
assert_eq!(
all_bridges.get("00-25-5C-BA-5A-00").cloned(),
Some((None, true)),
);
}
{
let mut real_config_path = test_data_dir.clone();
real_config_path.push("invalid-ini-file.ini");
assert!(matches!(
BridgeHostState::load_explicit_path(real_config_path).await,
Err(FSError::InvalidDataNeedsToBeINI(_)),
));
}
}
#[tokio::test]
pub async fn can_set_and_write_to_file() {
use tempfile::tempdir;
use tokio::fs::File;
let temporary_directory =
tempdir().expect("Failed to create temporary directory for tests!");
let mut path = PathBuf::from(temporary_directory.path());
path.push("bridge_env_custom_made.ini");
{
File::create(&path)
.await
.expect("Failed to create test file to write too!");
}
let mut host_env = BridgeHostState::load_explicit_path(path.clone())
.await
.expect("Failed to load empty file to write too!");
assert_eq!(
host_env.set_default_bridge("00-25-5C-BA-5A-00"),
Err(MionAPIError::DefaultDeviceMustExist),
);
assert_eq!(
host_env.upsert_bridge("", Ipv4Addr::new(192, 168, 1, 1)),
Err(MionAPIError::DeviceNameCannotBeEmpty),
);
assert_eq!(
host_env.upsert_bridge("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Ipv4Addr::new(192, 168, 1, 1)),
Err(MionAPIError::DeviceNameTooLong(256)),
);
assert_eq!(
host_env.upsert_bridge("ð’€€", Ipv4Addr::new(192, 168, 1, 1)),
Err(MionAPIError::DeviceNameMustBeAscii),
);
assert!(
host_env
.upsert_bridge(" with spaces ", Ipv4Addr::new(192, 168, 1, 1))
.is_ok()
);
assert!(host_env.set_default_bridge(" with spaces ").is_ok());
assert!(
host_env
.upsert_bridge("00-25-5C-BA-5A-00", Ipv4Addr::new(192, 168, 1, 2))
.is_ok()
);
assert!(
host_env
.upsert_bridge(" with spaces ", Ipv4Addr::new(192, 168, 1, 3))
.is_ok()
);
assert!(host_env.set_default_bridge("00-25-5C-BA-5A-00").is_ok());
assert!(host_env.write_to_disk().await.is_ok());
let read_data = String::from_utf8(
tokio::fs::read(path)
.await
.expect("Failed to read written data!"),
)
.expect("Written INI file wasn't UTF8?");
let choices = [
"[HOST_BRIDGES]\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\n".to_owned(),
"[HOST_BRIDGES]\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\n".to_owned(),
"[HOST_BRIDGES]\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\n".to_owned(),
"[HOST_BRIDGES]\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\n".to_owned(),
"[HOST_BRIDGES]\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\n".to_owned(),
"[HOST_BRIDGES]\r\nBRIDGE_DEFAULT_NAME=BRIDGE_NAME_00-25-5C-BA-5A-00\r\nBRIDGE_NAME_00-25-5C-BA-5A-00=192.168.1.2\r\nBRIDGE_NAME_ with spaces =192.168.1.3\r\n".to_owned(),
];
if !choices.contains(&read_data) {
panic!("Unexpected host bridges ini file:\n{read_data}");
}
}
}