| Crates.io | engram-rs |
| lib.rs | engram-rs |
| version | 1.1.1 |
| created_at | 2025-12-20 23:29:11.14169+00 |
| updated_at | 2025-12-25 04:09:38.825454+00 |
| description | Unified engram archive library with manifest, signatures, and VFS support |
| homepage | https://github.com/blackfall-labs/engram-rs |
| repository | https://github.com/blackfall-labs/engram-rs |
| max_upload_size | |
| id | 1997096 |
| size | 668,988 |
A unified Rust library for creating, reading, and managing Engram archives - compressed, cryptographically signed archive files with embedded metadata and SQLite database support.
Add this to your Cargo.toml:
[dependencies]
engram-rs = "1.0"
use engram_rs::{ArchiveWriter, CompressionMethod};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new archive
let mut writer = ArchiveWriter::create("my_archive.eng")?;
// Add files with automatic compression
writer.add_file("readme.txt", b"Hello, Engram!")?;
writer.add_file("data.json", br#"{"version": "1.0"}"#)?;
// Add file from disk
writer.add_file_from_disk("config.toml", std::path::Path::new("./config.toml"))?;
// Finalize the archive (writes central directory)
writer.finalize()?;
Ok(())
}
use engram_rs::ArchiveReader;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open existing archive (convenience method)
let mut reader = ArchiveReader::open_and_init("my_archive.eng")?;
// List all files
for filename in reader.list_files() {
println!("📄 {}", filename);
}
// Read a specific file
let data = reader.read_file("readme.txt")?;
println!("Content: {}", String::from_utf8_lossy(&data));
Ok(())
}
use engram_rs::{ArchiveWriter, Manifest, Author};
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate a keypair for signing
let signing_key = SigningKey::generate(&mut OsRng);
// Create manifest
let mut manifest = Manifest::new(
"my-archive".to_string(),
"My Archive".to_string(),
Author::new("John Doe"),
"1.0.0".to_string(),
);
// Sign the manifest
manifest.sign(&signing_key, Some("John Doe".to_string()))?;
// Create archive with signed manifest
let mut writer = ArchiveWriter::create("signed_archive.eng")?;
writer.add_file("data.txt", b"Important data")?;
writer.add_manifest(&serde_json::to_value(&manifest)?)?;
writer.finalize()?;
// Later: verify the signature
let mut reader = ArchiveReader::open_and_init("signed_archive.eng")?;
if let Some(manifest_value) = reader.read_manifest()? {
let loaded_manifest: Manifest =
Manifest::from_json(&serde_json::to_vec(&manifest_value)?)?;
let results = loaded_manifest.verify_signatures()?;
println!("Signature valid: {}", results[0]);
}
Ok(())
}
use engram_rs::VfsReader;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open archive with VFS
let mut vfs = VfsReader::open("archive_with_db.eng")?;
// Open embedded SQLite database
let conn = vfs.open_database("data.db")?;
// Execute SQL queries
let mut stmt = conn.prepare("SELECT name, email FROM users WHERE active = 1")?;
let users = stmt.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
for user in users {
let (name, email) = user?;
println!("{} <{}>", name, email);
}
Ok(())
}
Engram uses a custom binary format (v1.0) with the following structure:
┌─────────────────────────────────────────┐
│ File Header (64 bytes) │
│ - Magic: 0x89 'E' 'N' 'G' 0x0D 0x0A 0x1A 0x0A │
│ - Format version (major.minor) │
│ - Central directory offset/size │
│ - Entry count, content version │
│ - CRC32 checksum │
├─────────────────────────────────────────┤
│ Local Entry Header 1 (LOCA) │
│ Compressed File Data 1 │
├─────────────────────────────────────────┤
│ Local Entry Header 2 (LOCA) │
│ Compressed File Data 2 │
├─────────────────────────────────────────┤
│ ... │
├─────────────────────────────────────────┤
│ Central Directory │
│ - Entry 1 (320 bytes fixed) │
│ - Entry 2 (320 bytes fixed) │
│ - ... │
├─────────────────────────────────────────┤
│ End of Central Directory (ENDR) │
├─────────────────────────────────────────┤
│ manifest.json (optional) │
│ - Metadata, author, signatures │
└─────────────────────────────────────────┘
Key Features:
See ENGRAM_SPECIFICATION.md for complete binary format specification.
The library automatically selects compression based on file type and size:
| File Type | Size | Compression | Typical Ratio |
|---|---|---|---|
| Text files (.txt, .json, .md, etc.) | ≥ 4KB | Zstd (best ratio) | 50-100x |
| Binary files (.db, .wasm, etc.) | ≥ 4KB | LZ4 (fastest) | 2-5x |
| Already compressed (.png, .jpg, .zip, etc.) | Any | None | 1x |
| Small files | < 4KB | None | N/A |
| Large files | ≥ 50MB | Frame-based | Varies |
Compression Performance:
You can also manually specify compression:
writer.add_file_with_compression("data.bin", data, CompressionMethod::Zstd)?;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
// Generate keypair
let signing_key = SigningKey::generate(&mut OsRng);
// Sign manifest
manifest.sign(&signing_key, Some("Author Name".to_string()))?;
// Verify signatures
let results = manifest.verify_signatures()?;
println!("All signatures valid: {}", results.iter().all(|&v| v));
Security:
// Encrypt individual files (per-file encryption)
writer.add_encrypted_file("secret.txt", password, data)?;
// Decrypt when reading
let data = reader.read_encrypted_file("secret.txt", password)?;
Encryption Modes:
Benchmarks on a test file (10MB, Intel i7-12700K, NVMe SSD):
| Compression | Write Speed | Read Speed | Ratio |
|---|---|---|---|
| None | 450 MB/s | 500 MB/s | 1.0x |
| LZ4 | 380 MB/s | 420 MB/s | 2.1x |
| Zstd | 95 MB/s | 180 MB/s | 3.8x |
Scalability (tested):
VFS Performance:
engram-rs has undergone comprehensive testing across 4 major phases:
Coverage:
Findings:
Coverage:
Findings:
Operations Tested:
Coverage:
Findings:
Stress Tests (run with --ignored):
Coverage:
Findings:
Path Security:
Compression Security:
Cryptographic Security:
Verdict: No critical security vulnerabilities found. engram-rs is production-ready with proper application-level path sanitization.
Comprehensive testing documentation:
ArchiveWriter - Create and write to archivesArchiveReader - Read from existing archivesVfsReader - Query SQLite databases in archivesManifest - Archive metadata and signaturesCompressionMethod - Compression algorithm selectionEngramError - Error types| Operation | Method |
|---|---|
| Create archive | ArchiveWriter::create(path) |
| Open archive | ArchiveReader::open_and_init(path) |
| Open encrypted | ArchiveReader::open_encrypted(path, key) |
| Add file | writer.add_file(name, data) |
| Add from disk | writer.add_file_from_disk(name, path) |
| Read file | reader.read_file(name) |
| List files | reader.list_files() |
| Add manifest | writer.add_manifest(manifest) |
| Sign manifest | manifest.sign(key, signer) |
| Verify signatures | manifest.verify_signatures() |
| Query database | vfs.open_database(name) |
See the examples/ directory for complete examples:
basic.rs - Creating and reading archivesmanifest.rs - Working with manifests and signaturescompression.rs - Compression optionsvfs.rs - Querying embedded databasesRun examples with:
cargo run --example basic
cargo run --example manifest
cargo run --example vfs
# Run all tests (fast)
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test file
cargo test --test corruption_test
# Run stress tests (large archives, many files)
cargo test --test stress_large_archives_test -- --ignored --nocapture
Test Execution Time:
--ignored)This library replaces the previous two-crate structure:
// Old
use engram_core::{ArchiveReader, ArchiveWriter};
use engram_vfs::VfsReader;
// New (engram-rs)
use engram_rs::{ArchiveReader, ArchiveWriter, VfsReader};
All functionality is now unified in a single crate with improved APIs:
open_and_init() convenience method (was: open() then initialize())open_encrypted() convenience method for encrypted archivesengram-rs does not reject path traversal attempts during archive creation. Applications must sanitize paths during extraction:
use std::path::{Path, PathBuf};
fn safe_extract_path(archive_path: &str, dest_root: &Path) -> Result<PathBuf, &'static str> {
let normalized = archive_path.replace('\\', '/');
// Reject absolute paths
if normalized.starts_with('/') || normalized.contains(':') {
return Err("Absolute paths not allowed");
}
// Reject parent directory references
if normalized.contains("..") {
return Err("Parent directory references not allowed");
}
// Build final path and verify it's within dest_root
let final_path = dest_root.join(&normalized);
if !final_path.starts_with(dest_root) {
return Err("Path escapes destination directory");
}
Ok(final_path)
}
Always verify signatures before trusting archive contents:
let manifest: Manifest = Manifest::from_json(&manifest_data)?;
let results = manifest.verify_signatures()?;
if !results.iter().all(|&valid| valid) {
return Err("Invalid signature detected");
}
For untrusted archives, set resource limits:
# Unix/Linux: Set memory limit
ulimit -v 1048576 # 1GB virtual memory limit
# Monitor decompression size
if decompressed_size > max_allowed_size {
return Err("Decompression size exceeds limit");
}
Contributions are welcome! See CONTRIBUTING.md for guidelines.
Licensed under the MIT License.