decdn-bao-range 0.0.1

Leaf crate: iroh-blobs-free bao verified-range helpers (chunk-group alignment, encoded-size, range encode/verify) shared by decdn-cache (serve/import) and decdn-client (receive). Depends only on bao-tree so the CLI's iroh-blobs-free invariant (#578) holds.
Documentation
//! Filesystem-origin layout contract and outboard seeding.
//!
//! A filesystem cache origin stores each blob as a sharded data object
//! `{base}/{hex[0..2]}/{hex}` with a sibling pre-order bao outboard
//! `{base}/{hex[0..2]}/{hex}.obao4`
//! ([ADR 037 §Origin-tier pull-through](../../../adr/037-regional-proxy-warming.md)).
//! The reader side lives in `decdn-cache` (`origin::fs` / `origin::s3`); the
//! writer side is `decdn origin import`. Both derive the sibling key and the
//! outboard bytes from this module, so a blob written here is one the daemon
//! reads back verbatim.
//!
//! The outboard is a `bao-tree` pre-order outboard at [`crate::IROH_BLOCK_SIZE`]
//! (16 KiB chunk groups). A stock `bao encode --outboard` uses a different chunk
//! group size and produces a file the daemon rejects, so the outboard **cannot**
//! be produced by generic shell tooling — it must come from this shared encoder.

use std::io::Read;

use bao_tree::io::outboard::PreOrderOutboard;
use bao_tree::io::sync::CreateOutboard;

use crate::IROH_BLOCK_SIZE;

/// Sibling-key suffix for the published pre-order bao outboard (`{H}.obao4`),
/// per [ADR 037 §Origin-tier pull-through](../../../adr/037-regional-proxy-warming.md).
/// Shared across the filesystem / S3 reader adapters and the `origin import`
/// writer so an operator `aws s3 sync`-ing between backends keeps the same
/// object names.
pub const OBAO4_SUFFIX: &str = ".obao4";

/// The two-character shard prefix for a blob's lowercase-hex address:
/// `{base}/{hex[0..2]}/{hex}`. The first two hex chars shard the directory so a
/// content set with millions of entries doesn't land in a single dirent list.
///
/// Panic-free: a `hex` shorter than two chars (never produced by a real BLAKE3
/// address, but the workspace's anti-indexing lint forbids `&hex[..2]`) yields
/// the empty prefix rather than panicking.
#[must_use]
pub fn shard_prefix(hex: &str) -> &str {
    hex.get(..2).unwrap_or("")
}

/// A blob's content address plus its pre-order bao outboard, ready to write to a
/// filesystem origin as the sharded data object and its `{hex}.obao4` sibling.
#[derive(Debug, Clone)]
pub struct EncodedOutboard {
    /// The BLAKE3 root hash as 64 lowercase hex characters — the data object's
    /// file name and the shard prefix source.
    pub hash_hex: String,
    /// The pre-order bao outboard bytes, byte-identical to what the cache
    /// reader (`origin::fs::fetch_outboard`) expects at the `{hex}.obao4` key.
    pub outboard: Vec<u8>,
}

impl EncodedOutboard {
    /// The two-character shard prefix for this blob's address.
    #[must_use]
    pub fn shard(&self) -> &str {
        shard_prefix(&self.hash_hex)
    }

    /// The `{hex}.obao4` sibling file name for this blob.
    #[must_use]
    pub fn obao4_name(&self) -> String {
        format!("{}{OBAO4_SUFFIX}", self.hash_hex)
    }
}

/// Stream `reader` (exactly `size` bytes) once, computing the BLAKE3 root hash
/// and the pre-order bao outboard at [`crate::IROH_BLOCK_SIZE`] in a single
/// pass. `size` must equal the number of bytes `reader` yields — it frames the
/// bao tree, and a wrong value produces an outboard the reader rejects.
///
/// The source bytes are **not** buffered in memory; only the outboard (roughly
/// `1/256` of the blob at 16 KiB groups) is retained, so a multi-gigabyte blob
/// costs a few megabytes of RAM here rather than its whole length.
pub fn encode_outboard(reader: impl Read, size: u64) -> std::io::Result<EncodedOutboard> {
    let ob = PreOrderOutboard::<Vec<u8>>::create_sized(reader, size, IROH_BLOCK_SIZE)?;
    Ok(EncodedOutboard {
        hash_hex: ob.root.to_hex().to_string(),
        outboard: ob.data,
    })
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // tests
mod tests;