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 From<&PackageCreationConfig> for PackageFileName {
161    /// Creates a [`PackageFileName`] from a [`PackageCreationConfig`] reference.
162    fn from(value: &PackageCreationConfig) -> Self {
163        Self::new(
164            match value.package_input.package_info() {
165                alpm_pkginfo::PackageInfo::V1(package_info) => package_info.pkgname().clone(),
166                alpm_pkginfo::PackageInfo::V2(package_info) => package_info.pkgname().clone(),
167            },
168            match value.package_input.package_info() {
169                alpm_pkginfo::PackageInfo::V1(package_info) => package_info.pkgver().clone(),
170                alpm_pkginfo::PackageInfo::V2(package_info) => package_info.pkgver().clone(),
171            },
172            match value.package_input.package_info() {
173                alpm_pkginfo::PackageInfo::V1(package_info) => *package_info.arch(),
174                alpm_pkginfo::PackageInfo::V2(package_info) => *package_info.arch(),
175            },
176            value.compression.as_ref().map(|settings| settings.into()),
177        )
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use std::fs::File;
184
185    use tempfile::tempdir;
186    use testresult::TestResult;
187
188    use super::*;
189
190    /// Ensures that [`OutputDir::new`] creates non-existing, absolute directories.
191    #[test]
192    fn output_dir_new_creates_dir() -> TestResult {
193        let temp_dir = tempdir()?;
194        let non_existing_path = temp_dir.path().join("non-existing");
195        if let Err(error) = OutputDir::new(non_existing_path) {
196            return Err(format!("Failed although it should have succeeded:\n{error}").into());
197        }
198
199        Ok(())
200    }
201
202    /// Ensures that [`OutputDir::new`] fails on relative paths and non-directory paths.
203    #[test]
204    fn output_dir_new_fails() -> TestResult {
205        assert!(matches!(
206            OutputDir::new(PathBuf::from("test")),
207            Err(crate::Error::AlpmCommon(
208                alpm_common::Error::NonAbsolutePaths { paths: _ }
209            ))
210        ));
211
212        let temp_dir = tempdir()?;
213        let file_path = temp_dir.path().join("non-existing");
214        let _file = File::create(&file_path)?;
215        assert!(matches!(
216            OutputDir::new(file_path),
217            Err(crate::Error::AlpmCommon(
218                alpm_common::Error::NotADirectory { path: _ }
219            ))
220        ));
221
222        Ok(())
223    }
224
225    /// Ensures that [`OutputDir::as_ref`] works.
226    #[test]
227    fn output_dir_as_ref() -> TestResult {
228        let temp_dir = tempdir()?;
229        let path = temp_dir.path();
230
231        let output_dir = OutputDir::new(path.to_path_buf())?;
232
233        assert_eq!(output_dir.as_ref(), path);
234
235        Ok(())
236    }
237}