///|
/// An absolute, ASCII HTTP(S) URL without credentials or fragments.
/// Non-ASCII path/query characters must be percent-encoded by the caller.
pub struct HttpUrl {
priv value : String
} derive(Eq, Debug)
///|
pub fn HttpUrl::parse(text : String) -> HttpUrl? {
let start = if text.has_prefix("https://") {
8
} else if text.has_prefix("http://") {
7
} else {
return None
}
if text.length() > 8192 ||
text
.iter()
.any(c => {
c.to_int() <= 32 || c.to_int() >= 127 || "\\\"<>#".contains(c.to_string())
}) {
return None
}
let rest = text[start:]
let authority = rest.split("/").next().unwrap().split("?").next().unwrap()
if authority.is_empty() || authority.contains("@") || authority.contains("%") {
return None
}
let (host, port) = if authority.has_prefix("[") {
guard authority.find("]") is Some(end) else { return None }
let host = authority[1:end]
if !host.contains(":") ||
host.iter().any(c => !"0123456789abcdefABCDEF:.".contains(c.to_string())) {
return None
}
let suffix = authority[end + 1:]
if suffix != "" && !suffix.has_prefix(":") {
return None
}
(host, if suffix == "" { None } else { Some(suffix[1:]) })
} else {
let parts = authority.split(":").to_array()
if parts.length() > 2 {
return None
}
let host = parts[0]
if host
.iter()
.any(c => {
!"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_".contains(
c.to_string(),
)
}) {
return None
}
(host, if parts.length() == 2 { Some(parts[1]) } else { None })
}
if host.is_empty() {
return None
}
if port is Some(port) {
if port.is_empty() || port.length() > 5 {
return None
}
let mut number = 0
for c in port.iter() {
let digit = c.to_int() - 48
if digit < 0 || digit > 9 {
return None
}
number = number * 10 + digit
}
if number == 0 || number > 65535 {
return None
}
}
for i in 0..= text.length() ||
!"0123456789abcdefABCDEF".contains(text[i + 1:i + 2].to_owned()) ||
!"0123456789abcdefABCDEF".contains(text[i + 2:i + 3].to_owned())
) {
return None
}
}
Some({ value: text, })
}
///|
pub fn HttpUrl::to_string(self : HttpUrl) -> String {
self.value
}
///|
pub fn HttpUrl::location(self : HttpUrl) -> Location {
Location::parse(self.value).unwrap()
}
///|
pub(all) enum ArchiveFormat {
Zip
Tar
Tgz
} derive(Eq, Debug)
///|
/// Transport implementations must enforce these bounds while receiving bytes.
pub struct AcquisitionLimits {
max_bytes : Int32
timeout_seconds : Int32
} derive(Eq, Debug)
///|
pub fn AcquisitionLimits::new(
max_bytes~ : Int32 = 67108864,
timeout_seconds~ : Int32 = 30,
) -> AcquisitionLimits? {
if max_bytes <= 0 ||
max_bytes > 1073741824 ||
timeout_seconds <= 0 ||
timeout_seconds > 3600 {
None
} else {
Some({ max_bytes, timeout_seconds, })
}
}
///|
pub(all) struct HttpResponse {
resolved : HttpUrl
status : Int32
body : Bytes
}
///|
/// A synchronous host boundary, independent of archive and resource discovery.
/// Follow at most five redirects, prohibit URL user information and non-HTTP
/// schemes, strip authorization on origin changes,
/// preserve TLS certificate verification, and enforce the total time/byte bounds.
pub type HttpTransport = (HttpRequest, AcquisitionLimits) -> HttpResponse raise ResourceError
///|
pub(all) struct PublicArchive {
url : HttpUrl
format : ArchiveFormat
expected_digest : ContentDigest?
} derive(Eq, Debug)
///|
pub(all) struct Acquisition {
requested : HttpUrl
resolved : HttpUrl
digest : ContentDigest
} derive(Eq, Debug)
///|
/// Acquire and verify before parsing any archive bytes. The transport owns I/O;
/// this layer owns status policy, integrity, archive interpretation and provenance.
pub fn PublicArchive::open(
self : PublicArchive,
transport : HttpTransport,
acquisition_limits~ : AcquisitionLimits = AcquisitionLimits::new().unwrap(),
limits~ : Limits = Limits::new(),
cache~ : VerifiedCache? = None,
mode~ : AcquisitionMode = Online,
verify~ : IntegrityVerifier = (_, _) => (),
credentials~ : Credentials = Credentials::new().unwrap(),
accept~ : HttpAccept = AnyContent,
) -> EntryTree raise ResourceError {
check_limits(limits, self.url.location())
let origin = Provenance::new(self.url.location())
if limits.max_archive_bytes == 0 {
raise ResourceError(ResourceLimit, origin, "Archive byte limit is zero")
}
let acquisition_limits = {
..acquisition_limits,
max_bytes: Int32::min(
acquisition_limits.max_bytes,
limits.max_archive_bytes,
),
}
let acquired = acquire(
self.url,
transport,
expected_digest=self.expected_digest,
limits=acquisition_limits,
cache~,
mode~,
verify~,
credentials~,
accept~,
)
let response : HttpResponse = {
resolved: acquired.metadata.resolved,
status: 200,
body: acquired.body,
}
let provenance = Provenance::new(response.resolved.location()).acquired(
acquired.metadata,
)
let tree = try {
match self.format {
Zip =>
EntryTree::zip(response.body, response.resolved.location(), limits~)
Tar =>
EntryTree::tar(response.body, response.resolved.location(), limits~)
Tgz =>
EntryTree::tgz(response.body, response.resolved.location(), limits~)
}
} catch {
ResourceError(code, at, message) =>
raise ResourceError(code, at.through(provenance), message)
}
{ ..tree, provenance, }
}
///|
/// A repository location is a complete URL; its only artifact key is `.`.
pub fn register_public_http(
registry : Registry,
transport : HttpTransport,
format~ : ArchiveFormat = Zip,
expected_digest~ : ContentDigest? = None,
acquisition_limits~ : AcquisitionLimits = AcquisitionLimits::new().unwrap(),
limits~ : Limits = Limits::new(),
cache~ : VerifiedCache? = None,
mode~ : AcquisitionMode = Online,
credentials~ : Credentials = Credentials::new().unwrap(),
) -> Registry raise ResourceError {
registry.register_provider(ProviderId::parse("public-http").unwrap(), (
repository,
reference,
) => {
let origin = Provenance::new(Location::Source(repository.location))
guard reference.key.to_string() == "." &&
HttpUrl::parse(repository.location.to_string()) is Some(url) else {
raise ResourceError(
InvalidResolution,
origin,
"Public HTTP repositories require an absolute credential-free URL and artifact key `.`",
)
}
let tree = PublicArchive::{ url, format, expected_digest, }.open(
transport,
acquisition_limits~,
limits~,
cache~,
mode~,
credentials~,
)
{
reference,
identity: ArtifactIdentity::parse(tree.origin.to_string()).unwrap(),
tree,
}
})
}