///|
/// Archive layout and symbol-index width used when creating a regular archive.
pub(all) enum WriteFormat {
Gnu
Gnu64
Bsd
Bsd64
Darwin
Darwin64
Coff
} derive(Eq, Debug)
///|
/// Deterministic writes use time/uid/gid zero and mode 0644. Darwin assigns
/// increasing timestamps to repeated names, starting at zero for each name.
/// Preserve writes the supplied metadata, including blank fields represented by None.
pub(all) enum WriteMetadata {
Deterministic
Preserve
} derive(Eq, Debug)
///|
/// An ordinary entry to write. Names and payloads retain their exact bytes.
pub(all) struct WriteMember {
name : Bytes
data : Bytes
metadata : Metadata
} derive(Eq, Debug)
///|
pub fn WriteMember::new(
name : Bytes,
data : Bytes,
metadata~ : Metadata = {
timestamp: Some(0L),
uid: Some(0L),
gid: Some(0L),
mode: Some(420L),
},
) -> WriteMember {
{ name, data, metadata, }
}
///|
/// A symbol target is a zero-based ordinary-entry index in the final builder.
/// Supply mappings after all edits; mappings are never carried across edits.
pub(all) struct WriteSymbol {
name : Bytes
member_index : Int32
} derive(Eq, Debug)
///|
/// WR001: invalid entry/name/index; WR002: unrepresentable metadata;
/// WR003: resource limit; WR004: existing index needs an explicit decision;
/// WR005: invalid or unsupported symbol-index request.
pub(all) struct WriteError {
code : String
member_index : Int32?
message : String
} derive(Eq, Debug)
///|
/// An immutable editing plan. Operations return a new plan and preserve their input.
struct ArchiveBuilder {
members : Array[WriteMember]
} derive(Debug)
///|
pub fn ArchiveBuilder::new() -> ArchiveBuilder {
{ members: [], }
}
///|
/// Validate a complete collection and copy its array once.
/// Use this for bulk creation instead of repeatedly appending to an immutable plan.
pub fn ArchiveBuilder::from_members(
entries : Array[WriteMember],
) -> Result[ArchiveBuilder, WriteError] {
try {
for index, entry in entries {
write_member_valid(entry, index)
}
Ok({ members: entries.copy(), })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub fn ArchiveBuilder::member_count(self : ArchiveBuilder) -> Int32 {
self.members.length()
}
///|
/// Return an independent array; names and payloads are immutable byte strings.
pub fn ArchiveBuilder::members(self : ArchiveBuilder) -> Array[WriteMember] {
self.members.copy()
}
///|
priv suberror InvalidWrite {
InvalidWrite(WriteError)
}
///|
fn[T] write_reject(
code : String,
index : Int32?,
message : String,
) -> T raise InvalidWrite {
raise InvalidWrite({ code, member_index: index, message, })
}
///|
fn write_member_valid(
entry : WriteMember,
index : Int32,
) -> Unit raise InvalidWrite {
if entry.name.is_empty() {
write_reject("WR001", Some(index), "empty ordinary-entry name")
}
for byte in entry.name {
if byte == b'\x00' {
write_reject("WR001", Some(index), "NUL inside entry name")
}
}
let fields = [
("timestamp", entry.metadata.timestamp, 999999999999L),
("uid", entry.metadata.uid, 999999L),
("gid", entry.metadata.gid, 999999L),
("mode", entry.metadata.mode, 16777215L),
]
for (name, value, maximum) in fields {
if value is Some(number) && (number < 0L || number > maximum) {
write_reject(
"WR002",
Some(index),
"\{name} does not fit its archive header field",
)
}
}
}
///|
pub fn ArchiveBuilder::append(
self : ArchiveBuilder,
entry : WriteMember,
) -> Result[ArchiveBuilder, WriteError] {
try {
write_member_valid(entry, self.members.length())
let members = self.members.copy()
members.push(entry)
Ok({ members, })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
/// Insert before a zero-based position. The final position appends an entry.
pub fn ArchiveBuilder::insert(
self : ArchiveBuilder,
index : Int32,
entry : WriteMember,
) -> Result[ArchiveBuilder, WriteError] {
try {
if index < 0 || index > self.members.length() {
write_reject(
"WR001",
Some(index),
"insert position is outside the archive",
)
}
write_member_valid(entry, index)
let members = self.members.copy()
members.insert(index, entry)
Ok({ members, })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub fn ArchiveBuilder::replace(
self : ArchiveBuilder,
index : Int32,
entry : WriteMember,
) -> Result[ArchiveBuilder, WriteError] {
try {
if index < 0 || index >= self.members.length() {
write_reject("WR001", Some(index), "entry index is outside the archive")
}
write_member_valid(entry, index)
let members = self.members.copy()
members[index] = entry
Ok({ members, })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub fn ArchiveBuilder::remove(
self : ArchiveBuilder,
index : Int32,
) -> Result[ArchiveBuilder, WriteError] {
if index < 0 || index >= self.members.length() {
return Err({
code: "WR001",
member_index: Some(index),
message: "entry index is outside the archive",
})
}
let members = self.members.copy()
ignore(members.remove(index))
Ok({ members, })
}
///|
/// Move a entry to its final zero-based position, preserving the order of others.
pub fn ArchiveBuilder::move_member(
self : ArchiveBuilder,
from : Int32,
to : Int32,
) -> Result[ArchiveBuilder, WriteError] {
let count = self.members.length()
if from < 0 || from >= count || to < 0 || to >= count {
return Err({
code: "WR001",
member_index: Some(if from < 0 || from >= count { from } else { to }),
message: "move position is outside the archive",
})
}
let members = self.members.copy()
let entry = members.remove(from)
members.insert(to, entry)
Ok({ members, })
}
///|
/// Import ordinary members for editing. An existing symbol index must be
/// explicitly discarded; rebuilding uses fresh mappings passed to write().
/// Ordinary-entry positions do not include name or symbol table records.
pub fn Archive::edit(
self : Archive,
drop_symbol_indexes~ : Bool = false,
) -> Result[ArchiveBuilder, WriteError] {
if !self.indexes.is_empty() && !drop_symbol_indexes {
return Err({
code: "WR004",
member_index: None,
message: "choose drop_symbol_indexes=true and optionally supply fresh symbols when writing",
})
}
try {
let members : Array[WriteMember] = []
for record in self.records {
if record.kind == Regular {
let entry = WriteMember::new(
record.name,
self.data[record.data_offset:record.data_offset + record.size].to_owned(),
metadata=record.metadata,
)
write_member_valid(entry, members.length())
members.push(entry)
}
}
Ok({ members, })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
fn write_add(
total : Int32,
amount : Int32,
maximum : Int32,
index : Int32?,
message : String,
) -> Int32 raise InvalidWrite {
if amount > maximum - total {
write_reject("WR003", index, message)
}
total + amount
}
///|
fn write_record_end(
offset : Int32,
size : Int32,
maximum : Int32,
index : Int32?,
) -> Int32 raise InvalidWrite {
let offset = write_add(
offset, 60, maximum, index, "archive byte limit exceeded",
)
let offset = write_add(
offset, size, maximum, index, "archive byte limit exceeded",
)
write_add(offset, size % 2, maximum, index, "archive byte limit exceeded")
}
///|
fn write_gnu_short_name(name : Bytes) -> Bool {
// "#1/" is the BSD embedded-name marker, even inside a GNU archive.
if name.length() > 15 || name == b"#1" || member_kind(name) != Regular {
return false
}
for byte in name {
if byte == b'/' || byte <= b' ' {
return false
}
}
true
}
///|
fn write_field(output : @buffer.Buffer, text : Bytes, width : Int32) -> Unit {
output.write_bytes(text[:])
for i = text.length(); i < width; i = i + 1 {
output.write_byte(b' ')
}
}
///|
fn write_number_field(
output : @buffer.Buffer,
value : Int64?,
width : Int32,
radix : Int32,
) -> Unit {
let text = match value {
None => b""
Some(value) => @utf8.encode(value.to_string(radix~))
}
write_field(output, text, width)
}
///|
fn write_header(
output : @buffer.Buffer,
name : Bytes,
metadata : Metadata,
size : Int32,
) -> Unit {
write_field(output, name, 16)
write_number_field(output, metadata.timestamp, 12, 10)
write_number_field(output, metadata.uid, 6, 10)
write_number_field(output, metadata.gid, 6, 10)
write_number_field(output, metadata.mode, 8, 8)
write_number_field(output, Some(size.to_int64()), 10, 10)
output.write_bytes(b"`\n"[:])
}
///|
fn write_padding(output : @buffer.Buffer, size : Int32) -> Unit {
if size % 2 == 1 {
output.write_byte(b'\n')
}
}
///|
fn write_bsd_format(format : WriteFormat) -> Bool {
match format {
Gnu | Gnu64 | Coff => false
Bsd | Bsd64 | Darwin | Darwin64 => true
}
}
///|
fn write_darwin_format(format : WriteFormat) -> Bool {
format == Darwin || format == Darwin64
}
///|
fn write_index_width(format : WriteFormat) -> Int32 {
match format {
Gnu | Bsd | Darwin | Coff => 4
Gnu64 | Bsd64 | Darwin64 => 8
}
}
///|
fn write_index_number(
output : @buffer.Buffer,
value : Int32,
width : Int32,
little : Bool,
) -> Unit {
if width == 8 {
if little {
output.write_int64_le(value.to_int64())
} else {
output.write_int64_be(value.to_int64())
}
} else if little {
output.write_int_le(value)
} else {
output.write_int_be(value)
}
}
///|
// Avoid adding an untrusted name length to the absolute archive offset.
fn write_name_padding(offset : Int32, name_size : Int32) -> Int32 {
(8 - (offset % 8 + 4 + name_size % 8) % 8) % 8
}
///|
/// Write a regular archive after validating its complete layout and budgets.
/// GNU uses a deduplicated long-name table; BSD uses embedded names.
/// Symbol mappings refer to final ordinary-entry positions and retain their order.
/// `symbols=None` omits the index, except Darwin always emits at least an empty
/// index. BSD/Darwin indexes use the requested byte order; GNU is always big endian.
/// Darwin aligns object data to eight bytes and includes trailing newline padding
/// in the declared payload. Readers retain those bytes. Other formats preserve
/// exact payload bytes. Deterministic Darwin timestamps distinguish repeated names.
/// Limits count physical records, expanded names and final serialized bytes.
pub fn ArchiveBuilder::write(
self : ArchiveBuilder,
format~ : WriteFormat = Gnu,
metadata~ : WriteMetadata = Deterministic,
symbols~ : Array[WriteSymbol]? = None,
index_little_endian~ : Bool = true,
max_bytes~ : Int32 = 67108864,
max_members~ : Int32 = 100000,
max_name_bytes~ : Int32 = 67108864,
max_symbols~ : Int32 = 1000000,
max_symbol_name_bytes~ : Int32 = 67108864,
) -> Result[Bytes, WriteError] {
if format == Coff {
return write_coff(
self, metadata, symbols, max_bytes, max_members, max_name_bytes, max_symbols,
max_symbol_name_bytes,
)
}
try {
let bsd = write_bsd_format(format)
let darwin = write_darwin_format(format)
let width = write_index_width(format)
let symbols = if darwin && symbols is None { Some([]) } else { symbols }
if max_bytes < 0 ||
max_members < 0 ||
max_name_bytes < 0 ||
max_symbols < 0 ||
max_symbol_name_bytes < 0 {
write_reject("WR003", None, "limits must be nonnegative")
}
if self.members.length() > max_members {
write_reject("WR003", None, "archive entry limit exceeded")
}
let mut total_names = 0
let mut table_size = 0
let name_offsets : Map[Bytes, Int32] = Map([])
let table_names : Array[Bytes] = []
for index, entry in self.members {
write_member_valid(entry, index)
if bsd && member_kind(entry.name) != Regular {
write_reject(
"WR001",
Some(index),
"BSD embedded names reserve symbol and name table spellings",
)
}
total_names = write_add(
total_names,
entry.name.length(),
max_name_bytes,
Some(index),
"expanded entry-name byte limit exceeded",
)
if entry.data.length() > max_bytes {
write_reject("WR003", Some(index), "archive byte limit exceeded")
}
if !bsd {
if entry.name.find(b"\n"[:]) is Some(_) {
write_reject(
"WR001",
Some(index),
"GNU member name contains a newline",
)
}
if !write_gnu_short_name(entry.name) &&
name_offsets.get(entry.name) is None {
name_offsets[entry.name] = table_size
table_names.push(entry.name)
table_size = write_add(
table_size,
entry.name.length(),
max_bytes,
Some(index),
"archive byte limit exceeded",
)
table_size = write_add(
table_size,
2,
max_bytes,
Some(index),
"archive byte limit exceeded",
)
}
}
}
let mut index_size = 0
let mut index_padding = 0
let mut symbol_string_size = 0
let symbol_offsets : Array[Int32] = []
let index_name = if bsd {
if width == 8 {
b"__.SYMDEF_64"
} else {
b"__.SYMDEF"
}
} else if width == 8 {
b"/SYM64/"
} else {
b"/"
}
let index_name_size = if bsd {
index_name.length() + write_name_padding(8, index_name.length())
} else {
0
}
if symbols is Some(symbols) {
if symbols.length() > max_symbols {
write_reject("WR003", None, "symbol count limit exceeded")
}
index_size = write_add(
0,
if bsd {
width * 2
} else {
width
},
max_bytes,
None,
"archive byte limit exceeded",
)
let mut symbol_names = 0
for symbol in symbols {
if symbol.member_index < 0 ||
symbol.member_index >= self.members.length() {
write_reject(
"WR005",
Some(symbol.member_index),
"symbol target is outside the ordinary members",
)
}
if symbol.name.is_empty() {
write_reject(
"WR005",
Some(symbol.member_index),
"symbol name must not be empty",
)
}
for byte in symbol.name {
if byte == b'\x00' {
write_reject(
"WR005",
Some(symbol.member_index),
"NUL inside symbol name",
)
}
}
symbol_names = write_add(
symbol_names,
symbol.name.length(),
max_symbol_name_bytes,
Some(symbol.member_index),
"symbol-name byte limit exceeded",
)
symbol_offsets.push(symbol_string_size)
symbol_string_size = write_add(
symbol_string_size,
symbol.name.length(),
max_bytes,
None,
"archive byte limit exceeded",
)
symbol_string_size = write_add(
symbol_string_size,
1,
max_bytes,
None,
"archive byte limit exceeded",
)
index_size = write_add(
index_size,
if bsd {
width * 2 + 1
} else {
width + 1
},
max_bytes,
None,
"archive byte limit exceeded",
)
index_size = write_add(
index_size,
symbol.name.length(),
max_bytes,
None,
"archive byte limit exceeded",
)
}
if bsd {
index_padding = (8 - index_size % 8) % 8
index_size = write_add(
index_size,
index_padding,
max_bytes,
None,
"archive byte limit exceeded",
)
}
}
let mut count = self.members.length()
let mut offset = write_add(
0,
8,
max_bytes,
None,
"archive byte limit exceeded",
)
if symbols is Some(_) {
count = write_add(
count,
1,
max_members,
None,
"archive entry limit exceeded",
)
total_names = write_add(
total_names,
index_name.length(),
max_name_bytes,
None,
"expanded entry-name byte limit exceeded",
)
let stored_size = write_add(
index_size,
index_name_size,
max_bytes,
None,
"archive byte limit exceeded",
)
offset = write_record_end(offset, stored_size, max_bytes, None)
}
if table_size > 0 {
count = write_add(
count,
1,
max_members,
None,
"archive entry limit exceeded",
)
total_names = write_add(
total_names,
2,
max_name_bytes,
None,
"expanded entry-name byte limit exceeded",
)
offset = write_record_end(offset, table_size, max_bytes, None)
}
ignore(count)
ignore(total_names)
let headers : Array[Int32] = []
let sizes : Array[Int32] = []
let name_sizes : Array[Int32] = []
let payload_paddings : Array[Int32] = []
for index, entry in self.members {
headers.push(offset)
let name_size = if bsd {
write_add(
entry.name.length(),
if darwin || symbols is Some(_) {
write_name_padding(offset, entry.name.length())
} else {
0
},
max_bytes,
Some(index),
"archive byte limit exceeded",
)
} else {
0
}
let payload_padding = if darwin {
(8 - entry.data.length() % 8) % 8
} else {
0
}
let size = write_add(
entry.data.length(),
name_size,
max_bytes,
Some(index),
"archive byte limit exceeded",
)
let size = write_add(
size,
payload_padding,
max_bytes,
Some(index),
"archive byte limit exceeded",
)
sizes.push(size)
name_sizes.push(name_size)
payload_paddings.push(payload_padding)
offset = write_record_end(offset, size, max_bytes, Some(index))
}
// Allocation happens only after every field, reference and output size passes.
let output = @buffer.Buffer(size_hint=offset)
output.write_bytes(b"!\n"[:])
let special_metadata : Metadata = {
timestamp: None,
uid: None,
gid: None,
mode: None,
}
if symbols is Some(symbols) {
if bsd {
write_header(
output,
@utf8.encode("#1/\{index_name_size}"),
special_metadata,
index_name_size + index_size,
)
output.write_bytes(index_name[:])
for i = index_name.length(); i < index_name_size; i = i + 1 {
output.write_byte(b'\x00')
}
} else {
write_header(output, index_name, special_metadata, index_size)
}
let little = bsd && index_little_endian
write_index_number(
output,
if bsd {
symbols.length() * width * 2
} else {
symbols.length()
},
width,
little,
)
for i, symbol in symbols {
if bsd {
write_index_number(output, symbol_offsets[i], width, little)
}
write_index_number(output, headers[symbol.member_index], width, little)
}
if bsd {
write_index_number(output, symbol_string_size, width, little)
}
for symbol in symbols {
output.write_bytes(symbol.name[:])
output.write_byte(b'\x00')
}
for i = 0; i < index_padding; i = i + 1 {
output.write_byte(b'\x00')
}
write_padding(output, index_name_size + index_size)
}
if table_size > 0 {
write_header(output, b"//", special_metadata, table_size)
for name in table_names {
output.write_bytes(name[:])
output.write_bytes(b"/\n"[:])
}
write_padding(output, table_size)
}
let deterministic : Metadata = {
timestamp: Some(0L),
uid: Some(0L),
gid: Some(0L),
mode: Some(420L),
}
let timestamps : Map[Bytes, Int64] = Map([])
for index, entry in self.members {
let header_name = if !bsd {
match name_offsets.get(entry.name) {
Some(reference) => @utf8.encode("/\{reference}")
None => entry.name + b"/"
}
} else {
@utf8.encode("#1/\{name_sizes[index]}")
}
let entry_metadata = if metadata == Deterministic {
if darwin {
let timestamp = timestamps.get(entry.name).unwrap_or(0L)
timestamps[entry.name] = timestamp + 1L
{ ..deterministic, timestamp: Some(timestamp), }
} else {
deterministic
}
} else {
entry.metadata
}
write_header(output, header_name, entry_metadata, sizes[index])
if bsd {
output.write_bytes(entry.name[:])
for i = entry.name.length(); i < name_sizes[index]; i = i + 1 {
output.write_byte(b'\x00')
}
}
output.write_bytes(entry.data[:])
for i = 0; i < payload_paddings[index]; i = i + 1 {
output.write_byte(b'\n')
}
write_padding(output, sizes[index])
}
Ok(output.to_bytes())
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub extend WriteFormat with Eq::{equal, not_equal}
///|
pub extend WriteFormat with Debug::{to_repr}
///|
pub extend WriteMetadata with Eq::{equal, not_equal}
///|
pub extend WriteMetadata with Debug::{to_repr}
///|
pub extend WriteMember with Eq::{equal, not_equal}
///|
pub extend WriteMember with Debug::{to_repr}
///|
pub extend WriteSymbol with Eq::{equal, not_equal}
///|
pub extend WriteSymbol with Debug::{to_repr}
///|
pub extend WriteError with Eq::{equal, not_equal}
///|
pub extend WriteError with Debug::{to_repr}
///|
pub extend ArchiveBuilder with Debug::{to_repr}