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 log::debug;
16use which::which;
17
18use crate::{CreationError, Error, Mtree, MtreeSchema};
19
20#[derive(Clone, Copy, Debug, strum::Display, strum::IntoStaticStr)]
25pub enum BsdtarOptions {
26 #[strum(to_string = "!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link")]
31 MtreeV1,
32
33 #[strum(to_string = "!all,use-set,type,uid,gid,mode,time,size,sha256,link")]
38 MtreeV2,
39}
40
41impl From<BsdtarOptions> for MtreeSchema {
42 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
51fn 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 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
141fn 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 let _ = Mtree::from_reader_with_schema(mtree_data, Some(schema))?;
166
167 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 .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
189pub 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 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 let bsdtar_output = run_bsdtar(path, bsdtar_options, &all_files)?;
229
230 let schema: MtreeSchema = bsdtar_options.into();
232
233 create_mtree_file_in_dir(path, &bsdtar_output, schema)
234}