serializer/
binary_output.rs1use std::fs;
19use std::io::Write;
20use std::path::{Path, PathBuf};
21
22#[derive(Debug, Clone)]
24pub struct BinaryConfig {
25 pub project_root: PathBuf,
27 pub output_dir: PathBuf,
29}
30
31impl Default for BinaryConfig {
32 fn default() -> Self {
33 Self {
34 project_root: PathBuf::from("."),
35 output_dir: PathBuf::from(".dx/serializer"),
36 }
37 }
38}
39
40impl BinaryConfig {
41 pub fn with_root<P: AsRef<Path>>(root: P) -> Self {
43 let root = root.as_ref().to_path_buf();
44 Self {
45 output_dir: root.join(".dx/serializer"),
46 project_root: root,
47 }
48 }
49}
50
51pub fn hash_path(relative_path: &str) -> String {
55 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
57 const FNV_PRIME: u64 = 0x100000001b3;
58
59 let mut hash = FNV_OFFSET;
60 for byte in relative_path.bytes() {
61 hash ^= byte as u64;
62 hash = hash.wrapping_mul(FNV_PRIME);
63 }
64
65 format!("{:08x}", hash as u32)
67}
68
69pub fn get_binary_path<P: AsRef<Path>>(source_path: P, config: &BinaryConfig) -> PathBuf {
92 let source = source_path.as_ref();
93
94 let relative = source
96 .strip_prefix(&config.project_root)
97 .unwrap_or(source)
98 .to_string_lossy()
99 .replace('\\', "/"); let hash = hash_path(&relative);
103
104 let filename = source
106 .file_stem()
107 .map(|s| s.to_string_lossy().to_string())
108 .unwrap_or_else(|| "dx".to_string());
109
110 config
112 .output_dir
113 .join(&hash)
114 .join(format!("{}.dx", filename))
115}
116
117pub fn write_binary<P: AsRef<Path>>(
121 source_path: P,
122 binary_data: &[u8],
123 config: &BinaryConfig,
124) -> std::io::Result<PathBuf> {
125 let output_path = get_binary_path(&source_path, config);
126
127 if let Some(parent) = output_path.parent() {
129 fs::create_dir_all(parent)?;
130 }
131
132 let mut file = fs::File::create(&output_path)?;
134 file.write_all(binary_data)?;
135
136 Ok(output_path)
137}
138
139pub fn read_binary<P: AsRef<Path>>(
141 source_path: P,
142 config: &BinaryConfig,
143) -> std::io::Result<Vec<u8>> {
144 let binary_path = get_binary_path(&source_path, config);
145 fs::read(binary_path)
146}
147
148pub fn is_cache_valid<P: AsRef<Path>>(source_path: P, config: &BinaryConfig) -> bool {
150 let source = source_path.as_ref();
151 let binary_path = get_binary_path(source, config);
152
153 let binary_meta = match fs::metadata(&binary_path) {
155 Ok(m) => m,
156 Err(_) => return false,
157 };
158
159 let source_meta = match fs::metadata(source) {
161 Ok(m) => m,
162 Err(_) => return false,
163 };
164
165 match (source_meta.modified(), binary_meta.modified()) {
167 (Ok(src_time), Ok(bin_time)) => bin_time >= src_time,
168 _ => false,
169 }
170}
171
172pub fn get_manifest(config: &BinaryConfig) -> std::io::Result<Vec<(String, PathBuf)>> {
174 let mut manifest = Vec::new();
175
176 if !config.output_dir.exists() {
177 return Ok(manifest);
178 }
179
180 for entry in fs::read_dir(&config.output_dir)? {
182 let entry = entry?;
183 let path = entry.path();
184
185 if path.is_dir() {
186 let hash = path.file_name().map(|s| s.to_string_lossy().to_string());
188
189 for file_entry in fs::read_dir(&path)? {
190 let file_entry = file_entry?;
191 let file_path = file_entry.path();
192
193 if file_path.extension().map(|e| e == "dx").unwrap_or(false) {
194 if let Some(h) = &hash {
195 manifest.push((h.clone(), file_path));
196 }
197 }
198 }
199 }
200 }
201
202 Ok(manifest)
203}
204
205pub fn clean_stale(config: &BinaryConfig) -> std::io::Result<usize> {
207 let mut cleaned = 0;
208
209 if !config.output_dir.exists() {
210 return Ok(0);
211 }
212
213 for entry in fs::read_dir(&config.output_dir)? {
215 let entry = entry?;
216 let path = entry.path();
217
218 if path.is_dir() {
219 let mut is_empty = true;
221
222 for file_entry in fs::read_dir(&path)? {
223 let file_entry = file_entry?;
224 let file_path = file_entry.path();
225
226 if file_path.extension().map(|e| e == "dx").unwrap_or(false) {
227 is_empty = false;
230 }
231 }
232
233 if is_empty {
234 fs::remove_dir(&path)?;
235 cleaned += 1;
236 }
237 }
238 }
239
240 Ok(cleaned)
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_hash_path_consistency() {
249 let hash1 = hash_path("config.dx");
250 let hash2 = hash_path("config.dx");
251 assert_eq!(hash1, hash2);
252 }
253
254 #[test]
255 fn test_hash_path_uniqueness() {
256 let hash1 = hash_path("config.dx");
257 let hash2 = hash_path("src/config.dx");
258 let hash3 = hash_path("lib/config.dx");
259
260 assert_ne!(hash1, hash2);
261 assert_ne!(hash2, hash3);
262 assert_ne!(hash1, hash3);
263 }
264
265 #[test]
266 fn test_get_binary_path() {
267 let config = BinaryConfig::with_root("/project");
268 let path = get_binary_path("/project/config.dx", &config);
269
270 assert!(path.to_string_lossy().contains(".dx/serializer"));
271 assert!(path.to_string_lossy().ends_with(".dx"));
272 }
273
274 #[test]
275 fn test_binary_path_different_dirs() {
276 let config = BinaryConfig::with_root("/project");
277
278 let path1 = get_binary_path("/project/config.dx", &config);
279 let path2 = get_binary_path("/project/src/config.dx", &config);
280
281 assert_ne!(path1, path2);
283 assert!(path1.file_name() == path2.file_name()); }
285}