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