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