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