186 lines
6.0 KiB
Rust
186 lines
6.0 KiB
Rust
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<Vec<OTAConfiguration>, 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());
|
|
}
|
|
}
|