use std::fs;
use std::path::Path;
use md5::{Digest, Md5};
use crate::{Error, MagicContext, Result};
pub const MAGIC: [u8; 4] = *b"TDF$";
pub const READ_SUFFIXES: [&str; 3] = ["s", "1", "0"];
pub const WRITE_SUFFIX: &str = "s";
const HEADER_LEN: usize = MAGIC.len() + 4;
const CHECKSUM_LEN: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TdfFile {
pub version: u32,
pub payload: Vec<u8>,
}
fn checksum(version: u32, payload: &[u8]) -> [u8; CHECKSUM_LEN] {
let len = (payload.len() as u64).to_le_bytes();
let mut hasher = Md5::new();
hasher.update(payload);
hasher.update(&len[..4]);
hasher.update(version.to_le_bytes());
hasher.update(MAGIC);
let mut digest = [0; CHECKSUM_LEN];
digest.copy_from_slice(&hasher.finalize());
digest
}
impl TdfFile {
pub fn parse(data: &[u8]) -> Result<Self> {
let magic = data
.get(..MAGIC.len())
.ok_or(Error::QDataStreamFailed { context: None })?;
if magic != MAGIC {
return Err(Error::InvalidMagic {
context: MagicContext::File {
file: String::new(),
magic: magic.to_vec(),
},
});
}
if data.len() < HEADER_LEN + CHECKSUM_LEN {
return Err(Error::QDataStreamFailed { context: None });
}
let (header, rest) = data.split_at(HEADER_LEN);
let (payload, stored) = rest.split_at(rest.len() - CHECKSUM_LEN);
let mut version = [0; 4];
version.copy_from_slice(&header[MAGIC.len()..]);
let version = u32::from_le_bytes(version);
let computed = checksum(version, payload);
if computed != stored {
return Err(Error::InvalidCheckSum {
file: String::new(),
computed,
});
}
Ok(Self {
version,
payload: payload.to_vec(),
})
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER_LEN + self.payload.len() + CHECKSUM_LEN);
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&self.version.to_le_bytes());
out.extend_from_slice(&self.payload);
out.extend_from_slice(&checksum(self.version, &self.payload));
out
}
}
pub fn read(dir: impl AsRef<Path>, name: &str) -> Result<TdfFile> {
let dir = dir.as_ref();
let mut failure = None;
for suffix in READ_SUFFIXES {
let Ok(data) = fs::read(dir.join(format!("{name}{suffix}"))) else {
continue;
};
match TdfFile::parse(&data) {
Ok(file) => return Ok(file),
Err(error) => failure = Some(named(error, name)),
}
}
Err(failure.unwrap_or_else(|| Error::FileNotFound {
file: name.to_owned(),
}))
}
fn named(error: Error, name: &str) -> Error {
match error {
Error::InvalidMagic {
context: MagicContext::File { magic, .. },
} => Error::InvalidMagic {
context: MagicContext::File {
file: name.to_owned(),
magic,
},
},
Error::InvalidCheckSum { computed, .. } => Error::InvalidCheckSum {
file: name.to_owned(),
computed,
},
other => other,
}
}
pub fn write(dir: impl AsRef<Path>, name: &str, file: &TdfFile) -> Result<()> {
let dir = dir.as_ref();
fs::create_dir_all(dir)?;
fs::write(dir.join(format!("{name}{WRITE_SUFFIX}")), file.encode())?;
Ok(())
}