alpm_package/
config.rs

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