1use std::collections::{HashMap, HashSet, VecDeque};
59use std::fs;
60use std::io;
61use std::path::{Path, PathBuf};
62use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
63use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
64use std::time::Instant;
65
66fn recover_read_guard<'a, T>(lock: &'a RwLock<T>) -> RwLockReadGuard<'a, T> {
67 match lock.read() {
68 Ok(guard) => guard,
69 Err(poisoned) => poisoned.into_inner(),
70 }
71}
72
73fn recover_write_guard<'a, T>(lock: &'a RwLock<T>) -> RwLockWriteGuard<'a, T> {
74 match lock.write() {
75 Ok(guard) => guard,
76 Err(poisoned) => poisoned.into_inner(),
77 }
78}
79
80fn spill_lock_error(context: &'static str) -> SpillError {
81 SpillError::Io(io::Error::other(format!("{context} lock poisoned")))
82}
83
84fn read_guard_or_err<'a, T>(
85 lock: &'a RwLock<T>,
86 context: &'static str,
87) -> Result<RwLockReadGuard<'a, T>, SpillError> {
88 lock.read().map_err(|_| spill_lock_error(context))
89}
90
91fn write_guard_or_err<'a, T>(
92 lock: &'a RwLock<T>,
93 context: &'static str,
94) -> Result<RwLockWriteGuard<'a, T>, SpillError> {
95 lock.write().map_err(|_| spill_lock_error(context))
96}
97
98#[derive(Debug, Clone)]
104pub struct SpillConfig {
105 pub max_memory: usize,
107 pub spill_threshold: f64,
109 pub spill_dir: PathBuf,
111 pub target_after_spill: f64,
113 pub min_spill_size: usize,
115 pub access_decay: f64,
117}
118
119impl SpillConfig {
120 pub fn new() -> Self {
122 Self {
123 max_memory: 512 * 1024 * 1024, spill_threshold: 0.80, spill_dir: reddb_file::default_spill_dir(),
126 target_after_spill: 0.60, min_spill_size: 1024 * 1024, access_decay: 0.95, }
130 }
131
132 pub fn max_memory(mut self, bytes: usize) -> Self {
134 self.max_memory = bytes;
135 self
136 }
137
138 pub fn spill_threshold(mut self, threshold: f64) -> Self {
140 self.spill_threshold = threshold.clamp(0.1, 0.99);
141 self
142 }
143
144 pub fn spill_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
146 self.spill_dir = path.as_ref().to_path_buf();
147 self
148 }
149
150 pub fn target_after_spill(mut self, target: f64) -> Self {
152 self.target_after_spill = target.clamp(0.1, 0.9);
153 self
154 }
155
156 pub fn min_spill_size(mut self, size: usize) -> Self {
158 self.min_spill_size = size;
159 self
160 }
161}
162
163impl Default for SpillConfig {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169#[derive(Debug)]
175struct SegmentInfo {
176 name: String,
178 size: AtomicUsize,
180 access_score: AtomicU64,
182 access_count: AtomicU64,
184 last_access: RwLock<Instant>,
186 is_spilled: RwLock<bool>,
188 spill_path: RwLock<Option<PathBuf>>,
190}
191
192impl SegmentInfo {
193 fn new(name: String, size: usize) -> Self {
194 Self {
195 name,
196 size: AtomicUsize::new(size),
197 access_score: AtomicU64::new(100), access_count: AtomicU64::new(0),
199 last_access: RwLock::new(Instant::now()),
200 is_spilled: RwLock::new(false),
201 spill_path: RwLock::new(None),
202 }
203 }
204
205 fn touch(&self) {
206 self.access_count.fetch_add(1, Ordering::Relaxed);
207 self.access_score.fetch_add(10, Ordering::Relaxed);
209 *recover_write_guard(&self.last_access) = Instant::now();
210 }
211
212 fn decay_score(&self, factor: f64) {
213 let current = self.access_score.load(Ordering::Relaxed);
214 let new = (current as f64 * factor) as u64;
215 self.access_score.store(new.max(1), Ordering::Relaxed);
216 }
217
218 fn coldness_score(&self) -> u64 {
219 let access = self.access_score.load(Ordering::Relaxed).max(1);
222 let size = self.size.load(Ordering::Relaxed) as u64;
223
224 size / access
226 }
227}
228
229#[derive(Debug, Clone, Default)]
235pub struct SpillStats {
236 pub current_memory: usize,
238 pub max_memory: usize,
240 pub segment_count: usize,
242 pub spilled_count: usize,
244 pub bytes_spilled: u64,
246 pub bytes_reloaded: u64,
248 pub spill_operations: u64,
250 pub reload_operations: u64,
252 pub disk_usage: u64,
254}
255
256impl SpillStats {
257 pub fn utilization(&self) -> f64 {
259 if self.max_memory == 0 {
260 0.0
261 } else {
262 self.current_memory as f64 / self.max_memory as f64
263 }
264 }
265
266 pub fn at_threshold(&self, threshold: f64) -> bool {
268 self.utilization() >= threshold
269 }
270}
271
272#[derive(Debug)]
278pub enum SpillError {
279 Io(io::Error),
281 SegmentNotFound(String),
283 NotSpilled(String),
285 AlreadySpilled(String),
287 DirectoryCreation(io::Error),
289 ChecksumMismatch,
291 InvalidName(String),
293}
294
295impl std::fmt::Display for SpillError {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 match self {
298 Self::Io(e) => write!(f, "IO error: {}", e),
299 Self::SegmentNotFound(s) => write!(f, "Segment not found: {}", s),
300 Self::NotSpilled(s) => write!(f, "Segment not spilled: {}", s),
301 Self::AlreadySpilled(s) => write!(f, "Segment already spilled: {}", s),
302 Self::DirectoryCreation(e) => write!(f, "Failed to create spill dir: {}", e),
303 Self::ChecksumMismatch => write!(f, "Checksum mismatch on reload"),
304 Self::InvalidName(s) => write!(f, "Invalid spill segment name: {}", s),
305 }
306 }
307}
308
309fn sanitize_spill_name(name: &str) -> Result<(), SpillError> {
311 if name.is_empty() || name.contains('/') || name.contains('\\') || name.contains("..") {
312 return Err(SpillError::InvalidName(name.to_string()));
313 }
314 Ok(())
315}
316
317impl std::error::Error for SpillError {}
318
319impl From<io::Error> for SpillError {
320 fn from(e: io::Error) -> Self {
321 Self::Io(e)
322 }
323}
324
325pub struct SpillManager {
327 config: SpillConfig,
329 segments: RwLock<HashMap<String, SegmentInfo>>,
331 current_memory: AtomicUsize,
333 stats: RwLock<SpillStats>,
335 access_history: RwLock<VecDeque<String>>,
337 spilled_segments: RwLock<HashSet<String>>,
339}
340
341impl SpillManager {
342 pub fn new(config: SpillConfig) -> Self {
344 let max_memory = config.max_memory;
345
346 Self {
347 config,
348 segments: RwLock::new(HashMap::new()),
349 current_memory: AtomicUsize::new(0),
350 stats: RwLock::new(SpillStats {
351 max_memory,
352 ..Default::default()
353 }),
354 access_history: RwLock::new(VecDeque::with_capacity(1000)),
355 spilled_segments: RwLock::new(HashSet::new()),
356 }
357 }
358
359 fn ensure_spill_dir(&self) -> Result<(), SpillError> {
361 if !self.config.spill_dir.exists() {
362 fs::create_dir_all(&self.config.spill_dir).map_err(SpillError::DirectoryCreation)?;
363 }
364 Ok(())
365 }
366
367 pub fn register_segment(&self, name: &str, size: usize) {
369 let info = SegmentInfo::new(name.to_string(), size);
370
371 {
372 let mut segments = recover_write_guard(&self.segments);
373 if let Some(old) = segments.get(name) {
375 let old_size = old.size.load(Ordering::Relaxed);
376 self.current_memory.fetch_sub(old_size, Ordering::Relaxed);
377 }
378
379 segments.insert(name.to_string(), info);
380 self.current_memory.fetch_add(size, Ordering::Relaxed);
381 }
382
383 self.update_stats();
384 }
385
386 pub fn unregister_segment(&self, name: &str) {
388 {
389 let mut segments = recover_write_guard(&self.segments);
390 if let Some(info) = segments.remove(name) {
391 let size = info.size.load(Ordering::Relaxed);
392 self.current_memory.fetch_sub(size, Ordering::Relaxed);
393
394 let path = recover_read_guard(&info.spill_path);
396 if let Some(p) = path.as_ref() {
397 let _ = fs::remove_file(p);
398 }
399 }
400 }
401
402 recover_write_guard(&self.spilled_segments).remove(name);
403
404 self.update_stats();
405 }
406
407 pub fn update_size(&self, name: &str, new_size: usize) {
409 {
410 let segments = recover_read_guard(&self.segments);
411 if let Some(info) = segments.get(name) {
412 let old_size = info.size.swap(new_size, Ordering::Relaxed);
413 if new_size > old_size {
414 self.current_memory
415 .fetch_add(new_size - old_size, Ordering::Relaxed);
416 } else {
417 self.current_memory
418 .fetch_sub(old_size - new_size, Ordering::Relaxed);
419 }
420 }
421 }
422 self.update_stats();
423 }
424
425 pub fn access(&self, name: &str) {
427 let segments = recover_read_guard(&self.segments);
428 if let Some(info) = segments.get(name) {
429 info.touch();
430 }
431
432 let mut history = recover_write_guard(&self.access_history);
434 history.push_back(name.to_string());
435 while history.len() > 10000 {
437 history.pop_front();
438 }
439 }
440
441 pub fn needs_spill(&self) -> Option<Vec<String>> {
443 let current = self.current_memory.load(Ordering::Relaxed);
444 let threshold = (self.config.max_memory as f64 * self.config.spill_threshold) as usize;
445
446 if current < threshold {
447 return None;
448 }
449
450 self.decay_all_scores();
452
453 let target = (self.config.max_memory as f64 * self.config.target_after_spill) as usize;
455 let to_free = current.saturating_sub(target);
456
457 if to_free == 0 {
458 return None;
459 }
460
461 let mut candidates: Vec<(String, u64, usize)> = Vec::new();
463
464 let segments = recover_read_guard(&self.segments);
465 for (name, info) in segments.iter() {
466 if *recover_read_guard(&info.is_spilled) {
468 continue;
469 }
470
471 let size = info.size.load(Ordering::Relaxed);
472 if size < self.config.min_spill_size {
473 continue;
474 }
475
476 let coldness = info.coldness_score();
477 candidates.push((name.clone(), coldness, size));
478 }
479
480 candidates.sort_by_key(|b| std::cmp::Reverse(b.1));
482
483 let mut freed = 0usize;
485 let mut to_spill = Vec::new();
486
487 for (name, _, size) in candidates {
488 if freed >= to_free {
489 break;
490 }
491 to_spill.push(name);
492 freed += size;
493 }
494
495 if to_spill.is_empty() {
496 None
497 } else {
498 Some(to_spill)
499 }
500 }
501
502 pub fn spill(&self, name: &str, data: &[u8]) -> Result<PathBuf, SpillError> {
504 self.ensure_spill_dir()?;
505
506 let segments = read_guard_or_err(&self.segments, "spill manager segments")?;
507
508 let info = segments
509 .get(name)
510 .ok_or_else(|| SpillError::SegmentNotFound(name.to_string()))?;
511
512 if *read_guard_or_err(&info.is_spilled, "spill manager segment flag")? {
514 return Err(SpillError::AlreadySpilled(name.to_string()));
515 }
516
517 sanitize_spill_name(name)?;
519
520 let filename = reddb_file::spill_file_name(name, std::process::id());
522 let path = self.config.spill_dir.join(&filename);
523
524 if !path.starts_with(&self.config.spill_dir) {
526 return Err(SpillError::InvalidName(name.to_string()));
527 }
528
529 fs::write(&path, reddb_file::encode_spill_file_frame(data))?;
530
531 drop(segments);
533
534 let segments = read_guard_or_err(&self.segments, "spill manager segments")?;
535 if let Some(info) = segments.get(name) {
536 *write_guard_or_err(&info.is_spilled, "spill manager segment flag")? = true;
537 *write_guard_or_err(&info.spill_path, "spill manager segment spill path")? =
538 Some(path.clone());
539 }
540
541 self.current_memory.fetch_sub(data.len(), Ordering::Relaxed);
543
544 write_guard_or_err(&self.spilled_segments, "spill manager spilled set")?
546 .insert(name.to_string());
547
548 let mut stats = write_guard_or_err(&self.stats, "spill manager stats")?;
550 stats.spill_operations += 1;
551 stats.bytes_spilled += data.len() as u64;
552 stats.spilled_count += 1;
553 stats.disk_usage += data.len() as u64;
554 drop(stats);
555
556 self.update_stats();
557
558 Ok(path)
559 }
560
561 pub fn reload(&self, name: &str) -> Result<Option<Vec<u8>>, SpillError> {
563 let segments = read_guard_or_err(&self.segments, "spill manager segments")?;
564
565 let info = segments
566 .get(name)
567 .ok_or_else(|| SpillError::SegmentNotFound(name.to_string()))?;
568
569 if !*read_guard_or_err(&info.is_spilled, "spill manager segment flag")? {
571 return Ok(None);
572 }
573
574 let path = info
575 .spill_path
576 .read()
577 .map_err(|_| spill_lock_error("spill manager segment spill path"))?
578 .clone()
579 .ok_or_else(|| SpillError::NotSpilled(name.to_string()))?;
580
581 let raw = fs::read(&path)?;
582 let data =
583 reddb_file::decode_spill_file_frame(&raw).map_err(|_| SpillError::ChecksumMismatch)?;
584
585 drop(segments);
587
588 let segments = read_guard_or_err(&self.segments, "spill manager segments")?;
589 if let Some(info) = segments.get(name) {
590 *write_guard_or_err(&info.is_spilled, "spill manager segment flag")? = false;
591 *write_guard_or_err(&info.spill_path, "spill manager segment spill path")? = None;
592 }
593
594 self.current_memory.fetch_add(data.len(), Ordering::Relaxed);
596
597 write_guard_or_err(&self.spilled_segments, "spill manager spilled set")?.remove(name);
599
600 let _ = fs::remove_file(&path);
602
603 let mut stats = write_guard_or_err(&self.stats, "spill manager stats")?;
605 stats.reload_operations += 1;
606 stats.bytes_reloaded += data.len() as u64;
607 stats.spilled_count = stats.spilled_count.saturating_sub(1);
608 stats.disk_usage = stats.disk_usage.saturating_sub(data.len() as u64);
609 drop(stats);
610
611 self.update_stats();
612
613 Ok(Some(data))
614 }
615
616 pub fn is_spilled(&self, name: &str) -> bool {
618 recover_read_guard(&self.spilled_segments).contains(name)
619 }
620
621 pub fn stats(&self) -> SpillStats {
623 recover_read_guard(&self.stats).clone()
624 }
625
626 pub fn memory_usage(&self) -> usize {
628 self.current_memory.load(Ordering::Relaxed)
629 }
630
631 pub fn utilization(&self) -> f64 {
633 let current = self.current_memory.load(Ordering::Relaxed);
634 if self.config.max_memory == 0 {
635 0.0
636 } else {
637 current as f64 / self.config.max_memory as f64
638 }
639 }
640
641 pub fn list_segments(&self) -> Vec<(String, usize, bool)> {
643 let segments = recover_read_guard(&self.segments);
644 segments
645 .iter()
646 .map(|(name, info)| {
647 (
648 name.clone(),
649 info.size.load(Ordering::Relaxed),
650 *recover_read_guard(&info.is_spilled),
651 )
652 })
653 .collect()
654 }
655
656 pub fn cleanup(&self) -> io::Result<()> {
658 if self.config.spill_dir.exists() {
659 for entry in fs::read_dir(&self.config.spill_dir)? {
660 let entry = entry?;
661 let path = entry.path();
662 if reddb_file::is_spill_file_path(&path) {
663 let _ = fs::remove_file(path);
664 }
665 }
666 }
667
668 let segments = recover_read_guard(&self.segments);
670 for info in segments.values() {
671 *recover_write_guard(&info.is_spilled) = false;
672 *recover_write_guard(&info.spill_path) = None;
673 }
674
675 recover_write_guard(&self.spilled_segments).clear();
676
677 Ok(())
678 }
679
680 fn decay_all_scores(&self) {
682 let segments = recover_read_guard(&self.segments);
683 for info in segments.values() {
684 info.decay_score(self.config.access_decay);
685 }
686 }
687
688 fn update_stats(&self) {
690 let mut stats = recover_write_guard(&self.stats);
691 stats.current_memory = self.current_memory.load(Ordering::Relaxed);
692
693 let segments = recover_read_guard(&self.segments);
694 stats.segment_count = segments.len();
695 drop(segments);
696
697 let spilled = recover_read_guard(&self.spilled_segments);
698 stats.spilled_count = spilled.len();
699 }
700}
701
702impl Default for SpillManager {
703 fn default() -> Self {
704 Self::new(SpillConfig::default())
705 }
706}
707
708impl Drop for SpillManager {
709 fn drop(&mut self) {
710 let _ = self.cleanup();
712 }
713}
714
715pub struct SpillableGraph<G> {
721 pub graph: G,
723 pub spill_manager: SpillManager,
725 segment_name: String,
727}
728
729impl<G> SpillableGraph<G> {
730 pub fn new(graph: G, segment_name: &str, config: SpillConfig) -> Self {
732 Self {
733 graph,
734 spill_manager: SpillManager::new(config),
735 segment_name: segment_name.to_string(),
736 }
737 }
738
739 pub fn segment_name(&self) -> &str {
741 &self.segment_name
742 }
743
744 pub fn check_memory(&mut self, current_size: usize) -> bool {
746 self.spill_manager
747 .update_size(&self.segment_name, current_size);
748 self.spill_manager.needs_spill().is_some()
749 }
750
751 pub fn stats(&self) -> SpillStats {
753 self.spill_manager.stats()
754 }
755}
756
757#[cfg(test)]
762mod tests {
763 use super::*;
764 use std::env;
765
766 fn test_config() -> SpillConfig {
767 use std::sync::atomic::{AtomicU64, Ordering};
768 static COUNTER: AtomicU64 = AtomicU64::new(0);
769 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
770 SpillConfig::new()
771 .max_memory(1024 * 1024) .spill_threshold(0.5)
773 .target_after_spill(0.3)
774 .min_spill_size(100)
775 .spill_dir(env::temp_dir().join(format!(
776 "reddb-spill-test-{}-{}",
777 std::process::id(),
778 id
779 )))
780 }
781
782 #[test]
783 fn test_register_segment() {
784 let manager = SpillManager::new(test_config());
785
786 manager.register_segment("seg1", 100_000);
787 manager.register_segment("seg2", 200_000);
788
789 assert_eq!(manager.memory_usage(), 300_000);
790
791 let stats = manager.stats();
792 assert_eq!(stats.segment_count, 2);
793 }
794
795 #[test]
796 fn test_update_size() {
797 let manager = SpillManager::new(test_config());
798
799 manager.register_segment("seg1", 100_000);
800 assert_eq!(manager.memory_usage(), 100_000);
801
802 manager.update_size("seg1", 150_000);
803 assert_eq!(manager.memory_usage(), 150_000);
804
805 manager.update_size("seg1", 50_000);
806 assert_eq!(manager.memory_usage(), 50_000);
807 }
808
809 #[test]
810 fn test_needs_spill() {
811 let manager = SpillManager::new(test_config());
812
813 manager.register_segment("seg1", 400_000); assert!(manager.needs_spill().is_none());
816
817 manager.register_segment("seg2", 200_000); for _ in 0..100 {
822 manager.access("seg1");
823 }
824
825 let to_spill = manager.needs_spill();
826 assert!(to_spill.is_some());
827 let segments = to_spill.unwrap();
828 assert!(segments.contains(&"seg2".to_string()));
829 }
830
831 #[test]
832 fn test_spill_and_reload() {
833 let manager = SpillManager::new(test_config());
834
835 manager.register_segment("test_seg", 1000);
836
837 let data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
838 let path = manager.spill("test_seg", &data).unwrap();
839
840 assert!(path.exists());
841 assert!(manager.is_spilled("test_seg"));
842
843 let reloaded = manager.reload("test_seg").unwrap();
844 assert!(reloaded.is_some());
845 assert_eq!(reloaded.unwrap(), data);
846 assert!(!manager.is_spilled("test_seg"));
847 }
848
849 #[test]
850 fn test_checksum_validation() {
851 let manager = SpillManager::new(test_config());
852
853 manager.register_segment("checksum_test_seg", 100);
855
856 let data = b"test data for checksum validation";
857 manager.spill("checksum_test_seg", data).unwrap();
858
859 let reloaded = manager.reload("checksum_test_seg").unwrap();
861 assert!(reloaded.is_some());
862 assert_eq!(&reloaded.unwrap()[..], data);
863 }
864
865 #[test]
866 fn test_list_segments() {
867 let manager = SpillManager::new(test_config());
868
869 manager.register_segment("alpha", 1000);
870 manager.register_segment("beta", 2000);
871 manager.register_segment("gamma", 3000);
872
873 let segments = manager.list_segments();
874 assert_eq!(segments.len(), 3);
875
876 let names: Vec<_> = segments.iter().map(|(n, _, _)| n.as_str()).collect();
877 assert!(names.contains(&"alpha"));
878 assert!(names.contains(&"beta"));
879 assert!(names.contains(&"gamma"));
880 }
881
882 #[test]
883 fn test_unregister_segment() {
884 let manager = SpillManager::new(test_config());
885
886 manager.register_segment("seg1", 100_000);
887 manager.register_segment("seg2", 200_000);
888
889 assert_eq!(manager.memory_usage(), 300_000);
890
891 manager.unregister_segment("seg1");
892
893 assert_eq!(manager.memory_usage(), 200_000);
894 assert_eq!(manager.stats().segment_count, 1);
895 }
896
897 #[test]
898 fn test_cleanup() {
899 let manager = SpillManager::new(test_config());
900
901 manager.register_segment("seg1", 100);
902 manager.spill("seg1", b"test data").unwrap();
903
904 assert!(manager.is_spilled("seg1"));
905
906 manager.cleanup().unwrap();
907
908 assert!(!manager.is_spilled("seg1"));
909 }
910
911 #[test]
912 fn test_utilization() {
913 let config = SpillConfig::new().max_memory(1000);
914 let manager = SpillManager::new(config);
915
916 manager.register_segment("seg", 500);
917
918 let util = manager.utilization();
919 assert!((util - 0.5).abs() < 0.001);
920 }
921
922 #[test]
923 fn test_v2_round_trip() {
924 let manager = SpillManager::new(test_config());
925 manager.register_segment("rt_seg", 100);
926 let data: Vec<u8> = (0u8..=127).collect();
927 manager.spill("rt_seg", &data).unwrap();
928 let out = manager.reload("rt_seg").unwrap().unwrap();
929 assert_eq!(out, data);
930 }
931
932 #[test]
933 fn test_single_byte_mutation_detected() {
934 let manager = SpillManager::new(test_config());
935 manager.register_segment("mut_seg", 100);
936 let data = b"hello world mutation test data!!";
937 let path = manager.spill("mut_seg", data).unwrap();
938
939 let mut raw = std::fs::read(&path).unwrap();
941 raw[reddb_file::SPILL_FILE_HEADER_LEN] ^= 0xFF;
942 std::fs::write(&path, &raw).unwrap();
943
944 let result = manager.reload("mut_seg");
945 assert!(
946 matches!(result, Err(SpillError::ChecksumMismatch)),
947 "expected ChecksumMismatch, got {:?}",
948 result
949 );
950 }
951
952 #[test]
953 fn test_byte_permutation_detected() {
954 let manager = SpillManager::new(test_config());
957 manager.register_segment("perm_seg", 100);
958 let data = b"abcdefghij"; let path = manager.spill("perm_seg", data).unwrap();
960
961 let mut raw = std::fs::read(&path).unwrap();
963 raw.swap(
964 reddb_file::SPILL_FILE_HEADER_LEN,
965 reddb_file::SPILL_FILE_HEADER_LEN + 1,
966 );
967 std::fs::write(&path, &raw).unwrap();
968
969 let result = manager.reload("perm_seg");
970 assert!(
971 matches!(result, Err(SpillError::ChecksumMismatch)),
972 "expected ChecksumMismatch, got {:?}",
973 result
974 );
975 }
976
977 #[test]
978 fn test_path_traversal_rejected() {
979 let manager = SpillManager::new(test_config());
980 for bad_name in &["../foo", "/etc/passwd", "a/b"] {
981 manager.register_segment(bad_name, 100);
982 let result = manager.spill(bad_name, b"data");
983 assert!(
984 matches!(result, Err(SpillError::InvalidName(_))),
985 "expected InvalidName for {:?}, got {:?}",
986 bad_name,
987 result
988 );
989 }
990 }
991}