srcdmp 0.1.0-alpha.1

Amazing code snapshot tool and library
Documentation
//! # srcdmp
//! An amazing source code snapshot library and CLI tool.
//!
//! `srcdmp` packs entire project directories into compact binary archive files
//! (`.srcdmp`) using a family of specialized codecs. It provides both a
//! command-line interface and a library API for creating, inspecting, and
//! extracting snapshots.
//!
//! # Feature Flags
//!
//! - **`cli`** (enabled by default): CLI tooling, TUI viewer, and colored output.
//! - **`fs`** (enabled by default): Filesystem operations via [`Dump`], [`DumpBuilder`],
//!   and [`DumpWriteBuilder`].
//!
//! # Codecs
//!
//! | Codec | ID | Description |
//! | :--- | :--- | :--- |
//! | SDMP-7 | `0x07` | Three-tier compression optimized for source code ASCII characters |
//! | SDMP-8 | `0x08` | Raw UTF-8 passthrough, no compression |
//! | SDMP-C4 | `0x04` | Huffman-tree-based compression using empirically derived frequencies |
//! | SDMP-C2 | `0x02` | Reserved for future use |
//!
//! # Quick Start
//!
//! ```no_run
//! use srcdmp::{Dump, Codec};
//!
//! let dump = Dump::from_dir(".")
//!     .codec(Codec::Compact4)
//!     .gitignore()
//!     .build()
//!     .expect("failed to build snapshot");
//!
//! println!("packed {} files, {} bytes", dump.file_count(), dump.original_size());
//! ```

#![warn(missing_docs)]

use std::collections::HashMap;

use crate::raw::{codec::sdmp7::decode, entry::Entry, error::SdmpError, header::Header};

/// Raw encoding and decoding primitives.
///
/// Contains the on-disk format types ([`Header`], [`Entry`], [`SdmpError`]),
/// lookup tables ([`raw::table::TIER1_DECODE`]), and codec implementations
/// ([`raw::codec::sdmp7`], [`raw::codec::sdmp8`], [`raw::codec::sdmpc4`]).
pub mod raw {
    /// Per-file entry header serialization.
    pub mod entry;
    /// Error types for all srcdmp operations.
    pub mod error;
    /// File-level header: magic bytes, codec ID, version, and metadata.
    pub mod header;
    /// SDMP-7 tier-1 lookup table and the escape byte constant.
    pub mod table;

    /// Codec implementations for encoding and decoding file content.
    pub mod codec {
        /// Bit-level reader and writer (used by Huffman and C4 codecs).
        pub mod bits;
        /// Empirical character frequency table for Huffman tree construction.
        pub mod freq;
        /// Huffman tree builder and code generator.
        pub mod huffman;
        /// SDMP-7 three-tier codec.
        pub mod sdmp7;
        /// SDMP-8 raw UTF-8 passthrough codec.
        pub mod sdmp8;
        /// SDMP-C4 Huffman-based compression codec.
        pub mod sdmpc4;
    }
}

/// CLI tooling: logging and TUI viewer.
///
/// Only available when the `cli` feature is enabled.
#[cfg(feature = "cli")]
pub mod cli {
    /// Colored terminal logging helpers.
    pub mod log;
    /// Terminal-based interactive file browser (powered by ratatui).
    pub mod viewer;
}

#[cfg(feature = "fs")]
mod api;

#[cfg(feature = "fs")]
pub use api::{Codec, Dump, DumpBuilder, DumpWriteBuilder};

/// Decode a complete `.srcdmp` archive from an in-memory byte slice.
///
/// Parses the file header, then iterates over every file entry,
/// decoding each with the SDMP-7 codec and returning a map from
/// file path to decoded content.
///
/// # Errors
///
/// Returns [`SdmpError::UnexpectedEof`] if the input is truncated,
/// [`SdmpError::NotAnSdmpFile`] if the magic bytes are missing,
/// [`SdmpError::UnknownCodec`] if the codec ID byte is unrecognized,
/// [`SdmpError::InvalidUtf8`] if decoded content is not valid UTF-8,
/// or [`SdmpError::CorruptManifest`] for malformed entry data.
///
/// # Examples
///
/// ```rust
/// # use srcdmp::unpack_bytes;
/// // This example would need a valid .srcdmp byte slice.
/// // See the codec round-trip tests for full usage.
/// let empty: &[u8] = &[];
/// assert!(unpack_bytes(empty).is_err());
/// ```
pub fn unpack_bytes(data: &[u8]) -> Result<HashMap<String, String>, SdmpError> {
    let header = Header::read(data)?;
    let mut result = HashMap::new();

    let mut i = Header::SIZE;

    for _ in 0..header.file_count {
        let (entry, consumed) = Entry::read(&data[i..])?;
        i += consumed;

        let end = i + entry.file_size as usize;
        if end > data.len() {
            return Err(SdmpError::UnexpectedEof);
        }
        let encoded = &data[i..end];
        i = end;

        let content = decode(encoded)?;
        result.insert(entry.path, content);
    }

    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::raw::table::*;

    #[test]
    fn round_trip_tier1() {
        let encode = build_tier1_encode();
        for (index, slot) in TIER1_DECODE.iter().enumerate() {
            if let Some(c) = slot {
                assert_eq!(encode[c], index as u8);
            }
        }
    }

    #[test]
    fn header_round_trip() {
        use crate::raw::header::*;

        let original = Header {
            codec: CODEC_C4,
            version: 1,
            original_size: 123456,
            file_count: 34,
        };

        let bytes = original.write();
        let decoded = Header::read(&bytes).unwrap();

        assert_eq!(decoded.codec, original.codec);
        assert_eq!(decoded.version, original.version);
        assert_eq!(decoded.original_size, original.original_size);
        assert_eq!(decoded.file_count, original.file_count);
    }

    #[test]
    fn sdmp7_decode_basic() {
        use crate::raw::codec::sdmp7::decode;

        let bytes = vec![0x26, 0x27, 0x6F];
        let result = decode(&bytes).unwrap();
        assert_eq!(result, "hi!");
    }

    #[test]
    fn sdmp7_decode_escape() {
        use crate::raw::codec::sdmp7::decode;

        let bytes = vec![0x7F, 0x41];
        let result = decode(&bytes).unwrap();
        assert_eq!(result, "A");
    }

    #[test]
    fn sdmp7_round_trip() {
        use crate::raw::codec::sdmp7::{decode, encode};

        let original = "hello, world! fn main() { let x = 42; }";
        let encoded = encode(original);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn entry_round_trip() {
        use crate::raw::entry::Entry;

        let original = Entry {
            path: "src/main.rs".to_string(),
            file_size: 932,
        };

        let mut bytes = Vec::new();
        original.write(&mut bytes);

        let (decoded, consumed) = Entry::read(&bytes).unwrap();

        assert_eq!(decoded.path, original.path);
        assert_eq!(decoded.file_size, original.file_size);
        assert_eq!(consumed, 17);
    }

    #[test]
    fn sdmp7_tier2_round_trip() {
        use crate::raw::codec::sdmp7::{decode, encode};

        let original = "fn calculate(α: f64, β: f64) -> f64 { α + β }";
        let encoded = encode(original);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn sdmp7_tier3_round_trip() {
        use crate::raw::codec::sdmp7::{decode, encode};

        let original = "// 这是一个注释 hello world";
        let encoded = encode(original);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn sdmpc4_round_trip_basic() {
        use crate::raw::codec::sdmpc4::{decode, encode};

        let original = "fn main() {\n    let x = 42;\n}";
        let encoded = encode(original);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn sdmpc4_round_trip_unicode() {
        use crate::raw::codec::sdmpc4::{decode, encode};

        let original = "fn calculate(α: f64) -> f64 { α * 2.0 }";
        let encoded = encode(original);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(decoded, original);
    }

    #[test]
    fn sdmpc4_smaller_than_sdmp7() {
        use crate::raw::codec::sdmp7;
        use crate::raw::codec::sdmpc4;

        let input = include_str!("api.rs");
        let c4 = sdmpc4::encode(input);
        let s7 = sdmp7::encode(input);

        println!("sdmp7: {} bytes", s7.len());
        println!("sdmpc4: {} bytes", c4.len());

        assert!(c4.len() < s7.len(), "C4 should be smaller than SDMP-7");
    }
}