use document_container::backends::folder::FolderBackend; use document_container::backends::memory::MemoryBackend; use document_container::{AnyContainer, Container, ContainerError}; fn run_round_trip(container: C) { container.write("manifest.json", br#"{"format":"gdd"}"#).unwrap(); container.write("resources/abc123", &[0xDE, 0xAD, 0xBE, 0xEF]).unwrap(); container.write("resources/xyz789", b"another resource").unwrap(); assert!(container.exists("manifest.json")); assert!(container.exists("resources/abc123")); assert!(!container.exists("does-not-exist")); let manifest = container.read("manifest.json").unwrap(); assert_eq!(manifest.as_slice(), br#"{"format":"gdd"}"#); let blob = container.read("resources/abc123").unwrap(); assert_eq!(blob.as_slice(), &[0xDE, 0xAD, 0xBE, 0xEF]); let top_level = container.list("").unwrap(); assert!(top_level.iter().any(|p| p == "manifest.json")); assert!( top_level.iter().all(|p| !p.starts_with("resources/")), "list(\"\") must not descend into subdirectories, got {top_level:?}" ); let mut resources = container.list("resources").unwrap(); resources.sort(); assert_eq!(resources, vec!["resources/abc123".to_string(), "resources/xyz789".to_string()]); container.remove("resources/abc123").unwrap(); assert!(!container.exists("resources/abc123")); assert!(matches!(container.read("resources/abc123"), Err(ContainerError::NotFound(_)))); } #[test] fn memory_backend_round_trip() { run_round_trip(MemoryBackend::new()); } #[test] fn folder_backend_round_trip() { let dir = tempfile::tempdir().unwrap(); let backend = FolderBackend::create(dir.path()).unwrap(); run_round_trip(backend); } fn run_list_and_remove_semantics(container: C) { container.write("dir/file", b"x").unwrap(); // Removing a missing path is idempotent. container.remove("dir/missing").unwrap(); container.remove("dir/file").unwrap(); container.remove("dir/file").unwrap(); // Listing a missing prefix yields an empty list. assert_eq!(container.list("nonexistent").unwrap(), Vec::::new()); // Listing a prefix that names a file is an error. container.write("manifest.json", b"{}").unwrap(); assert!(matches!(container.list("manifest.json"), Err(ContainerError::NotADirectory(_)))); assert!(matches!(container.list_dirs("manifest.json"), Err(ContainerError::NotADirectory(_)))); } #[test] fn memory_backend_list_and_remove_semantics() { run_list_and_remove_semantics(MemoryBackend::new()); } #[test] fn folder_backend_list_and_remove_semantics() { let dir = tempfile::tempdir().unwrap(); run_list_and_remove_semantics(FolderBackend::create(dir.path()).unwrap()); } #[test] #[cfg(unix)] fn folder_backend_rejects_symlink_escape() { let outside = tempfile::tempdir().unwrap(); std::fs::write(outside.path().join("secret"), b"sensitive").unwrap(); let dir = tempfile::tempdir().unwrap(); let backend = FolderBackend::create(dir.path()).unwrap(); // A symlink planted inside the root pointing outside it must not be traversable. std::os::unix::fs::symlink(outside.path(), dir.path().join("link")).unwrap(); let result = backend.read("link/secret"); assert!(matches!(result, Err(ContainerError::InvalidPath(_))), "symlink escape should be rejected, got {result:?}"); // Listing must reject the symlinked prefix too, not just reads, so it can't leak entry names from outside the root. assert!(matches!(backend.list("link"), Err(ContainerError::InvalidPath(_))), "list through symlink should be rejected"); assert!(matches!(backend.list_dirs("link"), Err(ContainerError::InvalidPath(_))), "list_dirs through symlink should be rejected"); // A symlink entry directly under root must not appear in listings, since `resolve` would reject reading it. std::os::unix::fs::symlink(outside.path().join("secret"), dir.path().join("file_link")).unwrap(); std::fs::write(dir.path().join("real"), b"ok").unwrap(); assert_eq!(backend.list("").unwrap(), vec!["real".to_string()], "symlink entries should be omitted from file listings"); assert!(backend.list_dirs("").unwrap().is_empty(), "symlink-to-dir entries should be omitted from dir listings"); } #[test] fn folder_backend_rejects_path_traversal() { let dir = tempfile::tempdir().unwrap(); let backend = FolderBackend::create(dir.path()).unwrap(); for bad in ["../escape", "subdir/../escape", "/abs", "back\\slash", "a//b", "trailing/", "a/./b"] { let result = backend.write(bad, b"nope"); assert!(matches!(result, Err(ContainerError::InvalidPath(_))), "expected InvalidPath for {bad:?}, got {result:?}"); } } fn run_append(container: C) { // Appending to a non-existent path creates it — same semantics as `OpenOptions::append().create(true)`. container.append("history.jsonl", b"{\"op\":1}\n").unwrap(); container.append("history.jsonl", b"{\"op\":2}\n").unwrap(); container.append("history.jsonl", b"{\"op\":3}\n").unwrap(); let log = container.read("history.jsonl").unwrap(); assert_eq!(log.as_slice(), b"{\"op\":1}\n{\"op\":2}\n{\"op\":3}\n"); } #[test] fn memory_backend_append() { run_append(MemoryBackend::new()); } #[test] fn folder_backend_append() { let dir = tempfile::tempdir().unwrap(); let backend = FolderBackend::create(dir.path()).unwrap(); run_append(backend); } #[test] fn any_container_dispatches_to_active_variant() { use document_container::AsyncContainer; let container = AnyContainer::Memory(MemoryBackend::new()); futures::executor::block_on(async { container.write("manifest.json", br#"{"format":"gdd"}"#).await.unwrap(); container.append("history.jsonl", b"frame-1\n").await.unwrap(); container.append("history.jsonl", b"frame-2\n").await.unwrap(); assert!(container.exists("manifest.json").await); let manifest = container.read("manifest.json").await.unwrap(); assert_eq!(manifest.as_slice(), br#"{"format":"gdd"}"#); let history = container.read("history.jsonl").await.unwrap(); assert_eq!(history.as_slice(), b"frame-1\nframe-2\n"); }); } #[test] fn folder_backend_reads_empty_file() { let dir = tempfile::tempdir().unwrap(); let backend = FolderBackend::create(dir.path()).unwrap(); backend.write("empty.bin", &[]).unwrap(); let read_back = backend.read("empty.bin").unwrap(); assert_eq!(read_back.as_slice(), &[] as &[u8]); } #[test] fn folder_backend_write_sized_fills_via_mmap() { let dir = tempfile::tempdir().unwrap(); let backend = FolderBackend::create(dir.path()).unwrap(); let payload = b"hello world"; backend .write_sized("resources/sized", payload.len(), &mut |buffer| { buffer.copy_from_slice(payload); Ok(()) }) .unwrap(); let read_back = backend.read("resources/sized").unwrap(); assert_eq!(read_back.as_slice(), payload); }