Skip to main content

debian_workbench/
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().any(|e| {
506                    e.relations()
507                        .any(|r| r.try_name().as_deref() == Some("gnome-pkg-tools"))
508                })
509            }) {
510                return Some(TemplateType::Gnome);
511            }
512
513            if build_depends.iter().any(|d| {
514                d.entries().any(|e| {
515                    e.relations()
516                        .any(|r| r.try_name().as_deref() == Some("cdbs"))
517                })
518            }) {
519                return Some(TemplateType::Cdbs);
520            }
521        }
522        Err(_) if template_path.is_dir() => {
523            return Some(TemplateType::Directory);
524        }
525        Err(e) => panic!("Failed to read {}: {}", template_path.display(), e),
526    }
527    if let Some(debian_path) = debian_path {
528        if debian_path.join("debcargo.toml").exists() {
529            return Some(TemplateType::Debcargo);
530        }
531    }
532    None
533}
534
535/// Apply a set of changes to this deb822 instance.
536///
537/// # Arguments
538/// * `changes` - Changes to apply
539/// * `resolve_conflict` - Callback to resolve conflicts
540fn apply_changes(
541    deb822: &mut deb822_lossless::Deb822,
542    mut changes: Deb822Changes,
543    resolve_conflict: Option<ResolveDeb822Conflict>,
544) -> Result<(), ChangeConflict> {
545    fn default_resolve_conflict(
546        para_key: (&str, &str),
547        field: &str,
548        actual_old_value: Option<&str>,
549        template_old_value: Option<&str>,
550        actual_new_value: Option<&str>,
551    ) -> Result<Option<String>, ChangeConflict> {
552        Err(ChangeConflict {
553            para_key: (para_key.0.to_string(), para_key.1.to_string()),
554            field: field.to_string(),
555            actual_old_value: actual_old_value.map(|v| v.to_string()),
556            template_old_value: template_old_value.map(|v| v.to_string()),
557            actual_new_value: actual_new_value.map(|s| s.to_string()),
558        })
559    }
560
561    let resolve_conflict = resolve_conflict.unwrap_or(default_resolve_conflict);
562
563    for mut paragraph in deb822.paragraphs() {
564        let items: Vec<_> = paragraph.items().collect();
565        for item in items {
566            for (key, old_value, mut new_value) in changes.0.remove(&item).unwrap_or_default() {
567                if paragraph.get(&key) != old_value {
568                    new_value = resolve_conflict(
569                        (&item.0, &item.1),
570                        &key,
571                        old_value.as_deref(),
572                        paragraph.get(&key).as_deref(),
573                        new_value.as_deref(),
574                    )?;
575                }
576                if let Some(new_value) = new_value.as_ref() {
577                    paragraph.set(&key, new_value);
578                } else {
579                    paragraph.remove(&key);
580                }
581            }
582        }
583    }
584    // Add any new paragraphs that weren't processed earlier
585    for (key, p) in changes.0.drain() {
586        let mut paragraph = deb822.add_paragraph();
587        for (field, old_value, mut new_value) in p {
588            if old_value.is_some() {
589                new_value = resolve_conflict(
590                    (&key.0, &key.1),
591                    &field,
592                    old_value.as_deref(),
593                    paragraph.get(&field).as_deref(),
594                    new_value.as_deref(),
595                )?;
596            }
597            if let Some(new_value) = new_value {
598                paragraph.set(&field, &new_value);
599            }
600        }
601    }
602    Ok(())
603}
604
605fn find_template_path(path: &Path) -> Option<PathBuf> {
606    for ext in &["in", "m4"] {
607        let template_path = path.with_extension(ext);
608        if template_path.exists() {
609            return Some(template_path);
610        }
611    }
612    None
613}
614
615/// An editor for a control file that may be generated from a template.
616///
617/// This editor will automatically expand the template if it does not exist.
618/// It will also automatically update the template if the control file is changed.
619///
620/// # Example
621///
622/// ```rust
623/// use std::path::Path;
624/// use debian_workbench::control::TemplatedControlEditor;
625/// let td = tempfile::tempdir().unwrap();
626/// let mut editor = TemplatedControlEditor::create(td.path().join("control")).unwrap();
627/// editor.add_source("foo").set_architecture(Some("all"));
628/// editor.commit().unwrap();
629/// ```
630pub struct TemplatedControlEditor {
631    /// The primary editor for the control file.
632    primary: FsEditor<debian_control::Control>,
633    /// The template that was used to generate the control file.
634    template: Option<Template>,
635    /// Path to the control file.
636    path: PathBuf,
637    /// Whether the control file itself should not be written to disk.
638    template_only: bool,
639}
640
641impl Deref for TemplatedControlEditor {
642    type Target = debian_control::Control;
643
644    fn deref(&self) -> &Self::Target {
645        &self.primary
646    }
647}
648
649impl DerefMut for TemplatedControlEditor {
650    fn deref_mut(&mut self) -> &mut Self::Target {
651        &mut self.primary
652    }
653}
654
655impl TemplatedControlEditor {
656    /// Create a new control file editor.
657    pub fn create<P: AsRef<Path>>(control_path: P) -> Result<Self, EditorError> {
658        if control_path.as_ref().exists() {
659            return Err(EditorError::IoError(std::io::Error::new(
660                std::io::ErrorKind::AlreadyExists,
661                "Control file already exists",
662            )));
663        }
664        Self::new(control_path, true)
665    }
666
667    /// Return the type of the template used to generate the control file.
668    pub fn template_type(&self) -> Option<TemplateType> {
669        self.template.as_ref().map(|t| t.template_type)
670    }
671
672    /// Normalize field spacing in the control file.
673    ///
674    /// For template-based control files with deb822-style templates (CDBS, Directory),
675    /// this will normalize both the template file and the primary control file.
676    /// For non-deb822 templates (Rules, Gnome, Postgresql, etc.), this returns an error
677    /// since those control files are generated and shouldn't be normalized.
678    /// For files without templates, it normalizes the control file directly.
679    ///
680    /// # Returns
681    /// An error if a template exists but cannot be normalized, or if the template
682    /// is not a deb822-style template.
683    pub fn normalize_field_spacing(&mut self) -> Result<(), EditorError> {
684        let Some(template) = &self.template else {
685            // No template: normalize the control file directly
686            self.primary.as_mut_deb822().normalize_field_spacing();
687            return Ok(());
688        };
689
690        // Check if this is a deb822-style template
691        let is_deb822_template = matches!(
692            template.template_type,
693            TemplateType::Cdbs | TemplateType::Directory
694        );
695
696        if !is_deb822_template {
697            // Non-deb822 template: cannot normalize generated control files
698            return Err(EditorError::GeneratedFile(
699                self.path.clone(),
700                GeneratedFile {
701                    template_path: Some(template.template_path.clone()),
702                    template_type: None,
703                },
704            ));
705        }
706
707        // For deb822-style templates: normalize the template file
708        let mut template_editor =
709            FsEditor::<deb822_lossless::Deb822>::new(&template.template_path, true, false)?;
710        template_editor.normalize_field_spacing();
711        template_editor.commit()?;
712
713        // Also normalize the primary control file
714        self.primary.as_mut_deb822().normalize_field_spacing();
715        Ok(())
716    }
717
718    /// Open an existing control file.
719    pub fn open<P: AsRef<Path>>(control_path: P) -> Result<Self, EditorError> {
720        Self::new(control_path, false)
721    }
722
723    /// Create a new control file editor.
724    pub fn new<P: AsRef<Path>>(control_path: P, allow_missing: bool) -> Result<Self, EditorError> {
725        let path = control_path.as_ref();
726        let (template, template_only) = if !path.exists() {
727            if let Some(template) = Template::find(path) {
728                match template.expand() {
729                    Ok(_) => {}
730                    Err(e) => {
731                        return Err(EditorError::TemplateError(
732                            template.template_path,
733                            e.to_string(),
734                        ))
735                    }
736                }
737                (Some(template), true)
738            } else if !allow_missing {
739                return Err(EditorError::IoError(std::io::Error::new(
740                    std::io::ErrorKind::NotFound,
741                    "No control file or template found",
742                )));
743            } else {
744                (None, false)
745            }
746        } else {
747            (Template::find(path), false)
748        };
749        let primary = FsEditor::<debian_control::Control>::new(path, false, false)?;
750        Ok(Self {
751            path: path.to_path_buf(),
752            primary,
753            template_only,
754            template,
755        })
756    }
757
758    /// Return a dictionary describing the changes since the base.
759    ///
760    /// # Returns
761    /// A dictionary mapping tuples of (kind, name) to list of (field_name, old_value, new_value)
762    fn changes(&self) -> Deb822Changes {
763        let orig = deb822_lossless::Deb822::read_relaxed(self.primary.orig_content().unwrap())
764            .unwrap()
765            .0;
766        let mut changes = Deb822Changes::new();
767
768        fn by_key(
769            ps: impl Iterator<Item = Paragraph>,
770        ) -> std::collections::HashMap<(String, String), Paragraph> {
771            let mut ret = std::collections::HashMap::new();
772            for p in ps {
773                if let Some(s) = p.get("Source") {
774                    ret.insert(("Source".to_string(), s), p);
775                } else if let Some(s) = p.get("Package") {
776                    ret.insert(("Package".to_string(), s), p);
777                } else {
778                    let k = p.items().next().unwrap();
779                    ret.insert(k, p);
780                }
781            }
782            ret
783        }
784
785        let orig_by_key = by_key(orig.paragraphs());
786        let new_by_key = by_key(self.as_deb822().paragraphs());
787        let keys = orig_by_key
788            .keys()
789            .chain(new_by_key.keys())
790            .collect::<std::collections::HashSet<_>>();
791        for key in keys {
792            let old = orig_by_key.get(key);
793            let new = new_by_key.get(key);
794            if old == new {
795                continue;
796            }
797            let fields = std::collections::HashSet::<String>::from_iter(
798                old.iter()
799                    .flat_map(|p| p.keys())
800                    .chain(new.iter().flat_map(|p| p.keys())),
801            );
802            for field in &fields {
803                let old_val = old.and_then(|x| x.get(field));
804                let new_val = new.and_then(|x| x.get(field));
805                if old_val != new_val {
806                    changes.insert(key.clone(), field.to_string(), old_val, new_val);
807                }
808            }
809        }
810        changes
811    }
812
813    /// Commit the changes to the control file and template.
814    pub fn commit(&self) -> Result<Vec<PathBuf>, EditorError> {
815        let mut changed_files: Vec<PathBuf> = vec![];
816        if self.template_only {
817            // Remove the control file if it exists.
818            match std::fs::remove_file(self.path.as_path()) {
819                Ok(_) => {}
820                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
821                Err(e) => return Err(EditorError::IoError(e)),
822            }
823
824            changed_files.push(self.path.clone());
825
826            let template = self
827                .template
828                .as_ref()
829                .expect("template_only implies template");
830
831            // Update the template
832            let changed = match template.update(self.changes(), false) {
833                Ok(changed) => changed,
834                Err(e) => {
835                    return Err(EditorError::TemplateError(
836                        template.template_path.clone(),
837                        e.to_string(),
838                    ))
839                }
840            };
841            if changed {
842                changed_files.push(template.template_path.clone());
843            }
844        } else {
845            match self.primary.commit() {
846                Ok(files) => {
847                    changed_files.extend(files.iter().map(|p| p.to_path_buf()));
848                }
849                Err(EditorError::GeneratedFile(
850                    p,
851                    GeneratedFile {
852                        template_path: tp,
853                        template_type: tt,
854                    },
855                )) => {
856                    if tp.is_none() {
857                        return Err(EditorError::GeneratedFile(
858                            p,
859                            GeneratedFile {
860                                template_path: tp,
861                                template_type: tt,
862                            },
863                        ));
864                    }
865                    let template = if let Some(template) = self.template.as_ref() {
866                        template
867                    } else {
868                        return Err(EditorError::IoError(std::io::Error::new(
869                            std::io::ErrorKind::NotFound,
870                            "No control file or template found",
871                        )));
872                    };
873                    let changes = self.changes();
874                    let changed = match template.update(changes, true) {
875                        Ok(changed) => changed,
876                        Err(e) => {
877                            return Err(EditorError::TemplateError(tp.unwrap(), e.to_string()))
878                        }
879                    };
880                    changed_files = if changed {
881                        vec![tp.as_ref().unwrap().to_path_buf(), p]
882                    } else {
883                        vec![]
884                    };
885                }
886                Err(EditorError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
887                    let template = if let Some(p) = self.template.as_ref() {
888                        p
889                    } else {
890                        return Err(EditorError::IoError(std::io::Error::new(
891                            std::io::ErrorKind::NotFound,
892                            "No control file or template found",
893                        )));
894                    };
895                    let changed = match template.update(self.changes(), !self.template_only) {
896                        Ok(changed) => changed,
897                        Err(e) => {
898                            return Err(EditorError::TemplateError(
899                                template.template_path.clone(),
900                                e.to_string(),
901                            ))
902                        }
903                    };
904                    if changed {
905                        changed_files.push(template.template_path.clone());
906                        changed_files.push(self.path.clone());
907                    }
908                }
909                Err(e) => return Err(e),
910            }
911        }
912
913        Ok(changed_files)
914    }
915}
916
917struct Template {
918    path: PathBuf,
919    template_path: PathBuf,
920    template_type: TemplateType,
921}
922
923impl Template {
924    fn find(path: &Path) -> Option<Self> {
925        let template_path = find_template_path(path)?;
926        let template_type = guess_template_type(&template_path, Some(path.parent().unwrap()))?;
927        Some(Self {
928            path: path.to_path_buf(),
929            template_path,
930            template_type,
931        })
932    }
933
934    fn expand(&self) -> Result<(), TemplateExpansionError> {
935        expand_control_template(&self.template_path, &self.path, self.template_type)
936    }
937
938    fn update(&self, changes: Deb822Changes, expand: bool) -> Result<bool, TemplateExpansionError> {
939        update_control_template(
940            &self.template_path,
941            self.template_type,
942            &self.path,
943            changes,
944            expand,
945        )
946    }
947}
948
949impl Editor<debian_control::Control> for TemplatedControlEditor {
950    fn orig_content(&self) -> Option<&[u8]> {
951        self.primary.orig_content()
952    }
953
954    fn updated_content(&self) -> Option<Vec<u8>> {
955        self.primary.updated_content()
956    }
957
958    fn rewritten_content(&self) -> Option<&[u8]> {
959        self.primary.rewritten_content()
960    }
961
962    fn is_generated(&self) -> bool {
963        self.primary.is_generated()
964    }
965
966    fn commit(&self) -> Result<Vec<std::path::PathBuf>, EditorError> {
967        TemplatedControlEditor::commit(self)
968    }
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974
975    #[test]
976    fn test_format_description() {
977        let summary = "Summary";
978        let long_description = vec!["Long", "Description"];
979        let expected = "Summary\n Long\n Description\n";
980        assert_eq!(format_description(summary, long_description), expected);
981    }
982
983    #[test]
984    fn test_resolve_cdbs_conflicts() {
985        let val = resolve_cdbs_template(
986            ("Source", "libnetsds-perl"),
987            "Build-Depends",
988            Some("debhelper (>= 6), foo"),
989            Some("@cdbs@, debhelper (>= 9)"),
990            Some("debhelper (>= 10), foo"),
991        )
992        .unwrap();
993
994        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
995
996        let val = resolve_cdbs_template(
997            ("Source", "libnetsds-perl"),
998            "Build-Depends",
999            Some("debhelper (>= 6), foo"),
1000            Some("@cdbs@, foo"),
1001            Some("debhelper (>= 10), foo"),
1002        )
1003        .unwrap();
1004        assert_eq!(val, Some("@cdbs@, debhelper (>= 10), foo".to_string()));
1005        let val = resolve_cdbs_template(
1006            ("Source", "libnetsds-perl"),
1007            "Build-Depends",
1008            Some("debhelper (>= 6), foo"),
1009            Some("@cdbs@, debhelper (>= 9)"),
1010            Some("debhelper (>= 10), foo"),
1011        )
1012        .unwrap();
1013        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
1014    }
1015
1016    #[test]
1017    fn test_normalize_field_spacing_without_template() {
1018        // Test normalize_field_spacing on a control file without a template
1019        let td = tempfile::tempdir().unwrap();
1020        let control_path = td.path().join("debian").join("control");
1021        std::fs::create_dir_all(control_path.parent().unwrap()).unwrap();
1022        std::fs::write(
1023            &control_path,
1024            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1025        )
1026        .unwrap();
1027
1028        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1029
1030        // Check the original spacing
1031        let original = editor.as_deb822().to_string();
1032        assert_eq!(
1033            original,
1034            "Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n"
1035        );
1036
1037        // Normalize field spacing using the new method
1038        editor.normalize_field_spacing().unwrap();
1039
1040        // Check if normalization worked
1041        let normalized = editor.as_deb822().to_string();
1042        assert_eq!(
1043            normalized,
1044            "Source: test\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
1045        );
1046    }
1047
1048    #[test]
1049    fn test_normalize_field_spacing_with_non_deb822_template() {
1050        // Test that normalize_field_spacing returns an error for non-deb822 templates
1051        let td = tempfile::tempdir().unwrap();
1052        let debian_path = td.path().join("debian");
1053        std::fs::create_dir_all(&debian_path).unwrap();
1054
1055        let control_in_path = debian_path.join("control.in");
1056        let control_path = debian_path.join("control");
1057        let rules_path = debian_path.join("rules");
1058
1059        // Create a Rules-style template
1060        std::fs::write(
1061            &control_in_path,
1062            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1063        )
1064        .unwrap();
1065
1066        // Create a rules file that generates control
1067        std::fs::write(
1068            &rules_path,
1069            b"#!/usr/bin/make -f\n\ndebian/control: debian/control.in\n\tcp $< $@\n",
1070        )
1071        .unwrap();
1072
1073        // Create a generated control file
1074        std::fs::write(
1075            &control_path,
1076            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1077        )
1078        .unwrap();
1079
1080        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1081        assert_eq!(editor.template_type(), Some(TemplateType::Rules));
1082
1083        // Normalize field spacing should fail for non-deb822 templates
1084        let result = editor.normalize_field_spacing();
1085        assert!(result.is_err());
1086
1087        // Verify it's a GeneratedFile error
1088        match result {
1089            Err(EditorError::GeneratedFile(_, _)) => {
1090                // Expected error type
1091            }
1092            _ => panic!("Expected GeneratedFile error"),
1093        }
1094    }
1095
1096    #[test]
1097    fn test_normalize_field_spacing_with_cdbs_template() {
1098        // Test normalize_field_spacing with a CDBS template
1099        // Note: We can't actually expand CDBS templates in tests, so we test that
1100        // the template file itself gets normalized
1101        let td = tempfile::tempdir().unwrap();
1102        let debian_path = td.path().join("debian");
1103        std::fs::create_dir_all(&debian_path).unwrap();
1104
1105        let control_in_path = debian_path.join("control.in");
1106        let control_path = debian_path.join("control");
1107
1108        // Create a CDBS-style template with double spacing
1109        std::fs::write(
1110            &control_in_path,
1111            b"Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n",
1112        )
1113        .unwrap();
1114
1115        // Create a control file (pretending it was generated from the template)
1116        std::fs::write(
1117            &control_path,
1118            b"Source: test\nBuild-Depends: debhelper\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1119        )
1120        .unwrap();
1121
1122        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1123        assert_eq!(editor.template_type(), Some(TemplateType::Cdbs));
1124
1125        // Verify original template has double spacing
1126        let original_template = std::fs::read_to_string(&control_in_path).unwrap();
1127        assert_eq!(
1128            original_template,
1129            "Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
1130        );
1131
1132        // Normalize field spacing - this should normalize both template and primary
1133        editor.normalize_field_spacing().unwrap();
1134
1135        // Check that the template was normalized
1136        let template_content = std::fs::read_to_string(&control_in_path).unwrap();
1137        assert_eq!(
1138            template_content,
1139            "Source: test\nBuild-Depends: @cdbs@\nRecommends: ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
1140        );
1141
1142        // Check that the primary control file was also normalized
1143        let control_content = editor.as_deb822().to_string();
1144        assert_eq!(
1145            control_content,
1146            "Source: test\nBuild-Depends: debhelper\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
1147        );
1148    }
1149
1150    mod guess_template_type {
1151
1152        #[test]
1153        fn test_rules_generates_control() {
1154            let td = tempfile::tempdir().unwrap();
1155            std::fs::create_dir(td.path().join("debian")).unwrap();
1156            std::fs::write(
1157                td.path().join("debian/rules"),
1158                r#"%:
1159	dh $@
1160
1161debian/control: debian/control.in
1162	cp $@ $<
1163"#,
1164            )
1165            .unwrap();
1166            assert_eq!(
1167                super::guess_template_type(
1168                    &td.path().join("debian/control.in"),
1169                    Some(&td.path().join("debian"))
1170                ),
1171                Some(super::TemplateType::Rules)
1172            );
1173        }
1174
1175        #[test]
1176        fn test_rules_generates_control_percent() {
1177            let td = tempfile::tempdir().unwrap();
1178            std::fs::create_dir(td.path().join("debian")).unwrap();
1179            std::fs::write(
1180                td.path().join("debian/rules"),
1181                r#"%:
1182	dh $@
1183
1184debian/%: debian/%.in
1185	cp $@ $<
1186"#,
1187            )
1188            .unwrap();
1189            assert_eq!(
1190                super::guess_template_type(
1191                    &td.path().join("debian/control.in"),
1192                    Some(&td.path().join("debian"))
1193                ),
1194                Some(super::TemplateType::Rules)
1195            );
1196        }
1197
1198        #[test]
1199        fn test_rules_generates_control_blends() {
1200            let td = tempfile::tempdir().unwrap();
1201            std::fs::create_dir(td.path().join("debian")).unwrap();
1202            std::fs::write(
1203                td.path().join("debian/rules"),
1204                r#"%:
1205	dh $@
1206
1207include /usr/share/blends-dev/rules
1208"#,
1209            )
1210            .unwrap();
1211            assert_eq!(
1212                super::guess_template_type(
1213                    &td.path().join("debian/control.stub"),
1214                    Some(&td.path().join("debian"))
1215                ),
1216                Some(super::TemplateType::Rules)
1217            );
1218        }
1219
1220        #[test]
1221        fn test_empty_template() {
1222            let td = tempfile::tempdir().unwrap();
1223            std::fs::create_dir(td.path().join("debian")).unwrap();
1224            // No paragraph
1225            std::fs::write(td.path().join("debian/control.in"), "").unwrap();
1226
1227            assert_eq!(
1228                None,
1229                super::guess_template_type(
1230                    &td.path().join("debian/control.in"),
1231                    Some(&td.path().join("debian"))
1232                )
1233            );
1234        }
1235
1236        #[test]
1237        fn test_build_depends_cdbs() {
1238            let td = tempfile::tempdir().unwrap();
1239            std::fs::create_dir(td.path().join("debian")).unwrap();
1240            std::fs::write(
1241                td.path().join("debian/control.in"),
1242                r#"Source: blah
1243Build-Depends: cdbs
1244Vcs-Git: file://
1245
1246Package: bar
1247"#,
1248            )
1249            .unwrap();
1250            assert_eq!(
1251                Some(super::TemplateType::Cdbs),
1252                super::guess_template_type(
1253                    &td.path().join("debian/control.in"),
1254                    Some(&td.path().join("debian"))
1255                )
1256            );
1257        }
1258
1259        #[test]
1260        fn test_no_build_depends() {
1261            let td = tempfile::tempdir().unwrap();
1262            std::fs::create_dir(td.path().join("debian")).unwrap();
1263            std::fs::write(
1264                td.path().join("debian/control.in"),
1265                r#"Source: blah
1266Vcs-Git: file://
1267
1268Package: bar
1269"#,
1270            )
1271            .unwrap();
1272            assert_eq!(
1273                None,
1274                super::guess_template_type(
1275                    &td.path().join("debian/control.in"),
1276                    Some(&td.path().join("debian"))
1277                )
1278            );
1279        }
1280
1281        #[test]
1282        fn test_gnome() {
1283            let td = tempfile::tempdir().unwrap();
1284            std::fs::create_dir(td.path().join("debian")).unwrap();
1285            std::fs::write(
1286                td.path().join("debian/control.in"),
1287                r#"Foo @GNOME_TEAM@
1288"#,
1289            )
1290            .unwrap();
1291            assert_eq!(
1292                Some(super::TemplateType::Gnome),
1293                super::guess_template_type(
1294                    &td.path().join("debian/control.in"),
1295                    Some(&td.path().join("debian"))
1296                )
1297            );
1298        }
1299
1300        #[test]
1301        fn test_gnome_build_depends() {
1302            let td = tempfile::tempdir().unwrap();
1303            std::fs::create_dir(td.path().join("debian")).unwrap();
1304            std::fs::write(
1305                td.path().join("debian/control.in"),
1306                r#"Source: blah
1307Build-Depends: gnome-pkg-tools, libc6-dev
1308"#,
1309            )
1310            .unwrap();
1311            assert_eq!(
1312                Some(super::TemplateType::Gnome),
1313                super::guess_template_type(
1314                    &td.path().join("debian/control.in"),
1315                    Some(&td.path().join("debian"))
1316                )
1317            );
1318        }
1319
1320        #[test]
1321        fn test_cdbs() {
1322            let td = tempfile::tempdir().unwrap();
1323            std::fs::create_dir(td.path().join("debian")).unwrap();
1324            std::fs::write(
1325                td.path().join("debian/control.in"),
1326                r#"Source: blah
1327Build-Depends: debhelper, cdbs
1328"#,
1329            )
1330            .unwrap();
1331            assert_eq!(
1332                Some(super::TemplateType::Cdbs),
1333                super::guess_template_type(
1334                    &td.path().join("debian/control.in"),
1335                    Some(&td.path().join("debian"))
1336                )
1337            );
1338        }
1339
1340        #[test]
1341        fn test_multiple_paragraphs() {
1342            let td = tempfile::tempdir().unwrap();
1343            std::fs::create_dir(td.path().join("debian")).unwrap();
1344            std::fs::write(
1345                td.path().join("debian/control.in"),
1346                r#"Source: blah
1347Build-Depends: debhelper, cdbs
1348
1349Package: foo
1350"#,
1351            )
1352            .unwrap();
1353            assert_eq!(
1354                Some(super::TemplateType::Cdbs),
1355                super::guess_template_type(
1356                    &td.path().join("debian/control.in"),
1357                    Some(&td.path().join("debian"))
1358                )
1359            );
1360        }
1361
1362        #[test]
1363        fn test_directory() {
1364            let td = tempfile::tempdir().unwrap();
1365            std::fs::create_dir(td.path().join("debian")).unwrap();
1366            std::fs::create_dir(td.path().join("debian/control.in")).unwrap();
1367            assert_eq!(
1368                Some(super::TemplateType::Directory),
1369                super::guess_template_type(
1370                    &td.path().join("debian/control.in"),
1371                    Some(&td.path().join("debian"))
1372                )
1373            );
1374        }
1375
1376        #[test]
1377        fn test_debcargo() {
1378            let td = tempfile::tempdir().unwrap();
1379            std::fs::create_dir(td.path().join("debian")).unwrap();
1380            std::fs::write(
1381                td.path().join("debian/control.in"),
1382                r#"Source: blah
1383Build-Depends: bar
1384"#,
1385            )
1386            .unwrap();
1387            std::fs::write(
1388                td.path().join("debian/debcargo.toml"),
1389                r#"maintainer = Joe Example <joe@example.com>
1390"#,
1391            )
1392            .unwrap();
1393            assert_eq!(
1394                Some(super::TemplateType::Debcargo),
1395                super::guess_template_type(
1396                    &td.path().join("debian/control.in"),
1397                    Some(&td.path().join("debian"))
1398                )
1399            );
1400        }
1401    }
1402
1403    #[test]
1404    fn test_postgresql() {
1405        let td = tempfile::tempdir().unwrap();
1406        std::fs::create_dir(td.path().join("debian")).unwrap();
1407        std::fs::write(
1408            td.path().join("debian/control.in"),
1409            r#"Source: blah
1410Build-Depends: bar, postgresql
1411
1412Package: foo-PGVERSION
1413"#,
1414        )
1415        .unwrap();
1416        assert_eq!(
1417            Some(super::TemplateType::Postgresql),
1418            super::guess_template_type(
1419                &td.path().join("debian/control.in"),
1420                Some(&td.path().join("debian"))
1421            )
1422        );
1423    }
1424
1425    #[test]
1426    fn test_apply_changes() {
1427        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1428Build-Depends: debhelper (>= 6), foo
1429
1430Package: bar
1431"#
1432        .parse()
1433        .unwrap();
1434
1435        let mut changes = Deb822Changes(std::collections::HashMap::new());
1436        changes.0.insert(
1437            ("Source".to_string(), "blah".to_string()),
1438            vec![(
1439                "Build-Depends".to_string(),
1440                Some("debhelper (>= 6), foo".to_string()),
1441                Some("debhelper (>= 10), foo".to_string()),
1442            )],
1443        );
1444
1445        super::apply_changes(&mut deb822, changes, None).unwrap();
1446
1447        assert_eq!(
1448            deb822.to_string(),
1449            r#"Source: blah
1450Build-Depends: debhelper (>= 10), foo
1451
1452Package: bar
1453"#
1454        );
1455    }
1456
1457    #[test]
1458    fn test_apply_changes_new_paragraph() {
1459        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1460Build-Depends: debhelper (>= 6), foo
1461
1462Package: bar
1463"#
1464        .parse()
1465        .unwrap();
1466
1467        let mut changes = Deb822Changes(std::collections::HashMap::new());
1468        changes.0.insert(
1469            ("Source".to_string(), "blah".to_string()),
1470            vec![(
1471                "Build-Depends".to_string(),
1472                Some("debhelper (>= 6), foo".to_string()),
1473                Some("debhelper (>= 10), foo".to_string()),
1474            )],
1475        );
1476        changes.0.insert(
1477            ("Package".to_string(), "blah2".to_string()),
1478            vec![
1479                ("Package".to_string(), None, Some("blah2".to_string())),
1480                (
1481                    "Description".to_string(),
1482                    None,
1483                    Some("Some package".to_string()),
1484                ),
1485            ],
1486        );
1487
1488        super::apply_changes(&mut deb822, changes, None).unwrap();
1489
1490        assert_eq!(
1491            deb822.to_string(),
1492            r#"Source: blah
1493Build-Depends: debhelper (>= 10), foo
1494
1495Package: bar
1496
1497Package: blah2
1498Description: Some package
1499"#
1500        );
1501    }
1502
1503    #[test]
1504    fn test_apply_changes_conflict() {
1505        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1506Build-Depends: debhelper (>= 6), foo
1507
1508Package: bar
1509"#
1510        .parse()
1511        .unwrap();
1512
1513        let mut changes = Deb822Changes(std::collections::HashMap::new());
1514        changes.0.insert(
1515            ("Source".to_string(), "blah".to_string()),
1516            vec![(
1517                "Build-Depends".to_string(),
1518                Some("debhelper (>= 7), foo".to_string()),
1519                Some("debhelper (>= 10), foo".to_string()),
1520            )],
1521        );
1522
1523        let result = super::apply_changes(&mut deb822, changes, None);
1524        assert!(result.is_err());
1525        let err = result.unwrap_err();
1526        assert_eq!(
1527            err,
1528            ChangeConflict {
1529                para_key: ("Source".to_string(), "blah".to_string()),
1530                field: "Build-Depends".to_string(),
1531                actual_old_value: Some("debhelper (>= 7), foo".to_string()),
1532                template_old_value: Some("debhelper (>= 6), foo".to_string()),
1533                actual_new_value: Some("debhelper (>= 10), foo".to_string()),
1534            }
1535        );
1536    }
1537
1538    #[test]
1539    fn test_apply_changes_resolve_conflict() {
1540        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1541Build-Depends: debhelper (>= 6), foo
1542
1543Package: bar
1544"#
1545        .parse()
1546        .unwrap();
1547
1548        let mut changes = Deb822Changes(std::collections::HashMap::new());
1549        changes.0.insert(
1550            ("Source".to_string(), "blah".to_string()),
1551            vec![(
1552                "Build-Depends".to_string(),
1553                Some("debhelper (>= 7), foo".to_string()),
1554                Some("debhelper (>= 10), foo".to_string()),
1555            )],
1556        );
1557
1558        let result = super::apply_changes(&mut deb822, changes, Some(|_, _, _, _, _| Ok(None)));
1559        assert!(result.is_ok());
1560        assert_eq!(
1561            deb822.to_string(),
1562            r#"Source: blah
1563
1564Package: bar
1565"#
1566        );
1567    }
1568
1569    mod control_editor {
1570        #[test]
1571        fn test_do_not_edit() {
1572            let td = tempfile::tempdir().unwrap();
1573            std::fs::create_dir(td.path().join("debian")).unwrap();
1574            std::fs::write(
1575                td.path().join("debian/control"),
1576                r#"# DO NOT EDIT
1577# This file was generated by blah
1578
1579Source: blah
1580Testsuite: autopkgtest
1581
1582"#,
1583            )
1584            .unwrap();
1585            let editor =
1586                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1587            editor.source().unwrap().set_name("foo");
1588            let changes = editor.changes();
1589            assert_eq!(
1590                changes.normalized(),
1591                vec![
1592                    (
1593                        ("Source", "blah"),
1594                        vec![
1595                            ("Source", Some("blah"), None),
1596                            ("Testsuite", Some("autopkgtest"), None),
1597                        ]
1598                    ),
1599                    (
1600                        ("Source", "foo"),
1601                        vec![
1602                            ("Source", None, Some("foo")),
1603                            ("Testsuite", None, Some("autopkgtest"))
1604                        ]
1605                    )
1606                ]
1607            );
1608            assert!(matches!(
1609                editor.commit().unwrap_err(),
1610                super::EditorError::GeneratedFile(_, _)
1611            ));
1612        }
1613
1614        #[test]
1615        fn test_add_binary() {
1616            let td = tempfile::tempdir().unwrap();
1617            std::fs::create_dir(td.path().join("debian")).unwrap();
1618            std::fs::write(
1619                td.path().join("debian/control"),
1620                r#"Source: blah
1621Testsuite: autopkgtest
1622
1623Package: blah
1624Description: Some description
1625 And there are more lines
1626 And more lines
1627"#,
1628            )
1629            .unwrap();
1630            let mut editor =
1631                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1632            let mut binary = editor.add_binary("foo");
1633            binary.set_description(Some("A new package foo"));
1634            let paths = editor.commit().unwrap();
1635            assert_eq!(paths.len(), 1);
1636        }
1637
1638        #[test]
1639        fn test_list_binaries() {
1640            let td = tempfile::tempdir().unwrap();
1641            std::fs::create_dir(td.path().join("debian")).unwrap();
1642            std::fs::write(
1643                td.path().join("debian/control"),
1644                r#"Source: blah
1645Testsuite: autopkgtest
1646
1647Package: blah
1648Description: Some description
1649 And there are more lines
1650 And more lines
1651"#,
1652            )
1653            .unwrap();
1654            let editor =
1655                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1656            let binaries = editor.binaries().collect::<Vec<_>>();
1657            assert_eq!(binaries.len(), 1);
1658            assert_eq!(binaries[0].name().as_deref(), Some("blah"));
1659            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
1660        }
1661
1662        #[test]
1663        fn test_no_source() {
1664            let td = tempfile::tempdir().unwrap();
1665            std::fs::create_dir(td.path().join("debian")).unwrap();
1666            std::fs::write(
1667                td.path().join("debian/control"),
1668                r#"Package: blah
1669Testsuite: autopkgtest
1670
1671Package: bar
1672"#,
1673            )
1674            .unwrap();
1675            let editor =
1676                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1677            assert!(editor.source().is_none());
1678        }
1679
1680        #[test]
1681        fn test_create() {
1682            let td = tempfile::tempdir().unwrap();
1683            std::fs::create_dir(td.path().join("debian")).unwrap();
1684            let mut editor =
1685                super::TemplatedControlEditor::create(td.path().join("debian/control")).unwrap();
1686            editor.add_source("foo");
1687            assert_eq!(
1688                r#"Source: foo
1689"#,
1690                editor.as_deb822().to_string()
1691            );
1692        }
1693
1694        #[test]
1695        fn test_do_not_edit_no_change() {
1696            let td = tempfile::tempdir().unwrap();
1697            std::fs::create_dir(td.path().join("debian")).unwrap();
1698            std::fs::write(
1699                td.path().join("debian/control"),
1700                r#"# DO NOT EDIT
1701# This file was generated by blah
1702
1703Source: blah
1704Testsuite: autopkgtest
1705
1706"#,
1707            )
1708            .unwrap();
1709            let editor =
1710                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1711            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
1712        }
1713
1714        #[test]
1715        fn test_unpreservable() {
1716            let td = tempfile::tempdir().unwrap();
1717            std::fs::create_dir(td.path().join("debian")).unwrap();
1718            std::fs::write(
1719                td.path().join("debian/control"),
1720                r#"Source: blah
1721# A comment
1722Testsuite: autopkgtest
1723
1724"#,
1725            )
1726            .unwrap();
1727            let editor =
1728                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1729            editor
1730                .source()
1731                .unwrap()
1732                .as_mut_deb822()
1733                .set("NewField", "New Field");
1734
1735            editor.commit().unwrap();
1736
1737            assert_eq!(
1738                r#"Source: blah
1739# A comment
1740Testsuite: autopkgtest
1741NewField: New Field
1742
1743"#,
1744                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1745            );
1746        }
1747
1748        #[test]
1749        fn test_modify_source() {
1750            let td = tempfile::tempdir().unwrap();
1751            std::fs::create_dir(td.path().join("debian")).unwrap();
1752            std::fs::write(
1753                td.path().join("debian/control"),
1754                r#"Source: blah
1755Testsuite: autopkgtest
1756"#,
1757            )
1758            .unwrap();
1759
1760            let editor =
1761                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1762            editor
1763                .source()
1764                .unwrap()
1765                .as_mut_deb822()
1766                .set("XS-Vcs-Git", "git://github.com/example/example");
1767
1768            editor.commit().unwrap();
1769
1770            assert_eq!(
1771                r#"Source: blah
1772Testsuite: autopkgtest
1773XS-Vcs-Git: git://github.com/example/example
1774"#,
1775                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1776            );
1777        }
1778
1779        /*
1780                #[test]
1781                fn test_wrap_and_sort() {
1782                    let td = tempfile::tempdir().unwrap();
1783                    std::fs::create_dir(td.path().join("debian")).unwrap();
1784
1785                    std::fs::write(
1786                        td.path().join("debian/control"),
1787                        r#"Source: blah
1788        Testsuite: autopkgtest
1789        Depends: package3, package2
1790
1791        Package: libblah
1792        Section: extra
1793        "#,
1794                    )
1795                    .unwrap();
1796
1797                    let mut editor =
1798                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1799                    editor.wrap_and_sort(trailing_comma = true).unwrap();
1800
1801                    editor.commit().unwrap();
1802
1803                    assert_eq!(
1804                        r#"Source: blah
1805        Testsuite: autopkgtest
1806        Depends: package2, package3,
1807
1808        Package: libblah
1809        Section: extra
1810        "#,
1811                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1812                    );
1813                }
1814
1815                #[test]
1816                fn test_sort_binaries() {
1817                    let td = tempfile::tempdir().unwrap();
1818                    std::fs::create_dir(td.path().join("debian")).unwrap();
1819                    std::fs::write(
1820                        td.path().join("debian/control"),
1821                        r#"Source: blah
1822        Source: blah
1823        Testsuite: autopkgtest
1824        Depends: package3, package2
1825
1826        Package: libfoo
1827        Section: web
1828
1829        Package: libblah
1830        Section: extra
1831        "#,
1832                    )
1833                    .unwrap();
1834
1835                    let mut editor =
1836                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1837
1838                    editor.sort_binary_packages().unwrap();
1839
1840                    editor.commit().unwrap();
1841
1842                    assert_eq!(
1843                        r#"Source: blah
1844        Testsuite: autopkgtest
1845        Depends: package3, package2
1846
1847        Package: libblah
1848        Section: extra
1849
1850        Package: libfoo
1851        Section: web
1852        "#,
1853                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1854                    );
1855                }
1856
1857            */
1858
1859        #[test]
1860        fn test_modify_binary() {
1861            let td = tempfile::tempdir().unwrap();
1862            std::fs::create_dir(td.path().join("debian")).unwrap();
1863            std::fs::write(
1864                td.path().join("debian/control"),
1865                r#"Source: blah
1866Testsuite: autopkgtest
1867
1868Package: libblah
1869Section: extra
1870"#,
1871            )
1872            .unwrap();
1873
1874            let editor =
1875                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1876            let mut binary = editor
1877                .binaries()
1878                .find(|b| b.name().as_deref() == Some("libblah"))
1879                .unwrap();
1880            binary.set_architecture(Some("all"));
1881
1882            editor.commit().unwrap();
1883
1884            assert_eq!(
1885                r#"Source: blah
1886Testsuite: autopkgtest
1887
1888Package: libblah
1889Architecture: all
1890Section: extra
1891"#,
1892                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1893            );
1894        }
1895
1896        #[test]
1897        fn test_doesnt_strip_whitespace() {
1898            let td = tempfile::tempdir().unwrap();
1899            std::fs::create_dir(td.path().join("debian")).unwrap();
1900            std::fs::write(
1901                td.path().join("debian/control"),
1902                r#"Source: blah
1903Testsuite: autopkgtest
1904
1905"#,
1906            )
1907            .unwrap();
1908            let editor =
1909                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1910            editor.commit().unwrap();
1911
1912            assert_eq!(
1913                r#"Source: blah
1914Testsuite: autopkgtest
1915
1916"#,
1917                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1918            );
1919        }
1920
1921        #[cfg(unix)]
1922        #[test]
1923        fn test_update_template() {
1924            use std::os::unix::fs::PermissionsExt;
1925            let td = tempfile::tempdir().unwrap();
1926            std::fs::create_dir(td.path().join("debian")).unwrap();
1927            std::fs::write(
1928                td.path().join("debian/control"),
1929                r#"# DO NOT EDIT
1930# This file was generated by blah
1931
1932Source: blah
1933Testsuite: autopkgtest
1934Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
1935
1936"#,
1937            )
1938            .unwrap();
1939            std::fs::write(
1940                td.path().join("debian/control.in"),
1941                r#"Source: blah
1942Testsuite: autopkgtest
1943Uploaders: @lintian-brush-test@
1944
1945"#,
1946            )
1947            .unwrap();
1948            std::fs::write(
1949                td.path().join("debian/rules"),
1950                r#"#!/usr/bin/make -f
1951
1952debian/control: debian/control.in
1953	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
1954"#,
1955            )
1956            .unwrap();
1957            // Make debian/rules executable
1958            std::fs::set_permissions(
1959                td.path().join("debian/rules"),
1960                std::fs::Permissions::from_mode(0o755),
1961            )
1962            .unwrap();
1963
1964            let editor =
1965                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1966            editor
1967                .source()
1968                .unwrap()
1969                .as_mut_deb822()
1970                .set("Testsuite", "autopkgtest8");
1971
1972            assert_eq!(
1973                editor.commit().unwrap(),
1974                vec![
1975                    td.path().join("debian/control.in"),
1976                    td.path().join("debian/control")
1977                ]
1978            );
1979
1980            assert_eq!(
1981                r#"Source: blah
1982Testsuite: autopkgtest8
1983Uploaders: @lintian-brush-test@
1984
1985"#,
1986                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
1987            );
1988
1989            assert_eq!(
1990                r#"Source: blah
1991Testsuite: autopkgtest8
1992Uploaders: testvalue
1993
1994"#,
1995                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1996            );
1997        }
1998
1999        #[cfg(unix)]
2000        #[test]
2001        fn test_update_template_only() {
2002            use std::os::unix::fs::PermissionsExt;
2003            let td = tempfile::tempdir().unwrap();
2004            std::fs::create_dir(td.path().join("debian")).unwrap();
2005            std::fs::write(
2006                td.path().join("debian/control.in"),
2007                r#"Source: blah
2008Testsuite: autopkgtest
2009Uploaders: @lintian-brush-test@
2010
2011"#,
2012            )
2013            .unwrap();
2014            std::fs::write(
2015                td.path().join("debian/rules"),
2016                r#"#!/usr/bin/make -f
2017
2018debian/control: debian/control.in
2019	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
2020"#,
2021            )
2022            .unwrap();
2023
2024            std::fs::set_permissions(
2025                td.path().join("debian/rules"),
2026                std::fs::Permissions::from_mode(0o755),
2027            )
2028            .unwrap();
2029
2030            let editor =
2031                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2032            editor
2033                .source()
2034                .unwrap()
2035                .as_mut_deb822()
2036                .set("Testsuite", "autopkgtest8");
2037
2038            editor.commit().unwrap();
2039
2040            assert_eq!(
2041                r#"Source: blah
2042Testsuite: autopkgtest8
2043Uploaders: @lintian-brush-test@
2044
2045"#,
2046                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2047            );
2048
2049            assert!(!td.path().join("debian/control").exists());
2050        }
2051
2052        #[cfg(unix)]
2053        #[test]
2054        fn test_update_template_invalid_tokens() {
2055            use std::os::unix::fs::PermissionsExt;
2056            let td = tempfile::tempdir().unwrap();
2057            std::fs::create_dir(td.path().join("debian")).unwrap();
2058            std::fs::write(
2059                td.path().join("debian/control"),
2060                r#"# DO NOT EDIT
2061# This file was generated by blah
2062
2063Source: blah
2064Testsuite: autopkgtest
2065Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
2066"#,
2067            )
2068            .unwrap();
2069            std::fs::write(
2070                td.path().join("debian/control.in"),
2071                r#"Source: blah
2072Testsuite: autopkgtest
2073@OTHERSTUFF@
2074"#,
2075            )
2076            .unwrap();
2077
2078            std::fs::write(
2079                td.path().join("debian/rules"),
2080                r#"#!/usr/bin/make -f
2081
2082debian/control: debian/control.in
2083	sed -e 's/@OTHERSTUFF@/Vcs-Git: example.com/' < $< > $@
2084"#,
2085            )
2086            .unwrap();
2087
2088            std::fs::set_permissions(
2089                td.path().join("debian/rules"),
2090                std::fs::Permissions::from_mode(0o755),
2091            )
2092            .unwrap();
2093
2094            let editor =
2095                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2096            editor
2097                .source()
2098                .unwrap()
2099                .as_mut_deb822()
2100                .set("Testsuite", "autopkgtest8");
2101            editor.commit().unwrap();
2102
2103            assert_eq!(
2104                r#"Source: blah
2105Testsuite: autopkgtest8
2106@OTHERSTUFF@
2107"#,
2108                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2109            );
2110
2111            assert_eq!(
2112                r#"Source: blah
2113Testsuite: autopkgtest8
2114Vcs-Git: example.com
2115"#,
2116                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2117            );
2118        }
2119
2120        #[test]
2121        fn test_update_cdbs_template() {
2122            let td = tempfile::tempdir().unwrap();
2123            std::fs::create_dir(td.path().join("debian")).unwrap();
2124
2125            std::fs::write(
2126                td.path().join("debian/control"),
2127                r#"Source: blah
2128Testsuite: autopkgtest
2129Build-Depends: some-foo, libc6
2130
2131"#,
2132            )
2133            .unwrap();
2134
2135            std::fs::write(
2136                td.path().join("debian/control.in"),
2137                r#"Source: blah
2138Testsuite: autopkgtest
2139Build-Depends: @cdbs@, libc6
2140
2141"#,
2142            )
2143            .unwrap();
2144
2145            let editor =
2146                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2147
2148            editor
2149                .source()
2150                .unwrap()
2151                .as_mut_deb822()
2152                .set("Build-Depends", "some-foo, libc6, some-bar");
2153
2154            assert_eq!(
2155                editor
2156                    .source()
2157                    .unwrap()
2158                    .build_depends()
2159                    .unwrap()
2160                    .to_string(),
2161                "some-foo, libc6, some-bar".to_string()
2162            );
2163
2164            assert_eq!(Some(super::TemplateType::Cdbs), editor.template_type());
2165
2166            assert_eq!(
2167                editor.commit().unwrap(),
2168                vec![
2169                    td.path().join("debian/control.in"),
2170                    td.path().join("debian/control")
2171                ]
2172            );
2173
2174            assert_eq!(
2175                r#"Source: blah
2176Testsuite: autopkgtest
2177Build-Depends: @cdbs@, libc6, some-bar
2178
2179"#,
2180                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2181            );
2182
2183            assert_eq!(
2184                r#"Source: blah
2185Testsuite: autopkgtest
2186Build-Depends: some-foo, libc6, some-bar
2187
2188"#,
2189                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2190            );
2191        }
2192
2193        #[test]
2194        #[ignore = "Not implemented yet"]
2195        fn test_description_stays_last() {
2196            let td = tempfile::tempdir().unwrap();
2197            std::fs::create_dir(td.path().join("debian")).unwrap();
2198            std::fs::write(
2199                td.path().join("debian/control"),
2200                r#"Source: blah
2201Testsuite: autopkgtest
2202
2203Package: libblah
2204Section: extra
2205Description: foo
2206 bar
2207
2208"#,
2209            )
2210            .unwrap();
2211
2212            let editor =
2213                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2214            editor
2215                .binaries()
2216                .find(|b| b.name().as_deref() == Some("libblah"))
2217                .unwrap()
2218                .set_architecture(Some("all"));
2219
2220            editor.commit().unwrap();
2221
2222            assert_eq!(
2223                r#"Source: blah
2224Testsuite: autopkgtest
2225
2226Package: libblah
2227Section: extra
2228Architecture: all
2229Description: foo
2230 bar
2231"#,
2232                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2233            );
2234        }
2235
2236        #[test]
2237        fn test_no_new_heading_whitespace() {
2238            let td = tempfile::tempdir().unwrap();
2239            std::fs::create_dir(td.path().join("debian")).unwrap();
2240            std::fs::write(
2241                td.path().join("debian/control"),
2242                r#"Source: blah
2243Build-Depends:
2244 debhelper-compat (= 11),
2245 uuid-dev
2246
2247"#,
2248            )
2249            .unwrap();
2250
2251            let editor =
2252                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2253            editor
2254                .source()
2255                .unwrap()
2256                .as_mut_deb822()
2257                .set("Build-Depends", "debhelper-compat (= 12),\nuuid-dev");
2258
2259            editor.commit().unwrap();
2260
2261            assert_eq!(
2262                r#"Source: blah
2263Build-Depends:
2264 debhelper-compat (= 12),
2265 uuid-dev
2266
2267"#,
2268                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2269            );
2270        }
2271    }
2272}