///|
/// An external thin-archive member. The path is interpreted relative to the
/// archive file; size describes the external file, which is never embedded.
pub(all) struct ThinWriteMember {
name : Bytes
size : Int32
metadata : Metadata
archive_member_offset : Int32?
} derive(Eq, Debug)
///|
pub fn ThinWriteMember::new(
name : Bytes,
size : Int32,
metadata~ : Metadata = {
timestamp: Some(0L),
uid: Some(0L),
gid: Some(0L),
mode: Some(420L),
},
archive_member_offset~ : Int32? = None,
) -> ThinWriteMember {
{ name, size, metadata, archive_member_offset, }
}
///|
/// An immutable editing plan for GNU thin archives. Every position is an
/// ordinary-member position, excluding the symbol and name tables.
struct ThinArchiveBuilder {
members : Array[ThinWriteMember]
} derive(Debug)
///|
pub fn ThinArchiveBuilder::new() -> ThinArchiveBuilder {
{ members: [], }
}
///|
fn thin_write_member_valid(
entry : ThinWriteMember,
index : Int32,
) -> Unit raise InvalidWrite {
if entry.archive_member_offset is Some(offset) &&
(offset < 8 || offset % 2 != 0) {
write_reject(
"WR001",
Some(index),
"nested member offset must identify an aligned archive header",
)
}
if entry.size < 0 {
write_reject(
"WR001",
Some(index),
"external member size must be nonnegative",
)
}
if entry.name.find(b"\n"[:]) is Some(_) {
write_reject("WR001", Some(index), "GNU member path contains a newline")
}
// Writing stores names and performs no file I/O. Parent components are legal
// relative references; loading retains its own explicit path-policy choice.
ignore(thin_relative_path(entry.name, 0, allow_parent_paths=true)) catch {
InvalidArchive(error) => write_reject("WR001", Some(index), error.message)
}
// Reuse the numeric-field validation without reserving regular-archive
// special spellings: thin name-table entries are always external members.
write_member_valid(
WriteMember::new(b"member", b"", metadata=entry.metadata),
index,
)
}
///|
/// Validate and copy a collection once, preserving exact path bytes and order.
pub fn ThinArchiveBuilder::from_members(
entries : Array[ThinWriteMember],
) -> Result[ThinArchiveBuilder, WriteError] {
try {
for index, entry in entries {
thin_write_member_valid(entry, index)
}
Ok({ members: entries.copy(), })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub fn ThinArchiveBuilder::member_count(self : ThinArchiveBuilder) -> Int32 {
self.members.length()
}
///|
pub fn ThinArchiveBuilder::members(
self : ThinArchiveBuilder,
) -> Array[ThinWriteMember] {
self.members.copy()
}
///|
pub fn ThinArchiveBuilder::insert(
self : ThinArchiveBuilder,
index : Int32,
entry : ThinWriteMember,
) -> Result[ThinArchiveBuilder, WriteError] {
try {
if index < 0 || index > self.members.length() {
write_reject(
"WR001",
Some(index),
"insert position is outside the archive",
)
}
thin_write_member_valid(entry, index)
let members = self.members.copy()
members.insert(index, entry)
Ok({ members, })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub fn ThinArchiveBuilder::append(
self : ThinArchiveBuilder,
entry : ThinWriteMember,
) -> Result[ThinArchiveBuilder, WriteError] {
self.insert(self.members.length(), entry)
}
///|
pub fn ThinArchiveBuilder::replace(
self : ThinArchiveBuilder,
index : Int32,
entry : ThinWriteMember,
) -> Result[ThinArchiveBuilder, WriteError] {
try {
if index < 0 || index >= self.members.length() {
write_reject("WR001", Some(index), "entry index is outside the archive")
}
thin_write_member_valid(entry, index)
let members = self.members.copy()
members[index] = entry
Ok({ members, })
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub fn ThinArchiveBuilder::remove(
self : ThinArchiveBuilder,
index : Int32,
) -> Result[ThinArchiveBuilder, 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 an entry to its final zero-based position.
pub fn ThinArchiveBuilder::move_member(
self : ThinArchiveBuilder,
from : Int32,
to : Int32,
) -> Result[ThinArchiveBuilder, 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 external references without loading files. Rebuilt symbol mappings
/// must be supplied after editing; dropping an existing index is explicit.
pub fn ThinArchive::edit(
self : ThinArchive,
drop_symbol_indexes~ : Bool = false,
) -> Result[ThinArchiveBuilder, WriteError] {
if !self.archive.indexes.is_empty() && !drop_symbol_indexes {
return Err({
code: "WR004",
member_index: None,
message: "choose drop_symbol_indexes=true and supply fresh symbols when writing",
})
}
let members : Array[ThinWriteMember] = []
for index, entry in self.archive.records {
if entry.kind == Regular {
members.push(
ThinWriteMember::new(
entry.name,
entry.size,
metadata=entry.metadata,
archive_member_offset=self.references.get(index),
),
)
}
}
ThinArchiveBuilder::from_members(members)
}
///|
/// Load all external members into a regular-archive editing plan. max_bytes
/// bounds total payload bytes. All paths and declared sizes are checked before
/// the first loader call. Names, order and metadata are preserved. The loader
/// owns base-directory resolution, symlink policy and I/O allocation.
pub fn ThinArchive::materialize(
self : ThinArchive,
loader : (Bytes, Int32) -> Result[Bytes, String],
drop_symbol_indexes~ : Bool = false,
allow_parent_paths~ : Bool = false,
max_bytes~ : Int32 = 67108864,
nested_limits~ : NestedLimits = NestedLimits::new(),
) -> Result[ArchiveBuilder, ParseError] {
if !self.archive.indexes.is_empty() && !drop_symbol_indexes {
return Err({
code: "AR011",
offset: 0,
message: "materialization requires drop_symbol_indexes=true and fresh symbol discovery when writing",
})
}
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,
)
let members : Array[WriteMember] = []
for (index, path) in paths {
let entry = self.archive.records[index]
members.push(
context.resolve(
entry,
self.references.get(index),
path,
entry.header_offset,
),
)
}
Ok({ members, })
} catch {
InvalidArchive(error) => Err(error)
}
}
///|
/// Write GNU thin bytes without reading or embedding any external file.
/// All paths use a deduplicated GNU name table. symbols=None omits the index;
/// Some([]) emits an empty index. symbol_width selects 32 or 64 bit GNU offsets.
/// Symbol targets are final ordinary-member positions after editing.
/// Byte limits cover serialized archive bytes, not the external payload sizes.
pub fn ThinArchiveBuilder::write(
self : ThinArchiveBuilder,
metadata~ : WriteMetadata = Deterministic,
symbols~ : Array[WriteSymbol]? = None,
symbol_width~ : Int32 = 32,
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] {
try {
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 symbol_width != 32 && symbol_width != 64 {
write_reject("WR005", None, "symbol width must be 32 or 64")
}
if self.members.length() > max_members {
write_reject("WR003", None, "archive entry limit exceeded")
}
let width = symbol_width / 8
let index_name = if symbol_width == 32 { b"/" } else { b"/SYM64/" }
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 {
thin_write_member_valid(entry, index)
total_names = write_add(
total_names,
entry.name.length(),
max_name_bytes,
Some(index),
"expanded entry-name byte limit exceeded",
)
if 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",
)
}
}
for index, entry in self.members {
let name = thin_reference_name(
name_offsets[entry.name],
entry.archive_member_offset,
)
if name.length() > 16 {
write_reject(
"WR001",
Some(index),
"GNU thin name and archive offsets exceed the header name field",
)
}
}
let mut index_size = 0
if symbols is Some(symbols) {
if symbols.length() > max_symbols {
write_reject("WR003", None, "symbol count limit exceeded")
}
index_size = write_add(
0,
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",
)
index_size = write_add(
index_size,
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",
)
}
}
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",
)
offset = write_record_end(offset, index_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] = []
for index, _entry in self.members {
headers.push(offset)
offset = write_add(
offset,
60,
max_bytes,
Some(index),
"archive byte limit exceeded",
)
}
let output = @buffer.Buffer(size_hint=offset)
output.write_bytes(b"!\n"[:])
let special : Metadata = {
timestamp: None,
uid: None,
gid: None,
mode: None,
}
if symbols is Some(symbols) {
write_header(output, index_name, special, index_size)
if width == 4 {
output.write_int_be(symbols.length())
for symbol in symbols {
output.write_int_be(headers[symbol.member_index])
}
} else {
output.write_int64_be(symbols.length().to_int64())
for symbol in symbols {
output.write_int64_be(headers[symbol.member_index].to_int64())
}
}
for symbol in symbols {
output.write_bytes(symbol.name[:])
output.write_byte(b'\x00')
}
write_padding(output, index_size)
}
if table_size > 0 {
write_header(output, b"//", special, 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),
}
for entry in self.members {
write_header(
output,
thin_reference_name(
name_offsets[entry.name],
entry.archive_member_offset,
),
if metadata == Deterministic {
deterministic
} else {
entry.metadata
},
entry.size,
)
}
Ok(output.to_bytes())
} catch {
InvalidWrite(error) => Err(error)
}
}
///|
pub extend ThinWriteMember with Eq::{equal, not_equal}
///|
pub extend ThinWriteMember with Debug::{to_repr}
///|
pub extend ThinArchiveBuilder with Debug::{to_repr}
///|
fn thin_reference_name(name_offset : Int32, member_offset : Int32?) -> Bytes {
@utf8.encode(
match member_offset {
Some(offset) => "/\{name_offset}:\{offset}"
None => "/\{name_offset}"
},
)
}