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