use std::collections::HashSet;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const BUNDLE_MAGIC: &str = "BRPROJ";
pub const BUNDLE_FORMAT_VERSION: u32 = 1;
pub const MANIFEST_PATH: &str = "MANIFEST";
pub const KV_MEMBER_PATH: &str = "kv.brkvdump";
pub const IDENTITY_MEMBER_PATH: &str = "identity/identity.brid";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemberKind {
PgDump,
MysqlDump,
Libsql,
Kv,
Blob,
Identity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PgMemberInfo {
pub server_version_num: String,
pub client_image: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema_fingerprint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestMember {
pub path: String,
pub kind: MemberKind,
pub size: u64,
pub sha256: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pg: Option<PgMemberInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceInfo {
pub project: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub site: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Manifest {
pub magic: String,
pub format_version: u32,
pub created_at: u64,
pub data_only: bool,
pub source: SourceInfo,
pub members: Vec<ManifestMember>,
}
#[derive(Debug, thiserror::Error)]
pub enum BundleError {
#[error("bundle io: {0}")]
Io(#[from] io::Error),
#[error("bundle manifest json: {0}")]
Json(#[from] serde_json::Error),
#[error("not a boatramp project bundle: expected magic {expected:?}, found {found:?}")]
BadMagic {
expected: String,
found: String,
},
#[error("unsupported bundle format version {found} (this build reads {supported})")]
BadVersion {
found: u32,
supported: u32,
},
#[error("bundle is missing its {0:?} entry as the first entry")]
MissingManifest(String),
#[error("checksum MISMATCH for member {path:?}: manifest {expected}, extracted {actual}")]
ChecksumMismatch {
path: String,
expected: String,
actual: String,
},
#[error("member {0:?} named in the manifest is absent from the bundle")]
MissingMember(String),
#[error("member {0:?} is present in the bundle but absent from the manifest")]
UnexpectedMember(String),
#[error("unsafe member path {0:?} in bundle (absolute or traversal)")]
UnsafePath(String),
#[error("malformed v2 bundle stream: {0}")]
MalformedStream(String),
#[error("size MISMATCH for member {path:?}: declared {expected}, streamed {actual}")]
SizeMismatch {
path: String,
expected: u64,
actual: u64,
},
}
impl Manifest {
pub fn validate(&self) -> Result<(), BundleError> {
if self.magic != BUNDLE_MAGIC {
return Err(BundleError::BadMagic {
expected: BUNDLE_MAGIC.to_string(),
found: self.magic.clone(),
});
}
if self.format_version != BUNDLE_FORMAT_VERSION {
return Err(BundleError::BadVersion {
found: self.format_version,
supported: BUNDLE_FORMAT_VERSION,
});
}
Ok(())
}
pub fn pg_members(&self) -> impl Iterator<Item = &ManifestMember> {
self.members.iter().filter(|m| m.kind == MemberKind::PgDump)
}
}
#[derive(Debug, Clone)]
pub struct StagedMember {
pub path: String,
pub kind: MemberKind,
pub source: PathBuf,
pub pg: Option<PgMemberInfo>,
}
fn validate_member_path(path: &str) -> Result<(), BundleError> {
use std::path::Component;
if path.is_empty() {
return Err(BundleError::UnsafePath(path.to_string()));
}
let p = Path::new(path);
if p.is_absolute() {
return Err(BundleError::UnsafePath(path.to_string()));
}
for c in p.components() {
if !matches!(c, Component::Normal(_)) {
return Err(BundleError::UnsafePath(path.to_string()));
}
}
Ok(())
}
fn size_and_sha256<R: Read>(mut r: R) -> io::Result<(u64, String)> {
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
let mut total = 0u64;
loop {
let n = r.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
total += n as u64;
}
Ok((total, hex::encode(hasher.finalize())))
}
fn append_bytes<W: Write>(
builder: &mut tar::Builder<W>,
path: &str,
mtime: u64,
data: &[u8],
) -> Result<(), BundleError> {
let mut header = tar::Header::new_gnu();
header.set_size(data.len() as u64);
header.set_mode(0o600);
header.set_mtime(mtime);
builder.append_data(&mut header, path, data)?;
Ok(())
}
pub fn write_bundle(
out: &Path,
created_at: u64,
data_only: bool,
source: SourceInfo,
staged: &[StagedMember],
) -> Result<Manifest, BundleError> {
let mut members = Vec::with_capacity(staged.len());
for m in staged {
validate_member_path(&m.path)?;
let f = std::fs::File::open(&m.source)?;
let (size, sha256) = size_and_sha256(io::BufReader::new(f))?;
members.push(ManifestMember {
path: m.path.clone(),
kind: m.kind,
size,
sha256,
pg: m.pg.clone(),
});
}
let manifest = Manifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION,
created_at,
data_only,
source,
members,
};
manifest.validate()?;
let file = std::fs::File::create(out)?;
let mut builder = tar::Builder::new(io::BufWriter::new(file));
let manifest_json = serde_json::to_vec_pretty(&manifest)?;
append_bytes(&mut builder, MANIFEST_PATH, created_at, &manifest_json)?;
for (sm, mm) in staged.iter().zip(&manifest.members) {
let mut f = std::fs::File::open(&sm.source)?;
let mut header = tar::Header::new_gnu();
header.set_size(mm.size);
header.set_mode(0o600);
header.set_mtime(created_at);
builder.append_data(&mut header, &sm.path, &mut f)?;
}
let mut inner = builder.into_inner()?;
inner.flush()?;
Ok(manifest)
}
pub fn read_manifest(bundle: &Path) -> Result<Manifest, BundleError> {
let file = std::fs::File::open(bundle)?;
let mut ar = tar::Archive::new(io::BufReader::new(file));
let mut entries = ar.entries()?;
let first = entries
.next()
.ok_or_else(|| BundleError::MissingManifest(MANIFEST_PATH.to_string()))??;
let path = first.path()?.to_string_lossy().into_owned();
if path != MANIFEST_PATH {
return Err(BundleError::MissingManifest(MANIFEST_PATH.to_string()));
}
let mut buf = String::new();
let mut first = first;
first.read_to_string(&mut buf)?;
let manifest: Manifest = serde_json::from_str(&buf)?;
manifest.validate()?;
Ok(manifest)
}
pub fn extract_and_verify(bundle: &Path, out_dir: &Path) -> Result<Manifest, BundleError> {
let manifest = read_manifest(bundle)?;
let file = std::fs::File::open(bundle)?;
let mut ar = tar::Archive::new(io::BufReader::new(file));
let mut seen: HashSet<String> = HashSet::new();
for entry in ar.entries()? {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().into_owned();
if path == MANIFEST_PATH {
continue;
}
validate_member_path(&path)?;
let mm = manifest
.members
.iter()
.find(|m| m.path == path)
.ok_or_else(|| BundleError::UnexpectedMember(path.clone()))?;
let dst = out_dir.join(&path);
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
let mut hasher = Sha256::new();
let mut f = io::BufWriter::new(std::fs::File::create(&dst)?);
let mut buf = [0u8; 64 * 1024];
loop {
let n = entry.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
f.write_all(&buf[..n])?;
}
f.flush()?;
let actual = hex::encode(hasher.finalize());
if actual != mm.sha256 {
return Err(BundleError::ChecksumMismatch {
path,
expected: mm.sha256.clone(),
actual,
});
}
seen.insert(path);
}
for m in &manifest.members {
if !seen.contains(&m.path) {
return Err(BundleError::MissingMember(m.path.clone()));
}
}
Ok(manifest)
}
pub const BUNDLE_FORMAT_VERSION_V2: u32 = 2;
pub const META_SUFFIX: &str = ".brmeta";
pub const BUNDLE_FORMAT_VERSION_V3: u32 = 3;
pub const TRAILER_SUFFIX: &str = ".brtrailer";
pub const PART_INFIX: &str = ".part";
fn chunk_part_path(base: &str, n: u32) -> String {
format!("{base}{PART_INFIX}{n:05}")
}
fn chunk_trailer_path(base: &str) -> String {
format!("{base}{TRAILER_SUFFIX}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemberFraming {
#[default]
Sidecar,
ChunkedTrailer,
}
impl MemberFraming {
fn is_sidecar(&self) -> bool {
matches!(self, MemberFraming::Sidecar)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberSpec {
pub path: String,
pub kind: MemberKind,
#[serde(default, skip_serializing_if = "MemberFraming::is_sidecar")]
pub framing: MemberFraming,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pg: Option<PgMemberInfo>,
}
impl MemberSpec {
pub fn sidecar(path: String, kind: MemberKind, pg: Option<PgMemberInfo>) -> Self {
Self {
path,
kind,
framing: MemberFraming::Sidecar,
pg,
}
}
pub fn chunked(path: String, kind: MemberKind, pg: Option<PgMemberInfo>) -> Self {
Self {
path,
kind,
framing: MemberFraming::ChunkedTrailer,
pg,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberTrailer {
pub size: u64,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StreamManifest {
pub magic: String,
pub format_version: u32,
pub created_at: u64,
pub data_only: bool,
pub source: SourceInfo,
pub members: Vec<MemberSpec>,
}
impl StreamManifest {
fn validate(&self) -> Result<(), BundleError> {
if self.magic != BUNDLE_MAGIC {
return Err(BundleError::BadMagic {
expected: BUNDLE_MAGIC.to_string(),
found: self.magic.clone(),
});
}
if self.format_version != BUNDLE_FORMAT_VERSION_V2
&& self.format_version != BUNDLE_FORMAT_VERSION_V3
{
return Err(BundleError::BadVersion {
found: self.format_version,
supported: BUNDLE_FORMAT_VERSION_V3,
});
}
Ok(())
}
fn meta_path(path: &str) -> String {
format!("{path}{META_SUFFIX}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberMeta {
pub size: u64,
pub sha256: String,
}
pub struct BundleStreamWriter<W: Write> {
builder: tar::Builder<W>,
created_at: u64,
remaining: std::collections::VecDeque<MemberSpec>,
}
impl<W: Write> BundleStreamWriter<W> {
pub fn new(
out: W,
created_at: u64,
data_only: bool,
source: SourceInfo,
specs: Vec<MemberSpec>,
) -> Result<Self, BundleError> {
for s in &specs {
validate_member_path(&s.path)?;
}
let format_version = if specs
.iter()
.any(|s| s.framing == MemberFraming::ChunkedTrailer)
{
BUNDLE_FORMAT_VERSION_V3
} else {
BUNDLE_FORMAT_VERSION_V2
};
let manifest = StreamManifest {
magic: BUNDLE_MAGIC.to_string(),
format_version,
created_at,
data_only,
source,
members: specs.clone(),
};
manifest.validate()?;
let mut builder = tar::Builder::new(out);
let manifest_json = serde_json::to_vec_pretty(&manifest)?;
append_bytes(&mut builder, MANIFEST_PATH, created_at, &manifest_json)?;
Ok(Self {
builder,
created_at,
remaining: specs.into_iter().collect(),
})
}
fn next_expected(
&mut self,
path: &str,
kind: MemberKind,
framing: MemberFraming,
) -> Result<(), BundleError> {
let expected = self.remaining.pop_front().ok_or_else(|| {
BundleError::MalformedStream(format!(
"append of {path:?} but the MANIFEST declared no more members"
))
})?;
if expected.path != path || expected.kind != kind || expected.framing != framing {
return Err(BundleError::MalformedStream(format!(
"append of ({path:?}, {kind:?}, {framing:?}) does not match the next declared \
member ({:?}, {:?}, {:?})",
expected.path, expected.kind, expected.framing
)));
}
validate_member_path(path)
}
pub fn append_member(
&mut self,
path: &str,
kind: MemberKind,
src: &Path,
) -> Result<(), BundleError> {
self.next_expected(path, kind, MemberFraming::Sidecar)?;
let size = std::fs::metadata(src)?.len();
let (hashed, sha256) = size_and_sha256(io::BufReader::new(std::fs::File::open(src)?))?;
if hashed != size {
return Err(BundleError::SizeMismatch {
path: path.to_string(),
expected: size,
actual: hashed,
});
}
let meta = MemberMeta { size, sha256 };
let meta_json = serde_json::to_vec(&meta)?;
append_bytes(
&mut self.builder,
&StreamManifest::meta_path(path),
self.created_at,
&meta_json,
)?;
let mut f = std::fs::File::open(src)?;
let mut header = tar::Header::new_gnu();
header.set_size(size);
header.set_mode(0o600);
header.set_mtime(self.created_at);
self.builder.append_data(&mut header, path, &mut f)?;
Ok(())
}
pub fn append_member_chunked(
&mut self,
path: &str,
kind: MemberKind,
body: &mut dyn Read,
chunk_size: usize,
) -> Result<(), BundleError> {
self.next_expected(path, kind, MemberFraming::ChunkedTrailer)?;
let chunk_size = chunk_size.max(1);
let mut hasher = Sha256::new();
let mut total = 0u64;
let mut part: u32 = 0;
let mut buf = vec![0u8; chunk_size];
loop {
let mut filled = 0usize;
while filled < chunk_size {
let n = body.read(&mut buf[filled..])?;
if n == 0 {
break;
}
filled += n;
}
if filled == 0 {
break; }
hasher.update(&buf[..filled]);
total += filled as u64;
let mut header = tar::Header::new_gnu();
header.set_size(filled as u64);
header.set_mode(0o600);
header.set_mtime(self.created_at);
self.builder
.append_data(&mut header, chunk_part_path(path, part), &buf[..filled])?;
part = part.checked_add(1).ok_or_else(|| {
BundleError::MalformedStream(format!("chunked member {path:?} exceeded 2^32 parts"))
})?;
if filled < chunk_size {
break; }
}
let trailer = MemberTrailer {
size: total,
sha256: hex::encode(hasher.finalize()),
};
let trailer_json = serde_json::to_vec(&trailer)?;
append_bytes(
&mut self.builder,
&chunk_trailer_path(path),
self.created_at,
&trailer_json,
)?;
Ok(())
}
pub fn finish(mut self) -> Result<(), BundleError> {
if let Some(missing) = self.remaining.pop_front() {
return Err(BundleError::MissingMember(missing.path));
}
let mut inner = self.builder.into_inner()?;
inner.flush()?;
Ok(())
}
}
pub fn write_bundle_streaming<W, S>(
out: W,
created_at: u64,
data_only: bool,
source: SourceInfo,
specs: Vec<MemberSpec>,
mut stage: S,
) -> Result<(), BundleError>
where
W: Write,
S: FnMut(&MemberSpec) -> Result<PathBuf, BundleError>,
{
let mut writer = BundleStreamWriter::new(out, created_at, data_only, source, specs.clone())?;
for spec in &specs {
let tmp = stage(spec)?;
let appended = writer.append_member(&spec.path, spec.kind, &tmp);
let _ = std::fs::remove_file(&tmp);
appended?;
}
writer.finish()
}
#[allow(clippy::too_many_arguments)] pub fn write_bundle_streaming_v3<W, S, O>(
out: W,
created_at: u64,
data_only: bool,
source: SourceInfo,
specs: Vec<MemberSpec>,
chunk_size: usize,
mut stage: S,
mut open: O,
) -> Result<(), BundleError>
where
W: Write,
S: FnMut(&MemberSpec) -> Result<PathBuf, BundleError>,
O: FnMut(&MemberSpec) -> Result<Box<dyn Read>, BundleError>,
{
let mut writer = BundleStreamWriter::new(out, created_at, data_only, source, specs.clone())?;
for spec in &specs {
match spec.framing {
MemberFraming::Sidecar => {
let tmp = stage(spec)?;
let appended = writer.append_member(&spec.path, spec.kind, &tmp);
let _ = std::fs::remove_file(&tmp);
appended?;
}
MemberFraming::ChunkedTrailer => {
let mut body = open(spec)?;
writer.append_member_chunked(&spec.path, spec.kind, body.as_mut(), chunk_size)?;
}
}
}
writer.finish()
}
struct HashingReader<R: Read> {
inner: R,
hasher: Sha256,
count: u64,
}
impl<R: Read> HashingReader<R> {
fn new(inner: R) -> Self {
Self {
inner,
hasher: Sha256::new(),
count: 0,
}
}
fn finish(self) -> (u64, String) {
(self.count, hex::encode(self.hasher.finalize()))
}
}
impl<R: Read> Read for HashingReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.inner.read(buf)?;
if n > 0 {
self.hasher.update(&buf[..n]);
self.count += n as u64;
}
Ok(n)
}
}
fn peek_format_version(manifest_json: &str) -> Result<u32, BundleError> {
#[derive(Deserialize)]
struct VersionPeek {
#[serde(default)]
magic: String,
format_version: u32,
}
let peek: VersionPeek = serde_json::from_str(manifest_json)?;
if peek.magic != BUNDLE_MAGIC {
return Err(BundleError::BadMagic {
expected: BUNDLE_MAGIC.to_string(),
found: peek.magic,
});
}
Ok(peek.format_version)
}
fn verify_member<F>(
body: &mut dyn Read,
spec: &MemberSpec,
expected_size: u64,
expected_sha: &str,
restore: &mut F,
) -> Result<(), BundleError>
where
F: FnMut(&MemberSpec, u64, &mut dyn Read) -> Result<(), BundleError>,
{
let mut counting = HashingReader::new(body);
restore(spec, expected_size, &mut counting)?;
let mut sink = [0u8; 64 * 1024];
while counting.read(&mut sink)? != 0 {}
let (count, sha) = counting.finish();
if count != expected_size {
return Err(BundleError::SizeMismatch {
path: spec.path.clone(),
expected: expected_size,
actual: count,
});
}
if sha != expected_sha {
return Err(BundleError::ChecksumMismatch {
path: spec.path.clone(),
expected: expected_sha.to_string(),
actual: sha,
});
}
Ok(())
}
fn stage_and_verify_member<F>(
body: &mut dyn Read,
spec: &MemberSpec,
expected_size: u64,
expected_sha: &str,
stage_dir: &Path,
counter: &mut u64,
restore: &mut F,
) -> Result<(), BundleError>
where
F: FnMut(&MemberSpec, &Path) -> Result<(), BundleError>,
{
let temp = stage_dir.join(format!("brimport-{counter}"));
*counter += 1;
let mut hasher = Sha256::new();
let mut total = 0u64;
{
let mut f = io::BufWriter::new(std::fs::File::create(&temp)?);
let mut buf = [0u8; 64 * 1024];
loop {
let n = body.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
total += n as u64;
f.write_all(&buf[..n])?;
}
f.flush()?;
}
let sha = hex::encode(hasher.finalize());
if total != expected_size {
let _ = std::fs::remove_file(&temp);
return Err(BundleError::SizeMismatch {
path: spec.path.clone(),
expected: expected_size,
actual: total,
});
}
if sha != expected_sha {
let _ = std::fs::remove_file(&temp);
return Err(BundleError::ChecksumMismatch {
path: spec.path.clone(),
expected: expected_sha.to_string(),
actual: sha,
});
}
let r = restore(spec, &temp);
let _ = std::fs::remove_file(&temp);
r
}
pub fn read_bundle_streaming<R, F>(src: R, restore: F) -> Result<Vec<MemberSpec>, BundleError>
where
R: Read,
F: FnMut(&MemberSpec, u64, &mut dyn Read) -> Result<(), BundleError>,
{
read_bundle_streaming_with_head(src, |_data_only, _source| {}, restore)
}
pub fn read_bundle_streaming_with_head<R, H, F>(
src: R,
on_head: H,
mut restore: F,
) -> Result<Vec<MemberSpec>, BundleError>
where
R: Read,
H: FnMut(bool, &SourceInfo),
F: FnMut(&MemberSpec, u64, &mut dyn Read) -> Result<(), BundleError>,
{
read_bundle_core(src, on_head, |spec, size, sha, body| {
verify_member(body, spec, size, sha, &mut restore)
})
}
pub fn read_bundle_streaming_staged<R, H, F>(
src: R,
stage_dir: &Path,
on_head: H,
mut restore: F,
) -> Result<Vec<MemberSpec>, BundleError>
where
R: Read,
H: FnMut(bool, &SourceInfo),
F: FnMut(&MemberSpec, &Path) -> Result<(), BundleError>,
{
let mut counter = 0u64;
read_bundle_core(src, on_head, |spec, size, sha, body| {
stage_and_verify_member(body, spec, size, sha, stage_dir, &mut counter, &mut restore)
})
}
fn read_bundle_core<R, H, M>(
src: R,
mut on_head: H,
mut per_member: M,
) -> Result<Vec<MemberSpec>, BundleError>
where
R: Read,
H: FnMut(bool, &SourceInfo),
M: FnMut(&MemberSpec, u64, &str, &mut dyn Read) -> Result<(), BundleError>,
{
let mut ar = tar::Archive::new(src);
let mut entries = ar.entries()?;
let mut first = entries
.next()
.ok_or_else(|| BundleError::MissingManifest(MANIFEST_PATH.to_string()))??;
let fpath = first.path()?.to_string_lossy().into_owned();
if fpath != MANIFEST_PATH {
return Err(BundleError::MissingManifest(MANIFEST_PATH.to_string()));
}
let mut manifest_json = String::new();
first.read_to_string(&mut manifest_json)?;
let version = peek_format_version(&manifest_json)?;
let mut restored: Vec<MemberSpec> = Vec::new();
if version == BUNDLE_FORMAT_VERSION {
let manifest: Manifest = serde_json::from_str(&manifest_json)?;
manifest.validate()?;
on_head(manifest.data_only, &manifest.source);
let mut seen: HashSet<String> = HashSet::new();
for entry in entries {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().into_owned();
validate_member_path(&path)?;
let mm = manifest
.members
.iter()
.find(|m| m.path == path)
.ok_or_else(|| BundleError::UnexpectedMember(path.clone()))?;
let spec = MemberSpec::sidecar(mm.path.clone(), mm.kind, mm.pg.clone());
per_member(&spec, mm.size, &mm.sha256, &mut entry)?;
seen.insert(path);
restored.push(spec);
}
for m in &manifest.members {
if !seen.contains(&m.path) {
return Err(BundleError::MissingMember(m.path.clone()));
}
}
} else if version == BUNDLE_FORMAT_VERSION_V2 {
let manifest: StreamManifest = serde_json::from_str(&manifest_json)?;
manifest.validate()?;
on_head(manifest.data_only, &manifest.source);
let mut specs = manifest.members.iter();
let mut pending_meta: Option<(String, MemberMeta)> = None;
for entry in entries {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().into_owned();
if let Some(stripped) = path.strip_suffix(META_SUFFIX) {
if pending_meta.is_some() {
return Err(BundleError::MalformedStream(format!(
"sidecar {path:?} arrived with a prior sidecar still unmatched"
)));
}
let mut buf = String::new();
entry.read_to_string(&mut buf)?;
let meta: MemberMeta = serde_json::from_str(&buf)?;
pending_meta = Some((stripped.to_string(), meta));
continue;
}
validate_member_path(&path)?;
let (meta_path, meta) = pending_meta.take().ok_or_else(|| {
BundleError::MalformedStream(format!("member {path:?} with no preceding sidecar"))
})?;
if meta_path != path {
return Err(BundleError::MalformedStream(format!(
"sidecar for {meta_path:?} does not match the following member {path:?}"
)));
}
let spec = specs
.next()
.ok_or_else(|| BundleError::UnexpectedMember(path.clone()))?;
if spec.path != path {
return Err(BundleError::MalformedStream(format!(
"member {path:?} out of declared order (MANIFEST expected {:?})",
spec.path
)));
}
per_member(spec, meta.size, &meta.sha256, &mut entry)?;
restored.push(spec.clone());
}
if let Some((mp, _)) = pending_meta.take() {
return Err(BundleError::MissingMember(mp));
}
if let Some(missing) = specs.next() {
return Err(BundleError::MissingMember(missing.path.clone()));
}
} else {
return Err(BundleError::BadVersion {
found: version,
supported: BUNDLE_FORMAT_VERSION_V2,
});
}
Ok(restored)
}
pub trait BundleRestorer {
fn restore_sidecar(&mut self, spec: &MemberSpec, temp: &Path) -> Result<(), BundleError>;
fn begin_chunked(&mut self, spec: &MemberSpec) -> Result<(), BundleError>;
fn write_chunk(&mut self, spec: &MemberSpec, buf: &[u8]) -> Result<(), BundleError>;
fn finish_chunked(&mut self, spec: &MemberSpec, verified: bool) -> Result<(), BundleError>;
}
pub fn read_bundle_streaming_atomic<R, H>(
src: R,
stage_dir: &Path,
mut on_head: H,
restorer: &mut dyn BundleRestorer,
) -> Result<Vec<MemberSpec>, BundleError>
where
R: Read,
H: FnMut(bool, &SourceInfo),
{
let mut ar = tar::Archive::new(src);
let mut entries = ar.entries()?;
let mut first = entries
.next()
.ok_or_else(|| BundleError::MissingManifest(MANIFEST_PATH.to_string()))??;
let fpath = first.path()?.to_string_lossy().into_owned();
if fpath != MANIFEST_PATH {
return Err(BundleError::MissingManifest(MANIFEST_PATH.to_string()));
}
let mut manifest_json = String::new();
first.read_to_string(&mut manifest_json)?;
let version = peek_format_version(&manifest_json)?;
let mut counter = 0u64;
let mut restored: Vec<MemberSpec> = Vec::new();
if version == BUNDLE_FORMAT_VERSION {
let manifest: Manifest = serde_json::from_str(&manifest_json)?;
manifest.validate()?;
on_head(manifest.data_only, &manifest.source);
let mut seen: HashSet<String> = HashSet::new();
for entry in entries {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().into_owned();
validate_member_path(&path)?;
let mm = manifest
.members
.iter()
.find(|m| m.path == path)
.ok_or_else(|| BundleError::UnexpectedMember(path.clone()))?;
let spec = MemberSpec::sidecar(mm.path.clone(), mm.kind, mm.pg.clone());
stage_and_verify_member(
&mut entry,
&spec,
mm.size,
&mm.sha256,
stage_dir,
&mut counter,
&mut |s, t| restorer.restore_sidecar(s, t),
)?;
seen.insert(path);
restored.push(spec);
}
for m in &manifest.members {
if !seen.contains(&m.path) {
return Err(BundleError::MissingMember(m.path.clone()));
}
}
} else if version == BUNDLE_FORMAT_VERSION_V2 || version == BUNDLE_FORMAT_VERSION_V3 {
let manifest: StreamManifest = serde_json::from_str(&manifest_json)?;
manifest.validate()?;
on_head(manifest.data_only, &manifest.source);
for spec in manifest.members {
match spec.framing {
MemberFraming::Sidecar => {
let mut meta_entry = entries
.next()
.ok_or_else(|| BundleError::MissingMember(spec.path.clone()))??;
let mpath = meta_entry.path()?.to_string_lossy().into_owned();
let stripped = mpath.strip_suffix(META_SUFFIX).ok_or_else(|| {
BundleError::MalformedStream(format!(
"expected sidecar {:?} for member {:?}, found {mpath:?}",
StreamManifest::meta_path(&spec.path),
spec.path
))
})?;
if stripped != spec.path {
return Err(BundleError::MalformedStream(format!(
"sidecar {mpath:?} does not match the declared member {:?}",
spec.path
)));
}
let mut mbuf = String::new();
meta_entry.read_to_string(&mut mbuf)?;
let meta: MemberMeta = serde_json::from_str(&mbuf)?;
let mut body_entry = entries
.next()
.ok_or_else(|| BundleError::MissingMember(spec.path.clone()))??;
let bpath = body_entry.path()?.to_string_lossy().into_owned();
validate_member_path(&bpath)?;
if bpath != spec.path {
return Err(BundleError::MalformedStream(format!(
"member {bpath:?} out of declared order (MANIFEST expected {:?})",
spec.path
)));
}
stage_and_verify_member(
&mut body_entry,
&spec,
meta.size,
&meta.sha256,
stage_dir,
&mut counter,
&mut |s, t| restorer.restore_sidecar(s, t),
)?;
restored.push(spec);
}
MemberFraming::ChunkedTrailer => {
read_chunked_member(&mut entries, &spec, restorer)?;
restored.push(spec);
}
}
}
if let Some(extra) = entries.next() {
let extra = extra?;
let path = extra.path()?.to_string_lossy().into_owned();
return Err(BundleError::UnexpectedMember(path));
}
} else {
return Err(BundleError::BadVersion {
found: version,
supported: BUNDLE_FORMAT_VERSION_V3,
});
}
Ok(restored)
}
fn read_chunked_member<R: Read>(
entries: &mut tar::Entries<'_, R>,
spec: &MemberSpec,
restorer: &mut dyn BundleRestorer,
) -> Result<(), BundleError> {
restorer.begin_chunked(spec)?;
let mut hasher = Sha256::new();
let mut total: u64 = 0;
let mut next_part: u32 = 0;
let mut outcome: Result<MemberTrailer, BundleError> =
Err(BundleError::MalformedStream(format!(
"chunked member {:?} ended before its {TRAILER_SUFFIX} trailer",
spec.path
)));
loop {
let entry = match entries.next() {
Some(Ok(e)) => e,
Some(Err(e)) => {
outcome = Err(BundleError::Io(e));
break;
}
None => break, };
let mut entry = entry;
let epath = match entry.path() {
Ok(p) => p.to_string_lossy().into_owned(),
Err(e) => {
outcome = Err(BundleError::Io(e));
break;
}
};
if let Some(stripped) = epath.strip_suffix(TRAILER_SUFFIX) {
if stripped != spec.path {
outcome = Err(BundleError::MalformedStream(format!(
"trailer {epath:?} does not match the chunked member {:?}",
spec.path
)));
break;
}
let mut tbuf = String::new();
if let Err(e) = entry.read_to_string(&mut tbuf) {
outcome = Err(BundleError::Io(e));
break;
}
outcome = serde_json::from_str::<MemberTrailer>(&tbuf).map_err(BundleError::Json);
break;
}
let expected = chunk_part_path(&spec.path, next_part);
if epath != expected {
outcome = Err(BundleError::MalformedStream(format!(
"chunked member {:?}: expected chunk {expected:?}, found {epath:?} \
(missing/out-of-order part or wrong member)",
spec.path
)));
break;
}
let mut rbuf = [0u8; 64 * 1024];
let part_res: Result<(), BundleError> = loop {
match entry.read(&mut rbuf) {
Ok(0) => break Ok(()),
Ok(n) => {
hasher.update(&rbuf[..n]);
total += n as u64;
if let Err(e) = restorer.write_chunk(spec, &rbuf[..n]) {
break Err(e);
}
}
Err(e) => break Err(BundleError::Io(e)),
}
};
if let Err(e) = part_res {
outcome = Err(e);
break;
}
next_part = match next_part.checked_add(1) {
Some(v) => v,
None => {
outcome = Err(BundleError::MalformedStream(format!(
"chunked member {:?} exceeded 2^32 parts",
spec.path
)));
break;
}
};
}
match outcome {
Ok(trailer) => {
let actual = hex::encode(hasher.finalize());
let verified = total == trailer.size && actual == trailer.sha256;
restorer.finish_chunked(spec, verified)?;
if total != trailer.size {
return Err(BundleError::SizeMismatch {
path: spec.path.clone(),
expected: trailer.size,
actual: total,
});
}
if actual != trailer.sha256 {
return Err(BundleError::ChecksumMismatch {
path: spec.path.clone(),
expected: trailer.sha256,
actual,
});
}
Ok(())
}
Err(e) => {
let _ = restorer.finish_chunked(spec, false);
Err(e)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn write_staging(dir: &Path, name: &str, bytes: &[u8]) -> PathBuf {
let p = dir.join(name);
std::fs::write(&p, bytes).unwrap();
p
}
#[test]
fn round_trips_a_bundle_and_verifies_members() {
let td = tempfile::tempdir().unwrap();
let stage = td.path().join("stage");
std::fs::create_dir_all(&stage).unwrap();
let kv = write_staging(&stage, "kv.bin", b"BRKVDUMP\x01\x02\x03");
let pg = write_staging(&stage, "app.pgc", &vec![7u8; 5000]);
let bundle = td.path().join("out.brproj");
let staged = vec![
StagedMember {
path: KV_MEMBER_PATH.to_string(),
kind: MemberKind::Kv,
source: kv,
pg: None,
},
StagedMember {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
source: pg,
pg: Some(PgMemberInfo {
server_version_num: "160004".to_string(),
client_image: "pgvector/pgvector:pg16".to_string(),
schema_fingerprint: Some("deadbeef".to_string()),
}),
},
];
let written = write_bundle(
&bundle,
1_700_000_000,
true,
SourceInfo {
project: "acme".to_string(),
site: None,
},
&staged,
)
.unwrap();
assert_eq!(written.magic, BUNDLE_MAGIC);
assert!(written.data_only);
assert_eq!(written.pg_members().count(), 1);
let m = read_manifest(&bundle).unwrap();
assert_eq!(m, written);
assert_eq!(
m.pg_members()
.next()
.unwrap()
.pg
.as_ref()
.unwrap()
.client_image,
"pgvector/pgvector:pg16"
);
let out = td.path().join("extract");
let m2 = extract_and_verify(&bundle, &out).unwrap();
assert_eq!(m2, written);
assert_eq!(
std::fs::read(out.join("sql/app.pgc")).unwrap(),
vec![7u8; 5000]
);
assert_eq!(
std::fs::read(out.join(KV_MEMBER_PATH)).unwrap(),
b"BRKVDUMP\x01\x02\x03"
);
}
#[test]
fn rejects_a_foreign_tar_and_a_future_version() {
let td = tempfile::tempdir().unwrap();
let bad = td.path().join("bad.tar");
{
let mut b = tar::Builder::new(std::fs::File::create(&bad).unwrap());
let m = Manifest {
magic: "NOTBRPROJ".to_string(),
format_version: BUNDLE_FORMAT_VERSION,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "x".to_string(),
site: None,
},
members: vec![],
};
let j = serde_json::to_vec(&m).unwrap();
append_bytes(&mut b, MANIFEST_PATH, 0, &j).unwrap();
b.finish().unwrap();
}
assert!(matches!(
read_manifest(&bad),
Err(BundleError::BadMagic { .. })
));
let futv = td.path().join("futv.tar");
{
let mut b = tar::Builder::new(std::fs::File::create(&futv).unwrap());
let m = Manifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION + 99,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "x".to_string(),
site: None,
},
members: vec![],
};
let j = serde_json::to_vec(&m).unwrap();
append_bytes(&mut b, MANIFEST_PATH, 0, &j).unwrap();
b.finish().unwrap();
}
assert!(matches!(
read_manifest(&futv),
Err(BundleError::BadVersion { .. })
));
}
#[test]
fn extract_catches_a_checksum_mismatch() {
let td = tempfile::tempdir().unwrap();
let bad = td.path().join("tamper.tar");
{
let mut b = tar::Builder::new(std::fs::File::create(&bad).unwrap());
let m = Manifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "x".to_string(),
site: None,
},
members: vec![ManifestMember {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
size: 3,
sha256: "00".repeat(32), pg: None,
}],
};
let j = serde_json::to_vec(&m).unwrap();
append_bytes(&mut b, MANIFEST_PATH, 0, &j).unwrap();
append_bytes(&mut b, "sql/app.pgc", 0, b"abc").unwrap();
b.finish().unwrap();
}
let out = td.path().join("x");
assert!(matches!(
extract_and_verify(&bad, &out),
Err(BundleError::ChecksumMismatch { .. })
));
}
#[test]
fn rejects_unsafe_member_paths() {
assert!(validate_member_path("sql/app.pgc").is_ok());
assert!(validate_member_path("libsql/acme/site/db.sqlite").is_ok());
assert!(validate_member_path("/etc/passwd").is_err());
assert!(validate_member_path("../escape").is_err());
assert!(validate_member_path("a/../b").is_err());
assert!(validate_member_path("").is_err());
}
#[test]
fn v2_round_trips_streaming_write_then_single_pass_read() {
let td = tempfile::tempdir().unwrap();
let stage_dir = td.path().join("stage");
std::fs::create_dir_all(&stage_dir).unwrap();
let members: Vec<(MemberSpec, Vec<u8>)> = vec![
(
MemberSpec {
path: IDENTITY_MEMBER_PATH.to_string(),
kind: MemberKind::Identity,
framing: MemberFraming::Sidecar,
pg: None,
},
b"IDENTITY-KEK-TRANSPORT".to_vec(),
),
(
MemberSpec {
path: "blobs/acme/obj1".to_string(),
kind: MemberKind::Blob,
framing: MemberFraming::Sidecar,
pg: None,
},
vec![0xAB; 4096],
),
(
MemberSpec {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
framing: MemberFraming::Sidecar,
pg: Some(PgMemberInfo {
server_version_num: "160004".to_string(),
client_image: "pgvector/pgvector:pg16".to_string(),
schema_fingerprint: Some("deadbeef".to_string()),
}),
},
vec![7u8; 50_000],
),
(
MemberSpec {
path: KV_MEMBER_PATH.to_string(),
kind: MemberKind::Kv,
framing: MemberFraming::Sidecar,
pg: None,
},
b"BRKVDUMP\x01\x02\x03".to_vec(),
),
];
let specs: Vec<MemberSpec> = members.iter().map(|(s, _)| s.clone()).collect();
let bytes_by_path: std::collections::HashMap<String, Vec<u8>> = members
.iter()
.map(|(s, b)| (s.path.clone(), b.clone()))
.collect();
let mut bundle: Vec<u8> = Vec::new();
let mut idx = 0u32;
write_bundle_streaming(
&mut bundle,
1_700_000_000,
false,
SourceInfo {
project: "acme".to_string(),
site: Some("prod".to_string()),
},
specs.clone(),
|spec| {
let data = bytes_by_path.get(&spec.path).expect("a known member");
let tmp = stage_dir.join(format!("m{idx}"));
idx += 1;
std::fs::write(&tmp, data).unwrap();
Ok(tmp)
},
)
.unwrap();
let mut got: Vec<(MemberSpec, Vec<u8>)> = Vec::new();
let restored = read_bundle_streaming(bundle.as_slice(), |spec, size, body| {
let mut v = Vec::new();
body.read_to_end(&mut v).map_err(BundleError::Io)?;
assert_eq!(v.len() as u64, size, "declared size == streamed bytes");
got.push((spec.clone(), v));
Ok(())
})
.unwrap();
assert_eq!(restored, specs, "members restored in the declared order");
assert_eq!(got.len(), members.len());
for ((ws, wb), (gs, gb)) in members.iter().zip(&got) {
assert_eq!(ws, gs);
assert_eq!(wb, gb, "member {:?} bytes round-trip", ws.path);
}
let pg = restored
.iter()
.find(|m| m.kind == MemberKind::PgDump)
.unwrap();
assert_eq!(
pg.pg.as_ref().unwrap().client_image,
"pgvector/pgvector:pg16"
);
}
#[test]
fn read_bundle_streaming_reads_a_v1_bundle() {
let td = tempfile::tempdir().unwrap();
let stage = td.path().join("stage");
std::fs::create_dir_all(&stage).unwrap();
let kv = write_staging(&stage, "kv.bin", b"BRKVDUMP\x01\x02\x03");
let pg = write_staging(&stage, "app.pgc", &vec![7u8; 5000]);
let bundle = td.path().join("v1.brproj");
let staged = vec![
StagedMember {
path: KV_MEMBER_PATH.to_string(),
kind: MemberKind::Kv,
source: kv,
pg: None,
},
StagedMember {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
source: pg,
pg: Some(PgMemberInfo {
server_version_num: "160004".to_string(),
client_image: "pgvector/pgvector:pg16".to_string(),
schema_fingerprint: None,
}),
},
];
write_bundle(
&bundle,
1_700_000_000,
true,
SourceInfo {
project: "acme".to_string(),
site: None,
},
&staged,
)
.unwrap();
let file = std::fs::File::open(&bundle).unwrap();
let mut got: std::collections::HashMap<String, Vec<u8>> = std::collections::HashMap::new();
let restored = read_bundle_streaming(io::BufReader::new(file), |spec, _size, body| {
let mut v = Vec::new();
body.read_to_end(&mut v).map_err(BundleError::Io)?;
got.insert(spec.path.clone(), v);
Ok(())
})
.unwrap();
assert_eq!(restored.len(), 2);
assert_eq!(got.get(KV_MEMBER_PATH).unwrap(), b"BRKVDUMP\x01\x02\x03");
assert_eq!(got.get("sql/app.pgc").unwrap(), &vec![7u8; 5000]);
}
#[test]
fn read_bundle_streaming_catches_a_tampered_member() {
let mut buf: Vec<u8> = Vec::new();
{
let mut b = tar::Builder::new(&mut buf);
let manifest = StreamManifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION_V2,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "x".to_string(),
site: None,
},
members: vec![MemberSpec {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
framing: MemberFraming::Sidecar,
pg: None,
}],
};
append_bytes(
&mut b,
MANIFEST_PATH,
0,
&serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let meta = MemberMeta {
size: 3,
sha256: "00".repeat(32), };
append_bytes(
&mut b,
"sql/app.pgc.brmeta",
0,
&serde_json::to_vec(&meta).unwrap(),
)
.unwrap();
append_bytes(&mut b, "sql/app.pgc", 0, b"abc").unwrap();
b.finish().unwrap();
}
let r = read_bundle_streaming(buf.as_slice(), |_s, _sz, body| {
let mut v = Vec::new();
body.read_to_end(&mut v).map_err(BundleError::Io)?;
Ok(())
});
assert!(
matches!(r, Err(BundleError::ChecksumMismatch { .. })),
"got {r:?}"
);
}
#[test]
fn read_bundle_streaming_rejects_a_member_without_its_sidecar() {
let mut buf: Vec<u8> = Vec::new();
{
let mut b = tar::Builder::new(&mut buf);
let manifest = StreamManifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION_V2,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "x".to_string(),
site: None,
},
members: vec![MemberSpec {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
framing: MemberFraming::Sidecar,
pg: None,
}],
};
append_bytes(
&mut b,
MANIFEST_PATH,
0,
&serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
append_bytes(&mut b, "sql/app.pgc", 0, b"abc").unwrap();
b.finish().unwrap();
}
let r = read_bundle_streaming(buf.as_slice(), |_s, _sz, _b| Ok(()));
assert!(
matches!(r, Err(BundleError::MalformedStream(_))),
"got {r:?}"
);
}
#[test]
fn write_bundle_streaming_refuses_an_out_of_order_or_missing_member() {
let specs = vec![
MemberSpec {
path: "a".to_string(),
kind: MemberKind::Blob,
framing: MemberFraming::Sidecar,
pg: None,
},
MemberSpec {
path: "b".to_string(),
kind: MemberKind::Blob,
framing: MemberFraming::Sidecar,
pg: None,
},
];
let td = tempfile::tempdir().unwrap();
let tmp = td.path().join("x");
std::fs::write(&tmp, b"hello").unwrap();
let mut sink: Vec<u8> = Vec::new();
let mut w = BundleStreamWriter::new(
&mut sink,
0,
true,
SourceInfo {
project: "x".to_string(),
site: None,
},
specs.clone(),
)
.unwrap();
assert!(matches!(
w.append_member("b", MemberKind::Blob, &tmp),
Err(BundleError::MalformedStream(_))
));
let mut sink2: Vec<u8> = Vec::new();
let mut w2 = BundleStreamWriter::new(
&mut sink2,
0,
true,
SourceInfo {
project: "x".to_string(),
site: None,
},
specs,
)
.unwrap();
w2.append_member("a", MemberKind::Blob, &tmp).unwrap();
assert!(matches!(w2.finish(), Err(BundleError::MissingMember(_))));
}
fn staged_bytes(dir: &Path) -> u64 {
std::fs::read_dir(dir)
.unwrap()
.filter_map(|e| e.ok())
.filter_map(|e| e.metadata().ok())
.filter(|m| m.is_file())
.map(|m| m.len())
.sum()
}
#[test]
fn write_bundle_streaming_bounds_staging_to_the_largest_member() {
let td = tempfile::tempdir().unwrap();
let stage_dir = td.path().join("stage");
std::fs::create_dir_all(&stage_dir).unwrap();
const MEMBER: usize = 256 * 1024;
const N: usize = 6;
let largest = MEMBER as u64;
let whole = (MEMBER * N) as u64;
let specs: Vec<MemberSpec> = (0..N)
.map(|i| MemberSpec {
path: format!("blobs/acme/obj{i}"),
kind: MemberKind::Blob,
framing: MemberFraming::Sidecar,
pg: None,
})
.collect();
let mut peak: u64 = 0;
let mut bundle: Vec<u8> = Vec::new();
write_bundle_streaming(
&mut bundle,
0,
false,
SourceInfo {
project: "acme".to_string(),
site: None,
},
specs.clone(),
|spec| {
let tmp = stage_dir.join(spec.path.rsplit('/').next().unwrap());
std::fs::write(&tmp, vec![0u8; MEMBER]).unwrap();
peak = peak.max(staged_bytes(&stage_dir)); Ok(tmp)
},
)
.unwrap();
assert!(
peak <= largest + 4096,
"streaming staging peak {peak} must stay ≈ one member ({largest}), \
far below the whole bundle ({whole})"
);
assert!(
peak < whole / 2,
"peak {peak} must be far below whole-bundle staging {whole}"
);
for f in std::fs::read_dir(&stage_dir).unwrap() {
let _ = std::fs::remove_file(f.unwrap().path());
}
let mut mutant_peak: u64 = 0;
for i in 0..N {
let tmp = stage_dir.join(format!("all{i}"));
std::fs::write(&tmp, vec![0u8; MEMBER]).unwrap();
mutant_peak = mutant_peak.max(staged_bytes(&stage_dir));
}
assert_eq!(
mutant_peak, whole,
"whole-bundle-staging mutation drives the metric to the full bundle"
);
assert!(
mutant_peak > largest + 4096,
"so the bounded-staging assertion above is genuinely RED under that mutation"
);
}
#[test]
fn read_bundle_streaming_staged_verifies_before_apply() {
use std::sync::atomic::{AtomicUsize, Ordering};
let mut buf: Vec<u8> = Vec::new();
{
let mut b = tar::Builder::new(&mut buf);
let manifest = StreamManifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION_V2,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "x".to_string(),
site: None,
},
members: vec![MemberSpec {
path: "sql/app.pgc".to_string(),
kind: MemberKind::PgDump,
framing: MemberFraming::Sidecar,
pg: None,
}],
};
append_bytes(
&mut b,
MANIFEST_PATH,
0,
&serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let meta = MemberMeta {
size: 3,
sha256: "00".repeat(32), };
append_bytes(
&mut b,
"sql/app.pgc.brmeta",
0,
&serde_json::to_vec(&meta).unwrap(),
)
.unwrap();
append_bytes(&mut b, "sql/app.pgc", 0, b"abc").unwrap();
b.finish().unwrap();
}
let td = tempfile::tempdir().unwrap();
let applied = AtomicUsize::new(0);
let r = read_bundle_streaming_staged(
buf.as_slice(),
td.path(),
|_d, _s| {},
|_spec, _temp| {
applied.fetch_add(1, Ordering::SeqCst);
Ok(())
},
);
assert!(
matches!(r, Err(BundleError::ChecksumMismatch { .. })),
"got {r:?}"
);
assert_eq!(
applied.load(Ordering::SeqCst),
0,
"restore (apply) must NOT run for a member that fails verification"
);
let stage = td.path().join("gen");
std::fs::create_dir_all(&stage).unwrap();
let mut good: Vec<u8> = Vec::new();
write_bundle_streaming(
&mut good,
0,
true,
SourceInfo {
project: "x".to_string(),
site: None,
},
vec![MemberSpec {
path: KV_MEMBER_PATH.to_string(),
kind: MemberKind::Kv,
framing: MemberFraming::Sidecar,
pg: None,
}],
|_s| {
let p = stage.join("m");
std::fs::write(&p, b"BRKVDUMP-OK").unwrap();
Ok(p)
},
)
.unwrap();
let seen = std::cell::RefCell::new(Vec::new());
read_bundle_streaming_staged(
good.as_slice(),
td.path(),
|_d, _s| {},
|spec, temp| {
seen.borrow_mut()
.push((spec.path.clone(), std::fs::read(temp).unwrap()));
Ok(())
},
)
.unwrap();
assert_eq!(
seen.into_inner(),
vec![("kv.brkvdump".to_string(), b"BRKVDUMP-OK".to_vec())],
"a verified member is applied from its staged temp with the exact bytes"
);
}
#[derive(Default)]
struct RecordingRestorer {
sidecars: Vec<(MemberSpec, Vec<u8>)>,
begins: Vec<String>,
finishes: Vec<(String, bool)>,
cur: Option<Vec<u8>>,
committed_chunked: Vec<(MemberSpec, Vec<u8>)>,
max_write_buf: usize,
}
impl BundleRestorer for RecordingRestorer {
fn restore_sidecar(&mut self, spec: &MemberSpec, temp: &Path) -> Result<(), BundleError> {
self.sidecars.push((spec.clone(), std::fs::read(temp)?));
Ok(())
}
fn begin_chunked(&mut self, spec: &MemberSpec) -> Result<(), BundleError> {
self.begins.push(spec.path.clone());
self.cur = Some(Vec::new());
Ok(())
}
fn write_chunk(&mut self, _spec: &MemberSpec, buf: &[u8]) -> Result<(), BundleError> {
self.max_write_buf = self.max_write_buf.max(buf.len());
self.cur
.as_mut()
.expect("write_chunk between begin and finish")
.extend_from_slice(buf);
Ok(())
}
fn finish_chunked(&mut self, spec: &MemberSpec, verified: bool) -> Result<(), BundleError> {
self.finishes.push((spec.path.clone(), verified));
let bytes = self.cur.take().unwrap_or_default();
if verified {
self.committed_chunked.push((spec.clone(), bytes));
}
Ok(())
}
}
fn count_parts(bundle: &[u8], base: &str) -> (usize, u64) {
let mut ar = tar::Archive::new(bundle);
let mut n = 0usize;
let mut max = 0u64;
for e in ar.entries().unwrap() {
let e = e.unwrap();
let p = e.path().unwrap().to_string_lossy().into_owned();
if p.starts_with(&format!("{base}{PART_INFIX}")) {
n += 1;
max = max.max(e.header().size().unwrap());
}
}
(n, max)
}
#[test]
fn v3_chunked_round_trips_with_bounded_part_staging() {
const CHUNK: usize = 4096;
let body: Vec<u8> = (0..CHUNK * 5).map(|i| (i % 251) as u8).collect();
let specs = vec![
MemberSpec::sidecar(KV_MEMBER_PATH.to_string(), MemberKind::Kv, None),
MemberSpec::chunked(
"libsql/acme/blog/default.sqlite".to_string(),
MemberKind::Libsql,
None,
),
];
let td = tempfile::tempdir().unwrap();
let kv_tmp = td.path().join("kv");
std::fs::write(&kv_tmp, b"BRKVDUMP-KV").unwrap();
let body_for_open = body.clone();
let mut bundle: Vec<u8> = Vec::new();
write_bundle_streaming_v3(
&mut bundle,
1_700_000_000,
false,
SourceInfo {
project: "acme".to_string(),
site: None,
},
specs.clone(),
CHUNK,
|_spec| Ok(kv_tmp.clone()),
|_spec| Ok(Box::new(io::Cursor::new(body_for_open.clone())) as Box<dyn Read>),
)
.unwrap();
assert_eq!(read_manifest_version(&bundle), BUNDLE_FORMAT_VERSION_V3);
let (parts, max_part) = count_parts(&bundle, "libsql/acme/blog/default.sqlite");
assert_eq!(parts, 5, "a 5×chunk body is written as 5 `.part` entries");
assert!(
max_part <= CHUNK as u64,
"no part exceeds the chunk size (never a whole-member entry): {max_part}"
);
let stage = td.path().join("stage");
std::fs::create_dir_all(&stage).unwrap();
let mut r = RecordingRestorer::default();
let mut data_only = true;
let restored =
read_bundle_streaming_atomic(bundle.as_slice(), &stage, |d, _s| data_only = d, &mut r)
.unwrap();
assert!(!data_only);
assert_eq!(restored, specs, "both members restored in declared order");
assert_eq!(r.sidecars.len(), 1);
assert_eq!(r.sidecars[0].1, b"BRKVDUMP-KV");
assert_eq!(
r.begins,
vec!["libsql/acme/blog/default.sqlite".to_string()]
);
assert_eq!(
r.finishes,
vec![("libsql/acme/blog/default.sqlite".to_string(), true)],
"finish_chunked fired ONCE with verified=true (commit)"
);
assert_eq!(r.committed_chunked.len(), 1);
assert_eq!(
r.committed_chunked[0].1, body,
"the chunked member round-trips byte-for-byte"
);
assert!(
r.max_write_buf <= 64 * 1024,
"reader streams in ≤64 KiB reads, never the whole member: {}",
r.max_write_buf
);
}
#[test]
fn v3_chunked_trailer_mismatch_aborts_and_never_commits() {
let base = "libsql/acme/blog/default.sqlite";
let mut buf: Vec<u8> = Vec::new();
{
let mut b = tar::Builder::new(&mut buf);
let manifest = StreamManifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION_V3,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "acme".to_string(),
site: None,
},
members: vec![MemberSpec::chunked(
base.to_string(),
MemberKind::Libsql,
None,
)],
};
append_bytes(
&mut b,
MANIFEST_PATH,
0,
&serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
append_bytes(
&mut b,
&chunk_part_path(base, 0),
0,
b"the real member bytes",
)
.unwrap();
let trailer = MemberTrailer {
size: 21,
sha256: "00".repeat(32), };
append_bytes(
&mut b,
&chunk_trailer_path(base),
0,
&serde_json::to_vec(&trailer).unwrap(),
)
.unwrap();
b.finish().unwrap();
}
let td = tempfile::tempdir().unwrap();
let mut r = RecordingRestorer::default();
let res = read_bundle_streaming_atomic(buf.as_slice(), td.path(), |_d, _s| {}, &mut r);
assert!(
matches!(res, Err(BundleError::ChecksumMismatch { .. })),
"a lying trailer is a loud reject: {res:?}"
);
assert_eq!(
r.finishes,
vec![(base.to_string(), false)],
"finish_chunked fired with verified=false (DROP the shadow); commit NEVER fired"
);
assert!(
r.committed_chunked.is_empty(),
"no chunked member is committed when the trailer mismatches"
);
}
#[test]
fn v3_chunked_truncation_before_trailer_aborts() {
let base = "libsql/acme/blog/default.sqlite";
let mut buf: Vec<u8> = Vec::new();
{
let mut b = tar::Builder::new(&mut buf);
let manifest = StreamManifest {
magic: BUNDLE_MAGIC.to_string(),
format_version: BUNDLE_FORMAT_VERSION_V3,
created_at: 0,
data_only: true,
source: SourceInfo {
project: "acme".to_string(),
site: None,
},
members: vec![MemberSpec::chunked(
base.to_string(),
MemberKind::Libsql,
None,
)],
};
append_bytes(
&mut b,
MANIFEST_PATH,
0,
&serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
append_bytes(
&mut b,
&chunk_part_path(base, 0),
0,
b"a chunk but no trailer",
)
.unwrap();
b.finish().unwrap(); }
let td = tempfile::tempdir().unwrap();
let mut r = RecordingRestorer::default();
let res = read_bundle_streaming_atomic(buf.as_slice(), td.path(), |_d, _s| {}, &mut r);
assert!(
matches!(res, Err(BundleError::MalformedStream(_))),
"a truncated chunked member (no trailer) is rejected: {res:?}"
);
assert_eq!(
r.finishes,
vec![(base.to_string(), false)],
"sink DROPPED on truncation"
);
assert!(r.committed_chunked.is_empty());
}
#[test]
fn atomic_reader_reads_v1_and_v2_but_an_inc1_node_loud_rejects_v3() {
let td = tempfile::tempdir().unwrap();
let stage = td.path().join("stage");
std::fs::create_dir_all(&stage).unwrap();
let kv = write_staging(&stage, "kv.bin", b"V1-KV");
let v1 = td.path().join("v1.brproj");
write_bundle(
&v1,
0,
true,
SourceInfo {
project: "acme".to_string(),
site: None,
},
&[StagedMember {
path: KV_MEMBER_PATH.to_string(),
kind: MemberKind::Kv,
source: kv,
pg: None,
}],
)
.unwrap();
let mut r1 = RecordingRestorer::default();
read_bundle_streaming_atomic(
io::BufReader::new(std::fs::File::open(&v1).unwrap()),
&stage,
|_d, _s| {},
&mut r1,
)
.unwrap();
assert_eq!(r1.sidecars.len(), 1);
assert_eq!(r1.sidecars[0].1, b"V1-KV");
let gendir = td.path().join("gen");
std::fs::create_dir_all(&gendir).unwrap();
let mut v2: Vec<u8> = Vec::new();
write_bundle_streaming(
&mut v2,
0,
true,
SourceInfo {
project: "acme".to_string(),
site: None,
},
vec![MemberSpec::sidecar(
KV_MEMBER_PATH.to_string(),
MemberKind::Kv,
None,
)],
|_s| {
let p = gendir.join("m");
std::fs::write(&p, b"V2-KV").unwrap();
Ok(p)
},
)
.unwrap();
assert_eq!(read_manifest_version(&v2), BUNDLE_FORMAT_VERSION_V2);
let mut r2 = RecordingRestorer::default();
read_bundle_streaming_atomic(v2.as_slice(), &stage, |_d, _s| {}, &mut r2).unwrap();
assert_eq!(r2.sidecars[0].1, b"V2-KV");
let mut v3: Vec<u8> = Vec::new();
write_bundle_streaming_v3(
&mut v3,
0,
true,
SourceInfo {
project: "acme".to_string(),
site: None,
},
vec![MemberSpec::chunked(
"libsql/acme/blog/default.sqlite".to_string(),
MemberKind::Libsql,
None,
)],
16,
|_s| unreachable!("no sidecar member"),
|_s| Ok(Box::new(io::Cursor::new(b"hello v3 chunked".to_vec())) as Box<dyn Read>),
)
.unwrap();
assert_eq!(read_manifest_version(&v3), BUNDLE_FORMAT_VERSION_V3);
let legacy = read_bundle_streaming(v3.as_slice(), |_s, _sz, _b| Ok(()));
assert!(
matches!(legacy, Err(BundleError::BadVersion { found: 3, .. })),
"the legacy v1/v2 reader loud-rejects a v3 bundle: {legacy:?}"
);
}
fn read_manifest_version(bundle: &[u8]) -> u32 {
let mut ar = tar::Archive::new(bundle);
let mut first = ar.entries().unwrap().next().unwrap().unwrap();
let mut s = String::new();
first.read_to_string(&mut s).unwrap();
peek_format_version(&s).unwrap()
}
}