use kcode_k1_transaction_id::TxId;
use semver::{Version, VersionReq};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use toml::{Table, Value};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AuthorityId(TxId);
impl AuthorityId {
pub const fn new(transaction_id: TxId) -> Self {
Self(transaction_id)
}
pub const fn transaction_id(&self) -> &TxId {
&self.0
}
}
impl Display for AuthorityId {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, formatter)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LibraryFamily {
authority: AuthorityId,
logical_name: String,
}
impl LibraryFamily {
pub fn new(
authority: AuthorityId,
logical_name: impl Into<String>,
) -> Result<Self, PackageError> {
let logical_name = logical_name.into();
validate_logical_name(&logical_name)?;
Ok(Self {
authority,
logical_name,
})
}
pub const fn authority(&self) -> AuthorityId {
self.authority
}
pub fn logical_name(&self) -> &str {
&self.logical_name
}
pub fn package_name(&self) -> String {
format!("k1-{}-{}", self.authority, self.logical_name)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LibraryId {
family: LibraryFamily,
version: Version,
}
impl LibraryId {
pub fn new(family: LibraryFamily, version: Version) -> Result<Self, PackageError> {
validate_stable(&version)?;
Ok(Self { family, version })
}
pub fn family(&self) -> &LibraryFamily {
&self.family
}
pub fn version(&self) -> &Version {
&self.version
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceFile {
path: String,
bytes: Vec<u8>,
}
impl SourceFile {
pub fn new(path: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
path: path.into(),
bytes,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct K1Dependency {
alias: String,
family: LibraryFamily,
version: Version,
}
impl K1Dependency {
pub fn alias(&self) -> &str {
&self.alias
}
pub fn family(&self) -> &LibraryFamily {
&self.family
}
pub fn version(&self) -> &Version {
&self.version
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourcePackage {
id: LibraryId,
files: Vec<SourceFile>,
dependencies: Vec<K1Dependency>,
}
impl SourcePackage {
pub fn new(id: LibraryId, mut files: Vec<SourceFile>) -> Result<Self, PackageError> {
files
.iter()
.try_for_each(|file| validate_path(file.path()))?;
files.sort_by(|left, right| left.path.cmp(&right.path));
if files.windows(2).any(|pair| pair[0].path == pair[1].path) {
return fail("duplicate source path");
}
let manifest = required_utf8(&files, "Cargo.toml")?;
required_utf8(&files, "Documentation.md")?;
let dependencies = validate_manifest(&id, manifest)?;
Ok(Self {
id,
files,
dependencies,
})
}
pub fn id(&self) -> &LibraryId {
&self.id
}
pub fn files(&self) -> &[SourceFile] {
&self.files
}
pub fn dependencies(&self) -> &[K1Dependency] {
&self.dependencies
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackageError(String);
impl PackageError {
pub fn message(&self) -> &str {
&self.0
}
}
impl Display for PackageError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for PackageError {}
fn fail<T>(message: impl Into<String>) -> Result<T, PackageError> {
Err(PackageError(message.into()))
}
fn validate_logical_name(name: &str) -> Result<(), PackageError> {
let valid = !name.is_empty()
&& name.len() <= 36
&& name.split('-').all(|part| {
!part.is_empty()
&& part
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
});
if !valid {
return fail("logical name must be 1-36 bytes of lowercase kebab case");
}
Ok(())
}
fn validate_stable(version: &Version) -> Result<(), PackageError> {
if !version.pre.is_empty() || !version.build.is_empty() {
return fail("version must be stable");
}
Ok(())
}
fn validate_path(path: &str) -> Result<(), PackageError> {
if path.is_empty()
|| path.len() > 4096
|| path.starts_with('/')
|| path.contains(['\\', '\0'])
|| path.split('/').any(|component| {
component.is_empty()
|| matches!(component, "." | ".." | ".cargo")
|| component.len() > 255
})
{
return fail("invalid source path");
}
let leaf = path.rsplit('/').next().expect("nonempty validated path");
if leaf.starts_with("rust-toolchain")
|| path == "Cargo.lock"
|| leaf == "Cargo.toml" && path != "Cargo.toml"
{
return fail("forbidden source path");
}
Ok(())
}
fn required_utf8<'a>(files: &'a [SourceFile], path: &str) -> Result<&'a str, PackageError> {
let file = files
.iter()
.find(|file| file.path == path)
.ok_or_else(|| PackageError(format!("missing {path}")))?;
std::str::from_utf8(file.bytes()).map_err(|_| PackageError(format!("{path} must be UTF-8")))
}
fn validate_manifest(id: &LibraryId, source: &str) -> Result<Vec<K1Dependency>, PackageError> {
let root: Table = source
.parse()
.map_err(|cause| PackageError(format!("invalid Cargo.toml: {cause}")))?;
let package = root
.get("package")
.and_then(Value::as_table)
.ok_or_else(|| PackageError("missing [package]".into()))?;
require_text(package, "name", &id.family.package_name())?;
require_text(package, "version", &id.version.to_string())?;
require_text(package, "edition", "2024")?;
if package.contains_key("include")
|| package.contains_key("exclude")
|| package.get("publish") == Some(&Value::Boolean(false))
{
return fail("forbidden package selection or publication setting");
}
if root.contains_key("patch") || root.contains_key("replace") {
return fail("patch and replace are forbidden");
}
if let Some(workspace) = root.get("workspace") {
let workspace = workspace
.as_table()
.ok_or_else(|| PackageError("workspace must be a table".into()))?;
if workspace.len() != 1 || workspace.get("resolver").and_then(Value::as_str) != Some("3") {
return fail("workspace may contain only resolver = 3");
}
}
let mut dependencies = Vec::new();
scan_dependency_sets(&root, &mut dependencies)?;
if let Some(targets) = root.get("target") {
for target in targets
.as_table()
.ok_or_else(|| PackageError("target must be a table".into()))?
.values()
{
let target = target
.as_table()
.ok_or_else(|| PackageError("target entry must be a table".into()))?;
scan_dependency_sets(target, &mut dependencies)?;
}
}
dependencies.sort_by(|left, right| {
(&left.alias, &left.family, &left.version).cmp(&(
&right.alias,
&right.family,
&right.version,
))
});
Ok(dependencies)
}
fn require_text(table: &Table, key: &str, expected: &str) -> Result<(), PackageError> {
if table.get(key).and_then(Value::as_str) != Some(expected) {
return fail(format!("package {key} must equal {expected}"));
}
Ok(())
}
fn scan_dependency_sets(
table: &Table,
dependencies: &mut Vec<K1Dependency>,
) -> Result<(), PackageError> {
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
let Some(entries) = table.get(section) else {
continue;
};
for (alias, specification) in entries
.as_table()
.ok_or_else(|| PackageError(format!("{section} must be a table")))?
{
scan_dependency(alias, specification, dependencies)?;
}
}
Ok(())
}
fn scan_dependency(
alias: &str,
specification: &Value,
dependencies: &mut Vec<K1Dependency>,
) -> Result<(), PackageError> {
if let Some(requirement) = specification.as_str() {
if alias.starts_with("k1-") {
return fail(format!("K1 dependency {alias} needs registry k1"));
}
return VersionReq::parse(requirement)
.map(|_| ())
.map_err(|_| PackageError(format!("invalid requirement for {alias}")));
}
let table = specification
.as_table()
.ok_or_else(|| PackageError(format!("invalid dependency {alias}")))?;
if ["path", "git", "workspace"]
.iter()
.any(|key| table.contains_key(*key))
{
return fail(format!("forbidden dependency source for {alias}"));
}
match table.get("registry") {
Some(Value::String(registry)) if registry == "k1" => {
let package = table
.get("package")
.and_then(Value::as_str)
.ok_or_else(|| PackageError(format!("K1 dependency {alias} needs package")))?;
let family = parse_package_name(package)?;
let requirement = table
.get("version")
.and_then(Value::as_str)
.ok_or_else(|| PackageError(format!("K1 dependency {alias} needs version")))?;
dependencies.push(K1Dependency {
alias: alias.into(),
family,
version: parse_exact_version(requirement, alias)?,
});
}
Some(Value::String(_)) => return fail(format!("custom registry forbidden for {alias}")),
Some(_) => return fail(format!("invalid registry for {alias}")),
None => {
let package = table
.get("package")
.and_then(Value::as_str)
.unwrap_or(alias);
if package.starts_with("k1-") {
return fail(format!("K1 dependency {alias} needs registry k1"));
}
let requirement = table
.get("version")
.and_then(Value::as_str)
.ok_or_else(|| PackageError(format!("dependency {alias} needs version")))?;
VersionReq::parse(requirement)
.map_err(|_| PackageError(format!("invalid requirement for {alias}")))?;
}
}
Ok(())
}
fn parse_exact_version(requirement: &str, alias: &str) -> Result<Version, PackageError> {
let text = requirement
.strip_prefix('=')
.ok_or_else(|| PackageError(format!("K1 dependency {alias} needs exact version")))?;
let version = Version::parse(text)
.map_err(|_| PackageError(format!("invalid K1 version for {alias}")))?;
validate_stable(&version)?;
if requirement != format!("={version}") {
return fail(format!("noncanonical K1 version for {alias}"));
}
Ok(version)
}
fn parse_package_name(name: &str) -> Result<LibraryFamily, PackageError> {
let rest = name
.strip_prefix("k1-")
.ok_or_else(|| PackageError("malformed K1 package name".into()))?;
if rest.len() < 26 || rest.as_bytes().get(24) != Some(&b'-') {
return fail("malformed K1 package name");
}
let authority =
TxId::from_str(&rest[..24]).map_err(|_| PackageError("malformed K1 authority".into()))?;
LibraryFamily::new(AuthorityId::new(authority), &rest[25..])
}
#[cfg(test)]
mod tests {
use super::*;
fn family(name: &str) -> LibraryFamily {
LibraryFamily::new(AuthorityId::new(TxId::from_bytes([1; 12])), name).unwrap()
}
fn package(extra: &str, paths: &[&str]) -> Result<SourcePackage, PackageError> {
let id = LibraryId::new(family("alpha"), Version::new(1, 2, 3)).unwrap();
let manifest = format!(
"[package]\nname='k1-010101010101010101010101-alpha'\nversion='1.2.3'\nedition='2024'\n{extra}"
);
let mut files = vec![
SourceFile::new("Cargo.toml", manifest.into_bytes()),
SourceFile::new("Documentation.md", b"docs".to_vec()),
];
files.extend(paths.iter().map(|path| SourceFile::new(*path, Vec::new())));
SourcePackage::new(id, files)
}
#[test]
fn validates_identity_and_sorts_arbitrary_source() {
assert!(family("one-two2").package_name().ends_with("-one-two2"));
for name in ["", "A", "a_b", "-a", "a-", "a--b", &"a".repeat(37)] {
assert!(LibraryFamily::new(family("x").authority(), name).is_err());
}
let id = LibraryId::new(family("alpha"), Version::new(1, 2, 3)).unwrap();
let mut files = package("", &["z", "a"]).unwrap().files;
files
.iter_mut()
.find(|file| file.path == "z")
.unwrap()
.bytes = vec![255];
let value = SourcePackage::new(id, files).unwrap();
let paths = value
.files()
.iter()
.map(SourceFile::path)
.collect::<Vec<_>>();
assert_eq!(paths, ["Cargo.toml", "Documentation.md", "a", "z"]);
assert_eq!(value.files().last().unwrap().bytes(), [255]);
}
#[test]
fn rejects_unsafe_paths_and_invalid_manifests() {
for path in [
"/x",
"a\\b",
"a//b",
"a/../b",
".cargo/x",
"nested/Cargo.toml",
"Cargo.lock",
"nested/rust-toolchain.future",
] {
assert!(package("", &[path]).is_err());
}
for extra in [
"[patch.x]",
"[workspace]\nresolver='2'",
"[dependencies]\nx={path='x'}",
"[dependencies]\nx={registry='other',version='1'}",
"publish=false",
] {
assert!(package(extra, &[]).is_err());
}
}
#[test]
fn extracts_all_k1_dependency_locations() {
let other = "k1-020202020202020202020202-beta";
let extra = format!(
"[dependencies]\nserde='1'\n[target.'cfg(windows)'.dependencies]\nb={{registry='k1',package='{other}',version='=2.3.4'}}\n[target.'cfg(unix)'.dev-dependencies]\nc={{registry='k1',package='{other}',version='=3.0.0'}}"
);
let value = package(&extra, &[]).unwrap();
let aliases = value
.dependencies()
.iter()
.map(K1Dependency::alias)
.collect::<Vec<_>>();
assert_eq!(aliases, ["b", "c"]);
assert_eq!(value.dependencies()[0].version(), &Version::new(2, 3, 4));
assert_eq!(value.dependencies()[1].version(), &Version::new(3, 0, 0));
for invalid in [
"[dependencies]\nx={registry='k1',package='k1-bad',version='=1.0.0'}",
"[dependencies]\nk1-020202020202020202020202-beta='1'",
] {
assert!(package(invalid, &[]).is_err());
}
}
}