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 path
125///
126/// [`RelativePath`] wraps a [`PathBuf`] that is guaranteed to represent a relative path, regardless
127/// of whether it points to a file or a directory.
128///
129/// ## Examples
130///
131/// ```
132/// use std::{path::PathBuf, str::FromStr};
133///
134/// use alpm_types::{Error, RelativePath};
135///
136/// # fn main() -> Result<(), alpm_types::Error> {
137/// // Create RelativePath from &str
138/// assert_eq!(
139///     RelativePath::from_str("etc/test.conf"),
140///     RelativePath::new(PathBuf::from("etc/test.conf"))
141/// );
142/// assert_eq!(
143///     RelativePath::from_str("etc/"),
144///     RelativePath::new(PathBuf::from("etc/"))
145/// );
146/// assert_eq!(
147///     RelativePath::from_str("/etc/test.conf"),
148///     Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
149/// );
150///
151/// // Format as String
152/// assert_eq!("test/", RelativePath::from_str("test/")?.to_string());
153/// # Ok(())
154/// # }
155/// ```
156#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
157pub struct RelativePath(PathBuf);
158
159impl RelativePath {
160    /// Create a new [`RelativePath`]
161    pub fn new(path: PathBuf) -> Result<RelativePath, Error> {
162        if !path.is_relative() {
163            return Err(Error::PathNotRelative(path));
164        }
165        Ok(RelativePath(path))
166    }
167
168    /// Consume `self` and return the inner [`PathBuf`]
169    pub fn into_inner(self) -> PathBuf {
170        self.0
171    }
172}
173
174impl AsRef<Path> for RelativePath {
175    fn as_ref(&self) -> &Path {
176        &self.0
177    }
178}
179
180impl FromStr for RelativePath {
181    type Err = Error;
182
183    /// Parses a relative path from a string
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the path is not relative.
188    fn from_str(s: &str) -> Result<RelativePath, Self::Err> {
189        Self::new(PathBuf::from(s))
190    }
191}
192
193impl Display for RelativePath {
194    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
195        write!(fmt, "{}", self.as_ref().display())
196    }
197}
198
199/// A representation of a relative file path
200///
201/// `RelativeFilePath` wraps a `PathBuf` that is guaranteed to represent a
202/// relative file path (i.e. it does not end with a `/`).
203///
204/// ## Examples
205///
206/// ```
207/// use std::{path::PathBuf, str::FromStr};
208///
209/// use alpm_types::{Error, RelativeFilePath};
210///
211/// # fn main() -> Result<(), alpm_types::Error> {
212/// // Create RelativeFilePath from &str
213/// assert_eq!(
214///     RelativeFilePath::from_str("etc/test.conf"),
215///     RelativeFilePath::new(PathBuf::from("etc/test.conf"))
216/// );
217/// assert_eq!(
218///     RelativeFilePath::from_str("/etc/test.conf"),
219///     Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
220/// );
221///
222/// // Format as String
223/// assert_eq!(
224///     "test/test.txt",
225///     RelativeFilePath::from_str("test/test.txt")?.to_string()
226/// );
227/// # Ok(())
228/// # }
229/// ```
230#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
231pub struct RelativeFilePath(PathBuf);
232
233impl RelativeFilePath {
234    /// Create a new `RelativeFilePath`
235    pub fn new(path: PathBuf) -> Result<RelativeFilePath, Error> {
236        if path
237            .to_string_lossy()
238            .ends_with(std::path::MAIN_SEPARATOR_STR)
239        {
240            return Err(Error::PathIsNotAFile(path));
241        }
242        if !path.is_relative() {
243            return Err(Error::PathNotRelative(path));
244        }
245        Ok(RelativeFilePath(path))
246    }
247
248    /// Return a reference to the inner type
249    pub fn inner(&self) -> &Path {
250        &self.0
251    }
252}
253
254impl FromStr for RelativeFilePath {
255    type Err = Error;
256
257    /// Parses a relative path from a string
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if the path is not relative
262    fn from_str(s: &str) -> Result<RelativeFilePath, Self::Err> {
263        Self::new(PathBuf::from(s))
264    }
265}
266
267impl Display for RelativeFilePath {
268    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
269        write!(fmt, "{}", self.inner().display())
270    }
271}
272
273/// The path of a packaged file that should be preserved during package operations
274///
275/// This is a type alias for [`RelativeFilePath`]
276///
277/// ## Examples
278/// ```
279/// use std::str::FromStr;
280///
281/// use alpm_types::Backup;
282///
283/// # fn main() -> Result<(), alpm_types::Error> {
284/// // Create Backup from &str and format it
285/// assert_eq!(
286///     "etc/test.conf",
287///     Backup::from_str("etc/test.conf")?.to_string()
288/// );
289/// # Ok(())
290/// # }
291pub type Backup = RelativeFilePath;
292
293/// A special install script that is to be included in the package
294///
295/// This is a type alias for [RelativeFilePath`]
296///
297/// ## Examples
298/// ```
299/// use std::str::FromStr;
300///
301/// use alpm_types::{Error, Install};
302///
303/// # fn main() -> Result<(), alpm_types::Error> {
304/// // Create Install from &str and format it
305/// assert_eq!(
306///     "scripts/setup.install",
307///     Install::from_str("scripts/setup.install")?.to_string()
308/// );
309/// # Ok(())
310/// # }
311pub type Install = RelativeFilePath;
312
313/// The relative path to a changelog file that may be included in a package
314///
315/// This is a type alias for [`RelativeFilePath`]
316///
317/// ## Examples
318/// ```
319/// use std::str::FromStr;
320///
321/// use alpm_types::{Error, Changelog};
322///
323/// # fn main() -> Result<(), alpm_types::Error> {
324/// // Create Changelog from &str and format it
325/// assert_eq!(
326///     "changelog.md",
327///     Changelog::from_str("changelog.md")?.to_string()
328/// );
329/// # Ok(())
330/// # }
331pub type Changelog = RelativeFilePath;
332
333/// A lookup directory for shared object files.
334///
335/// Follows the [alpm-sonamev2] format, which encodes a `prefix` and a `directory`.
336/// The same `prefix` is later used to identify the location of a **soname**, see
337/// [`SonameV2`][crate::SonameV2].
338///
339/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
340#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
341pub struct SonameLookupDirectory {
342    /// The lookup prefix for shared objects.
343    pub prefix: SharedLibraryPrefix,
344    /// The directory to look for shared objects in.
345    pub directory: AbsolutePath,
346}
347
348impl SonameLookupDirectory {
349    /// Creates a new lookup directory with a prefix and a directory.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use alpm_types::SonameLookupDirectory;
355    ///
356    /// # fn main() -> Result<(), alpm_types::Error> {
357    /// SonameLookupDirectory::new("lib".parse()?, "/usr/lib".parse()?);
358    /// # Ok(())
359    /// # }
360    /// ```
361    pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
362        Self { prefix, directory }
363    }
364
365    /// Parses a [`SonameLookupDirectory`] from a string slice.
366    ///
367    /// Consumes all of its input.
368    ///
369    /// See [`SonameLookupDirectory::from_str`] for more details.
370    pub fn parser(input: &mut &str) -> ModalResult<Self> {
371        // Parse until the first `:`, which separates the prefix from the directory.
372        let prefix = cut_err(
373            repeat_till(1.., any, peek(alt((":", eof))))
374                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
375        )
376        .context(StrContext::Label("prefix for a shared object lookup path"))
377        .parse_next(input)?;
378
379        // Take the delimiter.
380        cut_err(":")
381            .context(StrContext::Label("shared library prefix delimiter"))
382            .context(StrContext::Expected(StrContextValue::Description(
383                "shared library prefix `:`",
384            )))
385            .parse_next(input)?;
386
387        // Parse the rest as a directory.
388        let directory = rest
389            .verify(|s: &str| !s.is_empty())
390            .try_map(AbsolutePath::from_str)
391            .context(StrContext::Label("directory"))
392            .context(StrContext::Expected(StrContextValue::Description(
393                "directory for a shared object lookup path",
394            )))
395            .parse_next(input)?;
396
397        Ok(Self { prefix, directory })
398    }
399}
400
401impl Display for SonameLookupDirectory {
402    /// Converts the [`SonameLookupDirectory`] to a string.
403    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404        write!(f, "{}:{}", self.prefix, self.directory)
405    }
406}
407
408impl FromStr for SonameLookupDirectory {
409    type Err = Error;
410
411    /// Creates a [`SonameLookupDirectory`] from a string slice.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error if `input` can not be converted into a [`SonameLookupDirectory`].
416    ///
417    /// # Examples
418    ///
419    /// ```
420    /// use std::str::FromStr;
421    ///
422    /// use alpm_types::SonameLookupDirectory;
423    ///
424    /// # fn main() -> Result<(), alpm_types::Error> {
425    /// let dir = SonameLookupDirectory::from_str("lib:/usr/lib")?;
426    /// assert_eq!(dir.to_string(), "lib:/usr/lib");
427    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
428    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
429    /// assert!(SonameLookupDirectory::from_str("lib:").is_err());
430    /// # Ok(())
431    /// # }
432    /// ```
433    fn from_str(s: &str) -> Result<Self, Self::Err> {
434        Ok(Self::parser.parse(s)?)
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use rstest::rstest;
441    use testresult::TestResult;
442
443    use super::*;
444
445    #[rstest]
446    #[case("/home", BuildDirectory::new(PathBuf::from("/home")))]
447    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
448    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
449    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
450    fn build_dir_from_string(#[case] s: &str, #[case] result: Result<BuildDirectory, Error>) {
451        assert_eq!(BuildDirectory::from_str(s), result);
452    }
453
454    #[rstest]
455    #[case("/start", StartDirectory::new(PathBuf::from("/start")))]
456    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
457    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
458    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
459    fn startdir_from_str(#[case] s: &str, #[case] result: Result<StartDirectory, Error>) {
460        assert_eq!(StartDirectory::from_str(s), result);
461    }
462
463    #[rstest]
464    #[case("etc/test.conf", RelativePath::new(PathBuf::from("etc/test.conf")))]
465    #[case("etc/", RelativePath::new(PathBuf::from("etc/")))]
466    #[case(
467        "/etc/test.conf",
468        Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
469    )]
470    #[case(
471        "../etc/test.conf",
472        RelativePath::new(PathBuf::from("../etc/test.conf"))
473    )]
474    fn relative_path_from_str(#[case] s: &str, #[case] result: Result<RelativePath, Error>) {
475        assert_eq!(RelativePath::from_str(s), result);
476    }
477
478    #[rstest]
479    #[case("etc/test.conf", RelativeFilePath::new(PathBuf::from("etc/test.conf")))]
480    #[case(
481        "/etc/test.conf",
482        Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
483    )]
484    #[case("etc/", Err(Error::PathIsNotAFile(PathBuf::from("etc/"))))]
485    #[case("etc", RelativeFilePath::new(PathBuf::from("etc")))]
486    #[case(
487        "../etc/test.conf",
488        RelativeFilePath::new(PathBuf::from("../etc/test.conf"))
489    )]
490    fn relative_file_path_from_str(
491        #[case] s: &str,
492        #[case] result: Result<RelativeFilePath, Error>,
493    ) {
494        assert_eq!(RelativeFilePath::from_str(s), result);
495    }
496
497    #[rstest]
498    #[case("lib:/usr/lib", SonameLookupDirectory {
499        prefix: "lib".parse()?,
500        directory: AbsolutePath::from_str("/usr/lib")?,
501    })]
502    #[case("lib32:/usr/lib32", SonameLookupDirectory {
503        prefix: "lib32".parse()?,
504        directory: AbsolutePath::from_str("/usr/lib32")?,
505    })]
506    fn soname_lookup_directory_from_string(
507        #[case] input: &str,
508        #[case] expected_result: SonameLookupDirectory,
509    ) -> TestResult {
510        let lookup_directory = SonameLookupDirectory::from_str(input)?;
511        assert_eq!(expected_result, lookup_directory);
512        assert_eq!(input, lookup_directory.to_string());
513        Ok(())
514    }
515
516    #[rstest]
517    #[case("lib", "invalid shared library prefix delimiter")]
518    #[case("lib:", "invalid directory")]
519    #[case(":/usr/lib", "invalid first character of package name")]
520    fn invalid_soname_lookup_directory_parser(#[case] input: &str, #[case] error_snippet: &str) {
521        let result = SonameLookupDirectory::from_str(input);
522        assert!(result.is_err(), "Expected LookupDirectory parsing to fail");
523        let err = result.unwrap_err();
524        let pretty_error = err.to_string();
525        assert!(
526            pretty_error.contains(error_snippet),
527            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
528        );
529    }
530}