aedb 0.2.10

Embedded Rust storage engine with transactional commits, WAL durability, and snapshot-consistent reads
Documentation
use crate::backup::{
    BACKUP_ARCHIVE_CHUNK_BYTES, BACKUP_ARCHIVE_CHUNKED_FILE_THRESHOLD_BYTES,
    BACKUP_ARCHIVE_ENTRY_CHUNKED_FILE, BACKUP_ARCHIVE_ENTRY_FILE, BACKUP_ARCHIVE_MAGIC,
    BackupManifest, MAX_BACKUP_ARCHIVE_PATH_BYTES, MAX_BACKUP_ARCHIVE_PAYLOAD_BYTES,
    extract_backup_archive, resolve_backup_output_path, sha256_file_hex, validate_backup_manifest,
    verify_backup_files, write_backup_archive,
};
use crate::error::AedbError;
use std::collections::BTreeMap;

fn sample_manifest() -> BackupManifest {
    BackupManifest {
        backup_id: "bk_1".into(),
        backup_type: "full".into(),
        parent_backup_id: None,
        from_seq: None,
        created_at_micros: 1,
        aedb_version: "0.1.0".into(),
        checkpoint_seq: 1,
        wal_head_seq: 2,
        checkpoint_file: "checkpoint_1.aedbcp".into(),
        wal_segments: vec!["segment_2.aedbwal".into()],
        file_sha256: BTreeMap::from([
            ("checkpoint_1.aedbcp".into(), "a".repeat(64)),
            ("wal_tail/segment_2.aedbwal".into(), "b".repeat(64)),
        ]),
    }
}

#[test]
fn backup_manifest_rejects_unsafe_paths() {
    let mut manifest = sample_manifest();
    manifest.wal_segments = vec!["../segment_2.aedbwal".into()];
    let err = validate_backup_manifest(&manifest).expect_err("must reject parent path");
    assert!(matches!(err, AedbError::Validation(_)));
}

#[test]
fn backup_manifest_requires_wal_checksums() {
    let mut manifest = sample_manifest();
    manifest.file_sha256.remove("wal_tail/segment_2.aedbwal");
    let err = validate_backup_manifest(&manifest).expect_err("must require checksum");
    assert!(matches!(err, AedbError::Validation(_)));
}

#[test]
fn backup_manifest_rejects_invalid_checksum_hex() {
    let mut manifest = sample_manifest();
    manifest
        .file_sha256
        .insert("checkpoint_1.aedbcp".into(), "not_hex".into());
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("checkpoint_1.aedbcp"), b"x").expect("write");
    std::fs::create_dir_all(dir.path().join("wal_tail")).expect("wal dir");
    std::fs::write(dir.path().join("wal_tail/segment_2.aedbwal"), b"x").expect("write wal");
    let err = verify_backup_files(dir.path(), &manifest).expect_err("must reject checksum");
    assert!(matches!(err, AedbError::Validation(_)));
}

#[test]
fn backup_archive_roundtrip_plain_and_encrypted() {
    let src = tempfile::tempdir().expect("src");
    let dst_plain = tempfile::tempdir().expect("dst plain");
    let dst_enc = tempfile::tempdir().expect("dst enc");
    let archive_plain = src.path().join("backup_plain.aedbarc");
    let archive_enc = src.path().join("backup_enc.aedbarc");
    std::fs::create_dir_all(src.path().join("wal_tail")).expect("wal dir");
    std::fs::create_dir_all(src.path().join("pages")).expect("pages dir");
    std::fs::write(src.path().join("backup_manifest.json"), b"{\"x\":1}").expect("manifest");
    std::fs::write(src.path().join("wal_tail/segment_1.aedbwal"), b"segment").expect("wal");
    std::fs::write(src.path().join("pages/rows.aedbpg"), b"page-data").expect("page store file");

    write_backup_archive(src.path(), &archive_plain, None, 3).expect("write plain");
    extract_backup_archive(&archive_plain, dst_plain.path(), None).expect("extract plain");
    assert_eq!(
        std::fs::read(dst_plain.path().join("backup_manifest.json")).expect("read manifest"),
        b"{\"x\":1}".to_vec()
    );

    let key = [9u8; 32];
    write_backup_archive(src.path(), &archive_enc, Some(&key), 3).expect("write enc");
    extract_backup_archive(&archive_enc, dst_enc.path(), Some(&key)).expect("extract enc");
    assert_eq!(
        std::fs::read(dst_enc.path().join("wal_tail/segment_1.aedbwal")).expect("read wal"),
        b"segment".to_vec()
    );
    assert_eq!(
        std::fs::read(dst_enc.path().join("pages/rows.aedbpg")).expect("read page store file"),
        b"page-data".to_vec()
    );

    let wrong = [7u8; 32];
    let err = extract_backup_archive(
        &archive_enc,
        tempfile::tempdir().expect("tmp").path(),
        Some(&wrong),
    )
    .expect_err("wrong key must fail");
    assert!(format!("{err}").contains("decryption failed"));
}

#[test]
fn backup_archive_streams_large_page_file_in_encrypted_chunks() {
    let src = tempfile::tempdir().expect("src");
    let dst = tempfile::tempdir().expect("dst");
    let archive = src.path().join("backup_large_page.aedbarc");
    let page_rel = "pages/rows.aedbpg";
    let page_path = src.path().join(page_rel);
    std::fs::create_dir_all(page_path.parent().expect("page parent")).expect("pages dir");
    std::fs::write(src.path().join("backup_manifest.json"), b"{\"x\":1}").expect("manifest");

    let large_len =
        BACKUP_ARCHIVE_CHUNKED_FILE_THRESHOLD_BYTES as usize + BACKUP_ARCHIVE_CHUNK_BYTES + 17;
    let mut large_page = Vec::with_capacity(large_len);
    for i in 0..large_len {
        large_page.push(((i.wrapping_mul(31) ^ (i >> 7)) & 0xff) as u8);
    }
    std::fs::write(&page_path, &large_page).expect("large page file");

    let key = [11u8; 32];
    write_backup_archive(src.path(), &archive, Some(&key), 3).expect("write archive");
    let archive_bytes = std::fs::read(&archive).expect("read archive");
    assert!(
        archive_bytes.contains(&BACKUP_ARCHIVE_ENTRY_CHUNKED_FILE),
        "large page file should use chunked archive entry"
    );

    extract_backup_archive(&archive, dst.path(), Some(&key)).expect("extract archive");
    assert_eq!(
        sha256_file_hex(&page_path).expect("source hash"),
        sha256_file_hex(&dst.path().join(page_rel)).expect("restored hash")
    );
    assert_eq!(
        std::fs::read(dst.path().join("backup_manifest.json")).expect("manifest"),
        b"{\"x\":1}".to_vec()
    );

    let wrong = [12u8; 32];
    let err = extract_backup_archive(
        &archive,
        tempfile::tempdir().expect("bad dst").path(),
        Some(&wrong),
    )
    .expect_err("wrong key must fail");
    assert!(format!("{err}").contains("decryption failed"));
}

#[test]
fn backup_archive_rejects_oversized_path_and_payload_lengths() {
    let archive_path = tempfile::NamedTempFile::new().expect("archive");
    let out_dir = tempfile::tempdir().expect("out");

    let mut bytes = Vec::new();
    bytes.extend_from_slice(BACKUP_ARCHIVE_MAGIC);
    bytes.push(0);
    bytes.extend_from_slice(&[0u8; 16]);
    bytes.push(BACKUP_ARCHIVE_ENTRY_FILE);
    bytes.extend_from_slice(&(MAX_BACKUP_ARCHIVE_PATH_BYTES + 1).to_le_bytes());
    std::fs::write(archive_path.path(), &bytes).expect("write malformed archive");
    let err = extract_backup_archive(archive_path.path(), out_dir.path(), None)
        .expect_err("oversized path must be rejected");
    assert!(format!("{err}").contains("path exceeds max length"));

    let archive_path = tempfile::NamedTempFile::new().expect("archive");
    let out_dir = tempfile::tempdir().expect("out");
    let mut bytes = Vec::new();
    bytes.extend_from_slice(BACKUP_ARCHIVE_MAGIC);
    bytes.push(0);
    bytes.extend_from_slice(&[0u8; 16]);
    bytes.push(BACKUP_ARCHIVE_ENTRY_FILE);
    bytes.extend_from_slice(&(8u32).to_le_bytes());
    bytes.extend_from_slice(b"file.bin");
    bytes.extend_from_slice(&(MAX_BACKUP_ARCHIVE_PAYLOAD_BYTES + 1).to_le_bytes());
    std::fs::write(archive_path.path(), &bytes).expect("write malformed archive");
    let err = extract_backup_archive(archive_path.path(), out_dir.path(), None)
        .expect_err("oversized payload must be rejected");
    assert!(format!("{err}").contains("payload exceeds max size"));
}

#[cfg(unix)]
#[test]
fn backup_manifest_rejects_symlink_escape() {
    let mut manifest = sample_manifest();
    let dir = tempfile::tempdir().expect("tempdir");
    let outside = tempfile::tempdir().expect("outside");
    let outside_file = outside.path().join("outside.bin");
    std::fs::write(&outside_file, b"outside").expect("write outside");
    std::os::unix::fs::symlink(&outside_file, dir.path().join("checkpoint_1.aedbcp"))
        .expect("symlink checkpoint");
    std::fs::create_dir_all(dir.path().join("wal_tail")).expect("wal dir");
    std::fs::write(dir.path().join("wal_tail/segment_2.aedbwal"), b"x").expect("write wal");
    manifest.file_sha256.insert(
        "checkpoint_1.aedbcp".into(),
        sha256_file_hex(&outside_file).expect("hash"),
    );
    manifest.file_sha256.insert(
        "wal_tail/segment_2.aedbwal".into(),
        sha256_file_hex(&dir.path().join("wal_tail/segment_2.aedbwal")).expect("hash wal"),
    );
    let err = verify_backup_files(dir.path(), &manifest).expect_err("must reject escape");
    assert!(matches!(err, AedbError::Validation(_)));
}

#[cfg(unix)]
#[test]
fn backup_output_path_rejects_symlinked_parent() {
    let dir = tempfile::tempdir().expect("tempdir");
    let outside = tempfile::tempdir().expect("outside");
    std::os::unix::fs::symlink(outside.path(), dir.path().join("wal_tail"))
        .expect("symlink parent");
    let err = resolve_backup_output_path(dir.path(), "wal_tail/segment_1.aedbwal")
        .expect_err("must reject symlinked output parent");
    assert!(matches!(err, AedbError::Validation(_)));
}