///|
/// One package's published binary artifact settings from moon-binstall.json.
pub struct ArtifactConfig {
package_path : String
binary : String
pkg_url : String?
pkg_fmt : String?
bin_path : String?
/// Minimum glibc version the Linux binary requires (e.g. "2.38"), if the
/// publisher declares one.
glibc_min : String?
}
///|
/// Parsed package metadata with exact-target overrides kept separate from the
/// stable public ArtifactConfig shape.
priv struct ArtifactConfigSet {
base : ArtifactConfig
overrides : Array[(String, ArtifactOverride)]
}
///|
priv struct ArtifactOverride {
pkg_url : String?
pkg_fmt : String?
bin_path : String?
glibc_min : String?
}
///|
/// Parse the optional package settings from a module-root moon-binstall.json.
/// The file is part of the Mooncakes source archive, whose independent source
/// checksum is verified before this parser is called.
pub fn parse_artifact_config(
payload : String,
package_path : String,
default_binary : String,
) -> ArtifactConfig? raise {
match parse_artifact_config_set(payload, package_path, default_binary) {
Some(config) => Some(config.base)
None => None
}
}
///|
/// Parse one package's base settings and validate all of its exact-target
/// overrides before target selection begins.
fn parse_artifact_config_set(
payload : String,
package_path : String,
default_binary : String,
) -> ArtifactConfigSet? raise {
let document = @json.parse(payload)
let entries = match document {
{ "packages": Array(values), .. } => values
_ =>
abort_install(
Resolve, "read moon-binstall.json", "",
"invalid moon-binstall.json: expected a packages array",
)
}
let mut found : ArtifactConfigSet? = None
for entry in entries {
let fields = match entry {
Object(value) => value
_ =>
abort_install(
Resolve, "read moon-binstall.json", "",
"invalid package entry in moon-binstall.json",
)
}
let name = match fields.get("package") {
Some(String(value)) => value
_ =>
abort_install(
Resolve, "read moon-binstall.json", "",
"invalid package entry in moon-binstall.json: missing package",
)
}
guard valid_package_path(name) else {
abort_install(
Resolve, "read moon-binstall.json", name,
"unsafe package path in moon-binstall.json",
)
}
if name == package_path {
if found is Some(_) {
abort_install(
Resolve, "read moon-binstall.json", package_path,
"duplicate package entry in moon-binstall.json",
)
}
validate_metadata_keys(
fields,
["package", "binary", "pkg-url", "pkg-fmt", "bin-path", "glibc-min", "overrides"],
"package entry",
)
let binary = match optional_string_field(fields, "binary") {
Some(value) => value
None => default_binary
}
let pkg_url = optional_string_field(fields, "pkg-url")
let pkg_fmt = optional_string_field(fields, "pkg-fmt")
let bin_path = optional_string_field(fields, "bin-path")
let glibc_min = optional_string_field(fields, "glibc-min")
validate_config_fields(pkg_url, pkg_fmt, bin_path, glibc_min)
guard safe_binary_name(binary) else {
abort_install(
Resolve, "read moon-binstall.json", binary,
"unsafe binary name in moon-binstall.json",
)
}
let overrides = parse_target_overrides(fields.get("overrides"))
found = Some({
base: { package_path, binary, pkg_url, pkg_fmt, bin_path, glibc_min, },
overrides,
})
}
}
found
}
///|
fn optional_string_field(
fields : Map[String, Json],
key : String,
) -> String? raise {
match fields.get(key) {
None => None
Some(String(value)) => Some(value)
Some(_) =>
abort_install(
Resolve, "read moon-binstall.json", key,
"invalid \{key} in moon-binstall.json: expected a string",
)
}
}
///|
fn parse_target_overrides(
value : Json?,
) -> Array[(String, ArtifactOverride)] raise {
let overrides : Array[(String, ArtifactOverride)] = []
match value {
None => ()
Some(Object(targets)) =>
for (target, raw_config) in targets.iter() {
validate_target(target)
let fields = match raw_config {
Object(value) => value
_ =>
abort_install(
Resolve, "read moon-binstall.json", target,
"invalid override for target: expected an object",
)
}
validate_metadata_keys(
fields,
["pkg-url", "pkg-fmt", "bin-path", "glibc-min"],
"target override",
)
let pkg_url = optional_string_field(fields, "pkg-url")
let pkg_fmt = optional_string_field(fields, "pkg-fmt")
let bin_path = optional_string_field(fields, "bin-path")
let glibc_min = optional_string_field(fields, "glibc-min")
guard pkg_url is Some(_) || pkg_fmt is Some(_) || bin_path is Some(_) ||
glibc_min is Some(_) else {
abort_install(
Resolve, "read moon-binstall.json", target,
"empty override for the target",
)
}
validate_config_fields(pkg_url, pkg_fmt, bin_path, glibc_min)
overrides.push((target, { pkg_url, pkg_fmt, bin_path, glibc_min, }))
}
Some(_) =>
abort_install(
Resolve, "read moon-binstall.json", "overrides",
"invalid overrides in moon-binstall.json: expected an object",
)
}
overrides
}
///|
fn validate_metadata_keys(
fields : Map[String, Json],
allowed : Array[String],
context : String,
) -> Unit raise {
for (key, _) in fields.iter() {
guard allowed.contains(key) else {
abort_install(
Resolve, "read moon-binstall.json", key,
"unsupported field in moon-binstall.json \{context}",
)
}
}
}
///|
fn valid_package_path(path : String) -> Bool {
let parts = path.split("/").collect()
if parts.length() == 0 {
return false
}
for part in parts {
if !valid_id(part.to_owned()) {
return false
}
}
true
}
///|
fn validate_config_fields(
pkg_url : String?,
pkg_fmt : String?,
bin_path : String?,
glibc_min : String?,
) -> Unit raise {
match pkg_fmt {
Some(format) => validate_archive_format(format)
None => ()
}
match pkg_url {
Some(template) => {
guard template != "" else {
abort_install(
Resolve, "read moon-binstall.json", "pkg-url",
"empty pkg-url in moon-binstall.json",
)
}
validate_artifact_template(template)
}
None => ()
}
match bin_path {
Some(template) => {
validate_artifact_template(template)
validate_member_template(template)
}
None => ()
}
match glibc_min {
Some(version) => validate_glibc_version(version)
None => ()
}
}
///|
/// A glibc version is a dotted numeric tuple such as "2.35" or "2.38".
fn validate_glibc_version(version : String) -> Unit raise {
let parts = version.split(".").collect()
guard parts.length() >= 2 else {
abort_install(
Resolve, "read moon-binstall.json", version,
"invalid glibc-min in moon-binstall.json",
)
}
for part in parts {
guard part != "" else {
abort_install(
Resolve, "read moon-binstall.json", version,
"invalid glibc-min in moon-binstall.json",
)
}
for c in part {
guard c >= '0' && c <= '9' else {
abort_install(
Resolve, "read moon-binstall.json", version,
"invalid glibc-min in moon-binstall.json",
)
}
}
}
}
///|
fn validate_member_template(template : String) -> Unit raise {
// Substitute path-safe values so unsafe literal traversal is rejected at
// manifest parse time, including for a lower-priority target override.
let expanded = expand_artifact_member_template(
template,
"owner",
"repo",
"module",
"package/path",
"binary",
"1.2.3",
"linux-x86_64",
".tgz",
)
validate_member_path(expanded)
}
///|
fn artifact_config_for_target(
set : ArtifactConfigSet,
target : String,
) -> ArtifactConfig {
let mut pkg_url = set.base.pkg_url
let mut pkg_fmt = set.base.pkg_fmt
let mut bin_path = set.base.bin_path
let mut glibc_min = set.base.glibc_min
for (override_target, value) in set.overrides {
if override_target == target {
if value.pkg_url is Some(url) {
pkg_url = Some(url)
}
if value.pkg_fmt is Some(format) {
pkg_fmt = Some(format)
}
if value.bin_path is Some(path) {
bin_path = Some(path)
}
if value.glibc_min is Some(min) {
glibc_min = Some(min)
}
break
}
}
{
package_path: set.base.package_path,
binary: set.base.binary,
pkg_url,
pkg_fmt,
bin_path,
glibc_min,
}
}
///|
/// Read the source repository from a local test/maintainer manifest. If it is
/// omitted, owner/module is used as the GitHub repository coordinate.
pub fn manifest_repository(
payload : String,
module_path : String,
) -> String raise {
let document = @json.parse(payload)
let repository = match document {
{ "repository": String(value), .. } => value
_ => {
let parts = module_path.split("/").collect()
guard parts.length() == 2 &&
valid_id(parts[0].to_owned()) &&
valid_id(parts[1].to_owned()) else {
abort_install(
Resolve, "read moon-binstall.json", module_path,
"the manifest needs a valid repository",
)
}
"https://github.com/\{parts[0]}/\{parts[1]}"
}
}
let (owner, repo) = parse_github_repository(repository)
"https://github.com/\{owner}/\{repo}"
}
///|
pub fn manifest_version(payload : String) -> String? raise {
let document = @json.parse(payload)
match document {
{ "version": String(value), .. } => {
guard valid_registry_version(value) else {
abort_install(
Resolve, "read moon-binstall.json", value,
"unsafe version in moon-binstall.json",
)
}
Some(value)
}
_ => None
}
}
///|
pub fn expand_artifact_template(
template : String,
owner : String,
repo : String,
module_name : String,
package_path : String,
binary : String,
version : String,
target : String,
archive_suffix : String,
) -> String raise {
expand_artifact_template_with_format_alias(
template, owner, repo, module_name, package_path, binary, version, target,
archive_suffix, false,
)
}
///|
/// In a member-path template, Cargo's soft-deprecated `{format}` alias refers
/// to the executable extension (`binary-ext`), which is empty for our targets.
fn expand_artifact_member_template(
template : String,
owner : String,
repo : String,
module_name : String,
package_path : String,
binary : String,
version : String,
target : String,
archive_suffix : String,
) -> String raise {
expand_artifact_template_with_format_alias(
template, owner, repo, module_name, package_path, binary, version, target,
archive_suffix, true,
)
}
///|
fn expand_artifact_template_with_format_alias(
template : String,
owner : String,
repo : String,
module_name : String,
package_path : String,
binary : String,
version : String,
target : String,
archive_suffix : String,
member_path : Bool,
) -> String raise {
let (target_family, target_os, target_arch) = target_template_facts(target)
let archive_format = if archive_suffix.has_prefix(".") {
archive_suffix[1:].to_owned()
} else {
archive_suffix
}
let values : Array[(String, String)] = [
("repo", "https://github.com/\{owner}/\{repo}"),
("name", module_name),
("module", module_name),
("package", package_path),
("bin", binary),
("binary", binary),
("version", version),
("target", target),
("archive-suffix", archive_suffix),
(
"archive-format",
archive_format,
),
("format", if member_path { "" } else { archive_format }),
("target-family", target_family),
("target-arch", target_arch),
("os-name", target_os),
("binary-ext", ""),
]
expand_template(template, values)
}
///|
fn target_template_facts(target : String) -> (String, String, String) raise {
match target {
"linux-x86_64" => ("linux", "linux", "x86_64")
"linux-aarch64" => ("linux", "linux", "aarch64")
"darwin-x86_64" => ("darwin", "macos", "x86_64")
"darwin-aarch64" => ("darwin", "macos", "aarch64")
_ =>
abort_install(
Resolve, "expand artifact template", target,
"unsupported target in artifact template",
)
}
}
///|
/// Validate placeholders without needing a resolved target. This runs over
/// every declared base and target-specific template before asset fallback.
pub fn validate_artifact_template(template : String) -> Unit raise {
let known = [
"repo", "name", "module", "package", "bin", "binary", "version",
"target", "archive-suffix", "archive-format", "format", "target-family",
"target-arch", "os-name", "binary-ext",
]
let chars : Array[Char] = []
for c in template {
chars.push(c)
}
let mut index = 0
while index < chars.length() {
let current = chars[index]
if current == '\\' && index + 1 < chars.length() {
let next = chars[index + 1]
if next == '{' || next == '}' || next == '\\' {
index = index + 2
continue
}
}
if current == '{' {
index = index + 1
let variable = StringBuilder()
while index < chars.length() && chars[index] != '}' {
guard chars[index] != '{' else {
abort_install(
Resolve, "expand artifact template", template,
"nested opening brace in artifact template",
)
}
variable.write_char(chars[index])
index = index + 1
}
guard index < chars.length() else {
abort_install(
Resolve, "expand artifact template", template,
"unclosed variable in artifact template",
)
}
let key = variable.to_string().trim().to_owned()
guard known.contains(key) else {
abort_install(
Resolve, "expand artifact template", key,
"unsupported variable in artifact template",
)
}
index = index + 1
continue
}
if current == '}' {
abort_install(
Resolve, "expand artifact template", template,
"unmatched closing brace in artifact template",
)
}
index = index + 1
}
}
///|
fn expand_template(
template : String,
values : Array[(String, String)],
) -> String raise {
let chars : Array[Char] = []
for c in template {
chars.push(c)
}
let result = StringBuilder(size_hint=template.length())
let mut index = 0
while index < chars.length() {
let current = chars[index]
if current == '\\' && index + 1 < chars.length() {
let next = chars[index + 1]
if next == '{' || next == '}' || next == '\\' {
result.write_char(next)
index = index + 2
continue
}
}
if current == '{' {
index = index + 1
let variable = StringBuilder()
while index < chars.length() && chars[index] != '}' {
guard chars[index] != '{' else {
abort_install(
Resolve, "expand artifact template", template,
"nested opening brace in artifact template",
)
}
variable.write_char(chars[index])
index = index + 1
}
guard index < chars.length() else {
abort_install(
Resolve, "expand artifact template", template,
"unclosed variable in artifact template",
)
}
let key = variable.to_string().trim().to_owned()
let mut resolved : String? = None
for (name, value) in values {
let candidate = if name.has_prefix("{") {
name[1:name.length() - 1].to_owned()
} else {
name
}
if candidate == key {
resolved = Some(value)
break
}
}
let value = match resolved {
Some(value) => value
None =>
abort_install(
Resolve, "expand artifact template", key,
"unsupported variable in artifact template",
)
}
result.write_string(value)
index = index + 1
continue
}
if current == '}' {
abort_install(
Resolve, "expand artifact template", template,
"unmatched closing brace in artifact template",
)
}
result.write_char(current)
index = index + 1
}
result.to_string()
}
///|
pub fn artifact_url_asset_name(
url : String,
owner : String,
repo : String,
tag : String,
) -> String raise {
let prefix = "https://github.com/\{owner}/\{repo}/releases/download/\{tag}/"
guard url.has_prefix(prefix) else {
abort_install(
Resolve, "expand artifact template", url,
"pkg-url must resolve to a GitHub release asset in the module repository",
)
}
let name = url[prefix.length():].to_owned()
guard valid_asset_name(name) else {
abort_install(
Resolve, "expand artifact template", name,
"unsafe release asset name in pkg-url",
)
}
name
}
///|
pub fn infer_archive_format(name : String) -> String {
if name.has_suffix(".tar.bz2") {
"tar.bz2"
} else if name.has_suffix(".tbz2") {
"tbz2"
} else if name.has_suffix(".tar.zstd") {
"tar.zstd"
} else if name.has_suffix(".tar.zst") {
"tzstd"
} else if name.has_suffix(".tzstd") {
"tzstd"
} else if name.has_suffix(".tar.xz") {
"tar.xz"
} else if name.has_suffix(".txz") {
"txz"
} else if name.has_suffix(".tar.gz") {
"tar.gz"
} else if name.has_suffix(".tgz") {
"tgz"
} else if name.has_suffix(".tar") {
"tar"
} else if name.has_suffix(".zip") {
"zip"
} else {
"bin"
}
}
///|
pub fn archive_suffix(format : String) -> String raise {
validate_archive_format(format)
match format {
"bin" => ""
"tar" => ".tar"
"tgz" => ".tgz"
"tar.gz" => ".tar.gz"
"tbz2" => ".tbz2"
"tar.bz2" => ".tar.bz2"
"txz" => ".txz"
"tar.xz" => ".tar.xz"
"tzstd" | "tar.zst" => ".tar.zst"
"tar.zstd" => ".tar.zstd"
"zip" => ".zip"
_ =>
abort_install(
Archive, "read archive", format, "unsupported archive format",
)
}
}
///|
pub fn validate_archive_format(format : String) -> Unit raise {
guard format == "bin" ||
format == "tar" ||
format == "tgz" ||
format == "tar.gz" ||
format == "tbz2" ||
format == "tar.bz2" ||
format == "txz" ||
format == "tar.xz" ||
format == "tzstd" ||
format == "tar.zst" ||
format == "tar.zstd" ||
format == "zip" else {
abort_install(
Archive, "read archive", format,
"unsupported package format (supported: bin, tar, tgz, tar.gz, tbz2, tar.bz2, txz, tar.xz, tzstd, tar.zst, zip)",
)
}
}
///|
pub fn safe_binary_name(name : String) -> Bool {
if name == "" || name == "." || name == ".." || name.has_prefix("-") {
return false
}
for c in name {
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_' ||
c == '.') {
return false
}
}
true
}
///|
/// Reject absolute paths, traversal, option-like member names, backslashes,
/// and control characters before a member path is passed to an archive tool.
pub fn validate_member_path(path : String) -> Unit raise {
guard path != "" &&
!path.has_prefix("/") &&
!path.has_prefix("-") &&
!path.contains("\\") else {
abort_install(
Archive, "validate archive member path", path,
"unsafe archive member path",
)
}
let parts = path.split("/").collect()
for part in parts {
guard part != "" && part != "." && part != ".." &&
!part.has_prefix("-") else {
abort_install(
Archive, "validate archive member path", path,
"unsafe archive member path",
)
}
for c in part {
guard c != '\n' && c != '\r' && c != '\t' else {
abort_install(
Archive, "validate archive member path", path,
"unsafe archive member path",
)
}
}
}
}
///|
pub fn default_artifact_names(
binary : String,
version : String,
target : String,
) -> Array[String] {
let names : Array[String] = []
let versions = [version, "v" + version]
let formats = [
"", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".tar.xz", ".txz",
".tar.zst", ".tar.zstd", ".tzstd", ".zip",
]
for format in formats {
names.push("\{binary}-\{target}\{format}")
for version_part in versions {
names.push("\{binary}-\{target}-\{version_part}\{format}")
names.push("\{binary}-\{version_part}-\{target}\{format}")
}
}
append_underscore_asset_names(names, binary, version, target, formats)
names
}
///|
fn append_underscore_asset_names(
names : Array[String],
stem : String,
version : String,
target : String,
formats : Array[String],
) -> Unit {
let versions = [version, "v" + version]
for format in formats {
append_unique(names, "\{stem}_\{target}\{format}")
for version_part in versions {
append_unique(names, "\{stem}_\{target}_\{version_part}\{format}")
append_unique(names, "\{stem}_\{version_part}_\{target}\{format}")
}
}
}
///|
fn append_unique(values : Array[String], value : String) -> Unit {
if !values.contains(value) {
values.push(value)
}
}
///|
/// When the caller selects an archive format, do not let an earlier raw
/// candidate shadow the matching archive asset in the release.
pub fn default_artifact_names_for_format(
binary : String,
version : String,
target : String,
format : String?,
) -> Array[String] raise {
match format {
None => default_artifact_names(binary, version, target)
Some(selected) => {
let suffix = archive_suffix(selected)
let names : Array[String] = []
let versions = [version, "v" + version]
names.push("\{binary}-\{target}\{suffix}")
for version_part in versions {
names.push("\{binary}-\{target}-\{version_part}\{suffix}")
names.push("\{binary}-\{version_part}-\{target}\{suffix}")
}
append_underscore_asset_names(names, binary, version, target, [suffix])
names
}
}
}
///|
/// Ordered exact archive member candidates for archives that omit bin-path.
/// The existing root executable path remains first; the remaining paths use
/// the standard release-directory spellings documented by cargo-binstall.
pub fn default_archive_member_candidates(
module_name : String,
package_path : String,
binary : String,
version : String,
target : String,
) -> Array[String] raise {
validate_target(target)
guard safe_binary_name(binary) && valid_id(module_name) &&
valid_package_path(package_path) &&
(valid_registry_version(version) || valid_tag(version)) else {
abort_install(
Archive, "discover archive member", package_path,
"unsafe values for archive member discovery",
)
}
let candidates : Array[String] = [binary]
let versions = [version, "v" + version]
let directories = [
"\{module_name}-\{target}-v\{version}",
"\{module_name}-\{target}-\{version}",
"\{module_name}-\{version}-\{target}",
"\{module_name}-v\{version}-\{target}",
"\{module_name}-\{target}",
"\{module_name}-\{version}",
"\{module_name}-v\{version}",
module_name,
]
for directory in directories {
append_unique(candidates, "\{directory}/\{binary}")
}
// MoonBit packages are often laid out by their package path in source
// archives. These are lower-priority exact matches after release layouts.
append_unique(candidates, "\{package_path}/\{binary}")
for version_part in versions {
append_unique(candidates, "\{binary}-\{target}-\{version_part}")
append_unique(candidates, "\{binary}-\{version_part}-\{target}")
}
for candidate in candidates {
validate_member_path(candidate)
}
candidates
}