alpm_package/
config.rs

1//! Package creation configuration.
2
3use std::{
4    fs::create_dir_all,
5    path::{Path, PathBuf},
6};
7
8use alpm_compress::compression::CompressionSettings;
9#[cfg(doc)]
10use alpm_pkginfo::PackageInfo;
11use alpm_types::PackageFileName;
12
13use crate::input::PackageInput;
14#[cfg(doc)]
15use crate::package::Package;
16
17/// An output directory that is guaranteed to be an absolute, writable directory.
18#[derive(Clone, Debug)]
19pub struct OutputDir(PathBuf);
20
21impl OutputDir {
22    /// Creates a new [`OutputDir`] from `path`.
23    ///
24    /// Creates a directory at `path` if it does not exist yet.
25    /// Also creates any missing parent directories.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if
30    ///
31    /// - `path` is not absolute,
32    /// - `path` does not exist and cannot be created,
33    /// - the metadata of `path` cannot be retrieved,
34    /// - `path` is not a directory,
35    /// - or `path` is only read-only.
36    pub fn new(path: PathBuf) -> Result<Self, crate::Error> {
37        if !path.is_absolute() {
38            return Err(alpm_common::Error::NonAbsolutePaths {
39                paths: vec![path.clone()],
40            }
41            .into());
42        }
43
44        if !path.exists() {
45            create_dir_all(&path).map_err(|source| crate::Error::IoPath {
46                path: path.clone(),
47                context: "creating output directory",
48                source,
49            })?;
50        }
51
52        let metadata = path.metadata().map_err(|source| crate::Error::IoPath {
53            path: path.clone(),
54            context: "retrieving metadata",
55            source,
56        })?;
57
58        if !metadata.is_dir() {
59            return Err(alpm_common::Error::NotADirectory { path: path.clone() }.into());
60        }
61
62        if metadata.permissions().readonly() {
63            return Err(crate::Error::PathIsReadOnly { path: path.clone() });
64        }
65
66        Ok(Self(path))
67    }
68
69    /// Coerces to a Path slice.
70    pub fn as_path(&self) -> &Path {
71        self.0.as_path()
72    }
73
74    /// Converts a Path to an owned PathBuf.
75    pub fn to_path_buf(&self) -> PathBuf {
76        self.0.to_path_buf()
77    }
78
79    /// Creates an owned PathBuf with path adjoined to self.
80    pub fn join(&self, path: impl AsRef<Path>) -> PathBuf {
81        self.0.join(path)
82    }
83}
84
85impl AsRef<Path> for OutputDir {
86    fn as_ref(&self) -> &Path {
87        &self.0
88    }
89}
90
91/// A config that tracks the components needed for the creation of an [alpm-package] from input
92/// directory.
93///
94/// Tracks a [`PackageInput`], optional [`CompressionSettings`] and an [`OutputDir`] in which an
95/// [alpm-package] is placed after creation.
96///
97/// [alpm-package]: https://alpm.archlinux.page/specifications/alpm-package.7.html
98#[derive(Clone, Debug)]
99pub struct PackageCreationConfig {
100    package_input: PackageInput,
101    output_dir: OutputDir,
102    compression: CompressionSettings,
103}
104
105impl PackageCreationConfig {
106    /// Creates a new [`PackageCreationConfig`].
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if
111    ///
112    /// - `package_input.input_dir` is equal to `output_dir`,
113    /// - `package_input.input_dir` is located inside of `output_dir`,
114    /// - or `output_dir` is located inside of `package_input.input_dir`.
115    pub fn new(
116        package_input: PackageInput,
117        output_dir: OutputDir,
118        compression: CompressionSettings,
119    ) -> Result<Self, crate::Error> {
120        if package_input.input_dir() == output_dir.as_path() {
121            return Err(crate::Error::InputDirIsOutputDir {
122                path: package_input.input_dir().to_path_buf(),
123            });
124        }
125        if output_dir.as_path().starts_with(package_input.input_dir()) {
126            return Err(crate::Error::OutputDirInInputDir {
127                input_path: package_input.input_dir().to_path_buf(),
128                output_path: output_dir.to_path_buf(),
129            });
130        }
131        if package_input.input_dir().starts_with(output_dir.as_path()) {
132            return Err(crate::Error::InputDirInOutputDir {
133                input_path: package_input.input_dir().to_path_buf(),
134                output_path: output_dir.to_path_buf(),
135            });
136        }
137
138        Ok(Self {
139            compression,
140            package_input,
141            output_dir,
142        })
143    }
144
145    /// Returns a reference to the [`PackageInput`].
146    pub fn package_input(&self) -> &PackageInput {
147        &self.package_input
148    }
149
150    /// Returns a reference to the [`OutputDir`].
151    pub fn output_dir(&self) -> &OutputDir {
152        &self.output_dir
153    }
154
155    /// Returns a reference to the [`CompressionSettings`].
156    pub fn compression(&self) -> &CompressionSettings {
157        &self.compression
158    }
159}
160
161impl From<&PackageCreationConfig> for PackageFileName {
162    /// Creates a [`PackageFileName`] from a [`PackageCreationConfig`] reference.
163    fn from(value: &PackageCreationConfig) -> Self {
164        Self::new(
165            match value.package_input.package_info() {
166                alpm_pkginfo::PackageInfo::V1(package_info) => package_info.pkgname().clone(),
167                alpm_pkginfo::PackageInfo::V2(package_info) => package_info.pkgname().clone(),
168            },
169            match value.package_input.package_info() {
170                alpm_pkginfo::PackageInfo::V1(package_info) => package_info.pkgver().clone(),
171                alpm_pkginfo::PackageInfo::V2(package_info) => package_info.pkgver().clone(),
172            },
173            match value.package_input.package_info() {
174                alpm_pkginfo::PackageInfo::V1(package_info) => package_info.arch().clone(),
175                alpm_pkginfo::PackageInfo::V2(package_info) => package_info.arch().clone(),
176            },
177            (&value.compression).into(),
178        )
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use std::fs::File;
185
186    use tempfile::tempdir;
187    use testresult::TestResult;
188
189    use super::*;
190
191    /// Ensures that [`OutputDir::new`] creates non-existing, absolute directories.
192    #[test]
193    fn output_dir_new_creates_dir() -> TestResult {
194        let temp_dir = tempdir()?;
195        let non_existing_path = temp_dir.path().join("non-existing");
196        if let Err(error) = OutputDir::new(non_existing_path) {
197            return Err(format!("Failed although it should have succeeded:\n{error}").into());
198        }
199
200        Ok(())
201    }
202
203    /// Ensures that [`OutputDir::new`] fails on relative paths and non-directory paths.
204    #[test]
205    fn output_dir_new_fails() -> TestResult {
206        assert!(matches!(
207            OutputDir::new(PathBuf::from("test")),
208            Err(crate::Error::AlpmCommon(
209                alpm_common::Error::NonAbsolutePaths { paths: _ }
210            ))
211        ));
212
213        let temp_dir = tempdir()?;
214        let file_path = temp_dir.path().join("non-existing");
215        let _file = File::create(&file_path)?;
216        assert!(matches!(
217            OutputDir::new(file_path),
218            Err(crate::Error::AlpmCommon(
219                alpm_common::Error::NotADirectory { path: _ }
220            ))
221        ));
222
223        Ok(())
224    }
225
226    /// Ensures that [`OutputDir::as_ref`] works.
227    #[test]
228    fn output_dir_as_ref() -> TestResult {
229        let temp_dir = tempdir()?;
230        let path = temp_dir.path();
231
232        let output_dir = OutputDir::new(path.to_path_buf())?;
233
234        assert_eq!(output_dir.as_ref(), path);
235
236        Ok(())
237    }
238}