debian: type the errors of the format primitives
Give every parser of the shared format layer a proper error enum (thiserror): VersionParseError, ArchError, deps::ParseError (wrapping the version and arch failures as #[source] so the context chain survives), ControlParseError, ChecksumParseError, FilesParseError and TestsControlParseError, each carrying its structured pieces (the offending string, the stanza number, the field name) instead of a pre-formatted String. Display texts are byte-identical to the old format! strings, so user-facing output and the message-asserting tests are unchanged; the few tests asserting on error text now render the error first. Flow boundaries that still take String (build::env, quirks, test::expand) render with to_string() at the edge — typing those flows is a later step. ControlInfo's FromStr impl follows parse_content's error type.
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@ pub fn build_env(
|
||||
/// (`*_ARCH`, `*_OS`, `*_CPU`, `*_MULTIARCH`, `*_GNU_TYPE`, ...), exactly as
|
||||
/// `dpkg-buildpackage` does.
|
||||
pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, String> {
|
||||
crate::debian::arch::arch_env(host_arch)
|
||||
crate::debian::arch::arch_env(host_arch).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Read the current vendor name from the active dpkg origins `default` file
|
||||
|
||||
+56
-18
@@ -682,14 +682,45 @@ pub fn is_invalid(arch: &str, positive: bool) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure of the architecture helpers: an invalid list entry, an
|
||||
/// undetectable native architecture, or an architecture unknown to dpkg's
|
||||
/// tables.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ArchError {
|
||||
/// An entry of the list is not a valid architecture name.
|
||||
#[error("'{arch}' is not a valid architecture in list '{list}'")]
|
||||
InvalidInList {
|
||||
/// The offending architecture name.
|
||||
arch: String,
|
||||
/// The whole list.
|
||||
list: String,
|
||||
},
|
||||
/// Neither dpkg nor uname could name the native architecture.
|
||||
#[error("cannot determine native Debian architecture")]
|
||||
NativeUndetermined,
|
||||
/// The architecture is not in dpkg's tables.
|
||||
#[error("unknown Debian architecture '{arch}'")]
|
||||
Unknown {
|
||||
/// The unknown architecture name.
|
||||
arch: String,
|
||||
},
|
||||
/// A GNU triplet without the expected `cpu-system` shape.
|
||||
#[error("invalid GNU triplet '{triplet}'")]
|
||||
InvalidTriplet {
|
||||
/// The malformed triplet.
|
||||
triplet: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Parse a whitespace-separated architecture list, validating every entry.
|
||||
pub fn list_parse(list: &str) -> Result<Vec<String>, String> {
|
||||
pub fn list_parse(list: &str) -> Result<Vec<String>, ArchError> {
|
||||
let arches: Vec<String> = list.split_whitespace().map(str::to_string).collect();
|
||||
for arch in &arches {
|
||||
if is_invalid(arch, false) {
|
||||
return Err(format!(
|
||||
"'{arch}' is not a valid architecture in list '{list}'"
|
||||
));
|
||||
return Err(ArchError::InvalidInList {
|
||||
arch: arch.clone(),
|
||||
list: list.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(arches)
|
||||
@@ -802,7 +833,7 @@ fn from_uname() -> Option<String> {
|
||||
/// the `DEB_BUILD_*` variables): the authoritative answer comes from the
|
||||
/// dpkg database itself; if the `dpkg` frontend cannot be executed, the
|
||||
/// architecture is derived from `uname` through the same tables.
|
||||
pub fn native() -> Result<String, String> {
|
||||
pub fn native() -> Result<String, ArchError> {
|
||||
if let Ok(output) = Command::new("dpkg").arg("--print-architecture").output()
|
||||
&& output.status.success()
|
||||
{
|
||||
@@ -811,7 +842,7 @@ pub fn native() -> Result<String, String> {
|
||||
return Ok(arch);
|
||||
}
|
||||
}
|
||||
from_uname().ok_or_else(|| "cannot determine native Debian architecture".to_string())
|
||||
from_uname().ok_or(ArchError::NativeUndetermined)
|
||||
}
|
||||
|
||||
/// Compute the complete architecture environment, the equivalent of
|
||||
@@ -821,7 +852,7 @@ pub fn native() -> Result<String, String> {
|
||||
/// The target architecture defaults to the host architecture, and the host
|
||||
/// architecture defaults to the native build architecture, exactly like
|
||||
/// dpkg-architecture.
|
||||
pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, String> {
|
||||
pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, ArchError> {
|
||||
let build_arch = native()?;
|
||||
let host_arch = host_arch.unwrap_or(&build_arch).to_string();
|
||||
let target_arch = host_arch.clone();
|
||||
@@ -832,8 +863,9 @@ pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, Str
|
||||
("HOST", host_arch),
|
||||
("TARGET", target_arch),
|
||||
] {
|
||||
let tuple = debarch_to_debtuple(&arch)
|
||||
.ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
||||
let tuple = debarch_to_debtuple(&arch).ok_or_else(|| ArchError::Unknown {
|
||||
arch: arch.to_string(),
|
||||
})?;
|
||||
|
||||
env.insert(format!("DEB_{role}_ARCH"), arch.clone());
|
||||
env.insert(format!("DEB_{role}_ARCH_ABI"), tuple.abi.clone());
|
||||
@@ -841,20 +873,26 @@ pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, Str
|
||||
env.insert(format!("DEB_{role}_ARCH_OS"), tuple.os.clone());
|
||||
env.insert(format!("DEB_{role}_ARCH_CPU"), tuple.cpu.clone());
|
||||
|
||||
let (bits, endian) =
|
||||
abi_attrs(&arch).ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
||||
let (bits, endian) = abi_attrs(&arch).ok_or_else(|| ArchError::Unknown {
|
||||
arch: arch.to_string(),
|
||||
})?;
|
||||
env.insert(format!("DEB_{role}_ARCH_BITS"), bits.to_string());
|
||||
env.insert(format!("DEB_{role}_ARCH_ENDIAN"), endian.to_string());
|
||||
|
||||
let multi =
|
||||
multiarch(&arch).ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
||||
let multi = multiarch(&arch).ok_or_else(|| ArchError::Unknown {
|
||||
arch: arch.to_string(),
|
||||
})?;
|
||||
env.insert(format!("DEB_{role}_MULTIARCH"), multi);
|
||||
|
||||
let gnu_type = debarch_to_gnutriplet(&arch)
|
||||
.ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
||||
let (gnu_cpu, gnu_system) = gnu_type
|
||||
.split_once('-')
|
||||
.ok_or_else(|| format!("invalid GNU triplet '{gnu_type}'"))?;
|
||||
let gnu_type = debarch_to_gnutriplet(&arch).ok_or_else(|| ArchError::Unknown {
|
||||
arch: arch.to_string(),
|
||||
})?;
|
||||
let (gnu_cpu, gnu_system) =
|
||||
gnu_type
|
||||
.split_once('-')
|
||||
.ok_or_else(|| ArchError::InvalidTriplet {
|
||||
triplet: gnu_type.clone(),
|
||||
})?;
|
||||
env.insert(format!("DEB_{role}_GNU_CPU"), gnu_cpu.to_string());
|
||||
env.insert(format!("DEB_{role}_GNU_SYSTEM"), gnu_system.to_string());
|
||||
env.insert(format!("DEB_{role}_GNU_TYPE"), gnu_type);
|
||||
|
||||
+72
-23
@@ -100,6 +100,48 @@ pub struct FileChecksums {
|
||||
index: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
/// Failure of [`FileChecksums::parse_field`]: a malformed checksum field
|
||||
/// line, naming the field, the offending line and what is wrong with it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ChecksumParseError {
|
||||
/// The line has other than `checksum size name` columns.
|
||||
#[error(
|
||||
"malformed '{field}' line (expected 'checksum size name', got {columns} columns): '{line}'"
|
||||
)]
|
||||
MalformedLine {
|
||||
/// The checksum field the line came from (`Checksums-Sha256`, ...).
|
||||
field: String,
|
||||
/// The actual number of whitespace-separated columns.
|
||||
columns: usize,
|
||||
/// The offending line.
|
||||
line: String,
|
||||
},
|
||||
/// The size column is not a number.
|
||||
#[error("malformed '{field}' line (size '{size}' is not a number): '{line}'")]
|
||||
BadSize {
|
||||
/// The checksum field the line came from.
|
||||
field: String,
|
||||
/// The non-numeric size text.
|
||||
size: String,
|
||||
/// The offending line.
|
||||
line: String,
|
||||
},
|
||||
/// The digest is not the expected number of lowercase hex characters.
|
||||
#[error(
|
||||
"malformed '{field}' line (digest '{digest}' is not {len} lowercase hex characters): '{line}'"
|
||||
)]
|
||||
BadDigest {
|
||||
/// The checksum field the line came from.
|
||||
field: String,
|
||||
/// The malformed digest.
|
||||
digest: String,
|
||||
/// The expected digest length.
|
||||
len: usize,
|
||||
/// The offending line.
|
||||
line: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl FileChecksums {
|
||||
/// Create an empty registry.
|
||||
pub fn new() -> Self {
|
||||
@@ -234,7 +276,11 @@ impl FileChecksums {
|
||||
/// `build::parse_checksum_field` helper (which additionally accepts the
|
||||
/// legacy 5-column `Files` layout); the two are intentionally not unified
|
||||
/// across modules.
|
||||
pub fn parse_field(kind: ChecksumKind, value: &str) -> Result<Vec<(String, Entry)>, String> {
|
||||
pub fn parse_field(
|
||||
kind: ChecksumKind,
|
||||
value: &str,
|
||||
) -> Result<Vec<(String, Entry)>, ChecksumParseError> {
|
||||
let field = kind.field_name().to_string();
|
||||
let mut entries = Vec::new();
|
||||
for line in value.lines() {
|
||||
if line.trim().is_empty() {
|
||||
@@ -242,30 +288,28 @@ impl FileChecksums {
|
||||
}
|
||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||
let [digest, size, name] = tokens.as_slice() else {
|
||||
return Err(format!(
|
||||
"malformed '{}' line (expected 'checksum size name', got {} \
|
||||
columns): '{line}'",
|
||||
kind.field_name(),
|
||||
tokens.len()
|
||||
));
|
||||
return Err(ChecksumParseError::MalformedLine {
|
||||
field,
|
||||
columns: tokens.len(),
|
||||
line: line.to_string(),
|
||||
});
|
||||
};
|
||||
let size: u64 = size.parse().map_err(|_| {
|
||||
format!(
|
||||
"malformed '{}' line (size '{size}' is not a number): '{line}'",
|
||||
kind.field_name()
|
||||
)
|
||||
let size: u64 = size.parse().map_err(|_| ChecksumParseError::BadSize {
|
||||
field: field.clone(),
|
||||
size: size.to_string(),
|
||||
line: line.to_string(),
|
||||
})?;
|
||||
let digest_ok = digest.len() == kind.digest_len()
|
||||
&& digest
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
|
||||
if !digest_ok {
|
||||
return Err(format!(
|
||||
"malformed '{}' line (digest '{digest}' is not {} lowercase \
|
||||
hex characters): '{line}'",
|
||||
kind.field_name(),
|
||||
kind.digest_len()
|
||||
));
|
||||
return Err(ChecksumParseError::BadDigest {
|
||||
field,
|
||||
digest: digest.to_string(),
|
||||
len: kind.digest_len(),
|
||||
line: line.to_string(),
|
||||
});
|
||||
}
|
||||
let mut entry = Entry {
|
||||
size,
|
||||
@@ -401,27 +445,32 @@ mod tests {
|
||||
assert_eq!(entries[0].1.size, 12);
|
||||
|
||||
// 2 columns: missing the name.
|
||||
let err =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!("{digest} 12")).unwrap_err();
|
||||
let err = FileChecksums::parse_field(ChecksumKind::Sha512, &format!("{digest} 12"))
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("Checksums-Sha512"), "{err}");
|
||||
assert!(err.contains(&format!("{digest} 12")), "{err}");
|
||||
|
||||
// 4 columns.
|
||||
let err =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!(" {digest} 12 bogus a.txt"))
|
||||
.unwrap_err();
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("Checksums-Sha512"), "{err}");
|
||||
assert!(err.contains("a.txt"), "{err}");
|
||||
|
||||
// Non-numeric size.
|
||||
let err =
|
||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!(" {digest} twelve a.txt"))
|
||||
.unwrap_err();
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("not a number"), "{err}");
|
||||
assert!(err.contains("twelve"), "{err}");
|
||||
|
||||
// Digest that is not 128 lowercase hex characters.
|
||||
let err = FileChecksums::parse_field(ChecksumKind::Sha512, " abc123 12 a.txt").unwrap_err();
|
||||
let err = FileChecksums::parse_field(ChecksumKind::Sha512, " abc123 12 a.txt")
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(err.contains("lowercase hex"), "{err}");
|
||||
}
|
||||
|
||||
|
||||
+20
-7
@@ -403,6 +403,21 @@ pub struct ControlInfo {
|
||||
pub binaries: Vec<Paragraph>,
|
||||
}
|
||||
|
||||
/// Failure of [`ControlInfo::parse_content`]: the content is not shaped
|
||||
/// like a `debian/control` file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ControlParseError {
|
||||
/// The content has no paragraphs at all.
|
||||
#[error("control file has no paragraphs")]
|
||||
NoParagraphs,
|
||||
/// The first (source) stanza carries no `Source` field.
|
||||
#[error("first control paragraph has no 'Source' field")]
|
||||
NoSourceField,
|
||||
/// A binary stanza carries no `Package` field.
|
||||
#[error("binary control paragraph has no 'Package' field")]
|
||||
NoBinaryPackageField,
|
||||
}
|
||||
|
||||
impl ControlInfo {
|
||||
/// Parse a `debian/control` file.
|
||||
pub fn parse(path: &Path) -> Result<ControlInfo, Box<dyn std::error::Error>> {
|
||||
@@ -416,19 +431,17 @@ impl ControlInfo {
|
||||
/// Parse control content from a string.
|
||||
///
|
||||
/// Prefer [`std::str::FromStr`] (`"...".parse::<ControlInfo>()`).
|
||||
pub fn parse_content(content: &str) -> Result<ControlInfo, String> {
|
||||
pub fn parse_content(content: &str) -> Result<ControlInfo, ControlParseError> {
|
||||
let paragraphs = parse_paragraphs(content);
|
||||
let mut iter = paragraphs.into_iter();
|
||||
let source = iter
|
||||
.next()
|
||||
.ok_or_else(|| "control file has no paragraphs".to_string())?;
|
||||
let source = iter.next().ok_or(ControlParseError::NoParagraphs)?;
|
||||
if source.get("Source").is_none() {
|
||||
return Err("first control paragraph has no 'Source' field".to_string());
|
||||
return Err(ControlParseError::NoSourceField);
|
||||
}
|
||||
let binaries: Vec<Paragraph> = iter.collect();
|
||||
for bin in &binaries {
|
||||
if bin.get("Package").is_none() {
|
||||
return Err("binary control paragraph has no 'Package' field".to_string());
|
||||
return Err(ControlParseError::NoBinaryPackageField);
|
||||
}
|
||||
}
|
||||
Ok(ControlInfo { source, binaries })
|
||||
@@ -451,7 +464,7 @@ impl ControlInfo {
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ControlInfo {
|
||||
type Err = String;
|
||||
type Err = ControlParseError;
|
||||
|
||||
fn from_str(content: &str) -> Result<Self, Self::Err> {
|
||||
ControlInfo::parse_content(content)
|
||||
|
||||
+135
-33
@@ -167,22 +167,96 @@ fn profile_name_regex() -> &'static Regex {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse one simple dependency alternative.
|
||||
///
|
||||
/// Fails when the text does not match the grammar, when `:native` appears in
|
||||
/// a non-build-dependency context, or when the architecture/version/profile
|
||||
/// syntax is invalid.
|
||||
pub fn parse_simple(dep: &str, build_dep: bool) -> Result<PkgRelation, String> {
|
||||
/// Failure of the dependency parsers: a syntactically invalid relation or
|
||||
/// restriction formula, an invalid parse option, or an unreadable dpkg
|
||||
/// status file.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParseError {
|
||||
/// The dependency text does not match the package relation grammar.
|
||||
#[error("cannot parse dependency '{dep}'")]
|
||||
Syntax {
|
||||
/// The unparsable dependency text.
|
||||
dep: String,
|
||||
},
|
||||
/// `:native` appeared outside a build-dependency context.
|
||||
#[error("':native' qualifier only allowed in build dependencies: '{dep}'")]
|
||||
NativeOutsideBuildDep {
|
||||
/// The offending dependency text.
|
||||
dep: String,
|
||||
},
|
||||
/// The version relation operator is not one of `<< <= = >= >>`.
|
||||
#[error("invalid relation '{relation}' in '{dep}'")]
|
||||
InvalidRelation {
|
||||
/// The unrecognized operator.
|
||||
relation: String,
|
||||
/// The dependency text carrying it.
|
||||
dep: String,
|
||||
},
|
||||
/// The constrained version does not parse.
|
||||
#[error("invalid version in dependency '{dep}': {source}")]
|
||||
InvalidVersion {
|
||||
/// The dependency text carrying it.
|
||||
dep: String,
|
||||
/// The version parse failure.
|
||||
source: crate::debian::version::VersionParseError,
|
||||
},
|
||||
/// The `[...]` architecture list does not parse.
|
||||
#[error("invalid architecture list in '{dep}': {source}")]
|
||||
InvalidArchList {
|
||||
/// The dependency text carrying it.
|
||||
dep: String,
|
||||
/// The architecture list failure.
|
||||
source: crate::debian::arch::ArchError,
|
||||
},
|
||||
/// A `[...]` restriction formula reduced to nothing.
|
||||
#[error("empty restriction formula in '{dep}'")]
|
||||
EmptyRestrictions {
|
||||
/// The dependency text carrying it.
|
||||
dep: String,
|
||||
},
|
||||
/// A `<...>` formula carries a non-profile term.
|
||||
#[error("'{formula}' is not a valid build profile restriction formula")]
|
||||
InvalidProfileFormula {
|
||||
/// The offending formula.
|
||||
formula: String,
|
||||
},
|
||||
/// [`ParseOpts::host_arch`] is empty or not a valid architecture.
|
||||
#[error("invalid host_arch {host_arch}")]
|
||||
InvalidHostArch {
|
||||
/// The offending host architecture.
|
||||
host_arch: String,
|
||||
},
|
||||
/// `union` mode saw a clause with several alternatives.
|
||||
#[error("an union dependency can only contain simple dependencies")]
|
||||
UnionNotSimple,
|
||||
/// The dpkg status file could not be read.
|
||||
#[error("cannot read status file '{path}': {source}")]
|
||||
ReadStatus {
|
||||
/// The status file path.
|
||||
path: String,
|
||||
/// The read failure.
|
||||
source: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// Parse one simple (no alternatives) package relation: `pkg`
|
||||
/// (`pkg:qualifier`, `pkg (op version)`, `pkg [arch list]`,
|
||||
/// `pkg <restrictions>` combinations), validating the pieces: `:native`
|
||||
/// is rejected in a non-build-dependency context, or when the
|
||||
/// architecture/version/profile syntax is invalid.
|
||||
pub fn parse_simple(dep: &str, build_dep: bool) -> Result<PkgRelation, ParseError> {
|
||||
let caps = dep_regex()
|
||||
.captures(dep)
|
||||
.ok_or_else(|| format!("cannot parse dependency '{dep}'"))?;
|
||||
.ok_or_else(|| ParseError::Syntax {
|
||||
dep: dep.to_string(),
|
||||
})?;
|
||||
|
||||
let package = caps[2].to_string();
|
||||
let arch_qualifier = caps.get(3).map(|m| m.as_str().to_string());
|
||||
if arch_qualifier.as_deref() == Some("native") && !build_dep {
|
||||
return Err(format!(
|
||||
"':native' qualifier only allowed in build dependencies: '{dep}'"
|
||||
));
|
||||
return Err(ParseError::NativeOutsideBuildDep {
|
||||
dep: dep.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let constraint = match (caps.get(5), caps.get(6)) {
|
||||
@@ -197,36 +271,49 @@ pub fn parse_simple(dep: &str, build_dep: bool) -> Result<PkgRelation, String> {
|
||||
"=" => Relation::Eq,
|
||||
">" | ">=" => Relation::Ge,
|
||||
">>" => Relation::Gt,
|
||||
other => return Err(format!("invalid relation '{other}' in '{dep}'")),
|
||||
other => {
|
||||
return Err(ParseError::InvalidRelation {
|
||||
relation: other.to_string(),
|
||||
dep: dep.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let version = DebianVersion::parse(version.as_str())
|
||||
.map_err(|e| format!("invalid version in dependency '{dep}': {e}"))?;
|
||||
let version = DebianVersion::parse(version.as_str()).map_err(|source| {
|
||||
ParseError::InvalidVersion {
|
||||
dep: dep.to_string(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
Some(VersionConstraint { relation, version })
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let arches = match caps.get(8) {
|
||||
Some(list) => Some(
|
||||
arch::list_parse(list.as_str())
|
||||
.map_err(|e| format!("invalid architecture list in '{dep}': {e}"))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let arches =
|
||||
match caps.get(8) {
|
||||
Some(list) => Some(arch::list_parse(list.as_str()).map_err(|source| {
|
||||
ParseError::InvalidArchList {
|
||||
dep: dep.to_string(),
|
||||
source,
|
||||
}
|
||||
})?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut restrictions = Vec::new();
|
||||
if let Some(formula) = caps.get(9) {
|
||||
for group in restriction_group_regex().captures_iter(formula.as_str()) {
|
||||
let terms: Vec<String> = group[1].split_whitespace().map(str::to_string).collect();
|
||||
if terms.is_empty() {
|
||||
return Err(format!("empty restriction formula in '{dep}'"));
|
||||
return Err(ParseError::EmptyRestrictions {
|
||||
dep: dep.to_string(),
|
||||
});
|
||||
}
|
||||
for term in &terms {
|
||||
if !profile_name_regex().is_match(term) {
|
||||
return Err(format!(
|
||||
"'{}' is not a valid build profile restriction formula",
|
||||
formula.as_str()
|
||||
));
|
||||
return Err(ParseError::InvalidProfileFormula {
|
||||
formula: formula.as_str().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
restrictions.push(terms);
|
||||
@@ -291,13 +378,19 @@ impl Deps {
|
||||
/// alternatives whose architecture or profile restrictions do not apply
|
||||
/// are dropped first; clauses losing all their alternatives disappear,
|
||||
/// and a fully reduced field yields an empty [`Deps`].
|
||||
pub fn parse(input: &str, opts: &ParseOpts) -> Result<Deps, String> {
|
||||
pub fn parse(input: &str, opts: &ParseOpts) -> Result<Deps, ParseError> {
|
||||
Self::parse_inner(input, opts, false)
|
||||
}
|
||||
|
||||
fn parse_inner(input: &str, opts: &ParseOpts, reduce_arch_only: bool) -> Result<Deps, String> {
|
||||
fn parse_inner(
|
||||
input: &str,
|
||||
opts: &ParseOpts,
|
||||
reduce_arch_only: bool,
|
||||
) -> Result<Deps, ParseError> {
|
||||
if opts.host_arch.is_empty() || arch::is_invalid(&opts.host_arch, true) {
|
||||
return Err(format!("invalid host_arch {}", opts.host_arch));
|
||||
return Err(ParseError::InvalidHostArch {
|
||||
host_arch: opts.host_arch.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Merge continuation lines and normalize whitespace.
|
||||
@@ -336,7 +429,7 @@ impl Deps {
|
||||
continue;
|
||||
}
|
||||
if opts.union && alternatives.len() > 1 {
|
||||
return Err("an union dependency can only contain simple dependencies".to_string());
|
||||
return Err(ParseError::UnionNotSimple);
|
||||
}
|
||||
clauses.push(alternatives);
|
||||
}
|
||||
@@ -652,9 +745,15 @@ impl Facts {
|
||||
|
||||
/// Load a dpkg status file (e.g. `/var/lib/dpkg/status`), binding the
|
||||
/// facts to the given host/build architectures.
|
||||
pub fn load_status(path: &Path, host_arch: &str, build_arch: &str) -> Result<Facts, String> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("cannot read status file '{}': {}", path.display(), e))?;
|
||||
pub fn load_status(
|
||||
path: &Path,
|
||||
host_arch: &str,
|
||||
build_arch: &str,
|
||||
) -> Result<Facts, ParseError> {
|
||||
let content = std::fs::read_to_string(path).map_err(|source| ParseError::ReadStatus {
|
||||
path: path.display().to_string(),
|
||||
source,
|
||||
})?;
|
||||
Ok(Facts::from_status(&content, host_arch, build_arch))
|
||||
}
|
||||
|
||||
@@ -936,7 +1035,10 @@ impl UnmetReport {
|
||||
///
|
||||
/// On success the report is empty; otherwise its [`UnmetReport::message`]
|
||||
/// carries the dpkg-compatible diagnostics.
|
||||
pub fn check_build_depends(control: &ControlInfo, opts: &CheckOpts) -> Result<UnmetReport, String> {
|
||||
pub fn check_build_depends(
|
||||
control: &ControlInfo,
|
||||
opts: &CheckOpts,
|
||||
) -> Result<UnmetReport, ParseError> {
|
||||
let source = &control.source;
|
||||
|
||||
let mut bd_parts: Vec<&str> = Vec::new();
|
||||
|
||||
+25
-4
@@ -121,6 +121,23 @@ pub struct FilesList {
|
||||
files: BTreeMap<String, FilesEntry>,
|
||||
}
|
||||
|
||||
/// Failure of [`FilesList::parse`]: a malformed `debian/files` line.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum FilesParseError {
|
||||
/// The line has fewer than `filename section priority` columns.
|
||||
#[error("badly formed line: {line}")]
|
||||
BadLine {
|
||||
/// The offending line.
|
||||
line: String,
|
||||
},
|
||||
/// The file name does not match any known artifact shape.
|
||||
#[error("badly formed file name: {name}")]
|
||||
BadFileName {
|
||||
/// The offending file name.
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl FilesList {
|
||||
/// An empty registry.
|
||||
pub fn new() -> Self {
|
||||
@@ -141,7 +158,7 @@ impl FilesList {
|
||||
|
||||
/// Parse a `debian/files` registry from its textual content
|
||||
/// (`filename section priority [key=value...]` lines).
|
||||
pub fn parse(content: &str) -> Result<FilesList, String> {
|
||||
pub fn parse(content: &str) -> Result<FilesList, FilesParseError> {
|
||||
let mut list = FilesList::new();
|
||||
for line in content.lines() {
|
||||
if line.trim().is_empty() {
|
||||
@@ -149,10 +166,14 @@ impl FilesList {
|
||||
}
|
||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||
if tokens.len() < 3 {
|
||||
return Err(format!("badly formed line: {line}"));
|
||||
return Err(FilesParseError::BadLine {
|
||||
line: line.to_string(),
|
||||
});
|
||||
}
|
||||
let mut entry = parse_filename(tokens[0])
|
||||
.ok_or_else(|| format!("badly formed file name: {}", tokens[0]))?;
|
||||
let mut entry =
|
||||
parse_filename(tokens[0]).ok_or_else(|| FilesParseError::BadFileName {
|
||||
name: tokens[0].to_string(),
|
||||
})?;
|
||||
entry.section = tokens[1].to_string();
|
||||
entry.priority = tokens[2].to_string();
|
||||
for attr in &tokens[3..] {
|
||||
|
||||
+31
-11
@@ -54,6 +54,30 @@ pub struct TestsControl {
|
||||
pub tests: Vec<Test>,
|
||||
}
|
||||
|
||||
/// Failure of [`TestsControl::parse_content`]: a malformed stanza, naming
|
||||
/// it by its 1-based position.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum TestsControlParseError {
|
||||
/// The stanza carries both `Tests:` and `Test-Command:`.
|
||||
#[error("stanza {stanza}: Tests and Test-Command are mutually exclusive")]
|
||||
MutuallyExclusive {
|
||||
/// The 1-based stanza position.
|
||||
stanza: usize,
|
||||
},
|
||||
/// The stanza carries neither `Tests:` nor `Test-Command:`.
|
||||
#[error("stanza {stanza}: missing a Tests or Test-Command field")]
|
||||
MissingField {
|
||||
/// The 1-based stanza position.
|
||||
stanza: usize,
|
||||
},
|
||||
/// `test-name:` appeared on a `Tests:` (script list) stanza.
|
||||
#[error("stanza {stanza}: test-name only applies to a Test-Command stanza")]
|
||||
TestNameOnScripts {
|
||||
/// The 1-based stanza position.
|
||||
stanza: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl TestsControl {
|
||||
/// Parse the test control file at `path`.
|
||||
pub fn parse(path: &Path) -> Result<TestsControl, Box<dyn std::error::Error>> {
|
||||
@@ -69,7 +93,7 @@ impl TestsControl {
|
||||
/// `Test-Command:` stanza. A file without stanzas parses to an empty
|
||||
/// [`TestsControl`] — whether the package declares tests at all is
|
||||
/// the caller's check (`Testsuite: autopkgtest`).
|
||||
pub fn parse_content(content: &str) -> Result<TestsControl, String> {
|
||||
pub fn parse_content(content: &str) -> Result<TestsControl, TestsControlParseError> {
|
||||
let mut tests_control = TestsControl::default();
|
||||
let mut command_index = 0usize;
|
||||
for (index, paragraph) in parse_paragraphs(content).iter().enumerate() {
|
||||
@@ -85,20 +109,14 @@ impl TestsControl {
|
||||
|
||||
match (tests_field, command_field) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(format!(
|
||||
"stanza {stanza}: Tests and Test-Command are mutually exclusive"
|
||||
));
|
||||
return Err(TestsControlParseError::MutuallyExclusive { stanza });
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(format!(
|
||||
"stanza {stanza}: missing a Tests or Test-Command field"
|
||||
));
|
||||
return Err(TestsControlParseError::MissingField { stanza });
|
||||
}
|
||||
(Some(scripts), None) => {
|
||||
if test_name.is_some() {
|
||||
return Err(format!(
|
||||
"stanza {stanza}: test-name only applies to a Test-Command stanza"
|
||||
));
|
||||
return Err(TestsControlParseError::TestNameOnScripts { stanza });
|
||||
}
|
||||
for script in scripts.split_whitespace() {
|
||||
tests_control.tests.push(Test {
|
||||
@@ -259,7 +277,9 @@ mod tests {
|
||||
"only applies",
|
||||
),
|
||||
] {
|
||||
let error = TestsControl::parse_content(content).unwrap_err();
|
||||
let error = TestsControl::parse_content(content)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains(expected), "{error}");
|
||||
}
|
||||
}
|
||||
|
||||
+66
-13
@@ -12,19 +12,65 @@ pub struct DebianVersion {
|
||||
pub debian_revision: Option<String>,
|
||||
}
|
||||
|
||||
/// Failure of [`DebianVersion::parse`]: the string is outside the
|
||||
/// `[epoch:]upstream[-revision]` syntax.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum VersionParseError {
|
||||
/// The string, once trimmed, is empty.
|
||||
#[error("empty version string")]
|
||||
Empty,
|
||||
/// The epoch (before the first `:`) is not a number.
|
||||
#[error("invalid epoch '{epoch}' in version '{raw}'")]
|
||||
InvalidEpoch {
|
||||
/// The non-numeric epoch text.
|
||||
epoch: String,
|
||||
/// The whole version string.
|
||||
raw: String,
|
||||
},
|
||||
/// Nothing precedes the revision separator: no upstream version.
|
||||
#[error("missing upstream version in '{raw}'")]
|
||||
MissingUpstream {
|
||||
/// The whole version string.
|
||||
raw: String,
|
||||
},
|
||||
/// A character outside dpkg's upstream alphabet.
|
||||
#[error("invalid character '{c}' in version '{raw}'")]
|
||||
InvalidUpstreamChar {
|
||||
/// The offending character.
|
||||
c: char,
|
||||
/// The whole version string.
|
||||
raw: String,
|
||||
},
|
||||
/// A trailing hyphen: the revision number is empty (dpkg rejects it).
|
||||
#[error("empty debian revision in '{raw}'")]
|
||||
EmptyRevision {
|
||||
/// The whole version string.
|
||||
raw: String,
|
||||
},
|
||||
/// A character outside dpkg's revision alphabet.
|
||||
#[error("invalid character '{c}' in revision of version '{raw}'")]
|
||||
InvalidRevisionChar {
|
||||
/// The offending character.
|
||||
c: char,
|
||||
/// The whole version string.
|
||||
raw: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl DebianVersion {
|
||||
/// Parse and validate a Debian version string.
|
||||
pub fn parse(raw: &str) -> Result<DebianVersion, String> {
|
||||
pub fn parse(raw: &str) -> Result<DebianVersion, VersionParseError> {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() {
|
||||
return Err("empty version string".to_string());
|
||||
return Err(VersionParseError::Empty);
|
||||
}
|
||||
|
||||
let (epoch, rest) = match raw.split_once(':') {
|
||||
Some((e, r)) => {
|
||||
let epoch: u32 = e
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid epoch '{}' in version '{}'", e, raw))?;
|
||||
let epoch: u32 = e.parse().map_err(|_| VersionParseError::InvalidEpoch {
|
||||
epoch: e.to_string(),
|
||||
raw: raw.to_string(),
|
||||
})?;
|
||||
(Some(epoch), r)
|
||||
}
|
||||
None => (None, raw),
|
||||
@@ -37,14 +83,19 @@ impl DebianVersion {
|
||||
};
|
||||
|
||||
if upstream.is_empty() {
|
||||
return Err(format!("missing upstream version in '{}'", raw));
|
||||
return Err(VersionParseError::MissingUpstream {
|
||||
raw: raw.to_string(),
|
||||
});
|
||||
}
|
||||
for c in upstream.chars() {
|
||||
if !(c.is_ascii_alphanumeric()
|
||||
|| matches!(c, '.' | '+' | '-' | '~' | ':')
|
||||
|| !c.is_ascii())
|
||||
{
|
||||
return Err(format!("invalid character '{}' in version '{}'", c, raw));
|
||||
return Err(VersionParseError::InvalidUpstreamChar {
|
||||
c,
|
||||
raw: raw.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(rev) = &debian_revision {
|
||||
@@ -52,14 +103,16 @@ impl DebianVersion {
|
||||
// dpkg rejects a trailing hyphen: "bad syntax: revision
|
||||
// number is empty". Native versions (no `-` at all) are
|
||||
// handled above and stay valid.
|
||||
return Err(format!("empty debian revision in '{}'", raw));
|
||||
return Err(VersionParseError::EmptyRevision {
|
||||
raw: raw.to_string(),
|
||||
});
|
||||
}
|
||||
for c in rev.chars() {
|
||||
if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~') || !c.is_ascii()) {
|
||||
return Err(format!(
|
||||
"invalid character '{}' in revision of version '{}'",
|
||||
c, raw
|
||||
));
|
||||
return Err(VersionParseError::InvalidRevisionChar {
|
||||
c,
|
||||
raw: raw.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,7 +393,7 @@ mod tests {
|
||||
/// the *last* hyphen (upstream `1.0-` + revision `1`).
|
||||
#[test]
|
||||
fn version_empty_revision() {
|
||||
let err = DebianVersion::parse("1.0-").unwrap_err();
|
||||
let err = DebianVersion::parse("1.0-").unwrap_err().to_string();
|
||||
assert!(err.contains("empty"), "unexpected message: {err}");
|
||||
|
||||
assert!(DebianVersion::parse("1.0-").is_err());
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ fn apply_rules(
|
||||
}
|
||||
|
||||
for injected in &deps.inject {
|
||||
let parsed = Deps::parse(injected, opts)?;
|
||||
let parsed = Deps::parse(injected, opts).map_err(|e| e.to_string())?;
|
||||
clauses.extend(parsed.clauses().map(<[PkgRelation]>::to_vec));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ fn split_top_level_commas(input: &str) -> Vec<String> {
|
||||
/// stripped") and keeping everything else verbatim.
|
||||
fn name_only_clauses(input: &str, host_arch: &str) -> Result<Vec<String>, String> {
|
||||
let opts = ParseOpts::build_deps(host_arch.to_string(), Vec::new());
|
||||
let deps = Deps::parse(input, &opts)?;
|
||||
let deps = Deps::parse(input, &opts).map_err(|e| e.to_string())?;
|
||||
let mut clauses = Vec::new();
|
||||
for clause in deps.clauses() {
|
||||
let kept: Vec<String> = clause
|
||||
|
||||
Reference in New Issue
Block a user