Skip to main content

cargo_deb/dh/
dh_installsysusers.rs

1/// This module is a partial implementation of the Debian `DebHelper` command
2/// for properly installing systemd sysusers files as part of a .deb package install aka
3/// `dh_installsysusers`.
4///
5/// Upstream only documents the "debian/$package.sysusers" spelling
6/// but also accepts "debian/sysusers".
7///
8/// # See also
9///
10/// <https://manpages.debian.org/trixie/debhelper/dh_installsysusers.1.en.html>
11/// <https://sources.debian.org/src/debhelper/13.24.2/dh_installsysusers>
12use std::path::{Path, PathBuf};
13use crate::dh::dh_installsystemd::InstallRecipe;
14use std::str;
15
16use crate::assets::Asset;
17use crate::dh::dh_lib::{autoscript, pkgfile, ScriptFragments};
18use crate::listener::Listener;
19use crate::util::fname_from_path;
20use crate::{CDResult, CargoDebError};
21
22const SYSUSERS_D_DIR: &str = "usr/lib/sysusers.d/";
23
24pub type ConfigFile = Option<(PathBuf, InstallRecipe)>;
25
26/// Find installable systemd sysusers file for the specified debian package
27/// in the given directory and return an install
28/// recipe for each file detailing the path at which the file should be
29/// installed and the mode (chmod) that the file should be given.
30pub fn find_config(dir: &Path, main_package: &str) -> ConfigFile {
31    let src_path = pkgfile(dir, main_package, main_package, "sysusers", None)?;
32
33    Some((src_path, InstallRecipe {
34        path: Path::new(SYSUSERS_D_DIR).join(format!("{main_package}.conf")),
35        mode: 0o644,
36    }))
37}
38
39pub fn generate(package: &str, assets: &[Asset],   scripts: &mut ScriptFragments, listener: &dyn Listener) -> CDResult<()> {
40    let mut sysusers_files  = assets
41        .iter()
42        .filter(|a| a.c.target_path.starts_with(SYSUSERS_D_DIR))
43        .map(|v| {
44            v.source.source_path()
45                .and_then(|p| fname_from_path(&p.with_extension("conf")))
46                .ok_or(CargoDebError::Str("dh_installsysusers: invalid source path"))
47        })
48        .collect::<CDResult<Vec<String>>>()?;
49
50    if sysusers_files.is_empty() {
51        return Ok(());
52    }
53
54    sysusers_files.sort();
55
56    autoscript(scripts, package, "postinst", "postinst-sysusers",
57        &map!{ "CONFILE_BASENAME" => sysusers_files.join(" ") }, false, listener)?;
58
59    Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::assets::{Asset, AssetKind, AssetSource, IsBuilt};
66    use crate::util::tests::{add_test_fs_paths, get_read_count, set_test_fs_path_content};
67
68    #[test]
69    fn find_units_in_empty_dir_finds_nothing() {
70        let pkg_config_file = find_config(Path::new(""), "mypkg");
71        assert_eq!(None, pkg_config_file);
72    }
73
74    fn assert_eq_found_config(pkg_config_file: &ConfigFile, expected_install_path: &str, source_path: &str) {
75        let expected = InstallRecipe {
76            path: PathBuf::from(expected_install_path),
77            mode: 0o644,
78        };
79        assert_eq!(*pkg_config_file, Some((PathBuf::from(source_path), expected)));
80    }
81
82    #[test]
83    fn find_config_for_package() {
84        // one of each valid pattern (without a specific unit) and one
85        // additional valid pattern with a unit (which should not be matched
86        // as we don't specify a specific unit name to match)
87        let _g = add_test_fs_paths(&[
88            "debian/mypkg.sysusers",
89        ]);
90        let pkg_config_file = find_config(Path::new("debian"), "mypkg");
91        assert_eq_found_config(&pkg_config_file, "usr/lib/sysusers.d/mypkg.conf", "debian/mypkg.sysusers");
92    }
93
94    #[test]
95    fn generate_with_empty_inputs_does_nothing() {
96        let mut mock_listener = crate::listener::MockListener::new();
97        mock_listener.expect_info().times(0).return_const(());
98
99        let mut fragments = ScriptFragments::new();
100        generate("", &[],  &mut fragments, &mock_listener).unwrap();
101
102        assert!(fragments.is_empty());
103    }
104
105    #[test]
106    fn generate_with_arbitrary_asset_does_nothing() {
107        let mut mock_listener = crate::listener::MockListener::new();
108        mock_listener.expect_info().times(0).return_const(());
109
110        let assets = vec![Asset::new(
111            AssetSource::Path(PathBuf::new()),
112            PathBuf::new(),
113            Some(0o0),
114            IsBuilt::No,
115            AssetKind::Any,
116        )];
117
118        let mut fragments = ScriptFragments::new();
119        generate("mypkg", &assets,  &mut fragments, &mock_listener).unwrap();
120        assert!(fragments.is_empty());
121    }
122
123    #[test]
124    fn generate_with_invalid_tmp_file_asset_fails() {
125        let mut mock_listener = crate::listener::MockListener::new();
126        mock_listener.expect_info().times(0).return_const(());
127
128        let assets = vec![Asset::new(
129            AssetSource::Path(PathBuf::new()), // path source with empty source path makes no sense
130            Path::new("usr/lib/sysusers.d/blah").to_path_buf(),
131            Some(0o0),
132            IsBuilt::No,
133            AssetKind::Any,
134        )];
135
136        assert!(generate("mypkg", &assets,  &mut ScriptFragments::new(), &mock_listener).is_err());
137    }
138
139    #[test]
140    fn generate_with_data_tmp_file_asset_fails() {
141        let mut mock_listener = crate::listener::MockListener::new();
142        mock_listener.expect_info().times(0).return_const(());
143
144        let assets = vec![Asset::new(
145            AssetSource::Data(vec![]), // only assets of type Path are currently supported
146            Path::new("usr/lib/sysusers.d/blah").to_path_buf(),
147            Some(0o0),
148            IsBuilt::No,
149            AssetKind::Any,
150        )];
151
152        assert!(generate("mypkg", &assets,  &mut ScriptFragments::new(), &mock_listener).is_err());
153    }
154
155    #[test]
156    fn generate_with_empty_sysusers_asset() {
157        use crate::dh::dh_lib::get_embedded_autoscript;
158
159        const TMP_FILE_NAME: &str = "mypkg.sysusers";
160        let tmp_file_path = PathBuf::from(format!("debian/{TMP_FILE_NAME}"));
161
162        let mut mock_listener = crate::listener::MockListener::new();
163        mock_listener.expect_progress().times(1).return_const(());
164
165        let assets = vec![Asset::new(
166            AssetSource::Path(tmp_file_path),
167            Path::new("usr/lib/sysusers.d/blah").to_path_buf(),
168            Some(0o0),
169            IsBuilt::No,
170            AssetKind::Any,
171        )];
172
173        let mut fragments = ScriptFragments::new();
174        generate("mypkg", &assets, &mut fragments, &mock_listener).unwrap();
175        assert_eq!(1, fragments.len());
176
177        let (fragment_name, created_text) = fragments.into_iter().next().unwrap();
178
179        // should create an augmentation for the postinst script
180        assert_eq!("mypkg.postinst.debhelper", fragment_name);
181
182        // Verify the created script contents. It should have two lines
183        // more than the autoscript fragment it was based on, like so:
184        //   # Automatically added by ...
185        //   <autoscript fragment lines with placeholders replaced>
186        //   # End automatically added section
187        let autoscript_text = get_embedded_autoscript("postinst-sysusers");
188        let autoscript_line_count = autoscript_text.lines().count();
189        let created_line_count = created_text.lines().count();
190        assert_eq!(autoscript_line_count + 2, created_line_count);
191
192        // Verify the content of the added comment lines
193        let mut lines = created_text.lines();
194        assert!(lines.next().unwrap().starts_with("# Automatically added by"));
195        assert_eq!(lines.nth_back(0).unwrap(), "# End automatically added section");
196
197        // Check that the autoscript fragment lines were properly copied
198        // into the created script complete with expected substitutions
199        let expected_autoscript_text = autoscript_text.replace("#CONFILE_BASENAME#", TMP_FILE_NAME.replace(".sysusers", ".conf").as_str());
200        let expected_autoscript_text = expected_autoscript_text.trim_end();
201        let start1 = 1;
202        let end1 = start1 + autoscript_line_count;
203        let created_autoscript_text = created_text.lines().collect::<Vec<&str>>()[start1..end1].join("\n");
204        assert_ne!(expected_autoscript_text, autoscript_text);
205        assert_eq!(expected_autoscript_text, created_autoscript_text);
206    }
207
208    #[test]
209    fn generate_acts_only_on_config_files_with_the_expected_install_path() {
210        // Note: find_units() will set the target path correctly.
211        let mut mock_listener = crate::listener::MockListener::new();
212        mock_listener.expect_info().times(0).return_const(());
213
214        let assets = vec![Asset::new(
215            AssetSource::Path(PathBuf::from("debian/mypkg.sysusers")),
216            Path::new("some/other/path/").to_path_buf(),
217            Some(0o0),
218            IsBuilt::No,
219            AssetKind::Any,
220        )];
221
222        let mut fragments = ScriptFragments::new();
223        generate("mypkg", &assets, &mut fragments, &mock_listener).unwrap();
224        assert_eq!(0, fragments.len());
225    }
226
227    #[test]
228    fn generate_creates_expected_autoscript_fragments() {
229        let config_file_path = "debian/mypkg.sysusers";
230
231        // setup input for generate()
232        let assets = vec![Asset::new(
233            AssetSource::Path(PathBuf::from(config_file_path)),
234            format!("usr/lib/sysusers.d/mypkg.conf").into(),
235            Some(0o0),
236            IsBuilt::No,
237            AssetKind::Any,
238        )];
239
240        // setup mocks
241        let mut mock_listener = crate::listener::MockListener::new();
242        mock_listener.expect_progress().return_const(());
243
244        let config_file_content = "u ego -\n".to_owned();
245        set_test_fs_path_content(config_file_path, config_file_content);
246
247        // Add all Autoscript paths to the in-memory test file system so that
248        // we can track whether they are read or not.
249        let _g = add_test_fs_paths(&[
250            "postinst-sysusers",
251        ]);
252
253        // generate!
254        let mut fragments = ScriptFragments::new();
255        generate("mypkg", &assets, &mut fragments, &mock_listener).unwrap();
256
257        // verify, though don't verify creation of autoscript fragments as that
258        // is verified in tests of the lower level functionality, instead verify
259        // only that the generate() logic creates the expected named fragments
260        // and while doing so read the expected autoscript files the expected
261        // number of times.
262
263        // Perl dh_installsysusers installs the postinst-sysusers fragment
264        // as long as there's at least 1 sysusers config file.
265
266        let mut autoscript_fragments_to_check_for = std::collections::HashSet::new();
267
268                assert_eq!(1, get_read_count("postinst-sysusers"));
269                autoscript_fragments_to_check_for.insert("postinst.debhelper");
270
271        for autoscript in &autoscript_fragments_to_check_for {
272            let key = format!("mypkg.{autoscript}");
273            assert!(fragments.contains_key(&key), "{}", key);
274        }
275    }
276}