alpm_types/package/file_name.rs
1//! Package filename handling.
2
3use std::{
4 fmt::Display,
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8
9use alpm_parsers::iter_str_context;
10use serde::{Deserialize, Serialize};
11use strum::VariantNames;
12use winnow::{
13 ModalResult,
14 Parser,
15 ascii::alphanumeric1,
16 combinator::{cut_err, eof, opt, peek, preceded, repeat},
17 error::{AddContext, ContextError, ErrMode, ParserError, StrContext, StrContextValue},
18 stream::Stream,
19 token::take_until,
20};
21
22use crate::{
23 Architecture,
24 CompressionAlgorithmFileExtension,
25 FileTypeIdentifier,
26 FullVersion,
27 Name,
28 PackageError,
29};
30
31/// The full filename of a package.
32///
33/// A package filename tracks its [`Name`], [`FullVersion`], [`Architecture`] and the optional
34/// [`CompressionAlgorithmFileExtension`].
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(into = "String")]
37#[serde(try_from = "String")]
38pub struct PackageFileName {
39 pub(crate) name: Name,
40 pub(crate) version: FullVersion,
41 pub(crate) architecture: Architecture,
42 pub(crate) compression: Option<CompressionAlgorithmFileExtension>,
43}
44
45impl PackageFileName {
46 /// Creates a new [`PackageFileName`].
47 ///
48 /// # Errors
49 ///
50 /// Returns an error if the provided `version` does not have the `pkgrel` component.
51 ///
52 /// # Examples
53 ///
54 /// ```
55 /// use std::str::FromStr;
56 ///
57 /// use alpm_types::PackageFileName;
58 ///
59 /// # fn main() -> Result<(), alpm_types::Error> {
60 /// assert_eq!(
61 /// "example-1:1.0.0-1-x86_64.pkg.tar.zst",
62 /// PackageFileName::new(
63 /// "example".parse()?,
64 /// "1:1.0.0-1".parse()?,
65 /// "x86_64".parse()?,
66 /// Some("zst".parse()?)
67 /// )
68 /// .to_string()
69 /// );
70 /// # Ok(())
71 /// # }
72 /// ```
73 pub fn new(
74 name: Name,
75 version: FullVersion,
76 architecture: Architecture,
77 compression: Option<CompressionAlgorithmFileExtension>,
78 ) -> Self {
79 Self {
80 name,
81 version,
82 architecture,
83 compression,
84 }
85 }
86
87 /// Returns a reference to the [`Name`].
88 ///
89 /// # Examples
90 ///
91 /// ```
92 /// use std::str::FromStr;
93 ///
94 /// use alpm_types::{Name, PackageFileName};
95 ///
96 /// # fn main() -> Result<(), alpm_types::Error> {
97 /// let file_name = PackageFileName::new(
98 /// "example".parse()?,
99 /// "1:1.0.0-1".parse()?,
100 /// "x86_64".parse()?,
101 /// Some("zst".parse()?),
102 /// );
103 ///
104 /// assert_eq!(file_name.name(), &Name::new("example")?);
105 /// # Ok(())
106 /// # }
107 /// ```
108 pub fn name(&self) -> &Name {
109 &self.name
110 }
111
112 /// Returns a reference to the [`FullVersion`].
113 ///
114 /// # Examples
115 ///
116 /// ```
117 /// use std::str::FromStr;
118 ///
119 /// use alpm_types::{FullVersion, PackageFileName};
120 ///
121 /// # fn main() -> Result<(), alpm_types::Error> {
122 /// let file_name = PackageFileName::new(
123 /// "example".parse()?,
124 /// "1:1.0.0-1".parse()?,
125 /// "x86_64".parse()?,
126 /// Some("zst".parse()?),
127 /// );
128 ///
129 /// assert_eq!(file_name.version(), &FullVersion::from_str("1:1.0.0-1")?);
130 /// # Ok(())
131 /// # }
132 /// ```
133 pub fn version(&self) -> &FullVersion {
134 &self.version
135 }
136
137 /// Returns the [`Architecture`].
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use std::str::FromStr;
143 ///
144 /// use alpm_types::{PackageFileName, SystemArchitecture};
145 ///
146 /// # fn main() -> Result<(), alpm_types::Error> {
147 /// let file_name = PackageFileName::new(
148 /// "example".parse()?,
149 /// "1:1.0.0-1".parse()?,
150 /// "x86_64".parse()?,
151 /// Some("zst".parse()?),
152 /// );
153 ///
154 /// assert_eq!(file_name.architecture(), &SystemArchitecture::X86_64.into());
155 /// # Ok(())
156 /// # }
157 /// ```
158 pub fn architecture(&self) -> &Architecture {
159 &self.architecture
160 }
161
162 /// Returns the optional [`CompressionAlgorithmFileExtension`].
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// use std::str::FromStr;
168 ///
169 /// use alpm_types::{CompressionAlgorithmFileExtension, PackageFileName};
170 ///
171 /// # fn main() -> Result<(), alpm_types::Error> {
172 /// let file_name = PackageFileName::new(
173 /// "example".parse()?,
174 /// "1:1.0.0-1".parse()?,
175 /// "x86_64".parse()?,
176 /// Some("zst".parse()?),
177 /// );
178 ///
179 /// assert_eq!(
180 /// file_name.compression(),
181 /// Some(CompressionAlgorithmFileExtension::Zstd)
182 /// );
183 /// # Ok(())
184 /// # }
185 /// ```
186 pub fn compression(&self) -> Option<CompressionAlgorithmFileExtension> {
187 self.compression
188 }
189
190 /// Returns the [`PackageFileName`] as [`PathBuf`].
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// use std::{path::PathBuf, str::FromStr};
196 ///
197 /// use alpm_types::PackageFileName;
198 ///
199 /// # fn main() -> Result<(), alpm_types::Error> {
200 /// let file_name = PackageFileName::new(
201 /// "example".parse()?,
202 /// "1:1.0.0-1".parse()?,
203 /// "x86_64".parse()?,
204 /// Some("zst".parse()?),
205 /// );
206 ///
207 /// assert_eq!(
208 /// file_name.to_path_buf(),
209 /// PathBuf::from("example-1:1.0.0-1-x86_64.pkg.tar.zst")
210 /// );
211 /// # Ok(())
212 /// # }
213 /// ```
214 pub fn to_path_buf(&self) -> PathBuf {
215 self.to_string().into()
216 }
217
218 /// Sets the compression of the [`PackageFileName`].
219 ///
220 /// # Examples
221 ///
222 /// ```
223 /// use std::str::FromStr;
224 ///
225 /// use alpm_types::{CompressionAlgorithmFileExtension, PackageFileName};
226 ///
227 /// # fn main() -> Result<(), alpm_types::Error> {
228 /// // Create package file name with compression
229 /// let mut file_name = PackageFileName::new(
230 /// "example".parse()?,
231 /// "1:1.0.0-1".parse()?,
232 /// "x86_64".parse()?,
233 /// Some("zst".parse()?),
234 /// );
235 /// // Remove the compression
236 /// file_name.set_compression(None);
237 ///
238 /// assert!(file_name.compression().is_none());
239 ///
240 /// // Add other compression
241 /// file_name.set_compression(Some(CompressionAlgorithmFileExtension::Gzip));
242 ///
243 /// assert!(
244 /// file_name
245 /// .compression()
246 /// .is_some_and(|compression| compression == CompressionAlgorithmFileExtension::Gzip)
247 /// );
248 /// # Ok(())
249 /// # }
250 /// ```
251 pub fn set_compression(&mut self, compression: Option<CompressionAlgorithmFileExtension>) {
252 self.compression = compression
253 }
254
255 /// Recognizes a [`PackageFileName`] in a string slice.
256 ///
257 /// Relies on [`winnow`] to parse `input` and recognize the [`Name`], [`FullVersion`],
258 /// [`Architecture`] and [`CompressionAlgorithmFileExtension`] components.
259 ///
260 /// # Errors
261 ///
262 /// Returns an error if
263 ///
264 /// - the [`Name`] component can not be recognized,
265 /// - the [`FullVersion`] component can not be recognized,
266 /// - the [`Architecture`] component can not be recognized,
267 /// - or the [`CompressionAlgorithmFileExtension`] component can not be recognized.
268 ///
269 /// # Examples
270 ///
271 /// ```
272 /// use alpm_types::PackageFileName;
273 /// use winnow::Parser;
274 ///
275 /// # fn main() -> Result<(), alpm_types::Error> {
276 /// let filename = "example-package-1:1.0.0-1-x86_64.pkg.tar.zst";
277 /// assert_eq!(
278 /// filename,
279 /// PackageFileName::parser.parse(filename)?.to_string()
280 /// );
281 /// # Ok(())
282 /// # }
283 /// ```
284 pub fn parser(input: &mut &str) -> ModalResult<Self> {
285 // Detect the amount of dashes in input and subsequently in the Name component.
286 //
287 // Note: This is a necessary step because dashes are used as delimiters between the
288 // components of the file name and the Name component (an alpm-package-name) can contain
289 // dashes, too.
290 // We know that the minimum amount of dashes in a valid alpm-package file name is
291 // three (one dash between the Name, FullVersion, PackageRelease, and Architecture
292 // component each).
293 // We rely on this fact to determine the amount of dashes in the Name component and
294 // thereby the cut-off point between the Name and the FullVersion component.
295 let dashes: usize = input.chars().filter(|char| char == &'-').count();
296
297 if dashes < 3 {
298 let context_error = ContextError::from_input(input)
299 .add_context(
300 input,
301 &input.checkpoint(),
302 StrContext::Label("alpm-package file name"),
303 )
304 .add_context(
305 input,
306 &input.checkpoint(),
307 StrContext::Expected(StrContextValue::Description(
308 concat!(
309 "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
310 "\nAll components must be delimited with a dash ('-')."
311 )
312 ))
313 );
314
315 return Err(ErrMode::Cut(context_error));
316 }
317
318 // The (zero or more) dashes in the Name component.
319 let dashes_in_name = dashes.saturating_sub(3);
320
321 // Advance the parser to the dash just behind the Name component, based on the amount of
322 // dashes in the Name, e.g.:
323 // "example-package-1:1.0.0-1-x86_64.pkg.tar.zst" -> "-1:1.0.0-1-x86_64.pkg.tar.zst"
324 let name = cut_err(
325 repeat::<_, _, (), _, _>(
326 dashes_in_name + 1,
327 // Advances to the next `-`.
328 // If multiple `-` are present, the `-` that has been previously advanced to will
329 // be consumed in the next itaration via the `opt("-")`. This enables us to go
330 // **up to** the last `-`, while still consuming all `-` in between.
331 (opt("-"), take_until(0.., "-"), peek("-")),
332 )
333 .take()
334 // example-package
335 .and_then(Name::parser),
336 )
337 .context(StrContext::Label("alpm-package-name"))
338 .parse_next(input)?;
339
340 // Consume leading dash in front of FullVersion, e.g.:
341 // "-1:1.0.0-1-x86_64.pkg.tar.zst" -> "1:1.0.0-1-x86_64.pkg.tar.zst"
342 "-".parse_next(input)?;
343
344 // Advance the parser to beyond the FullVersion component (which contains one dash), e.g.:
345 // "1:1.0.0-1-x86_64.pkg.tar.zst" -> "-x86_64.pkg.tar.zst"
346 let version: FullVersion = cut_err((take_until(0.., "-"), "-", take_until(0.., "-")))
347 .context(StrContext::Label("alpm-package-version"))
348 .context(StrContext::Expected(StrContextValue::Description(
349 "an alpm-package-version (full or full with epoch) followed by a `-` and an architecture",
350 )))
351 .take()
352 .and_then(cut_err(FullVersion::parser))
353 .parse_next(input)?;
354
355 // Consume leading dash, e.g.:
356 // "-x86_64.pkg.tar.zst" -> "x86_64.pkg.tar.zst"
357 "-".parse_next(input)?;
358
359 // Advance the parser to beyond the Architecture component, e.g.:
360 // "x86_64.pkg.tar.zst" -> ".pkg.tar.zst"
361 let architecture = take_until(0.., ".")
362 .try_map(Architecture::from_str)
363 .parse_next(input)?;
364
365 // Consume leading dot, e.g.:
366 // ".pkg.tar.zst" -> "pkg.tar.zst"
367 ".".parse_next(input)?;
368
369 // Consume the required alpm-package file type identifier, e.g.:
370 // "pkg.tar.zst" -> ".tar.zst"
371 take_until(0.., ".")
372 .and_then(Into::<&str>::into(FileTypeIdentifier::BinaryPackage))
373 .context(StrContext::Label("alpm-package file type identifier"))
374 .context(StrContext::Expected(StrContextValue::StringLiteral(
375 FileTypeIdentifier::BinaryPackage.into(),
376 )))
377 .parse_next(input)?;
378
379 // Consume leading dot, e.g.:
380 // ".tar.zst" -> "tar.zst"
381 ".".parse_next(input)?;
382
383 // Consume the required tar suffix, e.g.:
384 // "tar.zst" -> ".zst"
385 cut_err("tar")
386 .context(StrContext::Label("tar suffix"))
387 .context(StrContext::Expected(StrContextValue::Description("tar")))
388 .parse_next(input)?;
389
390 // Advance the parser to EOF for the CompressionAlgorithmFileExtension component, e.g.:
391 // ".zst" -> ""
392 // If input is "", we use no compression.
393 let compression = opt(preceded(
394 ".",
395 cut_err(alphanumeric1.try_map(|s| {
396 CompressionAlgorithmFileExtension::from_str(s).map_err(|_source| {
397 crate::Error::UnknownCompressionAlgorithmFileExtension {
398 value: s.to_string(),
399 }
400 })
401 }))
402 .context(StrContext::Label("file extension for compression"))
403 .context_with(iter_str_context!([
404 CompressionAlgorithmFileExtension::VARIANTS
405 ])),
406 ))
407 .parse_next(input)?;
408
409 // Ensure that there are no trailing chars left.
410 eof.context(StrContext::Expected(StrContextValue::Description(
411 "end of package filename",
412 )))
413 .parse_next(input)?;
414
415 Ok(Self {
416 name,
417 version,
418 architecture,
419 compression,
420 })
421 }
422}
423
424impl Display for PackageFileName {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 write!(
427 f,
428 "{}-{}-{}.{}.tar{}",
429 self.name,
430 self.version,
431 self.architecture,
432 FileTypeIdentifier::BinaryPackage,
433 match self.compression {
434 None => "".to_string(),
435 Some(suffix) => format!(".{suffix}"),
436 }
437 )
438 }
439}
440
441impl From<PackageFileName> for String {
442 /// Creates a [`String`] from a [`PackageFileName`].
443 fn from(value: PackageFileName) -> Self {
444 value.to_string()
445 }
446}
447
448impl FromStr for PackageFileName {
449 type Err = crate::Error;
450
451 /// Creates a [`PackageFileName`] from a string slice.
452 ///
453 /// Delegates to [`PackageFileName::parser`].
454 ///
455 /// # Errors
456 ///
457 /// Returns an error if [`PackageFileName::parser`] fails.
458 ///
459 /// # Examples
460 ///
461 /// ```
462 /// use std::str::FromStr;
463 ///
464 /// use alpm_types::PackageFileName;
465 ///
466 /// # fn main() -> Result<(), alpm_types::Error> {
467 /// let filename = "example-package-1:1.0.0-1-x86_64.pkg.tar.zst";
468 /// assert_eq!(filename, PackageFileName::from_str(filename)?.to_string());
469 /// # Ok(())
470 /// # }
471 /// ```
472 fn from_str(s: &str) -> Result<Self, Self::Err> {
473 Ok(Self::parser.parse(s)?)
474 }
475}
476
477impl TryFrom<&Path> for PackageFileName {
478 type Error = crate::Error;
479
480 /// Creates a [`PackageFileName`] from a [`Path`] reference.
481 ///
482 /// The file name in `value` is extracted and, if valid is turned into a string slice.
483 /// The creation of the [`PackageFileName`] is delegated to [`PackageFileName::parser`].
484 ///
485 /// # Errors
486 ///
487 /// Returns an error if
488 ///
489 /// - `value` does not contain a valid file name,
490 /// - `value` can not be turned into a string slice,
491 /// - or [`PackageFileName::parser`] fails.
492 ///
493 /// # Examples
494 ///
495 /// ```
496 /// use std::path::PathBuf;
497 ///
498 /// use alpm_types::PackageFileName;
499 ///
500 /// # fn main() -> Result<(), alpm_types::Error> {
501 /// let filename = PathBuf::from("../example-package-1:1.0.0-1-x86_64.pkg.tar.zst");
502 /// assert_eq!(
503 /// filename,
504 /// PathBuf::from("..").join(PackageFileName::try_from(filename.as_path())?.to_path_buf()),
505 /// );
506 /// # Ok(())
507 /// # }
508 /// ```
509 fn try_from(value: &Path) -> Result<Self, Self::Error> {
510 let Some(name) = value.file_name() else {
511 return Err(PackageError::InvalidPackageFileNamePath {
512 path: value.to_path_buf(),
513 }
514 .into());
515 };
516 let Some(s) = name.to_str() else {
517 return Err(PackageError::InvalidPackageFileNamePath {
518 path: value.to_path_buf(),
519 }
520 .into());
521 };
522 Ok(Self::parser.parse(s)?)
523 }
524}
525
526impl TryFrom<String> for PackageFileName {
527 type Error = crate::Error;
528
529 /// Creates a [`PackageFileName`] from a String.
530 ///
531 /// Delegates to [`PackageFileName::parser`].
532 ///
533 /// # Errors
534 ///
535 /// Returns an error if [`PackageFileName::parser`] fails.
536 ///
537 /// # Examples
538 ///
539 /// ```
540 /// use std::str::FromStr;
541 ///
542 /// use alpm_types::PackageFileName;
543 ///
544 /// # fn main() -> Result<(), alpm_types::Error> {
545 /// let filename = "example-package-1:1.0.0-1-x86_64.pkg.tar.zst".to_string();
546 /// assert_eq!(
547 /// filename.clone(),
548 /// PackageFileName::try_from(filename)?.to_string()
549 /// );
550 /// # Ok(())
551 /// # }
552 /// ```
553 fn try_from(value: String) -> Result<Self, Self::Error> {
554 Ok(Self::parser.parse(&value)?)
555 }
556}
557
558#[cfg(test)]
559mod test {
560 use log::{LevelFilter, debug};
561 use rstest::rstest;
562 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
563 use testresult::TestResult;
564
565 use super::*;
566 use crate::system::SystemArchitecture;
567
568 fn init_logger() -> TestResult {
569 if TermLogger::init(
570 LevelFilter::Info,
571 Config::default(),
572 TerminalMode::Mixed,
573 ColorChoice::Auto,
574 )
575 .is_err()
576 {
577 debug!("Not initializing another logger, as one is initialized already.");
578 }
579
580 Ok(())
581 }
582
583 /// Ensures that common and uncommon cases of package filenames can be created.
584 #[rstest]
585 #[case::name_with_dashes(Name::new("example-package")?, FullVersion::from_str("1.0.0-1")?, SystemArchitecture::X86_64.into(), Some(CompressionAlgorithmFileExtension::Zstd))]
586 #[case::name_with_dashes_version_with_epoch_no_compression(Name::new("example-package")?, FullVersion::from_str("1:1.0.0-1")?, SystemArchitecture::X86_64.into(), None)]
587 fn succeed_to_create_package_file_name(
588 #[case] name: Name,
589 #[case] version: FullVersion,
590 #[case] architecture: Architecture,
591 #[case] compression: Option<CompressionAlgorithmFileExtension>,
592 ) -> TestResult {
593 init_logger()?;
594
595 let package_file_name =
596 PackageFileName::new(name.clone(), version.clone(), architecture, compression);
597 debug!("Package file name: {package_file_name}");
598
599 Ok(())
600 }
601
602 /// Tests that common and uncommon cases of package file names can be recognized and
603 /// round-tripped.
604 #[rstest]
605 #[case::name_with_dashes("example-pkg-1.0.0-1-x86_64.pkg.tar.zst")]
606 #[case::no_compression("example-pkg-1.0.0-1-x86_64.pkg.tar")]
607 #[case::version_as_name("1.0.0-1-1.0.0-1-x86_64.pkg.tar.zst")]
608 #[case::version_with_epoch("example-1:1.0.0-1-x86_64.pkg.tar.zst")]
609 #[case::version_with_pkgrel_sub_version("example-1.0.0-1.1-x86_64.pkg.tar.zst")]
610 fn succeed_to_parse_package_file_name(#[case] s: &str) -> TestResult {
611 init_logger()?;
612
613 match PackageFileName::from_str(s) {
614 Err(error) => {
615 panic!("The parser failed parsing {s} although it should have succeeded:\n{error}");
616 }
617 Ok(value) => {
618 let file_name_string: String = value.clone().into();
619 assert_eq!(file_name_string, s);
620 assert_eq!(value.to_string(), s);
621 }
622 };
623
624 Ok(())
625 }
626
627 /// Ensures that [`PackageFileName`] can be created from common and uncommon cases of package
628 /// file names as [`Path`].
629 #[rstest]
630 #[case::name_with_dashes("example-pkg-1.0.0-1-x86_64.pkg.tar.zst")]
631 #[case::no_compression("example-pkg-1.0.0-1-x86_64.pkg.tar")]
632 #[case::version_as_name("1.0.0-1-1.0.0-1-x86_64.pkg.tar.zst")]
633 #[case::version_with_epoch("example-1:1.0.0-1-x86_64.pkg.tar.zst")]
634 #[case::version_with_pkgrel_sub_version("example-1.0.0-1.1-x86_64.pkg.tar.zst")]
635 fn package_file_name_from_path_succeeds(#[case] path: &str) -> TestResult {
636 init_logger()?;
637 let path = PathBuf::from(path);
638
639 match PackageFileName::try_from(path.as_path()) {
640 Err(error) => {
641 panic!(
642 "Failed creating PackageFileName from {path:?} although it should have succeeded:\n{error}"
643 );
644 }
645 Ok(value) => assert_eq!(value.to_path_buf(), path),
646 };
647
648 Ok(())
649 }
650
651 /// Tests that a matching [`Name`] can be derived from a [`PackageFileName`].
652 #[test]
653 fn package_file_name_name() -> TestResult {
654 let name = Name::new("example")?;
655 let file_name = PackageFileName::new(
656 name.clone(),
657 "1:1.0.0-1".parse()?,
658 "x86_64".parse()?,
659 Some("zst".parse()?),
660 );
661
662 assert_eq!(file_name.name(), &name);
663
664 Ok(())
665 }
666
667 /// Tests that a matching [`FullVersion`] can be derived from a [`PackageFileName`].
668 #[test]
669 fn package_file_name_version() -> TestResult {
670 let version = FullVersion::from_str("1:1.0.0-1")?;
671 let file_name = PackageFileName::new(
672 Name::new("example")?,
673 version.clone(),
674 "x86_64".parse()?,
675 Some("zst".parse()?),
676 );
677
678 assert_eq!(file_name.version(), &version);
679
680 Ok(())
681 }
682
683 /// Tests that a matching [`Architecture`] can be derived from a [`PackageFileName`].
684 #[test]
685 fn package_file_name_architecture() -> TestResult {
686 let architecture: Architecture = SystemArchitecture::X86_64.into();
687 let file_name = PackageFileName::new(
688 Name::new("example")?,
689 "1:1.0.0-1".parse()?,
690 architecture.clone(),
691 Some("zst".parse()?),
692 );
693
694 assert_eq!(file_name.architecture(), &architecture);
695
696 Ok(())
697 }
698
699 /// Tests that a matching optional [`CompressionAlgorithmFileExtension`] can be derived from a
700 /// [`PackageFileName`].
701 #[rstest]
702 #[case::with_compression(Some(CompressionAlgorithmFileExtension::Zstd))]
703 #[case::no_compression(None)]
704 fn package_file_name_compression(
705 #[case] compression: Option<CompressionAlgorithmFileExtension>,
706 ) -> TestResult {
707 let file_name = PackageFileName::new(
708 Name::new("example")?,
709 "1:1.0.0-1".parse()?,
710 "x86_64".parse()?,
711 compression,
712 );
713
714 assert_eq!(file_name.compression(), compression);
715
716 Ok(())
717 }
718
719 /// Tests that a [`PathBuf`] can be derived from a [`PackageFileName`].
720 #[rstest]
721 #[case::with_compression(Some("zst".parse()?), "example-1:1.0.0-1-x86_64.pkg.tar.zst")]
722 #[case::no_compression(None, "example-1:1.0.0-1-x86_64.pkg.tar")]
723 fn package_file_name_to_path_buf(
724 #[case] compression: Option<CompressionAlgorithmFileExtension>,
725 #[case] path: &str,
726 ) -> TestResult {
727 let file_name = PackageFileName::new(
728 "example".parse()?,
729 "1:1.0.0-1".parse()?,
730 "x86_64".parse()?,
731 compression,
732 );
733 assert_eq!(file_name.to_path_buf(), PathBuf::from(path));
734
735 Ok(())
736 }
737
738 /// Tests that an uncompressed [`PackageFileName`] representation can be derived from a
739 /// [`PackageFileName`].
740 #[rstest]
741 #[case::compression_to_no_compression(
742 Some(CompressionAlgorithmFileExtension::Zstd),
743 None,
744 PackageFileName::new(
745 "example".parse()?,
746 "1:1.0.0-1".parse()?,
747 "x86_64".parse()?,
748 None,
749 ))]
750 #[case::no_compression_to_compression(
751 None,
752 Some(CompressionAlgorithmFileExtension::Zstd),
753 PackageFileName::new(
754 "example".parse()?,
755 "1:1.0.0-1".parse()?,
756 "x86_64".parse()?,
757 Some(CompressionAlgorithmFileExtension::Zstd),
758 ))]
759 fn package_file_name_set_compression(
760 #[case] initial_compression: Option<CompressionAlgorithmFileExtension>,
761 #[case] compression: Option<CompressionAlgorithmFileExtension>,
762 #[case] output_file_name: PackageFileName,
763 ) -> TestResult {
764 let mut file_name = PackageFileName::new(
765 "example".parse()?,
766 "1:1.0.0-1".parse()?,
767 "x86_64".parse()?,
768 initial_compression,
769 );
770 file_name.set_compression(compression);
771 assert_eq!(file_name, output_file_name);
772
773 Ok(())
774 }
775}