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
|
/// (`*_ARCH`, `*_OS`, `*_CPU`, `*_MULTIARCH`, `*_GNU_TYPE`, ...), exactly as
|
||||||
/// `dpkg-buildpackage` does.
|
/// `dpkg-buildpackage` does.
|
||||||
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>, 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
|
/// 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.
|
/// 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();
|
let arches: Vec<String> = list.split_whitespace().map(str::to_string).collect();
|
||||||
for arch in &arches {
|
for arch in &arches {
|
||||||
if is_invalid(arch, false) {
|
if is_invalid(arch, false) {
|
||||||
return Err(format!(
|
return Err(ArchError::InvalidInList {
|
||||||
"'{arch}' is not a valid architecture in list '{list}'"
|
arch: arch.clone(),
|
||||||
));
|
list: list.to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(arches)
|
Ok(arches)
|
||||||
@@ -802,7 +833,7 @@ fn from_uname() -> Option<String> {
|
|||||||
/// the `DEB_BUILD_*` variables): the authoritative answer comes from the
|
/// the `DEB_BUILD_*` variables): the authoritative answer comes from the
|
||||||
/// dpkg database itself; if the `dpkg` frontend cannot be executed, the
|
/// dpkg database itself; if the `dpkg` frontend cannot be executed, the
|
||||||
/// architecture is derived from `uname` through the same tables.
|
/// 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()
|
if let Ok(output) = Command::new("dpkg").arg("--print-architecture").output()
|
||||||
&& output.status.success()
|
&& output.status.success()
|
||||||
{
|
{
|
||||||
@@ -811,7 +842,7 @@ pub fn native() -> Result<String, String> {
|
|||||||
return Ok(arch);
|
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
|
/// 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
|
/// The target architecture defaults to the host architecture, and the host
|
||||||
/// architecture defaults to the native build architecture, exactly like
|
/// architecture defaults to the native build architecture, exactly like
|
||||||
/// dpkg-architecture.
|
/// 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 build_arch = native()?;
|
||||||
let host_arch = host_arch.unwrap_or(&build_arch).to_string();
|
let host_arch = host_arch.unwrap_or(&build_arch).to_string();
|
||||||
let target_arch = host_arch.clone();
|
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),
|
("HOST", host_arch),
|
||||||
("TARGET", target_arch),
|
("TARGET", target_arch),
|
||||||
] {
|
] {
|
||||||
let tuple = debarch_to_debtuple(&arch)
|
let tuple = debarch_to_debtuple(&arch).ok_or_else(|| ArchError::Unknown {
|
||||||
.ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
arch: arch.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
env.insert(format!("DEB_{role}_ARCH"), arch.clone());
|
env.insert(format!("DEB_{role}_ARCH"), arch.clone());
|
||||||
env.insert(format!("DEB_{role}_ARCH_ABI"), tuple.abi.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_OS"), tuple.os.clone());
|
||||||
env.insert(format!("DEB_{role}_ARCH_CPU"), tuple.cpu.clone());
|
env.insert(format!("DEB_{role}_ARCH_CPU"), tuple.cpu.clone());
|
||||||
|
|
||||||
let (bits, endian) =
|
let (bits, endian) = abi_attrs(&arch).ok_or_else(|| ArchError::Unknown {
|
||||||
abi_attrs(&arch).ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
arch: arch.to_string(),
|
||||||
|
})?;
|
||||||
env.insert(format!("DEB_{role}_ARCH_BITS"), bits.to_string());
|
env.insert(format!("DEB_{role}_ARCH_BITS"), bits.to_string());
|
||||||
env.insert(format!("DEB_{role}_ARCH_ENDIAN"), endian.to_string());
|
env.insert(format!("DEB_{role}_ARCH_ENDIAN"), endian.to_string());
|
||||||
|
|
||||||
let multi =
|
let multi = multiarch(&arch).ok_or_else(|| ArchError::Unknown {
|
||||||
multiarch(&arch).ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
arch: arch.to_string(),
|
||||||
|
})?;
|
||||||
env.insert(format!("DEB_{role}_MULTIARCH"), multi);
|
env.insert(format!("DEB_{role}_MULTIARCH"), multi);
|
||||||
|
|
||||||
let gnu_type = debarch_to_gnutriplet(&arch)
|
let gnu_type = debarch_to_gnutriplet(&arch).ok_or_else(|| ArchError::Unknown {
|
||||||
.ok_or_else(|| format!("unknown Debian architecture '{arch}'"))?;
|
arch: arch.to_string(),
|
||||||
let (gnu_cpu, gnu_system) = gnu_type
|
})?;
|
||||||
.split_once('-')
|
let (gnu_cpu, gnu_system) =
|
||||||
.ok_or_else(|| format!("invalid GNU triplet '{gnu_type}'"))?;
|
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_CPU"), gnu_cpu.to_string());
|
||||||
env.insert(format!("DEB_{role}_GNU_SYSTEM"), gnu_system.to_string());
|
env.insert(format!("DEB_{role}_GNU_SYSTEM"), gnu_system.to_string());
|
||||||
env.insert(format!("DEB_{role}_GNU_TYPE"), gnu_type);
|
env.insert(format!("DEB_{role}_GNU_TYPE"), gnu_type);
|
||||||
|
|||||||
+72
-23
@@ -100,6 +100,48 @@ pub struct FileChecksums {
|
|||||||
index: HashMap<String, usize>,
|
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 {
|
impl FileChecksums {
|
||||||
/// Create an empty registry.
|
/// Create an empty registry.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -234,7 +276,11 @@ impl FileChecksums {
|
|||||||
/// `build::parse_checksum_field` helper (which additionally accepts the
|
/// `build::parse_checksum_field` helper (which additionally accepts the
|
||||||
/// legacy 5-column `Files` layout); the two are intentionally not unified
|
/// legacy 5-column `Files` layout); the two are intentionally not unified
|
||||||
/// across modules.
|
/// 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();
|
let mut entries = Vec::new();
|
||||||
for line in value.lines() {
|
for line in value.lines() {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
@@ -242,30 +288,28 @@ impl FileChecksums {
|
|||||||
}
|
}
|
||||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||||
let [digest, size, name] = tokens.as_slice() else {
|
let [digest, size, name] = tokens.as_slice() else {
|
||||||
return Err(format!(
|
return Err(ChecksumParseError::MalformedLine {
|
||||||
"malformed '{}' line (expected 'checksum size name', got {} \
|
field,
|
||||||
columns): '{line}'",
|
columns: tokens.len(),
|
||||||
kind.field_name(),
|
line: line.to_string(),
|
||||||
tokens.len()
|
});
|
||||||
));
|
|
||||||
};
|
};
|
||||||
let size: u64 = size.parse().map_err(|_| {
|
let size: u64 = size.parse().map_err(|_| ChecksumParseError::BadSize {
|
||||||
format!(
|
field: field.clone(),
|
||||||
"malformed '{}' line (size '{size}' is not a number): '{line}'",
|
size: size.to_string(),
|
||||||
kind.field_name()
|
line: line.to_string(),
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
let digest_ok = digest.len() == kind.digest_len()
|
let digest_ok = digest.len() == kind.digest_len()
|
||||||
&& digest
|
&& digest
|
||||||
.bytes()
|
.bytes()
|
||||||
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
|
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
|
||||||
if !digest_ok {
|
if !digest_ok {
|
||||||
return Err(format!(
|
return Err(ChecksumParseError::BadDigest {
|
||||||
"malformed '{}' line (digest '{digest}' is not {} lowercase \
|
field,
|
||||||
hex characters): '{line}'",
|
digest: digest.to_string(),
|
||||||
kind.field_name(),
|
len: kind.digest_len(),
|
||||||
kind.digest_len()
|
line: line.to_string(),
|
||||||
));
|
});
|
||||||
}
|
}
|
||||||
let mut entry = Entry {
|
let mut entry = Entry {
|
||||||
size,
|
size,
|
||||||
@@ -401,27 +445,32 @@ mod tests {
|
|||||||
assert_eq!(entries[0].1.size, 12);
|
assert_eq!(entries[0].1.size, 12);
|
||||||
|
|
||||||
// 2 columns: missing the name.
|
// 2 columns: missing the name.
|
||||||
let err =
|
let err = FileChecksums::parse_field(ChecksumKind::Sha512, &format!("{digest} 12"))
|
||||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!("{digest} 12")).unwrap_err();
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
assert!(err.contains("Checksums-Sha512"), "{err}");
|
assert!(err.contains("Checksums-Sha512"), "{err}");
|
||||||
assert!(err.contains(&format!("{digest} 12")), "{err}");
|
assert!(err.contains(&format!("{digest} 12")), "{err}");
|
||||||
|
|
||||||
// 4 columns.
|
// 4 columns.
|
||||||
let err =
|
let err =
|
||||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!(" {digest} 12 bogus a.txt"))
|
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("Checksums-Sha512"), "{err}");
|
||||||
assert!(err.contains("a.txt"), "{err}");
|
assert!(err.contains("a.txt"), "{err}");
|
||||||
|
|
||||||
// Non-numeric size.
|
// Non-numeric size.
|
||||||
let err =
|
let err =
|
||||||
FileChecksums::parse_field(ChecksumKind::Sha512, &format!(" {digest} twelve a.txt"))
|
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("not a number"), "{err}");
|
||||||
assert!(err.contains("twelve"), "{err}");
|
assert!(err.contains("twelve"), "{err}");
|
||||||
|
|
||||||
// Digest that is not 128 lowercase hex characters.
|
// 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}");
|
assert!(err.contains("lowercase hex"), "{err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-7
@@ -403,6 +403,21 @@ pub struct ControlInfo {
|
|||||||
pub binaries: Vec<Paragraph>,
|
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 {
|
impl ControlInfo {
|
||||||
/// Parse a `debian/control` file.
|
/// Parse a `debian/control` file.
|
||||||
pub fn parse(path: &Path) -> Result<ControlInfo, Box<dyn std::error::Error>> {
|
pub fn parse(path: &Path) -> Result<ControlInfo, Box<dyn std::error::Error>> {
|
||||||
@@ -416,19 +431,17 @@ impl ControlInfo {
|
|||||||
/// Parse control content from a string.
|
/// Parse control content from a string.
|
||||||
///
|
///
|
||||||
/// Prefer [`std::str::FromStr`] (`"...".parse::<ControlInfo>()`).
|
/// 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 paragraphs = parse_paragraphs(content);
|
||||||
let mut iter = paragraphs.into_iter();
|
let mut iter = paragraphs.into_iter();
|
||||||
let source = iter
|
let source = iter.next().ok_or(ControlParseError::NoParagraphs)?;
|
||||||
.next()
|
|
||||||
.ok_or_else(|| "control file has no paragraphs".to_string())?;
|
|
||||||
if source.get("Source").is_none() {
|
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();
|
let binaries: Vec<Paragraph> = iter.collect();
|
||||||
for bin in &binaries {
|
for bin in &binaries {
|
||||||
if bin.get("Package").is_none() {
|
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 })
|
Ok(ControlInfo { source, binaries })
|
||||||
@@ -451,7 +464,7 @@ impl ControlInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for ControlInfo {
|
impl std::str::FromStr for ControlInfo {
|
||||||
type Err = String;
|
type Err = ControlParseError;
|
||||||
|
|
||||||
fn from_str(content: &str) -> Result<Self, Self::Err> {
|
fn from_str(content: &str) -> Result<Self, Self::Err> {
|
||||||
ControlInfo::parse_content(content)
|
ControlInfo::parse_content(content)
|
||||||
|
|||||||
+135
-33
@@ -167,22 +167,96 @@ fn profile_name_regex() -> &'static Regex {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse one simple dependency alternative.
|
/// Failure of the dependency parsers: a syntactically invalid relation or
|
||||||
///
|
/// restriction formula, an invalid parse option, or an unreadable dpkg
|
||||||
/// Fails when the text does not match the grammar, when `:native` appears in
|
/// status file.
|
||||||
/// a non-build-dependency context, or when the architecture/version/profile
|
#[derive(Debug, thiserror::Error)]
|
||||||
/// syntax is invalid.
|
pub enum ParseError {
|
||||||
pub fn parse_simple(dep: &str, build_dep: bool) -> Result<PkgRelation, String> {
|
/// 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()
|
let caps = dep_regex()
|
||||||
.captures(dep)
|
.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 package = caps[2].to_string();
|
||||||
let arch_qualifier = caps.get(3).map(|m| m.as_str().to_string());
|
let arch_qualifier = caps.get(3).map(|m| m.as_str().to_string());
|
||||||
if arch_qualifier.as_deref() == Some("native") && !build_dep {
|
if arch_qualifier.as_deref() == Some("native") && !build_dep {
|
||||||
return Err(format!(
|
return Err(ParseError::NativeOutsideBuildDep {
|
||||||
"':native' qualifier only allowed in build dependencies: '{dep}'"
|
dep: dep.to_string(),
|
||||||
));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let constraint = match (caps.get(5), caps.get(6)) {
|
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::Eq,
|
||||||
">" | ">=" => Relation::Ge,
|
">" | ">=" => Relation::Ge,
|
||||||
">>" => Relation::Gt,
|
">>" => 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())
|
let version = DebianVersion::parse(version.as_str()).map_err(|source| {
|
||||||
.map_err(|e| format!("invalid version in dependency '{dep}': {e}"))?;
|
ParseError::InvalidVersion {
|
||||||
|
dep: dep.to_string(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
Some(VersionConstraint { relation, version })
|
Some(VersionConstraint { relation, version })
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let arches = match caps.get(8) {
|
let arches =
|
||||||
Some(list) => Some(
|
match caps.get(8) {
|
||||||
arch::list_parse(list.as_str())
|
Some(list) => Some(arch::list_parse(list.as_str()).map_err(|source| {
|
||||||
.map_err(|e| format!("invalid architecture list in '{dep}': {e}"))?,
|
ParseError::InvalidArchList {
|
||||||
),
|
dep: dep.to_string(),
|
||||||
None => None,
|
source,
|
||||||
};
|
}
|
||||||
|
})?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let mut restrictions = Vec::new();
|
let mut restrictions = Vec::new();
|
||||||
if let Some(formula) = caps.get(9) {
|
if let Some(formula) = caps.get(9) {
|
||||||
for group in restriction_group_regex().captures_iter(formula.as_str()) {
|
for group in restriction_group_regex().captures_iter(formula.as_str()) {
|
||||||
let terms: Vec<String> = group[1].split_whitespace().map(str::to_string).collect();
|
let terms: Vec<String> = group[1].split_whitespace().map(str::to_string).collect();
|
||||||
if terms.is_empty() {
|
if terms.is_empty() {
|
||||||
return Err(format!("empty restriction formula in '{dep}'"));
|
return Err(ParseError::EmptyRestrictions {
|
||||||
|
dep: dep.to_string(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for term in &terms {
|
for term in &terms {
|
||||||
if !profile_name_regex().is_match(term) {
|
if !profile_name_regex().is_match(term) {
|
||||||
return Err(format!(
|
return Err(ParseError::InvalidProfileFormula {
|
||||||
"'{}' is not a valid build profile restriction formula",
|
formula: formula.as_str().to_string(),
|
||||||
formula.as_str()
|
});
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
restrictions.push(terms);
|
restrictions.push(terms);
|
||||||
@@ -291,13 +378,19 @@ impl Deps {
|
|||||||
/// alternatives whose architecture or profile restrictions do not apply
|
/// alternatives whose architecture or profile restrictions do not apply
|
||||||
/// are dropped first; clauses losing all their alternatives disappear,
|
/// are dropped first; clauses losing all their alternatives disappear,
|
||||||
/// and a fully reduced field yields an empty [`Deps`].
|
/// 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)
|
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) {
|
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.
|
// Merge continuation lines and normalize whitespace.
|
||||||
@@ -336,7 +429,7 @@ impl Deps {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if opts.union && alternatives.len() > 1 {
|
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);
|
clauses.push(alternatives);
|
||||||
}
|
}
|
||||||
@@ -652,9 +745,15 @@ impl Facts {
|
|||||||
|
|
||||||
/// Load a dpkg status file (e.g. `/var/lib/dpkg/status`), binding the
|
/// Load a dpkg status file (e.g. `/var/lib/dpkg/status`), binding the
|
||||||
/// facts to the given host/build architectures.
|
/// facts to the given host/build architectures.
|
||||||
pub fn load_status(path: &Path, host_arch: &str, build_arch: &str) -> Result<Facts, String> {
|
pub fn load_status(
|
||||||
let content = std::fs::read_to_string(path)
|
path: &Path,
|
||||||
.map_err(|e| format!("cannot read status file '{}': {}", path.display(), e))?;
|
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))
|
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`]
|
/// On success the report is empty; otherwise its [`UnmetReport::message`]
|
||||||
/// carries the dpkg-compatible diagnostics.
|
/// 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 source = &control.source;
|
||||||
|
|
||||||
let mut bd_parts: Vec<&str> = Vec::new();
|
let mut bd_parts: Vec<&str> = Vec::new();
|
||||||
|
|||||||
+25
-4
@@ -121,6 +121,23 @@ pub struct FilesList {
|
|||||||
files: BTreeMap<String, FilesEntry>,
|
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 {
|
impl FilesList {
|
||||||
/// An empty registry.
|
/// An empty registry.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -141,7 +158,7 @@ impl FilesList {
|
|||||||
|
|
||||||
/// Parse a `debian/files` registry from its textual content
|
/// Parse a `debian/files` registry from its textual content
|
||||||
/// (`filename section priority [key=value...]` lines).
|
/// (`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();
|
let mut list = FilesList::new();
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
@@ -149,10 +166,14 @@ impl FilesList {
|
|||||||
}
|
}
|
||||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||||
if tokens.len() < 3 {
|
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])
|
let mut entry =
|
||||||
.ok_or_else(|| format!("badly formed file name: {}", tokens[0]))?;
|
parse_filename(tokens[0]).ok_or_else(|| FilesParseError::BadFileName {
|
||||||
|
name: tokens[0].to_string(),
|
||||||
|
})?;
|
||||||
entry.section = tokens[1].to_string();
|
entry.section = tokens[1].to_string();
|
||||||
entry.priority = tokens[2].to_string();
|
entry.priority = tokens[2].to_string();
|
||||||
for attr in &tokens[3..] {
|
for attr in &tokens[3..] {
|
||||||
|
|||||||
+31
-11
@@ -54,6 +54,30 @@ pub struct TestsControl {
|
|||||||
pub tests: Vec<Test>,
|
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 {
|
impl TestsControl {
|
||||||
/// Parse the test control file at `path`.
|
/// Parse the test control file at `path`.
|
||||||
pub fn parse(path: &Path) -> Result<TestsControl, Box<dyn std::error::Error>> {
|
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
|
/// `Test-Command:` stanza. A file without stanzas parses to an empty
|
||||||
/// [`TestsControl`] — whether the package declares tests at all is
|
/// [`TestsControl`] — whether the package declares tests at all is
|
||||||
/// the caller's check (`Testsuite: autopkgtest`).
|
/// 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 tests_control = TestsControl::default();
|
||||||
let mut command_index = 0usize;
|
let mut command_index = 0usize;
|
||||||
for (index, paragraph) in parse_paragraphs(content).iter().enumerate() {
|
for (index, paragraph) in parse_paragraphs(content).iter().enumerate() {
|
||||||
@@ -85,20 +109,14 @@ impl TestsControl {
|
|||||||
|
|
||||||
match (tests_field, command_field) {
|
match (tests_field, command_field) {
|
||||||
(Some(_), Some(_)) => {
|
(Some(_), Some(_)) => {
|
||||||
return Err(format!(
|
return Err(TestsControlParseError::MutuallyExclusive { stanza });
|
||||||
"stanza {stanza}: Tests and Test-Command are mutually exclusive"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
(None, None) => {
|
(None, None) => {
|
||||||
return Err(format!(
|
return Err(TestsControlParseError::MissingField { stanza });
|
||||||
"stanza {stanza}: missing a Tests or Test-Command field"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
(Some(scripts), None) => {
|
(Some(scripts), None) => {
|
||||||
if test_name.is_some() {
|
if test_name.is_some() {
|
||||||
return Err(format!(
|
return Err(TestsControlParseError::TestNameOnScripts { stanza });
|
||||||
"stanza {stanza}: test-name only applies to a Test-Command stanza"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
for script in scripts.split_whitespace() {
|
for script in scripts.split_whitespace() {
|
||||||
tests_control.tests.push(Test {
|
tests_control.tests.push(Test {
|
||||||
@@ -259,7 +277,9 @@ mod tests {
|
|||||||
"only applies",
|
"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}");
|
assert!(error.contains(expected), "{error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-13
@@ -12,19 +12,65 @@ pub struct DebianVersion {
|
|||||||
pub debian_revision: Option<String>,
|
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 {
|
impl DebianVersion {
|
||||||
/// Parse and validate a Debian version string.
|
/// 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();
|
let raw = raw.trim();
|
||||||
if raw.is_empty() {
|
if raw.is_empty() {
|
||||||
return Err("empty version string".to_string());
|
return Err(VersionParseError::Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (epoch, rest) = match raw.split_once(':') {
|
let (epoch, rest) = match raw.split_once(':') {
|
||||||
Some((e, r)) => {
|
Some((e, r)) => {
|
||||||
let epoch: u32 = e
|
let epoch: u32 = e.parse().map_err(|_| VersionParseError::InvalidEpoch {
|
||||||
.parse()
|
epoch: e.to_string(),
|
||||||
.map_err(|_| format!("invalid epoch '{}' in version '{}'", e, raw))?;
|
raw: raw.to_string(),
|
||||||
|
})?;
|
||||||
(Some(epoch), r)
|
(Some(epoch), r)
|
||||||
}
|
}
|
||||||
None => (None, raw),
|
None => (None, raw),
|
||||||
@@ -37,14 +83,19 @@ impl DebianVersion {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if upstream.is_empty() {
|
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() {
|
for c in upstream.chars() {
|
||||||
if !(c.is_ascii_alphanumeric()
|
if !(c.is_ascii_alphanumeric()
|
||||||
|| matches!(c, '.' | '+' | '-' | '~' | ':')
|
|| matches!(c, '.' | '+' | '-' | '~' | ':')
|
||||||
|| !c.is_ascii())
|
|| !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 {
|
if let Some(rev) = &debian_revision {
|
||||||
@@ -52,14 +103,16 @@ impl DebianVersion {
|
|||||||
// dpkg rejects a trailing hyphen: "bad syntax: revision
|
// dpkg rejects a trailing hyphen: "bad syntax: revision
|
||||||
// number is empty". Native versions (no `-` at all) are
|
// number is empty". Native versions (no `-` at all) are
|
||||||
// handled above and stay valid.
|
// 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() {
|
for c in rev.chars() {
|
||||||
if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~') || !c.is_ascii()) {
|
if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~') || !c.is_ascii()) {
|
||||||
return Err(format!(
|
return Err(VersionParseError::InvalidRevisionChar {
|
||||||
"invalid character '{}' in revision of version '{}'",
|
c,
|
||||||
c, raw
|
raw: raw.to_string(),
|
||||||
));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +393,7 @@ mod tests {
|
|||||||
/// the *last* hyphen (upstream `1.0-` + revision `1`).
|
/// the *last* hyphen (upstream `1.0-` + revision `1`).
|
||||||
#[test]
|
#[test]
|
||||||
fn version_empty_revision() {
|
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!(err.contains("empty"), "unexpected message: {err}");
|
||||||
|
|
||||||
assert!(DebianVersion::parse("1.0-").is_err());
|
assert!(DebianVersion::parse("1.0-").is_err());
|
||||||
|
|||||||
+1
-1
@@ -266,7 +266,7 @@ fn apply_rules(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for injected in &deps.inject {
|
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));
|
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.
|
/// stripped") and keeping everything else verbatim.
|
||||||
fn name_only_clauses(input: &str, host_arch: &str) -> Result<Vec<String>, String> {
|
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 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();
|
let mut clauses = Vec::new();
|
||||||
for clause in deps.clauses() {
|
for clause in deps.clauses() {
|
||||||
let kept: Vec<String> = clause
|
let kept: Vec<String> = clause
|
||||||
|
|||||||
Reference in New Issue
Block a user