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()
215 || matches!(component, "." | ".." | ".cargo")
216 || component.len() > 255
217 })
218 {
219 return fail("invalid source path");
220 }
221 let leaf = path.rsplit('/').next().expect("nonempty validated path");
222 if leaf.starts_with("rust-toolchain")
223 || path == "Cargo.lock"
224 || leaf == "Cargo.toml" && path != "Cargo.toml"
225 {
226 return fail("forbidden source path");
227 }
228 Ok(())
229}
230
231fn has_ancestor(files: &[SourceFile], path: &str) -> bool {
232 path.match_indices('/').any(|(index, _)| {
233 files
234 .binary_search_by(|file| file.path.as_str().cmp(&path[..index]))
235 .is_ok()
236 })
237}
238
239fn required_utf8<'a>(files: &'a [SourceFile], path: &str) -> Result<&'a str, PackageError> {
240 let file = files
241 .iter()
242 .find(|file| file.path == path)
243 .ok_or_else(|| PackageError(format!("missing {path}")))?;
244 std::str::from_utf8(file.bytes()).map_err(|_| PackageError(format!("{path} must be UTF-8")))
245}
246
247fn validate_manifest(id: &LibraryId, source: &str) -> Result<Vec<K1Dependency>, PackageError> {
248 let root: Table = source
249 .parse()
250 .map_err(|cause| PackageError(format!("invalid Cargo.toml: {cause}")))?;
251 let package = root
252 .get("package")
253 .and_then(Value::as_table)
254 .ok_or_else(|| PackageError("missing [package]".into()))?;
255 require_text(package, "name", &id.family.package_name())?;
256 require_text(package, "version", &id.version.to_string())?;
257 require_text(package, "edition", "2024")?;
258 for key in ["autobins", "autoexamples", "autotests", "autobenches"] {
259 if package.get(key) != Some(&Value::Boolean(false)) {
260 return fail(format!("package {key} must be false"));
261 }
262 }
263 if package.contains_key("include")
264 || package.contains_key("exclude")
265 || package.get("publish") == Some(&Value::Boolean(false))
266 {
267 return fail("forbidden package selection or publication setting");
268 }
269 if package
270 .iter()
271 .any(|(key, value)| key != "metadata" && inherits(value))
272 || root.get("lints").is_some_and(inherits)
273 {
274 return fail("workspace inheritance is forbidden");
275 }
276 if root.contains_key("patch") || root.contains_key("replace") {
277 return fail("patch and replace are forbidden");
278 }
279 match root.get("workspace") {
280 Some(Value::Table(table))
281 if table.len() == 1 && table.get("resolver").and_then(Value::as_str) == Some("3") => {}
282 None => {}
283 Some(_) => return fail("workspace may contain only resolver = 3"),
284 }
285 validate_targets(id, &root)?;
286 let mut dependencies = Vec::new();
287 scan_dependency_sets(&root, &mut dependencies)?;
288 if let Some(targets) = root.get("target") {
289 for target in targets
290 .as_table()
291 .ok_or_else(|| PackageError("target must be a table".into()))?
292 .values()
293 {
294 scan_dependency_sets(
295 target
296 .as_table()
297 .ok_or_else(|| PackageError("target entry must be a table".into()))?,
298 &mut dependencies,
299 )?;
300 }
301 }
302 dependencies.sort_by(|left, right| {
303 (&left.alias, &left.family, left.requirement.to_string()).cmp(&(
304 &right.alias,
305 &right.family,
306 right.requirement.to_string(),
307 ))
308 });
309 Ok(dependencies)
310}
311
312fn inherits(value: &Value) -> bool {
313 value
314 .as_table()
315 .and_then(|table| table.get("workspace"))
316 .and_then(Value::as_bool)
317 == Some(true)
318}
319
320fn validate_targets(id: &LibraryId, root: &Table) -> Result<(), PackageError> {
321 if ["example", "bench", "test"]
322 .iter()
323 .any(|key| root.contains_key(*key))
324 {
325 return fail("example, bench, and test targets are forbidden");
326 }
327 let has_library = match root.get("lib") {
328 None => false,
329 Some(value) => {
330 let library = value
331 .as_table()
332 .ok_or_else(|| PackageError("lib target must be a table".into()))?;
333 let expected = id.family.logical_name().replace('-', "_");
334 if library.get("name").and_then(Value::as_str) != Some(&expected) {
335 return fail(format!("lib name must equal {expected}"));
336 }
337 true
338 }
339 };
340 let mut bin_names = match root.get("bin") {
341 None => Vec::new(),
342 Some(value) => value
343 .as_array()
344 .ok_or_else(|| PackageError("bin targets must be an array".into()))?
345 .iter()
346 .map(bin_name)
347 .collect::<Result<Vec<_>, _>>()?,
348 };
349 bin_names
350 .iter()
351 .try_for_each(|name| validate_logical_name(name))?;
352 bin_names.sort_unstable();
353 if bin_names.windows(2).any(|pair| pair[0] == pair[1]) {
354 return fail("duplicate bin target name");
355 }
356 if !has_library && bin_names.is_empty() {
357 return fail("package needs a lib or bin target");
358 }
359 Ok(())
360}
361
362fn bin_name(value: &Value) -> Result<&str, PackageError> {
363 let table = value
364 .as_table()
365 .ok_or_else(|| PackageError("bin target must be a table".into()))?;
366 text(table, "name", "bin target needs a name".into())
367}
368
369fn text<'a>(table: &'a Table, key: &str, error: String) -> Result<&'a str, PackageError> {
370 table
371 .get(key)
372 .and_then(Value::as_str)
373 .ok_or(PackageError(error))
374}
375
376fn require_text(table: &Table, key: &str, expected: &str) -> Result<(), PackageError> {
377 if table.get(key).and_then(Value::as_str) != Some(expected) {
378 return fail(format!("package {key} must equal {expected}"));
379 }
380 Ok(())
381}
382
383fn scan_dependency_sets(
384 table: &Table,
385 dependencies: &mut Vec<K1Dependency>,
386) -> Result<(), PackageError> {
387 for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
388 let Some(entries) = table.get(section) else {
389 continue;
390 };
391 for (alias, specification) in entries
392 .as_table()
393 .ok_or_else(|| PackageError(format!("{section} must be a table")))?
394 {
395 scan_dependency(alias, specification, dependencies)?;
396 }
397 }
398 Ok(())
399}
400
401fn scan_dependency(
402 alias: &str,
403 specification: &Value,
404 dependencies: &mut Vec<K1Dependency>,
405) -> Result<(), PackageError> {
406 if let Some(requirement) = specification.as_str() {
407 if alias.starts_with("k1-") {
408 return fail(format!("K1 dependency {alias} needs registry k1"));
409 }
410 return parse_requirement(requirement, alias).map(|_| ());
411 }
412 let table = specification
413 .as_table()
414 .ok_or_else(|| PackageError(format!("invalid dependency {alias}")))?;
415 if ["path", "git", "workspace"]
416 .iter()
417 .any(|key| table.contains_key(*key))
418 {
419 return fail(format!("forbidden dependency source for {alias}"));
420 }
421 match table.get("registry") {
422 Some(Value::String(registry)) if registry == "k1" => {
423 let package = text(
424 table,
425 "package",
426 format!("K1 dependency {alias} needs package"),
427 )?;
428 let family = parse_package_name(package)?;
429 let requirement = text(
430 table,
431 "version",
432 format!("K1 dependency {alias} needs version"),
433 )?;
434 dependencies.push(K1Dependency {
435 alias: alias.into(),
436 family,
437 requirement: parse_requirement(requirement, alias)?,
438 });
439 }
440 Some(Value::String(_)) => return fail(format!("custom registry forbidden for {alias}")),
441 Some(_) => return fail(format!("invalid registry for {alias}")),
442 None => {
443 let package = table
444 .get("package")
445 .and_then(Value::as_str)
446 .unwrap_or(alias);
447 if package.starts_with("k1-") {
448 return fail(format!("K1 dependency {alias} needs registry k1"));
449 }
450 let requirement = text(
451 table,
452 "version",
453 format!("dependency {alias} needs version"),
454 )?;
455 parse_requirement(requirement, alias)?;
456 }
457 }
458 Ok(())
459}
460
461fn parse_requirement(requirement: &str, alias: &str) -> Result<VersionReq, PackageError> {
462 VersionReq::parse(requirement)
463 .map_err(|_| PackageError(format!("invalid requirement for {alias}")))
464}
465
466fn parse_package_name(name: &str) -> Result<LibraryFamily, PackageError> {
467 let rest = name
468 .strip_prefix("k1-")
469 .ok_or_else(|| PackageError("malformed K1 package name".into()))?;
470 if rest.len() < 26 || rest.as_bytes().get(24) != Some(&b'-') {
471 return fail("malformed K1 package name");
472 }
473 let authority =
474 TxId::from_str(&rest[..24]).map_err(|_| PackageError("malformed K1 authority".into()))?;
475 let family = LibraryFamily::new(AuthorityId::new(authority), &rest[25..])?;
476 if family.package_name() != name {
477 return fail("malformed K1 package name");
478 }
479 Ok(family)
480}