///|
suberror PackageDownloadError {
PackageDownloadError(String)
} derive(Eq)
///|
impl Show for PackageDownloadError with fn output(self, logger) {
match self {
PackageDownloadError(message) =>
logger.write_string("PackageDownloadError(\{message})")
}
}
///|
struct ZipEntry {
path : String
content : Bytes
is_dir : Bool
}
///|
fn is_http_redirect(code : Int) -> Bool {
code == 301 || code == 302 || code == 303 || code == 307 || code == 308
}
///|
fn package_download_error(message : String) -> Unit raise PackageDownloadError {
raise PackageDownloadError::PackageDownloadError(message)
}
///|
fn package_metadata_url_from_uri(uri : String) -> String? {
if uri.has_prefix("package://") {
match parse_package_uri(uri) {
Some(parsed) =>
Some(
package_metadata_url({
authority: parsed.authority,
path: strip_package_checksum_from_path(parsed.path),
fragment: parsed.fragment,
}),
)
None => None
}
} else if uri.has_prefix("projectpackage://") {
let body = String::unsafe_substring(uri, start=17, end=uri.length())
let without_fragment = match body.find("#") {
Some(idx) => String::unsafe_substring(body, start=0, end=idx)
None => body
}
Some("https://\{strip_package_checksum_from_path(without_fragment)}")
} else {
None
}
}
///|
fn extract_package_zip_sha256(body : String) -> String? {
let parsed = Some(@json.parse(body[:])) catch { _ => None }
match parsed {
Some(Object(top)) =>
match top.get("packageZipChecksums") {
Some(Object(checksums)) =>
match checksums.get("sha256") {
Some(String(value)) => Some(value)
_ => None
}
_ => None
}
_ => None
}
}
///|
fn ensure_dir(path : String) -> Unit raise {
if path == "" || path == "/" {
return
}
if @fs.path_exists(path) {
let dir_ok = @fs.is_dir(path) catch { _ => false }
if dir_ok {
return
}
package_download_error("path exists but is not a directory: \{path}")
}
let mut i = if path.has_prefix("/") { 1 } else { 0 }
while i <= path.length() {
if i == path.length() || path[i].to_int().unsafe_to_char() == '/' {
let part = String::unsafe_substring(path, start=0, end=i)
if part != "" && part != "/" && !@fs.path_exists(part) {
@fs.create_dir(part)
}
}
i += 1
}
}
///|
fn parent_dir(path : String) -> String {
match path.rev_find("/") {
Some(idx) => String::unsafe_substring(path, start=0, end=idx)
None => ""
}
}
///|
fn bytes_slice(data : Bytes, start : Int, end : Int) -> Bytes raise {
if start < 0 || end < start || end > data.length() {
package_download_error("byte slice out of range")
}
Bytes::from_array(FixedArray::makei(end - start, fn(i) { data[start + i] }))
}
///|
fn empty_bytes() -> Bytes {
Bytes::from_array([])
}
///|
fn read_u16_le(data : Bytes, offset : Int) -> Int raise {
if offset < 0 || offset + 2 > data.length() {
package_download_error("unexpected end of zip data")
}
data[offset].to_int() | (data[offset + 1].to_int() << 8)
}
///|
fn read_u32_le(data : Bytes, offset : Int) -> Int raise {
if offset < 0 || offset + 4 > data.length() {
package_download_error("unexpected end of zip data")
}
data[offset].to_int() |
(data[offset + 1].to_int() << 8) |
(data[offset + 2].to_int() << 16) |
(data[offset + 3].to_int() << 24)
}
///|
fn find_zip_eocd(data : Bytes) -> Int raise {
if data.length() < 22 {
package_download_error("zip data is too short")
}
let min = if data.length() > 66000 { data.length() - 66000 } else { 0 }
let mut i = data.length() - 22
while i >= min {
if read_u32_le(data, i) == 0x06054b50 {
return i
}
i -= 1
}
package_download_error("zip end-of-central-directory not found")
0
}
///|
fn read_zip_entries(data : Bytes) -> Array[ZipEntry] raise {
let eocd = find_zip_eocd(data)
let total_entries = read_u16_le(data, eocd + 10)
let central_offset = read_u32_le(data, eocd + 16)
let entries : Array[ZipEntry] = []
let mut offset = central_offset
for _ in 0.. compressed
8 => @zlib.deflate_decompress(compressed, max_size=uncompressed_size)
_ => {
package_download_error(
"unsupported zip compression method: \{compression_method}",
)
empty_bytes()
}
}
}
if !is_dir && content.length() != uncompressed_size {
package_download_error("zip entry size mismatch: \{name}")
}
ignore(flags)
entries.push({ path: name, content, is_dir })
offset = name_end + extra_len + comment_len
}
entries
}
///|
fn zip_entry_path_is_safe(path : String) -> Bool {
// Packages produced by the official Pkl tooling commonly contain a
// directory marker named exactly `/`. It maps to the extraction root and
// carries no payload, so accepting it does not permit an absolute file.
if path == "/" {
return true
}
if path == "" || path.has_prefix("/") || path.find("\\") is Some(_) {
return false
}
let mut start = 0
let mut i = 0
while i <= path.length() {
if i == path.length() || path[i].to_int().unsafe_to_char() == '/' {
let segment = String::unsafe_substring(path, start~, end=i)
if segment == ".." {
return false
}
start = i + 1
}
i += 1
}
true
}
///|
fn extract_zip_to_dir(data : Bytes, package_dir : String) -> Unit raise {
ensure_dir(package_dir)
for entry in read_zip_entries(data) {
if !zip_entry_path_is_safe(entry.path) {
package_download_error("unsafe zip entry path: \{entry.path}")
}
let target = join_path(package_dir, entry.path)
if entry.is_dir {
ensure_dir(target)
} else {
ensure_dir(parent_dir(target))
@fs.write_bytes_to_file(target, entry.content)
}
}
}
///|
async fn download_package_uri_to_cache(uri : String) -> Bool {
try {
let parts = match package_cache_parts(uri, "") {
Some(parts) => parts
None => {
package_download_error("malformed package URI: \{uri}")
return false
}
}
let metadata_url = match package_metadata_url_from_uri(uri) {
Some(url) => url
None => {
package_download_error("malformed package URI: \{uri}")
return false
}
}
let metadata = match fetch_uri(metadata_url) {
Some(body) => body
None => {
package_download_error("metadata fetch failed: \{metadata_url}")
return false
}
}
let zip_url = match extract_package_zip_url(metadata) {
Some(url) => url
None => {
package_download_error("metadata is missing packageZipUrl")
return false
}
}
let expected_sha256 = match extract_package_zip_sha256(metadata) {
Some(value) => value
None => {
package_download_error("metadata is missing packageZipChecksums.sha256")
return false
}
}
let zip_bytes = match fetch_uri_bytes(zip_url) {
Some(bytes) => bytes
None => {
package_download_error("zip fetch failed: \{zip_url}")
return false
}
}
let actual_sha256 = @crypto.bytes_to_hex_string(@crypto.sha256(zip_bytes))
if actual_sha256 != expected_sha256 {
package_download_error(
"package zip sha256 mismatch: expected \{expected_sha256}, got \{actual_sha256}",
)
}
let roots = package_cache_roots()
let cache_root = roots[0]
let package_root = join_path(cache_root, parts.package_root_relative)
let package_dir = join_path(cache_root, parts.package_dir_relative)
ensure_dir(package_root)
extract_zip_to_dir(zip_bytes, package_dir)
@fs.write_string_to_file(
join_path(cache_root, parts.metadata_relative),
metadata,
)
true
} catch {
error => {
println("package download failed for \{uri}: \{error}")
false
}
}
}
///|
async fn resolve_or_download_package_uri_to_cache(
uri : String,
rest : String,
) -> String? {
match resolve_package_uri_to_cache(uri, rest) {
Some(path) => Some(path)
None =>
if download_package_uri_to_cache(uri) {
resolve_package_uri_to_cache(uri, rest)
} else {
None
}
}
}