Skip to main content

debian_analyzer/
debcargo.rs

1//! debcargo.toml file manipulation
2
3// TODO: Reuse the debcargo crate for more of this.
4
5use debian_control::fields::MultiArch;
6use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use toml_edit::{value, DocumentMut, Table};
9
10pub use toml_edit;
11
12/// The default maintainer for Rust packages.
13pub const DEFAULT_MAINTAINER: &str =
14    "Debian Rust Maintainers <pkg-rust-maintainers@alioth-lists.debian.net>";
15
16/// The default section for Rust packages.
17pub const DEFAULT_SECTION: &str = "rust";
18
19/// The current standards version.
20pub const CURRENT_STANDARDS_VERSION: &str = "4.5.1";
21
22/// The default priority for Rust packages.
23pub const DEFAULT_PRIORITY: debian_control::Priority = debian_control::Priority::Optional;
24
25/// A wrapper around a debcargo.toml file.
26pub struct DebcargoEditor {
27    /// Path to the debcargo.toml file.
28    debcargo_toml_path: Option<PathBuf>,
29
30    /// The contents of the debcargo.toml file.
31    pub debcargo: DocumentMut,
32
33    /// The contents of the Cargo.toml file.
34    pub cargo: Option<DocumentMut>,
35}
36
37impl From<DocumentMut> for DebcargoEditor {
38    fn from(doc: DocumentMut) -> Self {
39        Self {
40            cargo: None,
41            debcargo_toml_path: None,
42            debcargo: doc,
43        }
44    }
45}
46
47impl Default for DebcargoEditor {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl DebcargoEditor {
54    /// Create a new DebcargoEditor with no contents.
55    pub fn new() -> Self {
56        Self {
57            debcargo_toml_path: None,
58            debcargo: DocumentMut::new(),
59            cargo: None,
60        }
61    }
62
63    /// Return the name of the crate.
64    fn crate_name(&self) -> Option<&str> {
65        self.cargo
66            .as_ref()
67            .and_then(|c| c["package"]["name"].as_str())
68    }
69
70    /// Return the version of the crate.
71    fn crate_version(&self) -> Option<semver::Version> {
72        self.cargo
73            .as_ref()
74            .and_then(|c| c["package"]["version"].as_str())
75            .map(|s| semver::Version::parse(s).unwrap())
76    }
77
78    /// Open a debcargo.toml file.
79    pub fn open(path: &Path) -> Result<Self, std::io::Error> {
80        let content = std::fs::read_to_string(path)?;
81        Ok(Self {
82            debcargo_toml_path: Some(path.to_path_buf()),
83            cargo: None,
84            debcargo: content.parse().unwrap(),
85        })
86    }
87
88    /// Open a debcargo.toml file in a directory.
89    pub fn from_directory(path: &std::path::Path) -> Result<Self, std::io::Error> {
90        let debcargo_toml_path = path.join("debian/debcargo.toml");
91        let debcargo_toml = std::fs::read_to_string(&debcargo_toml_path)?;
92        let cargo_toml = std::fs::read_to_string(path.join("Cargo.toml"))?;
93        Ok(Self {
94            debcargo_toml_path: Some(debcargo_toml_path),
95            debcargo: debcargo_toml.parse().unwrap(),
96            cargo: Some(cargo_toml.parse().unwrap()),
97        })
98    }
99
100    /// Commit changes to the debcargo.toml file.
101    pub fn commit(&self) -> std::io::Result<bool> {
102        let old_contents = std::fs::read_to_string(
103            self.debcargo_toml_path
104                .as_ref()
105                .expect("debcargo_toml_path should be set"),
106        )?;
107        let new_contents = self.debcargo.to_string();
108        if old_contents == new_contents {
109            return Ok(false);
110        }
111        std::fs::write(
112            self.debcargo_toml_path.as_ref().unwrap(),
113            new_contents.as_bytes(),
114        )?;
115        Ok(true)
116    }
117
118    /// Return the source package
119    pub fn source(&mut self) -> DebcargoSource<'_> {
120        DebcargoSource { main: self }
121    }
122
123    fn semver_suffix(&self) -> bool {
124        self.debcargo["source"]
125            .get("semver_suffix")
126            .and_then(|v| v.as_bool())
127            .unwrap_or(false)
128    }
129
130    /// Return an iterator over the binaries in the package.
131    pub fn binaries(&mut self) -> impl Iterator<Item = DebcargoBinary<'_>> {
132        let semver_suffix = self.semver_suffix();
133
134        let mut ret: HashMap<String, String> = HashMap::new();
135        ret.insert(
136            debcargo_binary_name(
137                self.crate_name().unwrap(),
138                &if semver_suffix {
139                    semver_pair(&self.crate_version().unwrap())
140                } else {
141                    "".to_string()
142                },
143            ),
144            "lib".to_string(),
145        );
146
147        if self.debcargo["bin"].as_bool().unwrap_or(!semver_suffix) {
148            let bin_name = self.debcargo["bin_name"]
149                .as_str()
150                .unwrap_or_else(|| self.crate_name().unwrap());
151            ret.insert(bin_name.to_owned(), "bin".to_string());
152        }
153
154        let global_summary = self.global_summary();
155        let global_description = self.global_description();
156        let crate_name = self.crate_name().unwrap().to_string();
157        let crate_version = self.crate_version().unwrap();
158        let features = self.features();
159
160        self.debcargo
161            .as_table_mut()
162            .iter_mut()
163            .filter_map(move |(key, item)| {
164                let kind = ret.remove(&key.to_string())?;
165                Some(DebcargoBinary::new(
166                    kind,
167                    key.to_string(),
168                    item.as_table_mut().unwrap(),
169                    global_summary.clone(),
170                    global_description.clone(),
171                    crate_name.clone(),
172                    crate_version.clone(),
173                    semver_suffix,
174                    features.clone(),
175                ))
176            })
177    }
178
179    fn global_summary(&self) -> Option<String> {
180        if let Some(summary) = self.debcargo.get("summary").and_then(|v| v.as_str()) {
181            Some(format!("{} - Rust source code", summary))
182        } else {
183            self.cargo.as_ref().and_then(|c| {
184                c["package"]
185                    .get("description")
186                    .and_then(|v| v.as_str())
187                    .map(|s| s.split('\n').next().unwrap().to_string())
188            })
189        }
190    }
191
192    fn global_description(&self) -> Option<String> {
193        self.debcargo
194            .get("description")
195            .and_then(|v| v.as_str())
196            .map(|description| description.to_owned())
197    }
198
199    fn features(&self) -> Option<HashSet<String>> {
200        self.cargo
201            .as_ref()
202            .and_then(|c| c["features"].as_table())
203            .map(|t| t.iter().map(|(k, _)| k.to_string()).collect())
204    }
205}
206
207/// The source package in a debcargo.toml file.
208pub struct DebcargoSource<'a> {
209    main: &'a mut DebcargoEditor,
210}
211
212impl DebcargoSource<'_> {
213    /// Return the source section of the debcargo.toml file.
214    pub fn toml_section_mut(&mut self) -> &mut Table {
215        if !self.main.debcargo.contains_key("source") {
216            self.main.debcargo["source"] = toml_edit::Item::Table(Table::new());
217        }
218        self.main.debcargo["source"].as_table_mut().unwrap()
219    }
220
221    /// Set the standards version.
222    pub fn set_standards_version(&mut self, version: &str) -> &mut Self {
223        self.toml_section_mut()["standards-version"] = value(version);
224        self
225    }
226
227    /// Return the standards version.
228    pub fn standards_version(&self) -> &str {
229        self.main
230            .debcargo
231            .get("source")
232            .and_then(|s| s.get("standards-version"))
233            .and_then(|v| v.as_str())
234            .unwrap_or(CURRENT_STANDARDS_VERSION)
235    }
236
237    /// Set the homepage.
238    pub fn set_homepage(&mut self, homepage: &str) -> &mut Self {
239        self.toml_section_mut()["homepage"] = value(homepage);
240        self
241    }
242
243    /// Return the homepage.
244    pub fn homepage(&self) -> Option<&str> {
245        let default_homepage = self
246            .main
247            .cargo
248            .as_ref()
249            .and_then(|c| c.get("package"))
250            .and_then(|x| x.get("homepage"))
251            .and_then(|v| v.as_str());
252        self.main
253            .debcargo
254            .get("source")
255            .and_then(|s| s.get("homepage"))
256            .and_then(|v| v.as_str())
257            .or(default_homepage)
258    }
259
260    /// Set the VCS Git URL.
261    pub fn set_vcs_git(&mut self, git: &str) -> &mut Self {
262        self.toml_section_mut()["vcs_git"] = value(git);
263        self
264    }
265
266    /// Return the VCS Git URL.
267    pub fn vcs_git(&self) -> Option<String> {
268        let default_git = self.main.crate_name().map(|c| {
269            format!(
270                "https://salsa.debian.org/rust-team/debcargo-conf.git [src/{}]",
271                c.to_lowercase()
272            )
273        });
274
275        self.main
276            .debcargo
277            .get("source")
278            .and_then(|s| s.get("vcs_git"))
279            .and_then(|v| v.as_str())
280            .map_or(default_git, |s| Some(s.to_string()))
281    }
282
283    /// Get the VCS browser URL.
284    pub fn vcs_browser(&self) -> Option<String> {
285        let default_vcs_browser = self.main.crate_name().map(|c| {
286            format!(
287                "https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/{}",
288                c.to_lowercase()
289            )
290        });
291
292        self.main
293            .debcargo
294            .get("source")
295            .and_then(|s| s.get("vcs_browser"))
296            .and_then(|v| v.as_str())
297            .map_or(default_vcs_browser, |s| Some(s.to_string()))
298    }
299
300    /// Set the VCS browser URL.
301    pub fn set_vcs_browser(&mut self, browser: &str) -> &mut Self {
302        self.toml_section_mut()["vcs_browser"] = value(browser);
303        self
304    }
305
306    /// Get the section.
307    pub fn section(&self) -> &str {
308        self.main
309            .debcargo
310            .get("source")
311            .and_then(|s| s.get("section"))
312            .and_then(|v| v.as_str())
313            .unwrap_or(DEFAULT_SECTION)
314    }
315
316    /// Set the section.
317    pub fn set_section(&mut self, section: &str) -> &mut Self {
318        self.toml_section_mut()["section"] = value(section);
319        self
320    }
321
322    /// Get the name of the package.
323    pub fn name(&self) -> Option<String> {
324        let crate_name = self.main.crate_name()?;
325        let semver_suffix = self.main.semver_suffix();
326        if semver_suffix {
327            let crate_version = self.main.crate_version()?;
328            Some(format!(
329                "rust-{}-{}",
330                debnormalize(crate_name),
331                semver_pair(&crate_version)
332            ))
333        } else {
334            Some(format!("rust-{}", debnormalize(crate_name)))
335        }
336    }
337
338    /// Get the priority.
339    pub fn priority(&self) -> debian_control::Priority {
340        self.main
341            .debcargo
342            .get("source")
343            .and_then(|s| s.get("priority"))
344            .and_then(|v| v.as_str())
345            .and_then(|s| s.parse().ok())
346            .unwrap_or(DEFAULT_PRIORITY)
347    }
348
349    /// Set the priority.
350    pub fn set_priority(&mut self, priority: debian_control::Priority) -> &mut Self {
351        self.toml_section_mut()["priority"] = value(priority.to_string());
352        self
353    }
354
355    /// Get whether the package build requires root.
356    pub fn rules_requires_root(&self) -> bool {
357        self.main
358            .debcargo
359            .get("source")
360            .and_then(|s| s.get("requires_root"))
361            .and_then(|v| v.as_bool())
362            .unwrap_or(false)
363    }
364
365    /// Set whether the package build requires root.
366    pub fn set_rules_requires_root(&mut self, requires_root: bool) -> &mut Self {
367        self.toml_section_mut()["requires_root"] = value(if requires_root { "yes" } else { "no" });
368        self
369    }
370
371    /// Get the maintainer.
372    pub fn maintainer(&self) -> &str {
373        self.main
374            .debcargo
375            .get("source")
376            .and_then(|s| s.get("maintainer"))
377            .and_then(|v| v.as_str())
378            .unwrap_or(DEFAULT_MAINTAINER)
379    }
380
381    /// Set the maintainer.
382    pub fn set_maintainer(&mut self, maintainer: &str) -> &mut Self {
383        self.toml_section_mut()["maintainer"] = value(maintainer);
384        self
385    }
386
387    /// Get the uploaders.
388    pub fn uploaders(&self) -> Option<Vec<String>> {
389        self.main
390            .debcargo
391            .get("source")
392            .and_then(|s| s.get("uploaders"))
393            .and_then(|x| x.as_array())
394            .map(|a| {
395                a.iter()
396                    .filter_map(|v| v.as_str())
397                    .map(|s| s.to_string())
398                    .collect()
399            })
400    }
401
402    /// Set the uploaders.
403    pub fn set_uploaders(&mut self, uploaders: Vec<String>) -> &mut Self {
404        let mut array = toml_edit::Array::new();
405        for u in uploaders {
406            array.push(u);
407        }
408        self.toml_section_mut()["uploaders"] = value(array);
409        self
410    }
411
412    /// Get the extra_lines field as a vector of strings.
413    pub fn extra_lines(&self) -> Vec<String> {
414        self.main
415            .debcargo
416            .get("source")
417            .and_then(|s| s.get("extra_lines"))
418            .and_then(|x| x.as_array())
419            .map(|a| {
420                a.iter()
421                    .filter_map(|v| v.as_str())
422                    .map(|s| s.to_string())
423                    .collect()
424            })
425            .unwrap_or_default()
426    }
427
428    /// Set the extra_lines field.
429    pub fn set_extra_lines(&mut self, lines: Vec<String>) -> &mut Self {
430        let mut array = toml_edit::Array::new();
431        for line in lines {
432            array.push(line);
433        }
434        self.toml_section_mut()["extra_lines"] = value(array);
435        self
436    }
437
438    /// Add a line to extra_lines if it doesn't already exist.
439    pub fn add_extra_line(&mut self, line: String) -> &mut Self {
440        let mut lines = self.extra_lines();
441        if !lines.contains(&line) {
442            lines.push(line);
443            self.set_extra_lines(lines);
444        }
445        self
446    }
447
448    /// Remove a line from extra_lines.
449    pub fn remove_extra_line(&mut self, line: &str) -> &mut Self {
450        let lines = self.extra_lines();
451        let filtered: Vec<String> = lines.into_iter().filter(|l| l != line).collect();
452        self.set_extra_lines(filtered);
453        self
454    }
455
456    /// Get a field value from extra_lines (for debian/control fields).
457    /// Looks for lines in the format "Field: value" and returns the value.
458    pub fn get_extra_field(&self, field_name: &str) -> Option<String> {
459        let prefix = format!("{}:", field_name);
460        self.extra_lines()
461            .iter()
462            .find(|line| line.starts_with(&prefix))
463            .map(|line| line[prefix.len()..].trim().to_string())
464    }
465
466    /// Set a field in extra_lines (for debian/control fields).
467    /// Updates existing field or adds new one if not present.
468    pub fn set_extra_field(&mut self, field_name: &str, value: &str) -> &mut Self {
469        let field_line = format!("{}: {}", field_name, value);
470        let prefix = format!("{}:", field_name);
471
472        let mut lines = self.extra_lines();
473        let mut found = false;
474
475        // Update existing field
476        for line in &mut lines {
477            if line.starts_with(&prefix) {
478                *line = field_line.clone();
479                found = true;
480                break;
481            }
482        }
483
484        // Add new field if not found
485        if !found {
486            lines.push(field_line);
487        }
488
489        self.set_extra_lines(lines);
490        self
491    }
492
493    /// Remove a field from extra_lines.
494    pub fn remove_extra_field(&mut self, field_name: &str) -> &mut Self {
495        let prefix = format!("{}:", field_name);
496        let lines = self.extra_lines();
497        let filtered: Vec<String> = lines
498            .into_iter()
499            .filter(|line| !line.starts_with(&prefix))
500            .collect();
501        self.set_extra_lines(filtered);
502        self
503    }
504
505    /// Set a VCS URL using the appropriate method.
506    /// Uses native fields for Git and Browser, extra_lines for others.
507    pub fn set_vcs_url(&mut self, vcs_type: &str, url: &str) -> &mut Self {
508        match vcs_type.to_lowercase().as_str() {
509            "git" => self.set_vcs_git(url),
510            "browser" => self.set_vcs_browser(url),
511            _ => self.set_extra_field(&format!("Vcs-{}", vcs_type), url),
512        }
513    }
514
515    /// Get a VCS URL using the appropriate method.
516    /// Uses native fields for Git and Browser, extra_lines for others.
517    pub fn get_vcs_url(&self, vcs_type: &str) -> Option<String> {
518        match vcs_type.to_lowercase().as_str() {
519            "git" => self.vcs_git(),
520            "browser" => self.vcs_browser(),
521            _ => self.get_extra_field(&format!("Vcs-{}", vcs_type)),
522        }
523    }
524}
525
526#[allow(dead_code)]
527/// A binary package in a debcargo.toml file.
528pub struct DebcargoBinary<'a> {
529    table: &'a mut Table,
530    key: String,
531    name: String,
532    section: String,
533    global_summary: Option<String>,
534    global_description: Option<String>,
535    crate_name: String,
536    crate_version: semver::Version,
537    semver_suffix: bool,
538    features: Option<HashSet<String>>,
539}
540
541impl<'a> DebcargoBinary<'a> {
542    fn new(
543        key: String,
544        name: String,
545        table: &'a mut Table,
546        global_summary: Option<String>,
547        global_description: Option<String>,
548        crate_name: String,
549        crate_version: semver::Version,
550        semver_suffix: bool,
551        features: Option<HashSet<String>>,
552    ) -> Self {
553        Self {
554            key: key.to_owned(),
555            name,
556            section: format!("packages.{}", key),
557            table,
558            global_summary,
559            global_description,
560            crate_name,
561            crate_version,
562            semver_suffix,
563            features,
564        }
565    }
566
567    /// Get the name of the binary package.
568    pub fn name(&self) -> &str {
569        &self.name
570    }
571
572    /// Get the architecture.
573    pub fn architecture(&self) -> Option<&str> {
574        Some("any")
575    }
576
577    /// Get the multi-architecture setting.
578    pub fn multi_arch(&self) -> Option<MultiArch> {
579        Some(MultiArch::Same)
580    }
581
582    /// Get the package section.
583    pub fn section(&self) -> Option<&str> {
584        self.table["section"].as_str()
585    }
586
587    /// Get the package summary.
588    pub fn summary(&self) -> Option<&str> {
589        if let Some(summary) = self.table.get("summary").and_then(|v| v.as_str()) {
590            Some(summary)
591        } else {
592            self.global_summary.as_deref()
593        }
594    }
595
596    /// Get the package long description.
597    pub fn long_description(&self) -> Option<String> {
598        if let Some(description) = self.table.get("description").and_then(|v| v.as_str()) {
599            Some(description.to_string())
600        } else if let Some(description) = self.global_description.as_ref() {
601            Some(description.clone())
602        } else {
603            match self.key.as_str() {
604                "lib" => Some(format!("Source code for Debianized Rust crate \"{}\"", self.crate_name)),
605                "bin" => Some("This package contains the source for the Rust mio crate, packaged by debcargo for use with cargo and dh-cargo.".to_string()),
606                _ => None,
607            }
608        }
609    }
610
611    /// Return the package description.
612    pub fn description(&self) -> Option<String> {
613        Some(crate::control::format_description(
614            self.summary()?,
615            self.long_description()?.lines().collect(),
616        ))
617    }
618
619    /// Get the extra dependencies.
620    pub fn depends(&self) -> Option<&str> {
621        self.table["depends"].as_str()
622    }
623
624    /// Get the extra recommends.
625    pub fn recommends(&self) -> Option<&str> {
626        self.table["recommends"].as_str()
627    }
628
629    /// Get the extra suggests.
630    pub fn suggests(&self) -> Option<&str> {
631        self.table["suggests"].as_str()
632    }
633
634    #[allow(dead_code)]
635    fn default_provides(&self) -> Option<String> {
636        let mut ret = HashSet::new();
637        let semver_suffix = self.semver_suffix;
638        let semver = &self.crate_version;
639
640        let mut suffixes = vec![];
641        if !semver_suffix {
642            suffixes.push("".to_string());
643        }
644
645        suffixes.push(format!("-{}", semver.major));
646        suffixes.push(format!("-{}.{}", semver.major, semver.minor));
647        suffixes.push(format!(
648            "-{}.{}.{}",
649            semver.major, semver.minor, semver.patch
650        ));
651        for ver_suffix in suffixes {
652            let mut feature_suffixes = HashSet::new();
653            feature_suffixes.insert("".to_string());
654            feature_suffixes.insert("+default".to_string());
655            feature_suffixes.extend(
656                self.features
657                    .as_ref()
658                    .map(|k| k.iter().map(|k| format!("+{}", k)).collect::<HashSet<_>>())
659                    .unwrap_or_default(),
660            );
661            for feature_suffix in feature_suffixes {
662                ret.insert(debcargo_binary_name(
663                    &self.crate_name,
664                    &format!("{}{}", ver_suffix, &feature_suffix),
665                ));
666            }
667        }
668        ret.remove(self.name());
669        if ret.is_empty() {
670            None
671        } else {
672            Some(format!(
673                "\n{}",
674                &ret.iter()
675                    .map(|s| format!("{} (= ${{binary:Version}})", s))
676                    .collect::<Vec<_>>()
677                    .join(",\n ")
678            ))
679        }
680    }
681}
682
683fn debnormalize(s: &str) -> String {
684    s.to_lowercase().replace('_', "-")
685}
686
687fn semver_pair(s: &semver::Version) -> String {
688    format!("{}.{}", s.major, s.minor)
689}
690
691fn debcargo_binary_name(crate_name: &str, suffix: &str) -> String {
692    format!("librust-{}{}-dev", debnormalize(crate_name), suffix)
693}
694
695/// Unmangle a debcargo version.
696pub fn unmangle_debcargo_version(version: &str) -> String {
697    version.replace("~", "-")
698}
699
700#[cfg(test)]
701mod tests {
702    #[test]
703    fn test_debcargo_binary_name() {
704        assert_eq!(super::debcargo_binary_name("foo", ""), "librust-foo-dev");
705        assert_eq!(
706            super::debcargo_binary_name("foo", "-1"),
707            "librust-foo-1-dev"
708        );
709        assert_eq!(
710            super::debcargo_binary_name("foo", "-1.2"),
711            "librust-foo-1.2-dev"
712        );
713        assert_eq!(
714            super::debcargo_binary_name("foo", "-1.2.3"),
715            "librust-foo-1.2.3-dev"
716        );
717    }
718
719    #[test]
720    fn test_semver_pair() {
721        assert_eq!(super::semver_pair(&"1.2.3".parse().unwrap()), "1.2");
722        assert_eq!(super::semver_pair(&"1.2.6".parse().unwrap()), "1.2");
723    }
724
725    #[test]
726    fn test_debnormalize() {
727        assert_eq!(super::debnormalize("foo_bar"), "foo-bar");
728        assert_eq!(super::debnormalize("foo"), "foo");
729    }
730
731    #[test]
732    fn test_debcargo_editor() {
733        let mut editor = super::DebcargoEditor::new();
734        editor.debcargo["source"]["standards-version"] = toml_edit::value("4.5.1");
735        editor.debcargo["source"]["homepage"] = toml_edit::value("https://example.com");
736        editor.debcargo["source"]["vcs_git"] = toml_edit::value("https://example.com");
737        editor.debcargo["source"]["vcs_browser"] = toml_edit::value("https://example.com");
738        editor.debcargo["source"]["section"] = toml_edit::value("notrust");
739        editor.debcargo["source"]["priority"] = toml_edit::value("optional");
740        editor.debcargo["source"]["requires_root"] = toml_edit::value("no");
741        editor.debcargo["source"]["maintainer"] =
742            toml_edit::value("Jelmer Vernooij <jelmer@debian.org>");
743
744        assert_eq!(editor.source().standards_version(), "4.5.1");
745        assert_eq!(
746            editor.source().vcs_git().as_deref(),
747            Some("https://example.com")
748        );
749        assert_eq!(
750            editor.source().vcs_browser().as_deref(),
751            Some("https://example.com")
752        );
753        assert_eq!(editor.source().section(), "notrust");
754        assert_eq!(editor.source().priority(), super::DEFAULT_PRIORITY);
755        assert!(!editor.source().rules_requires_root());
756        assert_eq!(
757            editor.source().maintainer(),
758            "Jelmer Vernooij <jelmer@debian.org>"
759        );
760        assert_eq!(editor.source().name(), None);
761        assert_eq!(editor.source().uploaders(), None);
762        assert_eq!(editor.source().homepage(), Some("https://example.com"));
763    }
764
765    #[test]
766    fn test_extra_lines_manipulation() {
767        let mut editor = super::DebcargoEditor::new();
768        let mut source = editor.source();
769
770        // Test initial state
771        assert_eq!(source.extra_lines(), Vec::<String>::new());
772
773        // Test set_extra_lines
774        source.set_extra_lines(vec![
775            "Vcs-Svn: https://svn.example.com/repo".to_string(),
776            "X-Custom: value".to_string(),
777        ]);
778        assert_eq!(
779            source.extra_lines(),
780            vec![
781                "Vcs-Svn: https://svn.example.com/repo".to_string(),
782                "X-Custom: value".to_string(),
783            ]
784        );
785
786        // Test add_extra_line
787        source.add_extra_line("Another-Field: another value".to_string());
788        assert_eq!(
789            source.extra_lines(),
790            vec![
791                "Vcs-Svn: https://svn.example.com/repo".to_string(),
792                "X-Custom: value".to_string(),
793                "Another-Field: another value".to_string(),
794            ]
795        );
796
797        // Test adding duplicate line (should not add)
798        source.add_extra_line("X-Custom: value".to_string());
799        assert_eq!(
800            source.extra_lines(),
801            vec![
802                "Vcs-Svn: https://svn.example.com/repo".to_string(),
803                "X-Custom: value".to_string(),
804                "Another-Field: another value".to_string(),
805            ]
806        );
807
808        // Test remove_extra_line
809        source.remove_extra_line("X-Custom: value");
810        assert_eq!(
811            source.extra_lines(),
812            vec![
813                "Vcs-Svn: https://svn.example.com/repo".to_string(),
814                "Another-Field: another value".to_string(),
815            ]
816        );
817    }
818
819    #[test]
820    fn test_extra_field_manipulation() {
821        let mut editor = super::DebcargoEditor::new();
822        let mut source = editor.source();
823
824        // Test initial state
825        assert_eq!(source.get_extra_field("Vcs-Svn"), None);
826
827        // Test set_extra_field
828        source.set_extra_field("Vcs-Svn", "https://svn.example.com/repo");
829        assert_eq!(
830            source.get_extra_field("Vcs-Svn"),
831            Some("https://svn.example.com/repo".to_string())
832        );
833
834        // Test updating existing field
835        source.set_extra_field("Vcs-Svn", "https://svn.example.com/new-repo");
836        assert_eq!(
837            source.get_extra_field("Vcs-Svn"),
838            Some("https://svn.example.com/new-repo".to_string())
839        );
840        // Should still have only one Vcs-Svn line
841        assert_eq!(
842            source.extra_lines(),
843            vec!["Vcs-Svn: https://svn.example.com/new-repo".to_string()]
844        );
845
846        // Test multiple fields
847        source.set_extra_field("X-Custom", "custom value");
848        assert_eq!(
849            source.get_extra_field("X-Custom"),
850            Some("custom value".to_string())
851        );
852        assert_eq!(
853            source.get_extra_field("Vcs-Svn"),
854            Some("https://svn.example.com/new-repo".to_string())
855        );
856        assert_eq!(
857            source.extra_lines(),
858            vec![
859                "Vcs-Svn: https://svn.example.com/new-repo".to_string(),
860                "X-Custom: custom value".to_string(),
861            ]
862        );
863
864        // Test remove_extra_field
865        source.remove_extra_field("Vcs-Svn");
866        assert_eq!(source.get_extra_field("Vcs-Svn"), None);
867        assert_eq!(
868            source.get_extra_field("X-Custom"),
869            Some("custom value".to_string())
870        );
871        assert_eq!(
872            source.extra_lines(),
873            vec!["X-Custom: custom value".to_string()]
874        );
875    }
876
877    #[test]
878    fn test_set_vcs_url() {
879        let mut editor = super::DebcargoEditor::new();
880        let mut source = editor.source();
881
882        // Test native Git field
883        source.set_vcs_url("Git", "https://github.com/example/repo.git");
884        assert_eq!(
885            source.vcs_git(),
886            Some("https://github.com/example/repo.git".to_string())
887        );
888
889        // Test native Browser field
890        source.set_vcs_url("Browser", "https://github.com/example/repo");
891        assert_eq!(
892            source.vcs_browser(),
893            Some("https://github.com/example/repo".to_string())
894        );
895
896        // Test non-native VCS type (should use extra_lines)
897        source.set_vcs_url("Svn", "https://svn.example.com/repo");
898        assert_eq!(
899            source.get_extra_field("Vcs-Svn"),
900            Some("https://svn.example.com/repo".to_string())
901        );
902
903        // Test another non-native VCS type
904        source.set_vcs_url("Bzr", "https://bzr.example.com/repo");
905        assert_eq!(
906            source.get_extra_field("Vcs-Bzr"),
907            Some("https://bzr.example.com/repo".to_string())
908        );
909
910        // Test case insensitivity for native fields
911        source.set_vcs_url("git", "https://gitlab.com/example/repo.git");
912        assert_eq!(
913            source.vcs_git(),
914            Some("https://gitlab.com/example/repo.git".to_string())
915        );
916
917        source.set_vcs_url("browser", "https://gitlab.com/example/repo");
918        assert_eq!(
919            source.vcs_browser(),
920            Some("https://gitlab.com/example/repo".to_string())
921        );
922    }
923
924    #[test]
925    fn test_extra_field_parsing() {
926        let mut editor = super::DebcargoEditor::new();
927        let mut source = editor.source();
928
929        // Test field with spaces after colon
930        source.set_extra_lines(vec!["Vcs-Svn:    https://svn.example.com/repo".to_string()]);
931        assert_eq!(
932            source.get_extra_field("Vcs-Svn"),
933            Some("https://svn.example.com/repo".to_string())
934        );
935
936        // Test field with no spaces after colon
937        source.set_extra_lines(vec!["Vcs-Bzr:https://bzr.example.com/repo".to_string()]);
938        assert_eq!(
939            source.get_extra_field("Vcs-Bzr"),
940            Some("https://bzr.example.com/repo".to_string())
941        );
942    }
943
944    #[test]
945    fn test_get_vcs_url() {
946        let mut editor = super::DebcargoEditor::new();
947        let mut source = editor.source();
948
949        // Set various VCS URLs
950        source.set_vcs_git("https://github.com/example/repo.git");
951        source.set_vcs_browser("https://github.com/example/repo");
952        source.set_extra_field("Vcs-Svn", "https://svn.example.com/repo");
953        source.set_extra_field("Vcs-Bzr", "https://bzr.example.com/repo");
954
955        // Test getting native Git field
956        assert_eq!(
957            source.get_vcs_url("Git"),
958            Some("https://github.com/example/repo.git".to_string())
959        );
960        assert_eq!(
961            source.get_vcs_url("git"),
962            Some("https://github.com/example/repo.git".to_string())
963        );
964
965        // Test getting native Browser field
966        assert_eq!(
967            source.get_vcs_url("Browser"),
968            Some("https://github.com/example/repo".to_string())
969        );
970        assert_eq!(
971            source.get_vcs_url("browser"),
972            Some("https://github.com/example/repo".to_string())
973        );
974
975        // Test getting non-native VCS types from extra_lines
976        assert_eq!(
977            source.get_vcs_url("Svn"),
978            Some("https://svn.example.com/repo".to_string())
979        );
980        assert_eq!(
981            source.get_vcs_url("Bzr"),
982            Some("https://bzr.example.com/repo".to_string())
983        );
984
985        // Test getting non-existent VCS type
986        assert_eq!(source.get_vcs_url("Hg"), None);
987    }
988}