Skip to main content

alpm_types/
path.rs

1use std::{
2    fmt::{Display, Formatter},
3    path::{Path, PathBuf},
4    str::FromStr,
5};
6
7use alpm_parsers::traits::ParserUntil;
8use serde::{Deserialize, Serialize};
9use winnow::{
10    ModalResult,
11    Parser,
12    combinator::{alt, eof, peek, repeat_till},
13    error::{ContextError, ErrMode, StrContext, StrContextValue},
14    token::any,
15};
16
17use crate::{Error, SharedLibraryPrefix};
18
19/// A representation of an absolute path
20///
21/// AbsolutePath wraps a `PathBuf`, that is guaranteed to be absolute.
22///
23/// ## Examples
24/// ```
25/// use std::{path::PathBuf, str::FromStr};
26///
27/// use alpm_types::{AbsolutePath, Error};
28///
29/// # fn main() -> Result<(), alpm_types::Error> {
30/// // Create AbsolutePath from &str
31/// assert_eq!(
32///     AbsolutePath::from_str("/"),
33///     AbsolutePath::new(PathBuf::from("/"))
34/// );
35/// assert_eq!(
36///     AbsolutePath::from_str("./"),
37///     Err(Error::PathNotAbsolute(PathBuf::from("./")))
38/// );
39///
40/// // Format as String
41/// assert_eq!("/", format!("{}", AbsolutePath::from_str("/")?));
42/// # Ok(())
43/// # }
44/// ```
45#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
46pub struct AbsolutePath(PathBuf);
47
48impl AbsolutePath {
49    /// Create a new `AbsolutePath`
50    pub fn new(path: PathBuf) -> Result<AbsolutePath, Error> {
51        match path.is_absolute() {
52            true => Ok(AbsolutePath(path)),
53            false => Err(Error::PathNotAbsolute(path)),
54        }
55    }
56
57    /// Return a reference to the inner type
58    pub fn inner(&self) -> &Path {
59        &self.0
60    }
61}
62
63impl FromStr for AbsolutePath {
64    type Err = Error;
65
66    /// Parses an absolute path from a string
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the path is not absolute
71    fn from_str(s: &str) -> Result<AbsolutePath, Self::Err> {
72        match Path::new(s).is_absolute() {
73            true => Ok(AbsolutePath(PathBuf::from(s))),
74            false => Err(Error::PathNotAbsolute(PathBuf::from(s))),
75        }
76    }
77}
78
79impl Display for AbsolutePath {
80    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
81        write!(fmt, "{}", self.inner().display())
82    }
83}
84
85/// An absolute path used as build directory
86///
87/// This is a type alias for [`AbsolutePath`]
88///
89/// ## Examples
90/// ```
91/// use std::str::FromStr;
92///
93/// use alpm_types::{Error, BuildDirectory};
94///
95/// # fn main() -> Result<(), alpm_types::Error> {
96/// // Create BuildDirectory from &str and format it
97/// assert_eq!(
98///     "/etc",
99///     BuildDirectory::from_str("/etc")?.to_string()
100/// );
101/// # Ok(())
102/// # }
103pub type BuildDirectory = AbsolutePath;
104
105/// An absolute path used as start directory in a package build environment
106///
107/// This is a type alias for [`AbsolutePath`]
108///
109/// ## Examples
110/// ```
111/// use std::str::FromStr;
112///
113/// use alpm_types::{Error, StartDirectory};
114///
115/// # fn main() -> Result<(), alpm_types::Error> {
116/// // Create StartDirectory from &str and format it
117/// assert_eq!(
118///     "/etc",
119///     StartDirectory::from_str("/etc")?.to_string()
120/// );
121/// # Ok(())
122/// # }
123pub type StartDirectory = AbsolutePath;
124
125/// A representation of a relative path
126///
127/// [`RelativePath`] wraps a [`PathBuf`] that is guaranteed to represent a relative path, regardless
128/// of whether it points to a file or a directory.
129///
130/// ## Examples
131///
132/// ```
133/// use std::{path::PathBuf, str::FromStr};
134///
135/// use alpm_types::{Error, RelativePath};
136///
137/// # fn main() -> Result<(), alpm_types::Error> {
138/// // Create RelativePath from &str
139/// assert_eq!(
140///     RelativePath::from_str("etc/test.conf"),
141///     RelativePath::new(PathBuf::from("etc/test.conf"))
142/// );
143/// assert_eq!(
144///     RelativePath::from_str("etc/"),
145///     RelativePath::new(PathBuf::from("etc/"))
146/// );
147/// assert_eq!(
148///     RelativePath::from_str("/etc/test.conf"),
149///     Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
150/// );
151///
152/// // Format as String
153/// assert_eq!("test/", RelativePath::from_str("test/")?.to_string());
154/// # Ok(())
155/// # }
156/// ```
157#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
158pub struct RelativePath(PathBuf);
159
160impl RelativePath {
161    /// Create a new [`RelativePath`]
162    pub fn new(path: PathBuf) -> Result<RelativePath, Error> {
163        if !path.is_relative() {
164            return Err(Error::PathNotRelative(path));
165        }
166        Ok(RelativePath(path))
167    }
168
169    /// Consume `self` and return the inner [`PathBuf`]
170    pub fn into_inner(self) -> PathBuf {
171        self.0
172    }
173}
174
175impl AsRef<Path> for RelativePath {
176    fn as_ref(&self) -> &Path {
177        &self.0
178    }
179}
180
181impl FromStr for RelativePath {
182    type Err = Error;
183
184    /// Parses a relative path from a string
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if the path is not relative.
189    fn from_str(s: &str) -> Result<RelativePath, Self::Err> {
190        Self::new(PathBuf::from(s))
191    }
192}
193
194impl Display for RelativePath {
195    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
196        write!(fmt, "{}", self.as_ref().display())
197    }
198}
199
200/// A representation of a relative file path
201///
202/// `RelativeFilePath` wraps a `PathBuf` that is guaranteed to represent a
203/// relative file path (i.e. it does not end with a `/`).
204///
205/// ## Examples
206///
207/// ```
208/// use std::{path::PathBuf, str::FromStr};
209///
210/// use alpm_types::{Error, RelativeFilePath};
211///
212/// # fn main() -> Result<(), alpm_types::Error> {
213/// // Create RelativeFilePath from &str
214/// assert_eq!(
215///     RelativeFilePath::from_str("etc/test.conf"),
216///     RelativeFilePath::new(PathBuf::from("etc/test.conf"))
217/// );
218/// assert_eq!(
219///     RelativeFilePath::from_str("/etc/test.conf"),
220///     Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
221/// );
222///
223/// // Format as String
224/// assert_eq!(
225///     "test/test.txt",
226///     RelativeFilePath::from_str("test/test.txt")?.to_string()
227/// );
228/// # Ok(())
229/// # }
230/// ```
231#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
232pub struct RelativeFilePath(PathBuf);
233
234impl RelativeFilePath {
235    /// Create a new `RelativeFilePath`
236    pub fn new(path: PathBuf) -> Result<RelativeFilePath, Error> {
237        if path
238            .to_string_lossy()
239            .ends_with(std::path::MAIN_SEPARATOR_STR)
240        {
241            return Err(Error::PathIsNotAFile(path));
242        }
243        if !path.is_relative() {
244            return Err(Error::PathNotRelative(path));
245        }
246        Ok(RelativeFilePath(path))
247    }
248
249    /// Return a reference to the inner type
250    pub fn inner(&self) -> &Path {
251        &self.0
252    }
253}
254
255impl FromStr for RelativeFilePath {
256    type Err = Error;
257
258    /// Parses a relative path from a string
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the path is not relative
263    fn from_str(s: &str) -> Result<RelativeFilePath, Self::Err> {
264        Self::new(PathBuf::from(s))
265    }
266}
267
268impl Display for RelativeFilePath {
269    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
270        write!(fmt, "{}", self.inner().display())
271    }
272}
273
274/// The path of a packaged file that should be preserved during package operations
275///
276/// This is a type alias for [`RelativeFilePath`]
277///
278/// ## Examples
279/// ```
280/// use std::str::FromStr;
281///
282/// use alpm_types::Backup;
283///
284/// # fn main() -> Result<(), alpm_types::Error> {
285/// // Create Backup from &str and format it
286/// assert_eq!(
287///     "etc/test.conf",
288///     Backup::from_str("etc/test.conf")?.to_string()
289/// );
290/// # Ok(())
291/// # }
292pub type Backup = RelativeFilePath;
293
294/// A special install script that is to be included in the package
295///
296/// This is a type alias for [RelativeFilePath`]
297///
298/// ## Examples
299/// ```
300/// use std::str::FromStr;
301///
302/// use alpm_types::{Error, Install};
303///
304/// # fn main() -> Result<(), alpm_types::Error> {
305/// // Create Install from &str and format it
306/// assert_eq!(
307///     "scripts/setup.install",
308///     Install::from_str("scripts/setup.install")?.to_string()
309/// );
310/// # Ok(())
311/// # }
312pub type Install = RelativeFilePath;
313
314/// The relative path to a changelog file that may be included in a package
315///
316/// This is a type alias for [`RelativeFilePath`]
317///
318/// ## Examples
319/// ```
320/// use std::str::FromStr;
321///
322/// use alpm_types::{Error, Changelog};
323///
324/// # fn main() -> Result<(), alpm_types::Error> {
325/// // Create Changelog from &str and format it
326/// assert_eq!(
327///     "changelog.md",
328///     Changelog::from_str("changelog.md")?.to_string()
329/// );
330/// # Ok(())
331/// # }
332pub type Changelog = RelativeFilePath;
333
334/// A lookup directory for shared object files.
335///
336/// Follows the [alpm-sonamev2] format, which encodes a `prefix` and a `directory`.
337/// The same `prefix` is later used to identify the location of a **soname**, see
338/// [`SonameV2`][crate::SonameV2].
339///
340/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
341#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
342pub struct SonameLookupDirectory {
343    /// The lookup prefix for shared objects.
344    pub prefix: SharedLibraryPrefix,
345    /// The directory to look for shared objects in.
346    pub directory: AbsolutePath,
347}
348
349impl SonameLookupDirectory {
350    /// Creates a new lookup directory with a prefix and a directory.
351    ///
352    /// # Examples
353    ///
354    /// ```
355    /// use alpm_types::SonameLookupDirectory;
356    ///
357    /// # fn main() -> Result<(), alpm_types::Error> {
358    /// SonameLookupDirectory::new("lib".parse()?, "/usr/lib".parse()?);
359    /// # Ok(())
360    /// # }
361    /// ```
362    pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
363        Self { prefix, directory }
364    }
365}
366
367impl ParserUntil for SonameLookupDirectory {
368    /// Parses a [`SonameLookupDirectory`] from a string slice.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error, if the parser input does not contain a valid [`SonameLookupDirectory`]
373    /// before the `delimiter`.
374    fn parser_until<'a, P>(delimiter: P) -> impl Parser<&'a str, Self, ErrMode<ContextError>>
375    where
376        P: Parser<&'a str, &'a str, ErrMode<ContextError>>,
377    {
378        // Define the actual parser closure.
379        // The delimiter is moved into the closure and borrowed via `by_ref()` on each call.
380        let mut delimiter_parser = delimiter;
381        move |input: &mut &'a str| -> ModalResult<Self> {
382            // Parse until the first `:`, which separates the prefix from the directory.
383            let prefix = repeat_till(1.., any, peek(alt((":", eof))))
384                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name))
385                .context(StrContext::Label("prefix for a shared object lookup path"))
386                .parse_next(input)?;
387
388            // Take the delimiter.
389            ":".context(StrContext::Label("shared library prefix delimiter"))
390                .context(StrContext::Expected(StrContextValue::Description(
391                    "shared library prefix `:`",
392                )))
393                .parse_next(input)?;
394
395            // Parse the rest as a directory.
396            let directory = repeat_till(1.., any, peek(delimiter_parser.by_ref()))
397                .try_map(|(path, _): (String, &str)| AbsolutePath::from_str(&path))
398                .context(StrContext::Label("directory"))
399                .context(StrContext::Expected(StrContextValue::Description(
400                    "directory for a shared object lookup path",
401                )))
402                .parse_next(input)?;
403
404            peek(delimiter_parser.by_ref())
405                .context(StrContext::Label("SonameLookupDirectory"))
406                .context(StrContext::Expected(StrContextValue::Description(
407                    "valid end of input.",
408                )))
409                .parse_next(input)?;
410
411            Ok(Self { prefix, directory })
412        }
413    }
414}
415
416impl Display for SonameLookupDirectory {
417    /// Converts the [`SonameLookupDirectory`] to a string.
418    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419        write!(f, "{}:{}", self.prefix, self.directory)
420    }
421}
422
423impl FromStr for SonameLookupDirectory {
424    type Err = Error;
425
426    /// Creates a [`SonameLookupDirectory`] from a string slice.
427    ///
428    /// Delegates to [`SonameLookupDirectory::parser_until`].
429    ///
430    /// # Errors
431    ///
432    /// Returns an error if [`SonameLookupDirectory::parser_until`] fails.
433    ///
434    /// # Examples
435    ///
436    /// ```
437    /// use std::str::FromStr;
438    ///
439    /// use alpm_types::SonameLookupDirectory;
440    ///
441    /// # fn main() -> Result<(), alpm_types::Error> {
442    /// let dir = SonameLookupDirectory::from_str("lib:/usr/lib")?;
443    /// assert_eq!(dir.to_string(), "lib:/usr/lib");
444    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
445    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
446    /// assert!(SonameLookupDirectory::from_str("lib:").is_err());
447    /// # Ok(())
448    /// # }
449    /// ```
450    fn from_str(s: &str) -> Result<Self, Self::Err> {
451        Ok(Self::parser_until_eof.parse(s)?)
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use insta::assert_snapshot;
458    use rstest::rstest;
459    use testresult::TestResult;
460
461    use super::*;
462    use crate::configure_insta;
463
464    #[rstest]
465    #[case("/home", BuildDirectory::new(PathBuf::from("/home")))]
466    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
467    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
468    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
469    fn build_dir_from_string(#[case] s: &str, #[case] result: Result<BuildDirectory, Error>) {
470        assert_eq!(BuildDirectory::from_str(s), result);
471    }
472
473    #[rstest]
474    #[case("/start", StartDirectory::new(PathBuf::from("/start")))]
475    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
476    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
477    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
478    fn startdir_from_str(#[case] s: &str, #[case] result: Result<StartDirectory, Error>) {
479        assert_eq!(StartDirectory::from_str(s), result);
480    }
481
482    #[rstest]
483    #[case("etc/test.conf", RelativePath::new(PathBuf::from("etc/test.conf")))]
484    #[case("etc/", RelativePath::new(PathBuf::from("etc/")))]
485    #[case(
486        "/etc/test.conf",
487        Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
488    )]
489    #[case(
490        "../etc/test.conf",
491        RelativePath::new(PathBuf::from("../etc/test.conf"))
492    )]
493    fn relative_path_from_str(#[case] s: &str, #[case] result: Result<RelativePath, Error>) {
494        assert_eq!(RelativePath::from_str(s), result);
495    }
496
497    #[rstest]
498    #[case("etc/test.conf", RelativeFilePath::new(PathBuf::from("etc/test.conf")))]
499    #[case(
500        "/etc/test.conf",
501        Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
502    )]
503    #[case("etc/", Err(Error::PathIsNotAFile(PathBuf::from("etc/"))))]
504    #[case("etc", RelativeFilePath::new(PathBuf::from("etc")))]
505    #[case(
506        "../etc/test.conf",
507        RelativeFilePath::new(PathBuf::from("../etc/test.conf"))
508    )]
509    fn relative_file_path_from_str(
510        #[case] s: &str,
511        #[case] result: Result<RelativeFilePath, Error>,
512    ) {
513        assert_eq!(RelativeFilePath::from_str(s), result);
514    }
515
516    #[rstest]
517    #[case("lib:/usr/lib", SonameLookupDirectory {
518        prefix: "lib".parse()?,
519        directory: AbsolutePath::from_str("/usr/lib")?,
520    })]
521    #[case("lib32:/usr/lib32", SonameLookupDirectory {
522        prefix: "lib32".parse()?,
523        directory: AbsolutePath::from_str("/usr/lib32")?,
524    })]
525    fn soname_lookup_directory_from_string(
526        #[case] input: &str,
527        #[case] expected_result: SonameLookupDirectory,
528    ) -> TestResult {
529        let lookup_directory = SonameLookupDirectory::from_str(input)?;
530        assert_eq!(expected_result, lookup_directory);
531        assert_eq!(input, lookup_directory.to_string());
532        Ok(())
533    }
534
535    #[rstest]
536    #[case("lib")]
537    #[case("lib:")]
538    #[case(":/usr/lib")]
539    fn invalid_soname_lookup_directory_parser(#[case] input: &str) {
540        let Err(Error::ParseError(err_msg)) = SonameLookupDirectory::from_str(input) else {
541            panic!("'{input}' erroneously parsed as a SonameLookupDirectory")
542        };
543
544        let (test_name, _guard) = configure_insta();
545        assert_snapshot!(test_name, err_msg.to_string());
546    }
547}