1use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8pub use zoe_encrypted_storage::{CompressionConfig, ConvergentEncryptionInfo};
10
11use crate::Metadata;
12
13pub mod image;
14
15pub use image::Image;
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct FileRef {
24 pub blob_hash: String,
29
30 pub encryption_info: ConvergentEncryptionInfo,
35
36 pub filename: Option<String>,
42
43 pub content_type: Option<String>,
48
49 pub metadata: Vec<Metadata>,
55}
56
57impl FileRef {
58 pub fn new(
60 blob_hash: String,
61 encryption_info: ConvergentEncryptionInfo,
62 filename: Option<String>,
63 ) -> Self {
64 Self {
65 blob_hash,
66 encryption_info,
67 filename,
68 content_type: None,
69 metadata: vec![],
70 }
71 }
72
73 pub fn with_metadata(mut self, key: String, value: String) -> Self {
75 self.metadata.push(Metadata::Generic { key, value });
76 self
77 }
78
79 pub fn with_structured_metadata(mut self, metadata: Metadata) -> Self {
81 self.metadata.push(metadata);
82 self
83 }
84
85 pub fn generic_metadata(&self) -> BTreeMap<String, String> {
91 self.metadata
92 .iter()
93 .filter_map(|meta| match meta {
94 Metadata::Generic { key, value } => Some((key.clone(), value.clone())),
95 _ => None,
96 })
97 .collect()
98 }
99
100 pub fn with_content_type(mut self, content_type: String) -> Self {
102 self.content_type = Some(content_type);
103 self
104 }
105
106 pub fn filename(&self) -> Option<&str> {
108 self.filename.as_deref()
109 }
110
111 pub fn file_extension(&self) -> Option<String> {
113 self.filename.as_ref().and_then(|filename| {
114 std::path::Path::new(filename)
115 .extension()
116 .and_then(|ext| ext.to_str())
117 .map(|s| s.to_string())
118 })
119 }
120
121 pub fn original_size(&self) -> usize {
123 self.encryption_info.source_size
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 use zoe_encrypted_storage::{CompressionConfig, ConvergentEncryptionInfo};
132
133 fn create_test_encryption_info() -> ConvergentEncryptionInfo {
134 ConvergentEncryptionInfo {
135 key: [42u8; 32],
136 was_compressed: false,
137 source_size: 1024,
138 }
139 }
140
141 fn create_test_file_ref() -> FileRef {
142 FileRef::new(
143 "test_blob_hash_123".to_string(),
144 create_test_encryption_info(),
145 Some("test_file.txt".to_string()),
146 )
147 }
148
149 #[test]
150 fn test_file_ref_new() {
151 let blob_hash = "test_hash_123".to_string();
152 let encryption_info = create_test_encryption_info();
153 let filename = Some("document.pdf".to_string());
154
155 let file_ref = FileRef::new(blob_hash.clone(), encryption_info.clone(), filename.clone());
156
157 assert_eq!(file_ref.blob_hash, blob_hash);
158 assert_eq!(file_ref.encryption_info, encryption_info);
159 assert_eq!(file_ref.filename, filename);
160 assert_eq!(file_ref.content_type, None);
161 assert!(file_ref.metadata.is_empty());
162 }
163
164 #[test]
165 fn test_file_ref_with_metadata() {
166 let file_ref = create_test_file_ref()
167 .with_metadata("author".to_string(), "alice".to_string())
168 .with_metadata("category".to_string(), "documents".to_string());
169
170 let generic_meta = file_ref.generic_metadata();
171 assert_eq!(generic_meta.get("author"), Some(&"alice".to_string()));
172 assert_eq!(generic_meta.get("category"), Some(&"documents".to_string()));
173 assert_eq!(file_ref.metadata.len(), 2);
174 }
175
176 #[test]
177 fn test_file_ref_with_content_type() {
178 let file_ref = create_test_file_ref().with_content_type("application/pdf".to_string());
179
180 assert_eq!(file_ref.content_type, Some("application/pdf".to_string()));
181 }
182
183 #[test]
184 fn test_file_ref_filename() {
185 let file_ref_with_name = create_test_file_ref();
187 assert_eq!(file_ref_with_name.filename(), Some("test_file.txt"));
188
189 let file_ref_no_name =
191 FileRef::new("hash".to_string(), create_test_encryption_info(), None);
192 assert_eq!(file_ref_no_name.filename(), None);
193 }
194
195 #[test]
196 fn test_file_ref_file_extension() {
197 let file_ref_txt = FileRef::new(
199 "hash".to_string(),
200 create_test_encryption_info(),
201 Some("document.txt".to_string()),
202 );
203 assert_eq!(file_ref_txt.file_extension(), Some("txt".to_string()));
204
205 let file_ref_tar_gz = FileRef::new(
207 "hash".to_string(),
208 create_test_encryption_info(),
209 Some("archive.tar.gz".to_string()),
210 );
211 assert_eq!(file_ref_tar_gz.file_extension(), Some("gz".to_string()));
212
213 let file_ref_no_ext = FileRef::new(
215 "hash".to_string(),
216 create_test_encryption_info(),
217 Some("README".to_string()),
218 );
219 assert_eq!(file_ref_no_ext.file_extension(), None);
220
221 let file_ref_no_name =
223 FileRef::new("hash".to_string(), create_test_encryption_info(), None);
224 assert_eq!(file_ref_no_name.file_extension(), None);
225
226 let file_ref_hidden = FileRef::new(
228 "hash".to_string(),
229 create_test_encryption_info(),
230 Some(".hidden".to_string()),
231 );
232 assert_eq!(file_ref_hidden.file_extension(), None);
233
234 let file_ref_hidden_with_ext = FileRef::new(
236 "hash".to_string(),
237 create_test_encryption_info(),
238 Some(".hidden.txt".to_string()),
239 );
240 assert_eq!(
241 file_ref_hidden_with_ext.file_extension(),
242 Some("txt".to_string())
243 );
244 }
245
246 #[test]
247 fn test_file_ref_original_size() {
248 let encryption_info = ConvergentEncryptionInfo {
249 key: [0u8; 32],
250 was_compressed: false,
251 source_size: 2048,
252 };
253
254 let file_ref = FileRef::new(
255 "hash".to_string(),
256 encryption_info,
257 Some("large_file.bin".to_string()),
258 );
259
260 assert_eq!(file_ref.original_size(), 2048);
261 }
262
263 #[test]
264 fn test_file_ref_builder_pattern() {
265 let file_ref = FileRef::new(
266 "test_hash".to_string(),
267 create_test_encryption_info(),
268 Some("image.png".to_string()),
269 )
270 .with_content_type("image/png".to_string())
271 .with_metadata("width".to_string(), "1920".to_string())
272 .with_metadata("height".to_string(), "1080".to_string());
273
274 assert_eq!(file_ref.content_type, Some("image/png".to_string()));
275 let generic_meta = file_ref.generic_metadata();
276 assert_eq!(generic_meta.get("width"), Some(&"1920".to_string()));
277 assert_eq!(generic_meta.get("height"), Some(&"1080".to_string()));
278 }
279
280 #[test]
281 fn test_postcard_serialization_file_ref() {
282 let file_ref = create_test_file_ref()
283 .with_content_type("text/plain".to_string())
284 .with_metadata("test".to_string(), "value".to_string());
285
286 let serialized = postcard::to_stdvec(&file_ref).expect("Failed to serialize");
287 let deserialized: FileRef =
288 postcard::from_bytes(&serialized).expect("Failed to deserialize");
289
290 assert_eq!(file_ref, deserialized);
291 }
292
293 #[test]
294 fn test_postcard_serialization_convergent_encryption_info() {
295 let encryption_info = create_test_encryption_info();
296
297 let serialized = postcard::to_stdvec(&encryption_info).expect("Failed to serialize");
298 let deserialized: ConvergentEncryptionInfo =
299 postcard::from_bytes(&serialized).expect("Failed to deserialize");
300
301 assert_eq!(encryption_info, deserialized);
302 }
303
304 #[test]
305 fn test_postcard_serialization_compression_config() {
306 let configs = [
307 CompressionConfig::default(),
308 CompressionConfig {
309 enabled: false,
310 quality: 1,
311 min_size: 0,
312 },
313 CompressionConfig {
314 enabled: true,
315 quality: 11,
316 min_size: 1024,
317 },
318 ];
319
320 for compression in configs {
321 let serialized = postcard::to_stdvec(&compression).expect("Failed to serialize");
322 let deserialized: CompressionConfig =
323 postcard::from_bytes(&serialized).expect("Failed to deserialize");
324 assert_eq!(compression.enabled, deserialized.enabled);
325 assert_eq!(compression.quality, deserialized.quality);
326 assert_eq!(compression.min_size, deserialized.min_size);
327 }
328 }
329
330 #[test]
331 fn test_file_ref_with_different_compression_states() {
332 let encryption_info_compressed = ConvergentEncryptionInfo {
334 key: [1u8; 32],
335 was_compressed: true,
336 source_size: 512,
337 };
338
339 let file_ref_compressed = FileRef::new(
340 "compressed_hash".to_string(),
341 encryption_info_compressed,
342 Some("compressed.txt".to_string()),
343 );
344
345 assert_eq!(file_ref_compressed.original_size(), 512);
346 assert!(file_ref_compressed.encryption_info.was_compressed);
347
348 let encryption_info_uncompressed = ConvergentEncryptionInfo {
350 key: [2u8; 32],
351 was_compressed: false,
352 source_size: 1024,
353 };
354
355 let file_ref_uncompressed = FileRef::new(
356 "uncompressed_hash".to_string(),
357 encryption_info_uncompressed,
358 Some("uncompressed.bin".to_string()),
359 );
360
361 assert_eq!(file_ref_uncompressed.original_size(), 1024);
362 assert!(!file_ref_uncompressed.encryption_info.was_compressed);
363 }
364
365 #[test]
366 fn test_file_ref_metadata_operations() {
367 use crate::Metadata;
368
369 let metadata = vec![Metadata::Generic {
370 key: "initial".to_string(),
371 value: "value".to_string(),
372 }];
373
374 let file_ref = FileRef {
375 blob_hash: "test".to_string(),
376 encryption_info: create_test_encryption_info(),
377 filename: None,
378 content_type: None,
379 metadata,
380 };
381
382 let generic_meta = file_ref.generic_metadata();
384 assert_eq!(generic_meta.get("initial"), Some(&"value".to_string()));
385
386 let updated_file_ref =
388 file_ref.with_metadata("new_key".to_string(), "new_value".to_string());
389
390 let updated_generic_meta = updated_file_ref.generic_metadata();
391 assert_eq!(
392 updated_generic_meta.get("initial"),
393 Some(&"value".to_string())
394 );
395 assert_eq!(
396 updated_generic_meta.get("new_key"),
397 Some(&"new_value".to_string())
398 );
399 }
400}