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/// `RelativePath` 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, 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/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///     RelativePath::from_str("test/test.txt")?.to_string()
151/// );
152/// # Ok(())
153/// # }
154/// ```
155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct RelativePath(PathBuf);
157
158impl RelativePath {
159    /// Create a new `RelativePath`
160    pub fn new(path: PathBuf) -> Result<RelativePath, Error> {
161        match path.is_relative()
162            && !path
163                .to_string_lossy()
164                .ends_with(std::path::MAIN_SEPARATOR_STR)
165        {
166            true => Ok(RelativePath(path)),
167            false => Err(Error::PathNotRelative(path)),
168        }
169    }
170
171    /// Return a reference to the inner type
172    pub fn inner(&self) -> &Path {
173        &self.0
174    }
175}
176
177impl FromStr for RelativePath {
178    type Err = Error;
179
180    /// Parses a relative path from a string
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if the path is not relative
185    fn from_str(s: &str) -> Result<RelativePath, Self::Err> {
186        Self::new(PathBuf::from(s))
187    }
188}
189
190impl Display for RelativePath {
191    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
192        write!(fmt, "{}", self.inner().display())
193    }
194}
195
196/// The path of a packaged file that should be preserved during package operations
197///
198/// This is a type alias for [`RelativePath`]
199///
200/// ## Examples
201/// ```
202/// use std::str::FromStr;
203///
204/// use alpm_types::Backup;
205///
206/// # fn main() -> Result<(), alpm_types::Error> {
207/// // Create Backup from &str and format it
208/// assert_eq!(
209///     "etc/test.conf",
210///     Backup::from_str("etc/test.conf")?.to_string()
211/// );
212/// # Ok(())
213/// # }
214pub type Backup = RelativePath;
215
216/// A special install script that is to be included in the package
217///
218/// This is a type alias for [RelativePath`]
219///
220/// ## Examples
221/// ```
222/// use std::str::FromStr;
223///
224/// use alpm_types::{Error, Install};
225///
226/// # fn main() -> Result<(), alpm_types::Error> {
227/// // Create Install from &str and format it
228/// assert_eq!(
229///     "scripts/setup.install",
230///     Install::from_str("scripts/setup.install")?.to_string()
231/// );
232/// # Ok(())
233/// # }
234pub type Install = RelativePath;
235
236/// The relative path to a changelog file that may be included in a package
237///
238/// This is a type alias for [`RelativePath`]
239///
240/// ## Examples
241/// ```
242/// use std::str::FromStr;
243///
244/// use alpm_types::{Error, Changelog};
245///
246/// # fn main() -> Result<(), alpm_types::Error> {
247/// // Create Changelog from &str and format it
248/// assert_eq!(
249///     "changelog.md",
250///     Changelog::from_str("changelog.md")?.to_string()
251/// );
252/// # Ok(())
253/// # }
254pub type Changelog = RelativePath;
255
256/// A lookup directory for shared object files.
257///
258/// Follows the [alpm-sonamev2] format, which encodes a `prefix` and a `directory`.
259/// The same `prefix` is later used to identify the location of a **soname**, see
260/// [`SonameV2`][crate::SonameV2].
261///
262/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
263#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
264pub struct SonameLookupDirectory {
265    /// The lookup prefix for shared objects.
266    pub prefix: SharedLibraryPrefix,
267    /// The directory to look for shared objects in.
268    pub directory: AbsolutePath,
269}
270
271impl SonameLookupDirectory {
272    /// Creates a new lookup directory with a prefix and a directory.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use alpm_types::SonameLookupDirectory;
278    ///
279    /// # fn main() -> Result<(), alpm_types::Error> {
280    /// SonameLookupDirectory::new("lib".parse()?, "/usr/lib".parse()?);
281    /// # Ok(())
282    /// # }
283    /// ```
284    pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
285        Self { prefix, directory }
286    }
287
288    /// Parses a [`SonameLookupDirectory`] from a string slice.
289    ///
290    /// Consumes all of its input.
291    ///
292    /// See [`SonameLookupDirectory::from_str`] for more details.
293    pub fn parser(input: &mut &str) -> ModalResult<Self> {
294        // Parse until the first `:`, which separates the prefix from the directory.
295        let prefix = cut_err(
296            repeat_till(1.., any, peek(alt((":", eof))))
297                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name)),
298        )
299        .context(StrContext::Label("prefix for a shared object lookup path"))
300        .parse_next(input)?;
301
302        // Take the delimiter.
303        cut_err(":")
304            .context(StrContext::Label("shared library prefix delimiter"))
305            .context(StrContext::Expected(StrContextValue::Description(
306                "shared library prefix `:`",
307            )))
308            .parse_next(input)?;
309
310        // Parse the rest as a directory.
311        let directory = rest
312            .verify(|s: &str| !s.is_empty())
313            .try_map(AbsolutePath::from_str)
314            .context(StrContext::Label("directory"))
315            .context(StrContext::Expected(StrContextValue::Description(
316                "directory for a shared object lookup path",
317            )))
318            .parse_next(input)?;
319
320        Ok(Self { prefix, directory })
321    }
322}
323
324impl Display for SonameLookupDirectory {
325    /// Converts the [`SonameLookupDirectory`] to a string.
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        write!(f, "{}:{}", self.prefix, self.directory)
328    }
329}
330
331impl FromStr for SonameLookupDirectory {
332    type Err = Error;
333
334    /// Creates a [`SonameLookupDirectory`] from a string slice.
335    ///
336    /// # Errors
337    ///
338    /// Returns an error if `input` can not be converted into a [`SonameLookupDirectory`].
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// use std::str::FromStr;
344    ///
345    /// use alpm_types::SonameLookupDirectory;
346    ///
347    /// # fn main() -> Result<(), alpm_types::Error> {
348    /// let dir = SonameLookupDirectory::from_str("lib:/usr/lib")?;
349    /// assert_eq!(dir.to_string(), "lib:/usr/lib");
350    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
351    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
352    /// assert!(SonameLookupDirectory::from_str("lib:").is_err());
353    /// # Ok(())
354    /// # }
355    /// ```
356    fn from_str(s: &str) -> Result<Self, Self::Err> {
357        Ok(Self::parser.parse(s)?)
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use rstest::rstest;
364    use testresult::TestResult;
365
366    use super::*;
367
368    #[rstest]
369    #[case("/home", BuildDirectory::new(PathBuf::from("/home")))]
370    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
371    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
372    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
373    fn build_dir_from_string(#[case] s: &str, #[case] result: Result<BuildDirectory, Error>) {
374        assert_eq!(BuildDirectory::from_str(s), result);
375    }
376
377    #[rstest]
378    #[case("/start", StartDirectory::new(PathBuf::from("/start")))]
379    #[case("./", Err(Error::PathNotAbsolute(PathBuf::from("./"))))]
380    #[case("~/", Err(Error::PathNotAbsolute(PathBuf::from("~/"))))]
381    #[case("foo.txt", Err(Error::PathNotAbsolute(PathBuf::from("foo.txt"))))]
382    fn startdir_from_str(#[case] s: &str, #[case] result: Result<StartDirectory, Error>) {
383        assert_eq!(StartDirectory::from_str(s), result);
384    }
385
386    #[rstest]
387    #[case("etc/test.conf", RelativePath::new(PathBuf::from("etc/test.conf")))]
388    #[case(
389        "/etc/test.conf",
390        Err(Error::PathNotRelative(PathBuf::from("/etc/test.conf")))
391    )]
392    #[case("etc/", Err(Error::PathNotRelative(PathBuf::from("etc/"))))]
393    #[case("etc", RelativePath::new(PathBuf::from("etc")))]
394    #[case(
395        "../etc/test.conf",
396        RelativePath::new(PathBuf::from("../etc/test.conf"))
397    )]
398    fn relative_path_from_str(#[case] s: &str, #[case] result: Result<RelativePath, Error>) {
399        assert_eq!(RelativePath::from_str(s), result);
400    }
401
402    #[rstest]
403    #[case("lib:/usr/lib", SonameLookupDirectory {
404        prefix: "lib".parse()?,
405        directory: AbsolutePath::from_str("/usr/lib")?,
406    })]
407    #[case("lib32:/usr/lib32", SonameLookupDirectory {
408        prefix: "lib32".parse()?,
409        directory: AbsolutePath::from_str("/usr/lib32")?,
410    })]
411    fn soname_lookup_directory_from_string(
412        #[case] input: &str,
413        #[case] expected_result: SonameLookupDirectory,
414    ) -> TestResult {
415        let lookup_directory = SonameLookupDirectory::from_str(input)?;
416        assert_eq!(expected_result, lookup_directory);
417        assert_eq!(input, lookup_directory.to_string());
418        Ok(())
419    }
420
421    #[rstest]
422    #[case("lib", "invalid shared library prefix delimiter")]
423    #[case("lib:", "invalid directory")]
424    #[case(":/usr/lib", "invalid first character of package name")]
425    fn invalid_soname_lookup_directory_parser(#[case] input: &str, #[case] error_snippet: &str) {
426        let result = SonameLookupDirectory::from_str(input);
427        assert!(result.is_err(), "Expected LookupDirectory parsing to fail");
428        let err = result.unwrap_err();
429        let pretty_error = err.to_string();
430        assert!(
431            pretty_error.contains(error_snippet),
432            "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
433        );
434    }
435}