//! APT sources.list management //! Provides a simple structure for managing APT repository sources use crate::context; use std::error::Error; use std::path::Path; use std::sync::Arc; /// Represents a single source entry in sources.list #[derive(Debug, Clone)] pub struct SourceEntry { /// Is the source enabled? pub enabled: bool, /// Source components (universe, main, contrib) pub components: Vec, /// Source architectures (amd64, riscv64, arm64) pub architectures: Vec, /// Source URI pub uri: String, /// Source suites (series-pocket) pub suite: Vec, } impl SourceEntry { /// Parse a string describing a source entry in deb822 format pub fn from_deb822(data: &str) -> Option { let mut current_entry = SourceEntry { enabled: true, components: Vec::new(), architectures: Vec::new(), uri: String::new(), suite: Vec::new(), }; for line in data.lines() { let line = line.trim(); if line.starts_with('#') { continue; } // Empty line: end of an entry, or beginning if line.is_empty() { if !current_entry.uri.is_empty() { return Some(current_entry); } else { continue; } } if let Some((key, value)) = line.split_once(':') { let key = key.trim(); let value = value.trim(); match key { "Types" => { // We only care about deb types } "URIs" => current_entry.uri = value.to_string(), "Suites" => { current_entry.suite = value.split_whitespace().map(|s| s.to_string()).collect(); } "Components" => { current_entry.components = value.split_whitespace().map(|s| s.to_string()).collect(); } "Architectures" => { current_entry.architectures = value.split_whitespace().map(|s| s.to_string()).collect(); } _ => {} } } } // End of entry, or empty file? if !current_entry.uri.is_empty() { Some(current_entry) } else { None } } /// Parse a line describing a legacy source entry pub fn from_legacy(data: &str) -> Option { let line = data.lines().next()?.trim(); if line.is_empty() || line.starts_with("#") { return None; } // Parse legacy deb line format: deb [arch=... / signed_by=] uri suite [components...] // Extract bracket parameters first let mut architectures = Vec::new(); let mut line_without_brackets = line.to_string(); // Find and process bracket parameters if let Some(start_bracket) = line.find('[') && let Some(end_bracket) = line.find(']') { let bracket_content = &line[start_bracket + 1..end_bracket]; // Parse parameters inside brackets for param in bracket_content.split_whitespace() { if param.starts_with("arch=") { let arch_values = param.split('=').nth(1).unwrap_or(""); architectures = arch_values .split(',') .map(|s| s.trim().to_string()) .collect(); } // signed-by parameter is parsed but not stored } // Remove the bracket section from the line line_without_brackets = line[..start_bracket].to_string() + &line[end_bracket + 1..]; } // Trim and split the remaining line let line_without_brackets = line_without_brackets.trim(); let parts: Vec<&str> = line_without_brackets.split_whitespace().collect(); // We need at least: deb, uri, suite if parts.len() < 3 || parts[0] != "deb" { return None; } let uri = parts[1].to_string(); let suite = vec![parts[2].to_string()]; let components: Vec = parts[3..].iter().map(|&s| s.to_string()).collect(); Some(SourceEntry { enabled: true, components, architectures, uri, suite, }) } /// Convert this source entry to legacy format pub fn to_legacy(&self) -> String { let mut result = String::new(); // Legacy entries contain one suite per line for suite in &self.suite { // Start with "deb" type result.push_str("deb"); // Add architectures if present if !self.architectures.is_empty() { result.push_str(" [arch="); result.push_str(&self.architectures.join(",")); result.push(']'); } // Add URI and suite result.push(' '); result.push_str(&self.uri); result.push(' '); result.push_str(suite); // Add components if !self.components.is_empty() { result.push(' '); result.push_str(&self.components.join(" ")); } result.push('\n'); } result } } /// Parse a 'source list' string in deb822 format into a SourceEntry vector pub fn parse_deb822(data: &str) -> Vec { data.split("\n\n") .flat_map(SourceEntry::from_deb822) .collect() } /// Parse a 'source list' string in legacy format into a SourceEntry vector pub fn parse_legacy(data: &str) -> Vec { data.split("\n") .flat_map(SourceEntry::from_legacy) .collect() } /// Load sources from context (or current context by default) pub fn load(ctx: Option>) -> Result, Box> { let mut sources = Vec::new(); let ctx = ctx.unwrap_or_else(context::current); // Try DEB822 format first (Ubuntu 24.04+ and Debian Trixie+) if let Ok(entries) = load_deb822(&ctx, "/etc/apt/sources.list.d/ubuntu.sources") { sources.extend(entries); } else if let Ok(entries) = load_deb822(&ctx, "/etc/apt/sources.list.d/debian.sources") { sources.extend(entries); } // Fall back to legacy format if let Ok(entries) = load_legacy(&ctx, "/etc/apt/sources.list") { sources.extend(entries); } Ok(sources) } /// Save sources back to context pub fn save_legacy( ctx: Option>, sources: Vec, path: &str, ) -> Result<(), Box> { let ctx = if let Some(c) = ctx { c } else { context::current() }; let content = sources .into_iter() .map(|s| s.to_legacy()) .collect::>() .join("\n"); ctx.write_file(Path::new(path), &content)?; Ok(()) } /// Load sources from DEB822 format fn load_deb822(ctx: &context::Context, path: &str) -> Result, Box> { let path = Path::new(path); if path.exists() { let content = ctx.read_file(path)?; return Ok(parse_deb822(&content)); } Ok(Vec::new()) } /// Load sources from legacy format fn load_legacy(ctx: &context::Context, path: &str) -> Result, Box> { let path = Path::new(path); if path.exists() { let content = ctx.read_file(path)?; return Ok(content.lines().flat_map(SourceEntry::from_legacy).collect()); } Ok(Vec::new()) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_parse_deb822() { let deb822 = "\ Types: deb\n\ URIs: http://fr.archive.ubuntu.com/ubuntu/\n\ Suites: questing questing-updates questing-backports\n\ Components: main restricted universe multiverse\n\ Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n\ Architectures: amd64\n\ \n\ Types: deb\n\ URIs: http://security.ubuntu.com/ubuntu/\n\ Suites: questing-security\n\ Components: main restricted universe multiverse\n\ Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n\ Architectures: amd64\n\ \n\ Types: deb\n\ URIs: http://ports.ubuntu.com/ubuntu-ports/\n\ Suites: questing questing-updates questing-backports\n\ Components: main restricted universe multiverse\n\ Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n\ Architectures: riscv64\n\ "; let sources = parse_deb822(deb822); assert_eq!(sources.len(), 3); assert_eq!(sources[0].uri, "http://fr.archive.ubuntu.com/ubuntu/"); assert_eq!(sources[0].architectures, vec!["amd64"]); assert_eq!( sources[0].suite, vec!["questing", "questing-updates", "questing-backports"] ); assert_eq!( sources[0].components, vec!["main", "restricted", "universe", "multiverse"] ); assert_eq!(sources[1].uri, "http://security.ubuntu.com/ubuntu/"); assert_eq!(sources[1].architectures, vec!["amd64"]); assert_eq!(sources[1].suite, vec!["questing-security"]); assert_eq!( sources[1].components, vec!["main", "restricted", "universe", "multiverse"] ); assert_eq!(sources[2].uri, "http://ports.ubuntu.com/ubuntu-ports/"); assert_eq!(sources[2].architectures.len(), 1); assert_eq!(sources[2].architectures, vec!["riscv64"]); assert_eq!( sources[2].suite, vec!["questing", "questing-updates", "questing-backports"] ); assert_eq!( sources[2].components, vec!["main", "restricted", "universe", "multiverse"] ); } #[tokio::test] async fn test_parse_legacy() { let legacy = "\ deb [signed-by=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\" arch=amd64] http://archive.ubuntu.com/ubuntu resolute main universe\n\ deb [arch=amd64,i386 signed-by=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\"] http://archive.ubuntu.com/ubuntu resolute-updates main\n\ deb [signed-by=\"/usr/share/keyrings/ubuntu-archive-keyring.gpg\"] http://security.ubuntu.com/ubuntu resolute-security main\n\ "; let sources = parse_legacy(legacy); assert_eq!(sources.len(), 3); assert_eq!(sources[0].uri, "http://archive.ubuntu.com/ubuntu"); assert_eq!(sources[0].suite, vec!["resolute"]); assert_eq!(sources[0].components, vec!["main", "universe"]); assert_eq!(sources[0].architectures, vec!["amd64"]); assert_eq!(sources[1].uri, "http://archive.ubuntu.com/ubuntu"); assert_eq!(sources[1].suite, vec!["resolute-updates"]); assert_eq!(sources[1].components, vec!["main"]); assert_eq!(sources[1].architectures, vec!["amd64", "i386"]); assert_eq!(sources[2].uri, "http://security.ubuntu.com/ubuntu"); assert_eq!(sources[2].suite, vec!["resolute-security"]); assert_eq!(sources[2].components, vec!["main"]); } }