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.
|
/// 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
|
/// Values are stored with continuation-line breaks as `\n` and without the
|
||||||
/// leading whitespace of continuation lines. Serialization re-adds a single
|
/// leading whitespace of continuation lines. Serialization re-adds a single
|
||||||
/// leading space in front of every continuation line, matching dpkg output;
|
/// 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).
|
/// 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> {
|
pub fn get(&self, field: &str) -> Option<&str> {
|
||||||
self.fields
|
self.fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -34,17 +41,26 @@ impl Paragraph {
|
|||||||
.map(|(_, v)| v.as_str())
|
.map(|(_, v)| v.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a field value, replacing any previous occurrence (case-insensitive).
|
/// Set a field value, replacing all case-insensitive duplicates: after
|
||||||
/// Appends the field at the end if it did not exist yet.
|
/// 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) {
|
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 k.eq_ignore_ascii_case(field) {
|
||||||
|
if updated {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
*v = value.to_string();
|
*v = value.to_string();
|
||||||
return;
|
updated = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
if !updated {
|
||||||
self.fields.push((field.to_string(), value.to_string()));
|
self.fields.push((field.to_string(), value.to_string()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a field (case-insensitive). Returns true if it was present.
|
/// Remove a field (case-insensitive). Returns true if it was present.
|
||||||
pub fn remove(&mut self, field: &str) -> bool {
|
pub fn remove(&mut self, field: &str) -> bool {
|
||||||
@@ -337,6 +353,37 @@ iQEcBAABCgAGBQJabcdAAoJEL abc
|
|||||||
assert_eq!(p.iter().count(), 1);
|
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]
|
#[test]
|
||||||
fn remove_field() {
|
fn remove_field() {
|
||||||
let mut p = Paragraph::new();
|
let mut p = Paragraph::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user