///|
pub const NEWC_HEADER_SIZE : Int32 = 110
///|
/// `newc` archive variants.
pub(all) enum NewcFormat {
Newc
NewcCrc
} derive(Eq, Debug)
///|
/// Errors reported while parsing a `newc` cpio archive.
///
/// Numeric payloads are byte offsets into the original input.
pub(all) enum CpioError {
TruncatedHeader(Int32)
UnsupportedMagic(Int32)
InvalidHex(Int32)
InvalidOctal(Int32)
InvalidNameSize(Int32)
TruncatedName(Int32)
MissingNameTerminator(Int32)
EmbeddedNul(Int32)
InvalidUtf8Name(Int32)
TruncatedData(Int32)
ChecksumMismatch(Int32, UInt64, UInt64)
MixedFormats(Int32)
InvalidTrailer(Int32)
} derive(Eq, Debug)
///|
/// Parsed `newc` header fields.
pub(all) struct Header {
format : NewcFormat
ino : UInt64
mode : UInt64
uid : UInt64
gid : UInt64
nlink : UInt64
mtime : UInt64
file_size : UInt64
dev_major : UInt64
dev_minor : UInt64
rdev_major : UInt64
rdev_minor : UInt64
name_size : UInt64
check : UInt64
} derive(Eq, Debug)
///|
/// One archive entry. `name_bytes` preserves the exact bytes from the archive,
/// while `name` is the validated UTF-8 representation.
pub(all) struct Entry {
name : String
name_bytes : Bytes
header : Header
data : Bytes
} derive(Eq, Debug)
///|
/// A successfully parsed archive.
pub(all) struct Archive {
format : NewcFormat
entries : Array[Entry]
} derive(Eq, Debug)
///|
fn hex_value(byte : Byte) -> UInt64? {
if byte >= b'0' && byte <= b'9' {
Some(byte.to_uint64() - b'0'.to_uint64())
} else if byte >= b'a' && byte <= b'f' {
Some(byte.to_uint64() - b'a'.to_uint64() + 10)
} else if byte >= b'A' && byte <= b'F' {
Some(byte.to_uint64() - b'A'.to_uint64() + 10)
} else {
None
}
}
///|
fn parse_hex_field(
header : BytesView,
field_index : Int32,
field_offset : Int32,
) -> Result[UInt64, CpioError] {
let mut value = 0UL
for i = 0; i < 8; i = i + 1 {
let byte_offset = 6 + field_index * 8 + i
let digit = match hex_value(header[byte_offset]) {
Some(value) => value
None => return Err(InvalidHex(field_offset + byte_offset))
}
value = (value << 4) | digit
}
Ok(value)
}
///|
fn parse_header(
header : BytesView,
archive_offset : Int32,
) -> Result[Header, CpioError] {
let format = if header[0:6] == b"070701" {
NewcFormat::Newc
} else if header[0:6] == b"070702" {
NewcFormat::NewcCrc
} else {
return Err(UnsupportedMagic(archive_offset))
}
let fields : Array[UInt64] = []
for index = 0; index < 13; index = index + 1 {
let value = match parse_hex_field(header, index, archive_offset) {
Ok(value) => value
Err(error) => return Err(error)
}
fields.push(value)
}
Ok({
format,
ino: fields[0],
mode: fields[1],
uid: fields[2],
gid: fields[3],
nlink: fields[4],
mtime: fields[5],
file_size: fields[6],
dev_major: fields[7],
dev_minor: fields[8],
rdev_major: fields[9],
rdev_minor: fields[10],
name_size: fields[11],
check: fields[12],
})
}
///|
fn add_checksum_byte(sum : UInt64, byte : Byte) -> UInt64 {
(sum + byte.to_uint64()) & 0xFFFFFFFFUL
}
///|
/// Computes the byte-sum value stored in a `070702` newc `check` field.
///
/// Despite the format's "CRC" name, the field is not a cyclic redundancy
/// check. It is the low 32 bits of the unsigned sum of all data bytes.
pub fn newc_checksum(data : BytesView) -> UInt64 {
let mut sum = 0UL
for byte in data {
sum = add_checksum_byte(sum, byte)
}
sum
}
///|
fn padding_for(length : Int32) -> Int32 {
(4 - length % 4) % 4
}
///|
fn newc_name_padding(name_size : Int32) -> Int32 {
padding_for(NEWC_HEADER_SIZE + name_size)
}
///|
/// Parses a SVR4 `newc` cpio archive (`070701`).
///
/// The parser is read-only and does not extract to the filesystem. Every
/// length and field is checked before conversion to an index, so malformed
/// archives return `Err` instead of panicking.
pub fn parse_newc(input : Bytes) -> Result[Archive, CpioError] {
let entries : Array[Entry] = []
let mut format = NewcFormat::Newc
let mut has_format = false
let mut offset = 0
while true {
let header_end = offset + NEWC_HEADER_SIZE
if header_end > input.length() {
return Err(TruncatedHeader(offset))
}
let header = match parse_header(input[offset:header_end], offset) {
Ok(header) => header
Err(error) => return Err(error)
}
if has_format {
if header.format != format {
return Err(MixedFormats(offset))
}
} else {
format = header.format
has_format = true
}
let name_start = header_end
let remaining = (input.length() - name_start).to_uint64()
if header.name_size == 0UL || header.name_size > remaining {
return Err(InvalidNameSize(offset))
}
let name_size = header.name_size.to_int()
let name_end = name_start + name_size
if input[name_end - 1] != b'\x00' {
return Err(MissingNameTerminator(name_start))
}
let name_view = input[name_start:name_end - 1]
for i = 0; i < name_view.length(); i = i + 1 {
if name_view[i] == b'\x00' {
return Err(EmbeddedNul(name_start + i))
}
}
let name_bytes = name_view.to_owned()
let name = @utf8.decode(name_view) catch {
_ => return Err(InvalidUtf8Name(name_start))
}
let data_start = name_end + newc_name_padding(name_size)
if data_start > input.length() {
return Err(TruncatedName(name_end))
}
if name == "TRAILER!!!" {
if header.file_size != 0UL {
return Err(InvalidTrailer(offset))
}
if header.format == NewcCrc && header.check != 0UL {
return Err(ChecksumMismatch(offset, header.check, 0UL))
}
return Ok({ format, entries, })
}
let data_remaining = (input.length() - data_start).to_uint64()
if header.file_size > data_remaining {
return Err(TruncatedData(data_start))
}
let file_size = header.file_size.to_int()
let data_end = data_start + file_size
let next_offset = data_end + padding_for(file_size)
if next_offset > input.length() {
return Err(TruncatedData(data_end))
}
let data = input[data_start:data_end].to_owned()
if header.format == NewcCrc {
let actual = newc_checksum(data[:])
if header.check != actual {
return Err(ChecksumMismatch(data_start, header.check, actual))
}
}
entries.push({ name, name_bytes, header, data, })
offset = next_offset
} nobreak {
abort("unreachable: parse_newc always returns")
}
}