1use crate::{Error, Result, Storage};
2use std::path::{Path, PathBuf};
3
4pub struct FilePerEvent {
5 directory: PathBuf,
6 extension: String,
7}
8
9impl FilePerEvent {
10 pub fn new(directory: impl AsRef<Path>, extension: impl Into<String>) -> Result<Self> {
11 let directory = directory.as_ref().to_path_buf();
12 let extension = extension.into();
13
14 if !directory.exists() {
16 std::fs::create_dir_all(&directory)
17 .map_err(|e| Error::Storage(format!("Failed to create directory: {}", e)))?;
18 }
19
20 Ok(Self {
21 directory,
22 extension,
23 })
24 }
25}
26
27impl Storage for FilePerEvent {
28 fn save(&mut self, key: &str, data: Vec<u8>) -> Result<()> {
29 let path = self.key_to_filename(key)?;
30
31 let temp_path = path.with_extension(format!(
34 "tmp.{}.{}",
35 std::process::id(),
36 std::time::SystemTime::now()
37 .duration_since(std::time::UNIX_EPOCH)
38 .unwrap()
39 .as_nanos()
40 ));
41
42 if let Err(e) = std::fs::write(&temp_path, data) {
44 let _ = std::fs::remove_file(&temp_path);
46 return Err(Error::Storage(format!("Failed to write file: {}", e)));
47 }
48
49 std::fs::rename(&temp_path, &path).map_err(|e| {
51 let _ = std::fs::remove_file(&temp_path);
53 Error::Storage(format!("Failed to rename temp file: {}", e))
54 })?;
55
56 Ok(())
57 }
58
59 fn load(&self, key: &str) -> Result<Option<Vec<u8>>> {
60 let path = self.key_to_filename(key)?;
61
62 if !path.exists() {
63 return Ok(None);
64 }
65
66 let data = std::fs::read(&path)
67 .map_err(|e| Error::Storage(format!("Failed to read file: {}", e)))?;
68 Ok(Some(data))
69 }
70
71 fn delete(&mut self, key: &str) -> Result<()> {
72 let path = self.key_to_filename(key)?;
73
74 if path.exists() {
75 std::fs::remove_file(&path)
76 .map_err(|e| Error::Storage(format!("Failed to delete file: {}", e)))?;
77 }
78
79 Ok(())
80 }
81
82 fn list_keys(&self) -> Result<Vec<String>> {
83 let mut keys = Vec::new();
84
85 let entries = std::fs::read_dir(&self.directory)
86 .map_err(|e| Error::Storage(format!("Failed to read directory: {}", e)))?;
87
88 for entry in entries {
89 let entry = entry
90 .map_err(|e| Error::Storage(format!("Failed to read directory entry: {}", e)))?;
91 let path = entry.path();
92
93 if !path.is_file() {
95 continue;
96 }
97
98 if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
100 let key = Self::decode_key(filename)?;
102 keys.push(key);
103 }
104 }
105
106 Ok(keys)
107 }
108}
109
110impl FilePerEvent {
111 fn encode_key(key: &str) -> String {
112 let mut result = String::new();
113 for c in key.chars() {
114 match c {
115 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
116 result.push(c);
117 }
118 _ => {
119 let mut buf = [0u8; 4];
120 let bytes = c.encode_utf8(&mut buf).as_bytes();
121 for &byte in bytes {
122 result.push_str(&format!("%{:02X}", byte));
123 }
124 }
125 }
126 }
127 result
128 }
129
130 fn decode_key(encoded: &str) -> Result<String> {
131 let mut bytes = Vec::new();
132 let mut chars = encoded.chars();
133
134 while let Some(c) = chars.next() {
135 if c == '%' {
136 let hex: String = chars.by_ref().take(2).collect();
137 if hex.len() != 2 {
138 return Err(Error::Storage(format!(
139 "Invalid percent encoding: %{}",
140 hex
141 )));
142 }
143 let byte = u8::from_str_radix(&hex, 16)
144 .map_err(|e| Error::Storage(format!("Invalid hex in encoding: {}", e)))?;
145 bytes.push(byte);
146 } else {
147 for byte in c.to_string().as_bytes() {
149 bytes.push(*byte);
150 }
151 }
152 }
153
154 String::from_utf8(bytes)
155 .map_err(|e| Error::Storage(format!("Invalid UTF-8 in decoded key: {}", e)))
156 }
157
158 fn key_to_filename(&self, key: &str) -> Result<PathBuf> {
159 if key == "."
165 || key == ".."
166 || key.starts_with("../")
167 || key.starts_with("..\\")
168 || key.contains("/..")
169 || key.contains("\\..")
170 {
171 return Err(Error::Storage(
172 "Invalid key: key cannot contain '.' or '..' path segments".to_string(),
173 ));
174 }
175
176 let encoded = Self::encode_key(key);
179
180 let filename = format!("{}{}", encoded, self.extension);
182 let path = self.directory.join(&filename);
183
184 let canonical_dir = self
189 .directory
190 .canonicalize()
191 .map_err(|e| Error::Storage(format!("Failed to canonicalize directory: {}", e)))?;
192
193 let canonical_path = match path.canonicalize() {
194 Ok(p) => p,
195 Err(_) => {
196 let parent = path
198 .parent()
199 .ok_or_else(|| Error::Storage("Path has no parent".to_string()))?;
200 let parent_canonical = parent
201 .canonicalize()
202 .map_err(|e| Error::Storage(format!("Failed to canonicalize parent: {}", e)))?;
203 parent_canonical.join(
204 path.file_name()
205 .ok_or_else(|| Error::Storage("Path has no filename".to_string()))?,
206 )
207 }
208 };
209
210 if !canonical_path.starts_with(&canonical_dir) {
212 return Err(Error::Storage(
213 "Invalid key: path escapes intended directory".to_string(),
214 ));
215 }
216
217 Ok(path)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use tempfile::TempDir;
225
226 fn temp_dir() -> TempDir {
227 TempDir::new().unwrap()
228 }
229
230 #[test]
231 fn test_encode_simple_key() {
232 assert_eq!(FilePerEvent::encode_key("simple"), "simple");
233 }
234
235 #[test]
236 fn test_encode_key_with_colon() {
237 assert_eq!(FilePerEvent::encode_key("user:123"), "user%3A123");
238 }
239
240 #[test]
241 fn test_encode_key_with_slash() {
242 assert_eq!(FilePerEvent::encode_key("api/endpoint"), "api%2Fendpoint");
243 }
244
245 #[test]
246 fn test_encode_decode_roundtrip() {
247 let keys = vec![
248 "simple",
249 "user:123",
250 "api/endpoint",
251 "key with spaces",
252 "special!@#$chars",
253 ];
254
255 for key in keys {
256 let encoded = FilePerEvent::encode_key(key);
257 let decoded = FilePerEvent::decode_key(&encoded).unwrap();
258 assert_eq!(decoded, key);
259 }
260 }
261
262 #[test]
263 fn test_encode_unicode_roundtrip() {
264 let keys = vec!["emoji👍test", "中文测试", "Ñoño", "café☕"];
265 for key in keys {
266 let encoded = FilePerEvent::encode_key(key);
267 let decoded = FilePerEvent::decode_key(&encoded).unwrap();
268 assert_eq!(decoded, key, "Failed for key: {}", key);
269 }
270 }
271
272 #[test]
273 fn test_new_creates_directory_if_missing() {
274 let dir = temp_dir();
275 let storage_dir = dir.path().join("events");
276 assert!(!storage_dir.exists());
277
278 let _storage = FilePerEvent::new(&storage_dir, ".dat").unwrap();
279 assert!(storage_dir.exists());
280 }
281
282 #[test]
283 fn test_save_and_load_roundtrip() {
284 let dir = temp_dir();
285 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
286
287 let data = vec![1, 2, 3, 4, 5];
288 storage.save("test_key", data.clone()).unwrap();
289
290 let loaded = storage.load("test_key").unwrap();
291 assert_eq!(loaded, Some(data));
292 }
293
294 #[test]
295 fn test_load_nonexistent_returns_none() {
296 let dir = temp_dir();
297 let storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
298
299 let loaded = storage.load("nonexistent").unwrap();
300 assert_eq!(loaded, None);
301 }
302
303 #[test]
304 fn test_delete_removes_file() {
305 let dir = temp_dir();
306 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
307
308 storage.save("test", vec![1, 2, 3]).unwrap();
309 storage.delete("test").unwrap();
310
311 let loaded = storage.load("test").unwrap();
312 assert_eq!(loaded, None);
313 }
314
315 #[test]
316 fn test_list_keys_returns_all_keys() {
317 let dir = temp_dir();
318 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
319
320 storage.save("key1", vec![1]).unwrap();
321 storage.save("key2", vec![2]).unwrap();
322 storage.save("key3", vec![3]).unwrap();
323
324 let mut keys = storage.list_keys().unwrap();
325 keys.sort();
326 assert_eq!(keys, vec!["key1", "key2", "key3"]);
327 }
328
329 #[test]
330 fn test_keys_with_special_characters() {
331 let dir = temp_dir();
332 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
333
334 let special_keys = vec![
335 "user:123",
336 "event:type:subtype",
337 "key with spaces",
338 "special!@#$%^&*()chars",
339 "api/endpoint", ];
341
342 for key in &special_keys {
343 storage.save(key, vec![1, 2, 3]).unwrap();
344 }
345
346 let mut loaded_keys = storage.list_keys().unwrap();
347 loaded_keys.sort();
348 let mut expected = special_keys.clone();
349 expected.sort();
350 assert_eq!(loaded_keys, expected);
351 }
352
353 #[test]
354 fn test_overwrite_existing_key() {
355 let dir = temp_dir();
356 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
357
358 storage.save("test", vec![1, 2, 3]).unwrap();
359 storage.save("test", vec![4, 5, 6]).unwrap();
360
361 let loaded = storage.load("test").unwrap();
362 assert_eq!(loaded, Some(vec![4, 5, 6]));
363 }
364
365 #[test]
366 fn test_extension_in_filename() {
367 let dir = temp_dir();
368 let mut storage = FilePerEvent::new(dir.path(), ".json").unwrap();
369
370 storage.save("test", vec![1, 2, 3]).unwrap();
371
372 let files: Vec<_> = std::fs::read_dir(dir.path())
374 .unwrap()
375 .filter_map(|e| e.ok())
376 .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("json"))
377 .collect();
378
379 assert_eq!(files.len(), 1);
380 }
381
382 #[test]
383 fn test_persistence_across_instances() {
384 let dir = temp_dir();
385
386 {
387 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
388 storage.save("key1", vec![1, 2, 3]).unwrap();
389 storage.save("key2", vec![4, 5, 6]).unwrap();
390 }
391
392 {
393 let storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
394 assert_eq!(storage.load("key1").unwrap(), Some(vec![1, 2, 3]));
395 assert_eq!(storage.load("key2").unwrap(), Some(vec![4, 5, 6]));
396 }
397 }
398
399 #[test]
402 fn test_path_traversal_relative_parent() {
403 let dir = temp_dir();
404 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
405
406 let result = storage.save("../../etc/passwd", vec![1, 2, 3]);
408 assert!(result.is_err());
409 assert!(result
410 .unwrap_err()
411 .to_string()
412 .contains("Invalid key: key cannot contain '.' or '..' path segments"));
413 }
414
415 #[test]
416 fn test_path_traversal_absolute_path() {
417 let dir = temp_dir();
418 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
419
420 let result = storage.save("/absolute/path", vec![1, 2, 3]);
422 assert!(result.is_ok());
423
424 let loaded = storage.load("/absolute/path").unwrap();
426 assert_eq!(loaded, Some(vec![1, 2, 3]));
427
428 let encoded_filename = "%2Fabsolute%2Fpath.dat".to_string();
430 let file_path = dir.path().join(&encoded_filename);
431 assert!(file_path.exists(), "File should exist with encoded slashes");
432 }
433
434 #[test]
435 fn test_path_traversal_windows_style() {
436 let dir = temp_dir();
437 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
438
439 let result = storage.save("windows\\path", vec![1, 2, 3]);
441 assert!(result.is_ok());
442
443 let loaded = storage.load("windows\\path").unwrap();
445 assert_eq!(loaded, Some(vec![1, 2, 3]));
446
447 let encoded_filename = "windows%5Cpath.dat".to_string();
449 let file_path = dir.path().join(&encoded_filename);
450 assert!(
451 file_path.exists(),
452 "File should exist with encoded filename"
453 );
454
455 let result = storage.save("..\\..\\windows\\system32", vec![1, 2, 3]);
457 assert!(result.is_err());
458 assert!(result
459 .unwrap_err()
460 .to_string()
461 .contains("Invalid key: key cannot contain '.' or '..' path segments"));
462 }
463
464 #[test]
465 fn test_path_traversal_embedded_slash() {
466 let dir = temp_dir();
467 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
468
469 let result = storage.save("normal/key", vec![1, 2, 3]);
471 assert!(result.is_ok());
472
473 let loaded = storage.load("normal/key").unwrap();
475 assert_eq!(loaded, Some(vec![1, 2, 3]));
476
477 let encoded_filename = "normal%2Fkey.dat".to_string();
479 let file_path = dir.path().join(&encoded_filename);
480 assert!(
481 file_path.exists(),
482 "File should exist with encoded filename"
483 );
484 }
485
486 #[test]
487 fn test_path_traversal_parent_reference() {
488 let dir = temp_dir();
489 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
490
491 let result = storage.save("..", vec![1, 2, 3]);
493 assert!(result.is_err());
494 assert!(result
495 .unwrap_err()
496 .to_string()
497 .contains("Invalid key: key cannot contain '.' or '..' path segments"));
498 }
499
500 #[test]
501 fn test_path_traversal_current_directory() {
502 let dir = temp_dir();
503 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
504
505 let result = storage.save(".", vec![1, 2, 3]);
507 assert!(result.is_err());
508 assert!(result
509 .unwrap_err()
510 .to_string()
511 .contains("Invalid key: key cannot contain '.' or '..' path segments"));
512 }
513
514 #[test]
515 fn test_path_traversal_url_encoded_slash() {
516 let dir = temp_dir();
517 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
518
519 let result = storage.save("test%2Ffile", vec![1, 2, 3]);
522 assert!(result.is_ok());
523
524 let loaded = storage.load("test%2Ffile").unwrap();
526 assert_eq!(loaded, Some(vec![1, 2, 3]));
527
528 let encoded_filename = "test%252Ffile.dat".to_string();
530 let file_path = dir.path().join(&encoded_filename);
531 assert!(
532 file_path.exists(),
533 "File should exist with double-encoded filename"
534 );
535 }
536
537 #[test]
538 fn test_path_traversal_url_encoded_backslash() {
539 let dir = temp_dir();
540 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
541
542 let result = storage.save("test%5Cfile", vec![1, 2, 3]);
545 assert!(result.is_ok());
546
547 let loaded = storage.load("test%5Cfile").unwrap();
549 assert_eq!(loaded, Some(vec![1, 2, 3]));
550
551 let encoded_filename = "test%255Cfile.dat".to_string();
553 let file_path = dir.path().join(&encoded_filename);
554 assert!(
555 file_path.exists(),
556 "File should exist with double-encoded filename"
557 );
558 }
559
560 #[test]
561 fn test_legitimate_key_simple() {
562 let dir = temp_dir();
563 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
564
565 let result = storage.save("event1", vec![1, 2, 3]);
567 assert!(result.is_ok());
568
569 let loaded = storage.load("event1").unwrap();
570 assert_eq!(loaded, Some(vec![1, 2, 3]));
571 }
572
573 #[test]
574 fn test_legitimate_key_with_colon() {
575 let dir = temp_dir();
576 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
577
578 let result = storage.save("user:login", vec![1, 2, 3]);
580 assert!(result.is_ok());
581
582 let loaded = storage.load("user:login").unwrap();
583 assert_eq!(loaded, Some(vec![1, 2, 3]));
584 }
585
586 #[test]
587 fn test_legitimate_key_with_spaces() {
588 let dir = temp_dir();
589 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
590
591 let result = storage.save("user login event", vec![1, 2, 3]);
593 assert!(result.is_ok());
594
595 let loaded = storage.load("user login event").unwrap();
596 assert_eq!(loaded, Some(vec![1, 2, 3]));
597 }
598
599 #[test]
600 fn test_legitimate_key_with_special_chars() {
601 let dir = temp_dir();
602 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
603
604 let result = storage.save("event!@#$%^&*()", vec![1, 2, 3]);
606 assert!(result.is_ok());
607
608 let loaded = storage.load("event!@#$%^&*()").unwrap();
609 assert_eq!(loaded, Some(vec![1, 2, 3]));
610 }
611
612 #[test]
613 fn test_load_with_invalid_key() {
614 let dir = temp_dir();
615 let storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
616
617 let result = storage.load("../../etc/passwd");
619 assert!(result.is_err());
620 assert!(result
621 .unwrap_err()
622 .to_string()
623 .contains("Invalid key: key cannot contain '.' or '..' path segments"));
624 }
625
626 #[test]
627 fn test_delete_with_invalid_key() {
628 let dir = temp_dir();
629 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
630
631 let result = storage.delete("../../etc/passwd");
633 assert!(result.is_err());
634 assert!(result
635 .unwrap_err()
636 .to_string()
637 .contains("Invalid key: key cannot contain '.' or '..' path segments"));
638 }
639
640 #[test]
641 fn test_key_with_forward_slash() {
642 let dir = temp_dir();
643 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
644
645 let key = "api/endpoint";
647 let data = vec![1, 2, 3, 4, 5];
648
649 storage.save(key, data.clone()).unwrap();
651
652 let loaded = storage.load(key).unwrap();
654 assert_eq!(loaded, Some(data.clone()));
655
656 let encoded_filename = "api%2Fendpoint.dat".to_string();
658 let file_path = dir.path().join(&encoded_filename);
659 assert!(
660 file_path.exists(),
661 "File should exist with %2F for forward slash"
662 );
663
664 let keys = storage.list_keys().unwrap();
666 assert!(keys.contains(&key.to_string()));
667
668 storage.delete(key).unwrap();
670 assert_eq!(storage.load(key).unwrap(), None);
671 }
672
673 #[test]
674 fn test_key_with_backslash() {
675 let dir = temp_dir();
676 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
677
678 let key = "path\\to\\resource";
680 let data = vec![6, 7, 8, 9, 10];
681
682 storage.save(key, data.clone()).unwrap();
684
685 let loaded = storage.load(key).unwrap();
687 assert_eq!(loaded, Some(data.clone()));
688
689 let encoded_filename = "path%5Cto%5Cresource.dat".to_string();
691 let file_path = dir.path().join(&encoded_filename);
692 assert!(
693 file_path.exists(),
694 "File should exist with %5C for backslash"
695 );
696
697 let keys = storage.list_keys().unwrap();
699 assert!(keys.contains(&key.to_string()));
700
701 storage.delete(key).unwrap();
703 assert_eq!(storage.load(key).unwrap(), None);
704 }
705
706 #[test]
707 fn test_atomic_write_no_temp_files_left() {
708 let dir = temp_dir();
709 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
710
711 storage.save("key1", vec![1, 2, 3]).unwrap();
713 storage.save("key2", vec![4, 5, 6]).unwrap();
714 storage.save("key3", vec![7, 8, 9]).unwrap();
715
716 let entries = std::fs::read_dir(dir.path()).unwrap();
718 for entry in entries {
719 let entry = entry.unwrap();
720 let path = entry.path();
721 if let Some(ext) = path.extension() {
722 let ext_str = ext.to_string_lossy();
723 assert!(
724 !ext_str.starts_with("tmp."),
725 "Found leftover temp file: {:?}",
726 path
727 );
728 }
729 }
730
731 assert_eq!(storage.load("key1").unwrap(), Some(vec![1, 2, 3]));
733 assert_eq!(storage.load("key2").unwrap(), Some(vec![4, 5, 6]));
734 assert_eq!(storage.load("key3").unwrap(), Some(vec![7, 8, 9]));
735 }
736
737 #[test]
738 fn test_atomic_write_overwrites_existing() {
739 let dir = temp_dir();
740 let mut storage = FilePerEvent::new(dir.path(), ".dat").unwrap();
741
742 storage.save("test", vec![1, 2, 3]).unwrap();
744 assert_eq!(storage.load("test").unwrap(), Some(vec![1, 2, 3]));
745
746 storage.save("test", vec![4, 5, 6, 7, 8]).unwrap();
748 assert_eq!(storage.load("test").unwrap(), Some(vec![4, 5, 6, 7, 8]));
749
750 let entries = std::fs::read_dir(dir.path()).unwrap();
752 let temp_files: Vec<_> = entries
753 .filter_map(|e| e.ok())
754 .filter(|e| {
755 e.path()
756 .extension()
757 .and_then(|ext| ext.to_str())
758 .map(|s| s.starts_with("tmp."))
759 .unwrap_or(false)
760 })
761 .collect();
762
763 assert_eq!(temp_files.len(), 0, "Found {} temp files", temp_files.len());
764 }
765}