alpm_mtree/file/
common.rs

1//! Common functionality for creating [ALPM-MTREE] files.
2//!
3//! [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
4
5use std::{
6    fs::File,
7    io::Write,
8    path::{Path, PathBuf},
9    process::{Command, Stdio},
10};
11
12use alpm_common::{MetadataFile, relative_files};
13use alpm_types::{MetadataFileName, SchemaVersion, semver_version::Version};
14use flate2::{Compression, GzBuilder};
15use log::debug;
16use which::which;
17
18use crate::{CreationError, Error, Mtree, MtreeSchema};
19
20/// The [bsdtar] options for different versions of [ALPM-MTREE].
21///
22/// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
23/// [bsdtar]: https://man.archlinux.org/man/bsdtar.1
24#[derive(Clone, Copy, Debug, strum::Display, strum::IntoStaticStr)]
25pub enum BsdtarOptions {
26    /// The [bsdtar] options for [ALPM-MTREEv1].
27    ///
28    /// [ALPM-MTREEv1]: https://alpm.archlinux.page/specifications/ALPM-MTREEv1.5.html
29    /// [bsdtar]: https://man.archlinux.org/man/bsdtar.1
30    #[strum(to_string = "!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link")]
31    MtreeV1,
32
33    /// The [bsdtar] options for [ALPM-MTREEv2].
34    ///
35    /// [ALPM-MTREEv2]: https://alpm.archlinux.page/specifications/ALPM-MTREEv2.5.html
36    /// [bsdtar]: https://man.archlinux.org/man/bsdtar.1
37    #[strum(to_string = "!all,use-set,type,uid,gid,mode,time,size,sha256,link")]
38    MtreeV2,
39}
40
41impl From<BsdtarOptions> for MtreeSchema {
42    /// Creates an [`MtreeSchema`] from a [`BsdtarOptions`]
43    fn from(value: BsdtarOptions) -> Self {
44        match value {
45            BsdtarOptions::MtreeV1 => MtreeSchema::V1(SchemaVersion::new(Version::new(1, 0, 0))),
46            BsdtarOptions::MtreeV2 => MtreeSchema::V2(SchemaVersion::new(Version::new(2, 0, 0))),
47        }
48    }
49}
50
51/// Runs [bsdtar] in `path` with dedicated `options` and return its stdout.
52///
53/// Creates [ALPM-MTREE] data based on the string slice `stdin` which contains all sorted paths
54/// below `path` and is passed to [bsdtar] on stdin.
55///
56/// # Errors
57///
58/// Returns an error if
59///
60/// - the [bsdtar] command can not be found,
61/// - the [bsdtar] command can not be spawned in the background,
62/// - the [bsdtar] command's stdin can not be attached to,
63/// - the [bsdtar] command's stdin can not be written to,
64/// - calling the [bsdtar] command is not possible,
65/// - or [bsdtar] returned a non-zero status code.
66///
67/// [bsdtar]: https://man.archlinux.org/man/bsdtar.1
68fn run_bsdtar(
69    path: impl AsRef<Path>,
70    options: BsdtarOptions,
71    stdin: &str,
72) -> Result<Vec<u8>, Error> {
73    let command = "bsdtar";
74    let bsdtar_command =
75        which(command).map_err(|source| CreationError::CommandNotFound { command, source })?;
76
77    let mut command = Command::new(bsdtar_command);
78    command
79        .current_dir(path)
80        .env("LANG", "C")
81        .args([
82            "--create",
83            "--exclude",
84            MetadataFileName::Mtree.as_ref(),
85            "--files-from",
86            "-",
87            "--file",
88            "-",
89            "--format=mtree",
90            "--no-recursion",
91            "--options",
92            options.into(),
93        ])
94        .stdin(Stdio::piped())
95        .stderr(Stdio::piped())
96        .stdout(Stdio::piped());
97    let mut command_child = command
98        .spawn()
99        .map_err(|source| CreationError::CommandBackground {
100            command: format!("{command:?}"),
101            source,
102        })?;
103
104    // Write to stdin.
105    command_child
106        .stdin
107        .take()
108        .ok_or(CreationError::CommandAttachToStdin {
109            command: format!("{command:?}"),
110        })?
111        .write_all(stdin.as_bytes())
112        .map_err(|source| CreationError::CommandWriteToStdin {
113            command: "bsdtar".to_string(),
114            source,
115        })?;
116
117    let command_output =
118        command_child
119            .wait_with_output()
120            .map_err(|source| CreationError::CommandExec {
121                command: format!("{command:?}"),
122                source,
123            })?;
124    if !command_output.status.success() {
125        return Err(CreationError::CommandNonZero {
126            command: format!("{command:?}"),
127            exit_status: command_output.status,
128            stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
129        }
130        .into());
131    }
132
133    debug!(
134        "bsdtar output:\n{}",
135        String::from_utf8_lossy(&command_output.stdout)
136    );
137
138    Ok(command_output.stdout)
139}
140
141/// Creates an [ALPM-MTREE] file in a directory.
142///
143/// Validates the `mtree_data` based on `schema` and then creates the [ALPM-MTREE] file in `path`
144/// using `mtree_data`.
145///
146/// # Errors
147///
148/// Returns an error if
149///
150/// - the `mtree_data` is not valid according to `schema`,
151/// - creating the [ALPM-MTREE] file in `path` fails,
152/// - or gzip compressing the [ALPM-MTREE] file fails.
153///
154/// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
155fn create_mtree_file_in_dir(
156    path: impl AsRef<Path>,
157    mtree_data: &[u8],
158    schema: MtreeSchema,
159) -> Result<PathBuf, Error> {
160    let path = path.as_ref();
161    let mtree_file = path.join(MetadataFileName::Mtree.as_ref());
162    debug!("Write ALPM-MTREE data to file: {mtree_file:?}");
163
164    // Ensure that the data is correct.
165    let _ = Mtree::from_reader_with_schema(mtree_data, Some(schema))?;
166
167    // Create the target file
168    let mtree = File::create(mtree_file.as_path())
169        .map_err(|source| Error::IoPath(mtree_file.clone(), "creating the file", source))?;
170
171    let mut gz = GzBuilder::new()
172        // Add "Unix" as operating system to the file header.
173        .operating_system(3)
174        .write(mtree, Compression::best());
175    gz.write_all(mtree_data).map_err(|source| {
176        Error::IoPath(
177            mtree_file.clone(),
178            "writing data to gzip compressed file",
179            source,
180        )
181    })?;
182    gz.finish().map_err(|source| {
183        Error::IoPath(mtree_file.clone(), "finishing gzip compressed file", source)
184    })?;
185
186    Ok(mtree_file)
187}
188
189/// Creates an [ALPM-MTREE] file from a package input directory.
190///
191/// Collects all files in `path` relative to it in a newline-delimited string.
192/// Calls the [bsdtar] command, using options specific to a version of [ALPM-MTREE] to create
193/// an [ALPM-MTREE] file in `path`.
194/// Returns the path to the [ALPM-MTREE] file.
195///
196/// # Errors
197///
198/// Returns an error if
199///
200/// - calling [`relative_files`] on `path` fails,
201/// - the [bsdtar] command can not be spawned in the background,
202/// - the [bsdtar] command's stdin can not be attached to,
203/// - the [bsdtar] command's stdin can not be written to,
204/// - calling the [bsdtar] command is not possible,
205/// - [bsdtar] returned a non-zero status code,
206/// - creating the [ALPM-MTREE] file fails,
207/// - or gzip compressing the [ALPM-MTREE] file fails.
208///
209/// [ALPM-MTREE]: https://alpm.archlinux.page/specifications/ALPM-MTREE.5.html
210/// [bsdtar]: https://man.archlinux.org/man/bsdtar.1
211pub fn create_mtree_file_from_input_dir(
212    path: impl AsRef<Path>,
213    bsdtar_options: BsdtarOptions,
214) -> Result<PathBuf, Error> {
215    let path = path.as_ref();
216    debug!("Create ALPM-MTREE file from input dir {path:?} with bsdtar options {bsdtar_options}");
217
218    // Collect all files and directories in newline-delimited String.
219    let collected_files: Vec<PathBuf> =
220        relative_files(path, &[]).map_err(CreationError::AlpmCommon)?;
221    let all_files = collected_files.iter().fold(String::new(), |mut acc, file| {
222        acc.push_str(&format!("{}\n", file.to_string_lossy()));
223        acc
224    });
225    debug!("Collected files:\n{all_files}");
226
227    // Run bsdtar and collect the output.
228    let bsdtar_output = run_bsdtar(path, bsdtar_options, &all_files)?;
229
230    // Get the schema for the bsdtar options.
231    let schema: MtreeSchema = bsdtar_options.into();
232
233    create_mtree_file_in_dir(path, &bsdtar_output, schema)
234}