use std::{fs, path::PathBuf}; use log::{debug, error, info}; use semver::Version; use std::str::FromStr; use thiserror::Error; use crate::schemas::{BoardConfig, BoardType, OTAConfiguration, Services}; pub fn parse_mac_address(mac: &str) -> Result<[u8; 6], MacAddressError> { if mac.len() != 12 { return Err(MacAddressError::Length(mac.len())); } let mut mac_bytes = [0u8; 6]; for i in 0..6 { let hex_part = &mac[i * 2..i * 2 + 2]; mac_bytes[i] = u8::from_str_radix(hex_part, 16)?; } Ok(mac_bytes) } #[derive(Error, Debug)] pub enum MacAddressError { #[error("Error converting MAC Address")] Conversion(#[from] std::num::ParseIntError), #[error("MAC Address length mismatch")] Length(usize), } #[derive(Error, Debug)] pub enum GetFilesError { #[error("IO Error while reading files")] IO(#[from] std::io::Error), #[error("Error getting filename")] Filename, #[error("Error getting extension (.bin)")] Extension, #[error("Strum parse Error")] Conversion(#[from] strum::ParseError), #[error("Failed to parse the version")] VersionParse(#[from] semver::Error), } pub fn get_files( root_path: &PathBuf, hostname: &str, ) -> Result, GetFilesError> { info!("Getting all files from path {}", root_path.display()); let mut configs = Vec::new(); let product_name = root_path .file_name() .ok_or(GetFilesError::Filename)? .to_string_lossy(); let entries = fs::read_dir(root_path)?; for entry in entries.flatten() { info!("Reading entry: {entry:?}"); let path = entry.path(); if path.is_file() { info!("processing file: {}", path.display()); // Splits the filename at the underscores. This is safe to do as names get sanitized on upload and are only uploaded by the pipeline let split_name: Vec<_> = path .file_name() .ok_or(GetFilesError::Filename)? .to_str() .ok_or(GetFilesError::Filename)? .split('_') .collect(); let version = split_name[2] .strip_suffix(".bin") .ok_or(GetFilesError::Extension)? .replace('-', "."); debug!("Version: {:?}", version); debug!("split_name: {:?}", split_name); // TODO this is kinda messy let board_config = BoardConfig::from_str(split_name[1])?; let service = split_name[0]; let board_type = BoardType::from_str(&product_name).unwrap(); let version = if version.contains('.') { Version::parse(&version)? } else { // Handle simple version number by adding .0.0 Version::parse(&format!("{}.0.0", version))? }; let version_replaced = format!("{}-{}-{}", version.major, version.minor, version.patch); let fw_url = format!("{hostname}/{service}/{board_type}/{board_config}/{version_replaced}.bin"); let cfg = OTAConfiguration { version, url: fw_url, board: Some(board_type), config: Some(board_config), }; configs.push(cfg); } else if path.is_dir() { println!("Directory: {}", path.display()); } } Ok(configs) } pub fn prune_files(path: &PathBuf, service: &Services, keep_last: usize) { let Ok(mut config_list) = get_files(path, "irrelevant") else { error!("failed to get file list for pruning"); return }; config_list.sort_by_key(|x| x.version.clone()); config_list.truncate(config_list.len() - keep_last); for cfg in config_list { println!("{cfg:?}"); let path = path.to_string_lossy(); let board_type = cfg. config.unwrap(); let path_to_remove = format!("{path}/{service}_{}_{}-{}-{}.bin", board_type, cfg.version.major, cfg.version.minor, cfg.version.patch); if let Err(e) = fs::remove_file(&path_to_remove) { error!("Failed to delete {path_to_remove}, {e:?}"); return; } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_file_loading() { let expected_1 = OTAConfiguration { version: Version::parse("1.2.3").unwrap(), url: "example.com/firmware/waterlevel/INA233/1-2-3.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA233), }; let expected_2 = OTAConfiguration { version: Version::parse("4.5.6").unwrap(), url: "example.com/firmware/waterlevel/INA226/4-5-6.bin".to_string(), board: Some(BoardType::Waterlevel), config: Some(BoardConfig::INA226), }; let loaded_configs = get_files(&PathBuf::from("./test/waterlevel"), "example.com").unwrap(); assert_eq!(loaded_configs[1], expected_1); assert_eq!(loaded_configs[0], expected_2); } #[test] fn test_valid_mac_address_plain() { let mac_str = "001A2B3C4D5E"; let expected = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; assert_eq!(parse_mac_address(mac_str).unwrap(), expected); } #[test] fn test_valid_lowercase_mac_address() { let mac_str = "001a2b3c4d5e"; let expected = [0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]; assert_eq!(parse_mac_address(mac_str).unwrap(), expected); } #[test] fn test_invalid_mac_address_wrong_length() { let mac_str = "001A2B3C4D"; assert!(parse_mac_address(mac_str).is_err()); } #[test] fn test_invalid_mac_address_invalid_characters() { let mac_str = "001A2B3C4DZZ"; assert!(parse_mac_address(mac_str).is_err()); } #[test] fn test_empty_mac_address() { let mac_str = ""; assert!(parse_mac_address(mac_str).is_err()); } #[test] fn test_mac_address_with_extra_spaces() { let mac_str = "001A2B3C 4D5E"; assert!(parse_mac_address(mac_str).is_err()); } }