debian_analyzer/
control.rs

1//! Tools for working with Debian control files.
2use crate::editor::{Editor, EditorError, FsEditor, GeneratedFile};
3use crate::relations::{ensure_relation, is_relation_implied};
4use deb822_lossless::Paragraph;
5use debian_control::lossless::relations::Relations;
6use std::ops::{Deref, DerefMut};
7use std::path::{Path, PathBuf};
8
9/// Format a description based on summary and long description lines.
10pub fn format_description(summary: &str, long_description: Vec<&str>) -> String {
11    let mut ret = summary.to_string() + "\n";
12    for line in long_description {
13        ret.push(' ');
14        ret.push_str(line);
15        ret.push('\n');
16    }
17    ret
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Copy)]
21/// The type of a control file template.
22pub enum TemplateType {
23    /// A rule in the debian/rules file that generates the control file.
24    Rules,
25
26    /// Generated by gnome-pkg-tools.
27    Gnome,
28
29    /// Generated by pg_buildext.
30    Postgresql,
31
32    /// Generated by a set of files in the debian/control.in directory.
33    Directory,
34
35    /// Generated by cdbs.
36    Cdbs,
37
38    /// Generated by debcargo.
39    Debcargo,
40}
41
42/// Error type for template expansion operations
43#[derive(Debug)]
44pub enum TemplateExpansionError {
45    /// The expansion failed with an error message
46    Failed(String),
47    /// The expand command is missing
48    ExpandCommandMissing(String),
49    /// Unknown templating type encountered
50    UnknownTemplating(PathBuf, Option<PathBuf>),
51    /// A change conflict occurred
52    Conflict(ChangeConflict),
53}
54
55impl From<ChangeConflict> for TemplateExpansionError {
56    fn from(e: ChangeConflict) -> Self {
57        TemplateExpansionError::Conflict(e)
58    }
59}
60
61impl std::fmt::Display for TemplateExpansionError {
62    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
63        match self {
64            TemplateExpansionError::Failed(s) => write!(f, "Failed: {}", s),
65            TemplateExpansionError::ExpandCommandMissing(s) => {
66                write!(f, "Command not found: {}", s)
67            }
68            TemplateExpansionError::UnknownTemplating(p1, p2) => {
69                if let Some(p2) = p2 {
70                    write!(
71                        f,
72                        "Unknown templating: {} -> {}",
73                        p1.display(),
74                        p2.display()
75                    )
76                } else {
77                    write!(f, "Unknown templating: {}", p1.display())
78                }
79            }
80            TemplateExpansionError::Conflict(c) => write!(f, "Conflict: {}", c),
81        }
82    }
83}
84
85impl std::error::Error for TemplateExpansionError {}
86
87/// Run the dh_gnome_clean command.
88///
89/// This needs to do some post-hoc cleaning, since dh_gnome_clean writes various debhelper log
90/// files that should not be checked in.
91///
92/// # Arguments
93/// * `path` - Path to run dh_gnome_clean in
94///
95/// # Errors
96/// Returns an error if:
97/// - Pre-existing .debhelper.log files are found
98/// - No changelog file exists
99/// - The dh_gnome_clean command is not found
100/// - The command fails to execute
101pub fn dh_gnome_clean(path: &std::path::Path) -> Result<(), TemplateExpansionError> {
102    for entry in std::fs::read_dir(path.join("debian")).unwrap().flatten() {
103        if entry
104            .file_name()
105            .to_string_lossy()
106            .ends_with(".debhelper.log")
107        {
108            return Err(TemplateExpansionError::Failed(
109                "pre-existing .debhelper.log files".to_string(),
110            ));
111        }
112    }
113
114    if !path.join("debian/changelog").exists() {
115        return Err(TemplateExpansionError::Failed(
116            "no changelog file".to_string(),
117        ));
118    }
119
120    let result = std::process::Command::new("dh_gnome_clean")
121        .current_dir(path)
122        .output();
123
124    match result {
125        Ok(output) => {
126            if !output.status.success() {
127                let stderr = String::from_utf8_lossy(&output.stderr);
128                return Err(TemplateExpansionError::Failed(stderr.to_string()));
129            }
130        }
131        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
132            return Err(TemplateExpansionError::ExpandCommandMissing(
133                "dh_gnome_clean".to_string(),
134            ));
135        }
136        Err(e) => {
137            return Err(TemplateExpansionError::Failed(e.to_string()));
138        }
139    }
140
141    for entry in std::fs::read_dir(path.join("debian")).unwrap().flatten() {
142        if entry
143            .file_name()
144            .to_string_lossy()
145            .ends_with(".debhelper.log")
146        {
147            std::fs::remove_file(entry.path()).unwrap();
148        }
149    }
150
151    Ok(())
152}
153
154/// Run the 'pg_buildext updatecontrol' command.
155///
156/// # Arguments
157/// * `path` - Path to run pg_buildext updatecontrol in
158///
159/// # Errors
160/// Returns an error if:
161/// - The pg_buildext command is not found
162/// - The command fails to execute
163pub fn pg_buildext_updatecontrol(path: &std::path::Path) -> Result<(), TemplateExpansionError> {
164    let result = std::process::Command::new("pg_buildext")
165        .arg("updatecontrol")
166        .current_dir(path)
167        .output();
168
169    match result {
170        Ok(output) => {
171            if !output.status.success() {
172                let stderr = String::from_utf8_lossy(&output.stderr);
173                return Err(TemplateExpansionError::Failed(stderr.to_string()));
174            }
175        }
176        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
177            return Err(TemplateExpansionError::ExpandCommandMissing(
178                "pg_buildext".to_string(),
179            ));
180        }
181        Err(e) => {
182            return Err(TemplateExpansionError::Failed(e.to_string()));
183        }
184    }
185    Ok(())
186}
187
188/// Expand a control template.
189///
190/// # Arguments
191/// * `template_path` - Path to the control template
192/// * `path` - Path to the control file
193/// * `template_type` - Type of the template
194///
195/// # Returns
196/// Ok if the template was successfully expanded
197fn expand_control_template(
198    template_path: &std::path::Path,
199    path: &std::path::Path,
200    template_type: TemplateType,
201) -> Result<(), TemplateExpansionError> {
202    let package_root = path.parent().unwrap().parent().unwrap();
203    match template_type {
204        TemplateType::Rules => {
205            let path_time = match std::fs::metadata(path) {
206                Ok(metadata) => Some(metadata.modified().unwrap()),
207                Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
208                Err(e) => panic!("Failed to get mtime of {}: {}", path.display(), e),
209            };
210            while let Ok(metadata) = std::fs::metadata(template_path) {
211                if Some(metadata.modified().unwrap()) == path_time {
212                    // Wait until mtime has changed, so that make knows to regenerate.
213                    filetime::set_file_mtime(template_path, filetime::FileTime::now()).unwrap();
214                } else {
215                    break;
216                }
217            }
218            let result = std::process::Command::new("./debian/rules")
219                .arg("debian/control")
220                .current_dir(package_root)
221                .output();
222
223            match result {
224                Ok(output) => {
225                    if !output.status.success() {
226                        let stderr = String::from_utf8_lossy(&output.stderr);
227                        Err(TemplateExpansionError::Failed(format!(
228                            "Exit code {} running ./debian/rules debian/control: {}",
229                            output.status, stderr
230                        )))
231                    } else {
232                        Ok(())
233                    }
234                }
235                Err(e) => Err(TemplateExpansionError::Failed(format!(
236                    "Failed to run ./debian/rules debian/control: {}",
237                    e
238                ))),
239            }
240        }
241        TemplateType::Gnome => dh_gnome_clean(package_root),
242        TemplateType::Postgresql => pg_buildext_updatecontrol(package_root),
243        TemplateType::Cdbs => unreachable!(),
244        TemplateType::Debcargo => unreachable!(),
245        TemplateType::Directory => Err(TemplateExpansionError::UnknownTemplating(
246            path.to_path_buf(),
247            Some(template_path.to_path_buf()),
248        )),
249    }
250}
251
252#[derive(Debug, Clone)]
253struct Deb822Changes(
254    std::collections::HashMap<(String, String), Vec<(String, Option<String>, Option<String>)>>,
255);
256
257impl Deb822Changes {
258    fn new() -> Self {
259        Self(std::collections::HashMap::new())
260    }
261
262    fn insert(
263        &mut self,
264        para_key: (String, String),
265        field: String,
266        old_value: Option<String>,
267        new_value: Option<String>,
268    ) {
269        self.0
270            .entry(para_key)
271            .or_default()
272            .push((field, old_value, new_value));
273    }
274
275    #[allow(dead_code)]
276    fn normalized(&self) -> Vec<((&str, &str), Vec<(&str, Option<&str>, Option<&str>)>)> {
277        let mut ret: Vec<_> = self
278            .0
279            .iter()
280            .map(|(k, v)| {
281                ((k.0.as_str(), k.1.as_str()), {
282                    let mut v: Vec<_> = v
283                        .iter()
284                        .map(|(f, o, n)| (f.as_str(), o.as_deref(), n.as_deref()))
285                        .collect();
286                    v.sort();
287                    v
288                })
289            })
290            .collect();
291        ret.sort_by_key(|(k, _)| *k);
292        ret
293    }
294}
295
296// Update a control file template based on changes to the file itself.
297//
298// # Arguments
299// * `template_path` - Path to the control template
300// * `path` - Path to the control file
301// * `changes` - Changes to apply
302// * `expand_template` - Whether to expand the template after updating it
303//
304// # Returns
305// Ok if the template was successfully updated
306fn update_control_template(
307    template_path: &std::path::Path,
308    template_type: TemplateType,
309    path: &std::path::Path,
310    changes: Deb822Changes,
311    expand_template: bool,
312) -> Result<bool, TemplateExpansionError> {
313    if template_type == TemplateType::Directory {
314        // We can't handle these yet
315        return Err(TemplateExpansionError::UnknownTemplating(
316            path.to_path_buf(),
317            Some(template_path.to_path_buf()),
318        ));
319    }
320
321    let mut template_editor =
322        FsEditor::<deb822_lossless::Deb822>::new(template_path, true, false).unwrap();
323
324    let resolve_conflict = match template_type {
325        TemplateType::Cdbs => Some(resolve_cdbs_template as ResolveDeb822Conflict),
326        _ => None,
327    };
328
329    apply_changes(&mut template_editor, changes.clone(), resolve_conflict)?;
330
331    if !template_editor.has_changed() {
332        // A bit odd, since there were changes to the output file. Anyway.
333        return Ok(false);
334    }
335
336    match template_editor.commit() {
337        Ok(_) => {}
338        Err(e) => return Err(TemplateExpansionError::Failed(e.to_string())),
339    }
340
341    if expand_template {
342        match template_type {
343            TemplateType::Cdbs => {
344                let mut editor =
345                    FsEditor::<deb822_lossless::Deb822>::new(path, true, false).unwrap();
346                apply_changes(&mut editor, changes, None)?;
347                match editor.commit() {
348                    Ok(_) => {}
349                    Err(e) => return Err(TemplateExpansionError::Failed(e.to_string())),
350                }
351            }
352            _ => {
353                expand_control_template(template_path, path, template_type)?;
354            }
355        }
356    }
357
358    Ok(true)
359}
360
361#[derive(Debug, PartialEq, Eq)]
362/// A change conflict.
363pub struct ChangeConflict {
364    /// Paragraph key, i.e. ("Source", "foo")
365    pub para_key: (String, String),
366    /// Field that conflicted
367    pub field: String,
368    /// Old value in the control file
369    pub actual_old_value: Option<String>,
370    /// Old value in the template
371    pub template_old_value: Option<String>,
372    /// New value in the control file
373    pub actual_new_value: Option<String>,
374}
375
376impl std::fmt::Display for ChangeConflict {
377    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
378        write!(
379            f,
380            "{}/{}: {} -> {} (template: {})",
381            self.para_key.0,
382            self.para_key.1,
383            self.actual_old_value.as_deref().unwrap_or(""),
384            self.actual_new_value.as_deref().unwrap_or(""),
385            self.template_old_value.as_deref().unwrap_or("")
386        )
387    }
388}
389
390impl std::error::Error for ChangeConflict {}
391
392type ResolveDeb822Conflict = fn(
393    para_key: (&str, &str),
394    field: &str,
395    actual_old_value: Option<&str>,
396    template_old_value: Option<&str>,
397    actual_new_value: Option<&str>,
398) -> Result<Option<String>, ChangeConflict>;
399
400fn resolve_cdbs_template(
401    para_key: (&str, &str),
402    field: &str,
403    actual_old_value: Option<&str>,
404    template_old_value: Option<&str>,
405    actual_new_value: Option<&str>,
406) -> Result<Option<String>, ChangeConflict> {
407    if para_key.0 == "Source"
408        && field == "Build-Depends"
409        && template_old_value.is_some()
410        && actual_old_value.is_some()
411        && actual_new_value.is_some()
412    {
413        if actual_new_value
414            .unwrap()
415            .contains(actual_old_value.unwrap())
416        {
417            // We're simply adding to the existing list
418            return Ok(Some(
419                actual_new_value
420                    .unwrap()
421                    .replace(actual_old_value.unwrap(), template_old_value.unwrap()),
422            ));
423        } else {
424            let old_rels: Relations = actual_old_value.unwrap().parse().unwrap();
425            let new_rels: Relations = actual_new_value.unwrap().parse().unwrap();
426            let template_old_value = template_old_value.unwrap();
427            let (mut ret, errors) = Relations::parse_relaxed(template_old_value, true);
428            if !errors.is_empty() {
429                log::debug!("Errors parsing template value: {:?}", errors);
430            }
431            for v in new_rels.entries() {
432                if old_rels.entries().any(|r| is_relation_implied(&v, &r)) {
433                    continue;
434                }
435                ensure_relation(&mut ret, v);
436            }
437            return Ok(Some(ret.to_string()));
438        }
439    }
440    Err(ChangeConflict {
441        para_key: (para_key.0.to_string(), para_key.1.to_string()),
442        field: field.to_string(),
443        actual_old_value: actual_old_value.map(|v| v.to_string()),
444        template_old_value: template_old_value.map(|v| v.to_string()),
445        actual_new_value: actual_new_value.map(|s| s.to_string()),
446    })
447}
448
449/// Guess the type for a control template.
450///
451/// # Arguments
452/// * `template_path` - Path to the control template
453/// * `debian_path` - Path to the debian directory
454///
455/// # Returns
456/// Template type; None if unknown
457pub fn guess_template_type(
458    template_path: &std::path::Path,
459    debian_path: Option<&std::path::Path>,
460) -> Option<TemplateType> {
461    // TODO(jelmer): This should use a proper make file parser of some sort..
462    if let Some(debian_path) = debian_path {
463        match std::fs::read(debian_path.join("rules")) {
464            Ok(file) => {
465                for line in file.split(|&c| c == b'\n') {
466                    if line.starts_with(b"debian/control:") {
467                        return Some(TemplateType::Rules);
468                    }
469                    if line.starts_with(b"debian/%: debian/%.in") {
470                        return Some(TemplateType::Rules);
471                    }
472                    if line.starts_with(b"include /usr/share/blends-dev/rules") {
473                        return Some(TemplateType::Rules);
474                    }
475                }
476            }
477            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
478            Err(e) => panic!(
479                "Failed to read {}: {}",
480                debian_path.join("rules").display(),
481                e
482            ),
483        }
484    }
485    match std::fs::read(template_path) {
486        Ok(template) => {
487            let template_str = std::str::from_utf8(&template).unwrap();
488            if template_str.contains("@GNOME_TEAM@") {
489                return Some(TemplateType::Gnome);
490            }
491            if template_str.contains("PGVERSION") {
492                return Some(TemplateType::Postgresql);
493            }
494            if template_str.contains("@cdbs@") {
495                return Some(TemplateType::Cdbs);
496            }
497
498            let control = debian_control::Control::read_relaxed(std::io::Cursor::new(&template))
499                .unwrap()
500                .0;
501
502            let build_depends = control.source().and_then(|s| s.build_depends());
503
504            if build_depends.iter().any(|d| {
505                d.entries()
506                    .any(|e| e.relations().any(|r| r.name() == "gnome-pkg-tools"))
507            }) {
508                return Some(TemplateType::Gnome);
509            }
510
511            if build_depends.iter().any(|d| {
512                d.entries()
513                    .any(|e| e.relations().any(|r| r.name() == "cdbs"))
514            }) {
515                return Some(TemplateType::Cdbs);
516            }
517        }
518        Err(_) if template_path.is_dir() => {
519            return Some(TemplateType::Directory);
520        }
521        Err(e) => panic!("Failed to read {}: {}", template_path.display(), e),
522    }
523    if let Some(debian_path) = debian_path {
524        if debian_path.join("debcargo.toml").exists() {
525            return Some(TemplateType::Debcargo);
526        }
527    }
528    None
529}
530
531/// Apply a set of changes to this deb822 instance.
532///
533/// # Arguments
534/// * `changes` - Changes to apply
535/// * `resolve_conflict` - Callback to resolve conflicts
536fn apply_changes(
537    deb822: &mut deb822_lossless::Deb822,
538    mut changes: Deb822Changes,
539    resolve_conflict: Option<ResolveDeb822Conflict>,
540) -> Result<(), ChangeConflict> {
541    fn default_resolve_conflict(
542        para_key: (&str, &str),
543        field: &str,
544        actual_old_value: Option<&str>,
545        template_old_value: Option<&str>,
546        actual_new_value: Option<&str>,
547    ) -> Result<Option<String>, ChangeConflict> {
548        Err(ChangeConflict {
549            para_key: (para_key.0.to_string(), para_key.1.to_string()),
550            field: field.to_string(),
551            actual_old_value: actual_old_value.map(|v| v.to_string()),
552            template_old_value: template_old_value.map(|v| v.to_string()),
553            actual_new_value: actual_new_value.map(|s| s.to_string()),
554        })
555    }
556
557    let resolve_conflict = resolve_conflict.unwrap_or(default_resolve_conflict);
558
559    for mut paragraph in deb822.paragraphs() {
560        let items: Vec<_> = paragraph.items().collect();
561        for item in items {
562            for (key, old_value, mut new_value) in changes.0.remove(&item).unwrap_or_default() {
563                if paragraph.get(&key) != old_value {
564                    new_value = resolve_conflict(
565                        (&item.0, &item.1),
566                        &key,
567                        old_value.as_deref(),
568                        paragraph.get(&key).as_deref(),
569                        new_value.as_deref(),
570                    )?;
571                }
572                if let Some(new_value) = new_value.as_ref() {
573                    paragraph.set(&key, new_value);
574                } else {
575                    paragraph.remove(&key);
576                }
577            }
578        }
579    }
580    // Add any new paragraphs that weren't processed earlier
581    for (key, p) in changes.0.drain() {
582        let mut paragraph = deb822.add_paragraph();
583        for (field, old_value, mut new_value) in p {
584            if old_value.is_some() {
585                new_value = resolve_conflict(
586                    (&key.0, &key.1),
587                    &field,
588                    old_value.as_deref(),
589                    paragraph.get(&field).as_deref(),
590                    new_value.as_deref(),
591                )?;
592            }
593            if let Some(new_value) = new_value {
594                paragraph.set(&field, &new_value);
595            }
596        }
597    }
598    Ok(())
599}
600
601fn find_template_path(path: &Path) -> Option<PathBuf> {
602    for ext in &["in", "m4"] {
603        let template_path = path.with_extension(ext);
604        if template_path.exists() {
605            return Some(template_path);
606        }
607    }
608    None
609}
610
611/// An editor for a control file that may be generated from a template.
612///
613/// This editor will automatically expand the template if it does not exist.
614/// It will also automatically update the template if the control file is changed.
615///
616/// # Example
617///
618/// ```rust
619/// use std::path::Path;
620/// use debian_analyzer::control::TemplatedControlEditor;
621/// let td = tempfile::tempdir().unwrap();
622/// let mut editor = TemplatedControlEditor::create(td.path().join("control")).unwrap();
623/// editor.add_source("foo").set_architecture(Some("all"));
624/// editor.commit().unwrap();
625/// ```
626pub struct TemplatedControlEditor {
627    /// The primary editor for the control file.
628    primary: FsEditor<debian_control::Control>,
629    /// The template that was used to generate the control file.
630    template: Option<Template>,
631    /// Path to the control file.
632    path: PathBuf,
633    /// Whether the control file itself should not be written to disk.
634    template_only: bool,
635}
636
637impl Deref for TemplatedControlEditor {
638    type Target = debian_control::Control;
639
640    fn deref(&self) -> &Self::Target {
641        &self.primary
642    }
643}
644
645impl DerefMut for TemplatedControlEditor {
646    fn deref_mut(&mut self) -> &mut Self::Target {
647        &mut self.primary
648    }
649}
650
651impl TemplatedControlEditor {
652    /// Create a new control file editor.
653    pub fn create<P: AsRef<Path>>(control_path: P) -> Result<Self, EditorError> {
654        if control_path.as_ref().exists() {
655            return Err(EditorError::IoError(std::io::Error::new(
656                std::io::ErrorKind::AlreadyExists,
657                "Control file already exists",
658            )));
659        }
660        Self::new(control_path, true)
661    }
662
663    /// Return the type of the template used to generate the control file.
664    pub fn template_type(&self) -> Option<TemplateType> {
665        self.template.as_ref().map(|t| t.template_type)
666    }
667
668    /// Normalize field spacing in the control file.
669    ///
670    /// For template-based control files with deb822-style templates (CDBS, Directory),
671    /// this will normalize both the template file and the primary control file.
672    /// For non-deb822 templates (Rules, Gnome, Postgresql, etc.), this returns an error
673    /// since those control files are generated and shouldn't be normalized.
674    /// For files without templates, it normalizes the control file directly.
675    ///
676    /// # Returns
677    /// An error if a template exists but cannot be normalized, or if the template
678    /// is not a deb822-style template.
679    pub fn normalize_field_spacing(&mut self) -> Result<(), EditorError> {
680        let Some(template) = &self.template else {
681            // No template: normalize the control file directly
682            self.primary.as_mut_deb822().normalize_field_spacing();
683            return Ok(());
684        };
685
686        // Check if this is a deb822-style template
687        let is_deb822_template = matches!(
688            template.template_type,
689            TemplateType::Cdbs | TemplateType::Directory
690        );
691
692        if !is_deb822_template {
693            // Non-deb822 template: cannot normalize generated control files
694            return Err(EditorError::GeneratedFile(
695                self.path.clone(),
696                GeneratedFile {
697                    template_path: Some(template.template_path.clone()),
698                    template_type: None,
699                },
700            ));
701        }
702
703        // For deb822-style templates: normalize the template file
704        let mut template_editor =
705            FsEditor::<deb822_lossless::Deb822>::new(&template.template_path, true, false)?;
706        template_editor.normalize_field_spacing();
707        template_editor.commit()?;
708
709        // Also normalize the primary control file
710        self.primary.as_mut_deb822().normalize_field_spacing();
711        Ok(())
712    }
713
714    /// Open an existing control file.
715    pub fn open<P: AsRef<Path>>(control_path: P) -> Result<Self, EditorError> {
716        Self::new(control_path, false)
717    }
718
719    /// Create a new control file editor.
720    pub fn new<P: AsRef<Path>>(control_path: P, allow_missing: bool) -> Result<Self, EditorError> {
721        let path = control_path.as_ref();
722        let (template, template_only) = if !path.exists() {
723            if let Some(template) = Template::find(path) {
724                match template.expand() {
725                    Ok(_) => {}
726                    Err(e) => {
727                        return Err(EditorError::TemplateError(
728                            template.template_path,
729                            e.to_string(),
730                        ))
731                    }
732                }
733                (Some(template), true)
734            } else if !allow_missing {
735                return Err(EditorError::IoError(std::io::Error::new(
736                    std::io::ErrorKind::NotFound,
737                    "No control file or template found",
738                )));
739            } else {
740                (None, false)
741            }
742        } else {
743            (Template::find(path), false)
744        };
745        let primary = FsEditor::<debian_control::Control>::new(path, false, false)?;
746        Ok(Self {
747            path: path.to_path_buf(),
748            primary,
749            template_only,
750            template,
751        })
752    }
753
754    /// Return a dictionary describing the changes since the base.
755    ///
756    /// # Returns
757    /// A dictionary mapping tuples of (kind, name) to list of (field_name, old_value, new_value)
758    fn changes(&self) -> Deb822Changes {
759        let orig = deb822_lossless::Deb822::read_relaxed(self.primary.orig_content().unwrap())
760            .unwrap()
761            .0;
762        let mut changes = Deb822Changes::new();
763
764        fn by_key(
765            ps: impl Iterator<Item = Paragraph>,
766        ) -> std::collections::HashMap<(String, String), Paragraph> {
767            let mut ret = std::collections::HashMap::new();
768            for p in ps {
769                if let Some(s) = p.get("Source") {
770                    ret.insert(("Source".to_string(), s), p);
771                } else if let Some(s) = p.get("Package") {
772                    ret.insert(("Package".to_string(), s), p);
773                } else {
774                    let k = p.items().next().unwrap();
775                    ret.insert(k, p);
776                }
777            }
778            ret
779        }
780
781        let orig_by_key = by_key(orig.paragraphs());
782        let new_by_key = by_key(self.as_deb822().paragraphs());
783        let keys = orig_by_key
784            .keys()
785            .chain(new_by_key.keys())
786            .collect::<std::collections::HashSet<_>>();
787        for key in keys {
788            let old = orig_by_key.get(key);
789            let new = new_by_key.get(key);
790            if old == new {
791                continue;
792            }
793            let fields = std::collections::HashSet::<String>::from_iter(
794                old.iter()
795                    .flat_map(|p| p.keys())
796                    .chain(new.iter().flat_map(|p| p.keys())),
797            );
798            for field in &fields {
799                let old_val = old.and_then(|x| x.get(field));
800                let new_val = new.and_then(|x| x.get(field));
801                if old_val != new_val {
802                    changes.insert(key.clone(), field.to_string(), old_val, new_val);
803                }
804            }
805        }
806        changes
807    }
808
809    /// Commit the changes to the control file and template.
810    pub fn commit(&self) -> Result<Vec<PathBuf>, EditorError> {
811        let mut changed_files: Vec<PathBuf> = vec![];
812        if self.template_only {
813            // Remove the control file if it exists.
814            match std::fs::remove_file(self.path.as_path()) {
815                Ok(_) => {}
816                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
817                Err(e) => return Err(EditorError::IoError(e)),
818            }
819
820            changed_files.push(self.path.clone());
821
822            let template = self
823                .template
824                .as_ref()
825                .expect("template_only implies template");
826
827            // Update the template
828            let changed = match template.update(self.changes(), false) {
829                Ok(changed) => changed,
830                Err(e) => {
831                    return Err(EditorError::TemplateError(
832                        template.template_path.clone(),
833                        e.to_string(),
834                    ))
835                }
836            };
837            if changed {
838                changed_files.push(template.template_path.clone());
839            }
840        } else {
841            match self.primary.commit() {
842                Ok(files) => {
843                    changed_files.extend(files.iter().map(|p| p.to_path_buf()));
844                }
845                Err(EditorError::GeneratedFile(
846                    p,
847                    GeneratedFile {
848                        template_path: tp,
849                        template_type: tt,
850                    },
851                )) => {
852                    if tp.is_none() {
853                        return Err(EditorError::GeneratedFile(
854                            p,
855                            GeneratedFile {
856                                template_path: tp,
857                                template_type: tt,
858                            },
859                        ));
860                    }
861                    let template = if let Some(template) = self.template.as_ref() {
862                        template
863                    } else {
864                        return Err(EditorError::IoError(std::io::Error::new(
865                            std::io::ErrorKind::NotFound,
866                            "No control file or template found",
867                        )));
868                    };
869                    let changes = self.changes();
870                    let changed = match template.update(changes, true) {
871                        Ok(changed) => changed,
872                        Err(e) => {
873                            return Err(EditorError::TemplateError(tp.unwrap(), e.to_string()))
874                        }
875                    };
876                    changed_files = if changed {
877                        vec![tp.as_ref().unwrap().to_path_buf(), p]
878                    } else {
879                        vec![]
880                    };
881                }
882                Err(EditorError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
883                    let template = if let Some(p) = self.template.as_ref() {
884                        p
885                    } else {
886                        return Err(EditorError::IoError(std::io::Error::new(
887                            std::io::ErrorKind::NotFound,
888                            "No control file or template found",
889                        )));
890                    };
891                    let changed = match template.update(self.changes(), !self.template_only) {
892                        Ok(changed) => changed,
893                        Err(e) => {
894                            return Err(EditorError::TemplateError(
895                                template.template_path.clone(),
896                                e.to_string(),
897                            ))
898                        }
899                    };
900                    if changed {
901                        changed_files.push(template.template_path.clone());
902                        changed_files.push(self.path.clone());
903                    }
904                }
905                Err(e) => return Err(e),
906            }
907        }
908
909        Ok(changed_files)
910    }
911}
912
913struct Template {
914    path: PathBuf,
915    template_path: PathBuf,
916    template_type: TemplateType,
917}
918
919impl Template {
920    fn find(path: &Path) -> Option<Self> {
921        let template_path = find_template_path(path)?;
922        let template_type = guess_template_type(&template_path, Some(path.parent().unwrap()))?;
923        Some(Self {
924            path: path.to_path_buf(),
925            template_path,
926            template_type,
927        })
928    }
929
930    fn expand(&self) -> Result<(), TemplateExpansionError> {
931        expand_control_template(&self.template_path, &self.path, self.template_type)
932    }
933
934    fn update(&self, changes: Deb822Changes, expand: bool) -> Result<bool, TemplateExpansionError> {
935        update_control_template(
936            &self.template_path,
937            self.template_type,
938            &self.path,
939            changes,
940            expand,
941        )
942    }
943}
944
945impl Editor<debian_control::Control> for TemplatedControlEditor {
946    fn orig_content(&self) -> Option<&[u8]> {
947        self.primary.orig_content()
948    }
949
950    fn updated_content(&self) -> Option<Vec<u8>> {
951        self.primary.updated_content()
952    }
953
954    fn rewritten_content(&self) -> Option<&[u8]> {
955        self.primary.rewritten_content()
956    }
957
958    fn is_generated(&self) -> bool {
959        self.primary.is_generated()
960    }
961
962    fn commit(&self) -> Result<Vec<std::path::PathBuf>, EditorError> {
963        TemplatedControlEditor::commit(self)
964    }
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970
971    #[test]
972    fn test_format_description() {
973        let summary = "Summary";
974        let long_description = vec!["Long", "Description"];
975        let expected = "Summary\n Long\n Description\n";
976        assert_eq!(format_description(summary, long_description), expected);
977    }
978
979    #[test]
980    fn test_resolve_cdbs_conflicts() {
981        let val = resolve_cdbs_template(
982            ("Source", "libnetsds-perl"),
983            "Build-Depends",
984            Some("debhelper (>= 6), foo"),
985            Some("@cdbs@, debhelper (>= 9)"),
986            Some("debhelper (>= 10), foo"),
987        )
988        .unwrap();
989
990        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
991
992        let val = resolve_cdbs_template(
993            ("Source", "libnetsds-perl"),
994            "Build-Depends",
995            Some("debhelper (>= 6), foo"),
996            Some("@cdbs@, foo"),
997            Some("debhelper (>= 10), foo"),
998        )
999        .unwrap();
1000        assert_eq!(val, Some("@cdbs@, debhelper (>= 10), foo".to_string()));
1001        let val = resolve_cdbs_template(
1002            ("Source", "libnetsds-perl"),
1003            "Build-Depends",
1004            Some("debhelper (>= 6), foo"),
1005            Some("@cdbs@, debhelper (>= 9)"),
1006            Some("debhelper (>= 10), foo"),
1007        )
1008        .unwrap();
1009        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
1010    }
1011
1012    #[test]
1013    fn test_normalize_field_spacing_without_template() {
1014        // Test normalize_field_spacing on a control file without a template
1015        let td = tempfile::tempdir().unwrap();
1016        let control_path = td.path().join("debian").join("control");
1017        std::fs::create_dir_all(control_path.parent().unwrap()).unwrap();
1018        std::fs::write(
1019            &control_path,
1020            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1021        )
1022        .unwrap();
1023
1024        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1025
1026        // Check the original spacing
1027        let original = editor.as_deb822().to_string();
1028        assert_eq!(
1029            original,
1030            "Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n"
1031        );
1032
1033        // Normalize field spacing using the new method
1034        editor.normalize_field_spacing().unwrap();
1035
1036        // Check if normalization worked
1037        let normalized = editor.as_deb822().to_string();
1038        assert_eq!(
1039            normalized,
1040            "Source: test\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
1041        );
1042    }
1043
1044    #[test]
1045    fn test_normalize_field_spacing_with_non_deb822_template() {
1046        // Test that normalize_field_spacing returns an error for non-deb822 templates
1047        let td = tempfile::tempdir().unwrap();
1048        let debian_path = td.path().join("debian");
1049        std::fs::create_dir_all(&debian_path).unwrap();
1050
1051        let control_in_path = debian_path.join("control.in");
1052        let control_path = debian_path.join("control");
1053        let rules_path = debian_path.join("rules");
1054
1055        // Create a Rules-style template
1056        std::fs::write(
1057            &control_in_path,
1058            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1059        )
1060        .unwrap();
1061
1062        // Create a rules file that generates control
1063        std::fs::write(
1064            &rules_path,
1065            b"#!/usr/bin/make -f\n\ndebian/control: debian/control.in\n\tcp $< $@\n",
1066        )
1067        .unwrap();
1068
1069        // Create a generated control file
1070        std::fs::write(
1071            &control_path,
1072            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1073        )
1074        .unwrap();
1075
1076        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1077        assert_eq!(editor.template_type(), Some(TemplateType::Rules));
1078
1079        // Normalize field spacing should fail for non-deb822 templates
1080        let result = editor.normalize_field_spacing();
1081        assert!(result.is_err());
1082
1083        // Verify it's a GeneratedFile error
1084        match result {
1085            Err(EditorError::GeneratedFile(_, _)) => {
1086                // Expected error type
1087            }
1088            _ => panic!("Expected GeneratedFile error"),
1089        }
1090    }
1091
1092    #[test]
1093    fn test_normalize_field_spacing_with_cdbs_template() {
1094        // Test normalize_field_spacing with a CDBS template
1095        // Note: We can't actually expand CDBS templates in tests, so we test that
1096        // the template file itself gets normalized
1097        let td = tempfile::tempdir().unwrap();
1098        let debian_path = td.path().join("debian");
1099        std::fs::create_dir_all(&debian_path).unwrap();
1100
1101        let control_in_path = debian_path.join("control.in");
1102        let control_path = debian_path.join("control");
1103
1104        // Create a CDBS-style template with double spacing
1105        std::fs::write(
1106            &control_in_path,
1107            b"Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n",
1108        )
1109        .unwrap();
1110
1111        // Create a control file (pretending it was generated from the template)
1112        std::fs::write(
1113            &control_path,
1114            b"Source: test\nBuild-Depends: debhelper\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1115        )
1116        .unwrap();
1117
1118        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1119        assert_eq!(editor.template_type(), Some(TemplateType::Cdbs));
1120
1121        // Verify original template has double spacing
1122        let original_template = std::fs::read_to_string(&control_in_path).unwrap();
1123        assert_eq!(
1124            original_template,
1125            "Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
1126        );
1127
1128        // Normalize field spacing - this should normalize both template and primary
1129        editor.normalize_field_spacing().unwrap();
1130
1131        // Check that the template was normalized
1132        let template_content = std::fs::read_to_string(&control_in_path).unwrap();
1133        assert_eq!(
1134            template_content,
1135            "Source: test\nBuild-Depends: @cdbs@\nRecommends: ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
1136        );
1137
1138        // Check that the primary control file was also normalized
1139        let control_content = editor.as_deb822().to_string();
1140        assert_eq!(
1141            control_content,
1142            "Source: test\nBuild-Depends: debhelper\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
1143        );
1144    }
1145
1146    mod guess_template_type {
1147
1148        #[test]
1149        fn test_rules_generates_control() {
1150            let td = tempfile::tempdir().unwrap();
1151            std::fs::create_dir(td.path().join("debian")).unwrap();
1152            std::fs::write(
1153                td.path().join("debian/rules"),
1154                r#"%:
1155	dh $@
1156
1157debian/control: debian/control.in
1158	cp $@ $<
1159"#,
1160            )
1161            .unwrap();
1162            assert_eq!(
1163                super::guess_template_type(
1164                    &td.path().join("debian/control.in"),
1165                    Some(&td.path().join("debian"))
1166                ),
1167                Some(super::TemplateType::Rules)
1168            );
1169        }
1170
1171        #[test]
1172        fn test_rules_generates_control_percent() {
1173            let td = tempfile::tempdir().unwrap();
1174            std::fs::create_dir(td.path().join("debian")).unwrap();
1175            std::fs::write(
1176                td.path().join("debian/rules"),
1177                r#"%:
1178	dh $@
1179
1180debian/%: debian/%.in
1181	cp $@ $<
1182"#,
1183            )
1184            .unwrap();
1185            assert_eq!(
1186                super::guess_template_type(
1187                    &td.path().join("debian/control.in"),
1188                    Some(&td.path().join("debian"))
1189                ),
1190                Some(super::TemplateType::Rules)
1191            );
1192        }
1193
1194        #[test]
1195        fn test_rules_generates_control_blends() {
1196            let td = tempfile::tempdir().unwrap();
1197            std::fs::create_dir(td.path().join("debian")).unwrap();
1198            std::fs::write(
1199                td.path().join("debian/rules"),
1200                r#"%:
1201	dh $@
1202
1203include /usr/share/blends-dev/rules
1204"#,
1205            )
1206            .unwrap();
1207            assert_eq!(
1208                super::guess_template_type(
1209                    &td.path().join("debian/control.stub"),
1210                    Some(&td.path().join("debian"))
1211                ),
1212                Some(super::TemplateType::Rules)
1213            );
1214        }
1215
1216        #[test]
1217        fn test_empty_template() {
1218            let td = tempfile::tempdir().unwrap();
1219            std::fs::create_dir(td.path().join("debian")).unwrap();
1220            // No paragraph
1221            std::fs::write(td.path().join("debian/control.in"), "").unwrap();
1222
1223            assert_eq!(
1224                None,
1225                super::guess_template_type(
1226                    &td.path().join("debian/control.in"),
1227                    Some(&td.path().join("debian"))
1228                )
1229            );
1230        }
1231
1232        #[test]
1233        fn test_build_depends_cdbs() {
1234            let td = tempfile::tempdir().unwrap();
1235            std::fs::create_dir(td.path().join("debian")).unwrap();
1236            std::fs::write(
1237                td.path().join("debian/control.in"),
1238                r#"Source: blah
1239Build-Depends: cdbs
1240Vcs-Git: file://
1241
1242Package: bar
1243"#,
1244            )
1245            .unwrap();
1246            assert_eq!(
1247                Some(super::TemplateType::Cdbs),
1248                super::guess_template_type(
1249                    &td.path().join("debian/control.in"),
1250                    Some(&td.path().join("debian"))
1251                )
1252            );
1253        }
1254
1255        #[test]
1256        fn test_no_build_depends() {
1257            let td = tempfile::tempdir().unwrap();
1258            std::fs::create_dir(td.path().join("debian")).unwrap();
1259            std::fs::write(
1260                td.path().join("debian/control.in"),
1261                r#"Source: blah
1262Vcs-Git: file://
1263
1264Package: bar
1265"#,
1266            )
1267            .unwrap();
1268            assert_eq!(
1269                None,
1270                super::guess_template_type(
1271                    &td.path().join("debian/control.in"),
1272                    Some(&td.path().join("debian"))
1273                )
1274            );
1275        }
1276
1277        #[test]
1278        fn test_gnome() {
1279            let td = tempfile::tempdir().unwrap();
1280            std::fs::create_dir(td.path().join("debian")).unwrap();
1281            std::fs::write(
1282                td.path().join("debian/control.in"),
1283                r#"Foo @GNOME_TEAM@
1284"#,
1285            )
1286            .unwrap();
1287            assert_eq!(
1288                Some(super::TemplateType::Gnome),
1289                super::guess_template_type(
1290                    &td.path().join("debian/control.in"),
1291                    Some(&td.path().join("debian"))
1292                )
1293            );
1294        }
1295
1296        #[test]
1297        fn test_gnome_build_depends() {
1298            let td = tempfile::tempdir().unwrap();
1299            std::fs::create_dir(td.path().join("debian")).unwrap();
1300            std::fs::write(
1301                td.path().join("debian/control.in"),
1302                r#"Source: blah
1303Build-Depends: gnome-pkg-tools, libc6-dev
1304"#,
1305            )
1306            .unwrap();
1307            assert_eq!(
1308                Some(super::TemplateType::Gnome),
1309                super::guess_template_type(
1310                    &td.path().join("debian/control.in"),
1311                    Some(&td.path().join("debian"))
1312                )
1313            );
1314        }
1315
1316        #[test]
1317        fn test_cdbs() {
1318            let td = tempfile::tempdir().unwrap();
1319            std::fs::create_dir(td.path().join("debian")).unwrap();
1320            std::fs::write(
1321                td.path().join("debian/control.in"),
1322                r#"Source: blah
1323Build-Depends: debhelper, cdbs
1324"#,
1325            )
1326            .unwrap();
1327            assert_eq!(
1328                Some(super::TemplateType::Cdbs),
1329                super::guess_template_type(
1330                    &td.path().join("debian/control.in"),
1331                    Some(&td.path().join("debian"))
1332                )
1333            );
1334        }
1335
1336        #[test]
1337        fn test_multiple_paragraphs() {
1338            let td = tempfile::tempdir().unwrap();
1339            std::fs::create_dir(td.path().join("debian")).unwrap();
1340            std::fs::write(
1341                td.path().join("debian/control.in"),
1342                r#"Source: blah
1343Build-Depends: debhelper, cdbs
1344
1345Package: foo
1346"#,
1347            )
1348            .unwrap();
1349            assert_eq!(
1350                Some(super::TemplateType::Cdbs),
1351                super::guess_template_type(
1352                    &td.path().join("debian/control.in"),
1353                    Some(&td.path().join("debian"))
1354                )
1355            );
1356        }
1357
1358        #[test]
1359        fn test_directory() {
1360            let td = tempfile::tempdir().unwrap();
1361            std::fs::create_dir(td.path().join("debian")).unwrap();
1362            std::fs::create_dir(td.path().join("debian/control.in")).unwrap();
1363            assert_eq!(
1364                Some(super::TemplateType::Directory),
1365                super::guess_template_type(
1366                    &td.path().join("debian/control.in"),
1367                    Some(&td.path().join("debian"))
1368                )
1369            );
1370        }
1371
1372        #[test]
1373        fn test_debcargo() {
1374            let td = tempfile::tempdir().unwrap();
1375            std::fs::create_dir(td.path().join("debian")).unwrap();
1376            std::fs::write(
1377                td.path().join("debian/control.in"),
1378                r#"Source: blah
1379Build-Depends: bar
1380"#,
1381            )
1382            .unwrap();
1383            std::fs::write(
1384                td.path().join("debian/debcargo.toml"),
1385                r#"maintainer = Joe Example <joe@example.com>
1386"#,
1387            )
1388            .unwrap();
1389            assert_eq!(
1390                Some(super::TemplateType::Debcargo),
1391                super::guess_template_type(
1392                    &td.path().join("debian/control.in"),
1393                    Some(&td.path().join("debian"))
1394                )
1395            );
1396        }
1397    }
1398
1399    #[test]
1400    fn test_postgresql() {
1401        let td = tempfile::tempdir().unwrap();
1402        std::fs::create_dir(td.path().join("debian")).unwrap();
1403        std::fs::write(
1404            td.path().join("debian/control.in"),
1405            r#"Source: blah
1406Build-Depends: bar, postgresql
1407
1408Package: foo-PGVERSION
1409"#,
1410        )
1411        .unwrap();
1412        assert_eq!(
1413            Some(super::TemplateType::Postgresql),
1414            super::guess_template_type(
1415                &td.path().join("debian/control.in"),
1416                Some(&td.path().join("debian"))
1417            )
1418        );
1419    }
1420
1421    #[test]
1422    fn test_apply_changes() {
1423        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1424Build-Depends: debhelper (>= 6), foo
1425
1426Package: bar
1427"#
1428        .parse()
1429        .unwrap();
1430
1431        let mut changes = Deb822Changes(std::collections::HashMap::new());
1432        changes.0.insert(
1433            ("Source".to_string(), "blah".to_string()),
1434            vec![(
1435                "Build-Depends".to_string(),
1436                Some("debhelper (>= 6), foo".to_string()),
1437                Some("debhelper (>= 10), foo".to_string()),
1438            )],
1439        );
1440
1441        super::apply_changes(&mut deb822, changes, None).unwrap();
1442
1443        assert_eq!(
1444            deb822.to_string(),
1445            r#"Source: blah
1446Build-Depends: debhelper (>= 10), foo
1447
1448Package: bar
1449"#
1450        );
1451    }
1452
1453    #[test]
1454    fn test_apply_changes_new_paragraph() {
1455        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1456Build-Depends: debhelper (>= 6), foo
1457
1458Package: bar
1459"#
1460        .parse()
1461        .unwrap();
1462
1463        let mut changes = Deb822Changes(std::collections::HashMap::new());
1464        changes.0.insert(
1465            ("Source".to_string(), "blah".to_string()),
1466            vec![(
1467                "Build-Depends".to_string(),
1468                Some("debhelper (>= 6), foo".to_string()),
1469                Some("debhelper (>= 10), foo".to_string()),
1470            )],
1471        );
1472        changes.0.insert(
1473            ("Package".to_string(), "blah2".to_string()),
1474            vec![
1475                ("Package".to_string(), None, Some("blah2".to_string())),
1476                (
1477                    "Description".to_string(),
1478                    None,
1479                    Some("Some package".to_string()),
1480                ),
1481            ],
1482        );
1483
1484        super::apply_changes(&mut deb822, changes, None).unwrap();
1485
1486        assert_eq!(
1487            deb822.to_string(),
1488            r#"Source: blah
1489Build-Depends: debhelper (>= 10), foo
1490
1491Package: bar
1492
1493Package: blah2
1494Description: Some package
1495"#
1496        );
1497    }
1498
1499    #[test]
1500    fn test_apply_changes_conflict() {
1501        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1502Build-Depends: debhelper (>= 6), foo
1503
1504Package: bar
1505"#
1506        .parse()
1507        .unwrap();
1508
1509        let mut changes = Deb822Changes(std::collections::HashMap::new());
1510        changes.0.insert(
1511            ("Source".to_string(), "blah".to_string()),
1512            vec![(
1513                "Build-Depends".to_string(),
1514                Some("debhelper (>= 7), foo".to_string()),
1515                Some("debhelper (>= 10), foo".to_string()),
1516            )],
1517        );
1518
1519        let result = super::apply_changes(&mut deb822, changes, None);
1520        assert!(result.is_err());
1521        let err = result.unwrap_err();
1522        assert_eq!(
1523            err,
1524            ChangeConflict {
1525                para_key: ("Source".to_string(), "blah".to_string()),
1526                field: "Build-Depends".to_string(),
1527                actual_old_value: Some("debhelper (>= 7), foo".to_string()),
1528                template_old_value: Some("debhelper (>= 6), foo".to_string()),
1529                actual_new_value: Some("debhelper (>= 10), foo".to_string()),
1530            }
1531        );
1532    }
1533
1534    #[test]
1535    fn test_apply_changes_resolve_conflict() {
1536        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1537Build-Depends: debhelper (>= 6), foo
1538
1539Package: bar
1540"#
1541        .parse()
1542        .unwrap();
1543
1544        let mut changes = Deb822Changes(std::collections::HashMap::new());
1545        changes.0.insert(
1546            ("Source".to_string(), "blah".to_string()),
1547            vec![(
1548                "Build-Depends".to_string(),
1549                Some("debhelper (>= 7), foo".to_string()),
1550                Some("debhelper (>= 10), foo".to_string()),
1551            )],
1552        );
1553
1554        let result = super::apply_changes(&mut deb822, changes, Some(|_, _, _, _, _| Ok(None)));
1555        assert!(result.is_ok());
1556        assert_eq!(
1557            deb822.to_string(),
1558            r#"Source: blah
1559
1560Package: bar
1561"#
1562        );
1563    }
1564
1565    mod control_editor {
1566        #[test]
1567        fn test_do_not_edit() {
1568            let td = tempfile::tempdir().unwrap();
1569            std::fs::create_dir(td.path().join("debian")).unwrap();
1570            std::fs::write(
1571                td.path().join("debian/control"),
1572                r#"# DO NOT EDIT
1573# This file was generated by blah
1574
1575Source: blah
1576Testsuite: autopkgtest
1577
1578"#,
1579            )
1580            .unwrap();
1581            let editor =
1582                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1583            editor.source().unwrap().set_name("foo");
1584            let changes = editor.changes();
1585            assert_eq!(
1586                changes.normalized(),
1587                vec![
1588                    (
1589                        ("Source", "blah"),
1590                        vec![
1591                            ("Source", Some("blah"), None),
1592                            ("Testsuite", Some("autopkgtest"), None),
1593                        ]
1594                    ),
1595                    (
1596                        ("Source", "foo"),
1597                        vec![
1598                            ("Source", None, Some("foo")),
1599                            ("Testsuite", None, Some("autopkgtest"))
1600                        ]
1601                    )
1602                ]
1603            );
1604            assert!(matches!(
1605                editor.commit().unwrap_err(),
1606                super::EditorError::GeneratedFile(_, _)
1607            ));
1608        }
1609
1610        #[test]
1611        fn test_add_binary() {
1612            let td = tempfile::tempdir().unwrap();
1613            std::fs::create_dir(td.path().join("debian")).unwrap();
1614            std::fs::write(
1615                td.path().join("debian/control"),
1616                r#"Source: blah
1617Testsuite: autopkgtest
1618
1619Package: blah
1620Description: Some description
1621 And there are more lines
1622 And more lines
1623"#,
1624            )
1625            .unwrap();
1626            let mut editor =
1627                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1628            let mut binary = editor.add_binary("foo");
1629            binary.set_description(Some("A new package foo"));
1630            let paths = editor.commit().unwrap();
1631            assert_eq!(paths.len(), 1);
1632        }
1633
1634        #[test]
1635        fn test_list_binaries() {
1636            let td = tempfile::tempdir().unwrap();
1637            std::fs::create_dir(td.path().join("debian")).unwrap();
1638            std::fs::write(
1639                td.path().join("debian/control"),
1640                r#"Source: blah
1641Testsuite: autopkgtest
1642
1643Package: blah
1644Description: Some description
1645 And there are more lines
1646 And more lines
1647"#,
1648            )
1649            .unwrap();
1650            let editor =
1651                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1652            let binaries = editor.binaries().collect::<Vec<_>>();
1653            assert_eq!(binaries.len(), 1);
1654            assert_eq!(binaries[0].name().as_deref(), Some("blah"));
1655            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
1656        }
1657
1658        #[test]
1659        fn test_no_source() {
1660            let td = tempfile::tempdir().unwrap();
1661            std::fs::create_dir(td.path().join("debian")).unwrap();
1662            std::fs::write(
1663                td.path().join("debian/control"),
1664                r#"Package: blah
1665Testsuite: autopkgtest
1666
1667Package: bar
1668"#,
1669            )
1670            .unwrap();
1671            let editor =
1672                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1673            assert!(editor.source().is_none());
1674        }
1675
1676        #[test]
1677        fn test_create() {
1678            let td = tempfile::tempdir().unwrap();
1679            std::fs::create_dir(td.path().join("debian")).unwrap();
1680            let mut editor =
1681                super::TemplatedControlEditor::create(td.path().join("debian/control")).unwrap();
1682            editor.add_source("foo");
1683            assert_eq!(
1684                r#"Source: foo
1685"#,
1686                editor.as_deb822().to_string()
1687            );
1688        }
1689
1690        #[test]
1691        fn test_do_not_edit_no_change() {
1692            let td = tempfile::tempdir().unwrap();
1693            std::fs::create_dir(td.path().join("debian")).unwrap();
1694            std::fs::write(
1695                td.path().join("debian/control"),
1696                r#"# DO NOT EDIT
1697# This file was generated by blah
1698
1699Source: blah
1700Testsuite: autopkgtest
1701
1702"#,
1703            )
1704            .unwrap();
1705            let editor =
1706                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1707            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
1708        }
1709
1710        #[test]
1711        fn test_unpreservable() {
1712            let td = tempfile::tempdir().unwrap();
1713            std::fs::create_dir(td.path().join("debian")).unwrap();
1714            std::fs::write(
1715                td.path().join("debian/control"),
1716                r#"Source: blah
1717# A comment
1718Testsuite: autopkgtest
1719
1720"#,
1721            )
1722            .unwrap();
1723            let editor =
1724                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1725            editor
1726                .source()
1727                .unwrap()
1728                .as_mut_deb822()
1729                .set("NewField", "New Field");
1730
1731            editor.commit().unwrap();
1732
1733            assert_eq!(
1734                r#"Source: blah
1735# A comment
1736Testsuite: autopkgtest
1737NewField: New Field
1738
1739"#,
1740                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1741            );
1742        }
1743
1744        #[test]
1745        fn test_modify_source() {
1746            let td = tempfile::tempdir().unwrap();
1747            std::fs::create_dir(td.path().join("debian")).unwrap();
1748            std::fs::write(
1749                td.path().join("debian/control"),
1750                r#"Source: blah
1751Testsuite: autopkgtest
1752"#,
1753            )
1754            .unwrap();
1755
1756            let editor =
1757                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1758            editor
1759                .source()
1760                .unwrap()
1761                .as_mut_deb822()
1762                .set("XS-Vcs-Git", "git://github.com/example/example");
1763
1764            editor.commit().unwrap();
1765
1766            assert_eq!(
1767                r#"Source: blah
1768Testsuite: autopkgtest
1769XS-Vcs-Git: git://github.com/example/example
1770"#,
1771                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1772            );
1773        }
1774
1775        /*
1776                #[test]
1777                fn test_wrap_and_sort() {
1778                    let td = tempfile::tempdir().unwrap();
1779                    std::fs::create_dir(td.path().join("debian")).unwrap();
1780
1781                    std::fs::write(
1782                        td.path().join("debian/control"),
1783                        r#"Source: blah
1784        Testsuite: autopkgtest
1785        Depends: package3, package2
1786
1787        Package: libblah
1788        Section: extra
1789        "#,
1790                    )
1791                    .unwrap();
1792
1793                    let mut editor =
1794                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1795                    editor.wrap_and_sort(trailing_comma = true).unwrap();
1796
1797                    editor.commit().unwrap();
1798
1799                    assert_eq!(
1800                        r#"Source: blah
1801        Testsuite: autopkgtest
1802        Depends: package2, package3,
1803
1804        Package: libblah
1805        Section: extra
1806        "#,
1807                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1808                    );
1809                }
1810
1811                #[test]
1812                fn test_sort_binaries() {
1813                    let td = tempfile::tempdir().unwrap();
1814                    std::fs::create_dir(td.path().join("debian")).unwrap();
1815                    std::fs::write(
1816                        td.path().join("debian/control"),
1817                        r#"Source: blah
1818        Source: blah
1819        Testsuite: autopkgtest
1820        Depends: package3, package2
1821
1822        Package: libfoo
1823        Section: web
1824
1825        Package: libblah
1826        Section: extra
1827        "#,
1828                    )
1829                    .unwrap();
1830
1831                    let mut editor =
1832                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1833
1834                    editor.sort_binary_packages().unwrap();
1835
1836                    editor.commit().unwrap();
1837
1838                    assert_eq!(
1839                        r#"Source: blah
1840        Testsuite: autopkgtest
1841        Depends: package3, package2
1842
1843        Package: libblah
1844        Section: extra
1845
1846        Package: libfoo
1847        Section: web
1848        "#,
1849                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1850                    );
1851                }
1852
1853            */
1854
1855        #[test]
1856        fn test_modify_binary() {
1857            let td = tempfile::tempdir().unwrap();
1858            std::fs::create_dir(td.path().join("debian")).unwrap();
1859            std::fs::write(
1860                td.path().join("debian/control"),
1861                r#"Source: blah
1862Testsuite: autopkgtest
1863
1864Package: libblah
1865Section: extra
1866"#,
1867            )
1868            .unwrap();
1869
1870            let editor =
1871                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1872            let mut binary = editor
1873                .binaries()
1874                .find(|b| b.name().as_deref() == Some("libblah"))
1875                .unwrap();
1876            binary.set_architecture(Some("all"));
1877
1878            editor.commit().unwrap();
1879
1880            assert_eq!(
1881                r#"Source: blah
1882Testsuite: autopkgtest
1883
1884Package: libblah
1885Architecture: all
1886Section: extra
1887"#,
1888                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1889            );
1890        }
1891
1892        #[test]
1893        fn test_doesnt_strip_whitespace() {
1894            let td = tempfile::tempdir().unwrap();
1895            std::fs::create_dir(td.path().join("debian")).unwrap();
1896            std::fs::write(
1897                td.path().join("debian/control"),
1898                r#"Source: blah
1899Testsuite: autopkgtest
1900
1901"#,
1902            )
1903            .unwrap();
1904            let editor =
1905                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1906            editor.commit().unwrap();
1907
1908            assert_eq!(
1909                r#"Source: blah
1910Testsuite: autopkgtest
1911
1912"#,
1913                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1914            );
1915        }
1916
1917        #[cfg(unix)]
1918        #[test]
1919        fn test_update_template() {
1920            use std::os::unix::fs::PermissionsExt;
1921            let td = tempfile::tempdir().unwrap();
1922            std::fs::create_dir(td.path().join("debian")).unwrap();
1923            std::fs::write(
1924                td.path().join("debian/control"),
1925                r#"# DO NOT EDIT
1926# This file was generated by blah
1927
1928Source: blah
1929Testsuite: autopkgtest
1930Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
1931
1932"#,
1933            )
1934            .unwrap();
1935            std::fs::write(
1936                td.path().join("debian/control.in"),
1937                r#"Source: blah
1938Testsuite: autopkgtest
1939Uploaders: @lintian-brush-test@
1940
1941"#,
1942            )
1943            .unwrap();
1944            std::fs::write(
1945                td.path().join("debian/rules"),
1946                r#"#!/usr/bin/make -f
1947
1948debian/control: debian/control.in
1949	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
1950"#,
1951            )
1952            .unwrap();
1953            // Make debian/rules executable
1954            std::fs::set_permissions(
1955                td.path().join("debian/rules"),
1956                std::fs::Permissions::from_mode(0o755),
1957            )
1958            .unwrap();
1959
1960            let editor =
1961                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1962            editor
1963                .source()
1964                .unwrap()
1965                .as_mut_deb822()
1966                .set("Testsuite", "autopkgtest8");
1967
1968            assert_eq!(
1969                editor.commit().unwrap(),
1970                vec![
1971                    td.path().join("debian/control.in"),
1972                    td.path().join("debian/control")
1973                ]
1974            );
1975
1976            assert_eq!(
1977                r#"Source: blah
1978Testsuite: autopkgtest8
1979Uploaders: @lintian-brush-test@
1980
1981"#,
1982                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
1983            );
1984
1985            assert_eq!(
1986                r#"Source: blah
1987Testsuite: autopkgtest8
1988Uploaders: testvalue
1989
1990"#,
1991                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1992            );
1993        }
1994
1995        #[cfg(unix)]
1996        #[test]
1997        fn test_update_template_only() {
1998            use std::os::unix::fs::PermissionsExt;
1999            let td = tempfile::tempdir().unwrap();
2000            std::fs::create_dir(td.path().join("debian")).unwrap();
2001            std::fs::write(
2002                td.path().join("debian/control.in"),
2003                r#"Source: blah
2004Testsuite: autopkgtest
2005Uploaders: @lintian-brush-test@
2006
2007"#,
2008            )
2009            .unwrap();
2010            std::fs::write(
2011                td.path().join("debian/rules"),
2012                r#"#!/usr/bin/make -f
2013
2014debian/control: debian/control.in
2015	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
2016"#,
2017            )
2018            .unwrap();
2019
2020            std::fs::set_permissions(
2021                td.path().join("debian/rules"),
2022                std::fs::Permissions::from_mode(0o755),
2023            )
2024            .unwrap();
2025
2026            let editor =
2027                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2028            editor
2029                .source()
2030                .unwrap()
2031                .as_mut_deb822()
2032                .set("Testsuite", "autopkgtest8");
2033
2034            editor.commit().unwrap();
2035
2036            assert_eq!(
2037                r#"Source: blah
2038Testsuite: autopkgtest8
2039Uploaders: @lintian-brush-test@
2040
2041"#,
2042                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2043            );
2044
2045            assert!(!td.path().join("debian/control").exists());
2046        }
2047
2048        #[cfg(unix)]
2049        #[test]
2050        fn test_update_template_invalid_tokens() {
2051            use std::os::unix::fs::PermissionsExt;
2052            let td = tempfile::tempdir().unwrap();
2053            std::fs::create_dir(td.path().join("debian")).unwrap();
2054            std::fs::write(
2055                td.path().join("debian/control"),
2056                r#"# DO NOT EDIT
2057# This file was generated by blah
2058
2059Source: blah
2060Testsuite: autopkgtest
2061Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
2062"#,
2063            )
2064            .unwrap();
2065            std::fs::write(
2066                td.path().join("debian/control.in"),
2067                r#"Source: blah
2068Testsuite: autopkgtest
2069@OTHERSTUFF@
2070"#,
2071            )
2072            .unwrap();
2073
2074            std::fs::write(
2075                td.path().join("debian/rules"),
2076                r#"#!/usr/bin/make -f
2077
2078debian/control: debian/control.in
2079	sed -e 's/@OTHERSTUFF@/Vcs-Git: example.com/' < $< > $@
2080"#,
2081            )
2082            .unwrap();
2083
2084            std::fs::set_permissions(
2085                td.path().join("debian/rules"),
2086                std::fs::Permissions::from_mode(0o755),
2087            )
2088            .unwrap();
2089
2090            let editor =
2091                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2092            editor
2093                .source()
2094                .unwrap()
2095                .as_mut_deb822()
2096                .set("Testsuite", "autopkgtest8");
2097            editor.commit().unwrap();
2098
2099            assert_eq!(
2100                r#"Source: blah
2101Testsuite: autopkgtest8
2102@OTHERSTUFF@
2103"#,
2104                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2105            );
2106
2107            assert_eq!(
2108                r#"Source: blah
2109Testsuite: autopkgtest8
2110Vcs-Git: example.com
2111"#,
2112                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2113            );
2114        }
2115
2116        #[test]
2117        fn test_update_cdbs_template() {
2118            let td = tempfile::tempdir().unwrap();
2119            std::fs::create_dir(td.path().join("debian")).unwrap();
2120
2121            std::fs::write(
2122                td.path().join("debian/control"),
2123                r#"Source: blah
2124Testsuite: autopkgtest
2125Build-Depends: some-foo, libc6
2126
2127"#,
2128            )
2129            .unwrap();
2130
2131            std::fs::write(
2132                td.path().join("debian/control.in"),
2133                r#"Source: blah
2134Testsuite: autopkgtest
2135Build-Depends: @cdbs@, libc6
2136
2137"#,
2138            )
2139            .unwrap();
2140
2141            let editor =
2142                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2143
2144            editor
2145                .source()
2146                .unwrap()
2147                .as_mut_deb822()
2148                .set("Build-Depends", "some-foo, libc6, some-bar");
2149
2150            assert_eq!(
2151                editor
2152                    .source()
2153                    .unwrap()
2154                    .build_depends()
2155                    .unwrap()
2156                    .to_string(),
2157                "some-foo, libc6, some-bar".to_string()
2158            );
2159
2160            assert_eq!(Some(super::TemplateType::Cdbs), editor.template_type());
2161
2162            assert_eq!(
2163                editor.commit().unwrap(),
2164                vec![
2165                    td.path().join("debian/control.in"),
2166                    td.path().join("debian/control")
2167                ]
2168            );
2169
2170            assert_eq!(
2171                r#"Source: blah
2172Testsuite: autopkgtest
2173Build-Depends: @cdbs@, libc6, some-bar
2174
2175"#,
2176                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2177            );
2178
2179            assert_eq!(
2180                r#"Source: blah
2181Testsuite: autopkgtest
2182Build-Depends: some-foo, libc6, some-bar
2183
2184"#,
2185                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2186            );
2187        }
2188
2189        #[test]
2190        #[ignore = "Not implemented yet"]
2191        fn test_description_stays_last() {
2192            let td = tempfile::tempdir().unwrap();
2193            std::fs::create_dir(td.path().join("debian")).unwrap();
2194            std::fs::write(
2195                td.path().join("debian/control"),
2196                r#"Source: blah
2197Testsuite: autopkgtest
2198
2199Package: libblah
2200Section: extra
2201Description: foo
2202 bar
2203
2204"#,
2205            )
2206            .unwrap();
2207
2208            let editor =
2209                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2210            editor
2211                .binaries()
2212                .find(|b| b.name().as_deref() == Some("libblah"))
2213                .unwrap()
2214                .set_architecture(Some("all"));
2215
2216            editor.commit().unwrap();
2217
2218            assert_eq!(
2219                r#"Source: blah
2220Testsuite: autopkgtest
2221
2222Package: libblah
2223Section: extra
2224Architecture: all
2225Description: foo
2226 bar
2227"#,
2228                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2229            );
2230        }
2231
2232        #[test]
2233        fn test_no_new_heading_whitespace() {
2234            let td = tempfile::tempdir().unwrap();
2235            std::fs::create_dir(td.path().join("debian")).unwrap();
2236            std::fs::write(
2237                td.path().join("debian/control"),
2238                r#"Source: blah
2239Build-Depends:
2240 debhelper-compat (= 11),
2241 uuid-dev
2242
2243"#,
2244            )
2245            .unwrap();
2246
2247            let editor =
2248                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2249            editor
2250                .source()
2251                .unwrap()
2252                .as_mut_deb822()
2253                .set("Build-Depends", "debhelper-compat (= 12),\nuuid-dev");
2254
2255            editor.commit().unwrap();
2256
2257            assert_eq!(
2258                r#"Source: blah
2259Build-Depends:
2260 debhelper-compat (= 12),
2261 uuid-dev
2262
2263"#,
2264                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2265            );
2266        }
2267    }
2268}