cartridge_rs/
lib.rs

1//! # Cartridge - High-Performance Mutable Archive Format
2//!
3//! `cartridge-rs` provides a high-level, easy-to-use API for working with Cartridge archives.
4//! Cartridge is a mutable archive format optimized for embedded systems with features like:
5//!
6//! - **Mutable archives** with in-place modifications
7//! - **SQLite VFS integration** for running databases directly inside archives
8//! - **Advanced features**: compression, encryption, snapshots, IAM policies
9//! - **Engram integration**: freeze to immutable, signed archives
10//!
11//! ## Quick Start
12//!
13//! ```rust,no_run
14//! use cartridge_rs::{Cartridge, Result};
15//!
16//! # fn main() -> Result<()> {
17//! // Create a new archive - auto-grows from 12KB as needed!
18//! let mut cart = Cartridge::create("my-data", "My Data Container")?;
19//!
20//! // Write files
21//! cart.write("documents/report.txt", b"Hello, World!")?;
22//!
23//! // Read files
24//! let content = cart.read("documents/report.txt")?;
25//!
26//! // List directory
27//! let files = cart.list("documents")?;
28//!
29//! // Automatic cleanup on drop
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ## Advanced Usage
35//!
36//! ```rust,no_run
37//! use cartridge_rs::{CartridgeBuilder, Result};
38//!
39//! # fn main() -> Result<()> {
40//! // Use builder for custom configuration
41//! let mut cart = CartridgeBuilder::new()
42//!     .slug("my-data")
43//!     .title("My Data Container")
44//!     .path("/data/my-container")  // Custom path
45//!     .with_audit_logging()
46//!     .build()?;
47//!
48//! cart.write("data.txt", b"content")?;
49//! # Ok(())
50//! # }
51//! ```
52
53// Core implementation (merged from cartridge-core)
54pub mod core;
55
56// Re-export core modules internally so crate:: paths in core still work
57#[allow(unused_imports)]
58pub(crate) use core::{
59    allocator, audit, buffer_pool, catalog, compression, encryption, engram_integration, error,
60    header, iam, io, manifest, page, snapshot, validation, vfs,
61};
62
63// Re-export core types that users need
64pub use crate::core::{
65    catalog::{FileMetadata, FileType},
66    encryption::EncryptionConfig,
67    error::{CartridgeError, Result},
68    header::{Header, S3AclMode, S3FeatureFuses, S3SseMode, S3VersioningMode, PAGE_SIZE},
69    iam::{Action, Effect, Policy, PolicyEngine, Statement},
70    manifest::Manifest,
71    snapshot::{SnapshotManager, SnapshotMetadata},
72    validation::ContainerSlug,
73};
74
75use crate::core::Cartridge as CoreCartridge;
76use serde::{Deserialize, Serialize};
77use std::collections::HashSet;
78use std::path::Path;
79use tracing::{debug, info};
80
81/// Rich metadata about a file or directory in the archive
82///
83/// Entry provides a convenient view of files and directories with parsed metadata,
84/// eliminating the need for consumers to manually parse paths and build hierarchies.
85///
86/// # Examples
87///
88/// ```rust,no_run
89/// use cartridge_rs::Cartridge;
90///
91/// # fn main() -> cartridge_rs::Result<()> {
92/// let cart = Cartridge::open("data.cart")?;
93/// let entries = cart.list_entries("documents")?;
94///
95/// for entry in entries {
96///     if entry.is_dir {
97///         println!("📁 {} ({})", entry.name, entry.path);
98///     } else {
99///         println!("📄 {} ({} bytes)", entry.name, entry.size.unwrap_or(0));
100///     }
101/// }
102/// # Ok(())
103/// # }
104/// ```
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct Entry {
107    /// Full path in the archive (e.g., "research/notes/overview.cml")
108    pub path: String,
109
110    /// Just the name (e.g., "overview.cml" or "notes")
111    pub name: String,
112
113    /// Parent directory path (e.g., "research/notes")
114    /// Empty string for root-level entries
115    pub parent: String,
116
117    /// True if this is a directory (has children under this prefix)
118    pub is_dir: bool,
119
120    /// File size in bytes (None for directories or if unavailable)
121    pub size: Option<u64>,
122
123    /// Creation timestamp as Unix epoch seconds (None if unavailable)
124    pub created: Option<u64>,
125
126    /// Last modification timestamp as Unix epoch seconds (None if unavailable)
127    pub modified: Option<u64>,
128
129    /// MIME type or content type (None if unavailable)
130    pub content_type: Option<String>,
131
132    /// File type (File, Directory, or Symlink)
133    pub file_type: FileType,
134
135    /// Compressed size on disk in bytes (None for directories or if unavailable)
136    /// This is the actual space used in the container, which may be less than
137    /// `size` when compression is enabled.
138    pub compressed_size: Option<u64>,
139}
140
141/// Convert flat paths to Entry objects with rich metadata
142///
143/// This helper parses paths, infers directory structure, and fetches metadata
144/// from the cartridge for each entry. Internal .cartridge/ files are filtered out.
145fn paths_to_entries(cart: &CoreCartridge, paths: &[String], _prefix: &str) -> Result<Vec<Entry>> {
146    let mut entries = Vec::new();
147    let mut seen_dirs: HashSet<String> = HashSet::new();
148
149    // Process each file path
150    for path in paths {
151        // Skip internal .cartridge directory (with or without leading slash)
152        if path.starts_with(".cartridge/")
153            || path == ".cartridge"
154            || path.starts_with("/.cartridge")
155        {
156            continue;
157        }
158        // Extract name and parent from path
159        let name = path.rsplit('/').next().unwrap_or(path).to_string();
160        let parent = if let Some(idx) = path.rfind('/') {
161            if idx == 0 {
162                // Root level: "/file.txt" -> parent is "/"
163                "/".to_string()
164            } else {
165                path[..idx].to_string()
166            }
167        } else {
168            String::new()
169        };
170
171        // Fetch metadata for the file
172        let metadata = cart.metadata(path).ok();
173
174        // Create entry for the file
175        entries.push(Entry {
176            path: path.clone(),
177            name,
178            parent: parent.clone(),
179            is_dir: false,
180            size: metadata.as_ref().map(|m| m.size),
181            created: metadata.as_ref().map(|m| m.created_at),
182            modified: metadata.as_ref().map(|m| m.modified_at),
183            content_type: metadata.as_ref().and_then(|m| m.content_type.clone()),
184            file_type: metadata
185                .as_ref()
186                .map(|m| m.file_type)
187                .unwrap_or(FileType::File),
188            compressed_size: metadata
189                .as_ref()
190                .map(|m| (m.blocks.len() as u64) * PAGE_SIZE as u64),
191        });
192
193        // Add parent directories (if not already seen)
194        let mut current_parent = parent.as_str();
195        while !current_parent.is_empty() && current_parent != "/" {
196            if seen_dirs.insert(current_parent.to_string()) {
197                let parent_name = current_parent
198                    .rsplit('/')
199                    .next()
200                    .unwrap_or(current_parent)
201                    .to_string();
202                let grandparent = if let Some(idx) = current_parent.rfind('/') {
203                    if idx == 0 {
204                        "/".to_string()
205                    } else {
206                        current_parent[..idx].to_string()
207                    }
208                } else {
209                    String::new()
210                };
211
212                entries.push(Entry {
213                    path: current_parent.to_string(),
214                    name: parent_name,
215                    parent: grandparent,
216                    is_dir: true,
217                    size: None,
218                    created: None,
219                    modified: None,
220                    content_type: None,
221                    file_type: FileType::Directory,
222                    compressed_size: None,
223                });
224            }
225
226            // Move up the tree
227            if let Some(idx) = current_parent.rfind('/') {
228                current_parent = &current_parent[..idx];
229            } else {
230                break;
231            }
232        }
233    }
234
235    // Sort: directories first, then alphabetically by name
236    entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
237        (true, false) => std::cmp::Ordering::Less,
238        (false, true) => std::cmp::Ordering::Greater,
239        _ => a.name.cmp(&b.name),
240    });
241
242    Ok(entries)
243}
244
245/// High-level Cartridge archive API
246///
247/// This is a wrapper around `cartridge_core::Cartridge` that provides:
248/// - Sensible defaults
249/// - Simpler method names
250/// - Automatic resource management
251/// - Better error messages
252///
253/// # Examples
254///
255/// ```rust,no_run
256/// use cartridge_rs::{Cartridge, Result};
257///
258/// # fn main() -> Result<()> {
259/// let mut cart = Cartridge::create("my-data", "My Data")?;
260/// cart.write("file.txt", b"content")?;
261/// let data = cart.read("file.txt")?;
262/// # Ok(())
263/// # }
264/// ```
265pub struct Cartridge {
266    inner: CoreCartridge,
267}
268
269impl Cartridge {
270    /// Create a new Cartridge archive with auto-growth
271    ///
272    /// Creates a container with the given slug and title.
273    /// - Starts at 12KB and grows automatically as needed
274    /// - Slug is used as the filename (kebab-case, becomes `{slug}.cart`)
275    /// - Title is the human-readable display name
276    ///
277    /// # Examples
278    ///
279    /// ```rust,no_run
280    /// use cartridge_rs::Cartridge;
281    ///
282    /// // Creates "my-data.cart" in current directory
283    /// let mut cart = Cartridge::create("my-data", "My Data Container")?;
284    /// # Ok::<(), cartridge_rs::CartridgeError>(())
285    /// ```
286    pub fn create(slug: &str, title: &str) -> Result<Self> {
287        info!("Creating cartridge with slug '{}', title '{}'", slug, title);
288        let inner = CoreCartridge::create(slug, title)?;
289        Ok(Cartridge { inner })
290    }
291
292    /// Create a new Cartridge archive at a specific path
293    ///
294    /// Use this when you need to specify a custom directory or path.
295    ///
296    /// # Examples
297    ///
298    /// ```rust,no_run
299    /// use cartridge_rs::Cartridge;
300    ///
301    /// // Creates "/data/my-container.cart"
302    /// let mut cart = Cartridge::create_at("/data/my-container", "my-container", "My Container")?;
303    /// # Ok::<(), cartridge_rs::CartridgeError>(())
304    /// ```
305    pub fn create_at<P: AsRef<Path>>(path: P, slug: &str, title: &str) -> Result<Self> {
306        info!(
307            "Creating cartridge at {:?} with slug '{}', title '{}'",
308            path.as_ref(),
309            slug,
310            title
311        );
312        let inner = CoreCartridge::create_at(path, slug, title)?;
313        Ok(Cartridge { inner })
314    }
315
316    /// Open an existing Cartridge archive
317    ///
318    /// # Examples
319    ///
320    /// ```rust,no_run
321    /// use cartridge_rs::Cartridge;
322    ///
323    /// let mut cart = Cartridge::open("existing.cart")?;
324    /// # Ok::<(), cartridge_rs::CartridgeError>(())
325    /// ```
326    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
327        info!("Opening cartridge at {:?}", path.as_ref());
328        let inner = CoreCartridge::open(path)?;
329        Ok(Cartridge { inner })
330    }
331
332    /// Write data to a file in the archive
333    ///
334    /// Creates the file if it doesn't exist, updates it if it does.
335    /// Automatically creates parent directories.
336    ///
337    /// # Examples
338    ///
339    /// ```rust,no_run
340    /// # use cartridge_rs::Cartridge;
341    /// # let mut cart = Cartridge::create("my-data", "My Data")?;
342    /// cart.write("documents/report.txt", b"Hello, World!")?;
343    /// # Ok::<(), cartridge_rs::CartridgeError>(())
344    /// ```
345    pub fn write<P: AsRef<str>>(&mut self, path: P, content: &[u8]) -> Result<()> {
346        let path = path.as_ref();
347        debug!("Writing {} bytes to {}", content.len(), path);
348
349        // Check if file exists, create or update accordingly
350        if self.inner.exists(path)? {
351            self.inner.write_file(path, content)
352        } else {
353            self.inner.create_file(path, content)
354        }
355    }
356
357    /// Read data from a file in the archive
358    ///
359    /// # Examples
360    ///
361    /// ```rust,no_run
362    /// # use cartridge_rs::Cartridge;
363    /// # let cart = Cartridge::create("my-data", "My Data")?;
364    /// let content = cart.read("documents/report.txt")?;
365    /// println!("Content: {}", String::from_utf8_lossy(&content));
366    /// # Ok::<(), cartridge_rs::CartridgeError>(())
367    /// ```
368    pub fn read<P: AsRef<str>>(&self, path: P) -> Result<Vec<u8>> {
369        let path = path.as_ref();
370        debug!("Reading {}", path);
371        self.inner.read_file(path)
372    }
373
374    /// Delete a file from the archive
375    ///
376    /// # Examples
377    ///
378    /// ```rust,no_run
379    /// # use cartridge_rs::Cartridge;
380    /// # let mut cart = Cartridge::create("my-data", "My Data")?;
381    /// cart.delete("old_file.txt")?;
382    /// # Ok::<(), cartridge_rs::CartridgeError>(())
383    /// ```
384    pub fn delete<P: AsRef<str>>(&mut self, path: P) -> Result<()> {
385        let path = path.as_ref();
386        debug!("Deleting {}", path);
387        self.inner.delete_file(path)
388    }
389
390    /// List all entries in a directory
391    ///
392    /// # Examples
393    ///
394    /// ```rust,no_run
395    /// # use cartridge_rs::Cartridge;
396    /// # let cart = Cartridge::create("my-data", "My Data")?;
397    /// let files = cart.list("documents")?;
398    /// for file in files {
399    ///     println!("Found: {}", file);
400    /// }
401    /// # Ok::<(), cartridge_rs::CartridgeError>(())
402    /// ```
403    pub fn list<P: AsRef<str>>(&self, path: P) -> Result<Vec<String>> {
404        let path = path.as_ref();
405        debug!("Listing directory {}", path);
406        self.inner.list_dir(path)
407    }
408
409    /// List all entries with rich metadata under a given prefix
410    ///
411    /// Returns Entry objects with parsed path components, file metadata,
412    /// and inferred directory information. This eliminates the need to
413    /// manually parse paths and build hierarchies.
414    ///
415    /// # Examples
416    ///
417    /// ```rust,no_run
418    /// # use cartridge_rs::Cartridge;
419    /// # let cart = Cartridge::create("my-data", "My Data")?;
420    /// let entries = cart.list_entries("documents")?;
421    /// for entry in entries {
422    ///     if entry.is_dir {
423    ///         println!("📁 {}", entry.name);
424    ///     } else {
425    ///         println!("📄 {} ({} bytes)", entry.name, entry.size.unwrap_or(0));
426    ///     }
427    /// }
428    /// # Ok::<(), cartridge_rs::CartridgeError>(())
429    /// ```
430    pub fn list_entries<P: AsRef<str>>(&self, prefix: P) -> Result<Vec<Entry>> {
431        let prefix = prefix.as_ref();
432        debug!("Listing entries under prefix {}", prefix);
433        let paths = self.inner.list_dir(prefix)?;
434        paths_to_entries(&self.inner, &paths, prefix)
435    }
436
437    /// List immediate children of a directory
438    ///
439    /// Like `list_entries()` but filters to only direct children,
440    /// providing a traditional directory-style listing.
441    ///
442    /// # Examples
443    ///
444    /// ```rust,no_run
445    /// # use cartridge_rs::Cartridge;
446    /// # let cart = Cartridge::create("my-data", "My Data")?;
447    /// // List only immediate children of "documents"
448    /// let children = cart.list_children("documents")?;
449    /// for child in children {
450    ///     println!("{} - parent: {}", child.name, child.parent);
451    /// }
452    /// # Ok::<(), cartridge_rs::CartridgeError>(())
453    /// ```
454    pub fn list_children<P: AsRef<str>>(&self, parent: P) -> Result<Vec<Entry>> {
455        let parent = parent.as_ref();
456        debug!("Listing immediate children of {}", parent);
457        let all_entries = self.list_entries(parent)?;
458
459        // Filter to immediate children only
460        Ok(all_entries
461            .into_iter()
462            .filter(|e| e.parent == parent)
463            .collect())
464    }
465
466    /// Check if a path is a directory
467    ///
468    /// Returns true if the path has children (i.e., is a directory),
469    /// false otherwise.
470    ///
471    /// # Examples
472    ///
473    /// ```rust,no_run
474    /// # use cartridge_rs::Cartridge;
475    /// # let cart = Cartridge::create("my-data", "My Data")?;
476    /// if cart.is_dir("documents")? {
477    ///     println!("documents is a directory");
478    /// }
479    /// # Ok::<(), cartridge_rs::CartridgeError>(())
480    /// ```
481    pub fn is_dir<P: AsRef<str>>(&self, path: P) -> Result<bool> {
482        let path = path.as_ref();
483        debug!("Checking if {} is a directory", path);
484
485        // A path is a directory if it has children
486        let prefix = if path.is_empty() {
487            String::new()
488        } else {
489            format!("{}/", path)
490        };
491
492        let paths = self.inner.list_dir(&prefix)?;
493        Ok(!paths.is_empty())
494    }
495
496    /// Check if a file or directory exists
497    ///
498    /// # Examples
499    ///
500    /// ```rust,no_run
501    /// # use cartridge_rs::Cartridge;
502    /// # let cart = Cartridge::create("my-data", "My Data")?;
503    /// if cart.exists("config.json")? {
504    ///     println!("Config file found!");
505    /// }
506    /// # Ok::<(), cartridge_rs::CartridgeError>(())
507    /// ```
508    pub fn exists<P: AsRef<str>>(&self, path: P) -> Result<bool> {
509        self.inner.exists(path.as_ref())
510    }
511
512    /// Get metadata for a file or directory
513    ///
514    /// # Examples
515    ///
516    /// ```rust,no_run
517    /// # use cartridge_rs::Cartridge;
518    /// # let cart = Cartridge::create("my-data", "My Data")?;
519    /// let meta = cart.metadata("file.txt")?;
520    /// println!("Size: {} bytes", meta.size);
521    /// # Ok::<(), cartridge_rs::CartridgeError>(())
522    /// ```
523    pub fn metadata<P: AsRef<str>>(&self, path: P) -> Result<FileMetadata> {
524        self.inner.metadata(path.as_ref())
525    }
526
527    /// Create a directory
528    ///
529    /// Automatically creates parent directories if needed.
530    ///
531    /// # Examples
532    ///
533    /// ```rust,no_run
534    /// # use cartridge_rs::Cartridge;
535    /// # let mut cart = Cartridge::create("my-data", "My Data")?;
536    /// cart.create_dir("documents/reports/2025")?;
537    /// # Ok::<(), cartridge_rs::CartridgeError>(())
538    /// ```
539    pub fn create_dir<P: AsRef<str>>(&mut self, path: P) -> Result<()> {
540        self.inner.create_dir(path.as_ref())
541    }
542
543    /// Flush all pending changes to disk
544    ///
545    /// # Examples
546    ///
547    /// ```rust,no_run
548    /// # use cartridge_rs::Cartridge;
549    /// # let mut cart = Cartridge::create("my-data", "My Data")?;
550    /// cart.write("file.txt", b"data")?;
551    /// cart.flush()?;  // Ensure changes are persisted
552    /// # Ok::<(), cartridge_rs::CartridgeError>(())
553    /// ```
554    pub fn flush(&mut self) -> Result<()> {
555        debug!("Flushing cartridge to disk");
556        self.inner.flush()
557    }
558
559    /// Get the container slug
560    ///
561    /// # Examples
562    ///
563    /// ```rust,no_run
564    /// # use cartridge_rs::Cartridge;
565    /// # let cart = Cartridge::create("my-data", "My Data")?;
566    /// assert_eq!(cart.slug()?, "my-data");
567    /// # Ok::<(), cartridge_rs::CartridgeError>(())
568    /// ```
569    pub fn slug(&self) -> Result<String> {
570        self.inner.slug()
571    }
572
573    /// Get the container title
574    ///
575    /// # Examples
576    ///
577    /// ```rust,no_run
578    /// # use cartridge_rs::Cartridge;
579    /// # let cart = Cartridge::create("my-data", "My Container")?;
580    /// assert_eq!(cart.title()?, "My Container");
581    /// # Ok::<(), cartridge_rs::CartridgeError>(())
582    /// ```
583    pub fn title(&self) -> Result<String> {
584        self.inner.title()
585    }
586
587    /// Read the container manifest
588    ///
589    /// # Examples
590    ///
591    /// ```rust,no_run
592    /// # use cartridge_rs::Cartridge;
593    /// # let cart = Cartridge::create("my-data", "My Container")?;
594    /// let manifest = cart.read_manifest()?;
595    /// println!("Version: {}", manifest.version);
596    /// # Ok::<(), cartridge_rs::CartridgeError>(())
597    /// ```
598    pub fn read_manifest(&self) -> Result<Manifest> {
599        self.inner.read_manifest()
600    }
601
602    /// Update the container manifest
603    ///
604    /// # Examples
605    ///
606    /// ```rust,no_run
607    /// # use cartridge_rs::Cartridge;
608    /// # let mut cart = Cartridge::create("my-data", "My Container")?;
609    /// cart.update_manifest(|manifest| {
610    ///     manifest.description = Some("Updated description".to_string());
611    /// })?;
612    /// # Ok::<(), cartridge_rs::CartridgeError>(())
613    /// ```
614    pub fn update_manifest<F>(&mut self, f: F) -> Result<()>
615    where
616        F: FnOnce(&mut Manifest),
617    {
618        self.inner.update_manifest(f)
619    }
620
621    /// Get the cartridge header with S3 fuses and metadata
622    ///
623    /// # Examples
624    ///
625    /// ```rust,no_run
626    /// # use cartridge_rs::Cartridge;
627    /// # fn main() -> cartridge_rs::Result<()> {
628    /// let cart = Cartridge::create("data", "My Data")?;
629    /// let fuses = cart.header().get_s3_fuses();
630    /// println!("ACL mode: {:?}", fuses.acl_mode);
631    /// # Ok(())
632    /// # }
633    /// ```
634    pub fn header(&self) -> &Header {
635        self.inner.header()
636    }
637
638    /// Get mutable access to the cartridge header
639    pub fn header_mut(&mut self) -> &mut Header {
640        self.inner.header_mut()
641    }
642
643    /// Update user-defined metadata for a file
644    ///
645    /// Stores custom key-value pairs in file metadata without modifying content.
646    /// Useful for S3-compatible metadata, ACLs, SSE headers, etc.
647    ///
648    /// # Examples
649    ///
650    /// ```rust,no_run
651    /// # use cartridge_rs::Cartridge;
652    /// # fn main() -> cartridge_rs::Result<()> {
653    /// let mut cart = Cartridge::create("data", "My Data")?;
654    /// cart.write("file.txt", b"content")?;
655    /// cart.update_user_metadata("file.txt", "s3:acl", r#"{"grants":[]}"#)?;
656    /// # Ok(())
657    /// # }
658    /// ```
659    pub fn update_user_metadata<P: AsRef<str>>(
660        &mut self,
661        path: P,
662        key: impl Into<String>,
663        value: impl Into<String>,
664    ) -> Result<()> {
665        self.inner.update_user_metadata(path.as_ref(), key, value)
666    }
667
668    /// Create a snapshot of the current cartridge state
669    ///
670    /// # Arguments
671    /// * `name` - Snapshot name
672    /// * `description` - Snapshot description
673    /// * `snapshot_dir` - Directory to store snapshot files
674    ///
675    /// # Returns
676    /// Snapshot ID (timestamp in microseconds)
677    ///
678    /// # Examples
679    ///
680    /// ```rust,no_run
681    /// # use cartridge_rs::Cartridge;
682    /// # use std::path::Path;
683    /// # fn main() -> cartridge_rs::Result<()> {
684    /// let mut cart = Cartridge::create("data", "My Data")?;
685    /// cart.write("file.txt", b"version 1")?;
686    /// let snapshot_id = cart.create_snapshot(
687    ///     "v1".to_string(),
688    ///     "First version".to_string(),
689    ///     Path::new("./snapshots")
690    /// )?;
691    /// # Ok(())
692    /// # }
693    /// ```
694    pub fn create_snapshot(
695        &self,
696        name: String,
697        description: String,
698        snapshot_dir: &std::path::Path,
699    ) -> Result<u64> {
700        self.inner.create_snapshot(name, description, snapshot_dir)
701    }
702
703    /// Restore cartridge state from a snapshot
704    ///
705    /// # Arguments
706    /// * `snapshot_id` - Snapshot ID to restore
707    /// * `snapshot_dir` - Directory containing snapshot files
708    ///
709    /// # Examples
710    ///
711    /// ```rust,no_run
712    /// # use cartridge_rs::Cartridge;
713    /// # use std::path::Path;
714    /// # fn main() -> cartridge_rs::Result<()> {
715    /// let mut cart = Cartridge::create("data", "My Data")?;
716    /// let snapshot_id = 1234567890000000u64;
717    /// cart.restore_snapshot(snapshot_id, Path::new("./snapshots"))?;
718    /// # Ok(())
719    /// # }
720    /// ```
721    pub fn restore_snapshot(&mut self, snapshot_id: u64, snapshot_dir: &std::path::Path) -> Result<()> {
722        self.inner.restore_snapshot(snapshot_id, snapshot_dir)
723    }
724
725    /// Enable encryption for all new files written to the cartridge
726    ///
727    /// Once enabled, all new files created or updated will be encrypted using AES-256-GCM.
728    /// Existing files are not automatically encrypted - they remain as-is.
729    ///
730    /// # Arguments
731    /// * `key` - 32-byte AES-256 encryption key
732    ///
733    /// # Examples
734    ///
735    /// ```rust,no_run
736    /// # use cartridge_rs::{Cartridge, EncryptionConfig};
737    /// # fn main() -> cartridge_rs::Result<()> {
738    /// let mut cart = Cartridge::create("secure-data", "Secure Data")?;
739    ///
740    /// // Generate a random encryption key
741    /// let key = EncryptionConfig::generate_key();
742    ///
743    /// // Enable encryption
744    /// cart.enable_encryption(&key)?;
745    ///
746    /// // All new files will be encrypted
747    /// cart.write("sensitive.txt", b"confidential data")?;
748    /// # Ok(())
749    /// # }
750    /// ```
751    pub fn enable_encryption(&mut self, key: &[u8; 32]) -> Result<()> {
752        self.inner.enable_encryption(key)
753    }
754
755    /// Disable encryption
756    ///
757    /// New files written after disabling encryption will not be encrypted.
758    /// Files that were already encrypted remain encrypted and require the key to read.
759    ///
760    /// # Examples
761    ///
762    /// ```rust,no_run
763    /// # use cartridge_rs::Cartridge;
764    /// # fn main() -> cartridge_rs::Result<()> {
765    /// let mut cart = Cartridge::create("data", "My Data")?;
766    /// cart.disable_encryption()?;
767    /// # Ok(())
768    /// # }
769    /// ```
770    pub fn disable_encryption(&mut self) -> Result<()> {
771        self.inner.disable_encryption()
772    }
773
774    /// Check if encryption is currently enabled
775    ///
776    /// Returns `true` if new files will be encrypted, `false` otherwise.
777    ///
778    /// # Examples
779    ///
780    /// ```rust,no_run
781    /// # use cartridge_rs::{Cartridge, EncryptionConfig};
782    /// # fn main() -> cartridge_rs::Result<()> {
783    /// let mut cart = Cartridge::create("data", "My Data")?;
784    /// assert!(!cart.is_encrypted());
785    ///
786    /// let key = EncryptionConfig::generate_key();
787    /// cart.enable_encryption(&key)?;
788    /// assert!(cart.is_encrypted());
789    /// # Ok(())
790    /// # }
791    /// ```
792    pub fn is_encrypted(&self) -> bool {
793        self.inner.is_encrypted()
794    }
795
796    /// Get access to the underlying core Cartridge for advanced operations
797    ///
798    /// Use this when you need features not exposed by the high-level API:
799    /// - IAM policies
800    /// - Snapshots
801    /// - Audit logging
802    /// - Custom allocator settings
803    ///
804    /// # Examples
805    ///
806    /// ```rust,ignore
807    /// use cartridge_rs::{Cartridge, Policy};
808    ///
809    /// let mut cart = Cartridge::create("data.cart")?;
810    ///
811    /// // Access advanced features
812    /// let policy = Policy::new("my-policy", vec![]);
813    /// cart.inner_mut().set_policy(policy);
814    /// ```
815    pub fn inner(&self) -> &CoreCartridge {
816        &self.inner
817    }
818
819    /// Get mutable access to the underlying core Cartridge
820    pub fn inner_mut(&mut self) -> &mut CoreCartridge {
821        &mut self.inner
822    }
823}
824
825/// Builder for customizing Cartridge creation
826///
827/// Provides a fluent API for configuring advanced options.
828///
829/// # Examples
830///
831/// ```rust,no_run
832/// use cartridge_rs::CartridgeBuilder;
833///
834/// # fn main() -> cartridge_rs::Result<()> {
835/// let cart = CartridgeBuilder::new()
836///     .slug("my-data")
837///     .title("My Data Container")
838///     .path("/data/my-container")  // Optional: custom path
839///     .with_audit_logging()
840///     .build()?;
841/// # Ok(())
842/// # }
843/// ```
844pub struct CartridgeBuilder {
845    path: Option<String>,
846    slug: Option<String>,
847    title: Option<String>,
848    enable_audit: bool,
849}
850
851impl CartridgeBuilder {
852    /// Create a new CartridgeBuilder with default settings
853    pub fn new() -> Self {
854        CartridgeBuilder {
855            path: None,
856            slug: None,
857            title: None,
858            enable_audit: false,
859        }
860    }
861
862    /// Set the slug (kebab-case identifier)
863    pub fn slug<S: Into<String>>(mut self, slug: S) -> Self {
864        self.slug = Some(slug.into());
865        self
866    }
867
868    /// Set the title (human-readable display name)
869    pub fn title<S: Into<String>>(mut self, title: S) -> Self {
870        self.title = Some(title.into());
871        self
872    }
873
874    /// Set a custom path (optional, defaults to slug in current directory)
875    pub fn path<P: Into<String>>(mut self, path: P) -> Self {
876        self.path = Some(path.into());
877        self
878    }
879
880    /// Enable audit logging for all operations
881    pub fn with_audit_logging(mut self) -> Self {
882        self.enable_audit = true;
883        self
884    }
885
886    /// Build the Cartridge instance
887    pub fn build(self) -> Result<Cartridge> {
888        let slug = self.slug.ok_or_else(|| {
889            CartridgeError::Io(std::io::Error::new(
890                std::io::ErrorKind::InvalidInput,
891                "slug must be set",
892            ))
893        })?;
894
895        let title = self.title.ok_or_else(|| {
896            CartridgeError::Io(std::io::Error::new(
897                std::io::ErrorKind::InvalidInput,
898                "title must be set",
899            ))
900        })?;
901
902        info!("Building cartridge with slug '{}', title '{}'", slug, title);
903
904        let mut inner = if let Some(path) = self.path {
905            CoreCartridge::create_at(&path, &slug, &title)?
906        } else {
907            CoreCartridge::create(&slug, &title)?
908        };
909
910        if self.enable_audit {
911            use crate::core::audit::AuditLogger;
912            use std::sync::Arc;
913            use std::time::Duration;
914
915            let logger = Arc::new(AuditLogger::new(1000, Duration::from_secs(60)));
916            inner.set_audit_logger(logger);
917            debug!("Audit logging enabled");
918        }
919
920        Ok(Cartridge { inner })
921    }
922}
923
924impl Default for CartridgeBuilder {
925    fn default() -> Self {
926        Self::new()
927    }
928}
929
930/// Virtual Filesystem trait for unified storage interface
931///
932/// Provides a common interface that can be implemented by different storage backends:
933/// - Cartridge (mutable containers)
934/// - Engram (immutable archives)
935/// - ZipVfs, TarVfs (other archive formats)
936/// - S3Vfs, LocalVfs (remote/local filesystems)
937///
938/// This allows applications to work with any storage backend using the same API.
939///
940/// # Examples
941///
942/// ```rust,no_run
943/// use cartridge_rs::{Cartridge, Vfs};
944///
945/// fn process_storage<V: Vfs>(vfs: &V, path: &str) -> Result<(), Box<dyn std::error::Error>> {
946///     // Works with any VFS implementation
947///     let entries = vfs.list_entries(path)?;
948///     for entry in entries {
949///         if !entry.is_dir {
950///             let content = vfs.read(&entry.path)?;
951///             println!("File: {} ({} bytes)", entry.name, content.len());
952///         }
953///     }
954///     Ok(())
955/// }
956///
957/// // Use with Cartridge
958/// let cart = Cartridge::create("my-data", "My Data")?;
959/// process_storage(&cart, "documents")?;
960/// # Ok::<(), Box<dyn std::error::Error>>(())
961/// ```
962pub trait Vfs {
963    /// List all entries under a given prefix with rich metadata
964    ///
965    /// Returns Entry objects with parsed path components, file metadata,
966    /// and inferred directory information.
967    fn list_entries(&self, prefix: &str) -> Result<Vec<Entry>>;
968
969    /// List immediate children of a directory (non-recursive)
970    ///
971    /// Returns only entries whose parent matches the given path,
972    /// providing a directory-style listing view.
973    fn list_children(&self, parent: &str) -> Result<Vec<Entry>>;
974
975    /// Read the contents of a file
976    ///
977    /// Returns the full file contents as a byte vector.
978    fn read(&self, path: &str) -> Result<Vec<u8>>;
979
980    /// Write or update a file
981    ///
982    /// Creates the file if it doesn't exist, updates it if it does.
983    /// Automatically creates parent directories as needed.
984    fn write(&mut self, path: &str, data: &[u8]) -> Result<()>;
985
986    /// Delete a file or directory
987    ///
988    /// For directories, this typically deletes all contents recursively.
989    fn delete(&mut self, path: &str) -> Result<()>;
990
991    /// Check if a path exists
992    fn exists(&self, path: &str) -> Result<bool>;
993
994    /// Check if a path is a directory
995    fn is_dir(&self, path: &str) -> Result<bool>;
996
997    /// Get metadata for a path
998    fn metadata(&self, path: &str) -> Result<FileMetadata>;
999}
1000
1001/// Implement VFS trait for Cartridge
1002impl Vfs for Cartridge {
1003    fn list_entries(&self, prefix: &str) -> Result<Vec<Entry>> {
1004        self.list_entries(prefix)
1005    }
1006
1007    fn list_children(&self, parent: &str) -> Result<Vec<Entry>> {
1008        self.list_children(parent)
1009    }
1010
1011    fn read(&self, path: &str) -> Result<Vec<u8>> {
1012        self.read(path)
1013    }
1014
1015    fn write(&mut self, path: &str, data: &[u8]) -> Result<()> {
1016        self.write(path, data)
1017    }
1018
1019    fn delete(&mut self, path: &str) -> Result<()> {
1020        self.delete(path)
1021    }
1022
1023    fn exists(&self, path: &str) -> Result<bool> {
1024        self.exists(path)
1025    }
1026
1027    fn is_dir(&self, path: &str) -> Result<bool> {
1028        self.is_dir(path)
1029    }
1030
1031    fn metadata(&self, path: &str) -> Result<FileMetadata> {
1032        self.metadata(path)
1033    }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038    use super::*;
1039
1040    #[test]
1041    fn test_create_and_write() -> Result<()> {
1042        let temp_dir = tempfile::TempDir::new().unwrap();
1043        let path = temp_dir.path().join("test-cart");
1044
1045        let mut cart = Cartridge::create_at(&path, "test-cart", "Test Cartridge")?;
1046        cart.write("test.txt", b"hello")?;
1047        cart.flush()?; // Ensure write is persisted
1048
1049        let content = cart.read("test.txt")?;
1050        assert_eq!(content, b"hello");
1051
1052        Ok(())
1053    }
1054
1055    #[test]
1056    fn test_builder() -> Result<()> {
1057        let temp_dir = tempfile::TempDir::new().unwrap();
1058        let path = temp_dir.path().join("builder-cart");
1059
1060        let cart = CartridgeBuilder::new()
1061            .slug("builder-cart")
1062            .title("Builder Cartridge")
1063            .path(path.to_str().unwrap())
1064            .build()?;
1065
1066        // Starts small with auto-growth
1067        assert!(cart.inner().stats().total_blocks >= 3);
1068
1069        Ok(())
1070    }
1071
1072    #[test]
1073    fn test_list_entries_flat_structure() -> Result<()> {
1074        let temp_dir = tempfile::TempDir::new().unwrap();
1075        let path = temp_dir.path().join("flat-cart");
1076
1077        let mut cart = Cartridge::create_at(&path, "flat-cart", "Flat Cartridge")?;
1078
1079        // Create flat structure (no nesting)
1080        cart.write("/file1.txt", b"content1")?;
1081        cart.write("/file2.txt", b"content2")?;
1082        cart.write("/file3.txt", b"content3")?;
1083        cart.flush()?;
1084
1085        let entries = cart.list_entries("/")?;
1086
1087        // Should have 3 files
1088        assert_eq!(entries.len(), 3);
1089        assert!(entries.iter().all(|e| !e.is_dir));
1090        assert!(entries.iter().all(|e| e.parent == "/"));
1091
1092        Ok(())
1093    }
1094
1095    #[test]
1096    fn test_list_entries_nested_structure() -> Result<()> {
1097        let temp_dir = tempfile::TempDir::new().unwrap();
1098        let path = temp_dir.path().join("nested-cart");
1099
1100        let mut cart = Cartridge::create_at(&path, "nested-cart", "Nested Cartridge")?;
1101
1102        // Create nested structure (3+ levels)
1103        cart.write("/docs/guides/getting-started.md", b"# Getting Started")?;
1104        cart.write("/docs/guides/advanced.md", b"# Advanced")?;
1105        cart.write("/docs/api/reference.md", b"# API Reference")?;
1106        cart.write("/src/main.rs", b"fn main() {}")?;
1107        cart.flush()?;
1108
1109        let entries = cart.list_entries("/")?;
1110
1111        // Should have: /docs, /docs/guides, /docs/api, /src (4 dirs) + 4 files = 8 entries
1112        assert_eq!(entries.len(), 8);
1113
1114        // Count directories and files
1115        let dirs: Vec<_> = entries.iter().filter(|e| e.is_dir).collect();
1116        let files: Vec<_> = entries.iter().filter(|e| !e.is_dir).collect();
1117
1118        assert_eq!(dirs.len(), 4);
1119        assert_eq!(files.len(), 4);
1120
1121        // Verify directory names
1122        let dir_names: Vec<_> = dirs.iter().map(|e| e.name.as_str()).collect();
1123        assert!(dir_names.contains(&"docs"));
1124        assert!(dir_names.contains(&"guides"));
1125        assert!(dir_names.contains(&"api"));
1126        assert!(dir_names.contains(&"src"));
1127
1128        Ok(())
1129    }
1130
1131    #[test]
1132    fn test_list_entries_empty() -> Result<()> {
1133        let temp_dir = tempfile::TempDir::new().unwrap();
1134        let path = temp_dir.path().join("empty-cart");
1135
1136        let cart = Cartridge::create_at(&path, "empty-cart", "Empty Cartridge")?;
1137
1138        let entries = cart.list_entries("/")?;
1139
1140        // Empty cartridge should return empty list (excluding internal .cartridge/)
1141        assert_eq!(entries.len(), 0);
1142
1143        Ok(())
1144    }
1145
1146    #[test]
1147    fn test_list_children_root_level() -> Result<()> {
1148        let temp_dir = tempfile::TempDir::new().unwrap();
1149        let path = temp_dir.path().join("children-cart");
1150
1151        let mut cart = Cartridge::create_at(&path, "children-cart", "Children Cartridge")?;
1152
1153        // Create structure with nested files
1154        cart.write("/root1.txt", b"root file 1")?;
1155        cart.write("/root2.txt", b"root file 2")?;
1156        cart.write("/docs/nested.md", b"nested file")?;
1157        cart.write("/docs/deep/very-nested.md", b"very nested")?;
1158        cart.flush()?;
1159
1160        let children = cart.list_children("/")?;
1161
1162        // Should only have root-level entries: root1.txt, root2.txt, docs/
1163        assert_eq!(children.len(), 3);
1164        assert!(children.iter().all(|e| e.parent == "/"));
1165
1166        // Count root files and directories
1167        let files: Vec<_> = children.iter().filter(|e| !e.is_dir).collect();
1168        let dirs: Vec<_> = children.iter().filter(|e| e.is_dir).collect();
1169
1170        assert_eq!(files.len(), 2);
1171        assert_eq!(dirs.len(), 1);
1172        assert_eq!(dirs[0].name, "docs");
1173
1174        Ok(())
1175    }
1176
1177    #[test]
1178    fn test_list_children_nested_directory() -> Result<()> {
1179        let temp_dir = tempfile::TempDir::new().unwrap();
1180        let path = temp_dir.path().join("nested-children-cart");
1181
1182        let mut cart = Cartridge::create_at(&path, "nested-children-cart", "Nested Children")?;
1183
1184        cart.write("/docs/readme.md", b"readme")?;
1185        cart.write("/docs/guides/tutorial.md", b"tutorial")?;
1186        cart.write("/docs/api/reference.md", b"reference")?;
1187        cart.flush()?;
1188
1189        let children = cart.list_children("/docs")?;
1190
1191        // Should have: readme.md, guides/, api/ (3 immediate children)
1192        assert_eq!(children.len(), 3);
1193        assert!(children.iter().all(|e| e.parent == "/docs"));
1194
1195        Ok(())
1196    }
1197
1198    #[test]
1199    fn test_list_children_only_subdirectories() -> Result<()> {
1200        let temp_dir = tempfile::TempDir::new().unwrap();
1201        let path = temp_dir.path().join("subdirs-cart");
1202
1203        let mut cart = Cartridge::create_at(&path, "subdirs-cart", "Subdirs")?;
1204
1205        // Create directories with no files at this level
1206        cart.write("/parent/child1/file.txt", b"file1")?;
1207        cart.write("/parent/child2/file.txt", b"file2")?;
1208        cart.flush()?;
1209
1210        let children = cart.list_children("/parent")?;
1211
1212        // Should only have child1/ and child2/ directories
1213        assert_eq!(children.len(), 2);
1214        assert!(children.iter().all(|e| e.is_dir));
1215        assert!(children.iter().all(|e| e.parent == "/parent"));
1216
1217        Ok(())
1218    }
1219
1220    #[test]
1221    fn test_list_children_only_files() -> Result<()> {
1222        let temp_dir = tempfile::TempDir::new().unwrap();
1223        let path = temp_dir.path().join("files-cart");
1224
1225        let mut cart = Cartridge::create_at(&path, "files-cart", "Files")?;
1226
1227        // Create directory with only files
1228        cart.write("/data/file1.dat", b"data1")?;
1229        cart.write("/data/file2.dat", b"data2")?;
1230        cart.write("/data/file3.dat", b"data3")?;
1231        cart.flush()?;
1232
1233        let children = cart.list_children("/data")?;
1234
1235        // Should only have 3 files
1236        assert_eq!(children.len(), 3);
1237        assert!(children.iter().all(|e| !e.is_dir));
1238        assert!(children.iter().all(|e| e.parent == "/data"));
1239
1240        Ok(())
1241    }
1242
1243    #[test]
1244    fn test_is_dir_known_directory() -> Result<()> {
1245        let temp_dir = tempfile::TempDir::new().unwrap();
1246        let path = temp_dir.path().join("isdir-cart");
1247
1248        let mut cart = Cartridge::create_at(&path, "isdir-cart", "IsDir")?;
1249
1250        cart.write("/documents/report.txt", b"report")?;
1251        cart.flush()?;
1252
1253        // "/documents" should be a directory
1254        assert!(cart.is_dir("/documents")?);
1255
1256        Ok(())
1257    }
1258
1259    #[test]
1260    fn test_is_dir_known_file() -> Result<()> {
1261        let temp_dir = tempfile::TempDir::new().unwrap();
1262        let path = temp_dir.path().join("isfile-cart");
1263
1264        let mut cart = Cartridge::create_at(&path, "isfile-cart", "IsFile")?;
1265
1266        cart.write("/documents/report.txt", b"report")?;
1267        cart.flush()?;
1268
1269        // "/documents/report.txt" should NOT be a directory
1270        assert!(!cart.is_dir("/documents/report.txt")?);
1271
1272        Ok(())
1273    }
1274
1275    #[test]
1276    fn test_is_dir_nonexistent() -> Result<()> {
1277        let temp_dir = tempfile::TempDir::new().unwrap();
1278        let path = temp_dir.path().join("nonexistent-cart");
1279
1280        let cart = Cartridge::create_at(&path, "nonexistent-cart", "NonExistent")?;
1281
1282        // Non-existent path should not be a directory
1283        assert!(!cart.is_dir("/does-not-exist")?);
1284
1285        Ok(())
1286    }
1287
1288    #[test]
1289    fn test_entry_metadata_fields() -> Result<()> {
1290        let temp_dir = tempfile::TempDir::new().unwrap();
1291        let path = temp_dir.path().join("metadata-cart");
1292
1293        let mut cart = Cartridge::create_at(&path, "metadata-cart", "Metadata")?;
1294
1295        cart.write("/test.txt", b"hello world")?;
1296        cart.flush()?;
1297
1298        let entries = cart.list_entries("/")?;
1299
1300        assert_eq!(entries.len(), 1);
1301        let entry = &entries[0];
1302
1303        // Verify basic fields
1304        assert_eq!(entry.path, "/test.txt");
1305        assert_eq!(entry.name, "test.txt");
1306        assert_eq!(entry.parent, "/");
1307        assert!(!entry.is_dir);
1308
1309        // Verify metadata fields are populated
1310        assert!(entry.size.is_some());
1311        assert_eq!(entry.size.unwrap(), 11); // "hello world" is 11 bytes
1312        assert!(entry.created.is_some());
1313        assert!(entry.modified.is_some());
1314
1315        Ok(())
1316    }
1317}