Skip to main content

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}