alpm_package/
scriptlet.rs

1//! Checks for [alpm-install-scriptlet] files.
2//!
3//! [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
4
5use std::{fs::File, io::Read, path::Path};
6
7use crate::Error;
8
9/// Function signatures of which at least one must be present in an [alpm-install-scriptlet]
10///
11/// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
12const REQUIRED_FUNCTION_SIGNATURES: &[&str] = &[
13    "pre_install",
14    "post_install",
15    "pre_upgrade",
16    "post_upgrade",
17    "pre_remove",
18    "post_remove",
19];
20
21/// Validates an [alpm-install-scriptlet] at `path`.
22///
23/// Naively checks whether at least one of the required function signatures is present in the file.
24///
25/// # Note
26///
27/// The file at `path` is _neither sourced nor fully evaluated_.
28/// This function only provides a _very limited_ validity check!
29///
30/// # Errors
31///
32/// Returns an error, if
33/// - `path` can not be opened for reading,
34/// - `path` can not be read to String,
35/// - none of the required function signatures is present in the file.
36///
37/// [alpm-install-scriptlet]: https://alpm.archlinux.page/specifications/alpm-install-scriptlet.5.html
38pub fn check_scriptlet(path: impl AsRef<Path>) -> Result<(), Error> {
39    let path = path.as_ref();
40    let mut file = File::open(path).map_err(|source| Error::IoPath {
41        path: path.to_path_buf(),
42        context: "opening an alpm-install-scriptlet file for reading",
43        source,
44    })?;
45    let mut buf = String::new();
46    file.read_to_string(&mut buf)
47        .map_err(|source| Error::IoPath {
48            path: path.to_path_buf(),
49            context: "reading the contents to string",
50            source,
51        })?;
52
53    for line in buf.lines() {
54        for function_name in REQUIRED_FUNCTION_SIGNATURES {
55            if line.starts_with(&format!("{function_name}()"))
56                || line.starts_with(&format!("{function_name}() {{"))
57                || line.starts_with(&format!("function {function_name}()"))
58                || line.starts_with(&format!("function {function_name}() {{"))
59            {
60                return Ok(());
61            }
62        }
63    }
64
65    Err(Error::InstallScriptlet {
66        path: path.to_path_buf(),
67        context: format!(
68            "it must implement at least one of the functions: {}",
69            REQUIRED_FUNCTION_SIGNATURES.join(", ")
70        ),
71    })
72}
73
74#[cfg(test)]
75mod test {
76    use std::io::Write;
77
78    use rstest::rstest;
79    use tempfile::NamedTempFile;
80    use testresult::TestResult;
81
82    use super::*;
83
84    const INSTALL_SCRIPTLET_FULL: &str = r#"pre_install() {
85  true
86}
87
88post_install() {
89  true
90}
91
92pre_upgrade() {
93  true
94}
95
96post_upgrade() {
97  true
98}
99
100pre_remove() {
101  true
102}
103
104post_remove() {
105  true
106}"#;
107    const INSTALL_SCRIPTLET_FULL_FUNCTION_PREFIX: &str = r#"function pre_install() {
108  true
109}
110
111function post_install() {
112  true
113}
114
115function pre_upgrade() {
116  true
117}
118
119function post_upgrade() {
120  true
121}
122
123function pre_remove() {
124  true
125}
126
127function post_remove() {
128  true
129}"#;
130    const INSTALL_SCRIPTLET_EMPTY: &str = "";
131    const INSTALL_SCRIPTLET_INVALID: &str = r#"pre_install
132post_install
133pre_upgrade
134post_upgrade
135pre_remove
136post_remove"#;
137    const INSTALL_SCRIPTLET_INVALID_FUNCTION_PREFIX: &str = r#"function pre_install
138function post_install
139function pre_upgrade
140function post_upgrade
141function pre_remove
142function post_remove"#;
143
144    #[rstest]
145    #[case(INSTALL_SCRIPTLET_FULL)]
146    #[case(INSTALL_SCRIPTLET_FULL_FUNCTION_PREFIX)]
147    fn valid_scriptlet(#[case] scriptlet: &str) -> TestResult {
148        let mut file = NamedTempFile::new()?;
149        write!(file, "{scriptlet}")?;
150
151        check_scriptlet(file.path())?;
152
153        Ok(())
154    }
155
156    #[rstest]
157    #[case(INSTALL_SCRIPTLET_EMPTY)]
158    #[case(INSTALL_SCRIPTLET_INVALID)]
159    #[case(INSTALL_SCRIPTLET_INVALID_FUNCTION_PREFIX)]
160    fn invalid_scriptlet(#[case] scriptlet: &str) -> TestResult {
161        let mut file = NamedTempFile::new()?;
162        write!(file, "{scriptlet}")?;
163
164        assert!(check_scriptlet(file.path()).is_err());
165
166        Ok(())
167    }
168}