use crate::fields::{MultiArch, Priority};
use crate::lossless::relations::Relations;
use deb822_lossless::{Deb822, Paragraph, TextRange};
use rowan::ast::AstNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseMode {
Strict,
Relaxed,
Substvar,
}
pub const SOURCE_FIELD_ORDER: &[&str] = &[
"Source",
"Section",
"Priority",
"Maintainer",
"Uploaders",
"Build-Depends",
"Build-Depends-Indep",
"Build-Depends-Arch",
"Build-Conflicts",
"Build-Conflicts-Indep",
"Build-Conflicts-Arch",
"Standards-Version",
"Vcs-Browser",
"Vcs-Git",
"Vcs-Svn",
"Vcs-Bzr",
"Vcs-Hg",
"Vcs-Darcs",
"Vcs-Cvs",
"Vcs-Arch",
"Vcs-Mtn",
"Homepage",
"Rules-Requires-Root",
"Testsuite",
"Testsuite-Triggers",
];
pub const BINARY_FIELD_ORDER: &[&str] = &[
"Package",
"Architecture",
"Section",
"Priority",
"Multi-Arch",
"Essential",
"Build-Profiles",
"Built-Using",
"Static-Built-Using",
"Pre-Depends",
"Depends",
"Recommends",
"Suggests",
"Enhances",
"Conflicts",
"Breaks",
"Replaces",
"Provides",
"Description",
];
fn format_field(name: &str, value: &str) -> String {
match name {
"Uploaders" => value
.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
.join(",\n"),
"Build-Depends"
| "Build-Depends-Indep"
| "Build-Depends-Arch"
| "Build-Conflicts"
| "Build-Conflicts-Indep"
| "Build-Conflics-Arch"
| "Depends"
| "Recommends"
| "Suggests"
| "Enhances"
| "Pre-Depends"
| "Breaks" => {
match value.parse::<Relations>() {
Ok(relations) => {
let relations = relations.wrap_and_sort();
relations.to_string()
}
Err(_) => value.to_string(),
}
}
_ => value.to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Control {
deb822: Deb822,
parse_mode: ParseMode,
}
impl Control {
pub fn new() -> Self {
Control {
deb822: Deb822::new(),
parse_mode: ParseMode::Strict,
}
}
pub fn new_with_mode(parse_mode: ParseMode) -> Self {
Control {
deb822: Deb822::new(),
parse_mode,
}
}
pub fn parse_mode(&self) -> ParseMode {
self.parse_mode
}
pub fn as_mut_deb822(&mut self) -> &mut Deb822 {
&mut self.deb822
}
pub fn as_deb822(&self) -> &Deb822 {
&self.deb822
}
pub fn snapshot(&self) -> Self {
Control {
deb822: self.deb822.snapshot(),
parse_mode: self.parse_mode,
}
}
pub fn parse(text: &str) -> deb822_lossless::Parse<Control> {
let deb822_parse = Deb822::parse(text);
let green = deb822_parse.green().clone();
let errors = deb822_parse.errors().to_vec();
let positioned_errors = deb822_parse.positioned_errors().to_vec();
deb822_lossless::Parse::new_with_positioned_errors(green, errors, positioned_errors)
}
pub fn source(&self) -> Option<Source> {
let parse_mode = self.parse_mode;
self.deb822
.paragraphs()
.find(|p| p.get("Source").is_some())
.map(|paragraph| Source {
paragraph,
parse_mode,
})
}
pub fn binaries(&self) -> impl Iterator<Item = Binary> + '_ {
let parse_mode = self.parse_mode;
self.deb822
.paragraphs()
.filter(|p| p.get("Package").is_some())
.map(move |paragraph| Binary {
paragraph,
parse_mode,
})
}
pub fn source_in_range(&self, range: TextRange) -> Option<Source> {
self.source().filter(|s| {
let para_range = s.as_deb822().text_range();
para_range.start() < range.end() && para_range.end() > range.start()
})
}
pub fn binaries_in_range(&self, range: TextRange) -> impl Iterator<Item = Binary> + '_ {
self.binaries().filter(move |b| {
let para_range = b.as_deb822().text_range();
para_range.start() < range.end() && para_range.end() > range.start()
})
}
pub fn add_source(&mut self, name: &str) -> Source {
let mut p = self.deb822.add_paragraph();
p.set("Source", name);
self.source().unwrap()
}
pub fn add_binary(&mut self, name: &str) -> Binary {
let mut p = self.deb822.add_paragraph();
p.set("Package", name);
Binary {
paragraph: p,
parse_mode: ParseMode::Strict,
}
}
pub fn remove_binary(&mut self, name: &str) -> bool {
let index = self
.deb822
.paragraphs()
.position(|p| p.get("Package").as_deref() == Some(name));
if let Some(index) = index {
self.deb822.remove_paragraph(index);
true
} else {
false
}
}
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, deb822_lossless::Error> {
Ok(Control {
deb822: Deb822::from_file(path)?,
parse_mode: ParseMode::Strict,
})
}
pub fn from_file_relaxed<P: AsRef<std::path::Path>>(
path: P,
) -> Result<(Self, Vec<String>), std::io::Error> {
let (deb822, errors) = Deb822::from_file_relaxed(path)?;
Ok((
Control {
deb822,
parse_mode: ParseMode::Relaxed,
},
errors,
))
}
pub fn read<R: std::io::Read>(mut r: R) -> Result<Self, deb822_lossless::Error> {
Ok(Control {
deb822: Deb822::read(&mut r)?,
parse_mode: ParseMode::Strict,
})
}
pub fn read_relaxed<R: std::io::Read>(
mut r: R,
) -> Result<(Self, Vec<String>), deb822_lossless::Error> {
let (deb822, errors) = Deb822::read_relaxed(&mut r)?;
Ok((
Control {
deb822,
parse_mode: ParseMode::Relaxed,
},
errors,
))
}
pub fn wrap_and_sort(
&mut self,
indentation: deb822_lossless::Indentation,
immediate_empty_line: bool,
max_line_length_one_liner: Option<usize>,
) {
let sort_paragraphs = |a: &Paragraph, b: &Paragraph| -> std::cmp::Ordering {
let a_is_source = a.get("Source").is_some();
let b_is_source = b.get("Source").is_some();
if a_is_source && !b_is_source {
return std::cmp::Ordering::Less;
} else if !a_is_source && b_is_source {
return std::cmp::Ordering::Greater;
} else if a_is_source && b_is_source {
return a.get("Source").cmp(&b.get("Source"));
}
a.get("Package").cmp(&b.get("Package"))
};
let wrap_paragraph = |p: &Paragraph| -> Paragraph {
p.wrap_and_sort(
indentation,
immediate_empty_line,
max_line_length_one_liner,
None,
Some(&format_field),
)
};
self.deb822 = self
.deb822
.wrap_and_sort(Some(&sort_paragraphs), Some(&wrap_paragraph));
}
pub fn sort_binaries(&mut self, keep_first: bool) {
let mut paragraphs: Vec<_> = self.deb822.paragraphs().collect();
if paragraphs.len() <= 1 {
return; }
let source_idx = paragraphs.iter().position(|p| p.get("Source").is_some());
let binary_start = source_idx.map(|i| i + 1).unwrap_or(0);
let sort_start = if keep_first && paragraphs.len() > binary_start + 1 {
binary_start + 1
} else {
binary_start
};
if sort_start >= paragraphs.len() {
return; }
paragraphs[sort_start..].sort_by(|a, b| {
let a_name = a.get("Package");
let b_name = b.get("Package");
a_name.cmp(&b_name)
});
let sort_paragraphs = |a: &Paragraph, b: &Paragraph| -> std::cmp::Ordering {
let a_pos = paragraphs.iter().position(|p| p == a);
let b_pos = paragraphs.iter().position(|p| p == b);
a_pos.cmp(&b_pos)
};
self.deb822 = self.deb822.wrap_and_sort(Some(&sort_paragraphs), None);
}
pub fn fields_in_range(
&self,
range: TextRange,
) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
self.deb822
.paragraphs()
.flat_map(move |p| p.entries().collect::<Vec<_>>())
.filter(move |entry| {
let entry_range = entry.syntax().text_range();
entry_range.start() < range.end() && range.start() < entry_range.end()
})
}
}
impl From<Control> for Deb822 {
fn from(c: Control) -> Self {
c.deb822
}
}
impl From<Deb822> for Control {
fn from(d: Deb822) -> Self {
Control {
deb822: d,
parse_mode: ParseMode::Strict,
}
}
}
impl Default for Control {
fn default() -> Self {
Self::new()
}
}
impl std::str::FromStr for Control {
type Err = deb822_lossless::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Control::parse(s).to_result()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Source {
paragraph: Paragraph,
parse_mode: ParseMode,
}
impl From<Source> for Paragraph {
fn from(s: Source) -> Self {
s.paragraph
}
}
impl From<Paragraph> for Source {
fn from(p: Paragraph) -> Self {
Source {
paragraph: p,
parse_mode: ParseMode::Strict,
}
}
}
impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.paragraph.fmt(f)
}
}
impl Source {
fn parse_relations(&self, s: &str) -> Relations {
match self.parse_mode {
ParseMode::Strict => s.parse().unwrap(),
ParseMode::Relaxed => Relations::parse_relaxed(s, false).0,
ParseMode::Substvar => Relations::parse_relaxed(s, true).0,
}
}
pub fn name(&self) -> Option<String> {
self.paragraph.get("Source")
}
pub fn wrap_and_sort(
&mut self,
indentation: deb822_lossless::Indentation,
immediate_empty_line: bool,
max_line_length_one_liner: Option<usize>,
) {
self.paragraph = self.paragraph.wrap_and_sort(
indentation,
immediate_empty_line,
max_line_length_one_liner,
None,
Some(&format_field),
);
}
pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
&mut self.paragraph
}
pub fn as_deb822(&self) -> &Paragraph {
&self.paragraph
}
pub fn set_name(&mut self, name: &str) {
self.set("Source", name);
}
pub fn section(&self) -> Option<String> {
self.paragraph.get("Section")
}
pub fn set_section(&mut self, section: Option<&str>) {
if let Some(section) = section {
self.set("Section", section);
} else {
self.paragraph.remove("Section");
}
}
pub fn priority(&self) -> Option<Priority> {
self.paragraph.get("Priority").and_then(|v| v.parse().ok())
}
pub fn set_priority(&mut self, priority: Option<Priority>) {
if let Some(priority) = priority {
self.set("Priority", priority.to_string().as_str());
} else {
self.paragraph.remove("Priority");
}
}
pub fn maintainer(&self) -> Option<String> {
self.paragraph.get("Maintainer")
}
pub fn set_maintainer(&mut self, maintainer: &str) {
self.set("Maintainer", maintainer);
}
pub fn build_depends(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Build-Depends")
.map(|s| self.parse_relations(&s))
}
pub fn set_build_depends(&mut self, relations: &Relations) {
self.set("Build-Depends", relations.to_string().as_str());
}
pub fn build_depends_indep(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Build-Depends-Indep")
.map(|s| self.parse_relations(&s))
}
pub fn set_build_depends_indep(&mut self, relations: &Relations) {
self.set("Build-Depends-Indep", relations.to_string().as_str());
}
pub fn build_depends_arch(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Build-Depends-Arch")
.map(|s| self.parse_relations(&s))
}
pub fn set_build_depends_arch(&mut self, relations: &Relations) {
self.set("Build-Depends-Arch", relations.to_string().as_str());
}
pub fn build_conflicts(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Build-Conflicts")
.map(|s| self.parse_relations(&s))
}
pub fn set_build_conflicts(&mut self, relations: &Relations) {
self.set("Build-Conflicts", relations.to_string().as_str());
}
pub fn build_conflicts_indep(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Build-Conflicts-Indep")
.map(|s| self.parse_relations(&s))
}
pub fn set_build_conflicts_indep(&mut self, relations: &Relations) {
self.set("Build-Conflicts-Indep", relations.to_string().as_str());
}
pub fn build_conflicts_arch(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Build-Conflicts-Arch")
.map(|s| self.parse_relations(&s))
}
pub fn standards_version(&self) -> Option<String> {
self.paragraph.get("Standards-Version")
}
pub fn set_standards_version(&mut self, version: &str) {
self.set("Standards-Version", version);
}
pub fn homepage(&self) -> Option<url::Url> {
self.paragraph.get("Homepage").and_then(|s| s.parse().ok())
}
pub fn set_homepage(&mut self, homepage: &url::Url) {
self.set("Homepage", homepage.to_string().as_str());
}
pub fn vcs_git(&self) -> Option<String> {
self.paragraph.get("Vcs-Git")
}
pub fn set_vcs_git(&mut self, url: &str) {
self.set("Vcs-Git", url);
}
pub fn vcs_svn(&self) -> Option<String> {
self.paragraph.get("Vcs-Svn").map(|s| s.to_string())
}
pub fn set_vcs_svn(&mut self, url: &str) {
self.set("Vcs-Svn", url);
}
pub fn vcs_bzr(&self) -> Option<String> {
self.paragraph.get("Vcs-Bzr").map(|s| s.to_string())
}
pub fn set_vcs_bzr(&mut self, url: &str) {
self.set("Vcs-Bzr", url);
}
pub fn vcs_arch(&self) -> Option<String> {
self.paragraph.get("Vcs-Arch").map(|s| s.to_string())
}
pub fn set_vcs_arch(&mut self, url: &str) {
self.set("Vcs-Arch", url);
}
pub fn vcs_svk(&self) -> Option<String> {
self.paragraph.get("Vcs-Svk").map(|s| s.to_string())
}
pub fn set_vcs_svk(&mut self, url: &str) {
self.set("Vcs-Svk", url);
}
pub fn vcs_darcs(&self) -> Option<String> {
self.paragraph.get("Vcs-Darcs").map(|s| s.to_string())
}
pub fn set_vcs_darcs(&mut self, url: &str) {
self.set("Vcs-Darcs", url);
}
pub fn vcs_mtn(&self) -> Option<String> {
self.paragraph.get("Vcs-Mtn").map(|s| s.to_string())
}
pub fn set_vcs_mtn(&mut self, url: &str) {
self.set("Vcs-Mtn", url);
}
pub fn vcs_cvs(&self) -> Option<String> {
self.paragraph.get("Vcs-Cvs").map(|s| s.to_string())
}
pub fn set_vcs_cvs(&mut self, url: &str) {
self.set("Vcs-Cvs", url);
}
pub fn vcs_hg(&self) -> Option<String> {
self.paragraph.get("Vcs-Hg").map(|s| s.to_string())
}
pub fn set_vcs_hg(&mut self, url: &str) {
self.set("Vcs-Hg", url);
}
pub fn set(&mut self, key: &str, value: &str) {
self.paragraph
.set_with_field_order(key, value, SOURCE_FIELD_ORDER);
}
pub fn get(&self, key: &str) -> Option<String> {
self.paragraph.get(key)
}
pub fn vcs_browser(&self) -> Option<String> {
self.paragraph.get("Vcs-Browser")
}
pub fn vcs(&self) -> Option<crate::vcs::Vcs> {
for (name, value) in self.paragraph.items() {
if name.starts_with("Vcs-") && name != "Vcs-Browser" {
return crate::vcs::Vcs::from_field(&name, &value).ok();
}
}
None
}
pub fn set_vcs_browser(&mut self, url: Option<&str>) {
if let Some(url) = url {
self.set("Vcs-Browser", url);
} else {
self.paragraph.remove("Vcs-Browser");
}
}
pub fn uploaders(&self) -> Option<Vec<String>> {
self.paragraph
.get("Uploaders")
.map(|s| s.split(',').map(|s| s.trim().to_owned()).collect())
}
pub fn set_uploaders(&mut self, uploaders: &[&str]) {
self.set(
"Uploaders",
uploaders
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(", ")
.as_str(),
);
}
pub fn architecture(&self) -> Option<String> {
self.paragraph.get("Architecture")
}
pub fn set_architecture(&mut self, arch: Option<&str>) {
if let Some(arch) = arch {
self.set("Architecture", arch);
} else {
self.paragraph.remove("Architecture");
}
}
pub fn rules_requires_root(&self) -> Option<bool> {
self.paragraph
.get("Rules-Requires-Root")
.map(|s| match s.to_lowercase().as_str() {
"yes" => true,
"no" => false,
_ => panic!("invalid Rules-Requires-Root value"),
})
}
pub fn set_rules_requires_root(&mut self, requires_root: bool) {
self.set(
"Rules-Requires-Root",
if requires_root { "yes" } else { "no" },
);
}
pub fn testsuite(&self) -> Option<String> {
self.paragraph.get("Testsuite")
}
pub fn set_testsuite(&mut self, testsuite: &str) {
self.set("Testsuite", testsuite);
}
pub fn overlaps_range(&self, range: TextRange) -> bool {
let para_range = self.paragraph.syntax().text_range();
para_range.start() < range.end() && range.start() < para_range.end()
}
pub fn fields_in_range(
&self,
range: TextRange,
) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
self.paragraph.entries().filter(move |entry| {
let entry_range = entry.syntax().text_range();
entry_range.start() < range.end() && range.start() < entry_range.end()
})
}
}
#[cfg(feature = "python-debian")]
impl<'py> pyo3::IntoPyObject<'py> for Source {
type Target = pyo3::PyAny;
type Output = pyo3::Bound<'py, Self::Target>;
type Error = pyo3::PyErr;
fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
self.paragraph.into_pyobject(py)
}
}
#[cfg(feature = "python-debian")]
impl<'py> pyo3::IntoPyObject<'py> for &Source {
type Target = pyo3::PyAny;
type Output = pyo3::Bound<'py, Self::Target>;
type Error = pyo3::PyErr;
fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
(&self.paragraph).into_pyobject(py)
}
}
#[cfg(feature = "python-debian")]
impl<'py> pyo3::FromPyObject<'_, 'py> for Source {
type Error = pyo3::PyErr;
fn extract(ob: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
Ok(Source {
paragraph: ob.extract()?,
parse_mode: ParseMode::Strict,
})
}
}
impl std::fmt::Display for Control {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.deb822.fmt(f)
}
}
impl AstNode for Control {
type Language = deb822_lossless::Lang;
fn can_cast(kind: <Self::Language as rowan::Language>::Kind) -> bool {
Deb822::can_cast(kind)
}
fn cast(syntax: rowan::SyntaxNode<Self::Language>) -> Option<Self> {
Deb822::cast(syntax).map(|deb822| Control {
deb822,
parse_mode: ParseMode::Strict,
})
}
fn syntax(&self) -> &rowan::SyntaxNode<Self::Language> {
self.deb822.syntax()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Binary {
paragraph: Paragraph,
parse_mode: ParseMode,
}
impl From<Binary> for Paragraph {
fn from(b: Binary) -> Self {
b.paragraph
}
}
impl From<Paragraph> for Binary {
fn from(p: Paragraph) -> Self {
Binary {
paragraph: p,
parse_mode: ParseMode::Strict,
}
}
}
#[cfg(feature = "python-debian")]
impl<'py> pyo3::IntoPyObject<'py> for Binary {
type Target = pyo3::PyAny;
type Output = pyo3::Bound<'py, Self::Target>;
type Error = pyo3::PyErr;
fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
self.paragraph.into_pyobject(py)
}
}
#[cfg(feature = "python-debian")]
impl<'py> pyo3::IntoPyObject<'py> for &Binary {
type Target = pyo3::PyAny;
type Output = pyo3::Bound<'py, Self::Target>;
type Error = pyo3::PyErr;
fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
(&self.paragraph).into_pyobject(py)
}
}
#[cfg(feature = "python-debian")]
impl<'py> pyo3::FromPyObject<'_, 'py> for Binary {
type Error = pyo3::PyErr;
fn extract(ob: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result<Self, Self::Error> {
Ok(Binary {
paragraph: ob.extract()?,
parse_mode: ParseMode::Strict,
})
}
}
impl Default for Binary {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for Binary {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.paragraph.fmt(f)
}
}
impl Binary {
fn parse_relations(&self, s: &str) -> Relations {
match self.parse_mode {
ParseMode::Strict => s.parse().unwrap(),
ParseMode::Relaxed => Relations::parse_relaxed(s, false).0,
ParseMode::Substvar => Relations::parse_relaxed(s, true).0,
}
}
pub fn new() -> Self {
Binary {
paragraph: Paragraph::new(),
parse_mode: ParseMode::Strict,
}
}
pub fn as_mut_deb822(&mut self) -> &mut Paragraph {
&mut self.paragraph
}
pub fn as_deb822(&self) -> &Paragraph {
&self.paragraph
}
pub fn wrap_and_sort(
&mut self,
indentation: deb822_lossless::Indentation,
immediate_empty_line: bool,
max_line_length_one_liner: Option<usize>,
) {
self.paragraph = self.paragraph.wrap_and_sort(
indentation,
immediate_empty_line,
max_line_length_one_liner,
None,
Some(&format_field),
);
}
pub fn name(&self) -> Option<String> {
self.paragraph.get("Package")
}
pub fn set_name(&mut self, name: &str) {
self.set("Package", name);
}
pub fn section(&self) -> Option<String> {
self.paragraph.get("Section")
}
pub fn set_section(&mut self, section: Option<&str>) {
if let Some(section) = section {
self.set("Section", section);
} else {
self.paragraph.remove("Section");
}
}
pub fn priority(&self) -> Option<Priority> {
self.paragraph.get("Priority").and_then(|v| v.parse().ok())
}
pub fn set_priority(&mut self, priority: Option<Priority>) {
if let Some(priority) = priority {
self.set("Priority", priority.to_string().as_str());
} else {
self.paragraph.remove("Priority");
}
}
pub fn architecture(&self) -> Option<String> {
self.paragraph.get("Architecture")
}
pub fn set_architecture(&mut self, arch: Option<&str>) {
if let Some(arch) = arch {
self.set("Architecture", arch);
} else {
self.paragraph.remove("Architecture");
}
}
pub fn depends(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Depends")
.map(|s| self.parse_relations(&s))
}
pub fn set_depends(&mut self, depends: Option<&Relations>) {
if let Some(depends) = depends {
self.set("Depends", depends.to_string().as_str());
} else {
self.paragraph.remove("Depends");
}
}
pub fn recommends(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Recommends")
.map(|s| self.parse_relations(&s))
}
pub fn set_recommends(&mut self, recommends: Option<&Relations>) {
if let Some(recommends) = recommends {
self.set("Recommends", recommends.to_string().as_str());
} else {
self.paragraph.remove("Recommends");
}
}
pub fn suggests(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Suggests")
.map(|s| self.parse_relations(&s))
}
pub fn set_suggests(&mut self, suggests: Option<&Relations>) {
if let Some(suggests) = suggests {
self.set("Suggests", suggests.to_string().as_str());
} else {
self.paragraph.remove("Suggests");
}
}
pub fn enhances(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Enhances")
.map(|s| self.parse_relations(&s))
}
pub fn set_enhances(&mut self, enhances: Option<&Relations>) {
if let Some(enhances) = enhances {
self.set("Enhances", enhances.to_string().as_str());
} else {
self.paragraph.remove("Enhances");
}
}
pub fn pre_depends(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Pre-Depends")
.map(|s| self.parse_relations(&s))
}
pub fn set_pre_depends(&mut self, pre_depends: Option<&Relations>) {
if let Some(pre_depends) = pre_depends {
self.set("Pre-Depends", pre_depends.to_string().as_str());
} else {
self.paragraph.remove("Pre-Depends");
}
}
pub fn breaks(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Breaks")
.map(|s| self.parse_relations(&s))
}
pub fn set_breaks(&mut self, breaks: Option<&Relations>) {
if let Some(breaks) = breaks {
self.set("Breaks", breaks.to_string().as_str());
} else {
self.paragraph.remove("Breaks");
}
}
pub fn conflicts(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Conflicts")
.map(|s| self.parse_relations(&s))
}
pub fn set_conflicts(&mut self, conflicts: Option<&Relations>) {
if let Some(conflicts) = conflicts {
self.set("Conflicts", conflicts.to_string().as_str());
} else {
self.paragraph.remove("Conflicts");
}
}
pub fn replaces(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Replaces")
.map(|s| self.parse_relations(&s))
}
pub fn set_replaces(&mut self, replaces: Option<&Relations>) {
if let Some(replaces) = replaces {
self.set("Replaces", replaces.to_string().as_str());
} else {
self.paragraph.remove("Replaces");
}
}
pub fn provides(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Provides")
.map(|s| self.parse_relations(&s))
}
pub fn set_provides(&mut self, provides: Option<&Relations>) {
if let Some(provides) = provides {
self.set("Provides", provides.to_string().as_str());
} else {
self.paragraph.remove("Provides");
}
}
pub fn built_using(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Built-Using")
.map(|s| self.parse_relations(&s))
}
pub fn set_built_using(&mut self, built_using: Option<&Relations>) {
if let Some(built_using) = built_using {
self.set("Built-Using", built_using.to_string().as_str());
} else {
self.paragraph.remove("Built-Using");
}
}
pub fn static_built_using(&self) -> Option<Relations> {
self.paragraph
.get_with_comments("Static-Built-Using")
.map(|s| self.parse_relations(&s))
}
pub fn set_static_built_using(&mut self, static_built_using: Option<&Relations>) {
if let Some(static_built_using) = static_built_using {
self.set(
"Static-Built-Using",
static_built_using.to_string().as_str(),
);
} else {
self.paragraph.remove("Static-Built-Using");
}
}
pub fn multi_arch(&self) -> Option<MultiArch> {
self.paragraph.get("Multi-Arch").map(|s| s.parse().unwrap())
}
pub fn set_multi_arch(&mut self, multi_arch: Option<MultiArch>) {
if let Some(multi_arch) = multi_arch {
self.set("Multi-Arch", multi_arch.to_string().as_str());
} else {
self.paragraph.remove("Multi-Arch");
}
}
pub fn essential(&self) -> bool {
self.paragraph
.get("Essential")
.map(|s| s == "yes")
.unwrap_or(false)
}
pub fn set_essential(&mut self, essential: bool) {
if essential {
self.set("Essential", "yes");
} else {
self.paragraph.remove("Essential");
}
}
pub fn description(&self) -> Option<String> {
self.paragraph.get_multiline("Description")
}
pub fn set_description(&mut self, description: Option<&str>) {
if let Some(description) = description {
self.paragraph.set_with_indent_pattern(
"Description",
description,
Some(&deb822_lossless::IndentPattern::Fixed(1)),
Some(BINARY_FIELD_ORDER),
);
} else {
self.paragraph.remove("Description");
}
}
pub fn homepage(&self) -> Option<url::Url> {
self.paragraph.get("Homepage").and_then(|s| s.parse().ok())
}
pub fn set_homepage(&mut self, url: &url::Url) {
self.set("Homepage", url.as_str());
}
pub fn set(&mut self, key: &str, value: &str) {
self.paragraph
.set_with_field_order(key, value, BINARY_FIELD_ORDER);
}
pub fn get(&self, key: &str) -> Option<String> {
self.paragraph.get(key)
}
pub fn overlaps_range(&self, range: TextRange) -> bool {
let para_range = self.paragraph.syntax().text_range();
para_range.start() < range.end() && range.start() < para_range.end()
}
pub fn fields_in_range(
&self,
range: TextRange,
) -> impl Iterator<Item = deb822_lossless::Entry> + '_ {
self.paragraph.entries().filter(move |entry| {
let entry_range = entry.syntax().text_range();
entry_range.start() < range.end() && range.start() < entry_range.end()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relations::VersionConstraint;
#[test]
fn test_source_set_field_ordering() {
let mut control = Control::new();
let mut source = control.add_source("mypackage");
source.set("Homepage", "https://example.com");
source.set("Build-Depends", "debhelper");
source.set("Standards-Version", "4.5.0");
source.set("Maintainer", "Test <test@example.com>");
let output = source.to_string();
let lines: Vec<&str> = output.lines().collect();
assert!(lines[0].starts_with("Source:"));
let maintainer_pos = lines
.iter()
.position(|l| l.starts_with("Maintainer:"))
.unwrap();
let build_depends_pos = lines
.iter()
.position(|l| l.starts_with("Build-Depends:"))
.unwrap();
let standards_pos = lines
.iter()
.position(|l| l.starts_with("Standards-Version:"))
.unwrap();
let homepage_pos = lines
.iter()
.position(|l| l.starts_with("Homepage:"))
.unwrap();
assert!(maintainer_pos < build_depends_pos);
assert!(build_depends_pos < standards_pos);
assert!(standards_pos < homepage_pos);
}
#[test]
fn test_binary_set_field_ordering() {
let mut control = Control::new();
let mut binary = control.add_binary("mypackage");
binary.set("Description", "A test package");
binary.set("Architecture", "amd64");
binary.set("Depends", "libc6");
binary.set("Section", "utils");
let output = binary.to_string();
let lines: Vec<&str> = output.lines().collect();
assert!(lines[0].starts_with("Package:"));
let arch_pos = lines
.iter()
.position(|l| l.starts_with("Architecture:"))
.unwrap();
let section_pos = lines
.iter()
.position(|l| l.starts_with("Section:"))
.unwrap();
let depends_pos = lines
.iter()
.position(|l| l.starts_with("Depends:"))
.unwrap();
let desc_pos = lines
.iter()
.position(|l| l.starts_with("Description:"))
.unwrap();
assert!(arch_pos < section_pos);
assert!(section_pos < depends_pos);
assert!(depends_pos < desc_pos);
}
#[test]
fn test_source_specific_set_methods_use_field_ordering() {
let mut control = Control::new();
let mut source = control.add_source("mypackage");
source.set_homepage(&"https://example.com".parse().unwrap());
source.set_maintainer("Test <test@example.com>");
source.set_standards_version("4.5.0");
source.set_vcs_git("https://github.com/example/repo");
let output = source.to_string();
let lines: Vec<&str> = output.lines().collect();
let source_pos = lines.iter().position(|l| l.starts_with("Source:")).unwrap();
let maintainer_pos = lines
.iter()
.position(|l| l.starts_with("Maintainer:"))
.unwrap();
let standards_pos = lines
.iter()
.position(|l| l.starts_with("Standards-Version:"))
.unwrap();
let vcs_git_pos = lines
.iter()
.position(|l| l.starts_with("Vcs-Git:"))
.unwrap();
let homepage_pos = lines
.iter()
.position(|l| l.starts_with("Homepage:"))
.unwrap();
assert!(source_pos < maintainer_pos);
assert!(maintainer_pos < standards_pos);
assert!(standards_pos < vcs_git_pos);
assert!(vcs_git_pos < homepage_pos);
}
#[test]
fn test_binary_specific_set_methods_use_field_ordering() {
let mut control = Control::new();
let mut binary = control.add_binary("mypackage");
binary.set_description(Some("A test package"));
binary.set_architecture(Some("amd64"));
let depends = "libc6".parse().unwrap();
binary.set_depends(Some(&depends));
binary.set_section(Some("utils"));
binary.set_priority(Some(Priority::Optional));
let output = binary.to_string();
let lines: Vec<&str> = output.lines().collect();
let package_pos = lines
.iter()
.position(|l| l.starts_with("Package:"))
.unwrap();
let arch_pos = lines
.iter()
.position(|l| l.starts_with("Architecture:"))
.unwrap();
let section_pos = lines
.iter()
.position(|l| l.starts_with("Section:"))
.unwrap();
let priority_pos = lines
.iter()
.position(|l| l.starts_with("Priority:"))
.unwrap();
let depends_pos = lines
.iter()
.position(|l| l.starts_with("Depends:"))
.unwrap();
let desc_pos = lines
.iter()
.position(|l| l.starts_with("Description:"))
.unwrap();
assert!(package_pos < arch_pos);
assert!(arch_pos < section_pos);
assert!(section_pos < priority_pos);
assert!(priority_pos < depends_pos);
assert!(depends_pos < desc_pos);
}
#[test]
fn test_parse() {
let control: Control = r#"Source: foo
Section: libs
Priority: optional
Build-Depends: bar (>= 1.0.0), baz (>= 1.0.0)
Homepage: https://example.com
"#
.parse()
.unwrap();
let source = control.source().unwrap();
assert_eq!(source.name(), Some("foo".to_owned()));
assert_eq!(source.section(), Some("libs".to_owned()));
assert_eq!(source.priority(), Some(super::Priority::Optional));
assert_eq!(
source.homepage(),
Some("https://example.com".parse().unwrap())
);
let bd = source.build_depends().unwrap();
let entries = bd.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
let rel = entries[0].relations().collect::<Vec<_>>().pop().unwrap();
assert_eq!(rel.name(), "bar");
assert_eq!(
rel.version(),
Some((
VersionConstraint::GreaterThanEqual,
"1.0.0".parse().unwrap()
))
);
let rel = entries[1].relations().collect::<Vec<_>>().pop().unwrap();
assert_eq!(rel.name(), "baz");
assert_eq!(
rel.version(),
Some((
VersionConstraint::GreaterThanEqual,
"1.0.0".parse().unwrap()
))
);
}
#[test]
fn test_description() {
let control: Control = r#"Source: foo
Package: foo
Description: this is the short description
And the longer one
.
is on the next lines
"#
.parse()
.unwrap();
let binary = control.binaries().next().unwrap();
assert_eq!(
binary.description(),
Some(
"this is the short description\nAnd the longer one\n.\nis on the next lines"
.to_owned()
)
);
}
#[test]
fn test_set_description_on_package_without_description() {
let control: Control = r#"Source: foo
Package: foo
Architecture: amd64
"#
.parse()
.unwrap();
let mut binary = control.binaries().next().unwrap();
binary.set_description(Some(
"Short description\nLonger description\n.\nAnother line",
));
let output = binary.to_string();
assert_eq!(
binary.description(),
Some("Short description\nLonger description\n.\nAnother line".to_owned())
);
assert_eq!(
output,
"Package: foo\nArchitecture: amd64\nDescription: Short description\n Longer description\n .\n Another line\n"
);
}
#[test]
fn test_as_mut_deb822() {
let mut control = Control::new();
let deb822 = control.as_mut_deb822();
let mut p = deb822.add_paragraph();
p.set("Source", "foo");
assert_eq!(control.source().unwrap().name(), Some("foo".to_owned()));
}
#[test]
fn test_as_deb822() {
let control = Control::new();
let _deb822: &Deb822 = control.as_deb822();
}
#[test]
fn test_set_depends() {
let mut control = Control::new();
let mut binary = control.add_binary("foo");
let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
binary.set_depends(Some(&relations));
}
#[test]
fn test_wrap_and_sort() {
let mut control: Control = r#"Package: blah
Section: libs
Package: foo
Description: this is a
bar
blah
"#
.parse()
.unwrap();
control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), false, None);
let expected = r#"Package: blah
Section: libs
Package: foo
Description: this is a
bar
blah
"#
.to_owned();
assert_eq!(control.to_string(), expected);
}
#[test]
fn test_wrap_and_sort_source() {
let mut control: Control = r#"Source: blah
Depends: foo, bar (<= 1.0.0)
"#
.parse()
.unwrap();
control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), true, None);
let expected = r#"Source: blah
Depends: bar (<= 1.0.0), foo
"#
.to_owned();
assert_eq!(control.to_string(), expected);
}
#[test]
fn test_source_wrap_and_sort() {
let control: Control = r#"Source: blah
Build-Depends: foo, bar (>= 1.0.0)
"#
.parse()
.unwrap();
let mut source = control.source().unwrap();
source.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), true, None);
assert!(source.build_depends().is_some());
}
#[test]
fn test_binary_set_breaks() {
let mut control = Control::new();
let mut binary = control.add_binary("foo");
let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
binary.set_breaks(Some(&relations));
assert!(binary.breaks().is_some());
}
#[test]
fn test_binary_set_pre_depends() {
let mut control = Control::new();
let mut binary = control.add_binary("foo");
let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
binary.set_pre_depends(Some(&relations));
assert!(binary.pre_depends().is_some());
}
#[test]
fn test_binary_set_provides() {
let mut control = Control::new();
let mut binary = control.add_binary("foo");
let relations: Relations = "bar (>= 1.0.0)".parse().unwrap();
binary.set_provides(Some(&relations));
assert!(binary.provides().is_some());
}
#[test]
fn test_source_build_conflicts() {
let control: Control = r#"Source: blah
Build-Conflicts: foo, bar (>= 1.0.0)
"#
.parse()
.unwrap();
let source = control.source().unwrap();
let conflicts = source.build_conflicts();
assert!(conflicts.is_some());
}
#[test]
fn test_source_vcs_svn() {
let control: Control = r#"Source: blah
Vcs-Svn: https://example.com/svn/repo
"#
.parse()
.unwrap();
let source = control.source().unwrap();
assert_eq!(
source.vcs_svn(),
Some("https://example.com/svn/repo".to_string())
);
}
#[test]
fn test_control_from_conversion() {
let deb822_data = r#"Source: test
Section: libs
"#;
let deb822: Deb822 = deb822_data.parse().unwrap();
let control = Control::from(deb822);
assert!(control.source().is_some());
}
#[test]
fn test_fields_in_range() {
let control_text = r#"Source: test-package
Maintainer: Test User <test@example.com>
Build-Depends: debhelper (>= 12)
Package: test-binary
Architecture: any
Depends: ${shlibs:Depends}
Description: Test package
This is a test package
"#;
let control: Control = control_text.parse().unwrap();
let source_start = 0;
let source_end = "Source: test-package".len();
let source_range = TextRange::new((source_start as u32).into(), (source_end as u32).into());
let fields: Vec<_> = control.fields_in_range(source_range).collect();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].key(), Some("Source".to_string()));
let maintainer_start = control_text.find("Maintainer:").unwrap();
let build_depends_end = control_text
.find("Build-Depends: debhelper (>= 12)")
.unwrap()
+ "Build-Depends: debhelper (>= 12)".len();
let multi_range = TextRange::new(
(maintainer_start as u32).into(),
(build_depends_end as u32).into(),
);
let fields: Vec<_> = control.fields_in_range(multi_range).collect();
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].key(), Some("Maintainer".to_string()));
assert_eq!(fields[1].key(), Some("Build-Depends".to_string()));
let cross_para_start = control_text.find("Build-Depends:").unwrap();
let cross_para_end =
control_text.find("Architecture: any").unwrap() + "Architecture: any".len();
let cross_range = TextRange::new(
(cross_para_start as u32).into(),
(cross_para_end as u32).into(),
);
let fields: Vec<_> = control.fields_in_range(cross_range).collect();
assert_eq!(fields.len(), 3); assert_eq!(fields[0].key(), Some("Build-Depends".to_string()));
assert_eq!(fields[1].key(), Some("Package".to_string()));
assert_eq!(fields[2].key(), Some("Architecture".to_string()));
let empty_range = TextRange::new(1000.into(), 1001.into());
let fields: Vec<_> = control.fields_in_range(empty_range).collect();
assert_eq!(fields.len(), 0);
}
#[test]
fn test_source_overlaps_range() {
let control_text = r#"Source: test-package
Maintainer: Test User <test@example.com>
Package: test-binary
Architecture: any
"#;
let control: Control = control_text.parse().unwrap();
let source = control.source().unwrap();
let overlap_range = TextRange::new(10.into(), 30.into());
assert!(source.overlaps_range(overlap_range));
let binary_start = control_text.find("Package:").unwrap();
let no_overlap_range = TextRange::new(
(binary_start as u32).into(),
((binary_start + 20) as u32).into(),
);
assert!(!source.overlaps_range(no_overlap_range));
let partial_overlap = TextRange::new(0.into(), 15.into());
assert!(source.overlaps_range(partial_overlap));
}
#[test]
fn test_source_fields_in_range() {
let control_text = r#"Source: test-package
Maintainer: Test User <test@example.com>
Build-Depends: debhelper (>= 12)
Package: test-binary
"#;
let control: Control = control_text.parse().unwrap();
let source = control.source().unwrap();
let maintainer_start = control_text.find("Maintainer:").unwrap();
let maintainer_end = maintainer_start + "Maintainer: Test User <test@example.com>".len();
let maintainer_range = TextRange::new(
(maintainer_start as u32).into(),
(maintainer_end as u32).into(),
);
let fields: Vec<_> = source.fields_in_range(maintainer_range).collect();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].key(), Some("Maintainer".to_string()));
let all_source_range = TextRange::new(0.into(), 100.into());
let fields: Vec<_> = source.fields_in_range(all_source_range).collect();
assert_eq!(fields.len(), 3); }
#[test]
fn test_binary_overlaps_range() {
let control_text = r#"Source: test-package
Package: test-binary
Architecture: any
Depends: ${shlibs:Depends}
"#;
let control: Control = control_text.parse().unwrap();
let binary = control.binaries().next().unwrap();
let package_start = control_text.find("Package:").unwrap();
let overlap_range = TextRange::new(
(package_start as u32).into(),
((package_start + 30) as u32).into(),
);
assert!(binary.overlaps_range(overlap_range));
let no_overlap_range = TextRange::new(0.into(), 10.into());
assert!(!binary.overlaps_range(no_overlap_range));
}
#[test]
fn test_binary_fields_in_range() {
let control_text = r#"Source: test-package
Package: test-binary
Architecture: any
Depends: ${shlibs:Depends}
Description: Test binary
This is a test binary package
"#;
let control: Control = control_text.parse().unwrap();
let binary = control.binaries().next().unwrap();
let arch_start = control_text.find("Architecture:").unwrap();
let depends_end = control_text.find("Depends: ${shlibs:Depends}").unwrap()
+ "Depends: ${shlibs:Depends}".len();
let range = TextRange::new((arch_start as u32).into(), (depends_end as u32).into());
let fields: Vec<_> = binary.fields_in_range(range).collect();
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].key(), Some("Architecture".to_string()));
assert_eq!(fields[1].key(), Some("Depends".to_string()));
let desc_start = control_text.find("Description:").unwrap();
let partial_range = TextRange::new(
((desc_start + 5) as u32).into(),
((desc_start + 15) as u32).into(),
);
let fields: Vec<_> = binary.fields_in_range(partial_range).collect();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].key(), Some("Description".to_string()));
}
#[test]
fn test_incremental_parsing_use_case() {
let control_text = r#"Source: example
Maintainer: John Doe <john@example.com>
Standards-Version: 4.6.0
Build-Depends: debhelper-compat (= 13)
Package: example-bin
Architecture: all
Depends: ${misc:Depends}
Description: Example package
This is an example.
"#;
let control: Control = control_text.parse().unwrap();
let change_start = control_text.find("Standards-Version:").unwrap();
let change_end = change_start + "Standards-Version: 4.6.0".len();
let change_range = TextRange::new((change_start as u32).into(), (change_end as u32).into());
let affected_fields: Vec<_> = control.fields_in_range(change_range).collect();
assert_eq!(affected_fields.len(), 1);
assert_eq!(
affected_fields[0].key(),
Some("Standards-Version".to_string())
);
for entry in &affected_fields {
let key = entry.key().unwrap();
assert_ne!(key, "Maintainer");
assert_ne!(key, "Build-Depends");
assert_ne!(key, "Architecture");
}
}
#[test]
fn test_positioned_parse_errors() {
let input = "Invalid: field\nBroken field without colon";
let parsed = Control::parse(input);
let positioned_errors = parsed.positioned_errors();
assert!(
!positioned_errors.is_empty(),
"Should have positioned errors"
);
for error in positioned_errors {
let start_offset: u32 = error.range.start().into();
let end_offset: u32 = error.range.end().into();
assert!(!error.message.is_empty());
assert!(start_offset <= end_offset);
assert!(end_offset <= input.len() as u32);
assert!(error.code.is_some());
println!(
"Error at {:?}: {} (code: {:?})",
error.range, error.message, error.code
);
}
let string_errors = parsed.errors();
assert!(!string_errors.is_empty());
assert_eq!(string_errors.len(), positioned_errors.len());
}
#[test]
fn test_sort_binaries_basic() {
let input = r#"Source: foo
Package: libfoo
Architecture: all
Package: libbar
Architecture: all
"#;
let mut control: Control = input.parse().unwrap();
control.sort_binaries(false);
let binaries: Vec<_> = control.binaries().collect();
assert_eq!(binaries.len(), 2);
assert_eq!(binaries[0].name(), Some("libbar".to_string()));
assert_eq!(binaries[1].name(), Some("libfoo".to_string()));
}
#[test]
fn test_sort_binaries_keep_first() {
let input = r#"Source: foo
Package: zzz-first
Architecture: all
Package: libbar
Architecture: all
Package: libaaa
Architecture: all
"#;
let mut control: Control = input.parse().unwrap();
control.sort_binaries(true);
let binaries: Vec<_> = control.binaries().collect();
assert_eq!(binaries.len(), 3);
assert_eq!(binaries[0].name(), Some("zzz-first".to_string()));
assert_eq!(binaries[1].name(), Some("libaaa".to_string()));
assert_eq!(binaries[2].name(), Some("libbar".to_string()));
}
#[test]
fn test_sort_binaries_already_sorted() {
let input = r#"Source: foo
Package: aaa
Architecture: all
Package: bbb
Architecture: all
Package: ccc
Architecture: all
"#;
let mut control: Control = input.parse().unwrap();
control.sort_binaries(false);
let binaries: Vec<_> = control.binaries().collect();
assert_eq!(binaries.len(), 3);
assert_eq!(binaries[0].name(), Some("aaa".to_string()));
assert_eq!(binaries[1].name(), Some("bbb".to_string()));
assert_eq!(binaries[2].name(), Some("ccc".to_string()));
}
#[test]
fn test_sort_binaries_no_binaries() {
let input = r#"Source: foo
Maintainer: test@example.com
"#;
let mut control: Control = input.parse().unwrap();
control.sort_binaries(false);
assert_eq!(control.binaries().count(), 0);
}
#[test]
fn test_sort_binaries_one_binary() {
let input = r#"Source: foo
Package: bar
Architecture: all
"#;
let mut control: Control = input.parse().unwrap();
control.sort_binaries(false);
let binaries: Vec<_> = control.binaries().collect();
assert_eq!(binaries.len(), 1);
assert_eq!(binaries[0].name(), Some("bar".to_string()));
}
#[test]
fn test_sort_binaries_preserves_fields() {
let input = r#"Source: foo
Package: zzz
Architecture: any
Depends: libc6
Description: ZZZ package
Package: aaa
Architecture: all
Depends: ${misc:Depends}
Description: AAA package
"#;
let mut control: Control = input.parse().unwrap();
control.sort_binaries(false);
let binaries: Vec<_> = control.binaries().collect();
assert_eq!(binaries.len(), 2);
assert_eq!(binaries[0].name(), Some("aaa".to_string()));
assert_eq!(binaries[0].architecture(), Some("all".to_string()));
assert_eq!(binaries[0].description(), Some("AAA package".to_string()));
assert_eq!(binaries[1].name(), Some("zzz".to_string()));
assert_eq!(binaries[1].architecture(), Some("any".to_string()));
assert_eq!(binaries[1].description(), Some("ZZZ package".to_string()));
}
#[test]
fn test_remove_binary_basic() {
let mut control = Control::new();
control.add_binary("foo");
assert_eq!(control.binaries().count(), 1);
assert!(control.remove_binary("foo"));
assert_eq!(control.binaries().count(), 0);
}
#[test]
fn test_remove_binary_nonexistent() {
let mut control = Control::new();
control.add_binary("foo");
assert!(!control.remove_binary("bar"));
assert_eq!(control.binaries().count(), 1);
}
#[test]
fn test_remove_binary_multiple() {
let mut control = Control::new();
control.add_binary("foo");
control.add_binary("bar");
control.add_binary("baz");
assert_eq!(control.binaries().count(), 3);
assert!(control.remove_binary("bar"));
assert_eq!(control.binaries().count(), 2);
let names: Vec<_> = control.binaries().map(|b| b.name().unwrap()).collect();
assert_eq!(names, vec!["foo", "baz"]);
}
#[test]
fn test_remove_binary_preserves_source() {
let input = r#"Source: mypackage
Package: foo
Architecture: all
Package: bar
Architecture: all
"#;
let mut control: Control = input.parse().unwrap();
assert!(control.source().is_some());
assert_eq!(control.binaries().count(), 2);
assert!(control.remove_binary("foo"));
assert!(control.source().is_some());
assert_eq!(
control.source().unwrap().name(),
Some("mypackage".to_string())
);
assert_eq!(control.binaries().count(), 1);
assert_eq!(
control.binaries().next().unwrap().name(),
Some("bar".to_string())
);
}
#[test]
fn test_remove_binary_from_parsed() {
let input = r#"Source: test
Package: test-bin
Architecture: any
Depends: libc6
Description: Test binary
Package: test-lib
Architecture: all
Description: Test library
"#;
let mut control: Control = input.parse().unwrap();
assert_eq!(control.binaries().count(), 2);
assert!(control.remove_binary("test-bin"));
let output = control.to_string();
assert!(!output.contains("test-bin"));
assert!(output.contains("test-lib"));
assert!(output.contains("Source: test"));
}
#[test]
fn test_build_depends_preserves_indentation_after_removal() {
let input = r#"Source: acpi-support
Section: admin
Priority: optional
Maintainer: Debian Acpi Team <pkg-acpi-devel@lists.alioth.debian.org>
Build-Depends: debhelper (>= 10), quilt (>= 0.40),
libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config
"#;
let control: Control = input.parse().unwrap();
let mut source = control.source().unwrap();
let mut build_depends = source.build_depends().unwrap();
let mut to_remove = Vec::new();
for (idx, entry) in build_depends.entries().enumerate() {
for relation in entry.relations() {
if relation.name() == "dh-systemd" {
to_remove.push(idx);
break;
}
}
}
for idx in to_remove.into_iter().rev() {
build_depends.remove_entry(idx);
}
source.set_build_depends(&build_depends);
let output = source.to_string();
assert!(
output.contains("Build-Depends: debhelper (>= 10), quilt (>= 0.40),\n libsystemd-dev [linux-any], pkg-config"),
"Expected 4-space indentation to be preserved, but got:\n{}",
output
);
}
#[test]
fn test_build_depends_direct_string_set_loses_indentation() {
let input = r#"Source: acpi-support
Section: admin
Priority: optional
Maintainer: Debian Acpi Team <pkg-acpi-devel@lists.alioth.debian.org>
Build-Depends: debhelper (>= 10), quilt (>= 0.40),
libsystemd-dev [linux-any], dh-systemd (>= 1.5), pkg-config
"#;
let control: Control = input.parse().unwrap();
let mut source = control.source().unwrap();
let mut build_depends = source.build_depends().unwrap();
let mut to_remove = Vec::new();
for (idx, entry) in build_depends.entries().enumerate() {
for relation in entry.relations() {
if relation.name() == "dh-systemd" {
to_remove.push(idx);
break;
}
}
}
for idx in to_remove.into_iter().rev() {
build_depends.remove_entry(idx);
}
source.set("Build-Depends", &build_depends.to_string());
let output = source.to_string();
println!("Output with string set:");
println!("{}", output);
assert!(
output.contains("Build-Depends: debhelper (>= 10), quilt (>= 0.40),\n libsystemd-dev [linux-any], pkg-config"),
"Expected 4-space indentation to be preserved, but got:\n{}",
output
);
}
#[test]
fn test_parse_mode_strict_default() {
let control = Control::new();
assert_eq!(control.parse_mode(), ParseMode::Strict);
let control: Control = "Source: test\n".parse().unwrap();
assert_eq!(control.parse_mode(), ParseMode::Strict);
}
#[test]
fn test_parse_mode_new_with_mode() {
let control_relaxed = Control::new_with_mode(ParseMode::Relaxed);
assert_eq!(control_relaxed.parse_mode(), ParseMode::Relaxed);
let control_substvar = Control::new_with_mode(ParseMode::Substvar);
assert_eq!(control_substvar.parse_mode(), ParseMode::Substvar);
}
#[test]
fn test_relaxed_mode_handles_broken_relations() {
let input = r#"Source: test-package
Build-Depends: debhelper, @@@broken@@@, python3
Package: test-pkg
Depends: libfoo, %%%invalid%%%, libbar
"#;
let (control, _errors) = Control::read_relaxed(input.as_bytes()).unwrap();
assert_eq!(control.parse_mode(), ParseMode::Relaxed);
if let Some(source) = control.source() {
let bd = source.build_depends();
assert!(bd.is_some());
let relations = bd.unwrap();
assert!(relations.len() >= 2); }
for binary in control.binaries() {
let deps = binary.depends();
assert!(deps.is_some());
let relations = deps.unwrap();
assert!(relations.len() >= 2); }
}
#[test]
fn test_substvar_mode_via_parse() {
let input = r#"Source: test-package
Build-Depends: debhelper, ${misc:Depends}
Package: test-pkg
Depends: ${shlibs:Depends}, libfoo
"#;
let (control, _errors) = Control::read_relaxed(input.as_bytes()).unwrap();
if let Some(source) = control.source() {
let bd = source.build_depends();
assert!(bd.is_some());
}
for binary in control.binaries() {
let deps = binary.depends();
assert!(deps.is_some());
}
}
#[test]
#[should_panic]
fn test_strict_mode_panics_on_broken_syntax() {
let input = r#"Source: test-package
Build-Depends: debhelper, @@@broken@@@
"#;
let control: Control = input.parse().unwrap();
if let Some(source) = control.source() {
let _ = source.build_depends();
}
}
#[test]
fn test_from_file_relaxed_sets_relaxed_mode() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
"#;
let (control, _errors) = Control::read_relaxed(input.as_bytes()).unwrap();
assert_eq!(control.parse_mode(), ParseMode::Relaxed);
}
#[test]
fn test_parse_mode_propagates_to_paragraphs() {
let input = r#"Source: test-package
Build-Depends: debhelper, @@@invalid@@@, python3
Package: test-pkg
Depends: libfoo, %%%bad%%%, libbar
"#;
let (control, _) = Control::read_relaxed(input.as_bytes()).unwrap();
if let Some(source) = control.source() {
assert!(source.build_depends().is_some());
}
for binary in control.binaries() {
assert!(binary.depends().is_some());
}
}
#[test]
fn test_preserves_final_newline() {
let input_with_newline = "Source: test-package\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any\n";
let control: Control = input_with_newline.parse().unwrap();
let output = control.to_string();
assert_eq!(output, input_with_newline);
}
#[test]
fn test_preserves_no_final_newline() {
let input_without_newline = "Source: test-package\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any";
let control: Control = input_without_newline.parse().unwrap();
let output = control.to_string();
assert_eq!(output, input_without_newline);
}
#[test]
fn test_final_newline_after_modifications() {
let input = "Source: test-package\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any\n";
let control: Control = input.parse().unwrap();
let mut source = control.source().unwrap();
source.set_section(Some("utils"));
let output = control.to_string();
let expected = "Source: test-package\nSection: utils\nMaintainer: Test <test@example.com>\n\nPackage: test-pkg\nArchitecture: any\n";
assert_eq!(output, expected);
}
#[test]
fn test_source_in_range() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Section: utils
Package: test-pkg
Architecture: any
"#;
let control: Control = input.parse().unwrap();
let source = control.source().unwrap();
let source_range = source.as_deb822().text_range();
let result = control.source_in_range(source_range);
assert!(result.is_some());
assert_eq!(result.unwrap().name(), Some("test-package".to_string()));
let overlap_range = TextRange::new(0.into(), 20.into());
let result = control.source_in_range(overlap_range);
assert!(result.is_some());
assert_eq!(result.unwrap().name(), Some("test-package".to_string()));
let no_overlap_range = TextRange::new(100.into(), 150.into());
let result = control.source_in_range(no_overlap_range);
assert!(result.is_none());
}
#[test]
fn test_binaries_in_range_single() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Package: test-pkg
Architecture: any
Package: another-pkg
Architecture: all
"#;
let control: Control = input.parse().unwrap();
let first_binary = control.binaries().next().unwrap();
let binary_range = first_binary.as_deb822().text_range();
let binaries: Vec<_> = control.binaries_in_range(binary_range).collect();
assert_eq!(binaries.len(), 1);
assert_eq!(binaries[0].name(), Some("test-pkg".to_string()));
}
#[test]
fn test_binaries_in_range_multiple() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Package: test-pkg
Architecture: any
Package: another-pkg
Architecture: all
Package: third-pkg
Architecture: any
"#;
let control: Control = input.parse().unwrap();
let range = TextRange::new(50.into(), 130.into());
let binaries: Vec<_> = control.binaries_in_range(range).collect();
assert!(binaries.len() >= 2);
assert!(binaries
.iter()
.any(|b| b.name() == Some("test-pkg".to_string())));
assert!(binaries
.iter()
.any(|b| b.name() == Some("another-pkg".to_string())));
}
#[test]
fn test_binaries_in_range_none() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Package: test-pkg
Architecture: any
"#;
let control: Control = input.parse().unwrap();
let range = TextRange::new(1000.into(), 2000.into());
let binaries: Vec<_> = control.binaries_in_range(range).collect();
assert_eq!(binaries.len(), 0);
}
#[test]
fn test_binaries_in_range_all() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Package: test-pkg
Architecture: any
Package: another-pkg
Architecture: all
"#;
let control: Control = input.parse().unwrap();
let range = TextRange::new(0.into(), input.len().try_into().unwrap());
let binaries: Vec<_> = control.binaries_in_range(range).collect();
assert_eq!(binaries.len(), 2);
}
#[test]
fn test_source_in_range_partial_overlap() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Package: test-pkg
Architecture: any
"#;
let control: Control = input.parse().unwrap();
let range = TextRange::new(10.into(), 30.into());
let result = control.source_in_range(range);
assert!(result.is_some());
assert_eq!(result.unwrap().name(), Some("test-package".to_string()));
}
#[test]
fn test_wrap_and_sort_with_malformed_relations() {
let input = r#"Source: test-package
Maintainer: Test <test@example.com>
Build-Depends: some invalid relation syntax here
Package: test-pkg
Architecture: any
"#;
let mut control: Control = input.parse().unwrap();
control.wrap_and_sort(deb822_lossless::Indentation::Spaces(2), false, None);
let output = control.to_string();
let expected = r#"Source: test-package
Maintainer: Test <test@example.com>
Build-Depends: some invalid relation syntax here
Package: test-pkg
Architecture: any
"#;
assert_eq!(output, expected);
}
}