+ Files/stdin input can be binary or ascii + Handles piping from other programs + Misc test fixes + Updated readme + clippy + fmt
317 lines
8.9 KiB
Rust
317 lines
8.9 KiB
Rust
pub mod format;
|
|
mod printers;
|
|
|
|
use crate::formatter::format::Format;
|
|
use platform_dirs::AppDirs;
|
|
use rust_embed::RustEmbed;
|
|
use serde::Deserialize;
|
|
use std::error::Error;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::fs::{read_dir, File};
|
|
use std::io::Read;
|
|
use std::path::PathBuf;
|
|
use std::{fs, str};
|
|
|
|
#[derive(Debug)]
|
|
#[allow(clippy::enum_variant_names)]
|
|
pub enum FormatConfigError {
|
|
IOError(std::io::Error),
|
|
TomlError {
|
|
file_name: String,
|
|
err: toml::de::Error,
|
|
},
|
|
Utf8Error(str::Utf8Error),
|
|
FormatNotFound(String),
|
|
}
|
|
|
|
impl Error for FormatConfigError {}
|
|
|
|
impl From<std::io::Error> for FormatConfigError {
|
|
fn from(e: std::io::Error) -> Self {
|
|
Self::IOError(e)
|
|
}
|
|
}
|
|
|
|
impl From<str::Utf8Error> for FormatConfigError {
|
|
fn from(e: str::Utf8Error) -> Self {
|
|
Self::Utf8Error(e)
|
|
}
|
|
}
|
|
|
|
impl Display for FormatConfigError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
let err_msg = match self {
|
|
FormatConfigError::IOError(e) => e.to_string(),
|
|
FormatConfigError::TomlError { file_name, err } => {
|
|
format!("Error parsing {}: {}", file_name, err)
|
|
}
|
|
FormatConfigError::Utf8Error(e) => e.to_string(),
|
|
FormatConfigError::FormatNotFound(s) => format!("{} was not found.", s),
|
|
};
|
|
|
|
write!(f, "Format Config Error: {}", err_msg)
|
|
}
|
|
}
|
|
|
|
#[derive(RustEmbed)]
|
|
#[folder = "formats/"]
|
|
#[include = "*.toml"]
|
|
struct BuiltInFormats;
|
|
|
|
#[derive(Debug, Deserialize, Clone, Default)]
|
|
pub struct FormatConfig {
|
|
pub formats: Vec<Format>,
|
|
}
|
|
|
|
impl FormatConfig {
|
|
fn parse_config_file(
|
|
src_file_name: &str,
|
|
config_data: &str,
|
|
) -> Result<Self, FormatConfigError> {
|
|
toml::from_str(config_data).map_err(|e| FormatConfigError::TomlError {
|
|
file_name: src_file_name.to_string(),
|
|
err: e,
|
|
})
|
|
}
|
|
|
|
fn get_built_in_config(&mut self) -> Result<(), FormatConfigError> {
|
|
for format_file_path in BuiltInFormats::iter() {
|
|
if format_file_path.ends_with("md") {
|
|
continue;
|
|
}
|
|
|
|
let format_file = BuiltInFormats::get(&format_file_path).unwrap();
|
|
|
|
let config_str = str::from_utf8(&format_file.data).unwrap();
|
|
|
|
let mut built_in: FormatConfig =
|
|
Self::parse_config_file(&format_file_path, config_str)?;
|
|
|
|
self.formats.append(&mut built_in.formats);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_file_config(&mut self, config_path: &Option<PathBuf>) -> Result<(), FormatConfigError> {
|
|
if let Some(config_path) = config_path {
|
|
let mut config_file = File::open(config_path)?;
|
|
let mut config_data = String::new();
|
|
config_file.read_to_string(&mut config_data)?;
|
|
let mut arg_config =
|
|
Self::parse_config_file(config_path.to_str().unwrap(), &config_data)?;
|
|
|
|
self.formats.append(&mut arg_config.formats);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_directory(
|
|
&mut self,
|
|
directory_path: &Option<PathBuf>,
|
|
) -> Result<(), FormatConfigError> {
|
|
if let Some(directory_path) = directory_path {
|
|
for path in read_dir(directory_path)? {
|
|
let file_name = path?.file_name();
|
|
let config_file_path = directory_path.join(file_name);
|
|
|
|
self.get_file_config(&Some(config_file_path))?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn new(
|
|
config_path: &Option<PathBuf>,
|
|
global_config_path: &Option<PathBuf>,
|
|
) -> Result<Self, FormatConfigError> {
|
|
let mut config = FormatConfig::default();
|
|
|
|
config.get_built_in_config()?;
|
|
|
|
config.get_file_config(config_path)?;
|
|
|
|
let global_dir = match global_config_path {
|
|
Some(g) => g.clone(),
|
|
None => {
|
|
let app_dirs = AppDirs::new(Some("formaty"), true).unwrap();
|
|
|
|
if !app_dirs.config_dir.exists() {
|
|
fs::create_dir(&app_dirs.config_dir)?;
|
|
}
|
|
|
|
app_dirs.config_dir
|
|
}
|
|
};
|
|
|
|
config.parse_directory(&Some(global_dir))?;
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
pub fn get_format(&self, name: &str) -> Result<Format, FormatConfigError> {
|
|
Ok(self
|
|
.formats
|
|
.iter()
|
|
.find(|f| f.name.as_str() == name)
|
|
.ok_or_else(|| FormatConfigError::FormatNotFound(name.to_string()))?
|
|
.clone())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::formatter::format::Format;
|
|
use crate::formatter::printers::print_bytes_as_array;
|
|
use crate::formatter::FormatConfig;
|
|
|
|
#[derive(PartialOrd, PartialEq, Debug, Clone)]
|
|
struct CCSDSPacket {
|
|
packet_version: u8,
|
|
packet_type: bool,
|
|
sec_header_flag: bool,
|
|
apid: u16,
|
|
seq_flag: u8,
|
|
seq: u16,
|
|
packet_len: u16,
|
|
data: Vec<u8>,
|
|
}
|
|
|
|
impl CCSDSPacket {
|
|
fn from_bytes(data: &[u8]) -> Self {
|
|
let packet_version = data[0] >> 5;
|
|
let packet_type = ((data[0] >> 4) & 0x01) == 1;
|
|
let sec_header_flag = ((data[0] >> 3) & 0x01) == 1;
|
|
let apid = (((data[0] & 0x7) as u16) << 8) | (data[1] as u16);
|
|
|
|
let seq_flag = data[2] >> 6;
|
|
let seq = (((data[2] & 0x3F) as u16) << 8) | (data[3] as u16);
|
|
|
|
let packet_len = ((data[4] as u16) << 8) | (data[5] as u16);
|
|
|
|
CCSDSPacket {
|
|
packet_version,
|
|
packet_type,
|
|
sec_header_flag,
|
|
apid,
|
|
seq_flag,
|
|
seq,
|
|
packet_len,
|
|
data: data[6..].to_vec(),
|
|
}
|
|
}
|
|
|
|
fn to_bytes(&self) -> Vec<u8> {
|
|
let mut data = vec![0u8; 6];
|
|
data[0] = (self.packet_version << 5)
|
|
| ((self.packet_type as u8) << 4)
|
|
| ((self.sec_header_flag as u8) << 3)
|
|
| ((self.apid >> 8) as u8);
|
|
data[1] = (self.apid & 0xFF) as u8;
|
|
data[2] = (self.seq_flag << 6) | ((self.seq >> 8) as u8);
|
|
data[3] = (self.seq & 0xFF) as u8;
|
|
data[4] = (self.packet_len >> 8) as u8;
|
|
data[5] = (self.packet_len & 0xff) as u8;
|
|
|
|
data.append(&mut self.data.clone());
|
|
data
|
|
}
|
|
|
|
// based off formats/ccsds.toml config
|
|
fn print(&self) -> String {
|
|
format!(
|
|
"\
|
|
Version Number: {}\n\
|
|
Packet Type: {}\n\
|
|
Secondary Header Flag: {}\n\
|
|
APID: {:#x}\n\
|
|
Sequence Flags: {}\n\
|
|
Packet Sequence Count: {}\n\
|
|
Data Length: {}\n\
|
|
Data: {}\n",
|
|
self.packet_version,
|
|
self.packet_type as u8,
|
|
self.sec_header_flag as u8,
|
|
self.apid,
|
|
self.seq_flag,
|
|
self.seq,
|
|
self.packet_len,
|
|
print_bytes_as_array(&self.data)
|
|
)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_ccsds() {
|
|
let ccsds_packet = CCSDSPacket {
|
|
packet_version: 0,
|
|
packet_type: true,
|
|
sec_header_flag: false,
|
|
apid: 0x2FF,
|
|
seq_flag: 3,
|
|
seq: 0,
|
|
packet_len: 5,
|
|
data: vec![0x01, 0x02, 0x03, 0x04, 0x05],
|
|
};
|
|
|
|
let bytes = ccsds_packet.to_bytes();
|
|
|
|
let parsed_packet = CCSDSPacket::from_bytes(&bytes);
|
|
|
|
assert_eq!(ccsds_packet, parsed_packet);
|
|
|
|
assert_eq!(bytes, parsed_packet.to_bytes())
|
|
}
|
|
|
|
fn get_ccsds_format() -> Format {
|
|
let format_config = FormatConfig::new(&None, &None).unwrap();
|
|
|
|
format_config.get_format("ccsds").unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_ccsds_parse() {
|
|
let ccsds_packet = CCSDSPacket {
|
|
packet_version: 0,
|
|
packet_type: true,
|
|
sec_header_flag: false,
|
|
apid: 0x2FF,
|
|
seq_flag: 3,
|
|
seq: 0,
|
|
packet_len: 5,
|
|
data: vec![0x01, 0x02, 0x03, 0x04, 0x05],
|
|
};
|
|
|
|
let format = get_ccsds_format();
|
|
let data = ccsds_packet.to_bytes();
|
|
let output = format.format_data(&data).unwrap();
|
|
|
|
assert_eq!(ccsds_packet.print(), output)
|
|
}
|
|
|
|
#[test]
|
|
fn test_ccsds_parse_hammer() {
|
|
let format = get_ccsds_format();
|
|
|
|
for apid in 0..(2u16.pow(11)) {
|
|
let ccsds_packet = CCSDSPacket {
|
|
packet_version: 0,
|
|
packet_type: (apid % 2) == 0,
|
|
sec_header_flag: (apid % 2) == 1,
|
|
apid,
|
|
seq_flag: (apid % 4) as u8,
|
|
seq: (apid % 11),
|
|
packet_len: 5,
|
|
data: vec![0x01, 0x02, 0x03, 0x04, 0x05],
|
|
};
|
|
|
|
let data = ccsds_packet.to_bytes();
|
|
let output = format.format_data(&data).unwrap();
|
|
|
|
assert_eq!(ccsds_packet.print(), output)
|
|
}
|
|
}
|
|
}
|