///|
/// A lexical rule for portable payload-owned relative names. This restriction
/// applies only to embedded asset names, never to argv, cwd or user paths.
pub(all) enum PayloadPathRule {
Empty
Absolute
EmptySegment
DotSegment
ParentSegment
Backslash
ControlCharacter
NonAscii
ReservedCharacter
TrailingDotOrSpace
ReservedName
} derive(Eq, Debug)
///|
pub extend PayloadPathRule with Eq::{equal, not_equal}
///|
pub extend PayloadPathRule with @debug.Debug::{to_repr}
///|
/// An invalid original asset name and the rule that it breaks.
pub suberror PayloadPathError {
InvalidPayloadPath(input~ : String, rule~ : PayloadPathRule)
} derive(Debug)
///|
pub extend PayloadPathError with @debug.Debug::{to_repr}
///|
/// A nonempty relative slash-separated ASCII asset name, portable to Windows
/// and POSIX filesystems, with no traversal, reserved syntax or ambiguous names.
pub struct RelativeFilePath {
priv text : String
priv identity : String
} derive(Debug)
///|
pub extend RelativeFilePath with @debug.Debug::{to_repr}
///|
/// Parse an unambiguous portable asset name before filesystem access.
pub fn RelativeFilePath::parse(input : String) -> RelativeFilePath raise PayloadPathError {
guard !input.is_empty() else { raise InvalidPayloadPath(input~, rule=Empty) }
guard !input.has_prefix("/") else { raise InvalidPayloadPath(input~, rule=Absolute) }
for code_unit in input.to_array() {
let code = code_unit.to_int()
if code == 92 { raise InvalidPayloadPath(input~, rule=Backslash) }
if code < 32 || code == 127 { raise InvalidPayloadPath(input~, rule=ControlCharacter) }
if code > 127 { raise InvalidPayloadPath(input~, rule=NonAscii) }
// A tilde could name a filesystem-generated Windows 8.3 alias of another
// entry. Exclude it so lexical identities remain sufficient before I/O.
if code_unit is ('<' | '>' | ':' | '"' | '|' | '?' | '*' | '~') {
raise InvalidPayloadPath(input~, rule=ReservedCharacter)
}
}
for segment in input.split("/") {
if segment.is_empty() { raise InvalidPayloadPath(input~, rule=EmptySegment) }
if segment == "." { raise InvalidPayloadPath(input~, rule=DotSegment) }
if segment == ".." { raise InvalidPayloadPath(input~, rule=ParentSegment) }
if segment.has_suffix(".") || segment.has_suffix(" ") {
raise InvalidPayloadPath(input~, rule=TrailingDotOrSpace)
}
// Windows reserves these base names even when followed by an extension.
let base = segment.split(".").next().unwrap().trim_end(chars=" ").to_owned().to_lower()
if ["con", "prn", "aux", "nul", "conin$", "conout$", "clock$"].contains(base) ||
(base.length() == 4 && (base.has_prefix("com") || base.has_prefix("lpt")) &&
base.code_unit_at(3) >= 49 && base.code_unit_at(3) <= 57) {
raise InvalidPayloadPath(input~, rule=ReservedName)
}
}
{ text: input, identity: input.to_lower() }
}
///|
/// The retained spelling of this relative asset name.
pub fn RelativeFilePath::to_string(self : RelativeFilePath) -> String { self.text }
///|
/// An immutable asset entry. Bytes are immutable MoonBit values.
pub struct PayloadFile {
priv path : RelativeFilePath
priv bytes : Bytes
}
///|
/// Pair a parsed asset name with immutable bytes.
pub fn PayloadFile::new(path : RelativeFilePath, bytes : Bytes) -> PayloadFile {
{ path, bytes }
}
///|
/// The asset's parsed relative name.
pub fn PayloadFile::path(self : PayloadFile) -> RelativeFilePath { self.path }
///|
/// The exact immutable asset bytes.
pub fn PayloadFile::bytes(self : PayloadFile) -> Bytes { self.bytes }
///|
/// A layout collision or missing entry, retaining the parsed names involved.
pub suberror PayloadError {
DuplicatePayloadPath(first~ : RelativeFilePath, second~ : RelativeFilePath)
PayloadFileDirectoryConflict(file~ : RelativeFilePath, descendant~ : RelativeFilePath)
PayloadDirectoryCaseConflict(first~ : RelativeFilePath, second~ : RelativeFilePath)
PayloadEntryMissing(entry~ : RelativeFilePath)
} derive(Debug)
///|
pub extend PayloadError with @debug.Debug::{to_repr}
///|
/// A complete collision-free immutable layout with an entry that names an
/// actual file. File order is retained; path identity is ASCII case insensitive.
pub struct Payload {
priv files : Array[PayloadFile]
priv entry : RelativeFilePath
}
///|
/// Parse collection rules once and take ownership of a separate file array.
pub fn Payload::new(
files : Array[PayloadFile],
entry : RelativeFilePath,
) -> Payload raise PayloadError {
let paths : Map[String, RelativeFilePath] = Map([])
for file in files {
let path = file.path
if paths.get(path.identity) is Some(first) {
raise DuplicatePayloadPath(first~, second=path)
}
paths.set(path.identity, path)
}
let directories : Map[String, (String, RelativeFilePath)] = Map([])
for file in files {
let segments = file.path.identity.split("/").to_array()
let exact_segments = file.path.text.split("/").to_array()
for end in 1.. file.path.text == entry.text) else { raise PayloadEntryMissing(entry~) }
{ files: files.copy(), entry }
}
///|
/// A fresh array whose immutable entries retain the original order.
pub fn Payload::files(self : Payload) -> Array[PayloadFile] { self.files.copy() }
///|
/// The exact retained entry filename.
pub fn Payload::entry(self : Payload) -> RelativeFilePath { self.entry }
///|
/// A Node executable, owned payload and opaque application arguments.
pub struct NodeLaunch {
priv node : Executable
priv payload : Payload
priv args : Arguments
}
///|
/// Construct a Node launch from a valid complete payload and parsed arguments.
pub fn NodeLaunch::new(
payload : Payload,
args : Arguments,
node~ : Executable = Executable::node(),
) -> NodeLaunch {
{ node, payload, args }
}
///|
/// The selected Node executable, defaulting to PATH lookup.
pub fn NodeLaunch::executable(self : NodeLaunch) -> Executable { self.node }
///|
/// The immutable complete file layout.
pub fn NodeLaunch::payload(self : NodeLaunch) -> Payload { self.payload }
///|
/// The opaque application arguments after the private absolute entry path.
pub fn NodeLaunch::arguments(self : NodeLaunch) -> Arguments { self.args }