1use std::{
6 fs::{File, create_dir_all},
7 io::Write,
8 path::{Path, PathBuf},
9 str::FromStr,
10};
11
12use alpm_common::InputPaths;
13use alpm_mtree::Mtree;
14use alpm_types::{MetadataFileName, PackageError, PackageFileName};
15use log::debug;
16use tar::Builder;
17
18use crate::{CompressionEncoder, OutputDir, PackageCreationConfig};
19
20#[derive(Debug, thiserror::Error)]
24pub enum Error {
25 #[error("Error while appending file {from_path} to package archive as {to_path}:\n{source}")]
27 AppendFileToArchive {
28 from_path: PathBuf,
30 to_path: PathBuf,
32 source: std::io::Error,
34 },
35
36 #[error("Error while finishing the creation of uncompressed package {package_path}:\n{source}")]
38 FinishArchive {
39 package_path: PathBuf,
41 source: std::io::Error,
43 },
44}
45
46#[derive(Clone, Debug)]
48pub struct ExistingAbsoluteDir(PathBuf);
49
50impl ExistingAbsoluteDir {
51 pub fn new(path: PathBuf) -> Result<Self, crate::Error> {
64 if !path.is_absolute() {
65 return Err(alpm_common::Error::NonAbsolutePaths {
66 paths: vec![path.clone()],
67 }
68 .into());
69 }
70
71 if !path.exists() {
72 create_dir_all(&path).map_err(|source| crate::Error::IoPath {
73 path: path.clone(),
74 context: "creating absolute directory",
75 source,
76 })?;
77 }
78
79 let metadata = path.metadata().map_err(|source| crate::Error::IoPath {
80 path: path.clone(),
81 context: "retrieving metadata",
82 source,
83 })?;
84
85 if !metadata.is_dir() {
86 return Err(alpm_common::Error::NotADirectory { path: path.clone() }.into());
87 }
88
89 Ok(Self(path))
90 }
91
92 pub fn as_path(&self) -> &Path {
96 self.0.as_path()
97 }
98
99 pub fn to_path_buf(&self) -> PathBuf {
103 self.0.to_path_buf()
104 }
105
106 pub fn join(&self, path: impl AsRef<Path>) -> PathBuf {
110 self.0.join(path)
111 }
112}
113
114impl AsRef<Path> for ExistingAbsoluteDir {
115 fn as_ref(&self) -> &Path {
116 &self.0
117 }
118}
119
120impl From<&OutputDir> for ExistingAbsoluteDir {
121 fn from(value: &OutputDir) -> Self {
125 Self(value.to_path_buf())
126 }
127}
128
129impl TryFrom<&Path> for ExistingAbsoluteDir {
130 type Error = crate::Error;
131
132 fn try_from(value: &Path) -> Result<Self, Self::Error> {
140 Self::new(value.to_path_buf())
141 }
142}
143
144fn append_relative_files<W>(
157 mut builder: Builder<W>,
158 mtree: &Mtree,
159 input_paths: &InputPaths,
160) -> Result<Builder<W>, crate::Error>
161where
162 W: Write,
163{
164 let mtree_path = PathBuf::from(MetadataFileName::Mtree.as_ref());
166 let check_paths = {
167 let all_paths = input_paths.paths();
168 if let Some(mtree_position) = all_paths.iter().position(|path| path == &mtree_path) {
171 let before = &all_paths[..mtree_position];
172 let after = if all_paths.len() > mtree_position {
173 &all_paths[mtree_position + 1..]
174 } else {
175 &[]
176 };
177 &[before, after].concat()
178 } else {
179 all_paths
180 }
181 };
182 mtree.validate_paths(&InputPaths::new(input_paths.base_dir(), check_paths)?)?;
183
184 for relative_file in input_paths.paths() {
186 let from_path = input_paths.base_dir().join(relative_file.as_path());
187 builder
188 .append_path_with_name(from_path.as_path(), relative_file.as_path())
189 .map_err(|source| Error::AppendFileToArchive {
190 from_path,
191 to_path: relative_file.clone(),
192 source,
193 })?
194 }
195
196 Ok(builder)
197}
198
199#[derive(Clone, Debug)]
205pub struct Package {
206 file_name: PackageFileName,
207 parent_dir: ExistingAbsoluteDir,
208}
209
210impl Package {
211 pub fn new(
217 file_name: PackageFileName,
218 parent_dir: ExistingAbsoluteDir,
219 ) -> Result<Self, crate::Error> {
220 let file_path = parent_dir.to_path_buf().join(file_name.to_path_buf());
221 if !file_path.exists() {
222 return Err(crate::Error::PathDoesNotExist { path: file_path });
223 }
224 if !file_path.is_file() {
225 return Err(crate::Error::PathIsNotAFile { path: file_path });
226 }
227
228 Ok(Self {
229 file_name,
230 parent_dir,
231 })
232 }
233
234 pub fn to_path_buf(&self) -> PathBuf {
236 self.parent_dir.join(self.file_name.to_path_buf())
237 }
238}
239
240impl TryFrom<&Path> for Package {
241 type Error = crate::Error;
242
243 fn try_from(value: &Path) -> Result<Self, Self::Error> {
253 debug!("Attempt to create a package representation from path {value:?}");
254 let Some(parent_dir) = value.parent() else {
255 return Err(crate::Error::PathHasNoParent {
256 path: value.to_path_buf(),
257 });
258 };
259 let Some(filename) = value.file_name().and_then(|name| name.to_str()) else {
260 return Err(PackageError::InvalidPackageFileNamePath {
261 path: value.to_path_buf(),
262 }
263 .into());
264 };
265
266 Self::new(PackageFileName::from_str(filename)?, parent_dir.try_into()?)
267 }
268}
269
270impl TryFrom<&PackageCreationConfig> for Package {
271 type Error = crate::Error;
272
273 fn try_from(value: &PackageCreationConfig) -> Result<Self, Self::Error> {
290 let filename = PackageFileName::try_from(value)?;
291 let parent_dir: ExistingAbsoluteDir = value.output_dir().into();
292 let output_path = value.output_dir().join(filename.to_path_buf());
293
294 let file = File::create(output_path.as_path()).map_err(|source| crate::Error::IoPath {
296 path: output_path.clone(),
297 context: "creating a package file",
298 source,
299 })?;
300
301 if let Some(compression) = value.compression() {
306 let encoder = CompressionEncoder::new(file, compression)?;
307 let mut builder = Builder::new(encoder);
308 builder.follow_symlinks(false);
310 let builder = append_relative_files(
311 builder,
312 value.package_input().mtree()?,
313 &value.package_input().input_paths()?,
314 )?;
315 let encoder = builder
316 .into_inner()
317 .map_err(|source| Error::FinishArchive {
318 package_path: output_path.clone(),
319 source,
320 })?;
321 encoder.finish()?;
322 } else {
325 let mut builder = Builder::new(file);
326 builder.follow_symlinks(false);
328 let mut builder = append_relative_files(
329 builder,
330 value.package_input().mtree()?,
331 &value.package_input().input_paths()?,
332 )?;
333 builder.finish().map_err(|source| Error::FinishArchive {
334 package_path: output_path.clone(),
335 source,
336 })?;
337 }
338
339 Self::new(filename, parent_dir)
340 }
341}
342
343#[cfg(test)]
344mod tests {
345
346 use std::fs::create_dir;
347
348 use log::{LevelFilter, debug};
349 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
350 use tempfile::{NamedTempFile, TempDir};
351 use testresult::TestResult;
352
353 use super::*;
354
355 fn init_logger() {
357 if TermLogger::init(
358 LevelFilter::Debug,
359 Config::default(),
360 TerminalMode::Mixed,
361 ColorChoice::Auto,
362 )
363 .is_err()
364 {
365 debug!("Not initializing another logger, as one is initialized already.");
366 }
367 }
368
369 #[test]
371 fn absolute_dir_new_creates_dir() -> TestResult {
372 init_logger();
373
374 let temp_dir = TempDir::new()?;
375 let path = temp_dir.path().join("additional");
376
377 if let Err(error) = ExistingAbsoluteDir::new(path) {
378 return Err(format!("Failed although it should have succeeded: {error}").into());
379 }
380
381 Ok(())
382 }
383
384 #[test]
387 fn absolute_dir_new_fails() -> TestResult {
388 init_logger();
389
390 if let Err(error) = ExistingAbsoluteDir::new(PathBuf::from("test")) {
391 assert!(matches!(
392 error,
393 crate::Error::AlpmCommon(alpm_common::Error::NonAbsolutePaths { paths: _ })
394 ));
395 } else {
396 return Err("Succeeded although it should have failed".into());
397 }
398
399 let temp_file = NamedTempFile::new()?;
400 let path = temp_file.path();
401 if let Err(error) = ExistingAbsoluteDir::new(path.to_path_buf()) {
402 assert!(matches!(
403 error,
404 crate::Error::AlpmCommon(alpm_common::Error::NotADirectory { path: _ })
405 ));
406 } else {
407 return Err("Succeeded although it should have failed".into());
408 }
409
410 Ok(())
411 }
412
413 #[test]
415 fn absolute_dir_utilities() -> TestResult {
416 let temp_dir = TempDir::new()?;
417 let path = temp_dir.path();
418
419 let absolute_dir: ExistingAbsoluteDir = path.try_into()?;
421
422 assert_eq!(absolute_dir.as_path(), path);
423 assert_eq!(absolute_dir.as_ref(), path);
424
425 Ok(())
426 }
427
428 #[test]
430 fn package_new() -> TestResult {
431 let temp_dir = TempDir::new()?;
432 let path = temp_dir.path();
433 let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
434 let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
435 File::create(absolute_dir.join(package_name))?;
436
437 let Ok(_package) = Package::new(package_name.parse()?, absolute_dir.clone()) else {
438 return Err("Failed although it should have succeeded".into());
439 };
440
441 Ok(())
442 }
443
444 #[test]
446 fn package_new_fails() -> TestResult {
447 let temp_dir = TempDir::new()?;
448 let path = temp_dir.path();
449 let absolute_dir = ExistingAbsoluteDir::new(path.to_path_buf())?;
450 let package_name = "example-1.0.0-1-x86_64.pkg.tar.zst";
451
452 if let Err(error) = Package::new(package_name.parse()?, absolute_dir.clone()) {
454 assert!(matches!(error, crate::Error::PathDoesNotExist { path: _ }))
455 } else {
456 return Err("Succeeded although it should have failed".into());
457 }
458
459 create_dir(absolute_dir.join(package_name))?;
461 if let Err(error) = Package::new(package_name.parse()?, absolute_dir.clone()) {
462 assert!(matches!(error, crate::Error::PathIsNotAFile { path: _ }))
463 } else {
464 return Err("Succeeded although it should have failed".into());
465 }
466
467 Ok(())
468 }
469
470 #[test]
473 fn package_try_from_path_fails() -> TestResult {
474 init_logger();
475
476 assert!(Package::try_from(PathBuf::from("/").as_path()).is_err());
478
479 assert!(
481 Package::try_from(
482 PathBuf::from("/something_very_unlikely_to_ever_exist_in_a_filesystem").as_path()
483 )
484 .is_err()
485 );
486
487 Ok(())
488 }
489}