use std::path::Path;
use crate::install::shell_single_quote;
use crate::logic::privilege::{PrivilegeTool, active_tool, build_privilege_command};
use crate::util::curl_args;
use super::config::{
RepoRow, ReposConfFile, repo_row_declares_apply_sources, row_is_enabled_for_repos_conf,
};
pub const PACMAN_MANAGED_BEGIN: &str = "# === pacsea managed begin";
pub const PACMAN_MANAGED_END: &str = "# === pacsea managed end";
pub const MANAGED_DROPIN_FILE: &str = "pacsea-repos.conf";
pub const DEFAULT_DROPIN_PATH: &str = "/etc/pacman.d/pacsea-repos.conf";
pub const DEFAULT_MAIN_PACMAN_PATH: &str = "/etc/pacman.conf";
pub const DEFAULT_DROPIN_SIG_LEVEL: &str = "Required DatabaseOptional";
#[derive(Debug)]
pub struct RepoApplyBundle {
pub summary_lines: Vec<String>,
pub commands: Vec<String>,
}
pub fn build_repo_apply_bundle(
repos: &ReposConfFile,
main_pacman_text: &str,
selected_section: &str,
) -> Result<RepoApplyBundle, String> {
let tool = active_tool()?;
build_repo_apply_bundle_with_tool(repos, main_pacman_text, selected_section, tool)
}
pub fn build_repo_apply_bundle_with_tool(
repos: &ReposConfFile,
main_pacman_text: &str,
selected_section: &str,
tool: PrivilegeTool,
) -> Result<RepoApplyBundle, String> {
let eligible: Vec<&RepoRow> = apply_eligible_rows(repos);
let want = selected_section.trim().to_lowercase();
if want.is_empty() {
return Err("No repository selected.".to_string());
}
let Some(selected_row) = find_repo_row_by_lower_name(repos, &want) else {
return Err(format!(
"Selected repository \"{selected_section}\" has no matching [[repo]] name in repos.conf."
));
};
if !row_has_apply_source(selected_row) {
return Err(format!(
"Selected repository \"{selected_section}\" needs `server`, `mirrorlist`, or `http`/`https` mirrorlist_url in repos.conf \
before it can be applied."
));
}
let mut summary_lines: Vec<String> = Vec::new();
let mut commands: Vec<String> = Vec::new();
if eligible.is_empty() {
summary_lines.push(
"No enabled [[repo]] rows: writing an empty managed drop-in (all custom repos disabled)."
.to_string(),
);
}
for r in &eligible {
let name = r.name.as_deref().map_or("", str::trim);
let has_ml = non_empty_trim(r.mirrorlist.as_deref());
let has_srv = non_empty_trim(r.server.as_deref());
let has_url = non_empty_trim(r.mirrorlist_url.as_deref());
if has_ml && has_url {
summary_lines.push(format!(
"Note: skipping mirrorlist_url for [{name}] (mirrorlist path is set)"
));
}
if has_srv && has_url {
summary_lines.push(format!(
"Note: skipping mirrorlist_url for [{name}] (server is set)"
));
}
}
let mirror_fetches = collect_mirror_fetch_steps(&eligible)?;
if !mirror_fetches.is_empty() {
ensure_curl_runnable()?;
}
for MirrorFetch { url, dest, name } in &mirror_fetches {
summary_lines.push(format!(
"Download mirrorlist for [{name}] via curl to {dest}"
));
commands.push(privileged_curl_fetch_command(tool, url, dest)?);
}
let body = render_dropin_body(&eligible)?;
let key_specs = distinct_key_recv_specs(&eligible);
for (fpr, ks) in &key_specs {
let recv_inner = pacman_key_recv_inner(fpr, ks.as_deref());
summary_lines.push(format!("Receive signing key {fpr} (pacman-key)"));
commands.push(build_priv_command(tool, &recv_inner));
summary_lines.push(format!("Locally sign key {fpr} (pacman-key)"));
commands.push(build_priv_command(
tool,
&format!("pacman-key --lsign-key {}", shell_single_quote(fpr)),
));
}
summary_lines.push(format!("Write managed drop-in {DEFAULT_DROPIN_PATH}"));
commands.push(write_dropin_command(tool, DEFAULT_DROPIN_PATH, &body)?);
if main_pacman_has_active_managed_marker(main_pacman_text) {
summary_lines.push(format!(
"Skip appending Include ({PACMAN_MANAGED_BEGIN} already active in {DEFAULT_MAIN_PACMAN_PATH})"
));
} else {
summary_lines.push(format!(
"Append Pacsea Include block to {DEFAULT_MAIN_PACMAN_PATH}"
));
commands.push(append_managed_include_command(
tool,
DEFAULT_MAIN_PACMAN_PATH,
DEFAULT_DROPIN_PATH,
)?);
}
summary_lines.push("Sync package databases (pacman -Sy --noconfirm)".to_string());
commands.push(build_priv_command(tool, "pacman -Sy --noconfirm"));
Ok(RepoApplyBundle {
summary_lines,
commands,
})
}
pub fn build_repo_key_refresh_bundle(
repos: &ReposConfFile,
selected_section: &str,
) -> Result<RepoApplyBundle, String> {
let tool = active_tool()?;
build_repo_key_refresh_bundle_with_tool(repos, selected_section, tool)
}
pub fn build_repo_key_refresh_bundle_with_tool(
repos: &ReposConfFile,
selected_section: &str,
tool: PrivilegeTool,
) -> Result<RepoApplyBundle, String> {
let want = selected_section.trim().to_lowercase();
let row = repos
.repo
.iter()
.find(|r| {
r.name
.as_deref()
.map(str::trim)
.is_some_and(|n| n.to_lowercase() == want)
})
.ok_or_else(|| {
format!(
"No [[repo]] row named \"{}\" in repos.conf.",
selected_section.trim()
)
})?;
let kid = row
.key_id
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| {
"This repository has no key_id in repos.conf; nothing to refresh.".to_string()
})?;
let fpr = normalized_fingerprint(kid)
.ok_or_else(|| "key_id must contain at least 8 hexadecimal digits.".to_string())?;
let ks = row
.key_server
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty());
let mut summary_lines: Vec<String> = Vec::new();
let mut commands: Vec<String> = Vec::new();
let recv_inner = pacman_key_recv_inner(&fpr, ks);
summary_lines.push(format!("Receive signing key {fpr} (pacman-key)"));
commands.push(build_privilege_command(tool, &recv_inner));
summary_lines.push(format!("Locally sign key {fpr} (pacman-key)"));
commands.push(build_privilege_command(
tool,
&format!("pacman-key --lsign-key {}", shell_single_quote(&fpr)),
));
Ok(RepoApplyBundle {
summary_lines,
commands,
})
}
fn build_priv_command(tool: PrivilegeTool, inner: &str) -> String {
build_privilege_command(tool, inner)
}
struct MirrorFetch {
url: String,
dest: String,
name: String,
}
fn main_pacman_has_active_managed_marker(text: &str) -> bool {
text.lines().any(|line| line.trim() == PACMAN_MANAGED_BEGIN)
}
fn non_empty_trim(s: Option<&str>) -> bool {
s.map(str::trim).is_some_and(|t| !t.is_empty())
}
fn apply_eligible_rows(repos: &ReposConfFile) -> Vec<&RepoRow> {
repos
.repo
.iter()
.filter(|r| {
row_enabled(r)
&& r.name
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.is_some()
&& row_has_apply_source(r)
})
.collect()
}
fn row_has_apply_source(r: &RepoRow) -> bool {
repo_row_declares_apply_sources(r)
}
fn looks_like_http_url(u: &str) -> bool {
let lower = u.to_ascii_lowercase();
lower.starts_with("https://") || lower.starts_with("http://")
}
fn row_enabled(r: &RepoRow) -> bool {
row_is_enabled_for_repos_conf(r)
}
fn find_repo_row_by_lower_name<'a>(
repos: &'a ReposConfFile,
want_lower: &str,
) -> Option<&'a RepoRow> {
repos.repo.iter().find(|r| {
r.name
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.is_some_and(|n| n.to_lowercase() == want_lower)
})
}
fn normalized_fingerprint(key_id: &str) -> Option<String> {
let hex: String = key_id.chars().filter(char::is_ascii_hexdigit).collect();
if hex.len() < 8 {
return None;
}
Some(hex.to_uppercase())
}
fn distinct_key_recv_specs(rows: &[&RepoRow]) -> Vec<(String, Option<String>)> {
let mut specs: Vec<(String, Option<String>)> = Vec::new();
for r in rows {
let Some(k) = r.key_id.as_deref().map(str::trim).filter(|s| !s.is_empty()) else {
continue;
};
let Some(fpr) = normalized_fingerprint(k) else {
continue;
};
let ks = r
.key_server
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(std::string::ToString::to_string);
if let Some(i) = specs.iter().position(|(f, _)| f == &fpr) {
if specs[i].1.is_none() {
specs[i].1 = ks;
}
} else {
specs.push((fpr, ks));
}
}
specs
}
fn pacman_key_recv_inner(fpr: &str, key_server: Option<&str>) -> String {
let qf = shell_single_quote(fpr);
let Some(ks) = key_server.map(str::trim).filter(|s| !s.is_empty()) else {
return format!("pacman-key --recv-keys {qf}");
};
format!(
"pacman-key --keyserver {} --recv-keys {qf}",
shell_single_quote(ks)
)
}
fn collect_mirror_fetch_steps(rows: &[&RepoRow]) -> Result<Vec<MirrorFetch>, String> {
let mut out = Vec::new();
for r in rows {
if non_empty_trim(r.server.as_deref()) || non_empty_trim(r.mirrorlist.as_deref()) {
continue;
}
let Some(url_raw) = r
.mirrorlist_url
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
else {
continue;
};
if !looks_like_http_url(url_raw) {
let name = r.name.as_deref().map_or("", str::trim);
return Err(format!(
"repos.conf: mirrorlist_url for [{name}] must start with http:// or https://"
));
}
let name = r
.name
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| "repos.conf: mirrorlist_url row missing name (internal)".to_string())?;
let dest = mirror_url_dest_path(name)?;
out.push(MirrorFetch {
url: url_raw.to_string(),
dest,
name: name.to_string(),
});
}
Ok(out)
}
fn ensure_curl_runnable() -> Result<(), String> {
let bin = crate::util::curl::curl_binary_path();
match std::process::Command::new(bin)
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
{
Ok(s) if s.success() => Ok(()),
Ok(_) => Err(format!(
"curl ('{bin}') is required for mirrorlist_url but did not run successfully. Install curl or use mirrorlist = \"/path\" instead."
)),
Err(e) => Err(format!(
"Could not run curl ('{bin}'): {e}. Install curl or use mirrorlist = \"/path\" instead."
)),
}
}
fn privileged_curl_fetch_command(
tool: PrivilegeTool,
url: &str,
dest: &str,
) -> Result<String, String> {
if !is_safe_abs_path(dest) {
return Err("Refusing unsafe mirrorlist destination path.".to_string());
}
let bin = crate::util::curl::curl_binary_path();
let argv = curl_args(url, &["-o", dest]);
let mut parts = vec![shell_single_quote(bin)];
for a in argv {
parts.push(shell_single_quote(&a));
}
let inner = parts.join(" ");
Ok(build_privilege_command(tool, &inner))
}
fn mirror_url_dest_path(section: &str) -> Result<String, String> {
let slug = sanitize_repo_slug(section)?;
let short = stable_fnv1a_u32(section.trim().as_bytes());
let p = format!("/etc/pacman.d/pacsea-mirror-{slug}-{short:x}.list");
if !is_safe_abs_path(&p) {
return Err("Refusing unsafe mirrorlist destination path.".to_string());
}
Ok(p)
}
fn sanitize_repo_slug(section: &str) -> Result<String, String> {
let mut out = String::new();
for c in section.trim().to_lowercase().chars() {
if c.is_ascii_alphanumeric() {
out.push(c);
} else {
out.push('_');
}
}
while out.contains("__") {
out = out.replace("__", "_");
}
let out = out.trim_matches('_').to_string();
if out.is_empty() {
return Err(
"repos.conf: repo `name` sanitizes to an empty path slug for mirrorlist_url."
.to_string(),
);
}
let out = if out.len() > 48 {
out[..48].trim_end_matches('_').to_string()
} else {
out
};
if out.is_empty() {
return Err(
"repos.conf: repo `name` too short after sanitizing for mirrorlist_url.".to_string(),
);
}
Ok(out)
}
fn render_dropin_body(rows: &[&RepoRow]) -> Result<String, String> {
if rows.is_empty() {
return Ok(
"# Pacsea managed repositories\n# No enabled [[repo]] rows in repos.conf.\n"
.to_string(),
);
}
let mut out = String::new();
for r in rows {
let name = r
.name
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| "repos.conf: missing name on eligible row (internal)".to_string())?;
out.push('[');
out.push_str(name);
out.push_str("]\n");
let sl_line = r
.sig_level
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or(DEFAULT_DROPIN_SIG_LEVEL);
out.push_str("SigLevel = ");
out.push_str(sl_line);
out.push('\n');
if let Some(srv) = r.server.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
if !looks_like_http_url(srv) {
return Err(format!(
"repos.conf: server for [{name}] must start with http:// or https://"
));
}
out.push_str("Server = ");
out.push_str(srv);
out.push('\n');
} else if let Some(inc) = r
.mirrorlist
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
out.push_str("Include = ");
out.push_str(inc);
out.push('\n');
} else if non_empty_trim(r.mirrorlist_url.as_deref())
&& !non_empty_trim(r.server.as_deref())
&& !non_empty_trim(r.mirrorlist.as_deref())
{
let dest = mirror_url_dest_path(name)?;
out.push_str("Include = ");
out.push_str(&dest);
out.push('\n');
} else {
return Err(
"Internal: row missing server, mirrorlist, and resolvable mirrorlist_url"
.to_string(),
);
}
out.push('\n');
}
Ok(out)
}
fn write_dropin_command(
tool: PrivilegeTool,
dropin_path: &str,
body: &str,
) -> Result<String, String> {
if !dropin_path
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '-' | '.' | '_'))
{
return Err("Refusing unsafe drop-in path.".to_string());
}
let pieces: Vec<String> = body.lines().map(shell_single_quote).collect();
if pieces.is_empty() {
return Err("Generated drop-in is empty.".to_string());
}
let printf_args = pieces.join(" ");
let inner = format!("printf '%s\\n' {printf_args} | tee {dropin_path} > /dev/null");
Ok(build_privilege_command(
tool,
&format!("sh -c {}", shell_single_quote(&inner)),
))
}
fn append_managed_include_command(
tool: PrivilegeTool,
main_path: &str,
dropin_path: &str,
) -> Result<String, String> {
if !is_safe_abs_path(main_path) || !is_safe_abs_path(dropin_path) {
return Err("Refusing unsafe pacman path.".to_string());
}
let include_line = format!("Include = {dropin_path}");
let b = shell_single_quote(PACMAN_MANAGED_BEGIN);
let inc = shell_single_quote(&include_line);
let e = shell_single_quote(PACMAN_MANAGED_END);
let q_main = shell_single_quote(main_path);
let inner = format!("printf '%s\\n' {b} {inc} {e} '' >> {q_main}");
Ok(build_privilege_command(
tool,
&format!("sh -c {}", shell_single_quote(&inner)),
))
}
fn is_safe_abs_path(p: &str) -> bool {
p.starts_with('/')
&& !p.contains("..")
&& p.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '-' | '.' | '_'))
}
fn stable_fnv1a_u32(data: &[u8]) -> u32 {
const OFFSET_BASIS: u32 = 0x811c_9dc5;
const PRIME: u32 = 0x0100_0193;
let mut h = OFFSET_BASIS;
for &b in data {
h ^= u32::from(b);
h = h.wrapping_mul(PRIME);
}
h
}
pub fn read_main_pacman_conf_text(path: &Path) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| {
format!(
"Could not read {}: {e}. Apply needs the live pacman configuration.",
path.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::logic::repos::{ReposConfFile, load_resolve_repos_from_str};
#[test]
fn bundle_contains_recv_lsign_write() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://example.com/$repo/os/$arch"
key_id = "AABBCCDDEEFF0011"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let main = "\n";
let b = build_repo_apply_bundle_with_tool(&file, main, "myrepo", PrivilegeTool::Sudo)
.expect("bundle");
assert!(
b.commands
.iter()
.any(|c| c.contains("pacman-key --recv-keys"))
);
assert!(
b.commands
.iter()
.any(|c| c.contains("pacman-key --lsign-key"))
);
assert!(b.commands.iter().any(|c| c.contains("pacsea-repos.conf")));
assert!(
b.summary_lines
.iter()
.any(|s| s.contains("Append Pacsea Include"))
);
assert!(
b.commands
.iter()
.any(|c| c.contains("pacman -Sy --noconfirm"))
);
}
#[test]
fn recv_uses_keyserver_when_configured() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://example.com/$repo/os/$arch"
key_id = "AABBCCDDEEFF0011"
key_server = "keyserver.ubuntu.com"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let b = build_repo_apply_bundle_with_tool(&file, "\n", "myrepo", PrivilegeTool::Sudo)
.expect("b");
assert!(b.commands.iter().any(|c| {
c.contains("pacman-key")
&& c.contains("--keyserver")
&& c.contains("keyserver.ubuntu.com")
}));
}
#[test]
fn skips_include_when_marker_present() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://x.test"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let main = format!(
"\n{PACMAN_MANAGED_BEGIN}\nInclude = /etc/pacman.d/pacsea-repos.conf\n{PACMAN_MANAGED_END}\n"
);
let b = build_repo_apply_bundle_with_tool(&file, &main, "myrepo", PrivilegeTool::Sudo)
.expect("b");
assert!(
b.summary_lines
.iter()
.any(|s| s.contains("Skip appending") && s.contains("already active"))
);
assert!(
!b.commands
.iter()
.any(|c| { c.contains(">>") && c.contains("/etc/pacman.conf") })
);
}
#[test]
fn commented_marker_line_still_appends_include() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://x.test"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let main = format!("# {PACMAN_MANAGED_BEGIN}\n");
let b = build_repo_apply_bundle_with_tool(&file, &main, "myrepo", PrivilegeTool::Sudo)
.expect("b");
assert!(
b.summary_lines
.iter()
.any(|s| s.contains("Append Pacsea Include"))
);
assert!(
b.commands
.iter()
.any(|c| { c.contains(">>") && c.contains("/etc/pacman.conf") })
);
}
#[test]
fn mirrorlist_url_fetch_and_include() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
mirrorlist_url = "https://archlinux.org/mirrorlist/all/http/"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let b = build_repo_apply_bundle_with_tool(&file, "\n", "myrepo", PrivilegeTool::Sudo)
.expect("b");
assert!(
b.commands
.iter()
.any(|c| c.contains("curl") && c.contains("archlinux.org"))
);
assert!(
b.commands
.iter()
.any(|c| c.contains("pacsea-mirror-myrepo-"))
);
}
#[test]
fn selected_row_without_server_errors() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://x.test"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let err = build_repo_apply_bundle_with_tool(&file, "", "other", PrivilegeTool::Sudo)
.expect_err("err");
assert!(
err.contains("no matching [[repo]]") || err.contains("needs"),
"{err}"
);
}
#[test]
fn all_disabled_rows_still_writes_empty_dropin() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://x.test"
enabled = false
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let b = build_repo_apply_bundle_with_tool(&file, "\n", "myrepo", PrivilegeTool::Sudo)
.expect("bundle");
assert!(
b.summary_lines
.iter()
.any(|s| s.contains("empty managed drop-in")),
"{:?}",
b.summary_lines
);
assert!(b.commands.iter().any(|c| c.contains("pacsea-repos.conf")));
assert!(b.commands.iter().any(|c| c.contains("pacman -Sy")));
}
#[test]
fn key_refresh_bundle_only_recv_and_lsign() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://example.com/$repo/os/$arch"
key_id = "AABBCCDDEEFF0011"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let b = build_repo_key_refresh_bundle_with_tool(&file, "myrepo", PrivilegeTool::Sudo)
.expect("bundle");
assert_eq!(b.commands.len(), 2);
assert!(
b.commands
.iter()
.any(|c| c.contains("pacman-key") && c.contains("--recv-keys"))
);
assert!(
b.commands
.iter()
.any(|c| c.contains("pacman-key --lsign-key"))
);
assert!(!b.commands.iter().any(|c| c.contains("pacsea-repos.conf")));
assert!(!b.commands.iter().any(|c| c.contains("pacman -Sy")));
}
#[test]
fn key_refresh_uses_keyserver_when_configured() {
let toml = r#"
[[repo]]
name = "kr"
results_filter = "k"
server = "https://x.test"
key_id = "AABBCCDDEEFF0011"
key_server = "keyserver.ubuntu.com"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let b =
build_repo_key_refresh_bundle_with_tool(&file, "kr", PrivilegeTool::Doas).expect("b");
assert!(b.commands.iter().any(|c| {
c.contains("pacman-key")
&& c.contains("--keyserver")
&& c.contains("keyserver.ubuntu.com")
}));
}
#[test]
fn is_safe_abs_path_rejects_dotdot() {
assert!(!is_safe_abs_path("/etc/pacman.d/../../tmp/evil"));
assert!(is_safe_abs_path("/etc/pacman.d/pacsea-repos.conf"));
}
#[test]
fn server_must_use_http_or_https() {
let toml = r#"
[[repo]]
name = "bad"
results_filter = "b"
server = "file:///etc/shadow"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let err = build_repo_apply_bundle_with_tool(&file, "\n", "bad", PrivilegeTool::Sudo)
.expect_err("err");
assert!(err.contains("http://") && err.contains("https://"), "{err}");
}
#[test]
fn dropin_command_contains_safe_default_sig_level() {
let toml = r#"
[[repo]]
name = "myrepo"
results_filter = "mine"
server = "https://example.com/$repo/os/$arch"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let b = build_repo_apply_bundle_with_tool(&file, "\n", "myrepo", PrivilegeTool::Sudo)
.expect("bundle");
assert!(
b.commands
.iter()
.any(|c| c.contains(DEFAULT_DROPIN_SIG_LEVEL)),
"expected drop-in write to use default sig level"
);
}
#[test]
fn key_refresh_errors_without_key_id() {
let toml = r#"
[[repo]]
name = "nok"
results_filter = "n"
server = "https://x.test"
"#;
let (repo, _) = load_resolve_repos_from_str(toml).expect("parse");
let file = ReposConfFile { repo };
let err = build_repo_key_refresh_bundle_with_tool(&file, "nok", PrivilegeTool::Sudo)
.expect_err("err");
assert!(err.contains("key_id"), "{err}");
}
}