1use std::fs;
14use std::path::Path;
15
16use md5::{Digest, Md5};
17
18use crate::{Error, MagicContext, Result};
19
20pub const MAGIC: [u8; 4] = *b"TDF$";
22
23pub const READ_SUFFIXES: [&str; 3] = ["s", "1", "0"];
25
26pub const WRITE_SUFFIX: &str = "s";
28
29const HEADER_LEN: usize = MAGIC.len() + 4;
30const CHECKSUM_LEN: usize = 16;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct TdfFile {
35 pub version: u32,
37 pub payload: Vec<u8>,
39}
40
41fn checksum(version: u32, payload: &[u8]) -> [u8; CHECKSUM_LEN] {
43 let len = (payload.len() as u64).to_le_bytes();
46 let mut hasher = Md5::new();
47 hasher.update(payload);
48 hasher.update(&len[..4]);
49 hasher.update(version.to_le_bytes());
50 hasher.update(MAGIC);
51 let mut digest = [0; CHECKSUM_LEN];
52 digest.copy_from_slice(&hasher.finalize());
53 digest
54}
55
56impl TdfFile {
57 pub fn parse(data: &[u8]) -> Result<Self> {
68 let magic = data
69 .get(..MAGIC.len())
70 .ok_or(Error::QDataStreamFailed { context: None })?;
71 if magic != MAGIC {
72 return Err(Error::InvalidMagic {
73 context: MagicContext::File {
74 file: String::new(),
75 magic: magic.to_vec(),
76 },
77 });
78 }
79 if data.len() < HEADER_LEN + CHECKSUM_LEN {
80 return Err(Error::QDataStreamFailed { context: None });
81 }
82 let (header, rest) = data.split_at(HEADER_LEN);
83 let (payload, stored) = rest.split_at(rest.len() - CHECKSUM_LEN);
84 let mut version = [0; 4];
85 version.copy_from_slice(&header[MAGIC.len()..]);
86 let version = u32::from_le_bytes(version);
87 let computed = checksum(version, payload);
88 if computed != stored {
89 return Err(Error::InvalidCheckSum {
90 file: String::new(),
91 computed,
92 });
93 }
94 Ok(Self {
95 version,
96 payload: payload.to_vec(),
97 })
98 }
99
100 #[must_use]
102 pub fn encode(&self) -> Vec<u8> {
103 let mut out = Vec::with_capacity(HEADER_LEN + self.payload.len() + CHECKSUM_LEN);
104 out.extend_from_slice(&MAGIC);
105 out.extend_from_slice(&self.version.to_le_bytes());
106 out.extend_from_slice(&self.payload);
107 out.extend_from_slice(&checksum(self.version, &self.payload));
108 out
109 }
110}
111
112pub fn read(dir: impl AsRef<Path>, name: &str) -> Result<TdfFile> {
125 let dir = dir.as_ref();
126 let mut failure = None;
127 for suffix in READ_SUFFIXES {
128 let Ok(data) = fs::read(dir.join(format!("{name}{suffix}"))) else {
130 continue;
131 };
132 match TdfFile::parse(&data) {
133 Ok(file) => return Ok(file),
134 Err(error) => failure = Some(named(error, name)),
135 }
136 }
137 Err(failure.unwrap_or_else(|| Error::FileNotFound {
138 file: name.to_owned(),
139 }))
140}
141
142fn named(error: Error, name: &str) -> Error {
144 match error {
145 Error::InvalidMagic {
146 context: MagicContext::File { magic, .. },
147 } => Error::InvalidMagic {
148 context: MagicContext::File {
149 file: name.to_owned(),
150 magic,
151 },
152 },
153 Error::InvalidCheckSum { computed, .. } => Error::InvalidCheckSum {
154 file: name.to_owned(),
155 computed,
156 },
157 other => other,
158 }
159}
160
161pub fn write(dir: impl AsRef<Path>, name: &str, file: &TdfFile) -> Result<()> {
168 let dir = dir.as_ref();
169 fs::create_dir_all(dir)?;
170 fs::write(dir.join(format!("{name}{WRITE_SUFFIX}")), file.encode())?;
171 Ok(())
172}