alpm_types/
path.rs

1use std::{
2    fmt::{Display, Formatter},
3    path::{Path, PathBuf},
4    str::FromStr,
5};
6
7use serde::{Deserialize, Serialize};
8use winnow::{
9    ModalResult,
10    Parser,
11    combinator::{alt, cut_err, eof, peek, repeat_till},
12    error::{StrContext, StrContextValue},
13    token::{any, rest},
14};
15
16use crate::{Error, SharedLibraryPrefix};
17
18/// A representation of an absolute path
19///
20/// AbsolutePath wraps a `PathBuf`, that is guaranteed to be absolute.
21///
22/// ## Examples
23/// ```
24/// use std::{path::PathBuf, str::FromStr};
25///
26/// use alpm_types::{AbsolutePath, Error};
27///
28/// # fn main() -> Result<(), alpm_types::Error> {
29/// // Create AbsolutePath from &str
30/// assert_eq!(
31///     AbsolutePath::from_str("/"),
32///     AbsolutePath::new(PathBuf::from("/"))
33/// );
34/// assert_eq!(
35///     AbsolutePath::from_str("./"),
36///     Err(Error::PathNotAbsolute(PathBuf::from("./")))
37/// );
38///
39/// // Format as String
40/// assert_eq!("/", format!("{}", AbsolutePath::from_str("/")?));
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
45pub struct AbsolutePath(PathBuf);
46
47impl AbsolutePath {
48    /// Create a new `AbsolutePath`
49    pub fn new(path: PathBuf) -> Result<AbsolutePath, Error> {
50        match path.is_absolute() {
51            true => Ok(AbsolutePath(path)),
52            false => Err(Error::PathNotAbsolute(path)),
53        }
54    }
55
56    /// Return a reference to the inner type
57    pub fn inner(&self) -> &Path {
58        &self.0
59    }
60}
61
62impl FromStr for AbsolutePath {
63    type Err = Error;
64
65    /// Parses an absolute path from a string
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the path is not absolute
70    fn from_str(s: &str) -> Result<AbsolutePath, Self::Err> {
71        match Path::new(s).is_absolute() {
72            true => Ok(AbsolutePath(PathBuf::from(s))),
73            false => Err(Error::PathNotAbsolute(PathBuf::from(s))),
74        }
75    }
76}
77
78impl Display for AbsolutePath {
79    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
80        write!(fmt, "{}", self.inner().display())
81    }
82}
83
84/// An absolute path used as build directory
85///
86/// This is a type alias for [`AbsolutePath`]
87///
88/// ## Examples
89/// ```
90/// use std::str::FromStr;
91///
92/// use alpm_types::{Error, BuildDirectory};
93///
94/// # fn main() -> Result<(), alpm_types::Error> {
95/// // Create BuildDirectory from &str and format it
96/// assert_eq!(
97///     "/etc",
98///     BuildDirectory::from_str("/etc")?.to_string()
99/// );
100/// # Ok(())
101/// # }
102pub type BuildDirectory = AbsolutePath;
103
104/// An absolute path used as start directory in a package build environment
105///
106/// This is a type alias for [`AbsolutePath`]
107///
108/// ## Examples
109/// ```
110/// use std::str::FromStr;
111///
112/// use alpm_types::{Error, StartDirectory};
113///
114/// # fn main() -> Result<(), alpm_types::Error> {
115/// // Create StartDirectory from &str and format it
116/// assert_eq!(
117///     "/etc",
118///     StartDirectory::from_str("/etc")?.to_string()
119/// );
120/// # Ok(())
121/// # }
122pub type StartDirectory = AbsolutePath;
123
124/// A representation of a relative file path
125///
126/// `RelativeFilePath` wraps a `PathBuf` that is guaranteed to represent a
127/// relative file path (i.e. it does not end with a `/`).
128///
129/// ## Examples
130///
131/// ```
132/// use std::{path::PathBuf, str::FromStr};
133///
134/// use alpm_types::{Error, RelativeFilePath};
135///
136/// # fn main() -> Result<(), alpm_types::Error> {
137/// // Create RelativeFilePath from &str
138/// assert_eq!(
139///     RelativeFilePath::from_str("etc/test.conf"),
140///     RelativeFilePath::new(PathBuf::from("etc/test.conf"))
141/// );
142/// assert_eq!(
143///     RelativeFilePath::from_str("/etc/test.conf"),
144///     Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
145/// );
146///
147/// // Format as String
148/// assert_eq!(
149///     "test/test.txt",
150///     RelativeFilePath::from_str("test/test.txt")?.to_string()
151/// );
152/// # Ok(())
153/// # }
154/// ```
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct RelativeFilePath(PathBuf);
157
158impl RelativeFilePath {
159    /// Create a new `RelativeFilePath`
160    pub fn new(path: PathBuf) -> Result<RelativeFilePath, Error> {
161        if path
162            .to_string_lossy()
163            .ends_with(std::path::MAIN_SEPARATOR_STR)
164        {
165            return Err(Error::PathIsNotAFile(path));
166        }
167        if !path.is_relative() {
168            return Err(Error::PathNotRelative(path));
169        }
170        Ok(RelativeFilePath(path))
171    }
172
173    /// Return a reference to the inner type
174    pub fn inner(&self) -> &Path {
175        &self.0
176    }
177}
178
179impl FromStr for RelativeFilePath {
180    type Err = Error;
181
182    /// Parses a relative path from a string
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the path is not relative
187    fn from_str(s: &str) -> Result<RelativeFilePath, Self::Err> {
188        Self::new(PathBuf::from(s))
189    }
190}
191
192impl Display for RelativeFilePath {
193    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
194        write!(fmt, "{}", self.inner().display())
195    }
196}
197
198/// The path of a packaged file that should be preserved during package operations
199///
200/// This is a type alias for [`RelativeFilePath`]
201///
202/// ## Examples
203/// ```
204/// use std::str::FromStr;
205///
206/// use alpm_types::Backup;
207///
208/// # fn main() -> Result<(), alpm_types::Error> {
209/// // Create Backup from &str and format it
210/// assert_eq!(
211///     "etc/test.conf",
212///     Backup::from_str("etc/test.conf")?.to_string()
213/// );
214/// # Ok(())
215/// # }
216pub type Backup = RelativeFilePath;
217
218/// A special install script that is to be included in the package
219///
220/// This is a type alias for [RelativeFilePath`]
221///
222/// ## Examples
223/// ```
224/// use std::str::FromStr;
225///
226/// use alpm_types::{Error, Install};
227///
228/// # fn main() -> Result<(), alpm_types::Error> {
229/// // Create Install from &str and format it
230/// assert_eq!(
231///     "scripts/setup.install",
232///     Install::from_str("scripts/setup.install")?.to_string()
233/// );
234/// # Ok(())
235/// # }
236pub type Install = RelativeFilePath;
237
238/// The relative path to a changelog file that may be included in a package
239///
240/// This is a type alias for [`RelativeFilePath`]
241///
242/// ## Examples
243/// ```
244/// use std::str::FromStr;
245///
246/// use alpm_types::{Error, Changelog};
247///
248/// # fn main() -> Result<(), alpm_types::Error> {
249/// // Create Changelog from &str and format it
250/// assert_eq!(
251///     "changelog.md",
252///     Changelog::from_str("changelog.md")?.to_string()
253/// );
254/// # Ok(())
255/// # }
256pub type Changelog = RelativeFilePath;
257
258/// A lookup directory for shared object files.
259///
260/// Follows the [alpm-sonamev2] format, which encodes a `prefix` and a `directory`.
261/// The same `prefix` is later used to identify the location of a **soname**, see
262/// [`SonameV2`][crate::SonameV2].
263///
264/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
265#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
266pub struct SonameLookupDirectory {
267    /// The lookup prefix for shared objects.
268    pub prefix: SharedLibraryPrefix,
269    /// The directory to look for shared objects in.
270    pub directory: AbsolutePath,
271}
272
273impl SonameLookupDirectory {
274    /// Creates a new lookup directory with a prefix and a directory.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use alpm_types::SonameLookupDirectory;
280    ///
281    /// # fn main() -> Result<(), alpm_types::Error> {
282    /// SonameLookupDirectory::new("lib".parse()?, "/usr/lib".parse()?);
283    /// # Ok(())
284    /// # }
285    /// ```
286    pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
287        Self { prefix, directory }
288    }
289
290    /// Parses a [`SonameLookupDirectory`] from a string slice.
291    ///
292    /// Consumes all of its input.
293    ///
294    /// See [`SonameLookupDirectory::from_str`] for more details.
295    pub fn parser(input: &mut &str) -> ModalResult<Self> {
296        // Parse until the first `:`, which separates the prefix from the directory.
297        let prefix = cut_err(
298            repeat_till(1.., any, peek(alt((":", eof))))
299                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
300        )
301        .context(StrContext::Label("prefix for a shared object lookup path"))
302        .parse_next(input)?;
303
304        // Take the delimiter.
305        cut_err(":")
306            .context(StrContext::Label("shared library prefix delimiter"))
307            .context(StrContext::Expected(StrContextValue::Description(
308                "shared library prefix `:`",
309            )))
310            .parse_next(input)?;
311
312        // Parse the rest as a directory.
313        let directory = rest
314            .verify(|s: &str| !s.is_empty())
315            .try_map(AbsolutePath::from_str)
316            .context(StrContext::Label("directory"))
317            .context(StrContext::Expected(StrContextValue::Description(
318                "directory for a shared object lookup path",
319            )))
320            .parse_next(input)?;
321
322        Ok(Self { prefix, directory })
323    }
324}
325
326impl Display for SonameLookupDirectory {
327    /// Converts the [`SonameLookupDirectory`] to a string.
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        write!(f, "{}:{}", self.prefix, self.directory)
330    }
331}
332
333impl FromStr for SonameLookupDirectory {
334    type Err = Error;
335
336    /// Creates a [`SonameLookupDirectory`] from a string slice.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if `input` can not be converted into a [`SonameLookupDirectory`].
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use std::str::FromStr;
346    ///
347    /// use alpm_types::SonameLookupDirectory;
348    ///
349    /// # fn main() -> Result<(), alpm_types::Error> {
350    /// let dir = SonameLookupDirectory::from_str("lib:/usr/lib")?;
351    /// assert_eq!(dir.to_string(), "lib:/usr/lib");
352    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
353    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
354    /// assert!(SonameLookupDirectory::from_str("lib:").is_err());
355    /// # Ok(())
356    /// # }
357    /// ```
358    fn from_str(s: &str) -> Result<Self, Self::Err> {
359        Ok(Self::parser.parse(s)?)
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use rstest::rstest;
366    use testresult::TestResult;
367
368    use super::*;
369
370    #[rstest]
371    #[case("/home", BuildDirectory::new(PathBuf::from("/home")))]
372    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
373    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
374    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
375    fn build_dir_from_string(#[case] s: &str, #[case] result: Result<BuildDirectory, Error>) {
376        assert_eq!(BuildDirectory::from_str(s), result);
377    }
378
379    #[rstest]
380    #[case("/start", StartDirectory::new(PathBuf::from("/start")))]
381    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
382    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
383    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
384    fn startdir_from_str(#[case] s: &str, #[case] result: Result<StartDirectory, Error>) {
385        assert_eq!(StartDirectory::from_str(s), result);
386    }
387
388    #[rstest]
389    #[case("etc/test.conf", RelativeFilePath::new(PathBuf::from("etc/test.conf")))]
390    #[case(
391        "/etc/test.conf",
392        Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
393    )]
394    #[case("etc/", Err(Error::PathIsNotAFile(PathBuf::from("etc/"))))]
395    #[case("etc", RelativeFilePath::new(PathBuf::from("etc")))]
396    #[case(
397        "../etc/test.conf",
398        RelativeFilePath::new(PathBuf::from("../etc/test.conf"))
399    )]
400    fn relative_path_from_str(#[case] s: &str, #[case] result: Result<RelativeFilePath, Error>) {
401        assert_eq!(RelativeFilePath::from_str(s), result);
402    }
403
404    #[rstest]
405    #[case("lib:/usr/lib", SonameLookupDirectory {
406        prefix: "lib".parse()?,
407        directory: AbsolutePath::from_str("/usr/lib")?,
408    })]
409    #[case("lib32:/usr/lib32", SonameLookupDirectory {
410        prefix: "lib32".parse()?,
411        directory: AbsolutePath::from_str("/usr/lib32")?,
412    })]
413    fn soname_lookup_directory_from_string(
414        #[case] input: &str,
415        #[case] expected_result: SonameLookupDirectory,
416    ) -> TestResult {
417        let lookup_directory = SonameLookupDirectory::from_str(input)?;
418        assert_eq!(expected_result, lookup_directory);
419        assert_eq!(input, lookup_directory.to_string());
420        Ok(())
421    }
422
423    #[rstest]
424    #[case("lib", "invalid shared library prefix delimiter")]
425    #[case("lib:", "invalid directory")]
426    #[case(":/usr/lib", "invalid first character of package name")]
427    fn invalid_soname_lookup_directory_parser(#[case] input: &str, #[case] error_snippet: &str) {
428        let result = SonameLookupDirectory::from_str(input);
429        assert!(result.is_err(), "Expected LookupDirectory parsing to fail");
430        let err = result.unwrap_err();
431        let pretty_error = err.to_string();
432        assert!(
433            pretty_error.contains(error_snippet),
434            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
435        );
436    }
437}