///|
/// Aggregate limits for archives fetched while resolving GNU archive-offset
/// references. They are separate from the selected external-payload byte limit.
pub(all) struct NestedLimits {
max_bytes : Int32
max_members : Int32
max_name_bytes : Int32
max_symbols : Int32
max_symbol_name_bytes : Int32
max_depth : Int32
} derive(Eq, Debug)
///|
pub fn NestedLimits::new(
max_bytes~ : Int32 = 67108864,
max_members~ : Int32 = 100000,
max_name_bytes~ : Int32 = 67108864,
max_symbols~ : Int32 = 1000000,
max_symbol_name_bytes~ : Int32 = 67108864,
max_depth~ : Int32 = 16,
) -> NestedLimits {
{
max_bytes,
max_members,
max_name_bytes,
max_symbols,
max_symbol_name_bytes,
max_depth,
}
}
///|
pub extend NestedLimits with Eq::{equal, not_equal}
///|
pub extend NestedLimits with Debug::{to_repr}
///|
priv enum NestedArchive {
RegularContainer(Archive, Map[Int32, Int32])
ThinContainer(ThinArchive, Map[Int32, Int32])
}
///|
priv struct NestedLoadContext {
loader : (Bytes, Int32) -> Result[Bytes, String]
allow_parent_paths : Bool
max_depth : Int32
payload_limit : Int32
path_limit : Int32
mut bytes_left : Int32
mut members_left : Int32
mut names_left : Int32
mut symbols_left : Int32
mut symbol_names_left : Int32
cache : Map[Bytes, NestedArchive]
active : Array[(Bytes, Int32)]
sources : Array[Bytes]
source_set : Map[Bytes, Unit]
}
///|
fn nested_context(
loader : (Bytes, Int32) -> Result[Bytes, String],
allow_parent_paths : Bool,
limits : NestedLimits,
payload_limit : Int32,
) -> NestedLoadContext raise InvalidArchive {
if limits.max_bytes < 0 ||
limits.max_members < 0 ||
limits.max_name_bytes < 0 ||
limits.max_symbols < 0 ||
limits.max_symbol_name_bytes < 0 ||
limits.max_depth < 0 ||
limits.max_depth > 64 {
reject(
"AR008", 0, "nested limits must be nonnegative and depth must not exceed 64",
)
}
{
loader,
allow_parent_paths,
payload_limit,
path_limit: limits.max_name_bytes,
max_depth: limits.max_depth,
bytes_left: limits.max_bytes,
members_left: limits.max_members,
names_left: limits.max_name_bytes,
symbols_left: limits.max_symbols,
symbol_names_left: limits.max_symbol_name_bytes,
cache: Map([]),
active: [],
sources: [],
source_set: Map([]),
}
}
///|
fn nested_diagnostic(
error : ParseError,
path : Bytes,
root_offset : Int32,
) -> ParseError {
{
..error,
offset: root_offset,
message: "nested archive \{Json(@utf8.decode_lossy(path[:], ignore_bom=false)).stringify()} at byte \{error.offset}: \{error.message}",
}
}
///|
fn NestedLoadContext::read_archive(
self : NestedLoadContext,
path : Bytes,
root_offset : Int32,
) -> NestedArchive raise InvalidArchive {
if self.cache.get(path) is Some(cached) {
return cached
}
if self.bytes_left < 8 {
reject(
"AR008", root_offset, "nested archive byte budget cannot hold a container",
)
}
let bytes = match (self.loader)(path, self.bytes_left) {
Ok(bytes) => bytes
Err(message) => reject("AR013", root_offset, message)
}
if bytes.length() > self.bytes_left {
reject("AR008", root_offset, "total nested archive byte limit exceeded")
}
let thin = bytes.has_prefix(b"!\n"[:])
let parsed_thin = if thin {
match
parse_thin(
bytes,
max_bytes=self.bytes_left,
max_members=self.members_left,
max_name_bytes=self.names_left,
max_symbols=self.symbols_left,
max_symbol_name_bytes=self.symbol_names_left,
) {
Ok(value) => Some(value)
Err(error) =>
raise InvalidArchive(nested_diagnostic(error, path, root_offset))
}
} else {
None
}
let archive = match parsed_thin {
Some(value) => value.archive
None =>
match
parse(
bytes,
max_bytes=self.bytes_left,
max_members=self.members_left,
max_name_bytes=self.names_left,
max_symbols=self.symbols_left,
max_symbol_name_bytes=self.symbol_names_left,
) {
Ok(value) => value
Err(error) =>
raise InvalidArchive(nested_diagnostic(error, path, root_offset))
}
}
self.bytes_left -= bytes.length()
self.members_left -= archive.records.length()
let headers : Map[Int32, Int32] = Map([])
for i, entry in archive.records {
headers[entry.header_offset] = i
self.names_left -= entry.name.length()
}
for table in archive.indexes {
let copies = if table.format == "coff" { 2 } else { 1 }
for _copy in 0.. ThinContainer(value, headers)
None => RegularContainer(archive, headers)
}
self.cache[path] = loaded
loaded
}
///|
fn nested_child_path(
parent : Bytes,
child : Bytes,
maximum : Int32,
root_offset : Int32,
) -> Bytes raise InvalidArchive {
let mut cursor = parent.length()
while cursor > 0 && parent[cursor - 1] != b'/' {
cursor -= 1
}
if cursor > maximum || child.length() > maximum - cursor {
reject("AR008", root_offset, "resolved nested source-path budget exceeded")
}
parent[:cursor].to_owned() + child
}
///|
fn NestedLoadContext::resolve(
self : NestedLoadContext,
entry : Member,
reference : Int32?,
path : Bytes,
root_offset : Int32,
load_payload~ : Bool = true,
) -> WriteMember raise InvalidArchive {
if self.source_set.get(path) is None {
if path.length() > self.names_left {
reject(
"AR008", root_offset, "resolved nested source-path budget exceeded",
)
}
self.names_left -= path.length()
self.source_set[path] = ()
self.sources.push(path)
}
guard reference is Some(offset) else {
if !load_payload {
return WriteMember::new(entry.name, b"", metadata=entry.metadata)
}
let bytes = thin_load_bytes(
{ ..entry, header_offset: root_offset, },
path,
self.loader,
max_bytes=self.payload_limit,
)
return WriteMember::new(entry.name, bytes, metadata=entry.metadata)
}
if self.active.contains((path, offset)) {
reject("AR015", root_offset, "cyclic nested archive reference")
}
if self.active.length() >= self.max_depth {
reject("AR008", root_offset, "nested archive depth limit exceeded")
}
self.active.push((path, offset))
let result = match self.read_archive(path, root_offset) {
RegularContainer(archive, headers) => {
guard headers.get(offset) is Some(index) &&
archive.records[index].kind == Regular else {
reject(
"AR015", root_offset, "nested offset is not an ordinary member header",
)
}
let target = archive.records[index]
if target.size != entry.size {
reject(
"AR014", root_offset, "nested member size differs from the thin header",
)
}
WriteMember::new(
target.name,
if load_payload {
archive.member_bytes(index).unwrap()
} else {
b""
},
metadata=entry.metadata,
)
}
ThinContainer(archive, headers) => {
guard headers.get(offset) is Some(index) &&
archive.archive.records[index].kind == Regular else {
reject(
"AR015", root_offset, "nested offset is not an ordinary thin member header",
)
}
let target = archive.archive.records[index]
if target.size != entry.size {
reject(
"AR014", root_offset, "nested member size differs from the thin header",
)
}
let child = match
archive.member_path(index, allow_parent_paths=self.allow_parent_paths) {
Ok(value) => value
Err(error) =>
raise InvalidArchive(nested_diagnostic(error, path, root_offset))
}
let resolved = self.resolve(
target,
archive.references.get(index),
// A previously charged path can be visited after names_left reaches zero.
// Bound the temporary join by the total limit; resolve charges new paths.
nested_child_path(path, child, self.path_limit, root_offset),
root_offset,
load_payload~,
)
{ ..resolved, metadata: entry.metadata, }
}
}
ignore(self.active.pop())
result
}
///|
/// Resolve one ordinary external member, retaining the outer thin metadata.
/// Nested references return the referenced member's logical name and payload.
/// A direct loader request carries the exact declared size; a container request
/// carries the remaining archive-byte budget. The loader controls actual reads.
pub fn ThinArchive::resolve_member(
self : ThinArchive,
index : Int32,
loader : (Bytes, Int32) -> Result[Bytes, String],
max_bytes~ : Int32 = 67108864,
allow_parent_paths~ : Bool = false,
nested_limits~ : NestedLimits = NestedLimits::new(),
) -> Result[WriteMember, ParseError] {
try {
if max_bytes < 0 {
reject("AR008", 0, "limits must be nonnegative")
}
let context = nested_context(
loader, allow_parent_paths, nested_limits, max_bytes,
)
let path = match self.member_path(index, allow_parent_paths~) {
Ok(path) => path
Err(error) => raise InvalidArchive(error)
}
let entry = self.archive.records[index]
if entry.size > max_bytes {
reject(
"AR008",
entry.header_offset,
"external member byte limit exceeded",
)
}
Ok(
context.resolve(
entry,
self.references.get(index),
path,
entry.header_offset,
),
)
} catch {
InvalidArchive(error) => Err(error)
}
}
///|
/// Return portable source paths reached by direct and archive-offset references.
/// Archive containers are parsed and cached, while direct payload files are not
/// opened. Useful for dependency manifests and output-alias protection.
pub fn ThinArchive::member_sources(
self : ThinArchive,
loader : (Bytes, Int32) -> Result[Bytes, String],
allow_parent_paths~ : Bool = false,
nested_limits~ : NestedLimits = NestedLimits::new(),
) -> Result[Array[Bytes], ParseError] {
try {
let context = nested_context(
loader, allow_parent_paths, nested_limits, 2147483647,
)
let paths : Array[(Int32, Bytes)] = []
for index, entry in self.archive.records {
if entry.kind == Regular {
let path = match self.member_path(index, allow_parent_paths~) {
Ok(path) => path
Err(error) => raise InvalidArchive(error)
}
paths.push((index, path))
}
}
for (index, path) in paths {
let entry = self.archive.records[index]
ignore(
context.resolve(
entry,
self.references.get(index),
path,
entry.header_offset,
load_payload=false,
),
)
}
Ok(context.sources.copy())
} catch {
InvalidArchive(error) => Err(error)
}
}