///|
/// Filesystem layout shared by the native/JS package downloader and the
/// cache-only Wasm Marketplace loader.
struct PackageCacheParts {
package_root_relative : String
package_dir_relative : String
file_relative : String
metadata_relative : String
}
///|
fn is_package_uri_like(uri : String) -> Bool {
uri.has_prefix("package://") || uri.has_prefix("projectpackage://")
}
///|
fn strip_package_scheme(uri : String) -> String? {
if uri.has_prefix("projectpackage://") {
Some(String::unsafe_substring(uri, start=17, end=uri.length()))
} else if uri.has_prefix("package://") {
Some(String::unsafe_substring(uri, start=10, end=uri.length()))
} else {
None
}
}
///|
fn trim_leading_slashes(path : String) -> String {
let mut start = 0
while start < path.length() && path[start].to_int().unsafe_to_char() == '/' {
start += 1
}
String::unsafe_substring(path, start~, end=path.length())
}
///|
fn join_relative_segments(a : String, b : String, c : String) -> String {
let segments = [
trim_leading_slashes(a),
trim_leading_slashes(b),
trim_leading_slashes(c),
]
let buf = StringBuilder()
let mut needs_sep = false
for segment in segments {
if segment == "" {
continue
}
if needs_sep {
buf.write_char('/')
}
buf.write_string(segment)
needs_sep = !segment.has_suffix("/")
}
buf.to_string()
}
///|
fn basename(path : String) -> String {
match path.rev_find("/") {
Some(idx) =>
String::unsafe_substring(path, start=idx + 1, end=path.length())
None => path
}
}
///|
fn strip_package_checksum_from_version(version : String) -> String {
match version.find("::") {
Some(idx) => String::unsafe_substring(version, start=0, end=idx)
None => version
}
}
///|
fn strip_fixture_redirect_segments(path : String) -> String {
let mut rest = trim_leading_slashes(path)
let mut changed = true
while changed {
if rest.has_prefix("HTTP301/") {
rest = String::unsafe_substring(rest, start=8, end=rest.length())
} else if rest.has_prefix("HTTP307/") {
rest = String::unsafe_substring(rest, start=8, end=rest.length())
} else {
changed = false
}
}
rest
}
///|
fn join_path(base : String, relative : String) -> String {
if relative == "" {
base
} else if base.has_suffix("/") {
base + relative
} else {
base + "/" + relative
}
}
///|
fn default_package_cache_root() -> String {
match @env.get_env_var("PKL_MBT_PACKAGE_CACHE") {
Some(root) if root != "" => root
_ =>
match @env.get_env_var("XDG_CACHE_HOME") {
Some(root) if root != "" => root + "/pkl-mbt/package-2"
_ =>
match @env.get_env_var("HOME") {
Some(home) if home != "" => home + "/.cache/pkl-mbt/package-2"
_ => ".pkl-mbt/package-2"
}
}
}
}
///|
fn package_cache_roots_from(configured : Array[String]) -> Array[String] {
if configured.length() > 0 {
configured
} else {
[default_package_cache_root()]
}
}
///|
fn cli_hex_value(c : UInt16) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(c.to_int() - 'a'.to_int() + 10)
} else if c >= 'A' && c <= 'F' {
Some(c.to_int() - 'A'.to_int() + 10)
} else {
None
}
}
///|
fn percent_decode_uri_path(path : String) -> String {
let buf = StringBuilder()
let mut i = 0
while i < path.length() {
if path[i] == '%' && i + 2 < path.length() {
match (cli_hex_value(path[i + 1]), cli_hex_value(path[i + 2])) {
(Some(hi), Some(lo)) => {
buf.write_char((hi * 16 + lo).unsafe_to_char())
i = i + 3
continue
}
_ => ()
}
}
buf.write_char(path[i].to_int().unsafe_to_char())
i = i + 1
}
buf.to_string()
}
///|
fn package_cache_parts(uri : String, rest : String) -> PackageCacheParts? {
let body = match strip_package_scheme(uri) {
Some(body) => body
None => return None
}
let (without_fragment, fragment) = match body.find("#") {
Some(idx) =>
(
String::unsafe_substring(body, start=0, end=idx),
String::unsafe_substring(body, start=idx + 1, end=body.length()),
)
None => (body, "")
}
let after_host = match without_fragment.find("/") {
Some(idx) =>
strip_fixture_redirect_segments(
String::unsafe_substring(
without_fragment,
start=idx + 1,
end=without_fragment.length(),
),
)
None => return None
}
let at_idx = match after_host.rev_find("@") {
Some(idx) => idx
None => return None
}
let name = String::unsafe_substring(after_host, start=0, end=at_idx)
if name == "" {
return None
}
let after_at = String::unsafe_substring(
after_host,
start=at_idx + 1,
end=after_host.length(),
)
let (version, extra) = match after_at.find("/") {
Some(idx) =>
(
String::unsafe_substring(after_at, start=0, end=idx),
String::unsafe_substring(after_at, start=idx + 1, end=after_at.length()),
)
None => (after_at, "")
}
if version == "" {
return None
}
let package_root_relative = "\{name}@\{strip_package_checksum_from_version(version)}"
let package_dir_relative = package_root_relative + "/package"
let suffix = join_relative_segments(
extra,
percent_decode_uri_path(fragment),
rest,
)
let file_relative = if suffix == "" {
package_dir_relative
} else {
package_dir_relative + "/" + suffix
}
let metadata_relative = package_root_relative +
"/" +
basename(package_root_relative) +
".json"
Some({
package_root_relative,
package_dir_relative,
file_relative,
metadata_relative,
})
}
///|
pub fn package_cache_relative(uri : String, rest : String) -> String {
match package_cache_parts(uri, rest) {
Some(parts) => {
// The downloader reads these fields only on native/JS. Keep the shared
// Wasm build warning-clean while exposing one target-neutral layout.
ignore(parts.package_root_relative)
ignore(parts.package_dir_relative)
ignore(parts.metadata_relative)
parts.file_relative
}
None => ""
}
}
///|
fn resolve_package_uri_to_cache_in_roots(
uri : String,
rest : String,
roots : Array[String],
) -> String? {
let relative = match package_cache_parts(uri, rest) {
Some(parts) => parts.file_relative
None => return None
}
for cache_dir in roots {
let candidate = join_path(cache_dir, relative)
if @fs.path_exists(candidate) {
return Some(candidate)
}
}
None
}