debian_analyzer/
abstract_control.rs

1//! Abstract interface for editing debian packages, whether backed by real control files or
2//! debcargo files.
3use crate::relations::ensure_relation;
4use debian_control::lossless::relations::{Entry, Relations};
5use std::path::Path;
6
7/// Interface for editing debian packages, whether backed by real control files or debcargo files.
8pub trait AbstractControlEditor {
9    /// Get the source package.
10    fn source<'a>(&'a mut self) -> Option<Box<dyn AbstractSource<'a> + 'a>>;
11
12    /// Get the binary packages.
13    fn binaries<'a>(&'a mut self) -> Vec<Box<dyn AbstractBinary + 'a>>;
14
15    /// Commit the changes.
16    fn commit(&self) -> bool;
17
18    /// Wrap and sort the control file.
19    fn wrap_and_sort(&mut self);
20}
21
22/// An abstract source package.
23pub trait AbstractSource<'a> {
24    /// Get the name of the source package.
25    fn name(&self) -> Option<String>;
26
27    /// Ensure that a build dependency is present.
28    fn ensure_build_dep(&mut self, dep: Entry);
29
30    /// Set the maintainer of the source package.
31    fn set_maintainer(&mut self, maintainer: &str);
32
33    /// Set the uploaders of the source package.
34    fn set_uploaders(&mut self, uploaders: &[&str]);
35
36    /// Set the VCS URL for the source package.
37    fn set_vcs_url(&mut self, vcs_type: &str, url: &str);
38
39    /// Get the VCS URL for the source package.
40    fn get_vcs_url(&self, vcs_type: &str) -> Option<String>;
41}
42
43/// An abstract binary package.
44pub trait AbstractBinary {
45    /// Get the name of the binary package.
46    fn name(&self) -> Option<String>;
47}
48
49use crate::debcargo::{DebcargoBinary, DebcargoEditor, DebcargoSource};
50use debian_control::{Binary as PlainBinary, Control as PlainControl, Source as PlainSource};
51
52impl AbstractControlEditor for DebcargoEditor {
53    fn source<'a>(&'a mut self) -> Option<Box<dyn AbstractSource<'a> + 'a>> {
54        Some(Box::new(DebcargoEditor::source(self)) as Box<dyn AbstractSource<'a>>)
55    }
56
57    fn binaries<'a>(&'a mut self) -> Vec<Box<dyn AbstractBinary + 'a>> {
58        DebcargoEditor::binaries(self)
59            .map(|b| Box::new(b) as Box<dyn AbstractBinary>)
60            .collect()
61    }
62
63    fn commit(&self) -> bool {
64        DebcargoEditor::commit(self).unwrap()
65    }
66
67    fn wrap_and_sort(&mut self) {}
68}
69
70impl AbstractBinary for PlainBinary {
71    fn name(&self) -> Option<String> {
72        self.name()
73    }
74}
75
76impl AbstractSource<'_> for PlainSource {
77    fn name(&self) -> Option<String> {
78        self.name()
79    }
80
81    fn ensure_build_dep(&mut self, dep: Entry) {
82        if let Some(mut build_deps) = self.build_depends() {
83            ensure_relation(&mut build_deps, dep);
84            self.set_build_depends(&build_deps);
85        } else {
86            self.set_build_depends(&Relations::from(vec![dep]));
87        }
88    }
89
90    fn set_maintainer(&mut self, maintainer: &str) {
91        (self as &mut debian_control::lossless::Source).set_maintainer(maintainer);
92    }
93
94    fn set_uploaders(&mut self, uploaders: &[&str]) {
95        (self as &mut debian_control::lossless::Source).set_uploaders(uploaders);
96    }
97
98    fn set_vcs_url(&mut self, vcs_type: &str, url: &str) {
99        let field_name = format!("Vcs-{}", vcs_type);
100        self.as_mut_deb822().set(&field_name, url);
101    }
102
103    fn get_vcs_url(&self, vcs_type: &str) -> Option<String> {
104        let field_name = format!("Vcs-{}", vcs_type);
105        self.as_deb822().get(&field_name)
106    }
107}
108
109impl AbstractBinary for DebcargoBinary<'_> {
110    fn name(&self) -> Option<String> {
111        Some(self.name().to_string())
112    }
113}
114
115impl<'a> AbstractSource<'a> for DebcargoSource<'a> {
116    fn name(&self) -> Option<String> {
117        self.name()
118    }
119
120    fn ensure_build_dep(&mut self, dep: Entry) {
121        // TODO: Check that it's not already there
122        if let Some(build_deps) = self
123            .toml_section_mut()
124            .get_mut("build_depends")
125            .and_then(|v| v.as_array_mut())
126        {
127            build_deps.push(dep.to_string());
128        }
129    }
130
131    fn set_maintainer(&mut self, maintainer: &str) {
132        (self as &mut crate::debcargo::DebcargoSource).set_maintainer(maintainer);
133    }
134
135    fn set_uploaders(&mut self, uploaders: &[&str]) {
136        (self as &mut crate::debcargo::DebcargoSource)
137            .set_uploaders(uploaders.iter().map(|s| s.to_string()).collect::<Vec<_>>());
138    }
139
140    fn set_vcs_url(&mut self, vcs_type: &str, url: &str) {
141        (self as &mut crate::debcargo::DebcargoSource).set_vcs_url(vcs_type, url);
142    }
143
144    fn get_vcs_url(&self, vcs_type: &str) -> Option<String> {
145        match vcs_type.to_lowercase().as_str() {
146            "git" => self.vcs_git(),
147            "browser" => self.vcs_browser(),
148            _ => self.get_extra_field(&format!("Vcs-{}", vcs_type)),
149        }
150    }
151}
152
153impl<E: crate::editor::Editor<PlainControl>> AbstractControlEditor for E {
154    fn source<'a>(&'a mut self) -> Option<Box<dyn AbstractSource<'a> + 'a>> {
155        PlainControl::source(self).map(|s| Box::new(s) as Box<dyn AbstractSource>)
156    }
157
158    fn binaries<'a>(&'a mut self) -> Vec<Box<dyn AbstractBinary + 'a>> {
159        PlainControl::binaries(self)
160            .map(|b| Box::new(b) as Box<dyn AbstractBinary>)
161            .collect()
162    }
163
164    fn commit(&self) -> bool {
165        !(self as &dyn crate::editor::Editor<PlainControl>)
166            .commit()
167            .unwrap()
168            .is_empty()
169    }
170
171    fn wrap_and_sort(&mut self) {
172        (self as &mut dyn crate::editor::Editor<PlainControl>).wrap_and_sort(
173            deb822_lossless::Indentation::Spaces(4),
174            false,
175            None,
176        )
177    }
178}
179
180/// Open a control file for editing.
181pub fn edit_control<'a>(
182    tree: &dyn breezyshim::workingtree::WorkingTree,
183    subpath: &Path,
184) -> Result<Box<dyn AbstractControlEditor + 'a>, crate::editor::EditorError> {
185    if tree.has_filename(&subpath.join("debian/debcargo.toml")) {
186        Ok(Box::new(crate::debcargo::DebcargoEditor::from_directory(
187            &tree.abspath(subpath).unwrap(),
188        )?))
189    } else {
190        let control_path = tree.abspath(&subpath.join(std::path::Path::new("debian/control")));
191        Ok(Box::new(crate::control::TemplatedControlEditor::open(
192            control_path.unwrap(),
193        )?) as Box<dyn AbstractControlEditor>)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
200    use breezyshim::prelude::*;
201    use std::path::Path;
202    use std::str::FromStr;
203
204    #[test]
205    fn test_edit_control_debcargo() {
206        let td = tempfile::tempdir().unwrap();
207        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
208        // Write dummy debcargo.toml
209        tree.mkdir(Path::new("debian")).unwrap();
210        std::fs::write(
211            td.path().join("debian/debcargo.toml"),
212            br#"
213maintainer = "Alice <alice@example.com>"
214homepage = "https://example.com"
215description = "Example package"
216"#,
217        )
218        .unwrap();
219
220        std::fs::write(
221            td.path().join("Cargo.toml"),
222            br#"
223[package]
224name = "example"
225version = "0.1.0"
226edition = "2018"
227"#,
228        )
229        .unwrap();
230
231        tree.add(&[(Path::new("debian")), (Path::new("debian/debcargo.toml"))])
232            .unwrap();
233
234        let editor = super::edit_control(&tree, Path::new("")).unwrap();
235
236        editor.commit();
237    }
238
239    #[test]
240    fn test_edit_control_regular() {
241        let td = tempfile::tempdir().unwrap();
242        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
243        // Write dummy debian/control
244        tree.mkdir(Path::new("debian")).unwrap();
245        tree.put_file_bytes_non_atomic(
246            Path::new("debian/control"),
247            br#"
248Source: example
249Maintainer: Alice <alice@example.com>
250Homepage: https://example.com
251
252Package: example
253Architecture: any
254Description: Example package
255"#,
256        )
257        .unwrap();
258
259        tree.add(&[(Path::new("debian")), (Path::new("debian/control"))])
260            .unwrap();
261
262        let editor = super::edit_control(&tree, Path::new("")).unwrap();
263
264        editor.commit();
265    }
266
267    #[test]
268    fn test_edit_source_ensure_build_depends() {
269        let td = tempfile::tempdir().unwrap();
270        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
271        // Write dummy debian/control
272        tree.mkdir(Path::new("debian")).unwrap();
273        tree.put_file_bytes_non_atomic(
274            Path::new("debian/control"),
275            br#"
276Source: example
277Maintainer: Alice <alice@example.com>
278Build-Depends: libc6
279
280Package: example
281Architecture: any
282Description: Example package
283"#,
284        )
285        .unwrap();
286        tree.add(&[Path::new("debian/control")]).unwrap();
287
288        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
289        let mut source = editor.source().unwrap();
290        source.ensure_build_dep(
291            debian_control::lossless::relations::Entry::from_str("libssl-dev").unwrap(),
292        );
293        std::mem::drop(source);
294        editor.commit();
295
296        let text = tree.get_file_text(Path::new("debian/control")).unwrap();
297        assert_eq!(
298            std::str::from_utf8(&text).unwrap(),
299            r#"
300Source: example
301Maintainer: Alice <alice@example.com>
302Build-Depends: libc6, libssl-dev
303
304Package: example
305Architecture: any
306Description: Example package
307"#
308        );
309    }
310
311    #[test]
312    fn test_abstract_source_set_vcs_url_plain() {
313        let td = tempfile::tempdir().unwrap();
314        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
315        // Write dummy debian/control
316        tree.mkdir(Path::new("debian")).unwrap();
317        tree.put_file_bytes_non_atomic(
318            Path::new("debian/control"),
319            br#"Source: example
320Maintainer: Alice <alice@example.com>
321
322Package: example
323Architecture: any
324Description: Example package
325"#,
326        )
327        .unwrap();
328        tree.add(&[Path::new("debian/control")]).unwrap();
329
330        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
331        let mut source = editor.source().unwrap();
332
333        // Test setting various VCS URLs
334        source.set_vcs_url("Git", "https://github.com/example/repo.git");
335        source.set_vcs_url("Browser", "https://github.com/example/repo");
336
337        std::mem::drop(source);
338        editor.commit();
339
340        let text = tree.get_file_text(Path::new("debian/control")).unwrap();
341        assert_eq!(
342            std::str::from_utf8(&text).unwrap(),
343            r#"Source: example
344Maintainer: Alice <alice@example.com>
345Vcs-Git: https://github.com/example/repo.git
346Vcs-Browser: https://github.com/example/repo
347
348Package: example
349Architecture: any
350Description: Example package
351"#
352        );
353    }
354
355    #[test]
356    fn test_abstract_source_set_vcs_url_debcargo() {
357        let td = tempfile::tempdir().unwrap();
358        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
359        // Write dummy debcargo.toml
360        tree.mkdir(Path::new("debian")).unwrap();
361        std::fs::write(
362            td.path().join("debian/debcargo.toml"),
363            br#"maintainer = "Alice <alice@example.com>"
364"#,
365        )
366        .unwrap();
367
368        std::fs::write(
369            td.path().join("Cargo.toml"),
370            br#"[package]
371name = "example"
372version = "0.1.0"
373"#,
374        )
375        .unwrap();
376
377        tree.add(&[(Path::new("debian")), (Path::new("debian/debcargo.toml"))])
378            .unwrap();
379
380        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
381        let mut source = editor.source().unwrap();
382
383        // Test setting native VCS URLs
384        source.set_vcs_url("Git", "https://github.com/example/repo.git");
385        source.set_vcs_url("Browser", "https://github.com/example/repo");
386
387        // Test setting non-native VCS URL
388        source.set_vcs_url("Svn", "https://svn.example.com/repo");
389
390        std::mem::drop(source);
391        editor.commit();
392
393        // Read back the debcargo.toml to verify
394        let content = std::fs::read_to_string(td.path().join("debian/debcargo.toml")).unwrap();
395        assert_eq!(
396            content,
397            r#"maintainer = "Alice <alice@example.com>"
398
399[source]
400vcs_git = "https://github.com/example/repo.git"
401vcs_browser = "https://github.com/example/repo"
402extra_lines = ["Vcs-Svn: https://svn.example.com/repo"]
403"#
404        );
405    }
406
407    #[test]
408    fn test_abstract_source_get_vcs_url_plain() {
409        let td = tempfile::tempdir().unwrap();
410        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
411        // Write dummy debian/control with VCS fields
412        tree.mkdir(Path::new("debian")).unwrap();
413        tree.put_file_bytes_non_atomic(
414            Path::new("debian/control"),
415            br#"Source: example
416Maintainer: Alice <alice@example.com>
417Vcs-Git: https://github.com/example/repo.git
418Vcs-Browser: https://github.com/example/repo
419Vcs-Svn: https://svn.example.com/repo
420
421Package: example
422Architecture: any
423Description: Example package
424"#,
425        )
426        .unwrap();
427        tree.add(&[Path::new("debian/control")]).unwrap();
428
429        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
430        let source = editor.source().unwrap();
431
432        // Test getting various VCS URLs
433        assert_eq!(
434            source.get_vcs_url("Git"),
435            Some("https://github.com/example/repo.git".to_string())
436        );
437        assert_eq!(
438            source.get_vcs_url("Browser"),
439            Some("https://github.com/example/repo".to_string())
440        );
441        assert_eq!(
442            source.get_vcs_url("Svn"),
443            Some("https://svn.example.com/repo".to_string())
444        );
445        assert_eq!(source.get_vcs_url("Bzr"), None);
446    }
447
448    #[test]
449    fn test_abstract_source_get_vcs_url_debcargo() {
450        let td = tempfile::tempdir().unwrap();
451        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
452        // Write dummy debcargo.toml with VCS fields
453        tree.mkdir(Path::new("debian")).unwrap();
454        std::fs::write(
455            td.path().join("debian/debcargo.toml"),
456            br#"maintainer = "Alice <alice@example.com>"
457
458[source]
459vcs_git = "https://github.com/example/repo.git"
460vcs_browser = "https://github.com/example/repo"
461extra_lines = ["Vcs-Svn: https://svn.example.com/repo", "Vcs-Bzr: https://bzr.example.com/repo"]
462"#,
463        )
464        .unwrap();
465
466        std::fs::write(
467            td.path().join("Cargo.toml"),
468            br#"[package]
469name = "example"
470version = "0.1.0"
471"#,
472        )
473        .unwrap();
474
475        tree.add(&[(Path::new("debian")), (Path::new("debian/debcargo.toml"))])
476            .unwrap();
477
478        let mut editor = super::edit_control(&tree, Path::new("")).unwrap();
479        let source = editor.source().unwrap();
480
481        // Test getting native VCS URLs
482        assert_eq!(
483            source.get_vcs_url("Git"),
484            Some("https://github.com/example/repo.git".to_string())
485        );
486        assert_eq!(
487            source.get_vcs_url("Browser"),
488            Some("https://github.com/example/repo".to_string())
489        );
490
491        // Test getting non-native VCS URLs from extra_lines
492        assert_eq!(
493            source.get_vcs_url("Svn"),
494            Some("https://svn.example.com/repo".to_string())
495        );
496        assert_eq!(
497            source.get_vcs_url("Bzr"),
498            Some("https://bzr.example.com/repo".to_string())
499        );
500
501        // Test getting non-existent VCS URL
502        assert_eq!(source.get_vcs_url("Hg"), None);
503    }
504}