1use 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#[derive(Clone, Copy, Debug, strum::Display, strum::IntoStaticStr)]
26pub enum BsdtarOptions {
27 #[strum(to_string = "!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link")]
32 MtreeV1,
33
34 #[strum(to_string = "!all,use-set,type,uid,gid,mode,time,size,sha256,link")]
39 MtreeV2,
40}
41
42impl From<BsdtarOptions> for MtreeSchema {
43 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
52fn 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 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
142fn 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 let _ = Mtree::from_reader_with_schema(mtree_data, Some(schema))?;
167
168 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 .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
193pub 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 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 let bsdtar_output = run_bsdtar(path, bsdtar_options, &all_files)?;
233
234 let schema: MtreeSchema = bsdtar_options.into();
236
237 create_mtree_file_in_dir(path, &bsdtar_output, schema)
238}