use std::collections::HashMap;
use std::fs;
use std::path::Path;
use color_eyre::eyre::{Result, eyre};
use crate::models::{RepoConfig, ChannelConfig, PackageFormat};
use glob;
use crate::lfs;
fn subst_var(input: &str, arch: &str) -> String {
input.replace("$(ARCH)", arch)
}
fn parse_sources_list_line(line: &str, arch: &str) -> Result<Option<RepoConfig>> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return Ok(None);
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 || parts[0] != "deb" {
return Err(eyre!("Malformed line: expected 'deb' type"));
}
let mut options = std::collections::HashMap::new();
let mut start_idx = 1;
if parts.len() > 1 && parts[1].starts_with('[') {
let mut bracket_end = 1;
let mut option_str = String::new();
while bracket_end < parts.len() {
option_str.push_str(parts[bracket_end]);
option_str.push(' ');
if parts[bracket_end].ends_with(']') {
break;
}
bracket_end += 1;
}
if bracket_end >= parts.len() || !option_str.trim().ends_with(']') {
return Err(eyre!("Malformed options: missing closing bracket"));
}
let option_content = &option_str.trim()[1..option_str.trim().len()-1];
let option_parts: Vec<&str> = option_content.split_whitespace().collect();
for option in option_parts {
if let Some(eq_pos) = option.find('=') {
let key = option[..eq_pos].to_string();
let value = option[eq_pos+1..].to_string();
if key.is_empty() {
return Err(eyre!("Malformed option: empty key"));
}
if value.is_empty() {
return Err(eyre!("Malformed option: empty value"));
}
options.insert(key, value);
} else {
return Err(eyre!("Malformed option: missing '='"));
}
}
start_idx = bracket_end + 1;
}
if start_idx + 1 >= parts.len() {
return Err(eyre!("Malformed line: missing URI or suite"));
}
let uri = subst_var(parts[start_idx], arch);
let suite = subst_var(parts[start_idx + 1], arch);
let components = if suite.ends_with('/') {
if start_idx + 2 < parts.len() {
return Err(eyre!("Malformed absolute suite: components not allowed"));
}
Vec::new()
} else {
parts[start_idx + 2..].iter().map(|s| s.to_string()).collect()
};
let package_baseurl = uri.trim_end_matches('/').to_string();
let index_url = if suite.ends_with('/') {
format!("{}/{}Release", package_baseurl, suite)
} else {
format!("{}/dists/{}/Release", package_baseurl, suite)
};
Ok(Some(RepoConfig {
index_url,
package_baseurl,
components,
..RepoConfig::default()
}))
}
fn try_add_stanza(current_repo: &mut HashMap<String, String>, repos: &mut Vec<RepoConfig>, arch: &str, path: &Path) {
let has_uris = current_repo.contains_key("URIs");
let has_suites = current_repo.contains_key("Suites");
if !has_uris || !has_suites {
return;
}
repos.extend(extract_repos_from_deb822(current_repo, arch, path));
current_repo.clear();
}
fn parse_deb822_content(content: &str, arch: &str, path: &Path) -> Result<Vec<RepoConfig>> {
let mut repos = Vec::new();
let mut current_repo: HashMap<String, String> = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
if key.is_empty() {
if let Some(last_value) = current_repo.values_mut().last() {
last_value.push(' ');
last_value.push_str(value);
}
continue;
}
if current_repo.contains_key(key) {
eprintln!("Warning: {}: duplicate key '{}', overwriting old value", path.display(), key);
}
current_repo.insert(key.to_string(), value.to_string());
} else if line.is_empty() {
try_add_stanza(&mut current_repo, &mut repos, arch, path);
}
}
try_add_stanza(&mut current_repo, &mut repos, arch, path);
Ok(repos)
}
fn validate_deb822_types(types: &str) -> bool {
let type_list: Vec<&str> = types.split_whitespace().collect();
type_list.contains(&"deb") && type_list != ["deb-src"]
}
fn parse_deb822_fields(repo_data: &HashMap<String, String>) -> Option<(String, String, Vec<String>)> {
let _types = repo_data.get("Types")?;
let uris = repo_data.get("URIs")?;
let suites = repo_data.get("Suites")?;
let empty_string = String::new();
let components_str = repo_data.get("Components").unwrap_or(&empty_string);
let component_list: Vec<String> = if components_str.is_empty() {
Vec::new()
} else {
components_str.split_whitespace().map(|s| s.to_string()).collect()
};
Some((uris.clone(), suites.clone(), component_list))
}
fn extract_deb822_options(repo_data: &HashMap<String, String>) -> std::collections::HashMap<String, String> {
let mut options = std::collections::HashMap::new();
let plusminus_options = ["Architectures", "Languages", "Targets"];
for &opt in &plusminus_options {
if let Some(value) = repo_data.get(opt) {
options.insert(opt.to_lowercase(), value.clone());
}
if let Some(value) = repo_data.get(&format!("{}-Add", opt)) {
options.insert(format!("{}_add", opt.to_lowercase()), value.clone());
}
if let Some(value) = repo_data.get(&format!("{}-Remove", opt)) {
options.insert(format!("{}_remove", opt.to_lowercase()), value.clone());
}
}
let simple_options = [
"Trusted", "Check-Valid-Until", "Valid-Until-Min", "Valid-Until-Max",
"Check-Date", "Date-Max-Future", "Snapshot", "Signed-By",
"PDiffs", "By-Hash", "Include", "Exclude", "Enabled"
];
for &opt in &simple_options {
if let Some(value) = repo_data.get(opt) {
options.insert(opt.to_lowercase().replace("-", "_"), value.clone());
}
}
options
}
fn generate_repos_from_deb822_data(
uris: &str,
suites: &str,
component_list: &[String],
options: &std::collections::HashMap<String, String>,
arch: &str,
path: &Path
) -> Vec<RepoConfig> {
let enabled = options.get("enabled")
.map(|v| v.to_lowercase() == "yes")
.unwrap_or(true);
let all_uris: Vec<String> = uris.split_whitespace()
.map(|uri_str| subst_var(uri_str, arch))
.collect();
if all_uris.is_empty() {
eprintln!("Warning: {}: No URIs specified in repository configuration", path.display());
return Vec::new();
}
let mut repos = Vec::new();
for suite_str in suites.split_whitespace() {
let suite = subst_var(suite_str, arch);
let (primary_uri, alternatives) = all_uris.split_first().unwrap();
let package_baseurl = primary_uri.trim_end_matches('/').to_string();
let index_url = if suite.ends_with('/') {
if !component_list.is_empty() {
eprintln!("Warning: {}: Components specified for absolute suite '{}', skipping", path.display(), suite);
continue;
}
format!("{}/{}Release", package_baseurl, suite)
} else {
format!("{}/dists/{}/Release", package_baseurl, suite)
};
repos.push(RepoConfig {
enabled,
index_url,
package_baseurl,
alternative_baseurls: alternatives.to_vec(),
components: component_list.to_vec(),
..RepoConfig::default()
});
}
repos
}
fn extract_repos_from_deb822(repo_data: &HashMap<String, String>, arch: &str, path: &Path) -> Vec<RepoConfig> {
if !repo_data.get("Types").is_some_and(|types| validate_deb822_types(types)) {
return Vec::new();
}
let (uris, suites, component_list) = match parse_deb822_fields(repo_data) {
Some(fields) => fields,
None => return Vec::new(),
};
let options = extract_deb822_options(repo_data);
generate_repos_from_deb822_data(&uris, &suites, &component_list, &options, arch, path)
}
fn process_single_source_file(path: &Path, parser: fn(&Path, &str) -> Result<Vec<RepoConfig>>, arch: &str) -> Result<Vec<ChannelConfig>> {
let filename = path.file_name().and_then(|n| n.to_str());
if matches!(filename, Some("debian.sources") | Some("ubuntu.sources")) {
return Ok(Vec::new());
}
let mut channel_configs = Vec::new();
if let Some(filename) = path.file_stem() {
let repo_name = filename.to_string_lossy().to_string();
match parser(path, arch) {
Ok(repo_configs) => {
let repo_count = repo_configs.len();
for (i, repo_config) in repo_configs.into_iter().enumerate() {
let repo_name_key = if repo_count == 1 {
repo_name.clone()
} else {
format!("{}-{}", repo_name, i)
};
let mut repos = HashMap::new();
repos.insert(repo_name_key, repo_config);
let channel_config = ChannelConfig {
format: PackageFormat::Deb,
distro: "debian".to_string(),
repos,
file_path: path.to_string_lossy().to_string(),
..ChannelConfig::default()
};
channel_configs.push(channel_config);
}
}
Err(e) => {
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
}
}
}
Ok(channel_configs)
}
fn load_apt_sources_with_glob(env_root: &Path, glob_pattern: &str, parser: fn(&Path, &str) -> Result<Vec<RepoConfig>>, arch: &str) -> Result<Vec<ChannelConfig>> {
let mut channel_configs = Vec::new();
let sources_dir = crate::dirs::path_join(env_root, &["etc", "apt", "sources.list.d"]);
let full_pattern = sources_dir.join(glob_pattern);
if !lfs::exists_on_host(&sources_dir) {
return Ok(channel_configs);
}
for entry in glob::glob(&full_pattern.to_string_lossy())? {
match entry {
Ok(path) => {
match process_single_source_file(&path, parser, arch) {
Ok(mut configs) => channel_configs.append(&mut configs),
Err(e) => eprintln!("Warning: Failed to process {}: {}", path.display(), e),
}
}
Err(e) => {
eprintln!("Warning: Failed to read glob entry: {}", e);
}
}
}
Ok(channel_configs)
}
pub fn load_sources_list_configs(env_root: &Path, arch: &str) -> Result<Vec<ChannelConfig>> {
load_apt_sources_with_glob(env_root, "*.list", parse_sources_list_file, arch)
}
pub fn load_sources_configs(env_root: &Path, arch: &str) -> Result<Vec<ChannelConfig>> {
load_apt_sources_with_glob(env_root, "*.sources", parse_sources_file, arch)
}
fn parse_sources_list_file(path: &Path, arch: &str) -> Result<Vec<RepoConfig>> {
let content = fs::read_to_string(path)?;
let mut repos = Vec::new();
for (line_num, line) in content.lines().enumerate() {
match parse_sources_list_line(line, arch) {
Ok(Some(repo)) => repos.push(repo),
Ok(None) => {}
Err(e) => {
eprintln!("Warning: {}:{}: {}", path.display(), line_num + 1, e);
}
}
}
Ok(repos)
}
fn parse_sources_file(path: &Path, arch: &str) -> Result<Vec<RepoConfig>> {
let content = fs::read_to_string(path)?;
parse_deb822_content(&content, arch, path)
}
pub fn load_deb_system_repos(env_root: &Path, arch: &str) -> Result<Vec<ChannelConfig>> {
let loaders = [
(load_sources_list_configs as fn(&Path, &str) -> Result<Vec<ChannelConfig>>, "Failed to load sources.list configs"),
(load_sources_configs, "Failed to load sources configs"),
];
let mut all_channel_configs = Vec::new();
for (loader, error_msg) in loaders {
match loader(env_root, arch) {
Ok(channel_configs) => all_channel_configs.extend(channel_configs),
Err(e) => eprintln!("Warning: {}: {}", error_msg, e),
}
}
Ok(all_channel_configs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_sources_list_line() {
let arch = "amd64";
let line = "deb http://deb.debian.org/debian/ stable main contrib";
let result = parse_sources_list_line(line, arch).unwrap();
assert!(result.is_some());
let repo = result.unwrap();
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/dists/stable/Release".to_string());
assert_eq!(repo.components, vec!["main", "contrib"]);
assert!(repo.alternative_baseurls.is_empty());
let line_with_options = "deb [arch=amd64] http://deb.debian.org/debian/ testing main";
let result = parse_sources_list_line(line_with_options, arch).unwrap();
assert!(result.is_some());
let repo = result.unwrap();
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/dists/testing/Release".to_string());
assert_eq!(repo.components, vec!["main"]);
assert!(repo.alternative_baseurls.is_empty());
let line_with_vars = "deb http://deb.debian.org/debian/ $(ARCH) main";
let result = parse_sources_list_line(line_with_vars, arch).unwrap();
assert!(result.is_some());
let repo = result.unwrap();
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/dists/amd64/Release".to_string());
assert_eq!(repo.components, vec!["main"]);
assert!(repo.alternative_baseurls.is_empty());
let absolute_suite_line = "deb http://deb.debian.org/debian/ stable/updates/";
let result = parse_sources_list_line(absolute_suite_line, arch).unwrap();
assert!(result.is_some());
let repo = result.unwrap();
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/stable/updates/Release".to_string());
assert!(repo.components.is_empty());
assert!(repo.alternative_baseurls.is_empty());
let comment = "# deb http://example.com/debian/ stable main";
assert!(parse_sources_list_line(comment, arch).unwrap().is_none());
assert!(parse_sources_list_line("", arch).unwrap().is_none());
let rpm_line = "rpm http://example.com/repo/ stable main";
assert!(parse_sources_list_line(rpm_line, arch).is_err());
}
#[test]
fn test_parse_deb822_content() {
let arch = "amd64";
let content = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main contrib non-free
Types: deb-src
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main contrib
"#;
let result = parse_deb822_content(content, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
let repo = &repos[0];
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/dists/stable/Release".to_string());
assert!(repo.alternative_baseurls.is_empty());
assert_eq!(repo.components, vec!["main", "contrib", "non-free"]);
let content_with_vars = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: $(ARCH)
Components: main
"#;
let result = parse_deb822_content(content_with_vars, "arm64", Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
let repo = &repos[0];
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/dists/arm64/Release".to_string());
assert_eq!(repo.components, vec!["main"]);
let content_with_options = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main
Architectures: amd64 arm64
Trusted: yes
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
"#;
let result = parse_deb822_content(content_with_options, "amd64", Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
let repo = &repos[0];
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.index_url, "http://deb.debian.org/debian/dists/stable/Release".to_string());
assert_eq!(repo.components, vec!["main"]);
assert!(repo.alternative_baseurls.is_empty());
}
#[test]
fn test_repos_to_configs() {
let line = "deb http://deb.debian.org/debian/ stable main";
let result = parse_sources_list_line(line, "amd64").unwrap().unwrap();
assert_eq!(result.index_url, "http://deb.debian.org/debian/dists/stable/Release".to_string());
assert!(result.alternative_baseurls.is_empty());
let line = "deb http://deb.debian.org/debian/ stable/updates/";
let result = parse_sources_list_line(line, "amd64").unwrap().unwrap();
assert_eq!(result.index_url, "http://deb.debian.org/debian/stable/updates/Release".to_string());
assert!(result.alternative_baseurls.is_empty());
let content = r#"Types: deb
URIs: http://deb.debian.org/debian/ http://mirror.com/
Suites: stable
Components: main
"#;
let result = parse_deb822_content(content, "amd64", Path::new("test")).unwrap();
assert_eq!(result[0].alternative_baseurls, vec!["http://mirror.com/".to_string()]);
}
#[test]
fn test_parse_sources_list_error_cases() {
let arch = "amd64";
let malformed_options = "deb [arch=amd64 http://example.com/ stable main";
assert!(parse_sources_list_line(malformed_options, arch).is_err());
let empty_key = "deb [=value] http://example.com/ stable main";
assert!(parse_sources_list_line(empty_key, arch).is_err());
let empty_value = "deb [key=] http://example.com/ stable main";
assert!(parse_sources_list_line(empty_value, arch).is_err());
let no_equals = "deb [key] http://example.com/ stable main";
assert!(parse_sources_list_line(no_equals, arch).is_err());
let absolute_with_components = "deb http://example.com/ stable/ main";
assert!(parse_sources_list_line(absolute_with_components, arch).is_err());
let missing_suite = "deb http://example.com/";
assert!(parse_sources_list_line(missing_suite, arch).is_err());
}
#[test]
fn test_deb822_multiple_combinations() {
let arch = "amd64";
let content = r#"Types: deb
URIs: http://deb.debian.org/debian/ http://archive.ubuntu.com/ubuntu/
Suites: focal stable
Components: main contrib
"#;
let result = parse_deb822_content(content, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 2);
for repo in &repos {
assert_eq!(repo.package_baseurl, "http://deb.debian.org/debian");
assert_eq!(repo.alternative_baseurls, vec!["http://archive.ubuntu.com/ubuntu/".to_string()]);
}
let expected_combinations = vec![
("focal", vec!["main", "contrib"]),
("stable", vec!["main", "contrib"]),
];
for (i, (expected_suite, expected_components)) in expected_combinations.iter().enumerate() {
let repo = &repos[i];
assert_eq!(repo.index_url, format!("http://deb.debian.org/debian/dists/{}/Release", expected_suite));
assert_eq!(repo.components, expected_components.iter().map(|s| s.to_string()).collect::<Vec<_>>());
assert_eq!(repo.alternative_baseurls, vec!["http://archive.ubuntu.com/ubuntu/".to_string()]);
}
}
#[test]
fn test_deb822_type_filtering() {
let arch = "amd64";
let content = r#"Types: deb deb-src rpm
URIs: http://example.com/
Suites: stable
Components: main
"#;
let result = parse_deb822_content(content, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].package_baseurl, "http://example.com");
assert_eq!(repos[0].index_url, "http://example.com/dists/stable/Release".to_string());
assert_eq!(repos[0].components, vec!["main"]);
let content_deb_src_only = r#"Types: deb-src
URIs: http://example.com/
Suites: stable
Components: main
"#;
let result = parse_deb822_content(content_deb_src_only, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 0);
}
#[test]
fn test_deb822_enabled_field() {
let arch = "amd64";
let content_default = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main
"#;
let result = parse_deb822_content(content_default, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].enabled, true);
let content_yes = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main
Enabled: yes
"#;
let result = parse_deb822_content(content_yes, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].enabled, true);
let content_no = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main
Enabled: no
"#;
let result = parse_deb822_content(content_no, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].enabled, false);
let content_yes_upper = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main
Enabled: YES
"#;
let result = parse_deb822_content(content_yes_upper, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].enabled, true);
let content_no_upper = r#"Types: deb
URIs: http://deb.debian.org/debian/
Suites: stable
Components: main
Enabled: NO
"#;
let result = parse_deb822_content(content_no_upper, arch, Path::new("test"));
assert!(result.is_ok());
let repos = result.unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].enabled, false);
}
}