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}