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