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]); 10] = [
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];
42pub(crate) type ScriptFragments = HashMap<String, String>;
43
44/// Find a file in the given directory that best matches the given package,
45/// filename and (optional) unit name. Enables callers to use the most specific
46/// match while also falling back to a less specific match (e.g. a file to be
47/// used as a default) when more specific matches are not available.
48///
49/// Returns one of the following, in order of most preferred first:
50///
51///   - `Some("<dir>/<package>.<unit_name>.<filename>")`
52///   - `Some("<dir>/<package>.<filename>")`
53///   - `Some("<dir>/<unit_name>.<filename>")`
54///   - `Some("<dir>/<filename>")`
55///   - `None`
56///
57/// <filename> is either a systemd unit type such as `service` or `socket`, or a
58/// maintainer script name such as `postinst`.
59///
60/// Note: `main_package` should ne the first package listed in the Debian package
61/// control file.
62///
63/// # Known limitations
64///
65/// The `pkgfile()` subroutine in the actual `dh_installsystemd` code is capable of
66/// matching architecture and O/S specific unit files, but this implementation
67/// does not support architecture or O/S specific unit files.
68///
69/// # References
70///
71/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n286>
72/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n957>
73pub(crate) fn pkgfile(dir: &Path, main_package: &str, package: &str, filename: &str, unit_name: Option<&str>) -> Option<PathBuf> {
74    let mut paths_to_try = Vec::new();
75    let is_main_package = main_package == package;
76
77    // From man 1 dh_installsystemd on Ubuntu 20.04 LTS. See:
78    //   http://manpages.ubuntu.com/manpages/focal/en/man1/dh_installsystemd.1.html
79    // --name=name
80    //     ...
81    //     It changes the name that dh_installsystemd uses when it looks for
82    //     maintainer provided systemd unit files as listed in the "FILES"
83    //     section.  As an example, dh_installsystemd --name foo will look for
84    //     debian/package.foo.service instead of debian/package.service).  These
85    //     unit files are installed as name.unit-extension (in the example, it
86    //     would be installed as foo.service).
87    //     ...
88    if let Some(str) = unit_name {
89        let named_filename = format!("{str}.{filename}");
90        paths_to_try.push(dir.join(format!("{package}.{named_filename}")));
91        if is_main_package {
92            paths_to_try.push(dir.join(named_filename));
93        }
94    }
95
96    paths_to_try.push(dir.join(format!("{package}.{filename}")));
97    if is_main_package {
98        paths_to_try.push(dir.join(filename));
99    }
100
101    paths_to_try.into_iter().find(|p| {
102        log::debug!("Looking for a systemd unit in {}", p.display());
103        is_path_file(p)
104    })
105}
106
107/// Get the bytes for the specified filename whose contents were embedded in our
108/// binary by the rust-embed crate. See #[derive(RustEmbed)] above, decode them
109/// as UTF-8 and return as an owned copy of the resulting String. Also appends
110/// a trailing newline '\n' if missing.
111pub(crate) fn get_embedded_autoscript(snippet_filename: &str) -> String {
112    let mut snippet: Option<String> = None;
113
114    // load from test data if defined
115    if cfg!(test) {
116        let path = Path::new(snippet_filename);
117        if is_path_file(path) {
118            snippet = read_file_to_string(path).ok();
119        }
120    }
121
122    // else load from embedded strings
123    let mut snippet = snippet.unwrap_or_else(|| {
124        let (_, snippet_bytes) = AUTOSCRIPTS.iter().find(|(s, _)| *s == snippet_filename)
125            .unwrap_or_else(|| panic!("Unknown autoscript '{snippet_filename}'"));
126
127        // convert to string
128        String::from_utf8_lossy(snippet_bytes).into_owned()
129    });
130
131    // normalize
132    if !snippet.ends_with('\n') {
133        snippet.push('\n');
134    }
135
136    // return
137    snippet
138}
139
140/// Build up one or more shell script fragments for a given maintainer script
141/// for a debian package in preparation for writing them into or as complete
142/// maintainer scripts in `apply()`, pulling fragments from a "library" of
143/// so-called "autoscripts".
144///
145/// Takes a map of values to search and replace in the selected "autoscript"
146/// fragment such as a systemd unit name placeholder and value.
147///
148/// # Cargo Deb specific behaviour
149///
150/// The autoscripts are sourced from within the binary via the `rust_embed` crate.
151///
152/// Results are stored as updated or new entries in the `ScriptFragments` map,
153/// rather than being written to temporary files on disk.
154///
155/// # Known limitations
156///
157/// Arbitrary sed command based file editing is not supported.
158///
159/// # References
160///
161/// <https://git.launchpad.net/ubuntu/+source/debhelper/tree/lib/Debian/Debhelper/Dh_Lib.pm?h=applied/12.10ubuntu1#n1135>
162pub(crate) fn autoscript(
163    scripts: &mut ScriptFragments,
164    package: &str,
165    script: &str,
166    snippet_filename: &str,
167    replacements: &HashMap<&str, String>,
168    service_order: bool,
169    listener: &dyn Listener,
170) -> CDResult<()> {
171    let bin_name = std::env::current_exe().unwrap();
172    let bin_name = bin_name.file_name().unwrap();
173    let bin_name = bin_name.to_str().unwrap();
174    let outfile_ext = if service_order { "service" } else { "debhelper" };
175    let outfile = format!("{package}.{script}.{outfile_ext}");
176
177    listener.progress("Applying", format!("autoscript {snippet_filename} to maintainer script {script}"));
178
179    if scripts.contains_key(&outfile) && (script == "postrm" || script == "prerm") {
180        if !replacements.is_empty() {
181            let existing_text = scripts.get(&outfile).unwrap();
182
183            // prepend new text to existing script fragment
184            let new_text = [
185                &format!("# Automatically added by {bin_name}\n"),
186                &autoscript_sed(snippet_filename, replacements),
187                "# End automatically added section\n",
188                existing_text,
189            ].concat();
190            scripts.insert(outfile, new_text);
191        } else {
192            // We don't support sed commands yet.
193            return Err(CargoDebError::Str("unsupported"));
194        }
195    } else if !replacements.is_empty() {
196        // append to existing script fragment (if any)
197        let new_text = [
198            scripts.get(&outfile).unwrap_or(&String::new()),
199            &format!("# Automatically added by {bin_name}\n"),
200            &autoscript_sed(snippet_filename, replacements),
201            "# End automatically added section\n",
202        ].concat();
203        scripts.insert(outfile, new_text);
204    } else {
205        // We don't support sed commands yet.
206        return Err(CargoDebError::Str("unsupported"));
207    }
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            "postrm-systemd",
455            "postrm-systemd-reload-only",
456            "prerm-systemd",
457            "prerm-systemd-restart",
458        ];
459
460        assert_eq!(expected_scripts, actual_scripts);
461    }
462
463    #[test]
464    fn autoscript_sanity_check_all_embedded_autoscripts() {
465        for (autoscript_filename, _) in &AUTOSCRIPTS {
466            autoscript_test_wrapper("mypkg", "somescript", autoscript_filename, "dummyunit", None);
467        }
468    }
469
470    #[rstest(maintainer_script, prepend,
471        case::prerm("prerm", true),
472        case::preinst("preinst", false),
473        case::postinst("postinst", false),
474        case::postrm("postrm", true),
475    )]
476    fn autoscript_detailed_check(maintainer_script: &str, prepend: bool) {
477        let autoscript_name = "postrm-systemd";
478
479        // Populate an autoscript template and add the result to a
480        // collection of scripts and return it to us.
481        let scripts = autoscript_test_wrapper("mypkg", maintainer_script, autoscript_name, "dummyunit", None);
482
483        // Expect autoscript() to have created one temporary script
484        // fragment called <package>.<script>.debhelper.
485        assert_eq!(1, scripts.len());
486
487        let expected_created_name = &format!("mypkg.{maintainer_script}.debhelper");
488        let (created_name, created_text) = scripts.iter().next().unwrap();
489
490        // Verify the created script filename key
491        assert_eq!(expected_created_name, created_name);
492
493        // Verify the created script contents. It should have two lines
494        // more than the autoscript fragment it was based on, like so:
495        //   # Automatically added by ...
496        //   <autoscript fragment lines with placeholders replaced>
497        //   # End automatically added section
498        let autoscript_text = get_embedded_autoscript(autoscript_name);
499        let autoscript_line_count = autoscript_text.lines().count();
500        let created_line_count = created_text.lines().count();
501        assert_eq!(autoscript_line_count + 2, created_line_count);
502
503        // Verify the content of the added comment lines
504        let mut lines = created_text.lines();
505        assert!(lines.next().unwrap().starts_with("# Automatically added by"));
506        assert_eq!(lines.nth_back(0).unwrap(), "# End automatically added section");
507
508        // Check that the autoscript fragment lines were properly copied
509        // into the created script complete with expected substitutions
510        let expected_autoscript_text1 = autoscript_text.replace("#UNITFILES#", "dummyunit");
511        let expected_autoscript_text1 = expected_autoscript_text1.trim_end();
512        let start1 = 1;
513        let end1 = start1 + autoscript_line_count;
514        let created_autoscript_text1 = created_text.lines().collect::<Vec<&str>>()[start1..end1].join("\n");
515        assert_ne!(expected_autoscript_text1, autoscript_text);
516        assert_eq!(expected_autoscript_text1, created_autoscript_text1);
517
518        // Process the same autoscript again but use a different unit
519        // name so that we can see if the autoscript template was again
520        // populated but this time with the different value, and pass in
521        // the existing set of created scripts to check how it gets
522        // modified.
523        let scripts = autoscript_test_wrapper("mypkg", maintainer_script, autoscript_name, "otherunit", Some(scripts));
524
525        // The number and name of the output scripts should remain the same
526        assert_eq!(1, scripts.len());
527        let (created_name, created_text) = scripts.iter().next().unwrap();
528        assert_eq!(expected_created_name, created_name);
529
530        // The line structure should now contain two injected blocks
531        let created_line_count = created_text.lines().count();
532        assert_eq!((autoscript_line_count + 2) * 2, created_line_count);
533
534        let mut lines = created_text.lines();
535        assert!(lines.next().unwrap().starts_with("# Automatically added by"));
536        assert_eq!(lines.nth_back(0).unwrap(), "# End automatically added section");
537
538        // The content should be different
539        let expected_autoscript_text2 = autoscript_text.replace("#UNITFILES#", "otherunit");
540        let expected_autoscript_text2 = expected_autoscript_text2.trim_end();
541        let start2 = end1 + 2;
542        let end2 = start2 + autoscript_line_count;
543        let created_autoscript_text1 = created_text.lines().collect::<Vec<&str>>()[start1..end1].join("\n");
544        let created_autoscript_text2 = created_text.lines().collect::<Vec<&str>>()[start2..end2].join("\n");
545        assert_ne!(expected_autoscript_text1, autoscript_text);
546        assert_ne!(expected_autoscript_text2, autoscript_text);
547
548        if prepend {
549            assert_eq!(expected_autoscript_text1, created_autoscript_text2);
550            assert_eq!(expected_autoscript_text2, created_autoscript_text1);
551        } else {
552            assert_eq!(expected_autoscript_text1, created_autoscript_text1);
553            assert_eq!(expected_autoscript_text2, created_autoscript_text2);
554        }
555    }
556
557    #[test]
558    fn autoscript_check_service_order() {
559        let mut mock_listener = crate::listener::MockListener::new();
560        mock_listener.expect_progress().return_const(());
561        let replacements = map! { "UNITFILES" => "someunit".to_owned() };
562
563        let in_out = vec![(false, "debhelper"), (true, "service")];
564
565        for (service_order, expected_ext) in in_out {
566            let mut scripts = ScriptFragments::new();
567            autoscript(&mut scripts, "mypkg", "prerm", "postrm-systemd", &replacements, service_order, &mock_listener).unwrap();
568
569            assert_eq!(1, scripts.len());
570
571            let expected_path = &format!("mypkg.prerm.{expected_ext}");
572            let actual_path = scripts.keys().next().unwrap();
573            assert_eq!(expected_path, actual_path);
574        }
575    }
576
577    #[fixture]
578    #[allow(unused_braces)]
579    fn empty_user_file() -> String { String::new() }
580
581    #[fixture]
582    #[allow(unused_braces)]
583    fn invalid_user_file() -> String { "some content".to_owned() }
584
585    #[fixture]
586    #[allow(unused_braces)]
587    fn valid_user_file() -> String { "some #DEBHELPER# content".to_owned() }
588
589    #[test]
590    fn debhelper_script_subst_with_no_matching_files() {
591        let mut mock_listener = crate::listener::MockListener::new();
592        mock_listener.expect_info().times(0).return_const(());
593
594        let mut scripts = ScriptFragments::new();
595
596        assert_eq!(0, scripts.len());
597        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
598        assert_eq!(0, scripts.len());
599    }
600
601    #[rstest]
602    #[should_panic(expected = "Test failed as expected")]
603    fn debhelper_script_subst_errs_if_user_file_lacks_token(invalid_user_file: String) {
604        let _g = add_test_fs_paths(&[]);
605        set_test_fs_path_content("myscript", invalid_user_file);
606
607        let mut mock_listener = crate::listener::MockListener::new();
608        mock_listener.expect_progress().times(1).return_const(());
609
610        let mut scripts = ScriptFragments::new();
611
612        match debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener) {
613            Ok(()) => (),
614            Err(CargoDebError::DebHelperReplaceFailed(_)) => panic!("Test failed as expected"),
615            Err(err) => panic!("Unexpected error {err:?}"),
616        }
617    }
618
619    #[rstest]
620    #[test]
621    fn debhelper_script_subst_with_user_file_only(valid_user_file: String) {
622        let _g = add_test_fs_paths(&[]);
623        set_test_fs_path_content("myscript", valid_user_file);
624
625        let mut mock_listener = crate::listener::MockListener::new();
626        mock_listener.expect_progress().times(1).return_const(());
627
628        let mut scripts = ScriptFragments::new();
629
630        assert_eq!(0, scripts.len());
631        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
632        assert_eq!(1, scripts.len());
633        assert!(scripts.contains_key("myscript"));
634    }
635
636    fn script_to_string<'a>(scripts: &'a ScriptFragments, script: &str) -> &'a str {
637        scripts.get(script).unwrap()
638    }
639
640    #[test]
641    fn debhelper_script_subst_with_generated_file_only() {
642        let _g = add_test_fs_paths(&[]);
643        let mut mock_listener = crate::listener::MockListener::new();
644        mock_listener.expect_progress().times(1).return_const(());
645
646        let mut scripts = ScriptFragments::new();
647        scripts.insert("mypkg.myscript.debhelper".to_owned(), "injected".into());
648
649        assert_eq!(1, scripts.len());
650        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
651        assert_eq!(2, scripts.len());
652        assert!(scripts.contains_key("mypkg.myscript.debhelper"));
653        assert!(scripts.contains_key("myscript"));
654
655        assert_eq!(script_to_string(&scripts, "mypkg.myscript.debhelper"), "injected");
656        assert_eq!(script_to_string(&scripts, "myscript"), "#!/bin/sh\nset -e\ninjected");
657    }
658
659    #[rstest]
660    #[test]
661    fn debhelper_script_subst_with_user_and_generated_file(valid_user_file: String) {
662        let _g = add_test_fs_paths(&[]);
663        set_test_fs_path_content("myscript", valid_user_file);
664
665        let mut mock_listener = crate::listener::MockListener::new();
666        mock_listener.expect_progress().times(1).return_const(());
667
668        let mut scripts = ScriptFragments::new();
669        scripts.insert("mypkg.myscript.debhelper".to_owned(), "injected".into());
670
671        assert_eq!(1, scripts.len());
672        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener).unwrap();
673        assert_eq!(2, scripts.len());
674        assert!(scripts.contains_key("mypkg.myscript.debhelper"));
675        assert!(scripts.contains_key("myscript"));
676
677        assert_eq!(script_to_string(&scripts, "mypkg.myscript.debhelper"), "injected");
678        assert_eq!(script_to_string(&scripts, "myscript"), "some injected content");
679    }
680
681    #[rstest(maintainer_script, service_order,
682        case("preinst", false),
683        case("prerm", true),
684        case("postinst", false),
685        case("postrm", true),
686    )]
687    #[test]
688    fn debhelper_script_subst_with_user_and_generated_files(
689        valid_user_file: String,
690        maintainer_script: &'static str,
691        service_order: bool,
692    ) {
693        let _g = add_test_fs_paths(&[]);
694        set_test_fs_path_content(maintainer_script, valid_user_file);
695
696        let mut mock_listener = crate::listener::MockListener::new();
697        mock_listener.expect_progress().times(1).return_const(());
698
699        let mut scripts = ScriptFragments::new();
700        scripts.insert(format!("mypkg.{maintainer_script}.debhelper"), "first".into());
701        scripts.insert(format!("mypkg.{maintainer_script}.service"), "second".into());
702
703        assert_eq!(2, scripts.len());
704        debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", maintainer_script, None, &mock_listener).unwrap();
705        assert_eq!(3, scripts.len());
706        assert!(scripts.contains_key(&format!("mypkg.{maintainer_script}.debhelper")));
707        assert!(scripts.contains_key(&format!("mypkg.{maintainer_script}.service")));
708        assert!(scripts.contains_key(maintainer_script));
709
710        assert_eq!(script_to_string(&scripts, &format!("mypkg.{maintainer_script}.debhelper")), "first");
711        assert_eq!(script_to_string(&scripts, &format!("mypkg.{maintainer_script}.service")), "second");
712        if service_order {
713            assert_eq!(script_to_string(&scripts, maintainer_script), "some secondfirst content");
714        } else {
715            assert_eq!(script_to_string(&scripts, maintainer_script), "some firstsecond content");
716        }
717    }
718
719    #[rstest(
720        error,
721        case::invalid_input("InvalidInput"),
722        case::interrupted("Interrupted"),
723        case::permission_denied("PermissionDenied"),
724        case::not_found("NotFound"),
725        case::other("Other")
726    )]
727    #[test]
728    fn debhelper_script_subst_with_user_file_access_error(error: &str) {
729        let _g = add_test_fs_paths(&[]);
730        set_test_fs_path_content("myscript", format!("error:{error}"));
731
732        let mut mock_listener = crate::listener::MockListener::new();
733        mock_listener.expect_progress().times(1).return_const(());
734
735        let mut scripts = ScriptFragments::new();
736
737        assert_eq!(0, scripts.len());
738        let result = debhelper_script_subst(Path::new(""), &mut scripts, "mypkg", "myscript", None, &mock_listener);
739
740        assert!(matches!(result, Err(CargoDebError::IoFile(..))));
741        if let CargoDebError::IoFile(_, err, _) = result.unwrap_err() {
742            assert_eq!(error, format!("{:?}", err.kind()));
743        } else {
744            unreachable!()
745        }
746    }
747
748    #[test]
749    fn apply_with_no_matching_files() {
750        let mut mock_listener = crate::listener::MockListener::new();
751        mock_listener.expect_info().times(0).return_const(());
752        apply(Path::new(""), &mut ScriptFragments::new(), "mypkg", None, &mock_listener).unwrap();
753    }
754
755    #[rstest]
756    #[test]
757    fn apply_with_valid_user_files(valid_user_file: String) {
758        let _g = add_test_fs_paths(&[]);
759        let scripts = &["postinst", "preinst", "prerm", "postrm"];
760
761        for script in scripts {
762            set_test_fs_path_content(script, valid_user_file.clone());
763        }
764
765        let mut mock_listener = crate::listener::MockListener::new();
766        mock_listener.expect_progress().times(scripts.len()).return_const(());
767
768        apply(Path::new(""), &mut ScriptFragments::new(), "mypkg", None, &mock_listener).unwrap();
769    }
770}