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