Skip to main content

cargo_deb/dh/
dh_lib.rs

1/// This module is a partial implementation of the Debian `DebHelper` core library
2/// aka `dh_lib`. Specifically this implementation is based on the Ubuntu version
3/// labelled 12.10ubuntu1 which is included in Ubuntu 20.04 LTS. I believe 12 is
4/// a reference to Debian 12 "Bookworm", i.e. Ubuntu uses future Debian sources
5/// and is also referred to as compat level 12 by debhelper documentation. Only
6/// functionality that was needed to properly script installation of systemd
7/// units, i.e. that used by the debhelper `dh_instalsystemd` command or rather
8/// our `dh_installsystemd.rs` implementation of it, is included here.
9///
10/// # See also
11///
12/// Ubuntu 20.04 `dh_lib` sources:
13/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1>
14///
15/// Ubuntu 20.04 `dh_installsystemd` man page (online HTML version):
16/// <http://manpages.ubuntu.com/manpages/focal/en/man1/dh_installdeb.1.html>
17use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19
20use crate::error::CargoDebError;
21use crate::listener::Listener;
22use crate::util::{is_path_file, read_file_to_string};
23use crate::CDResult;
24
25/// DebHelper autoscripts are embedded in the Rust library binary.
26/// The autoscripts were taken from:
27///   <https://git.launchpad.net/ubuntu/+source/debhelper/tree/autoscripts?h=applied/12.10ubuntu1>
28/// To understand which scripts are invoked when, consult:
29///   <https://www.debian.org/doc/debian-policy/ap-flowcharts.htm>
30static AUTOSCRIPTS: [(&str, &[u8]); 11] = [
31    ("postinst-init-tmpfiles", include_bytes!("../../autoscripts/postinst-init-tmpfiles")),
32    ("postinst-systemd-dont-enable", include_bytes!("../../autoscripts/postinst-systemd-dont-enable")),
33    ("postinst-systemd-enable", include_bytes!("../../autoscripts/postinst-systemd-enable")),
34    ("postinst-systemd-restart", include_bytes!("../../autoscripts/postinst-systemd-restart")),
35    ("postinst-systemd-restartnostart", include_bytes!("../../autoscripts/postinst-systemd-restartnostart")),
36    ("postinst-systemd-start", include_bytes!("../../autoscripts/postinst-systemd-start")),
37    ("postrm-systemd", include_bytes!("../../autoscripts/postrm-systemd")),
38    ("postrm-systemd-reload-only", include_bytes!("../../autoscripts/postrm-systemd-reload-only")),
39    ("prerm-systemd", include_bytes!("../../autoscripts/prerm-systemd")),
40    ("prerm-systemd-restart", include_bytes!("../../autoscripts/prerm-systemd-restart")),
41    ("postinst-sysusers", include_bytes!("../../autoscripts/postinst-sysusers")),
42];
43pub(crate) type ScriptFragments = HashMap<String, String>;
44
45/// Find a file in the given directory that best matches the given package,
46/// filename and (optional) unit name. Enables callers to use the most specific
47/// match while also falling back to a less specific match (e.g. a file to be
48/// used as a default) when more specific matches are not available.
49///
50/// Returns one of the following, in order of most preferred first:
51///
52///   - `Some("<dir>/<package>.<unit_name>.<filename>")`
53///   - `Some("<dir>/<package>.<filename>")`
54///   - `Some("<dir>/<unit_name>.<filename>")`
55///   - `Some("<dir>/<filename>")`
56///   - `None`
57///
58/// <filename> is either a systemd unit type such as `service` or `socket`, or a
59/// maintainer script name such as `postinst`.
60///
61/// Note: `main_package` should ne the first package listed in the Debian package
62/// control file.
63///
64/// # Known limitations
65///
66/// The `pkgfile()` subroutine in the actual `dh_installsystemd` code is capable of
67/// matching architecture and O/S specific unit files, but this implementation
68/// does not support architecture or O/S specific unit files.
69///
70/// # References
71///
72/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n286>
73/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n957>
74pub(crate) fn pkgfile(dir: &Path, main_package: &str, package: &str, filename: &str, unit_name: Option<&str>) -> Option<PathBuf> {
75    let mut paths_to_try = Vec::new();
76    let is_main_package = main_package == package;
77
78    // From man 1 dh_installsystemd on Ubuntu 20.04 LTS. See:
79    //   http://manpages.ubuntu.com/manpages/focal/en/man1/dh_installsystemd.1.html
80    // --name=name
81    //     ...
82    //     It changes the name that dh_installsystemd uses when it looks for
83    //     maintainer provided systemd unit files as listed in the "FILES"
84    //     section.  As an example, dh_installsystemd --name foo will look for
85    //     debian/package.foo.service instead of debian/package.service).  These
86    //     unit files are installed as name.unit-extension (in the example, it
87    //     would be installed as foo.service).
88    //     ...
89    if let Some(str) = unit_name {
90        let named_filename = format!("{str}.{filename}");
91        paths_to_try.push(dir.join(format!("{package}.{named_filename}")));
92        if is_main_package {
93            paths_to_try.push(dir.join(named_filename));
94        }
95    }
96
97    paths_to_try.push(dir.join(format!("{package}.{filename}")));
98    if is_main_package {
99        paths_to_try.push(dir.join(filename));
100    }
101
102    paths_to_try.into_iter().find(|p| {
103        log::debug!("Looking for a systemd unit in {}", p.display());
104        is_path_file(p)
105    })
106}
107
108/// Get the bytes for the specified filename whose contents were embedded in our
109/// binary by the rust-embed crate. See #[derive(RustEmbed)] above, decode them
110/// as UTF-8 and return as an owned copy of the resulting String. Also appends
111/// a trailing newline '\n' if missing.
112pub(crate) fn get_embedded_autoscript(snippet_filename: &str) -> String {
113    let mut snippet: Option<String> = None;
114
115    // load from test data if defined
116    if cfg!(test) {
117        let path = Path::new(snippet_filename);
118        if is_path_file(path) {
119            snippet = read_file_to_string(path).ok();
120        }
121    }
122
123    // else load from embedded strings
124    let mut snippet = snippet.unwrap_or_else(|| {
125        let (_, snippet_bytes) = AUTOSCRIPTS.iter().find(|(s, _)| *s == snippet_filename)
126            .unwrap_or_else(|| panic!("Unknown autoscript '{snippet_filename}'"));
127
128        // convert to string
129        String::from_utf8_lossy(snippet_bytes).into_owned()
130    });
131
132    // normalize
133    if !snippet.ends_with('\n') {
134        snippet.push('\n');
135    }
136
137    // return
138    snippet
139}
140
141/// Build up one or more shell script fragments for a given maintainer script
142/// for a debian package in preparation for writing them into or as complete
143/// maintainer scripts in `apply()`, pulling fragments from a "library" of
144/// so-called "autoscripts".
145///
146/// Takes a map of values to search and replace in the selected "autoscript"
147/// fragment such as a systemd unit name placeholder and value.
148///
149/// # Cargo Deb specific behaviour
150///
151/// The autoscripts are sourced from within the binary via the `rust_embed` crate.
152///
153/// Results are stored as updated or new entries in the `ScriptFragments` map,
154/// rather than being written to temporary files on disk.
155///
156/// # Known limitations
157///
158/// Arbitrary sed command based file editing is not supported.
159///
160/// # References
161///
162/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n1135>
163pub(crate) fn autoscript(
164    scripts: &mut ScriptFragments,
165    package: &str,
166    script: &str,
167    snippet_filename: &str,
168    replacements: &HashMap<&str, String>,
169    service_order: bool,
170    listener: &dyn Listener,
171) -> CDResult<()> {
172    let bin_name = std::env::current_exe().unwrap();
173    let bin_name = bin_name.file_name().unwrap();
174    let bin_name = bin_name.to_str().unwrap();
175    let outfile_ext = if service_order { "service" } else { "debhelper" };
176    let outfile = format!("{package}.{script}.{outfile_ext}");
177
178    listener.progress("Applying", format!("autoscript {snippet_filename} to maintainer script {script}"));
179
180    if replacements.is_empty() {
181        // We don't support sed commands yet.
182        return Err(CargoDebError::Str("unsupported"));
183    }
184
185    let new_block = [
186        &format!("# Automatically added by {bin_name}\n"),
187        &autoscript_sed(snippet_filename, replacements),
188        "# End automatically added section\n",
189    ].concat();
190
191    let existing_text = scripts.get(&outfile).map(String::as_str).unwrap_or_default();
192
193    // prevent things like `postinst` `daemon-reload` and others being emitted > 1
194    if existing_text.contains(&new_block) {
195        return Ok(());
196    }
197
198    let new_text = if script == "postrm" || script == "prerm" {
199        // prepend: teardown fragments accumulate in reverse order so units are stopped
200        // and cleaned up in the opposite order of how postinst set them up
201        [new_block.as_str(), existing_text].concat()
202    } else {
203        // append to existing script fragment (if any)
204        [existing_text, new_block.as_str()].concat()
205    };
206
207    scripts.insert(outfile, new_text);
208
209    Ok(())
210}
211
212/// Search and replace a collection of key => value pairs in the given file and
213/// return the resulting text as a String.
214///
215/// # Known limitations
216///
217/// Keys are replaced in arbitrary order, not in reverse sorted order. See:
218///   <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n1214>
219///
220/// # References
221///
222/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n1203>
223fn autoscript_sed(snippet_filename: &str, replacements: &HashMap<&str, String>) -> String {
224    let mut snippet = get_embedded_autoscript(snippet_filename);
225
226    for (from, to) in replacements {
227        snippet = snippet.replace(&format!("#{from}#"), to);
228    }
229
230    snippet
231}
232
233/// Copy the merged autoscript fragments to the final maintainer script, either
234/// at the point where the user placed a #DEBHELPER# token to indicate where
235/// they should be inserted, or by adding a shebang header to make the fragments
236/// into a complete shell script.
237///
238/// # Cargo Deb specific behaviour
239///
240/// Results are stored as updated or new entries in the `ScriptFragments` map,
241/// rather than being written to temporary files on disk.
242///
243/// # Known limitations
244///
245/// Only the #DEBHELPER# token is replaced. Is that enough? See:
246///   <https://www.man7.org/linux/man-pages/man1/dh_installdeb.1.html#SUBSTITUTION_IN_MAINTAINER_SCRIPTS>
247///
248/// # References
249///
250/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n2161>
251fn debhelper_script_subst(user_scripts_dir: &Path, scripts: &mut ScriptFragments, package: &str, script: &str, unit_name: Option<&str>,
252    listener: &dyn Listener) -> CDResult<()>
253{
254    let user_file = pkgfile(user_scripts_dir, package, package, script, unit_name);
255    let mut generated_scripts: Vec<String> = vec![
256        format!("{package}.{script}.debhelper"),
257        format!("{package}.{script}.service"),
258    ];
259
260    if let "prerm" | "postrm" = script {
261        generated_scripts.reverse();
262    }
263
264    // merge the generated scripts if they exist into the user script
265    let mut generated_text = String::new();
266    for generated_file_name in &generated_scripts {
267        if let Some(contents) = scripts.get(generated_file_name) {
268            generated_text.push_str(contents);
269        }
270    }
271
272    if let Some(user_file_path) = user_file {
273        listener.progress("Augmenting", format!("maintainer script {}", user_file_path.display()));
274
275        // merge the generated scripts if they exist into the user script
276        // if no generated script exists, we still need to remove #DEBHELPER# if
277        // present otherwise the script will be syntactically invalid
278        let user_text = read_file_to_string(&user_file_path)
279            .map_err(|e| CargoDebError::IoFile("Unable to read maintainer script file", e, user_file_path.clone()))?;
280        let new_text = user_text.replace("#DEBHELPER#", &generated_text);
281        if new_text == user_text {
282            return Err(CargoDebError::DebHelperReplaceFailed(user_file_path));
283        }
284        scripts.insert(script.into(), new_text);
285    } else if !generated_text.is_empty() {
286        listener.progress("Generating", format!("maintainer script {script}"));
287
288        // give it a shebang header and rename it
289        let mut new_text = String::new();
290        new_text.push_str("#!/bin/sh\n");
291        new_text.push_str("set -e\n");
292        new_text.push_str(&generated_text);
293
294        scripts.insert(script.into(), new_text);
295    }
296
297    Ok(())
298}
299
300/// Generate final maintainer scripts by merging the autoscripts that have been
301/// collected in the `ScriptFragments` map  with the maintainer scripts
302/// on disk supplied by the user.
303///
304/// See: <https://git.launchpad.net/ubuntu/+source/debhelper/tree/dh_installdeb?h=applied/12.10ubuntu1#n300>
305pub(crate) fn apply(user_scripts_dir: &Path, scripts: &mut ScriptFragments, package: &str, unit_name: Option<&str>, listener: &dyn Listener) -> CDResult<()> {
306    for script in &["postinst", "preinst", "prerm", "postrm"] {
307        // note: we don't support custom defines thus we don't have the final
308        // 'package_subst' argument to debhelper_script_subst().
309        debhelper_script_subst(user_scripts_dir, scripts, package, script, unit_name, listener)?;
310    }
311
312    Ok(())
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::util::tests::{add_test_fs_paths, set_test_fs_path_content};
319    use rstest::*;
320
321    // helper conversion
322    // create a new type to work around error "only traits defined in
323    // the current crate can be implemented for arbitrary types"
324    #[derive(Debug)]
325    struct LocalOptionPathBuf(Option<PathBuf>);
326    // Implement <&str> == <LocalOptionPathBuf> comparisons
327    impl PartialEq<LocalOptionPathBuf> for &str {
328        fn eq(&self, other: &LocalOptionPathBuf) -> bool {
329            Some(Path::new(self).to_path_buf()) == other.0
330        }
331    }
332    // Implement <LocalOptionPathBuf> == <&str> comparisons
333    impl PartialEq<&str> for LocalOptionPathBuf {
334        fn eq(&self, other: &&str) -> bool {
335            self.0 == Some(Path::new(*other).to_path_buf())
336        }
337    }
338
339    #[test]
340    fn pkgfile_finds_most_specific_match_with_pkg_unit_file() {
341        let _g = add_test_fs_paths(&[
342            "/parent/dir/postinst",
343            "/parent/dir/myunit.postinst",
344            "/parent/dir/mypkg.postinst",
345            "/parent/dir/mypkg.myunit.postinst",
346            "/parent/dir/nested/mypkg.myunit.postinst",
347            "/parent/mypkg.myunit.postinst",
348        ]);
349
350        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", Some("myunit"));
351        assert_eq!("/parent/dir/mypkg.myunit.postinst", LocalOptionPathBuf(r));
352
353        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", None);
354        assert_eq!("/parent/dir/mypkg.postinst", LocalOptionPathBuf(r));
355    }
356
357    #[test]
358    fn pkgfile_finds_most_specific_match_without_unit_file() {
359        let _g = add_test_fs_paths(&["/parent/dir/postinst", "/parent/dir/mypkg.postinst"]);
360
361        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", Some("myunit"));
362        assert_eq!("/parent/dir/mypkg.postinst", LocalOptionPathBuf(r));
363
364        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", None);
365        assert_eq!("/parent/dir/mypkg.postinst", LocalOptionPathBuf(r));
366    }
367
368    #[test]
369    fn pkgfile_finds_most_specific_match_without_pkg_file() {
370        let _g = add_test_fs_paths(&["/parent/dir/postinst", "/parent/dir/myunit.postinst"]);
371
372        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", Some("myunit"));
373        assert_eq!("/parent/dir/myunit.postinst", LocalOptionPathBuf(r));
374
375        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", None);
376        assert_eq!("/parent/dir/postinst", LocalOptionPathBuf(r));
377    }
378
379    #[test]
380    fn pkgfile_finds_a_fallback_match() {
381        let _g = add_test_fs_paths(&[
382            "/parent/dir/postinst",
383            "/parent/dir/myunit.postinst",
384            "/parent/dir/mypkg.postinst",
385            "/parent/dir/mypkg.myunit.postinst",
386            "/parent/dir/nested/mypkg.myunit.postinst",
387            "/parent/mypkg.myunit.postinst",
388        ]);
389
390        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "postinst", Some("wrongunit"));
391        assert_eq!("/parent/dir/mypkg.postinst", LocalOptionPathBuf(r));
392
393        let r = pkgfile(Path::new("/parent/dir/"), "wrongpkg", "wrongpkg", "postinst", None);
394        assert_eq!("/parent/dir/postinst", LocalOptionPathBuf(r));
395    }
396
397    #[test]
398    fn pkgfile_fails_to_find_a_match() {
399        let _g = add_test_fs_paths(&[
400            "/parent/dir/postinst",
401            "/parent/dir/myunit.postinst",
402            "/parent/dir/mypkg.postinst",
403            "/parent/dir/mypkg.myunit.postinst",
404            "/parent/dir/nested/mypkg.myunit.postinst",
405            "/parent/mypkg.myunit.postinst",
406        ]);
407
408        let r = pkgfile(Path::new("/parent/dir/"), "mypkg", "mypkg", "wrongfile", None);
409        assert_eq!(None, r);
410
411        let r = pkgfile(Path::new("/wrong/dir/"), "mypkg", "mypkg", "postinst", None);
412        assert_eq!(None, r);
413    }
414
415    fn autoscript_test_wrapper(pkg: &str, script: &str, snippet: &str, unit: &str, scripts: Option<ScriptFragments>) -> ScriptFragments {
416        let mut mock_listener = crate::listener::MockListener::new();
417        mock_listener.expect_progress().times(1).return_const(());
418        let mut scripts = scripts.unwrap_or_default();
419        let replacements = map! { "UNITFILES" => unit.to_owned() };
420        autoscript(&mut scripts, pkg, script, snippet, &replacements, false, &mock_listener).unwrap();
421        scripts
422    }
423
424    #[test]
425    #[should_panic(expected = "Unknown autoscript 'idontexist'")]
426    fn autoscript_panics_with_unknown_autoscript() {
427        autoscript_test_wrapper("mypkg", "somescript", "idontexist", "dummyunit", None);
428    }
429
430    #[test]
431    fn autoscript_panics_in_sed_mode() {
432        let mut mock_listener = crate::listener::MockListener::new();
433        mock_listener.expect_progress().times(1).return_const(());
434        let mut scripts = ScriptFragments::new();
435
436        // sed mode is when no search -> replacement pairs are defined
437        let sed_mode = &HashMap::new();
438
439        assert!(autoscript(&mut scripts, "mypkg", "somescript", "idontexist", sed_mode, false, &mock_listener).is_err());
440    }
441
442    #[test]
443    fn autoscript_check_embedded_files() {
444        let mut actual_scripts: Vec<_> = AUTOSCRIPTS.iter().map(|(name, _)| *name).collect();
445        actual_scripts.sort_unstable();
446
447        let expected_scripts = vec![
448            "postinst-init-tmpfiles",
449            "postinst-systemd-dont-enable",
450            "postinst-systemd-enable",
451            "postinst-systemd-restart",
452            "postinst-systemd-restartnostart",
453            "postinst-systemd-start",
454            "postinst-sysusers",
455            "postrm-systemd",
456            "postrm-systemd-reload-only",
457            "prerm-systemd",
458            "prerm-systemd-restart",
459        ];
460
461        assert_eq!(expected_scripts, actual_scripts);
462    }
463
464    #[test]
465    fn autoscript_sanity_check_all_embedded_autoscripts() {
466        for (autoscript_filename, _) in &AUTOSCRIPTS {
467            autoscript_test_wrapper("mypkg", "somescript", autoscript_filename, "dummyunit", None);
468        }
469    }
470
471    #[rstest(maintainer_script, prepend,
472        case::prerm("prerm", true),
473        case::preinst("preinst", false),
474        case::postinst("postinst", false),
475        case::postrm("postrm", true),
476    )]
477    fn autoscript_detailed_check(maintainer_script: &str, prepend: bool) {
478        let autoscript_name = "postrm-systemd";
479
480        // Populate an autoscript template and add the result to a
481        // collection of scripts and return it to us.
482        let scripts = autoscript_test_wrapper("mypkg", maintainer_script, autoscript_name, "dummyunit", None);
483
484        // Expect autoscript() to have created one temporary script
485        // fragment called <package>.<script>.debhelper.
486        assert_eq!(1, scripts.len());
487
488        let expected_created_name = &format!("mypkg.{maintainer_script}.debhelper");
489        let (created_name, created_text) = scripts.iter().next().unwrap();
490
491        // Verify the created script filename key
492        assert_eq!(expected_created_name, created_name);
493
494        // Verify the created script contents. It should have two lines
495        // more than the autoscript fragment it was based on, like so:
496        //   # Automatically added by ...
497        //   <autoscript fragment lines with placeholders replaced>
498        //   # End automatically added section
499        let autoscript_text = get_embedded_autoscript(autoscript_name);
500        let autoscript_line_count = autoscript_text.lines().count();
501        let created_line_count = created_text.lines().count();
502        assert_eq!(autoscript_line_count + 2, created_line_count);
503
504        // Verify the content of the added comment lines
505        let mut lines = created_text.lines();
506        assert!(lines.next().unwrap().starts_with("# Automatically added by"));
507        assert_eq!(lines.nth_back(0).unwrap(), "# End automatically added section");
508
509        // Check that the autoscript fragment lines were properly copied
510        // into the created script complete with expected substitutions
511        let expected_autoscript_text1 = autoscript_text.replace("#UNITFILES#", "dummyunit");
512        let expected_autoscript_text1 = expected_autoscript_text1.trim_end();
513        let start1 = 1;
514        let end1 = start1 + autoscript_line_count;
515        let created_autoscript_text1 = created_text.lines().collect::<Vec<&str>>()[start1..end1].join("\n");
516        assert_ne!(expected_autoscript_text1, autoscript_text);
517        assert_eq!(expected_autoscript_text1, created_autoscript_text1);
518
519        // Process the same autoscript again but use a different unit
520        // name so that we can see if the autoscript template was again
521        // populated but this time with the different value, and pass in
522        // the existing set of created scripts to check how it gets
523        // modified.
524        let scripts = autoscript_test_wrapper("mypkg", maintainer_script, autoscript_name, "otherunit", Some(scripts));
525
526        // The number and name of the output scripts should remain the same
527        assert_eq!(1, scripts.len());
528        let (created_name, created_text) = scripts.iter().next().unwrap();
529        assert_eq!(expected_created_name, created_name);
530
531        // The line structure should now contain two injected blocks
532        let created_line_count = created_text.lines().count();
533        assert_eq!((autoscript_line_count + 2) * 2, created_line_count);
534
535        let mut lines = created_text.lines();
536        assert!(lines.next().unwrap().starts_with("# Automatically added by"));
537        assert_eq!(lines.nth_back(0).unwrap(), "# End automatically added section");
538
539        // The content should be different
540        let expected_autoscript_text2 = autoscript_text.replace("#UNITFILES#", "otherunit");
541        let expected_autoscript_text2 = expected_autoscript_text2.trim_end();
542        let start2 = end1 + 2;
543        let end2 = start2 + autoscript_line_count;
544        let created_autoscript_text1 = created_text.lines().collect::<Vec<&str>>()[start1..end1].join("\n");
545        let created_autoscript_text2 = created_text.lines().collect::<Vec<&str>>()[start2..end2].join("\n");
546        assert_ne!(expected_autoscript_text1, autoscript_text);
547        assert_ne!(expected_autoscript_text2, autoscript_text);
548
549        if prepend {
550            assert_eq!(expected_autoscript_text1, created_autoscript_text2);
551            assert_eq!(expected_autoscript_text2, created_autoscript_text1);
552        } else {
553            assert_eq!(expected_autoscript_text1, created_autoscript_text1);
554            assert_eq!(expected_autoscript_text2, created_autoscript_text2);
555        }
556    }
557
558    #[test]
559    fn autoscript_does_not_duplicate_identical_fragments() {
560        let scripts = autoscript_test_wrapper("mypkg", "postinst", "postrm-systemd", "dummyunit", None);
561        let text_after_first = scripts.get("mypkg.postinst.debhelper").unwrap().clone();
562
563        // requesting the exact same fragment again must not duplicate it
564        let scripts = autoscript_test_wrapper("mypkg", "postinst", "postrm-systemd", "dummyunit", Some(scripts));
565        let text_after_second = scripts.get("mypkg.postinst.debhelper").unwrap();
566
567        assert_eq!(&text_after_first, text_after_second);
568    }
569
570    #[test]
571    fn autoscript_check_service_order() {
572        let mut mock_listener = crate::listener::MockListener::new();
573        mock_listener.expect_progress().return_const(());
574        let replacements = map! { "UNITFILES" => "someunit".to_owned() };
575
576        let in_out = vec![(false, "debhelper"), (true, "service")];
577
578        for (service_order, expected_ext) in in_out {
579            let mut scripts = ScriptFragments::new();
580            autoscript(&mut scripts, "mypkg", "prerm", "postrm-systemd", &replacements, service_order, &mock_listener).unwrap();
581
582            assert_eq!(1, scripts.len());
583
584            let expected_path = &format!("mypkg.prerm.{expected_ext}");
585            let actual_path = scripts.keys().next().unwrap();
586            assert_eq!(expected_path, actual_path);
587        }
588    }
589
590    #[fixture]
591    #[allow(unused_braces)]
592    fn empty_user_file() -> String { String::new() }
593
594    #[fixture]
595    #[allow(unused_braces)]
596    fn invalid_user_file() -> String { "some content".to_owned() }
597
598    #[fixture]
599    #[allow(unused_braces)]
600    fn valid_user_file() -> String { "some #DEBHELPER# content".to_owned() }
601
602    #[test]
603    fn debhelper_script_subst_with_no_matching_files() {
604        let mut mock_listener = crate::listener::MockListener::new();
605        mock_listener.expect_info().times(0).return_const(());
606
607        let mut scripts = ScriptFragments::new();
608
609        assert_eq!(0, scripts.len());
610        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
611        assert_eq!(0, scripts.len());
612    }
613
614    #[rstest]
615    #[should_panic(expected = "Test failed as expected")]
616    fn debhelper_script_subst_errs_if_user_file_lacks_token(invalid_user_file: String) {
617        let _g = add_test_fs_paths(&[]);
618        set_test_fs_path_content("myscript", invalid_user_file);
619
620        let mut mock_listener = crate::listener::MockListener::new();
621        mock_listener.expect_progress().times(1).return_const(());
622
623        let mut scripts = ScriptFragments::new();
624
625        match debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener) {
626            Ok(()) => (),
627            Err(CargoDebError::DebHelperReplaceFailed(_)) => panic!("Test failed as expected"),
628            Err(err) => panic!("Unexpected error {err:?}"),
629        }
630    }
631
632    #[rstest]
633    #[test]
634    fn debhelper_script_subst_with_user_file_only(valid_user_file: String) {
635        let _g = add_test_fs_paths(&[]);
636        set_test_fs_path_content("myscript", valid_user_file);
637
638        let mut mock_listener = crate::listener::MockListener::new();
639        mock_listener.expect_progress().times(1).return_const(());
640
641        let mut scripts = ScriptFragments::new();
642
643        assert_eq!(0, scripts.len());
644        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
645        assert_eq!(1, scripts.len());
646        assert!(scripts.contains_key("myscript"));
647    }
648
649    fn script_to_string<'a>(scripts: &'a ScriptFragments, script: &str) -> &'a str {
650        scripts.get(script).unwrap()
651    }
652
653    #[test]
654    fn debhelper_script_subst_with_generated_file_only() {
655        let _g = add_test_fs_paths(&[]);
656        let mut mock_listener = crate::listener::MockListener::new();
657        mock_listener.expect_progress().times(1).return_const(());
658
659        let mut scripts = ScriptFragments::new();
660        scripts.insert("mypkg.myscript.debhelper".to_owned(), "injected".into());
661
662        assert_eq!(1, scripts.len());
663        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
664        assert_eq!(2, scripts.len());
665        assert!(scripts.contains_key("mypkg.myscript.debhelper"));
666        assert!(scripts.contains_key("myscript"));
667
668        assert_eq!(script_to_string(&scripts, "mypkg.myscript.debhelper"), "injected");
669        assert_eq!(script_to_string(&scripts, "myscript"), "#!/bin/sh\nset -e\ninjected");
670    }
671
672    #[rstest]
673    #[test]
674    fn debhelper_script_subst_with_user_and_generated_file(valid_user_file: String) {
675        let _g = add_test_fs_paths(&[]);
676        set_test_fs_path_content("myscript", valid_user_file);
677
678        let mut mock_listener = crate::listener::MockListener::new();
679        mock_listener.expect_progress().times(1).return_const(());
680
681        let mut scripts = ScriptFragments::new();
682        scripts.insert("mypkg.myscript.debhelper".to_owned(), "injected".into());
683
684        assert_eq!(1, scripts.len());
685        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
686        assert_eq!(2, scripts.len());
687        assert!(scripts.contains_key("mypkg.myscript.debhelper"));
688        assert!(scripts.contains_key("myscript"));
689
690        assert_eq!(script_to_string(&scripts, "mypkg.myscript.debhelper"), "injected");
691        assert_eq!(script_to_string(&scripts, "myscript"), "some injected content");
692    }
693
694    #[rstest(maintainer_script, service_order,
695        case("preinst", false),
696        case("prerm", true),
697        case("postinst", false),
698        case("postrm", true),
699    )]
700    #[test]
701    fn debhelper_script_subst_with_user_and_generated_files(
702        valid_user_file: String,
703        maintainer_script: &'static str,
704        service_order: bool,
705    ) {
706        let _g = add_test_fs_paths(&[]);
707        set_test_fs_path_content(maintainer_script, valid_user_file);
708
709        let mut mock_listener = crate::listener::MockListener::new();
710        mock_listener.expect_progress().times(1).return_const(());
711
712        let mut scripts = ScriptFragments::new();
713        scripts.insert(format!("mypkg.{maintainer_script}.debhelper"), "first".into());
714        scripts.insert(format!("mypkg.{maintainer_script}.service"), "second".into());
715
716        assert_eq!(2, scripts.len());
717        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", maintainer_script, None, &mock_listener).unwrap();
718        assert_eq!(3, scripts.len());
719        assert!(scripts.contains_key(&format!("mypkg.{maintainer_script}.debhelper")));
720        assert!(scripts.contains_key(&format!("mypkg.{maintainer_script}.service")));
721        assert!(scripts.contains_key(maintainer_script));
722
723        assert_eq!(script_to_string(&scripts, &format!("mypkg.{maintainer_script}.debhelper")), "first");
724        assert_eq!(script_to_string(&scripts, &format!("mypkg.{maintainer_script}.service")), "second");
725        if service_order {
726            assert_eq!(script_to_string(&scripts, maintainer_script), "some secondfirst content");
727        } else {
728            assert_eq!(script_to_string(&scripts, maintainer_script), "some firstsecond content");
729        }
730    }
731
732    #[rstest(
733        error,
734        case::invalid_input("InvalidInput"),
735        case::interrupted("Interrupted"),
736        case::permission_denied("PermissionDenied"),
737        case::not_found("NotFound"),
738        case::other("Other")
739    )]
740    #[test]
741    fn debhelper_script_subst_with_user_file_access_error(error: &str) {
742        let _g = add_test_fs_paths(&[]);
743        set_test_fs_path_content("myscript", format!("error:{error}"));
744
745        let mut mock_listener = crate::listener::MockListener::new();
746        mock_listener.expect_progress().times(1).return_const(());
747
748        let mut scripts = ScriptFragments::new();
749
750        assert_eq!(0, scripts.len());
751        let result = debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener);
752
753        assert!(matches!(result, Err(CargoDebError::IoFile(..))));
754        if let CargoDebError::IoFile(_, err, _) = result.unwrap_err() {
755            assert_eq!(error, format!("{:?}", err.kind()));
756        } else {
757            unreachable!()
758        }
759    }
760
761    #[test]
762    fn apply_with_no_matching_files() {
763        let mut mock_listener = crate::listener::MockListener::new();
764        mock_listener.expect_info().times(0).return_const(());
765        apply(Path::new(""), &mut ScriptFragments::new(), "mypkg", None, &mock_listener).unwrap();
766    }
767
768    #[rstest]
769    #[test]
770    fn apply_with_valid_user_files(valid_user_file: String) {
771        let _g = add_test_fs_paths(&[]);
772        let scripts = &["postinst", "preinst", "prerm", "postrm"];
773
774        for script in scripts {
775            set_test_fs_path_content(script, valid_user_file.clone());
776        }
777
778        let mut mock_listener = crate::listener::MockListener::new();
779        mock_listener.expect_progress().times(scripts.len()).return_const(());
780
781        apply(Path::new(""), &mut ScriptFragments::new(), "mypkg", None, &mock_listener).unwrap();
782    }
783}