/// Manifest construction, canonical encoding and lookup helpers.
pub fn manifest_from_chunks(name : String, chunks : Array[Chunk]) -> Manifest {
let refs = []
for chunk in chunks {
refs.push(ChunkRef::new(chunk.index(), chunk.id(), chunk.size()))
}
Manifest::new(name, refs)
}
pub fn manifest_from_digests(name : String, digests : Array[String], sizes : Array[Int]) -> Result[Manifest, String] {
if digests.length() != sizes.length() {
return Err("digest and size arrays must have the same length")
}
let refs = []
for i in 0.. ChunkRef? {
for reference in manifest.references() {
if digest_equal(reference.id(), digest) { return Some(reference) }
}
None
}
pub fn manifest_contains(manifest : Manifest, digest : String) -> Bool {
manifest_lookup(manifest, digest) is Some(_)
}
pub fn manifest_position(manifest : Manifest, digest : String) -> Int? {
match manifest_lookup(manifest, digest) {
Some(reference) => Some(reference.at())
None => None
}
}
pub fn manifest_chunk_sizes(manifest : Manifest) -> Array[Int] {
manifest.references().map(reference => reference.length())
}
pub fn manifest_digests(manifest : Manifest) -> Array[String] {
manifest.references().map(reference => reference.id())
}
fn clean_name(name : String) -> String {
name.replace(old="\n", new="_").replace(old="|", new="_")
}
/// Canonical line-oriented encoding. It is intentionally simple to make the
/// format inspectable in a hex editor and stable across MoonBit backends.
pub fn encode_manifest(manifest : Manifest) -> String {
let out = StringBuilder::new()
out.write_string("MOONCHUNK-MANIFEST|1\n")
out.write_string("name|")
out.write_string(clean_name(manifest.filename()))
out.write_string("\nsize|")
out.write_string(manifest.size().to_string())
out.write_string("\nroot|")
out.write_string(manifest.root_digest())
out.write_string("\nchunks|")
out.write_string(manifest.count().to_string())
out.write_string("\n")
for reference in manifest.references() {
out.write_string("chunk|")
out.write_string(reference.at().to_string())
out.write_string("|")
out.write_string(reference.id())
out.write_string("|")
out.write_string(reference.length().to_string())
out.write_string("\n")
}
out.to_string()
}
fn parse_int(value : StringView) -> Result[Int, String] {
try {
Ok(@strconv.parse_int(value))
} catch {
_ => Err("invalid integer in manifest")
}
}
/// Parse the canonical format and validate the recomputed Merkle root.
pub fn decode_manifest(encoded : String) -> Result[Manifest, String] {
let lines = encoded.split("\n").collect()
if lines.length() < 5 || lines[0] != "MOONCHUNK-MANIFEST|1" {
return Err("unsupported manifest header")
}
let name_parts = lines[1].split("|").collect()
let size_parts = lines[2].split("|").collect()
let root_parts = lines[3].split("|").collect()
let count_parts = lines[4].split("|").collect()
if name_parts.length() != 2 || size_parts.length() != 2 || root_parts.length() != 2 || count_parts.length() != 2 {
return Err("malformed manifest metadata")
}
let total = match parse_int(size_parts[1]) {
Ok(x) => x
Err(e) => return Err(e)
}
let count = match parse_int(count_parts[1]) {
Ok(x) => x
Err(e) => return Err(e)
}
if count < 0 || lines.length() < count + 5 { return Err("malformed manifest chunk count") }
let refs = []
for i in 0.. x
Err(e) => return Err(e)
}
let size = match parse_int(parts[3]) {
Ok(x) => x
Err(e) => return Err(e)
}
refs.push(ChunkRef::new(position, parts[2].to_string(), size))
}
let manifest = Manifest::new(name_parts[1].to_string(), refs)
if manifest.size() != total { return Err("manifest total size mismatch") }
if !digest_equal(manifest.root_digest(), root_parts[1].to_string()) { return Err("manifest Merkle root mismatch") }
Ok(manifest)
}
pub fn manifest_summary(manifest : Manifest) -> String {
"{manifest.filename()} {manifest.size()} bytes, {manifest.count()} chunks, root={manifest.root_digest()}"
}
pub fn manifest_same_content(left : Manifest, right : Manifest) -> Bool {
digest_equal(left.root_digest(), right.root_digest()) && left.size() == right.size()
}
pub fn manifest_validate_order(manifest : Manifest) -> Bool {
let mut expected = 0
for reference in manifest.references() {
if reference.at() != expected || reference.length() < 0 { return false }
expected = expected + 1
}
true
}
pub fn manifest_reorder(manifest : Manifest, order : Array[Int]) -> Result[Manifest, String] {
if order.length() != manifest.count() { return Err("order length mismatch") }
let references = manifest.references()
let output = []
for i in 0..= references.length() { return Err("order index out of range") }
let reference = references[source]
output.push(ChunkRef::new(i, reference.id(), reference.length()))
}
Ok(Manifest::new(manifest.filename(), output))
}
pub fn manifest_prefix(manifest : Manifest, count : Int) -> Manifest {
let limit = if count < 0 { 0 } else if count > manifest.count() { manifest.count() } else { count }
let refs = []
for i in 0.. Manifest {
let first = if start < 0 { 0 } else if start > manifest.count() { manifest.count() } else { start }
let refs = []
let all = manifest.references()
for i in first..