revault_lockbox_api 0.0.6

reVault lockbox API to create and manage lockboxes
Documentation
use crate::{Lockbox, LockboxPath};
use std::fs::File;
use std::io::{Read, Result as IoResult};
use std::path::{Path, PathBuf};

const KEY: &[u8] = b"compression regression key";
const MIB: usize = 1024 * 1024;

fn p(path: impl AsRef<str>) -> LockboxPath {
    LockboxPath::new(path).unwrap()
}

#[test]
#[ignore = "compression corpus regression; GitHub Actions runs this explicitly"]
fn repeated_small_files_keep_meaningful_compression() {
    let file_count = 100_000usize;
    let file_size = 1024usize;
    let mut lockbox = Lockbox::create(KEY);
    let payload = repeated_small_payload(file_size);
    lockbox
        .create_parent_dirs_for(&p("/small/file.bin"))
        .unwrap();

    for index in 0..file_count {
        lockbox
            .add_file(&p(format!("/small/file-{index:05}.bin")), &payload, false)
            .unwrap();
    }
    lockbox.commit().unwrap();

    let logical = file_count * file_size;
    let lockbox_len = lockbox.to_bytes().len();

    assert!(
        lockbox_len < logical,
        "lockbox should compress repeated small-file corpus: lockbox={lockbox_len}, logical={logical}"
    );
}

#[test]
#[ignore = "compression corpus regression; GitHub Actions runs this explicitly"]
fn moderately_large_zero_file_uses_few_fixed_pages() {
    let logical = 256 * MIB;
    let mut lockbox = Lockbox::create(KEY);
    lockbox
        .create_parent_dirs_for(&p("/large/zero.bin"))
        .unwrap();

    lockbox
        .add_file_from_reader(
            &p("/large/zero.bin"),
            corpus_reader("zero-1g.bin", logical, Pattern::Zero),
            false,
        )
        .unwrap();
    lockbox.commit().unwrap();

    let lockbox_len = lockbox.to_bytes().len();
    assert!(
        lockbox_len <= 32 * MIB,
        "zero corpus should stay within four 8 MiB pages: lockbox={lockbox_len}"
    );
}

#[test]
#[ignore = "production-scale compression regression; GitHub Actions runs this explicitly"]
fn one_gib_zero_file_compression_does_not_regress() {
    let logical = 1024 * MIB;
    let mut lockbox = Lockbox::create(KEY);
    lockbox
        .create_parent_dirs_for(&p("/large/zero.bin"))
        .unwrap();

    lockbox
        .add_file_from_reader(
            &p("/large/zero.bin"),
            corpus_reader("zero-1g.bin", logical, Pattern::Zero),
            false,
        )
        .unwrap();
    lockbox.commit().unwrap();

    let lockbox_len = lockbox.to_bytes().len();
    assert!(
        lockbox_len <= 32 * MIB,
        "1 GiB zero corpus should stay within four 8 MiB pages: lockbox={lockbox_len}"
    );
}

#[test]
#[ignore = "production-scale compression regression; GitHub Actions runs this explicitly"]
fn one_gib_high_entropy_file_avoids_excessive_expansion() {
    let logical = 1024 * MIB;
    let mut lockbox = Lockbox::create(KEY);
    lockbox
        .create_parent_dirs_for(&p("/large/randomish.bin"))
        .unwrap();

    lockbox
        .add_file_from_reader(
            &p("/large/randomish.bin"),
            corpus_reader("randomish-1g.bin", logical, Pattern::Randomish),
            false,
        )
        .unwrap();
    lockbox.commit().unwrap();

    let lockbox_len = lockbox.to_bytes().len();
    assert!(
        lockbox_len <= logical + 32 * MIB,
        "1 GiB high-entropy corpus should not expand by more than four pages: lockbox={lockbox_len}"
    );
}

fn repeated_small_payload(len: usize) -> Vec<u8> {
    if let Some(path) = corpus_file("small-repeated-1k.bin") {
        if let Ok(bytes) = std::fs::read(path) {
            if bytes.len() == len {
                return bytes;
            }
        }
    }
    vec![b'x'; len]
}

fn corpus_reader(name: &str, len: usize, pattern: Pattern) -> Box<dyn Read> {
    if let Some(path) = corpus_file(name) {
        if let Ok(file) = File::open(path) {
            return Box::new(file.take(len as u64));
        }
    }
    Box::new(PatternReader::new(len, pattern))
}

fn corpus_file(name: &str) -> Option<PathBuf> {
    let root = std::env::var_os("LOCKBOX_COMPRESSION_CORPUS_DIR")?;
    Some(Path::new(&root).join(name))
}

struct PatternReader {
    remaining: usize,
    offset: usize,
    pattern: Pattern,
}

enum Pattern {
    Zero,
    Randomish,
}

impl PatternReader {
    fn new(bytes: usize, pattern: Pattern) -> Self {
        Self {
            remaining: bytes,
            offset: 0,
            pattern,
        }
    }
}

impl Read for PatternReader {
    fn read(&mut self, out: &mut [u8]) -> IoResult<usize> {
        let len = out.len().min(self.remaining);
        if len == 0 {
            return Ok(0);
        }
        match self.pattern {
            Pattern::Zero => out[..len].fill(0),
            Pattern::Randomish => {
                for (i, byte) in out[..len].iter_mut().enumerate() {
                    let n = (self.offset + i) as u64;
                    let mut x = n.wrapping_add(0x9e37_79b9_7f4a_7c15);
                    x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
                    x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
                    *byte = (x ^ (x >> 31)) as u8;
                }
            }
        }
        self.offset += len;
        self.remaining -= len;
        Ok(len)
    }
}