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