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