use std::fmt;
use std::str::FromStr;
use rucc_tuple::{TargetTuple, Version};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Licence {
Mit,
Lgpl,
Bsd,
LinuxUapi,
MingwPermissive,
Apache2,
AppleSdk,
MicrosoftSdk,
}
impl Licence {
#[must_use]
pub const fn redistributable(self) -> bool {
!matches!(self, Licence::AppleSdk | Licence::MicrosoftSdk)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Licence::Mit => "mit",
Licence::Lgpl => "lgpl",
Licence::Bsd => "bsd",
Licence::LinuxUapi => "gpl-2.0-with-linux-syscall-note",
Licence::MingwPermissive => "mingw-permissive",
Licence::Apache2 => "apache-2.0",
Licence::AppleSdk => "apple-sdk",
Licence::MicrosoftSdk => "microsoft-sdk",
}
}
}
impl fmt::Display for Licence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Licence {
type Err = ManifestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mit" => Ok(Licence::Mit),
"lgpl" => Ok(Licence::Lgpl),
"bsd" => Ok(Licence::Bsd),
"gpl-2.0-with-linux-syscall-note" => Ok(Licence::LinuxUapi),
"mingw-permissive" => Ok(Licence::MingwPermissive),
"apache-2.0" => Ok(Licence::Apache2),
"apple-sdk" => Ok(Licence::AppleSdk),
"microsoft-sdk" => Ok(Licence::MicrosoftSdk),
other => Err(ManifestError::UnknownLicence(other.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Provenance {
Bundled,
Generated,
Fetched,
}
impl Provenance {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Provenance::Bundled => "bundled",
Provenance::Generated => "generated",
Provenance::Fetched => "fetched",
}
}
}
impl fmt::Display for Provenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Provenance {
type Err = ManifestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"bundled" => Ok(Provenance::Bundled),
"generated" => Ok(Provenance::Generated),
"fetched" => Ok(Provenance::Fetched),
other => Err(ManifestError::UnknownProvenance(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Input {
pub path: String,
pub source: String,
pub url: String,
pub sha256: String,
pub licence: Licence,
pub provenance: Provenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Manifest {
target: TargetTuple,
kernel: Option<Version>,
inputs: Vec<Input>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManifestError {
NotAManifest,
UnknownVersion(String),
BadTarget(String),
BadKernel(String),
BadInput {
line: usize,
fields: usize,
},
BadHash {
line: usize,
found: String,
},
UnknownLicence(String),
UnknownProvenance(String),
EmptyField {
line: usize,
field: &'static str,
},
}
impl fmt::Display for ManifestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ManifestError::NotAManifest => write!(f, "this does not start like a sysroot manifest"),
ManifestError::UnknownVersion(v) => {
write!(f, "manifest format version {v}, which this build does not read")
}
ManifestError::BadTarget(t) => write!(f, "`{t}` is not a target this understands"),
ManifestError::BadKernel(k) => {
write!(f, "`{k}` is not a Linux release, which is what a kernel line carries")
}
ManifestError::BadInput { line, fields } => {
write!(f, "line {line} has {fields} fields where an input has six")
}
ManifestError::BadHash { line, found } => {
write!(f, "line {line} has `{found}` where a sha256 belongs")
}
ManifestError::UnknownLicence(l) => write!(f, "`{l}` is not a licence this knows"),
ManifestError::UnknownProvenance(o) => {
write!(f, "`{o}` is not bundled, generated or fetched")
}
ManifestError::EmptyField { line, field } => {
write!(f, "line {line} has nothing where its {field} belongs")
}
}
}
}
impl std::error::Error for ManifestError {}
const HEADER: &str = "rucc sysroot manifest 3";
impl Manifest {
#[must_use]
pub const fn new(target: TargetTuple) -> Self {
Manifest { target, kernel: None, inputs: Vec::new() }
}
#[must_use]
pub const fn target(&self) -> TargetTuple {
self.target
}
#[must_use]
pub const fn kernel(&self) -> Option<Version> {
self.kernel
}
pub const fn set_kernel(&mut self, version: Version) {
self.kernel = Some(version);
}
#[must_use]
pub fn inputs(&self) -> &[Input] {
&self.inputs
}
pub fn push(&mut self, input: Input) {
self.inputs.push(input);
}
#[must_use]
pub fn redistributable(&self) -> bool {
self.inputs.iter().all(|input| input.licence.redistributable())
}
#[must_use]
pub fn sources(&self) -> Vec<&str> {
let mut sources: Vec<&str> =
self.inputs.iter().map(|input| input.source.as_str()).collect();
sources.sort_unstable();
sources.dedup();
sources
}
#[must_use]
pub fn render(&self) -> String {
let mut sorted = self.inputs.clone();
sorted.sort();
let mut text = String::new();
text.push_str(HEADER);
text.push('\n');
text.push_str("target\t");
text.push_str(&self.target.to_canonical_string());
text.push('\n');
if let Some(kernel) = self.kernel {
text.push_str("kernel\t");
text.push_str(&kernel.to_string());
text.push('\n');
}
for input in &sorted {
text.push_str(&input.path);
text.push('\t');
text.push_str(&input.source);
text.push('\t');
text.push_str(&input.url);
text.push('\t');
text.push_str(&input.sha256);
text.push('\t');
text.push_str(input.licence.as_str());
text.push('\t');
text.push_str(input.provenance.as_str());
text.push('\n');
}
text
}
#[must_use]
pub fn digest(&self) -> String {
crate::sha256::hex(self.render().as_bytes())
}
pub fn parse(text: &str) -> Result<Self, ManifestError> {
let mut lines = text.lines().enumerate().peekable();
let (_, first) = lines.next().ok_or(ManifestError::NotAManifest)?;
if first != HEADER {
let Some(version) = first.strip_prefix("rucc sysroot manifest ") else {
return Err(ManifestError::NotAManifest);
};
return Err(ManifestError::UnknownVersion(version.to_string()));
}
let (_, second) = lines.next().ok_or(ManifestError::NotAManifest)?;
let spelling = second
.strip_prefix("target\t")
.ok_or_else(|| ManifestError::BadTarget(second.into()))?;
let target = TargetTuple::from_str(spelling)
.map_err(|_| ManifestError::BadTarget(spelling.to_string()))?;
let mut manifest = Manifest::new(target);
if let Some(spelling) = lines.peek().and_then(|(_, line)| line.strip_prefix("kernel\t")) {
let version = Version::parse(spelling)
.ok_or_else(|| ManifestError::BadKernel(spelling.into()))?;
manifest.set_kernel(version);
lines.next();
}
for (index, line) in lines {
if line.is_empty() {
continue;
}
let number = index + 1;
let fields: Vec<&str> = line.split('\t').collect();
let [path, source, url, sha256, licence, provenance] = fields.as_slice() else {
return Err(ManifestError::BadInput { line: number, fields: fields.len() });
};
if !is_sha256(sha256) {
return Err(ManifestError::BadHash { line: number, found: (*sha256).to_string() });
}
for (value, field) in [(path, "path"), (source, "source"), (url, "url")] {
if value.is_empty() {
return Err(ManifestError::EmptyField { line: number, field });
}
}
manifest.push(Input {
path: (*path).to_string(),
source: (*source).to_string(),
url: (*url).to_string(),
sha256: (*sha256).to_string(),
licence: licence.parse()?,
provenance: provenance.parse()?,
});
}
Ok(manifest)
}
}
fn is_sha256(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}