///|
/// 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?
}
///|
/// 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?
}
///|
/// 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
_ => fail("invalid moon-binstall.json: expected a packages array")
}
let mut found : ArtifactConfigSet? = None
for entry in entries {
let fields = match entry {
Object(value) => value
_ => fail("invalid package entry in moon-binstall.json")
}
let name = match fields.get("package") {
Some(String(value)) => value
_ => fail("invalid package entry in moon-binstall.json: missing package")
}
guard valid_package_path(name) else {
fail("unsafe package path in moon-binstall.json")
}
if name == package_path {
if found is Some(_) {
fail("duplicate package entry: \{package_path}")
}
validate_metadata_keys(
fields,
["package", "binary", "pkg-url", "pkg-fmt", "bin-path", "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")
validate_config_fields(pkg_url, pkg_fmt, bin_path)
guard safe_binary_name(binary) else {
fail("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, },
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(_) => fail("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
_ => fail("invalid override for target \{target}: expected an object")
}
validate_metadata_keys(
fields,
["pkg-url", "pkg-fmt", "bin-path"],
"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")
guard pkg_url is Some(_) || pkg_fmt is Some(_) || bin_path is Some(_) else {
fail("empty override for target \{target}")
}
validate_config_fields(pkg_url, pkg_fmt, bin_path)
overrides.push((target, { pkg_url, pkg_fmt, bin_path, }))
}
Some(_) => fail("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 {
fail("unsupported field in moon-binstall.json \{context}: \{key}")
}
}
}
///|
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?,
) -> Unit raise {
match pkg_fmt {
Some(format) => validate_archive_format(format)
None => ()
}
match pkg_url {
Some(template) => {
guard template != "" else { fail("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 => ()
}
}
///|
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
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)
}
break
}
}
{
package_path: set.base.package_path,
binary: set.base.binary,
pkg_url,
pkg_fmt,
bin_path,
}
}
///|
/// 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 {
fail("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 {
fail("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")
_ => fail("unsupported target in artifact template: \{target}")
}
}
///|
/// 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 {
fail("nested opening brace in artifact template")
}
variable.write_char(chars[index])
index = index + 1
}
guard index < chars.length() else {
fail("unclosed variable in artifact template")
}
let key = variable.to_string().trim().to_owned()
guard known.contains(key) else {
fail("unsupported variable in artifact template: \{key}")
}
index = index + 1
continue
}
if current == '}' {
fail("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 {
fail("nested opening brace in artifact template")
}
variable.write_char(chars[index])
index = index + 1
}
guard index < chars.length() else {
fail("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 => fail("unsupported variable in artifact template: \{key}")
}
result.write_string(value)
index = index + 1
continue
}
if current == '}' {
fail("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 {
fail(
"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 {
fail("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"
_ => fail("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 {
fail(
"unsupported package format: \{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 {
fail("unsafe archive member path")
}
let parts = path.split("/").collect()
for part in parts {
guard part != "" && part != "." && part != ".." &&
!part.has_prefix("-") else {
fail("unsafe archive member path")
}
for c in part {
guard c != '\n' && c != '\r' && c != '\t' else {
fail("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 {
fail("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
}