Skip to main content

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}