Skip to main content

alpm_types/
path.rs

1use std::{
2    fmt::{Display, Formatter},
3    path::{MAIN_SEPARATOR_STR, Path, PathBuf},
4    str::FromStr,
5};
6
7use alpm_parsers::prelude::*;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10#[cfg(feature = "serde")]
11use serde_with::DeserializeFromStr;
12use winnow::{
13    combinator::{alt, eof, peek, repeat_till},
14    error::ErrMode,
15    token::any,
16};
17
18use crate::{Error, SharedLibraryPrefix};
19
20/// A representation of an absolute path
21///
22/// AbsolutePath wraps a `PathBuf`, that is guaranteed to be absolute.
23///
24/// ## Examples
25/// ```
26/// use std::{path::PathBuf, str::FromStr};
27///
28/// use alpm_types::{AbsolutePath, Error};
29///
30/// # fn main() -> Result<(), alpm_types::Error> {
31/// // Create AbsolutePath from &str
32/// assert_eq!(
33///     AbsolutePath::from_str("/")?,
34///     AbsolutePath::new(PathBuf::from("/"))?
35/// );
36/// assert!(AbsolutePath::from_str("./").is_err());
37///
38/// // Format as String
39/// assert_eq!("/", AbsolutePath::from_str("/")?.to_string());
40/// # Ok(())
41/// # }
42/// ```
43#[derive(Clone, Debug, Eq, PartialEq)]
44#[cfg_attr(feature = "serde", derive(DeserializeFromStr, 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
57impl AsRef<Path> for AbsolutePath {
58    /// Returns a reference to the inner [`Path`].
59    fn as_ref(&self) -> &Path {
60        &self.0
61    }
62}
63
64impl FromStr for AbsolutePath {
65    type Err = Error;
66
67    /// Parses an absolute path from a string
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the path is not absolute
72    fn from_str(s: &str) -> Result<AbsolutePath, Self::Err> {
73        match Path::new(s).is_absolute() {
74            true => Ok(AbsolutePath(PathBuf::from(s))),
75            false => Err(Error::PathNotAbsolute(PathBuf::from(s))),
76        }
77    }
78}
79
80impl Display for AbsolutePath {
81    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
82        write!(fmt, "{}", self.as_ref().display())
83    }
84}
85
86/// An absolute path used as build directory
87///
88/// This is a type alias for [`AbsolutePath`]
89///
90/// ## Examples
91/// ```
92/// use std::str::FromStr;
93///
94/// use alpm_types::{Error, BuildDirectory};
95///
96/// # fn main() -> Result<(), alpm_types::Error> {
97/// // Create BuildDirectory from &str and format it
98/// assert_eq!(
99///     "/etc",
100///     BuildDirectory::from_str("/etc")?.to_string()
101/// );
102/// # Ok(())
103/// # }
104pub type BuildDirectory = AbsolutePath;
105
106/// An absolute path used as start directory in a package build environment
107///
108/// This is a type alias for [`AbsolutePath`]
109///
110/// ## Examples
111/// ```
112/// use std::str::FromStr;
113///
114/// use alpm_types::{Error, StartDirectory};
115///
116/// # fn main() -> Result<(), alpm_types::Error> {
117/// // Create StartDirectory from &str and format it
118/// assert_eq!(
119///     "/etc",
120///     StartDirectory::from_str("/etc")?.to_string()
121/// );
122/// # Ok(())
123/// # }
124pub type StartDirectory = AbsolutePath;
125
126/// A representation of a relative path
127///
128/// [`RelativePath`] wraps a [`PathBuf`] that is guaranteed to represent a relative path, regardless
129/// of whether it points to a file or a directory.
130///
131/// ## Examples
132///
133/// ```
134/// use std::{path::PathBuf, str::FromStr};
135///
136/// use alpm_types::{Error, RelativePath};
137///
138/// # fn main() -> Result<(), alpm_types::Error> {
139/// // Create RelativePath from &str
140/// assert_eq!(
141///     RelativePath::from_str("etc/test.conf")?,
142///     RelativePath::new(PathBuf::from("etc/test.conf"))?
143/// );
144/// assert_eq!(
145///     RelativePath::from_str("etc/")?,
146///     RelativePath::new(PathBuf::from("etc/"))?
147/// );
148/// assert!(RelativePath::from_str("/etc/test.conf").is_err());
149///
150/// // Format as String
151/// assert_eq!("test/", RelativePath::from_str("test/")?.to_string());
152/// # Ok(())
153/// # }
154/// ```
155#[derive(Clone, Debug, Eq, Hash, PartialEq)]
156#[cfg_attr(feature = "serde", derive(DeserializeFromStr, 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!(RelativeFilePath::from_str("/etc/test.conf").is_err());
218///
219/// // Format as String
220/// assert_eq!(
221///     "test/test.txt",
222///     RelativeFilePath::from_str("test/test.txt")?.to_string()
223/// );
224/// # Ok(())
225/// # }
226/// ```
227#[derive(Clone, Debug, Eq, Hash, PartialEq)]
228#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
229pub struct RelativeFilePath(PathBuf);
230
231impl RelativeFilePath {
232    /// Create a new `RelativeFilePath`
233    pub fn new(path: PathBuf) -> Result<RelativeFilePath, Error> {
234        if path.to_string_lossy().ends_with(MAIN_SEPARATOR_STR) {
235            return Err(Error::PathIsNotAFile(path));
236        }
237        if !path.is_relative() {
238            return Err(Error::PathNotRelative(path));
239        }
240        Ok(RelativeFilePath(path))
241    }
242}
243
244impl AsRef<Path> for RelativeFilePath {
245    /// Returns a reference to the inner [`Path`].
246    fn as_ref(&self) -> &Path {
247        &self.0
248    }
249}
250
251impl FromStr for RelativeFilePath {
252    type Err = Error;
253
254    /// Parses a relative path from a string
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the path is not relative
259    fn from_str(s: &str) -> Result<RelativeFilePath, Self::Err> {
260        Self::new(PathBuf::from(s))
261    }
262}
263
264impl Display for RelativeFilePath {
265    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
266        write!(fmt, "{}", self.as_ref().display())
267    }
268}
269
270/// The basename of a path.
271///
272/// Wraps a [`PathBuf`] that is guaranteed to represent a relative
273/// local (one level deep) file path (i.e. it does not contain a `/`).
274///
275/// ## Examples
276///
277/// ```
278/// use std::{path::PathBuf, str::FromStr};
279///
280/// use alpm_types::{Basename, Error};
281///
282/// # fn main() -> Result<(), alpm_types::Error> {
283/// // Create Basename from &str
284/// assert_eq!(
285///     Basename::from_str("test.conf")?,
286///     Basename::new(PathBuf::from("test.conf"))?
287/// );
288/// assert!(Basename::from_str("etc/test.conf").is_err(),);
289///
290/// // Format as String
291/// assert_eq!("test.txt", Basename::from_str("test.txt")?.to_string());
292/// # Ok(())
293/// # }
294/// ```
295#[derive(Clone, Debug, Eq, Hash, PartialEq)]
296#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
297pub struct Basename(PathBuf);
298
299impl Basename {
300    /// Create a new [`Basename`].
301    ///
302    /// # Errors
303    ///
304    /// Returns an error, if `path`
305    ///
306    /// - ends with a [`MAIN_SEPARATOR_STR`]
307    /// - is relative
308    /// - has more than one component
309    pub fn new(path: PathBuf) -> Result<Self, Error> {
310        if path.to_string_lossy().ends_with(MAIN_SEPARATOR_STR) {
311            return Err(Error::PathIsNotAFile(path));
312        }
313        if !path.is_relative() {
314            return Err(Error::PathNotSibling(path));
315        }
316        // Make sure the path only consists of a single filename.
317        if path.iter().count() != 1 {
318            return Err(Error::PathNotSibling(path));
319        }
320
321        Ok(Self(path))
322    }
323}
324
325impl AsRef<Path> for Basename {
326    /// Returns a reference to the inner [`Path`].
327    fn as_ref(&self) -> &Path {
328        &self.0
329    }
330}
331
332impl FromStr for Basename {
333    type Err = Error;
334
335    /// Parses a sibling file path from a string
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if the path is not relative or contains a directory separator.
340    fn from_str(s: &str) -> Result<Basename, Self::Err> {
341        Self::new(PathBuf::from(s))
342    }
343}
344
345impl Display for Basename {
346    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
347        write!(fmt, "{}", self.as_ref().display())
348    }
349}
350
351/// The path of a packaged file that should be preserved during package operations
352///
353/// This is a type alias for [`RelativeFilePath`]
354///
355/// ## Examples
356/// ```
357/// use std::str::FromStr;
358///
359/// use alpm_types::Backup;
360///
361/// # fn main() -> Result<(), alpm_types::Error> {
362/// // Create Backup from &str and format it
363/// assert_eq!(
364///     "etc/test.conf",
365///     Backup::from_str("etc/test.conf")?.to_string()
366/// );
367/// # Ok(())
368/// # }
369pub type Backup = RelativeFilePath;
370
371/// A special install script that is to be included in the package
372///
373/// This is a type alias for [RelativeFilePath`]
374///
375/// ## Examples
376/// ```
377/// use std::str::FromStr;
378///
379/// use alpm_types::{Error, Install};
380///
381/// # fn main() -> Result<(), alpm_types::Error> {
382/// // Create Install from &str and format it
383/// assert_eq!(
384///     "scripts/setup.install",
385///     Install::from_str("scripts/setup.install")?.to_string()
386/// );
387/// # Ok(())
388/// # }
389pub type Install = RelativeFilePath;
390
391/// The relative path to a changelog file that may be included in a package
392///
393/// This is a type alias for [`RelativeFilePath`]
394///
395/// ## Examples
396/// ```
397/// use std::str::FromStr;
398///
399/// use alpm_types::{Error, Changelog};
400///
401/// # fn main() -> Result<(), alpm_types::Error> {
402/// // Create Changelog from &str and format it
403/// assert_eq!(
404///     "changelog.md",
405///     Changelog::from_str("changelog.md")?.to_string()
406/// );
407/// # Ok(())
408/// # }
409pub type Changelog = RelativeFilePath;
410
411/// A lookup directory for shared object files.
412///
413/// Follows the [alpm-sonamev2] format, which encodes a `prefix` and a `directory`.
414/// The same `prefix` is later used to identify the location of a **soname**, see
415/// [`SonameV2`][crate::SonameV2].
416///
417/// [alpm-sonamev2]: https://alpm.archlinux.page/specifications/alpm-sonamev2.7.html
418#[derive(Clone, Debug, Eq, PartialEq)]
419#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
420pub struct SonameLookupDirectory {
421    /// The lookup prefix for shared objects.
422    pub prefix: SharedLibraryPrefix,
423    /// The directory to look for shared objects in.
424    pub directory: AbsolutePath,
425}
426
427impl SonameLookupDirectory {
428    /// Creates a new lookup directory with a prefix and a directory.
429    ///
430    /// # Examples
431    ///
432    /// ```
433    /// use alpm_types::SonameLookupDirectory;
434    ///
435    /// # fn main() -> Result<(), alpm_types::Error> {
436    /// SonameLookupDirectory::new("lib".parse()?, "/usr/lib".parse()?);
437    /// # Ok(())
438    /// # }
439    /// ```
440    pub fn new(prefix: SharedLibraryPrefix, directory: AbsolutePath) -> Self {
441        Self { prefix, directory }
442    }
443}
444
445impl ParserUntil for SonameLookupDirectory {
446    /// Parses a [`SonameLookupDirectory`] from a string slice.
447    ///
448    /// # Errors
449    ///
450    /// Returns an error, if the parser input does not contain a valid [`SonameLookupDirectory`]
451    /// before the `delimiter`.
452    fn parser_until<'a, P>(delimiter: P) -> impl Parser<Input<'a>, Self, ErrMode<ParseStack<'a>>>
453    where
454        P: Parser<Input<'a>, &'a str, ErrMode<ParseStack<'a>>>,
455    {
456        // Define the actual parser closure.
457        // The delimiter is moved into the closure and borrowed via `by_ref()` on each call.
458        let mut delimiter_parser = delimiter;
459        let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
460            // Parse until the first `:`, which separates the prefix from the directory.
461            let prefix = repeat_till(1.., any, peek(alt((":", eof))))
462                .try_map(|(name, _): (String, &str)| SharedLibraryPrefix::from_str(&name))
463                .label("prefix for a shared object lookup path")
464                .parse_next(input)?;
465
466            // Take the delimiter.
467            ":".label("delimiter")
468                .expected_text("shared library delimiter `:`")
469                .parse_next(input)?;
470
471            // Parse the rest as a directory.
472            let directory = repeat_till(1.., any, peek(delimiter_parser.by_ref()))
473                .try_map(|(path, _): (String, &str)| AbsolutePath::from_str(&path))
474                .label("directory")
475                .expected_text("directory for a shared object lookup path")
476                .parse_next(input)?;
477
478            peek(delimiter_parser.by_ref())
479                .label("SonameLookupDirectory")
480                .expected_text("valid end of input.")
481                .parse_next(input)?;
482
483            Ok(Self { prefix, directory })
484        };
485
486        parser.layer("soname lookup directory")
487    }
488}
489
490impl Display for SonameLookupDirectory {
491    /// Converts the [`SonameLookupDirectory`] to a string.
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        write!(f, "{}:{}", self.prefix, self.directory)
494    }
495}
496
497impl FromStr for SonameLookupDirectory {
498    type Err = Error;
499
500    /// Creates a [`SonameLookupDirectory`] from a string slice.
501    ///
502    /// Delegates to [`SonameLookupDirectory::parser_until`].
503    ///
504    /// # Errors
505    ///
506    /// Returns an error if [`SonameLookupDirectory::parser_until`] fails.
507    ///
508    /// # Examples
509    ///
510    /// ```
511    /// use std::str::FromStr;
512    ///
513    /// use alpm_types::SonameLookupDirectory;
514    ///
515    /// # fn main() -> Result<(), alpm_types::Error> {
516    /// let dir = SonameLookupDirectory::from_str("lib:/usr/lib")?;
517    /// assert_eq!(dir.to_string(), "lib:/usr/lib");
518    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
519    /// assert!(SonameLookupDirectory::from_str(":/usr/lib").is_err());
520    /// assert!(SonameLookupDirectory::from_str("lib:").is_err());
521    /// # Ok(())
522    /// # }
523    /// ```
524    fn from_str(s: &str) -> Result<Self, Self::Err> {
525        Ok(Self::parser_until_eof.parse(Input::new(s))?)
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use insta::assert_snapshot;
532    use rstest::rstest;
533    use testresult::TestResult;
534
535    use super::*;
536    use crate::configure_insta;
537
538    /// Ensures, that [`BuildDirectory::from_str`] succeeds on valid input.
539    #[test]
540    fn build_dir_from_str_succeeds() -> TestResult {
541        let _ = BuildDirectory::from_str("/home")?;
542
543        Ok(())
544    }
545
546    /// Ensures, that [`BuildDirectory::from_str`] fails on invalid input.
547    #[rstest]
548    #[case("./")]
549    #[case("~/")]
550    #[case("foo.txt")]
551    fn build_dir_from_str_fails_on_invalid_input(#[case] s: &str) -> TestResult {
552        let Err(Error::PathNotAbsolute(failing_path)) = BuildDirectory::from_str(s) else {
553            panic!("Expected to fail with Error::PathNotAbsolute");
554        };
555
556        assert_eq!(PathBuf::from(s), failing_path);
557
558        Ok(())
559    }
560
561    /// Make sure that relative paths don't deserialize as absolute paths.
562    #[cfg(feature = "serde")]
563    #[rstest]
564    #[case::relative_path("./")]
565    #[case::relative_file("foo.txt")]
566    fn absolute_path_deserialize_error(#[case] input: &str) {
567        let Err(serde_json::Error { .. }) =
568            serde_json::from_str::<AbsolutePath>(&format!("\"{input}\""))
569        else {
570            panic!("'{input}' erroneously deserialized as an AbsolutePath")
571        };
572    }
573
574    /// Ensures, that [`StartDirectory::from_str`] succeeds on valid input.
575    #[test]
576    fn startdir_from_str_succeeds() -> TestResult {
577        let _ = StartDirectory::from_str("/start")?;
578
579        Ok(())
580    }
581
582    /// Ensures, that [`StartDirectory::from_str`] fails on invalid input.
583    #[rstest]
584    #[case("./")]
585    #[case("~/")]
586    #[case("foo.txt")]
587    fn startdir_from_str_fails_on_invalid_input(#[case] s: &str) -> TestResult {
588        let Err(Error::PathNotAbsolute(failing_path)) = StartDirectory::from_str(s) else {
589            panic!("Expected to fail with Error::PathNotAbsolute");
590        };
591
592        assert_eq!(PathBuf::from(s), failing_path);
593
594        Ok(())
595    }
596
597    /// Ensures, that [`RelativePath::from_str`] succeeds on valid input.
598    #[rstest]
599    #[case("etc/test.conf", RelativePath::new(PathBuf::from("etc/test.conf"))?)]
600    #[case("etc/", RelativePath::new(PathBuf::from("etc/"))?)]
601    #[case(
602        "../etc/test.conf",
603        RelativePath::new(PathBuf::from("../etc/test.conf"))?
604    )]
605    fn relative_path_from_str(#[case] s: &str, #[case] expected: RelativePath) -> TestResult {
606        assert_eq!(RelativePath::from_str(s)?, expected);
607
608        Ok(())
609    }
610
611    /// Ensures, that [`RelativePath::from_str`] fails on invalid input.
612    #[test]
613    fn relative_path_from_str_fails_on_invalid_input() {
614        let Err(Error::PathNotRelative(invalid_path)) = RelativePath::from_str("/etc/test.conf")
615        else {
616            panic!("Expected to fail with Error::PathNotRelative");
617        };
618        assert_eq!(PathBuf::from("/etc/test.conf"), invalid_path);
619    }
620
621    /// Make sure that absolute paths don't deserialize as relative paths.
622    #[cfg(feature = "serde")]
623    #[rstest]
624    #[case::root("/")]
625    #[case::absolute_path("/etc")]
626    fn relative_path_deserialize_error(#[case] input: &str) {
627        let Err(serde_json::Error { .. }) =
628            serde_json::from_str::<RelativePath>(&format!("\"{input}\""))
629        else {
630            panic!("'{input}' erroneously deserialized as a RelativePath")
631        };
632    }
633
634    /// Ensures, that [`RelativeFilePath::from_str`] succeeds on valid input.
635    #[rstest]
636    #[case("etc/test.conf", RelativeFilePath::new(PathBuf::from("etc/test.conf"))?)]
637    #[case("etc", RelativeFilePath::new(PathBuf::from("etc"))?)]
638    #[case(
639        "../etc/test.conf",
640        RelativeFilePath::new(PathBuf::from("../etc/test.conf"))?
641    )]
642    fn relative_file_path_from_str_succeeds(
643        #[case] s: &str,
644        #[case] expected: RelativeFilePath,
645    ) -> TestResult {
646        assert_eq!(RelativeFilePath::from_str(s)?, expected);
647
648        Ok(())
649    }
650
651    /// Ensures, that [`RelativeFilePath::from_str`] fails on invalid input.
652    #[test]
653    fn relative_file_path_from_str_fails_on_not_relative() {
654        let input = "/etc/test.conf";
655        let Err(Error::PathNotRelative(returned_path)) = RelativeFilePath::from_str(input) else {
656            panic!("Expected to fail with Error::PathNotRelative");
657        };
658
659        assert_eq!(returned_path, PathBuf::from(input));
660    }
661
662    /// Ensures, that [`RelativeFilePath::from_str`] fails on invalid input.
663    #[test]
664    fn relative_file_path_from_str_fails_on_not_a_file() {
665        let input = "etc/";
666        let Err(Error::PathIsNotAFile(returned_path)) = RelativeFilePath::from_str(input) else {
667            panic!("Expected to fail with Error::PathIsNotAFile")
668        };
669        assert_eq!(returned_path, PathBuf::from(input));
670    }
671
672    /// Make sure that invalid relative file paths don't deserialize.
673    #[cfg(feature = "serde")]
674    #[rstest]
675    #[case::not_a_file("etc/")]
676    #[case::absolute_path("/etc/test.conf")]
677    fn relative_file_path_deserialize_error(#[case] input: &str) {
678        let Err(serde_json::Error { .. }) =
679            serde_json::from_str::<RelativeFilePath>(&format!("\"{input}\""))
680        else {
681            panic!("'{input}' erroneously deserialized as a RelativeFilePath")
682        };
683    }
684
685    #[rstest]
686    #[case("lib:/usr/lib", SonameLookupDirectory {
687        prefix: "lib".parse()?,
688        directory: AbsolutePath::from_str("/usr/lib")?,
689    })]
690    #[case("lib32:/usr/lib32", SonameLookupDirectory {
691        prefix: "lib32".parse()?,
692        directory: AbsolutePath::from_str("/usr/lib32")?,
693    })]
694    fn soname_lookup_directory_from_string(
695        #[case] input: &str,
696        #[case] expected_result: SonameLookupDirectory,
697    ) -> TestResult {
698        let lookup_directory = SonameLookupDirectory::from_str(input)?;
699        assert_eq!(expected_result, lookup_directory);
700        assert_eq!(input, lookup_directory.to_string());
701        Ok(())
702    }
703
704    #[rstest]
705    #[case("lib")]
706    #[case("lib:")]
707    #[case(":/usr/lib")]
708    fn invalid_soname_lookup_directory_parser(#[case] input: &str) {
709        let (test_name, _guard) = configure_insta();
710
711        let Err(Error::ParseError(err_msg)) = SonameLookupDirectory::from_str(input) else {
712            panic!("'{input}' erroneously parsed as a SonameLookupDirectory")
713        };
714
715        assert_snapshot!(test_name, err_msg.to_string());
716    }
717}