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}