Files
pkh/src/apt/sources.rs

337 lines
11 KiB
Rust

//! 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<String>,
/// Source architectures (amd64, riscv64, arm64)
pub architectures: Vec<String>,
/// Source URI
pub uri: String,
/// Source suites (series-pocket)
pub suite: Vec<String>,
}
impl SourceEntry {
/// Parse a string describing a source entry in deb822 format
pub fn from_deb822(data: &str) -> Option<Self> {
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<Self> {
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<String> = 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<SourceEntry> {
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<SourceEntry> {
data.split("\n")
.flat_map(SourceEntry::from_legacy)
.collect()
}
/// Load sources from context (or current context by default)
pub fn load(ctx: Option<Arc<crate::context::Context>>) -> Result<Vec<SourceEntry>, Box<dyn Error>> {
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<Arc<crate::context::Context>>,
sources: Vec<SourceEntry>,
path: &str,
) -> Result<(), Box<dyn Error>> {
let ctx = if let Some(c) = ctx {
c
} else {
context::current()
};
let content = sources
.into_iter()
.map(|s| s.to_legacy())
.collect::<Vec<_>>()
.join("\n");
ctx.write_file(Path::new(path), &content)?;
Ok(())
}
/// Load sources from DEB822 format
fn load_deb822(ctx: &context::Context, path: &str) -> Result<Vec<SourceEntry>, Box<dyn Error>> {
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<Vec<SourceEntry>, Box<dyn Error>> {
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"]);
}
}