obj-cli 1.0.0

Command-line tools (dump, check, stat, backup) for the obj embedded document database.
Documentation
//! Flip a single byte inside a `.obj` file's primary B-tree root so
//! the CLI smoke test (issue #100) can confirm `obj check` detects
//! corruption on every host OS.
//!
//! Usage:
//!
//! ```sh
//! cargo run --release -p obj-cli --example corrupt_one_byte -- <path>
//! ```
//!
//! The helper opens `<path>` at the pager layer, looks up the
//! `smoke_users` collection's primary B-tree root via the catalog
//! (matching `seed_for_cli`), checkpoints + closes so the WAL view
//! does not mask the on-disk bytes, then seeks to a position inside
//! the root page's slot directory and writes `0xFF`. Re-running
//! the helper against an already-corrupted file is a no-op for the
//! caller's purposes (the same flip).
//!
//! Cross-platform: no `dd` / `fsutil` divergence — every host the
//! workspace targets has the same `std::fs::OpenOptions` semantics.

#![forbid(unsafe_code)]

use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};
use std::process::ExitCode;

use obj_core::pager::page::PAGE_SIZE;
use obj_core::pager::{Config, Pager};
use obj_core::platform::FileHandle;
use obj_core::Catalog;

fn main() -> ExitCode {
    let mut args = std::env::args_os().skip(1);
    let Some(path_os) = args.next() else {
        eprintln!("usage: corrupt_one_byte <path>");
        return ExitCode::from(2);
    };
    let path = std::path::PathBuf::from(path_os);

    // Checkpoint + close so the WAL no longer shadows the bytes we
    // are about to overwrite — mirrors the recipe in
    // `crates/obj/tests/integrity.rs`.
    match Pager::<FileHandle>::open(&path, Config::default()) {
        Ok(p) => {
            if let Err(e) = p.close() {
                eprintln!("corrupt_one_byte: checkpoint failed: {e}");
                return ExitCode::from(2);
            }
        }
        Err(e) => {
            eprintln!("corrupt_one_byte: open for checkpoint failed: {e}");
            return ExitCode::from(2);
        }
    }

    let target_pid = match locate_primary_root(&path) {
        Ok(pid) => pid,
        Err(e) => {
            eprintln!("corrupt_one_byte: locate failed: {e}");
            return ExitCode::from(2);
        }
    };

    let mut file = match OpenOptions::new().read(true).write(true).open(&path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("corrupt_one_byte: open for write failed: {e}");
            return ExitCode::from(2);
        }
    };
    let target_off = target_pid
        .saturating_mul(PAGE_SIZE as u64)
        .saturating_add(64);
    if let Err(e) = file.seek(SeekFrom::Start(target_off)) {
        eprintln!("corrupt_one_byte: seek failed: {e}");
        return ExitCode::from(2);
    }
    if let Err(e) = file.write_all(&[0xFFu8]) {
        eprintln!("corrupt_one_byte: write failed: {e}");
        return ExitCode::from(2);
    }
    println!("corrupt_one_byte: flipped byte at offset {target_off}");
    ExitCode::SUCCESS
}

/// Read the catalog to locate `smoke_users`' primary B-tree root.
fn locate_primary_root(path: &std::path::Path) -> obj::Result<u64> {
    let mut pager = Pager::<FileHandle>::open(path, Config::default())?;
    pager.begin_txn();
    let catalog = Catalog::<FileHandle>::open_or_init(&mut pager)?;
    let descriptor =
        catalog
            .get(&mut pager, "smoke_users")?
            .ok_or(obj::Error::CollectionNotFound {
                name: "smoke_users".to_owned(),
            })?;
    pager.end_txn();
    Ok(descriptor.primary_root)
}