This is the canonical source repository. Please report issues and submit pull requests through the GitHub repository. https://github.com/RinCatShrine/rcsp
Find a file
Rin Cat (鈴猫) 714b4cc73e
feat: initial RCSP implementation
Signed-off-by: Rin Cat (鈴猫) <rincat@rincat.dev>
2026-07-12 13:26:28 +09:00
src feat: initial RCSP implementation 2026-07-12 13:26:28 +09:00
.gitignore feat: initial RCSP implementation 2026-07-12 13:26:28 +09:00
Cargo.toml feat: initial RCSP implementation 2026-07-12 13:26:28 +09:00
LICENSE feat: initial RCSP implementation 2026-07-12 13:26:28 +09:00
README.md feat: initial RCSP implementation 2026-07-12 13:26:28 +09:00

RCSP — Rin Cat's Simple Pack

RCSP is a small, dependency-free container for bundling files into a Rust binary and decoding them at runtime.

Requires Rust 1.85 or newer.

Not a virtual filesystem

RCSP stores paths and file data. It does not provide mounting, lookup, mutation, directory objects, or filesystem metadata. For a virtual filesystem, see vfs, virtual-filesystem, or vfs-kit.

Input safety

RCSP does not enforce resource limits. Declared file counts, path lengths, and file sizes determine allocations, so untrusted input requires a validation layer with application-specific limits. Limiting the source reader alone does not limit the size of an attempted allocation.

Pack

Pack a directory to a file:

rcsp::pack_dir_to_file("assets", "assets.rcsp")?;

Pack generated data into memory:

let encoded = rcsp::pack([
    ("config.json", br#"{"enabled":true}"#.as_slice()),
    ("message.txt", b"hello".as_slice()),
])?;

Pack selected filesystem files:

let encoded = rcsp::pack_paths([
    ("config.json", "assets/config.json"),
    ("images/icon.png", "assets/icon.png"),
])?;

The corresponding output functions are:

  • pack, pack_to_file
  • pack_paths, pack_paths_to_file, pack_paths_to_writer
  • pack_dir, pack_dir_to_file, pack_dir_to_writer

Writer provides direct control over streaming output:

use std::fs::File;

let source = File::open("assets/data.bin")?;
let mut writer = rcsp::Writer::new(File::create("assets.rcsp")?, 1)?;
writer.write_file("data.bin", source.metadata()?.len(), source)?;
writer.finish()?;

After any write_file I/O error, discard the Writer and its output; a partial entry may already have been written.

The *_to_file functions write through a temporary file in the destination directory. Replacement behavior follows std::fs::rename; Windows may reject replacing an existing file.

Read

Embed and decode the complete bundle:

static BUNDLE: &[u8] = include_bytes!("../assets.rcsp");

let pack = rcsp::Pack::from_bytes(BUNDLE)?;
for file in pack.files {
    println!("{}: {} bytes", file.path, file.size());
}

Or read one complete file at a time:

static BUNDLE: &[u8] = include_bytes!("../assets.rcsp");

let mut reader = rcsp::Reader::new(BUNDLE)?;
while let Some(file) = reader.next_file()? {
    println!("{}: {} bytes", file.path, file.size());
}

Trailing bytes after the declared files are ignored.

Compression

Compression wraps the complete RCSP stream and is not part of the format.

Pack a directory directly into Zstandard:

use std::fs::File;

let encoder = zstd::stream::write::Encoder::new(File::create("assets.rcsp.zst")?, 3)?;
let encoder = rcsp::pack_dir_to_writer("assets", encoder)?;
encoder.finish()?;

Decode an embedded compressed bundle with any Read decoder:

static ZSTD: &[u8] = include_bytes!("../assets.rcsp.zst");
let decoder = zstd::stream::read::Decoder::new(ZSTD)?;
let pack = rcsp::Pack::read_from(decoder)?;

static XZ: &[u8] = include_bytes!("../assets.rcsp.xz");
let decoder = xz2::read::XzDecoder::new(XZ);
let pack = rcsp::Pack::read_from(decoder)?;

static GZIP: &[u8] = include_bytes!("../assets.rcsp.gz");
let decoder = flate2::read::GzDecoder::new(GZIP);
let pack = rcsp::Pack::read_from(decoder)?;

lzma-rs decodes into a buffer:

static LZMA: &[u8] = include_bytes!("../assets.rcsp.lzma");

let mut input = LZMA;
let mut decoded = Vec::new();
lzma_rs::lzma_decompress(&mut input, &mut decoded)?;
let pack = rcsp::Pack::from_bytes(&decoded)?;

Format

All integers are little-endian.

"RCSP" | version:u8 | file_count:u32

repeated file_count times:
path_length:u32 | file_size:u64 | path:UTF-8 | data

Paths are unique, non-empty UTF-8 relative paths using /. Empty, ., and .. components, backslashes, and NUL are rejected.

Directory packing is deterministic. File and directory symlinks are followed, including targets outside the source directory. Cycles, broken links, and non-regular entries are skipped. RCSP does not store compression, checksums, timestamps, permissions, directory entries, or symlink metadata.