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}