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