rucc_sysroot/manifest.rs
1//! What a produced sysroot carries: every input, where it came from, its hash and its licence.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.6 for the licence half,
4//! `spec/cross-compile/13-distribution.md` section 13.5 for the provenance half, and
5//! `spec/cross-compile/02-the-goal.md` claim 5 for the rest.
6//!
7//! # Three jobs, one file
8//!
9//! Claim 5 asks for byte identical output from two hosts for the same target. Checking that over a
10//! sysroot means comparing several thousand files, and the first thing anybody does when the
11//! comparison fails is ask which file and where it came from. A manifest answers both, and comparing
12//! two manifests is a diff of a few hundred lines rather than of a directory tree.
13//! [`Manifest::digest`] is the same comparison in one line, for when the answer wanted is yes or no
14//! rather than which file.
15//!
16//! The second job is the licence wall. Section 8.6 says the macOS SDK and the Windows SDK cannot be
17//! redistributed, and the way that rule gets enforced rather than remembered is that every input
18//! carries its licence and [`Manifest::redistributable`] is a function anything that publishes an
19//! artifact can call. A rule in a document is a rule somebody breaks in eighteen months.
20//!
21//! The third is provenance. Section 13.5 asks the compiler to emit, for every input that is not its
22//! own code, the name, the upstream project, the version, the source URL, the content hash, the
23//! licence and whether the input was bundled, generated or fetched. That is this file's line with
24//! two fields added, so `-print-sysroot-provenance` prints a manifest rather than a second format
25//! saying the same things in a different order.
26//!
27//! # The format
28//!
29//! Tab separated lines, sorted by path, under a header that is two lines and sometimes three: the
30//! format version, the target, and the Linux release the kernel headers came out of when the sysroot
31//! has kernel headers in it. Not JSON, because the thing this is optimized for is a person reading a
32//! diff between two of them, and not TOML, because it has no nesting and a parser for it is thirty
33//! lines. Sorted because the order files come out of a directory walk is a property of the
34//! filesystem, and a manifest whose line order depended on that would report a difference between
35//! two identical sysroots.
36//!
37//! ```
38//! use rucc_sysroot::{Input, Licence, Manifest, Provenance};
39//! use rucc_tuple::{TargetTuple, Version};
40//!
41//! let target: TargetTuple = "aarch64-linux-musl".parse().unwrap();
42//! let mut manifest = Manifest::new(target);
43//! manifest.set_kernel(Version::new(6, 12));
44//! manifest.push(Input {
45//! path: "include/generic/stdio.h".into(),
46//! source: "musl-1.2.5".into(),
47//! url: "https://musl.libc.org/releases/musl-1.2.5.tar.gz".into(),
48//! sha256: "0".repeat(64),
49//! licence: Licence::Mit,
50//! provenance: Provenance::Bundled,
51//! });
52//!
53//! let text = manifest.render();
54//! assert_eq!(Manifest::parse(&text).unwrap(), manifest);
55//! assert!(manifest.redistributable());
56//! ```
57
58use std::fmt;
59use std::str::FromStr;
60
61use rucc_tuple::{TargetTuple, Version};
62
63/// The licence an input arrives under.
64///
65/// The list is section 8.2's table with one variant per row, plus the kernel headers, which every
66/// Linux row needs and which no row is about. A closed list rather than a free text field, because
67/// the question [`Licence::redistributable`] answers has to have an answer for every input and a
68/// string does not.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
70pub enum Licence {
71 /// musl, which is MIT and the reason it goes first.
72 Mit,
73 /// glibc, which is LGPL. Redistributable, with obligations that
74 /// `spec/cross-compile/13-distribution.md` owns.
75 Lgpl,
76 /// The BSD libcs, which are permissive.
77 Bsd,
78 /// The Linux uapi headers, which are GPL-2.0 with the syscall note.
79 ///
80 /// The note is the whole reason this is a separate variant and not a refusal: it says that
81 /// using the headers to make a system call does not put the calling program under the GPL,
82 /// which is what every libc and every cross toolchain relies on. Redistributing the headers
83 /// themselves carries the GPL's own obligation, and the pinned source URL in the manifest is
84 /// how it is met, the same way it is for glibc.
85 LinuxUapi,
86 /// mingw-w64, which is a mix of permissive licences and public domain headers.
87 MingwPermissive,
88 /// Ours. The compiler's own headers and its runtime.
89 Apache2,
90 /// The macOS SDK, restricted by the Xcode agreement to Apple-branded hardware. Never shipped,
91 /// only ever pointed at.
92 AppleSdk,
93 /// The Windows SDK and the universal CRT, which are not redistributable.
94 MicrosoftSdk,
95}
96
97impl Licence {
98 /// Whether an artifact containing this input can be published.
99 ///
100 /// Two of the eight answer false, and they are the two section 8.6 calls legal walls rather
101 /// than engineering. A sysroot containing either is a local thing on the machine of somebody
102 /// who accepted the licence themselves.
103 #[must_use]
104 pub const fn redistributable(self) -> bool {
105 !matches!(self, Licence::AppleSdk | Licence::MicrosoftSdk)
106 }
107
108 /// The spelling in a manifest file.
109 #[must_use]
110 pub const fn as_str(self) -> &'static str {
111 match self {
112 Licence::Mit => "mit",
113 Licence::Lgpl => "lgpl",
114 Licence::Bsd => "bsd",
115 Licence::LinuxUapi => "gpl-2.0-with-linux-syscall-note",
116 Licence::MingwPermissive => "mingw-permissive",
117 Licence::Apache2 => "apache-2.0",
118 Licence::AppleSdk => "apple-sdk",
119 Licence::MicrosoftSdk => "microsoft-sdk",
120 }
121 }
122}
123
124impl fmt::Display for Licence {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.write_str(self.as_str())
127 }
128}
129
130impl FromStr for Licence {
131 type Err = ManifestError;
132
133 fn from_str(s: &str) -> Result<Self, Self::Err> {
134 match s {
135 "mit" => Ok(Licence::Mit),
136 "lgpl" => Ok(Licence::Lgpl),
137 "bsd" => Ok(Licence::Bsd),
138 "gpl-2.0-with-linux-syscall-note" => Ok(Licence::LinuxUapi),
139 "mingw-permissive" => Ok(Licence::MingwPermissive),
140 "apache-2.0" => Ok(Licence::Apache2),
141 "apple-sdk" => Ok(Licence::AppleSdk),
142 "microsoft-sdk" => Ok(Licence::MicrosoftSdk),
143 other => Err(ManifestError::UnknownLicence(other.to_string())),
144 }
145 }
146}
147
148/// How an input got to where it is.
149///
150/// Section 13.5 asks for this field and does not say what the three answers mean, which makes the
151/// meaning a decision rather than a transcription. The question each answer has to settle is what a
152/// person reproducing a file has to have: an upstream release is enough for one of them, our own
153/// generator and its version is needed for the second, and the third did not exist on any machine
154/// until somebody's network fetched it.
155///
156/// The test to apply is whether the bytes can be found in the upstream release. A header we unpack
157/// and ship unchanged can be, so it is [`Provenance::Bundled`]. A stub shared object, a merged header
158/// tree or a linker script cannot be, because this compiler wrote it, so it is
159/// [`Provenance::Generated`] even though what it was derived from is upstream's. Anything that arrived
160/// over the network after the release was built is [`Provenance::Fetched`], whoever wrote it.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
162pub enum Provenance {
163 /// Shipped inside the distribution, byte for byte as upstream released it. Reproducing it needs
164 /// the release named in [`Input::source`] and nothing of ours.
165 Bundled,
166 /// Written by this compiler from something upstream describes, which is the stubs of
167 /// `spec/cross-compile/09-libc-stubs.md` and the merged header trees of section 8.3.
168 /// Reproducing it needs our generator at the version that wrote it as well as the release.
169 Generated,
170 /// Downloaded onto this machine, which for the two licence walls of section 13.4 is the only way
171 /// the input can legally arrive at all. The record says so because a sysroot holding one is not
172 /// the same artifact as a sysroot that shipped complete.
173 Fetched,
174}
175
176impl Provenance {
177 /// The spelling in a manifest file.
178 #[must_use]
179 pub const fn as_str(self) -> &'static str {
180 match self {
181 Provenance::Bundled => "bundled",
182 Provenance::Generated => "generated",
183 Provenance::Fetched => "fetched",
184 }
185 }
186}
187
188impl fmt::Display for Provenance {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 f.write_str(self.as_str())
191 }
192}
193
194impl FromStr for Provenance {
195 type Err = ManifestError;
196
197 fn from_str(s: &str) -> Result<Self, Self::Err> {
198 match s {
199 "bundled" => Ok(Provenance::Bundled),
200 "generated" => Ok(Provenance::Generated),
201 "fetched" => Ok(Provenance::Fetched),
202 other => Err(ManifestError::UnknownProvenance(other.to_string())),
203 }
204 }
205}
206
207/// One file in a sysroot, and everything that has to be true of it.
208#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
209pub struct Input {
210 /// Where it sits, relative to the sysroot root. Relative because an absolute path is a fact
211 /// about the machine that built it, and two hosts have different ones.
212 pub path: String,
213 /// What it came out of, named so that the same manifest can be produced again. A release name
214 /// and version rather than a URL, because a URL moves and a release does not.
215 pub source: String,
216 /// Where that release was fetched from, which is the field section 13.5 asks for by name.
217 ///
218 /// The URL of the release rather than of the file, because what anybody checking this does is
219 /// download the release and look inside it, and because a per file URL would be a claim about
220 /// somebody else's directory layout. It is here in spite of a URL moving and a release not,
221 /// which is the reason [`Input::source`] exists and is not replaced by this: the two fields
222 /// answer what it is and where it was got, and only the first of those is still true in ten
223 /// years.
224 pub url: String,
225 /// The hash of the file, lowercase hex.
226 pub sha256: String,
227 /// What it may be done with.
228 pub licence: Licence,
229 /// Whether it was bundled, generated or fetched.
230 pub provenance: Provenance,
231}
232
233/// The record of one produced sysroot.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct Manifest {
236 target: TargetTuple,
237 kernel: Option<Version>,
238 inputs: Vec<Input>,
239}
240
241/// What went wrong reading a manifest.
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum ManifestError {
244 /// The first line was not the one this format starts with.
245 NotAManifest,
246 /// The version in the header is one this build does not read.
247 UnknownVersion(String),
248 /// The second line did not name a target, or named one that does not parse.
249 BadTarget(String),
250 /// The `kernel` line was there and what followed it is not a Linux release.
251 BadKernel(String),
252 /// A line did not have the six fields an input has.
253 BadInput {
254 /// Which line, counting from one.
255 line: usize,
256 /// How many tab separated fields it had.
257 fields: usize,
258 },
259 /// A line of the kernel tree's record did not have the four fields a file has there.
260 BadKernelFile {
261 /// Which line, counting from one.
262 line: usize,
263 /// How many tab separated fields it had.
264 fields: usize,
265 },
266 /// A hash that is not sixty four lowercase hex characters.
267 BadHash {
268 /// Which line, counting from one.
269 line: usize,
270 /// What was there instead.
271 found: String,
272 },
273 /// A licence spelling nothing here knows.
274 UnknownLicence(String),
275 /// A provenance spelling nothing here knows.
276 UnknownProvenance(String),
277 /// An empty field where a source, a URL or a path belongs. A record with a hole in it is worse
278 /// than no record, because it reads as an answer.
279 EmptyField {
280 /// Which line, counting from one.
281 line: usize,
282 /// Which field was empty, spelled the way this module names it.
283 field: &'static str,
284 },
285}
286
287impl fmt::Display for ManifestError {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 match self {
290 ManifestError::NotAManifest => {
291 write!(f, "this does not start like a sysroot manifest or a kernel tree's record")
292 }
293 ManifestError::UnknownVersion(v) => {
294 write!(f, "manifest format version {v}, which this build does not read")
295 }
296 ManifestError::BadTarget(t) => write!(f, "`{t}` is not a target this understands"),
297 ManifestError::BadKernel(k) => {
298 write!(f, "`{k}` is not a Linux release, which is what a kernel line carries")
299 }
300 ManifestError::BadInput { line, fields } => {
301 write!(f, "line {line} has {fields} fields where an input has six")
302 }
303 ManifestError::BadKernelFile { line, fields } => {
304 write!(f, "line {line} has {fields} fields where a kernel header has four")
305 }
306 ManifestError::BadHash { line, found } => {
307 write!(f, "line {line} has `{found}` where a sha256 belongs")
308 }
309 ManifestError::UnknownLicence(l) => write!(f, "`{l}` is not a licence this knows"),
310 ManifestError::UnknownProvenance(o) => {
311 write!(f, "`{o}` is not bundled, generated or fetched")
312 }
313 ManifestError::EmptyField { line, field } => {
314 write!(f, "line {line} has nothing where its {field} belongs")
315 }
316 }
317 }
318}
319
320impl std::error::Error for ManifestError {}
321
322/// The first line of every manifest, which is also how one is recognized.
323///
324/// Version 2 is version 1 with the source URL and the provenance on every line, which is what
325/// section 13.5 asks a record of an input to carry. The number went up rather than the two fields
326/// being optional, because a reader that accepted both would have to decide what a missing
327/// provenance means and there is no honest answer to that: an input nobody wrote a provenance for is
328/// an input whose provenance nobody knows. Nothing has written a version 1 file to a place that
329/// outlives a build, so the only cost of the bump is this sentence.
330///
331/// Version 3 adds the optional `kernel` line. The number went up even though the line is optional,
332/// because the whole point of a format version is that a reader can say it does not read a file, and
333/// a version 2 reader handed a file with a `kernel` line in it would report the line as an input
334/// with two fields rather than as a format it does not know.
335const HEADER: &str = "rucc sysroot manifest 3";
336
337impl Manifest {
338 /// An empty manifest for this target.
339 #[must_use]
340 pub const fn new(target: TargetTuple) -> Self {
341 Manifest { target, kernel: None, inputs: Vec::new() }
342 }
343
344 /// The target this sysroot is for.
345 #[must_use]
346 pub const fn target(&self) -> TargetTuple {
347 self.target
348 }
349
350 /// The Linux release the kernel headers in this sysroot came out of, when one was recorded.
351 ///
352 /// Absent has one meaning and it is not "nobody knows": it is that this sysroot has no kernel
353 /// headers in it. Every target that is not Linux is in that case, and so is a Linux sysroot
354 /// produced before a kernel tree was installed beside it, which is a state the producer allows
355 /// because the two trees are two commands. That is the one thing an optional line can mean here
356 /// and it is why this one is optional where the provenance field is not: a file with no kernel
357 /// headers in it has no kernel version, and an input always came from somewhere.
358 ///
359 /// What it is for is the question somebody asks after a cross build read a header nobody
360 /// expected. `-print-sysroot` answers where and the manifest answers what, and a sysroot whose
361 /// record names the release its `linux/` headers came out of makes a stale pairing visible
362 /// instead of leaving it to be guessed at. Nothing here checks the version against the headers
363 /// themselves, which is tamnd/rucc#925's argument applied to the kernel tree rather than to
364 /// glibc.
365 #[must_use]
366 pub const fn kernel(&self) -> Option<Version> {
367 self.kernel
368 }
369
370 /// Record which Linux release the kernel headers came out of.
371 ///
372 /// Infallible, and in particular it does not refuse a target that has no kernel headers. The
373 /// property this type owes its callers is that [`Manifest::parse`] reads back what
374 /// [`Manifest::render`] wrote, so the reader accepts every manifest a producer can build and a
375 /// `kernel` line on a Windows sysroot is a bug in the producer rather than a corrupt file.
376 pub const fn set_kernel(&mut self, version: Version) {
377 self.kernel = Some(version);
378 }
379
380 /// Every input, in the order they were added.
381 #[must_use]
382 pub fn inputs(&self) -> &[Input] {
383 &self.inputs
384 }
385
386 /// Record one input.
387 pub fn push(&mut self, input: Input) {
388 self.inputs.push(input);
389 }
390
391 /// Whether an artifact containing this whole sysroot can be published.
392 ///
393 /// One input under a licence that says no makes the answer no, which is the only reading of a
394 /// licence wall that is worth anything.
395 #[must_use]
396 pub fn redistributable(&self) -> bool {
397 self.inputs.iter().all(|input| input.licence.redistributable())
398 }
399
400 /// Every distinct source in the manifest, sorted.
401 ///
402 /// What a person asks first when two manifests differ, and what a licence notice is generated
403 /// from.
404 #[must_use]
405 pub fn sources(&self) -> Vec<&str> {
406 let mut sources: Vec<&str> =
407 self.inputs.iter().map(|input| input.source.as_str()).collect();
408 sources.sort_unstable();
409 sources.dedup();
410 sources
411 }
412
413 /// The manifest as text, sorted by path.
414 ///
415 /// The sort is what makes two runs comparable. A directory walk returns files in whatever order
416 /// the filesystem keeps them, which differs between ext4 and APFS and sometimes between two
417 /// runs on one of them, and a manifest that carried that order would report a difference
418 /// between two identical sysroots.
419 #[must_use]
420 pub fn render(&self) -> String {
421 let mut sorted = self.inputs.clone();
422 sorted.sort();
423
424 let mut text = String::new();
425 text.push_str(HEADER);
426 text.push('\n');
427 text.push_str("target\t");
428 text.push_str(&self.target.to_canonical_string());
429 text.push('\n');
430 if let Some(kernel) = self.kernel {
431 text.push_str("kernel\t");
432 text.push_str(&kernel.to_string());
433 text.push('\n');
434 }
435 for input in &sorted {
436 text.push_str(&input.path);
437 text.push('\t');
438 text.push_str(&input.source);
439 text.push('\t');
440 text.push_str(&input.url);
441 text.push('\t');
442 text.push_str(&input.sha256);
443 text.push('\t');
444 text.push_str(input.licence.as_str());
445 text.push('\t');
446 text.push_str(input.provenance.as_str());
447 text.push('\n');
448 }
449 text
450 }
451
452 /// One number naming everything this sysroot is made of: the sha256 of [`Manifest::render`],
453 /// as sixty four lowercase hex characters.
454 ///
455 /// The same number `sha256sum` prints for the manifest file itself, which is the property worth
456 /// having. Whoever is handed a digest can check it with a tool they already have, and a digest
457 /// that only our own code could compute would be a claim nobody can audit.
458 ///
459 /// # What it is for
460 ///
461 /// `spec/cross-compile/13-distribution.md` section 13.2 asks for the hash of a cache directory's
462 /// contents in the directory's name, and a name cannot carry one: the path has to be computable
463 /// before anything has been read, by the producer that is about to write the files and by the
464 /// compiler that is about to read them, and neither of them has the contents when it asks. What
465 /// the rule wanted is a way to say in one line what is under a directory, and this is that line.
466 /// Two hosts producing a sysroot for one target compare a digest instead of a few thousand
467 /// files, and a digest published with a release can be held against a directory on a machine.
468 ///
469 /// # What it covers
470 ///
471 /// What the manifest covers, which is every file in the sysroot and the Linux release its kernel
472 /// headers came out of. Not the kernel tree's own files, because they are not in the sysroot:
473 /// one tree serves every Linux target, so it sits in the cache beside the sysroots and
474 /// [`crate::Manifest::kernel`] is what a sysroot says about it.
475 ///
476 /// It is a fact about the target and its inputs rather than about the host, because the render
477 /// is sorted, holds no absolute path and holds no timestamp. That is the same argument
478 /// `spec/cross-compile/02-the-goal.md` claim 5 rests on, applied to the record rather than to
479 /// the output.
480 #[must_use]
481 pub fn digest(&self) -> String {
482 crate::sha256::hex(self.render().as_bytes())
483 }
484
485 /// Read a manifest back.
486 ///
487 /// # Errors
488 ///
489 /// Returns which line was wrong and what was wrong with it. A manifest that fails to parse is
490 /// a cache entry somebody has to decide about, and "invalid manifest" is not enough to decide
491 /// with.
492 pub fn parse(text: &str) -> Result<Self, ManifestError> {
493 let mut lines = text.lines().enumerate().peekable();
494
495 let (_, first) = lines.next().ok_or(ManifestError::NotAManifest)?;
496 if first != HEADER {
497 let Some(version) = first.strip_prefix("rucc sysroot manifest ") else {
498 return Err(ManifestError::NotAManifest);
499 };
500 return Err(ManifestError::UnknownVersion(version.to_string()));
501 }
502
503 let (_, second) = lines.next().ok_or(ManifestError::NotAManifest)?;
504 let spelling = second
505 .strip_prefix("target\t")
506 .ok_or_else(|| ManifestError::BadTarget(second.into()))?;
507 let target = TargetTuple::from_str(spelling)
508 .map_err(|_| ManifestError::BadTarget(spelling.to_string()))?;
509
510 let mut manifest = Manifest::new(target);
511
512 // The kernel line is read only here, immediately after the target, rather than wherever it
513 // turns up. The render order is what makes two manifests comparable with `diff`, and a
514 // reader that took the line anywhere would accept files that do not compare.
515 if let Some(spelling) = lines.peek().and_then(|(_, line)| line.strip_prefix("kernel\t")) {
516 let version = Version::parse(spelling)
517 .ok_or_else(|| ManifestError::BadKernel(spelling.into()))?;
518 manifest.set_kernel(version);
519 lines.next();
520 }
521
522 for (index, line) in lines {
523 if line.is_empty() {
524 continue;
525 }
526 let number = index + 1;
527 let fields: Vec<&str> = line.split('\t').collect();
528 let [path, source, url, sha256, licence, provenance] = fields.as_slice() else {
529 return Err(ManifestError::BadInput { line: number, fields: fields.len() });
530 };
531 if !is_sha256(sha256) {
532 return Err(ManifestError::BadHash { line: number, found: (*sha256).to_string() });
533 }
534 for (value, field) in [(path, "path"), (source, "source"), (url, "url")] {
535 if value.is_empty() {
536 return Err(ManifestError::EmptyField { line: number, field });
537 }
538 }
539 manifest.push(Input {
540 path: (*path).to_string(),
541 source: (*source).to_string(),
542 url: (*url).to_string(),
543 sha256: (*sha256).to_string(),
544 licence: licence.parse()?,
545 provenance: provenance.parse()?,
546 });
547 }
548 Ok(manifest)
549 }
550}
551
552/// Whether this is sixty four lowercase hex characters.
553///
554/// Checked on the way in rather than assumed, because a manifest with a truncated hash in it is a
555/// manifest that verifies nothing while looking like it does.
556fn is_sha256(s: &str) -> bool {
557 s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
558}
559
560/// The first line of the kernel header tree's own record.
561const KERNEL_HEADER: &str = "rucc kernel headers manifest 1";
562
563/// One file in the kernel header tree.
564///
565/// Four fields where a sysroot's input has six, because every file in the tree came out of one
566/// source and got there one way. The URL is the kernel release's and the provenance is bundled for
567/// all of them, so a column of each would be the same word a thousand times.
568#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
569pub struct KernelFile {
570 /// Where it sits, relative to the tree's root, which puts the architecture or `generic` first.
571 pub path: String,
572 /// The Linux release it came out of, as `linux-6.19`.
573 pub source: String,
574 /// The hash of the file, lowercase hex.
575 pub sha256: String,
576 /// What it may be done with, which for every file here is [`Licence::LinuxUapi`].
577 pub licence: Licence,
578}
579
580/// The record the kernel header tree carries, which `bin/kernel-headers` in `tamnd/rucc-cross`
581/// writes.
582///
583/// A separate type rather than a [`Manifest`] with no target, because the tree is not any target's.
584/// One copy serves every Linux row in the table, which is [`crate::Kernel`]'s whole argument, and a
585/// record that had to name a target would name the wrong one for every target but one. What it
586/// names instead is the Linux release, which is what a sysroot's own `kernel` line is about, so the
587/// two can be read side by side.
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct KernelManifest {
590 linux: Version,
591 files: Vec<KernelFile>,
592}
593
594impl KernelManifest {
595 /// An empty record for the tree out of this Linux release.
596 #[must_use]
597 pub const fn new(linux: Version) -> Self {
598 KernelManifest { linux, files: Vec::new() }
599 }
600
601 /// The Linux release the tree came out of.
602 #[must_use]
603 pub const fn linux(&self) -> Version {
604 self.linux
605 }
606
607 /// Every file, in the order they were added.
608 #[must_use]
609 pub fn files(&self) -> &[KernelFile] {
610 &self.files
611 }
612
613 /// Record one file.
614 pub fn push(&mut self, file: KernelFile) {
615 self.files.push(file);
616 }
617
618 /// The record as text, sorted by path, which is the same text the producer writes.
619 #[must_use]
620 pub fn render(&self) -> String {
621 let mut sorted = self.files.clone();
622 sorted.sort();
623 let mut text = String::new();
624 text.push_str(KERNEL_HEADER);
625 text.push_str("\nlinux\t");
626 text.push_str(&self.linux.to_string());
627 text.push('\n');
628 for file in &sorted {
629 text.push_str(&file.path);
630 text.push('\t');
631 text.push_str(&file.source);
632 text.push('\t');
633 text.push_str(&file.sha256);
634 text.push('\t');
635 text.push_str(file.licence.as_str());
636 text.push('\n');
637 }
638 text
639 }
640
641 /// The sha256 of [`KernelManifest::render`], which is what `sha256sum` says about the file, for
642 /// the reason [`Manifest::digest`] gives.
643 #[must_use]
644 pub fn digest(&self) -> String {
645 crate::sha256::hex(self.render().as_bytes())
646 }
647
648 /// Read a record back.
649 ///
650 /// # Errors
651 ///
652 /// Which line was wrong and how, with the same errors a sysroot manifest has. The header being
653 /// some other file's is [`ManifestError::NotAManifest`], and a file line without its four fields
654 /// is [`ManifestError::BadKernelFile`].
655 pub fn parse(text: &str) -> Result<Self, ManifestError> {
656 let mut lines = text.lines().enumerate();
657
658 let (_, first) = lines.next().ok_or(ManifestError::NotAManifest)?;
659 if first != KERNEL_HEADER {
660 let Some(version) = first.strip_prefix("rucc kernel headers manifest ") else {
661 return Err(ManifestError::NotAManifest);
662 };
663 return Err(ManifestError::UnknownVersion(version.to_string()));
664 }
665
666 let (_, second) = lines.next().ok_or(ManifestError::NotAManifest)?;
667 let spelling = second
668 .strip_prefix("linux\t")
669 .ok_or_else(|| ManifestError::BadKernel(second.into()))?;
670 let linux =
671 Version::parse(spelling).ok_or_else(|| ManifestError::BadKernel(spelling.into()))?;
672 let mut manifest = KernelManifest::new(linux);
673
674 for (index, line) in lines {
675 if line.is_empty() {
676 continue;
677 }
678 let number = index + 1;
679 let fields: Vec<&str> = line.split('\t').collect();
680 let [path, source, sha256, licence] = fields.as_slice() else {
681 return Err(ManifestError::BadKernelFile { line: number, fields: fields.len() });
682 };
683 if !is_sha256(sha256) {
684 return Err(ManifestError::BadHash { line: number, found: (*sha256).to_string() });
685 }
686 for (value, field) in [(path, "path"), (source, "source")] {
687 if value.is_empty() {
688 return Err(ManifestError::EmptyField { line: number, field });
689 }
690 }
691 manifest.push(KernelFile {
692 path: (*path).to_string(),
693 source: (*source).to_string(),
694 sha256: (*sha256).to_string(),
695 licence: licence.parse()?,
696 });
697 }
698 Ok(manifest)
699 }
700}