Skip to main content

kcode_k1_rust_package/
lib.rs

1use kcode_k1_transaction_id::TxId;
2use semver::{Version, VersionReq};
3use std::fmt::{Display, Formatter};
4use std::str::FromStr;
5use toml::{Table, Value};
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct AuthorityId(TxId);
9
10impl AuthorityId {
11    pub const fn new(transaction_id: TxId) -> Self {
12        Self(transaction_id)
13    }
14
15    pub const fn transaction_id(&self) -> &TxId {
16        &self.0
17    }
18}
19
20impl Display for AuthorityId {
21    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
22        Display::fmt(&self.0, formatter)
23    }
24}
25
26#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct LibraryFamily {
28    authority: AuthorityId,
29    logical_name: String,
30}
31
32impl LibraryFamily {
33    pub fn new(
34        authority: AuthorityId,
35        logical_name: impl Into<String>,
36    ) -> Result<Self, PackageError> {
37        let logical_name = logical_name.into();
38        validate_logical_name(&logical_name)?;
39        Ok(Self {
40            authority,
41            logical_name,
42        })
43    }
44
45    pub const fn authority(&self) -> AuthorityId {
46        self.authority
47    }
48
49    pub fn logical_name(&self) -> &str {
50        &self.logical_name
51    }
52
53    pub fn package_name(&self) -> String {
54        format!("k1-{}-{}", self.authority, self.logical_name)
55    }
56}
57
58#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub struct LibraryId {
60    family: LibraryFamily,
61    version: Version,
62}
63
64impl LibraryId {
65    pub fn new(family: LibraryFamily, version: Version) -> Result<Self, PackageError> {
66        validate_stable(&version)?;
67        Ok(Self { family, version })
68    }
69
70    pub fn family(&self) -> &LibraryFamily {
71        &self.family
72    }
73
74    pub fn version(&self) -> &Version {
75        &self.version
76    }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct SourceFile {
81    path: String,
82    bytes: Vec<u8>,
83}
84
85impl SourceFile {
86    pub fn new(path: impl Into<String>, bytes: Vec<u8>) -> Self {
87        Self {
88            path: path.into(),
89            bytes,
90        }
91    }
92
93    pub fn path(&self) -> &str {
94        &self.path
95    }
96
97    pub fn bytes(&self) -> &[u8] {
98        &self.bytes
99    }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct K1Dependency {
104    alias: String,
105    family: LibraryFamily,
106    requirement: VersionReq,
107}
108
109impl K1Dependency {
110    pub fn alias(&self) -> &str {
111        &self.alias
112    }
113
114    pub fn family(&self) -> &LibraryFamily {
115        &self.family
116    }
117
118    pub fn requirement(&self) -> &VersionReq {
119        &self.requirement
120    }
121}
122
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct SourcePackage {
125    id: LibraryId,
126    files: Vec<SourceFile>,
127    dependencies: Vec<K1Dependency>,
128}
129
130impl SourcePackage {
131    pub fn new(id: LibraryId, mut files: Vec<SourceFile>) -> Result<Self, PackageError> {
132        files
133            .iter()
134            .try_for_each(|file| validate_path(file.path()))?;
135        files.sort_by(|left, right| left.path.cmp(&right.path));
136        if files.windows(2).any(|pair| pair[0].path == pair[1].path) {
137            return fail("duplicate source path");
138        }
139        if files.iter().any(|file| has_ancestor(&files, file.path())) {
140            return fail("source path collides with an ancestor");
141        }
142        let manifest = required_utf8(&files, "Cargo.toml")?;
143        required_utf8(&files, "Documentation.md")?;
144        let dependencies = validate_manifest(&id, manifest)?;
145        Ok(Self {
146            id,
147            files,
148            dependencies,
149        })
150    }
151
152    pub fn id(&self) -> &LibraryId {
153        &self.id
154    }
155
156    pub fn files(&self) -> &[SourceFile] {
157        &self.files
158    }
159
160    pub fn dependencies(&self) -> &[K1Dependency] {
161        &self.dependencies
162    }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct PackageError(String);
167
168impl PackageError {
169    pub fn message(&self) -> &str {
170        &self.0
171    }
172}
173
174impl Display for PackageError {
175    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
176        formatter.write_str(&self.0)
177    }
178}
179
180impl std::error::Error for PackageError {}
181
182fn fail<T>(message: impl Into<String>) -> Result<T, PackageError> {
183    Err(PackageError(message.into()))
184}
185
186fn validate_logical_name(name: &str) -> Result<(), PackageError> {
187    let valid = !name.is_empty()
188        && name.len() <= 36
189        && name.split('-').all(|part| {
190            !part.is_empty()
191                && part
192                    .bytes()
193                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
194        });
195    if !valid {
196        return fail("logical name must be 1-36 bytes of lowercase kebab case");
197    }
198    Ok(())
199}
200
201fn validate_stable(version: &Version) -> Result<(), PackageError> {
202    if !version.pre.is_empty() || !version.build.is_empty() {
203        return fail("version must be stable");
204    }
205    Ok(())
206}
207
208fn validate_path(path: &str) -> Result<(), PackageError> {
209    if path.is_empty()
210        || path.len() > 4096
211        || path.starts_with('/')
212        || path.contains(['\\', '\0'])
213        || path.split('/').any(|component| {
214            component.is_empty() || matches!(component, "." | "..") || component.len() > 255
215        })
216    {
217        return fail("invalid source path");
218    }
219    if path == "Cargo.lock" {
220        return fail("root Cargo.lock is generated by K1");
221    }
222    Ok(())
223}
224
225fn has_ancestor(files: &[SourceFile], path: &str) -> bool {
226    path.match_indices('/').any(|(index, _)| {
227        files
228            .binary_search_by(|file| file.path.as_str().cmp(&path[..index]))
229            .is_ok()
230    })
231}
232
233fn required_utf8<'a>(files: &'a [SourceFile], path: &str) -> Result<&'a str, PackageError> {
234    let file = files
235        .iter()
236        .find(|file| file.path == path)
237        .ok_or_else(|| PackageError(format!("missing {path}")))?;
238    std::str::from_utf8(file.bytes()).map_err(|_| PackageError(format!("{path} must be UTF-8")))
239}
240
241fn validate_manifest(id: &LibraryId, source: &str) -> Result<Vec<K1Dependency>, PackageError> {
242    let root: Table = source
243        .parse()
244        .map_err(|cause| PackageError(format!("invalid Cargo.toml: {cause}")))?;
245    let package = root
246        .get("package")
247        .and_then(Value::as_table)
248        .ok_or_else(|| PackageError("missing [package]".into()))?;
249    require_text(package, "name", &id.family.package_name())?;
250    require_text(package, "version", &id.version.to_string())?;
251
252    let mut dependencies = Vec::new();
253    scan_dependency_sets(&root, &mut dependencies)?;
254    if let Some(targets) = root.get("target").and_then(Value::as_table) {
255        for target in targets.values().filter_map(Value::as_table) {
256            scan_dependency_sets(target, &mut dependencies)?;
257        }
258    }
259    dependencies.sort_by(|left, right| {
260        (&left.alias, &left.family, left.requirement.to_string()).cmp(&(
261            &right.alias,
262            &right.family,
263            right.requirement.to_string(),
264        ))
265    });
266    Ok(dependencies)
267}
268
269fn require_text(table: &Table, key: &str, expected: &str) -> Result<(), PackageError> {
270    if table.get(key).and_then(Value::as_str) != Some(expected) {
271        return fail(format!("package {key} must equal {expected}"));
272    }
273    Ok(())
274}
275
276fn scan_dependency_sets(
277    table: &Table,
278    dependencies: &mut Vec<K1Dependency>,
279) -> Result<(), PackageError> {
280    for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
281        let Some(entries) = table.get(section).and_then(Value::as_table) else {
282            continue;
283        };
284        for (alias, specification) in entries {
285            scan_dependency(alias, specification, dependencies)?;
286        }
287    }
288    Ok(())
289}
290
291fn scan_dependency(
292    alias: &str,
293    specification: &Value,
294    dependencies: &mut Vec<K1Dependency>,
295) -> Result<(), PackageError> {
296    let Some(table) = specification.as_table() else {
297        return Ok(());
298    };
299    if table.get("registry").and_then(Value::as_str) != Some("k1") {
300        return Ok(());
301    }
302    let package = text(
303        table,
304        "package",
305        format!("K1 dependency {alias} needs package"),
306    )?;
307    let family = parse_package_name(package)?;
308    let requirement = text(
309        table,
310        "version",
311        format!("K1 dependency {alias} needs version"),
312    )?;
313    dependencies.push(K1Dependency {
314        alias: alias.into(),
315        family,
316        requirement: parse_requirement(requirement, alias)?,
317    });
318    Ok(())
319}
320
321fn text<'a>(table: &'a Table, key: &str, error: String) -> Result<&'a str, PackageError> {
322    table
323        .get(key)
324        .and_then(Value::as_str)
325        .ok_or(PackageError(error))
326}
327
328fn parse_requirement(requirement: &str, alias: &str) -> Result<VersionReq, PackageError> {
329    VersionReq::parse(requirement)
330        .map_err(|_| PackageError(format!("invalid requirement for {alias}")))
331}
332
333fn parse_package_name(name: &str) -> Result<LibraryFamily, PackageError> {
334    let rest = name
335        .strip_prefix("k1-")
336        .ok_or_else(|| PackageError("malformed K1 package name".into()))?;
337    if rest.len() < 26 || rest.as_bytes().get(24) != Some(&b'-') {
338        return fail("malformed K1 package name");
339    }
340    let authority =
341        TxId::from_str(&rest[..24]).map_err(|_| PackageError("malformed K1 authority".into()))?;
342    let family = LibraryFamily::new(AuthorityId::new(authority), &rest[25..])?;
343    if family.package_name() != name {
344        return fail("malformed K1 package name");
345    }
346    Ok(family)
347}