use itertools::Itertools;
use std::collections::{BTreeSet, HashMap};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::str;
use crate::dh_lib::{ScriptFragments, autoscript, pkgfile};
use crate::listener::Listener;
use crate::manifest::Asset;
use crate::util::{MyJoin, fname_from_path};
use crate::CDResult;
const LIB_SYSTEMD_SYSTEM_DIR: &str = "lib/systemd/system/";
const USR_LIB_TMPFILES_D_DIR: &str = "usr/lib/tmpfiles.d/";
const SYSTEMD_UNIT_FILE_INSTALL_MAPPINGS: [(&str, &str, &str); 12] = [
("", "mount", LIB_SYSTEMD_SYSTEM_DIR),
("", "path", LIB_SYSTEMD_SYSTEM_DIR),
("@", "path", LIB_SYSTEMD_SYSTEM_DIR),
("", "service", LIB_SYSTEMD_SYSTEM_DIR),
("@", "service", LIB_SYSTEMD_SYSTEM_DIR),
("", "socket", LIB_SYSTEMD_SYSTEM_DIR),
("@", "socket", LIB_SYSTEMD_SYSTEM_DIR),
("", "target", LIB_SYSTEMD_SYSTEM_DIR),
("@", "target", LIB_SYSTEMD_SYSTEM_DIR),
("", "timer", LIB_SYSTEMD_SYSTEM_DIR),
("@", "timer", LIB_SYSTEMD_SYSTEM_DIR),
("", "tmpfile", USR_LIB_TMPFILES_D_DIR),
];
#[derive(Debug, PartialEq, Eq)]
pub struct InstallRecipe {
pub path: PathBuf,
pub mode: u32,
}
pub type PackageUnitFiles = HashMap<PathBuf, InstallRecipe>;
#[derive(Default, Debug)]
pub struct Options {
pub no_enable: bool,
pub no_start: bool,
pub restart_after_upgrade: bool,
pub no_stop_on_upgrade: bool,
}
pub fn find_units(dir: &Path, main_package: &str, unit_name: Option<&str>) -> PackageUnitFiles {
let mut installables = HashMap::new();
for (package_suffix, unit_type, install_dir) in &SYSTEMD_UNIT_FILE_INSTALL_MAPPINGS {
let package = &format!("{main_package}{package_suffix}");
if let Some(src_path) = pkgfile(dir, main_package, package, unit_type, unit_name) {
let actual_suffix = match &unit_type[..] {
"tmpfile" => "conf",
_ => unit_type,
};
let install_filename = match unit_name {
Some(name) => format!("{name}{package_suffix}.{actual_suffix}"),
None => format!("{package}.{actual_suffix}"),
};
let install_path = Path::new(install_dir).join(install_filename);
installables.insert(
src_path,
InstallRecipe {
path: install_path,
mode: 0o644,
},
);
}
}
installables
}
fn is_comment(s: &str) -> bool {
matches!(s.chars().next(), Some('#' | ';'))
}
fn unquote(s: &str) -> &str {
if s.len() > 1 &&
((s.starts_with('"') && s.ends_with('"')) ||
(s.starts_with('\'') && s.ends_with('\''))) {
&s[1..s.len()-1]
} else {
s
}
}
pub fn generate(package: &str, assets: &[Asset], options: &Options, listener: &dyn Listener) -> CDResult<ScriptFragments> {
let mut scripts = ScriptFragments::new();
let tmp_file_names = assets
.iter()
.filter(|a| a.c.target_path.starts_with(USR_LIB_TMPFILES_D_DIR))
.map(|v| fname_from_path(v.source.path().unwrap()))
.collect::<Vec<String>>()
.join(" ");
if !tmp_file_names.is_empty() {
autoscript(&mut scripts, package, "postinst", "postinst-init-tmpfiles",
&map!{ "TMPFILES" => tmp_file_names }, false, listener)?;
}
let mut installed_non_template_units: BTreeSet<String> = BTreeSet::new();
installed_non_template_units.extend(
assets
.iter()
.filter(|a| a.c.target_path.parent() == Some(LIB_SYSTEMD_SYSTEM_DIR.as_ref()))
.map(|a| fname_from_path(a.c.target_path.as_path()))
.filter(|fname| !fname.contains('@')),
);
let mut aliases = BTreeSet::new();
let mut enable_units = BTreeSet::new();
let mut start_units = BTreeSet::new();
let mut seen = BTreeSet::new();
let mut units = installed_non_template_units;
while !units.is_empty() {
let mut also_units = BTreeSet::<String>::new();
for unit in &units {
listener.info(format!("Determining augmentations needed for systemd unit {unit}"));
start_units.insert(unit.clone());
let needle = Path::new(LIB_SYSTEMD_SYSTEM_DIR).join(unit);
let data = assets.iter().find(move |&item| item.c.target_path == needle).unwrap().source.data()?;
let reader = data.into_owned();
for line in reader.lines().map(|line| line.unwrap()).filter(|s| !is_comment(s)) {
let possible_kv_pair = line.splitn(2, '=').map(|s| s.trim()).next_tuple();
if let Some((key, value)) = possible_kv_pair {
let other_unit = unquote(value).to_string();
match key {
"Also" => {
if seen.insert(other_unit.clone()) {
also_units.insert(other_unit);
}
},
"Alias" => {
aliases.insert(other_unit);
},
_ => (),
};
} else if line.starts_with("[Install]") {
enable_units.insert(unit.clone());
}
}
}
units = also_units;
}
if !enable_units.is_empty() {
let snippet = match options.no_enable {
true => "postinst-systemd-dont-enable",
false => "postinst-systemd-enable",
};
for unit in &enable_units {
autoscript(&mut scripts, package, "postinst", snippet,
&map!{ "UNITFILE" => unit.clone() }, true, listener)?;
}
autoscript(&mut scripts, package, "postrm", "postrm-systemd",
&map!{ "UNITFILES" => enable_units.join(" ") }, false, listener)?;
}
if !start_units.is_empty() {
let mut replace = map! { "UNITFILES" => start_units.join(" ") };
if options.restart_after_upgrade {
let snippet = if options.no_start {
replace.insert("RESTART_ACTION", "try-restart".into());
"postinst-systemd-restartnostart"
} else {
replace.insert("RESTART_ACTION", "restart".into());
"postinst-systemd-restart"
};
autoscript(&mut scripts, package, "postinst", snippet, &replace, true, listener)?;
} else if !options.no_start {
autoscript(&mut scripts, package, "postinst", "postinst-systemd-start", &replace, true, listener)?;
}
if options.no_stop_on_upgrade || options.restart_after_upgrade {
autoscript(&mut scripts, package, "prerm", "prerm-systemd-restart", &replace, true, listener)?;
} else if !options.no_start {
autoscript(&mut scripts, package, "prerm", "prerm-systemd", &replace, true, listener)?;
}
autoscript(&mut scripts, package, "postrm", "postrm-systemd-reload-only", &replace, false, listener)?;
}
Ok(scripts)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{Asset, AssetSource};
use crate::util::tests::add_test_fs_paths;
use crate::util::tests::get_read_count;
use crate::util::tests::set_test_fs_path_content;
use rstest::*;
#[test]
fn is_comment_detects_comments() {
assert!(is_comment("#"));
assert!(is_comment("# "));
assert!(is_comment("# some comment"));
assert!(is_comment(";"));
assert!(is_comment("; "));
assert!(is_comment("; some comment"));
}
#[test]
fn is_comment_detects_non_comments() {
assert!(!is_comment(" #"));
assert!(!is_comment(" # "));
assert!(!is_comment(" # some comment"));
assert!(!is_comment(" ;"));
assert!(!is_comment(" ; "));
assert!(!is_comment(" ; some comment"));
}
#[test]
fn unquote_unquotes_matching_single_quotes() {
assert_eq!("", unquote("''"));
assert_eq!("a", unquote("'a'"));
assert_eq!("ab", unquote("'ab'"));
}
#[test]
fn unquote_unquotes_matching_double_quotes() {
assert_eq!("", unquote(r#""""#));
assert_eq!("a", unquote(r#""a""#));
assert_eq!("ab", unquote(r#""ab""#));
}
#[test]
fn unquote_ignores_embedded_quotes() {
assert_eq!("a'b", unquote("'a'b'"));
assert_eq!(r#"a"b"#, unquote(r#"'a"b'"#));
assert_eq!(r#"a"b"#, unquote(r#""a"b""#));
assert_eq!(r#"a'b"#, unquote(r#""a'b""#));
}
#[test]
fn unquote_ignores_partial_quotes() {
assert_eq!("'", unquote("'"));
assert_eq!("'ab", unquote("'ab"));
assert_eq!("ab'", unquote("ab'"));
assert_eq!("'ab'ab", unquote("'ab'ab"));
assert_eq!("ab'ab'", unquote("ab'ab'"));
assert_eq!(r#"""#, unquote(r#"""#));
assert_eq!(r#""ab"#, unquote(r#""ab"#));
assert_eq!(r#"ab""#, unquote(r#"ab""#));
assert_eq!(r#""ab"ab"#, unquote(r#""ab"ab"#));
assert_eq!(r#"ab"ab""#, unquote(r#"ab"ab""#));
}
#[test]
fn unquote_ignores_mismatched_quotes() {
assert_eq!(r#""'"#, unquote(r#""'"#));
assert_eq!(r#"'""#, unquote(r#"'""#));
assert_eq!(r#""a'"#, unquote(r#""a'"#));
assert_eq!(r#"'a""#, unquote(r#"'a""#));
assert_eq!(r#""ab'"#, unquote(r#""ab'"#));
assert_eq!(r#"'ab""#, unquote(r#"'ab""#));
}
#[test]
fn find_units_in_empty_dir_finds_nothing() {
let pkg_unit_files = find_units(Path::new(""), "mypkg", None);
assert!(pkg_unit_files.is_empty());
}
fn assert_eq_found_unit(pkg_unit_files: &PackageUnitFiles, expected_install_path: &str, source_path: &str) {
let expected = InstallRecipe {
path: PathBuf::from(expected_install_path),
mode: 0o644,
};
let actual = pkg_unit_files.get(&PathBuf::from(source_path)).unwrap();
assert_eq!(&expected, actual);
}
#[test]
fn find_units_for_package() {
let _g = add_test_fs_paths(&[
"debian/mypkg.mount",
"debian/mypkg@.path",
"debian/service", "debian/mypkg@.socket",
"debian/mypkg.target",
"debian/mypkg@.timer",
"debian/mypkg.tmpfile",
"debian/mypkg.myunit.service", ]);
let pkg_unit_files = find_units(Path::new("debian"), "mypkg", None);
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/mypkg.mount", "debian/mypkg.mount");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/mypkg@.path", "debian/mypkg@.path");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/mypkg.service", "debian/service");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/mypkg@.socket", "debian/mypkg@.socket");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/mypkg.target", "debian/mypkg.target");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/mypkg@.timer", "debian/mypkg@.timer");
assert_eq_found_unit(&pkg_unit_files, "usr/lib/tmpfiles.d/mypkg.conf", "debian/mypkg.tmpfile");
assert_eq!(7, pkg_unit_files.len());
}
#[test]
fn find_named_units_for_package() {
let _g = add_test_fs_paths(&[
"debian/mypkg.myunit.mount",
"debian/mypkg@.myunit.path",
"debian/service", "debian/mypkg@.myunit.socket",
"debian/target", "debian/mypkg@.myunit.timer",
"debian/mypkg.tmpfile", "debian/mypkg.myunit.service", ]);
let _g = add_test_fs_paths(&[
"debian/nested/dir/mykpg.myunit.mount",
"debian/README.md",
"mypkg.myunit.mount",
"mypkg.mount",
"mount",
"postinit",
"mypkg.postinit",
"mypkg.myunit.postinit",
]);
let pkg_unit_files = find_units(Path::new("debian"), "mypkg", Some("myunit"));
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/myunit.mount", "debian/mypkg.myunit.mount");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/myunit@.path", "debian/mypkg@.myunit.path");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/myunit.service", "debian/mypkg.myunit.service");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/myunit@.socket", "debian/mypkg@.myunit.socket");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/myunit.target", "debian/target");
assert_eq_found_unit(&pkg_unit_files, "lib/systemd/system/myunit@.timer", "debian/mypkg@.myunit.timer");
assert_eq_found_unit(&pkg_unit_files, "usr/lib/tmpfiles.d/myunit.conf", "debian/mypkg.tmpfile");
assert_eq!(7, pkg_unit_files.len());
}
#[test]
fn generate_with_empty_inputs_does_nothing() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let fragments = generate("", &[], &Options::default(), &mock_listener).unwrap();
assert!(fragments.is_empty());
}
#[test]
fn generate_with_arbitrary_asset_does_nothing() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let assets = vec![Asset::new(
AssetSource::Path(PathBuf::new()),
PathBuf::new(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert!(fragments.is_empty());
}
#[test]
#[should_panic(expected = "unwrap")]
fn generate_with_invalid_tmp_file_asset_panics() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let assets = vec![Asset::new(
AssetSource::Path(PathBuf::new()), Path::new("usr/lib/tmpfiles.d/blah").to_path_buf(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert!(fragments.is_empty());
}
#[test]
#[should_panic(expected = "unwrap")]
fn generate_with_data_tmp_file_asset_panics() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let assets = vec![Asset::new(
AssetSource::Data(vec![]), Path::new("usr/lib/tmpfiles.d/blah").to_path_buf(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert!(fragments.is_empty());
}
#[test]
fn generate_with_empty_tmp_file_asset() {
use crate::dh_lib::get_embedded_autoscript;
const TMP_FILE_NAME: &str = "my_tmp_file";
let tmp_file_path = PathBuf::from(format!("debian/{TMP_FILE_NAME}"));
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(1).return_const(());
let assets = vec![Asset::new(
AssetSource::Path(tmp_file_path),
Path::new("usr/lib/tmpfiles.d/blah").to_path_buf(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert_eq!(1, fragments.len());
let (fragment_name, fragment_bytes) = fragments.into_iter().next().unwrap();
assert_eq!("mypkg.postinst.debhelper", fragment_name);
let autoscript_text = get_embedded_autoscript("postinst-init-tmpfiles");
let autoscript_line_count = autoscript_text.lines().count();
let created_text = String::from_utf8(fragment_bytes).unwrap();
let created_line_count = created_text.lines().count();
assert_eq!(autoscript_line_count + 2, created_line_count);
let mut lines = created_text.lines();
assert!(lines.next().unwrap().starts_with("# Automatically added by"));
assert_eq!(lines.nth_back(0).unwrap(), "# End automatically added section");
let expected_autoscript_text = autoscript_text.replace("#TMPFILES#", TMP_FILE_NAME);
let expected_autoscript_text = expected_autoscript_text.trim_end();
let start1 = 1;
let end1 = start1 + autoscript_line_count;
let created_autoscript_text = created_text.lines().collect::<Vec<&str>>()[start1..end1].join("\n");
assert_ne!(expected_autoscript_text, autoscript_text);
assert_eq!(expected_autoscript_text, created_autoscript_text);
}
#[test]
fn generate_filters_out_template_units() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let assets = vec![Asset::new(
AssetSource::Path(PathBuf::from("debian/my_unit@.service")),
Path::new("lib/systemd/system/").to_path_buf(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert_eq!(0, fragments.len());
}
#[test]
fn generate_filters_out_subdir() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let assets = vec![Asset::new(
AssetSource::Path(PathBuf::from("debian/10-extra-hardening.conf")),
Path::new("lib/systemd/system/foobar.service.d/").to_path_buf(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert_eq!(0, fragments.len());
}
#[test]
fn generate_acts_only_on_unit_files_with_the_expected_install_path() {
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().times(0).return_const(());
let assets = vec![Asset::new(
AssetSource::Path(PathBuf::from("debian/my_unit.service")),
Path::new("some/other/path/").to_path_buf(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let fragments = generate("mypkg", &assets, &Options::default(), &mock_listener).unwrap();
assert_eq!(0, fragments.len());
}
#[rstest(ip, inst, ne, rau, ns, nsou,
case("ult", false, false, false, false, false),
case("lss", false, false, false, false, false),
case("lss", false, false, false, false, true),
case("lss", false, false, false, true, false),
case("lss", false, false, false, true, true),
case("lss", false, false, true, false, false),
case("lss", false, false, true, false, true),
case("lss", false, false, true, true, false),
case("lss", false, false, true, true, true),
case("lss", false, true, false, false, false),
case("lss", false, true, false, false, true),
case("lss", false, true, false, true, false),
case("lss", false, true, false, true, true),
case("lss", false, true, true, false, false),
case("lss", false, true, true, false, true),
case("lss", false, true, true, true, false),
case("lss", false, true, true, true, true),
case("lss", true, false, false, false, false),
case("lss", true, false, false, false, true),
case("lss", true, false, false, true, false),
case("lss", true, false, false, true, true),
case("lss", true, false, true, false, false),
case("lss", true, false, true, false, true),
case("lss", true, false, true, true, false),
case("lss", true, false, true, true, true),
case("lss", true, true, false, false, false),
case("lss", true, true, false, false, true),
case("lss", true, true, false, true, false),
case("lss", true, true, false, true, true),
case("lss", true, true, true, false, false),
case("lss", true, true, true, false, true),
case("lss", true, true, true, true, false),
case("lss", true, true, true, true, true),
)]
#[test]
fn generate_creates_expected_autoscript_fragments(
ip: &str,
inst: bool,
ne: bool,
rau: bool,
ns: bool,
nsou: bool,
) {
let unit_file_path = "debian/mypkg.service";
let install_base_path = match ip {
"ult" => "usr/lib/tmpfiles.d",
"lss" => "lib/systemd/system",
x => panic!("Unsupported install path value '{x}'"),
};
let assets = vec![Asset::new(
AssetSource::Path(PathBuf::from(unit_file_path)),
format!("{install_base_path}/mypkg.service").into(),
0o0,
crate::manifest::IsBuilt::No,
false,
)];
let options = Options {
no_enable: ne,
no_start: ns,
restart_after_upgrade: rau,
no_stop_on_upgrade: nsou,
};
let mut mock_listener = crate::listener::MockListener::new();
mock_listener.expect_info().return_const(());
let mut unit_file_content = "[Unit]
Description=A test unit
[Service]
Type=simple
".to_owned();
if inst {
unit_file_content.push_str("[Install]
WantedBy=multi-user.target");
}
set_test_fs_path_content(unit_file_path, unit_file_content);
let _g = add_test_fs_paths(&[
"postinst-init-tmpfiles",
"postinst-systemd-dont-enable",
"postinst-systemd-enable",
"postinst-systemd-restart",
"postinst-systemd-restartnostart",
"postinst-systemd-start",
"postrm-systemd",
"postrm-systemd-reload-only",
"prerm-systemd",
"prerm-systemd-restart",
]);
let fragments = generate("mypkg", &assets, &options, &mock_listener).unwrap();
let mut autoscript_fragments_to_check_for = std::collections::HashSet::new();
match ip {
"ult" => {
assert_eq!(1, get_read_count("postinst-init-tmpfiles"));
autoscript_fragments_to_check_for.insert("postinst.debhelper");
},
"lss" => {
assert_eq!(1, get_read_count(unit_file_path));
if inst {
match options.no_enable {
true => assert_eq!(1, get_read_count("postinst-systemd-dont-enable")),
false => assert_eq!(1, get_read_count("postinst-systemd-enable")),
};
assert_eq!(1, get_read_count("postrm-systemd"));
autoscript_fragments_to_check_for.insert("postinst.service");
autoscript_fragments_to_check_for.insert("postrm.debhelper");
}
match options.restart_after_upgrade {
true => {
match options.no_start {
true => assert_eq!(1, get_read_count("postinst-systemd-restartnostart")),
false => assert_eq!(1, get_read_count("postinst-systemd-restart")),
};
autoscript_fragments_to_check_for.insert("postinst.service");
},
false => if !options.no_start {
assert_eq!(1, get_read_count("postinst-systemd-start"));
autoscript_fragments_to_check_for.insert("postinst.service");
},
}
if options.restart_after_upgrade || options.no_stop_on_upgrade {
assert_eq!(1, get_read_count("prerm-systemd-restart"));
autoscript_fragments_to_check_for.insert("prerm.service");
} else if !options.no_start {
assert_eq!(1, get_read_count("prerm-systemd"));
autoscript_fragments_to_check_for.insert("prerm.service");
}
assert_eq!(1, get_read_count("postrm-systemd-reload-only"));
autoscript_fragments_to_check_for.insert("postrm.debhelper");
},
_ => unreachable!(),
}
for autoscript in &autoscript_fragments_to_check_for {
let key = format!("mypkg.{autoscript}");
assert!(fragments.contains_key(&key), "{}", key);
}
}
}