///|
/// Stable code, complete origin and detail. Hosts can render these separately.
pub(all) suberror ResourceError {
ResourceError(ResourceCode, Provenance, String)
} derive(Debug)
///|
pub(all) struct Limits {
max_archive_bytes : Int32
max_entries : Int32
max_entry_bytes : Int32
max_match_steps : Int32
max_total_bytes : Int32
max_expanded_bytes : Int32
}
///|
pub fn Limits::new(
max_archive_bytes~ : Int32 = 67108864,
max_entries~ : Int32 = 10000,
max_entry_bytes~ : Int32 = 16777216,
max_total_bytes~ : Int32 = 268435456,
max_match_steps~ : Int32 = 16000000,
max_expanded_bytes~ : Int32 = 268435456,
) -> Limits {
{
max_archive_bytes,
max_entries,
max_entry_bytes,
max_total_bytes,
max_match_steps,
max_expanded_bytes,
}
}
///|
pub struct EntryTree {
origin : Location
priv provenance : Provenance
priv paths : Array[EntryPath]
priv members : Set[String]
priv read_entry : (EntryPath) -> Bytes raise ResourceError
priv limits : Limits
priv locate_entry : (EntryPath) -> Location
digest : ContentDigest?
}
///|
/// Validate portable, relative file paths without normalizing distinct names.
pub fn valid_path(path : String) -> Bool {
!path.is_empty() &&
!path
.iter()
.any(c => {
c.to_int() < 32 ||
c.to_int() == 127 ||
"\\:?#%*|\"<>".contains(c.to_string())
}) &&
path.split("/").all(p => p != "" && p != "." && p != "..")
}
///|
fn check_limits(limits : Limits, origin : Location) -> Unit raise ResourceError {
if limits.max_archive_bytes < 0 ||
limits.max_entries < 0 ||
limits.max_entry_bytes < 0 ||
limits.max_total_bytes < 0 ||
limits.max_expanded_bytes < 0 ||
limits.max_match_steps < 0 {
raise ResourceError(
InvalidLimits,
Provenance::new(origin),
"Resource limits must be nonnegative",
)
}
}
///|
/// A host-supplied directory reader. Paths are copied and validated before use.
/// The callback receives only listed relative paths and must return file bytes.
pub fn EntryTree::directory(
paths : Array[EntryPath],
read : (EntryPath) -> Bytes raise ResourceError,
origin : Location,
limits~ : Limits = Limits::new(),
style~ : PathStyle = Slash,
directories~ : Array[EntryPath] = [],
) -> EntryTree raise ResourceError {
check_limits(limits, origin)
if paths.length() > limits.max_entries ||
directories.length() > limits.max_entries - paths.length() {
raise ResourceError(
ResourceLimit,
Provenance::new(origin),
"Too many entries",
)
}
let seen : Set[EntryPath] = Set([])
for path in paths {
if seen.contains(path) {
raise ResourceError(
DuplicateEntry,
Provenance::new(origin),
"Duplicate entry `" + path.to_string() + "`",
)
}
seen.add(path)
}
let members = Set(paths.map(path => path.to_string()))
for directory in directories {
members.add(directory.to_string() + "/")
}
let paths = paths.copy()
paths.sort()
{
origin,
provenance: Provenance::new(origin),
paths,
members,
read_entry: read,
limits,
locate_entry: path => origin.directory_entry(path, style~),
digest: None,
}
}
///|
pub fn EntryTree::entries(self : EntryTree) -> Array[EntryPath] {
self.paths.copy()
}
///|
pub fn EntryTree::contains(self : EntryTree, path : EntryPath) -> Bool {
self.members.contains(path.to_string())
}
///|
pub fn EntryTree::entry_origin(self : EntryTree, path : EntryPath) -> Location {
(self.locate_entry)(path)
}
///|
pub fn EntryTree::read(
self : EntryTree,
path : EntryPath,
) -> Bytes raise ResourceError {
if !self.contains(path) {
raise ResourceError(
MissingResource,
self.entry_provenance(path),
"Missing entry `" + path.to_string() + "`",
)
}
let bytes = (self.read_entry)(path) catch {
ResourceError(code, at, message) =>
raise ResourceError(code, self.contextualize(at), message)
}
if bytes.length() > self.limits.max_entry_bytes ||
bytes.length() > self.limits.max_total_bytes {
raise ResourceError(
ResourceLimit,
self.entry_provenance(path),
"Entry exceeds byte limit",
)
}
bytes
}
///|
pub fn EntryTree::text(
self : EntryTree,
path : EntryPath,
) -> String raise ResourceError {
let bytes = self.read(path)
@utf8.decode(bytes) catch {
_ =>
raise ResourceError(
InvalidUtf8,
self.entry_provenance(path),
"Invalid UTF-8 in `" + path.to_string() + "`",
)
}
}
///|
/// Logical names are independent of physical paths inside the container.
pub(all) struct Declaration {
name : ResourceName
path : EntryPath
} derive(Debug, Eq)
///|
pub struct Resource {
name : ResourceName
path : EntryPath
origin : Location
provenance : Provenance
bytes : Bytes
}
///|
pub fn Resource::text(self : Resource) -> String raise ResourceError {
@utf8.decode(self.bytes) catch {
_ =>
raise ResourceError(
InvalidUtf8,
self.provenance,
"Invalid UTF-8 in `" + self.path.to_string() + "`",
)
}
}
///|
/// Load declarations in order. Duplicate logical names are always an error.
pub fn load_resources(
tree : EntryTree,
declarations : Array[Declaration],
) -> Array[Resource] raise ResourceError {
if declarations.length() > tree.limits.max_entries {
raise ResourceError(
ResourceLimit,
tree.provenance,
"Too many declared resources",
)
}
let seen : Set[ResourceName] = Set([])
let resources = []
let mut total = 0
for declaration in declarations {
if seen.contains(declaration.name) {
raise ResourceError(
DuplicateResource,
tree.provenance,
"Duplicate logical resource `" + declaration.name.to_string() + "`",
)
}
seen.add(declaration.name)
let bytes = tree.read(declaration.path)
if bytes.length() > tree.limits.max_total_bytes - total {
raise ResourceError(
ResourceLimit,
tree.provenance,
"Resources exceed total byte limit",
)
}
total += bytes.length()
resources.push({
name: declaration.name,
path: declaration.path,
origin: tree.entry_origin(declaration.path),
provenance: tree.entry_provenance(declaration.path),
bytes,
})
}
resources
}
///|
/// The diagnostic chain, including repository and resolved identity after resolution.
pub fn EntryTree::provenance(self : EntryTree) -> Provenance {
self.provenance
}
///|
pub fn EntryTree::entry_provenance(
self : EntryTree,
path : EntryPath,
) -> Provenance {
self.provenance.at(self.entry_origin(path))
}
///|
fn EntryTree::contextualize(self : EntryTree, at : Provenance) -> Provenance {
at.through(self.provenance)
}
///|
/// Keep the parent artifact chain when reading a nested ZIP/JAR.
pub fn EntryTree::open_zip(
self : EntryTree,
path : EntryPath,
) -> EntryTree raise ResourceError {
let bytes = self.read(path)
let tree = EntryTree::zip(bytes, self.entry_origin(path), limits=self.limits) catch {
ResourceError(code, at, message) =>
raise ResourceError(code, self.contextualize(at), message)
}
{ ..tree, provenance: self.entry_provenance(path), }
}