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