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#[derive(Clone, Debug, Eq, PartialEq)]
49#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
50pub struct AbsolutePath(PathBuf);
51
52impl AbsolutePath {
53 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 pub fn inner(&self) -> &Path {
63 &self.0
64 }
65}
66
67impl FromStr for AbsolutePath {
68 type Err = Error;
69
70 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
89pub type BuildDirectory = AbsolutePath;
108
109pub type StartDirectory = AbsolutePath;
128
129#[derive(Clone, Debug, Eq, Hash, PartialEq)]
162#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
163pub struct RelativePath(PathBuf);
164
165impl RelativePath {
166 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 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 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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
237#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
238pub struct RelativeFilePath(PathBuf);
239
240impl RelativeFilePath {
241 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 pub fn inner(&self) -> &Path {
257 &self.0
258 }
259}
260
261impl FromStr for RelativeFilePath {
262 type Err = Error;
263
264 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
280pub type Backup = RelativeFilePath;
299
300pub type Install = RelativeFilePath;
319
320pub type Changelog = RelativeFilePath;
339
340#[derive(Clone, Debug, Eq, PartialEq)]
348#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
349pub struct SonameLookupDirectory {
350 pub prefix: SharedLibraryPrefix,
352 pub directory: AbsolutePath,
354}
355
356impl SonameLookupDirectory {
357 pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
370 Self { prefix, directory }
371 }
372}
373
374impl ParserUntil for SonameLookupDirectory {
375 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 let mut delimiter_parser = delimiter;
388 move |input: &mut &'a str| -> ModalResult<Self> {
389 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 ":".context(StrContext::Label("shared library prefix delimiter"))
397 .context(StrContext::Expected(StrContextValue::Description(
398 "shared library prefix `:`",
399 )))
400 .parse_next(input)?;
401
402 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 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 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 #[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 #[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 #[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}