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