///|
/// An immutable comparison input with loaded member bytes. The original
/// container bytes and physical record positions remain the source identity.
/// A snapshot does not access external files after construction.
struct ArchiveSnapshot {
source : Bytes
archive : Archive
} derive(Debug)
///|
pub extend ArchiveSnapshot with Debug::{to_repr}
///|
/// Ordinary archives already contain every member and need no loader.
pub fn Archive::snapshot(self : Archive) -> ArchiveSnapshot {
{ source: self.data, archive: self, }
}
///|
// Check every declaration and path before requesting any external contents.
fn thin_load_plan(
archive : ThinArchive,
max_bytes : Int32,
allow_parent_paths : Bool,
) -> (Array[(Int32, Bytes)], Int32) raise InvalidArchive {
if max_bytes < 0 {
reject("AR008", 0, "limits must be nonnegative")
}
let paths : Array[(Int32, Bytes)] = []
let mut total = 0
for index, entry in archive.archive.records {
if entry.kind != Regular {
continue
}
let path = match archive.member_path(index, allow_parent_paths~) {
Ok(path) => path
Err(error) => raise InvalidArchive(error)
}
if entry.size > max_bytes - total {
reject(
"AR008",
entry.header_offset,
"total external member byte limit exceeded",
)
}
total += entry.size
paths.push((index, path))
}
(paths, total)
}
///|
fn thin_load_bytes(
entry : Member,
path : Bytes,
loader : (Bytes, Int32) -> Result[Bytes, String],
max_bytes~ : Int32 = 2147483647,
) -> Bytes raise InvalidArchive {
let bytes = match loader(path, entry.size) {
Ok(bytes) => bytes
Err(message) => reject("AR013", entry.header_offset, message)
}
if bytes.length() > max_bytes {
reject("AR008", entry.header_offset, "external member byte limit exceeded")
}
if bytes.length() != entry.size {
reject(
"AR014",
entry.header_offset,
"external member size differs from its archive header",
)
}
bytes
}
///|
/// Validate every external member without retaining its contents. Returns the
/// total loaded byte count. Repeated references each count toward max_bytes.
/// Paths and cumulative declared sizes are checked before the first loader call.
pub fn ThinArchive::validate_members(
self : ThinArchive,
loader : (Bytes, Int32) -> Result[Bytes, String],
max_bytes~ : Int32 = 67108864,
allow_parent_paths~ : Bool = false,
nested_limits~ : NestedLimits = NestedLimits::new(),
) -> Result[Int32, ParseError] {
try {
let (paths, total) = thin_load_plan(self, max_bytes, allow_parent_paths)
let context = nested_context(
loader, allow_parent_paths, nested_limits, max_bytes,
)
for (index, path) in paths {
let entry = self.archive.records[index]
ignore(
context.resolve(
entry,
self.references.get(index),
path,
entry.header_offset,
),
)
}
Ok(total)
} catch {
InvalidArchive(error) => Err(error)
}
}
///|
/// Load an immutable snapshot for logical comparison with another thin snapshot
/// or an ordinary Archive.snapshot(). Index mappings are retained as parsed;
/// this operation neither discovers symbols nor rewrites the input archive.
/// max_bytes limits cumulative external payloads, separately from container size.
pub fn ThinArchive::snapshot(
self : ThinArchive,
loader : (Bytes, Int32) -> Result[Bytes, String],
max_bytes~ : Int32 = 67108864,
allow_parent_paths~ : Bool = false,
nested_limits~ : NestedLimits = NestedLimits::new(),
) -> Result[ArchiveSnapshot, ParseError] {
try {
let (paths, total) = thin_load_plan(self, max_bytes, allow_parent_paths)
if total > 2147483647 - self.archive.data.length() {
reject("AR008", 0, "snapshot byte length exceeds the supported range")
}
let context = nested_context(
loader, allow_parent_paths, nested_limits, max_bytes,
)
let data = @buffer.Buffer(size_hint=self.archive.data.length() + total)
data.write_bytes(self.archive.data[:])
let records = self.archive.records.copy()
for (index, path) in paths {
let entry = records[index]
let resolved = context.resolve(
entry,
self.references.get(index),
path,
entry.header_offset,
)
records[index] = {
..entry,
name: resolved.name,
data_offset: data.length(),
}
data.write_bytes(resolved.data[:])
}
// This private view is used only by the comparison engine. It is never
// exposed as an Archive or serialized as a physical ordinary archive.
Ok({
source: self.archive.data,
archive: {
data: data.to_bytes(),
records,
indexes: self.archive.indexes,
},
})
} catch {
InvalidArchive(error) => Err(error)
}
}
///|
/// Compare loaded contents, metadata, order and original symbol mappings.
/// bytes_equal compares the original container bytes. For thin snapshots it can
/// be true while content_equal is false because external contents have changed.
/// Change and ambiguity indices refer to the original physical input records.
pub fn ArchiveSnapshot::compare_logical(
self : ArchiveSnapshot,
other : ArchiveSnapshot,
) -> LogicalComparison {
let result = compare_logical(self.archive, other.archive)
{ ..result, bytes_equal: self.source == other.source, }
}