apt_sources/
ppa.rs

1//!  This module provides functionality for handling Personal Package Archives (PPAs) in a
2//!  Debian/Ubuntu context.
3
4use url::Url;
5
6/// Default URL for Launchpad PPAs
7pub const LAUNCHPAD_PPA_URL: &str = "https://ppa.launchpadcontent.net";
8
9/// Valid components for PPAs
10pub const VALID_PPA_COMPONENTS: &[&str] = &["main", "main/debug"];
11
12/// Information about a PPA (Personal Package Archive)
13#[derive(Debug, Clone)]
14pub struct PpaInfo {
15    /// The PPA owner's username
16    pub user: String,
17    /// The PPA name
18    pub name: String,
19}
20
21impl PpaInfo {
22    /// Parse a PPA specification string (e.g., "ppa:user/ppa-name")
23    pub fn parse(ppa_spec: &str) -> Result<PpaInfo, String> {
24        if !ppa_spec.starts_with("ppa:") {
25            return Err("Not a PPA format".to_string());
26        }
27
28        let ppa_part = &ppa_spec[4..];
29        let parts: Vec<&str> = ppa_part.split('/').collect();
30
31        if parts.len() != 2 {
32            return Err("Invalid PPA format. Expected ppa:user/ppa-name".to_string());
33        }
34
35        Ok(PpaInfo {
36            user: parts[0].to_string(),
37            name: parts[1].to_string(),
38        })
39    }
40
41    /// Generate the repository URL for this PPA
42    pub fn repository_url(&self, _codename: &str) -> Result<Url, String> {
43        Url::parse(&format!(
44            "{}/{}/{}/ubuntu",
45            LAUNCHPAD_PPA_URL, self.user, self.name
46        ))
47        .map_err(|e| format!("Failed to construct PPA URL: {}", e))
48    }
49
50    /// Generate a filename for this PPA
51    pub fn filename(&self, extension: &str) -> String {
52        format!("{}-ubuntu-{}.{}", self.user, self.name, extension)
53    }
54}
55
56/// Validate PPA components
57pub fn validate_ppa_components(components: &[String]) -> Result<(), String> {
58    for component in components {
59        if !VALID_PPA_COMPONENTS.contains(&component.as_str()) {
60            return Err(format!(
61                "Invalid component '{}' for PPA.\n\
62                 Valid components are: {}\n\
63                 Suggestion: Use 'main' for regular packages or 'main/debug' for debug symbols.",
64                component,
65                VALID_PPA_COMPONENTS.join(", ")
66            ));
67        }
68    }
69    Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_parse_ppa_format() {
78        // Valid PPA
79        let ppa = PpaInfo::parse("ppa:user/repo").unwrap();
80        assert_eq!(ppa.user, "user");
81        assert_eq!(ppa.name, "repo");
82
83        // Invalid formats
84        assert!(PpaInfo::parse("not-a-ppa").is_err());
85        assert!(PpaInfo::parse("ppa:invalid").is_err());
86        assert!(PpaInfo::parse("ppa:too/many/parts").is_err());
87    }
88
89    #[test]
90    fn test_validate_ppa_components() {
91        assert!(validate_ppa_components(&["main".to_string()]).is_ok());
92        assert!(validate_ppa_components(&["main/debug".to_string()]).is_ok());
93        assert!(validate_ppa_components(&["invalid".to_string()]).is_err());
94    }
95
96    #[test]
97    fn test_ppa_filename() {
98        let ppa = PpaInfo {
99            user: "test-user".to_string(),
100            name: "test-repo".to_string(),
101        };
102
103        assert_eq!(ppa.filename("list"), "test-user-ubuntu-test-repo.list");
104        assert_eq!(
105            ppa.filename("sources"),
106            "test-user-ubuntu-test-repo.sources"
107        );
108
109        // Test with empty extension
110        assert_eq!(ppa.filename(""), "test-user-ubuntu-test-repo.");
111
112        // Test with special characters (they remain as-is)
113        let ppa_special = PpaInfo {
114            user: "user_123".to_string(),
115            name: "repo-name".to_string(),
116        };
117        assert_eq!(
118            ppa_special.filename("list"),
119            "user_123-ubuntu-repo-name.list"
120        );
121    }
122}