///|
struct RecoverySet {
width : Int32
members : Array[Member]
parity : Map[Int32, Array[Int32]]
}
///|
pub fn RecoverySet::names(self : RecoverySet) -> Array[String] {
self.members.map(fn(m) { m.name })
}
///|
fn record(map : Map[Bytes, Bytes], body : Bytes) -> Unit raise {
if body.length() < 16 {
fail("short member packet")
}
let id = body[:16].to_owned()
if map.get(id) is Some(old) && old != body {
fail("conflicting member packets")
}
map[id] = body
}
///|
pub fn read_set(volumes : Array[Bytes]) -> RecoverySet raise {
let mut total = 0
let mut main : Bytes? = None
let mut id : Bytes? = None
let descriptors : Map[Bytes, Bytes] = Map([])
let sums : Map[Bytes, Bytes] = Map([])
let raw_parity : Map[Int32, Bytes] = Map([])
for volume in volumes {
if volume.length() > MaxVolumeBytes - total {
fail("recovery volumes exceed 16 MiB")
}
total = total + volume.length()
for packet in read_packets(volume) {
if id is Some(old) && old != packet.id {
fail("different recovery sets")
}
id = Some(packet.id)
let b = packet.body
if packet.tag == main_tag {
if main is Some(old) && old != b {
fail("conflicting main packets")
}
main = Some(b)
} else if packet.tag == desc_tag {
record(descriptors, b)
} else if packet.tag == sums_tag {
record(sums, b)
} else if packet.tag == parity_tag {
if b.length() < 8 {
fail("short recovery packet")
}
let exp = le32(b, 0)
if exp > 65535U {
fail("invalid exponent")
}
let key = exp.reinterpret_as_int()
if raw_parity.get(key) is Some(old) && old != b {
fail("conflicting recovery slices")
}
raw_parity[key] = b
if raw_parity.length() > MaxParity {
fail("too many recovery slices")
}
}
}
}
guard main is Some(header) else { fail("missing main packet") }
if header.length() < 12 || (header.length() - 12) % 16 != 0 {
fail("invalid main packet")
}
let width = le32(header, 0).reinterpret_as_int()
let count = le32(header, 8).reinterpret_as_int()
if le32(header, 4) != 0U ||
width <= 0 ||
width > MaxWidth ||
width % 4 != 0 ||
count < 1 ||
count > MaxFiles ||
count > (header.length() - 12) / 16 {
fail("unsupported set dimensions")
}
let members = []
let names : Map[String, Bool] = Map([])
let mut source_total = 0
let mut slices = 0
for i = 0; i < count; i = i + 1 {
let fid = header[12 + i * 16:28 + i * 16].to_owned()
guard descriptors.get(fid) is Some(desc) else {
fail("missing file description")
}
guard sums.get(fid) is Some(checks) else {
fail("missing input slice checksums")
}
if desc.length() <= 56 {
fail("invalid description")
}
let length = le32(desc, 48).reinterpret_as_int()
if length <= 0 ||
le32(desc, 52) != 0U ||
length > MaxSourceBytes - source_total {
fail("unsupported file length")
}
source_total = source_total + length
let blocks = (length + width - 1) / width
slices = slices + blocks
if slices > MaxSlices || checks.length() != 16 + 20 * blocks {
fail("invalid slice count")
}
let mut end = desc.length()
while end > 56 && desc[end - 1] == b'\x00' {
end = end - 1
}
if desc.length() - end > 3 || md5(desc[32:end].to_owned()) != fid {
fail("invalid file identifier")
}
let name = @utf8.decode(desc[56:end])
if !safe_name(name) || names.contains(name.to_lower()) {
fail("unsupported or duplicate filename")
}
names[name.to_lower()] = true
members.push({
id: fid,
name,
length,
hash: desc[16:32].to_owned(),
desc,
sums: checks,
})
}
let parity : Map[Int32, Array[Int32]] = Map([])
for exp, body in raw_parity {
if body.length() != 4 + width {
fail("recovery slice size mismatch")
}
parity[exp] = as_words(body[4:].to_owned())
}
{ width, members, parity, }
}
///|
test "read repeated metadata and reject unrelated sets" {
let a = create([("a", b"aaa")], width=4)
let b = create([("b", b"bbb")], width=4)
assert_eq(read_set([a, a]).names(), ["a"])
assert_true(
try {
ignore(read_set([a, b]))
false
} catch {
_ => true
},
)
}