debian: make Paragraph::set drop case-insensitive duplicates
set only replaced the first match and appended otherwise, so a paragraph holding both 'Depends:' and 'depends:' kept a stale second value after an update, silently re-emitted on serialization. set now updates the first match in place and removes any other case-insensitive duplicate; the parser stays lenient and keeps duplicates reachable via iter().
This commit is contained in:
+52
-5
@@ -10,6 +10,9 @@ use std::path::Path;
|
||||
|
||||
/// A single deb822 paragraph: an ordered list of `(field, value)` pairs.
|
||||
///
|
||||
/// The parser is lenient: duplicate field names are kept as separate entries
|
||||
/// (accessors see the first one; `set` collapses them back to a single one).
|
||||
///
|
||||
/// Values are stored with continuation-line breaks as `\n` and without the
|
||||
/// leading whitespace of continuation lines. Serialization re-adds a single
|
||||
/// leading space in front of every continuation line, matching dpkg output;
|
||||
@@ -27,6 +30,10 @@ impl Paragraph {
|
||||
}
|
||||
|
||||
/// Look up a field value (case-insensitive field name).
|
||||
///
|
||||
/// Returns the first match. The parser is lenient and keeps duplicate
|
||||
/// field names as-is; use [`Paragraph::iter`] to reach the other
|
||||
/// occurrences. [`Paragraph::set`] collapses them.
|
||||
pub fn get(&self, field: &str) -> Option<&str> {
|
||||
self.fields
|
||||
.iter()
|
||||
@@ -34,17 +41,26 @@ impl Paragraph {
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
/// Set a field value, replacing any previous occurrence (case-insensitive).
|
||||
/// Appends the field at the end if it did not exist yet.
|
||||
/// Set a field value, replacing all case-insensitive duplicates: after
|
||||
/// the call at most one entry with this field name remains — the updated
|
||||
/// one, kept at its original position. Appends the field at the end if
|
||||
/// no entry existed yet.
|
||||
pub fn set(&mut self, field: &str, value: &str) {
|
||||
for (k, v) in self.fields.iter_mut() {
|
||||
let mut updated = false;
|
||||
self.fields.retain_mut(|(k, v)| {
|
||||
if k.eq_ignore_ascii_case(field) {
|
||||
if updated {
|
||||
return false;
|
||||
}
|
||||
*v = value.to_string();
|
||||
return;
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
true
|
||||
});
|
||||
if !updated {
|
||||
self.fields.push((field.to_string(), value.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a field (case-insensitive). Returns true if it was present.
|
||||
pub fn remove(&mut self, field: &str) -> bool {
|
||||
@@ -337,6 +353,37 @@ iQEcBAABCgAGBQJabcdAAoJEL abc
|
||||
assert_eq!(p.iter().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lenient_parse_keeps_duplicate_fields() {
|
||||
let paras = parse_paragraphs("Package: hello\nDepends: a\ndepends: b\n");
|
||||
let p = ¶s[0];
|
||||
// deb822 forbids duplicate fields but the parser is lenient and keeps
|
||||
// both entries; `get` returns the first.
|
||||
let depends: Vec<_> = p
|
||||
.iter()
|
||||
.filter(|(k, _)| k.eq_ignore_ascii_case("Depends"))
|
||||
.collect();
|
||||
assert_eq!(depends, [("Depends", "a"), ("depends", "b")]);
|
||||
assert_eq!(p.get("Depends"), Some("a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_removes_case_insensitive_duplicates() {
|
||||
let mut paras = parse_paragraphs("Package: hello\nDepends: a\ndepends: b\n");
|
||||
let mut p = paras.remove(0);
|
||||
p.set("Depends", "c");
|
||||
// Exactly one depends-family entry remains, with the new value.
|
||||
let depends: Vec<_> = p
|
||||
.iter()
|
||||
.filter(|(k, _)| k.eq_ignore_ascii_case("Depends"))
|
||||
.collect();
|
||||
assert_eq!(depends, [("Depends", "c")]);
|
||||
assert_eq!(p.get("depends"), Some("c"));
|
||||
// ...kept at its original position, and a write round-trip no longer
|
||||
// leaks the stale duplicate.
|
||||
assert_eq!(write_paragraph(&p), "Package: hello\nDepends: c\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_field() {
|
||||
let mut p = Paragraph::new();
|
||||
|
||||
Reference in New Issue
Block a user