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};
10use serde::{Deserialize, Serialize};
11use winnow::{
12    ModalResult,
13    Parser,
14    combinator::{opt, peek, repeat_till},
15    error::{AddContext, ContextError, ErrMode, ParserError, StrContext, StrContextValue},
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, Deserialize, Eq, PartialEq, Serialize)]
34#[serde(into = "String")]
35#[serde(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::traits::ParserUntil;
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(filename)?
280    ///         .to_string()
281    /// );
282    /// # Ok(())
283    /// # }
284    /// ```
285    fn parser_until<'a, P>(delimiter: P) -> impl Parser<&'a str, Self, ErrMode<ContextError>>
286    where
287        P: Parser<&'a str, &'a str, ErrMode<ContextError>>,
288    {
289        // Define the actual parser closure.
290        // The delimiter is moved into the closure and borrowed via `by_ref()` on each call.
291        let mut delimiter_parser = delimiter;
292        move |input: &mut &'a str| -> ModalResult<Self> {
293            // Detect the amount of dashes in input and subsequently in the Name component.
294            //
295            // Note: This is a necessary step because dashes are used as delimiters between the
296            // components of the file name and the Name component (an alpm-package-name) can contain
297            // dashes, too.
298            // We know that the minimum amount of dashes in a valid alpm-package file name is
299            // three (one dash between the Name, FullVersion, PackageRelease, and Architecture
300            // component each).
301            // We rely on this fact to determine the amount of dashes in the Name component and
302            // thereby the cut-off point between the Name and the FullVersion component.
303            let checkpoint = input.checkpoint();
304            let dashes: usize =
305                repeat_till::<_, _, (), _, _, _, _>(0.., any, peek(delimiter_parser.by_ref()))
306                    .take()
307                    .map(|s| {
308                        s.chars().fold(0, |acc, char| {
309                            if char == '-' {
310                                return acc + 1;
311                            }
312                            acc
313                        })
314                    })
315                    .parse_next(input)?;
316            input.reset(&checkpoint);
317
318            if dashes < 3 {
319                let context_error = ContextError::from_input(input)
320                .add_context(
321                    input,
322                    &input.checkpoint(),
323                    StrContext::Label("alpm-package file name"),
324                )
325                .add_context(
326                    input,
327                    &input.checkpoint(),
328                    StrContext::Expected(StrContextValue::Description(
329                        concat!(
330                        "a package name, followed by an alpm-package-version (full or full with epoch) and an architecture.",
331                        "\nAll components must be delimited with a dash ('-')."
332                        )
333                    ))
334                );
335
336                return Err(ErrMode::Backtrack(context_error));
337            }
338
339            // The (zero or more) dashes in the Name component.
340            let dashes_till_version = dashes.saturating_sub(2);
341
342            // Advance the parser to the dash just behind the Name component, based on the amount of
343            // dashes in the Name, e.g.:
344            // "example-package-1:1.0.0-1-x86_64.pkg.tar.zst" -> "-1:1.0.0-1-x86_64.pkg.tar.zst"
345            let name = Name::parse_name_followed_by_version(dashes_till_version)
346                .context(StrContext::Label("alpm-package-name"))
347                .parse_next(input)?;
348
349            // Consume leading dash in front of FullVersion, e.g.:
350            // "-1:1.0.0-1-x86_64.pkg.tar.zst" -> "1:1.0.0-1-x86_64.pkg.tar.zst"
351            "-".parse_next(input)?;
352
353            // Advance the parser to beyond the FullVersion component (which contains one dash),
354            // e.g.: "1:1.0.0-1-x86_64.pkg.tar.zst" -> "-x86_64.pkg.tar.zst"
355            let version: FullVersion = FullVersion::parser_until("-").parse_next(input)?;
356
357            // Consume leading dash, e.g.:
358            // "-x86_64.pkg.tar.zst" -> "x86_64.pkg.tar.zst"
359            "-".parse_next(input)?;
360
361            // Advance the parser to beyond the Architecture component, e.g.:
362            // "x86_64.pkg.tar.zst" -> ".pkg.tar.zst"
363            let architecture = Architecture::parser_until(".").parse_next(input)?;
364
365            // Consume leading dot, e.g.:
366            // ".pkg.tar.zst" -> "pkg.tar.zst"
367            ".".context(StrContext::Label("alpm-package file name"))
368                .context(StrContext::Expected(StrContextValue::StringLiteral(
369                    "a `.` between the architecture and the `pkg` extension",
370                )))
371                .parse_next(input)?;
372
373            // Consume the required alpm-package file type identifier, e.g.:
374            // "pkg.tar.zst" -> ".tar.zst"
375            "pkg"
376                .context(StrContext::Label("alpm-package file type identifier"))
377                .context(StrContext::Expected(StrContextValue::StringLiteral(
378                    FileTypeIdentifier::BinaryPackage.into(),
379                )))
380                .parse_next(input)?;
381
382            // Consume leading dot, e.g.:
383            // ".tar.zst" -> "tar.zst"
384            ".".context(StrContext::Label("alpm-package file name"))
385                .context(StrContext::Expected(StrContextValue::StringLiteral(
386                    "a `.` between the `pkg` and `tar` extension",
387                )))
388                .parse_next(input)?;
389
390            // Consume the required tar suffix, e.g.:
391            // "tar.zst" -> ".zst"
392            "tar"
393                .context(StrContext::Label("tar suffix"))
394                .context(StrContext::Expected(StrContextValue::Description("tar")))
395                .parse_next(input)?;
396
397            // Check if there's a `.`, which hints that a CompressionAlgorithmFileExtension exists.
398            // ".zst" -> "zst"
399            // If input is "", no compression is present.
400            let has_compression = opt(".").parse_next(input)?;
401
402            let mut compression = None;
403            if has_compression.is_some() {
404                // Advance the parser for the CompressionAlgorithmFileExtension component, e.g.:
405                // "zst" -> ""
406                compression = Some(CompressionAlgorithmFileExtension::parser.parse_next(input)?);
407            }
408
409            peek(delimiter_parser.by_ref())
410                .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}
424
425impl Display for PackageFileName {
426    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427        write!(
428            f,
429            "{}-{}-{}.{}.tar{}",
430            self.name,
431            self.version,
432            self.architecture,
433            FileTypeIdentifier::BinaryPackage,
434            match self.compression {
435                None => "".to_string(),
436                Some(suffix) => format!(".{suffix}"),
437            }
438        )
439    }
440}
441
442impl From<PackageFileName> for String {
443    /// Creates a [`String`] from a [`PackageFileName`].
444    fn from(value: PackageFileName) -> Self {
445        value.to_string()
446    }
447}
448
449impl FromStr for PackageFileName {
450    type Err = crate::Error;
451
452    /// Creates a [`PackageFileName`] from a string slice.
453    ///
454    /// Delegates to [`PackageFileName::parser_until`].
455    ///
456    /// # Errors
457    ///
458    /// Returns an error if [`PackageFileName::parser_until`] fails.
459    ///
460    /// # Examples
461    ///
462    /// ```
463    /// use std::str::FromStr;
464    ///
465    /// use alpm_types::PackageFileName;
466    ///
467    /// # fn main() -> Result<(), alpm_types::Error> {
468    /// let filename = "example-package-1:1.0.0-1-x86_64.pkg.tar.zst";
469    /// assert_eq!(filename, PackageFileName::from_str(filename)?.to_string());
470    /// # Ok(())
471    /// # }
472    /// ```
473    fn from_str(s: &str) -> Result<Self, Self::Err> {
474        Ok(Self::parser_until_eof.parse(s)?)
475    }
476}
477
478impl TryFrom<&Path> for PackageFileName {
479    type Error = crate::Error;
480
481    /// Creates a [`PackageFileName`] from a [`Path`] reference.
482    ///
483    /// The file name in `value` is extracted and, if valid is turned into a string slice.
484    /// The creation of the [`PackageFileName`] is delegated to [`PackageFileName::parser_until`].
485    ///
486    /// # Errors
487    ///
488    /// Returns an error if
489    ///
490    /// - `value` does not contain a valid file name,
491    /// - `value` can not be turned into a string slice,
492    /// - or [`PackageFileName::parser_until`] fails.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use std::path::PathBuf;
498    ///
499    /// use alpm_types::PackageFileName;
500    ///
501    /// # fn main() -> Result<(), alpm_types::Error> {
502    /// let filename = PathBuf::from("../example-package-1:1.0.0-1-x86_64.pkg.tar.zst");
503    /// assert_eq!(
504    ///     filename,
505    ///     PathBuf::from("..").join(PackageFileName::try_from(filename.as_path())?.to_path_buf()),
506    /// );
507    /// # Ok(())
508    /// # }
509    /// ```
510    fn try_from(value: &Path) -> Result<Self, Self::Error> {
511        let Some(name) = value.file_name() else {
512            return Err(PackageError::InvalidPackageFileNamePath {
513                path: value.to_path_buf(),
514            }
515            .into());
516        };
517        let Some(s) = name.to_str() else {
518            return Err(PackageError::InvalidPackageFileNamePath {
519                path: value.to_path_buf(),
520            }
521            .into());
522        };
523        Ok(Self::parser_until_eof.parse(s)?)
524    }
525}
526
527impl TryFrom<String> for PackageFileName {
528    type Error = crate::Error;
529
530    /// Creates a [`PackageFileName`] from a String.
531    ///
532    /// Delegates to [`PackageFileName::parser_until`].
533    ///
534    /// # Errors
535    ///
536    /// Returns an error if [`PackageFileName::parser_until`] fails.
537    ///
538    /// # Examples
539    ///
540    /// ```
541    /// use std::str::FromStr;
542    ///
543    /// use alpm_types::PackageFileName;
544    ///
545    /// # fn main() -> Result<(), alpm_types::Error> {
546    /// let filename = "example-package-1:1.0.0-1-x86_64.pkg.tar.zst".to_string();
547    /// assert_eq!(
548    ///     filename.clone(),
549    ///     PackageFileName::try_from(filename)?.to_string()
550    /// );
551    /// # Ok(())
552    /// # }
553    /// ```
554    fn try_from(value: String) -> Result<Self, Self::Error> {
555        Ok(Self::parser_until_eof.parse(&value)?)
556    }
557}
558
559#[cfg(test)]
560mod test {
561    use log::{LevelFilter, debug};
562    use rstest::rstest;
563    use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
564    use testresult::TestResult;
565
566    use super::*;
567    use crate::system::SystemArchitecture;
568
569    fn init_logger() -> TestResult {
570        if TermLogger::init(
571            LevelFilter::Info,
572            Config::default(),
573            TerminalMode::Mixed,
574            ColorChoice::Auto,
575        )
576        .is_err()
577        {
578            debug!("Not initializing another logger, as one is initialized already.");
579        }
580
581        Ok(())
582    }
583
584    /// Ensures that common and uncommon cases of package filenames can be created.
585    #[rstest]
586    #[case::name_with_dashes(Name::new("example-package")?, FullVersion::from_str("1.0.0-1")?, SystemArchitecture::X86_64.into(), Some(CompressionAlgorithmFileExtension::Zstd))]
587    #[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)]
588    fn succeed_to_create_package_file_name(
589        #[case] name: Name,
590        #[case] version: FullVersion,
591        #[case] architecture: Architecture,
592        #[case] compression: Option<CompressionAlgorithmFileExtension>,
593    ) -> TestResult {
594        init_logger()?;
595
596        let package_file_name =
597            PackageFileName::new(name.clone(), version.clone(), architecture, compression);
598        debug!("Package file name: {package_file_name}");
599
600        Ok(())
601    }
602
603    /// Tests that common and uncommon cases of package file names can be recognized and
604    /// round-tripped.
605    #[rstest]
606    #[case::name_with_dashes("example-pkg-1.0.0-1-x86_64.pkg.tar.zst")]
607    #[case::no_compression("example-pkg-1.0.0-1-x86_64.pkg.tar")]
608    #[case::version_as_name("1.0.0-1-1.0.0-1-x86_64.pkg.tar.zst")]
609    #[case::version_with_epoch("example-1:1.0.0-1-x86_64.pkg.tar.zst")]
610    #[case::version_with_pkgrel_sub_version("example-1.0.0-1.1-x86_64.pkg.tar.zst")]
611    fn succeed_to_parse_package_file_name(#[case] s: &str) -> TestResult {
612        init_logger()?;
613
614        match PackageFileName::from_str(s) {
615            Err(error) => {
616                panic!("The parser failed parsing {s} although it should have succeeded:\n{error}");
617            }
618            Ok(value) => {
619                let file_name_string: String = value.clone().into();
620                assert_eq!(file_name_string, s);
621                assert_eq!(value.to_string(), s);
622            }
623        };
624
625        Ok(())
626    }
627
628    /// Ensures that [`PackageFileName`] can be created from common and uncommon cases of package
629    /// file names as [`Path`].
630    #[rstest]
631    #[case::name_with_dashes("example-pkg-1.0.0-1-x86_64.pkg.tar.zst")]
632    #[case::no_compression("example-pkg-1.0.0-1-x86_64.pkg.tar")]
633    #[case::version_as_name("1.0.0-1-1.0.0-1-x86_64.pkg.tar.zst")]
634    #[case::version_with_epoch("example-1:1.0.0-1-x86_64.pkg.tar.zst")]
635    #[case::version_with_pkgrel_sub_version("example-1.0.0-1.1-x86_64.pkg.tar.zst")]
636    fn package_file_name_from_path_succeeds(#[case] path: &str) -> TestResult {
637        init_logger()?;
638        let path = PathBuf::from(path);
639
640        match PackageFileName::try_from(path.as_path()) {
641            Err(error) => {
642                panic!(
643                    "Failed creating PackageFileName from {path:?} although it should have succeeded:\n{error}"
644                );
645            }
646            Ok(value) => assert_eq!(value.to_path_buf(), path),
647        };
648
649        Ok(())
650    }
651
652    /// Tests that a matching [`Name`] can be derived from a [`PackageFileName`].
653    #[test]
654    fn package_file_name_name() -> TestResult {
655        let name = Name::new("example")?;
656        let file_name = PackageFileName::new(
657            name.clone(),
658            "1:1.0.0-1".parse()?,
659            "x86_64".parse()?,
660            Some("zst".parse()?),
661        );
662
663        assert_eq!(file_name.name(), &name);
664
665        Ok(())
666    }
667
668    /// Tests that a matching [`FullVersion`] can be derived from a [`PackageFileName`].
669    #[test]
670    fn package_file_name_version() -> TestResult {
671        let version = FullVersion::from_str("1:1.0.0-1")?;
672        let file_name = PackageFileName::new(
673            Name::new("example")?,
674            version.clone(),
675            "x86_64".parse()?,
676            Some("zst".parse()?),
677        );
678
679        assert_eq!(file_name.version(), &version);
680
681        Ok(())
682    }
683
684    /// Tests that a matching [`Architecture`] can be derived from a [`PackageFileName`].
685    #[test]
686    fn package_file_name_architecture() -> TestResult {
687        let architecture: Architecture = SystemArchitecture::X86_64.into();
688        let file_name = PackageFileName::new(
689            Name::new("example")?,
690            "1:1.0.0-1".parse()?,
691            architecture.clone(),
692            Some("zst".parse()?),
693        );
694
695        assert_eq!(file_name.architecture(), &architecture);
696
697        Ok(())
698    }
699
700    /// Tests that a matching optional [`CompressionAlgorithmFileExtension`] can be derived from a
701    /// [`PackageFileName`].
702    #[rstest]
703    #[case::with_compression(Some(CompressionAlgorithmFileExtension::Zstd))]
704    #[case::no_compression(None)]
705    fn package_file_name_compression(
706        #[case] compression: Option<CompressionAlgorithmFileExtension>,
707    ) -> TestResult {
708        let file_name = PackageFileName::new(
709            Name::new("example")?,
710            "1:1.0.0-1".parse()?,
711            "x86_64".parse()?,
712            compression,
713        );
714
715        assert_eq!(file_name.compression(), compression);
716
717        Ok(())
718    }
719
720    /// Tests that a [`PathBuf`] can be derived from a [`PackageFileName`].
721    #[rstest]
722    #[case::with_compression(Some("zst".parse()?), "example-1:1.0.0-1-x86_64.pkg.tar.zst")]
723    #[case::no_compression(None, "example-1:1.0.0-1-x86_64.pkg.tar")]
724    fn package_file_name_to_path_buf(
725        #[case] compression: Option<CompressionAlgorithmFileExtension>,
726        #[case] path: &str,
727    ) -> TestResult {
728        let file_name = PackageFileName::new(
729            "example".parse()?,
730            "1:1.0.0-1".parse()?,
731            "x86_64".parse()?,
732            compression,
733        );
734        assert_eq!(file_name.to_path_buf(), PathBuf::from(path));
735
736        Ok(())
737    }
738
739    /// Tests that an uncompressed [`PackageFileName`] representation can be derived from a
740    /// [`PackageFileName`].
741    #[rstest]
742    #[case::compression_to_no_compression(
743        Some(CompressionAlgorithmFileExtension::Zstd),
744        None,
745        PackageFileName::new(
746            "example".parse()?,
747            "1:1.0.0-1".parse()?,
748            "x86_64".parse()?,
749            None,
750        ))]
751    #[case::no_compression_to_compression(
752        None,
753        Some(CompressionAlgorithmFileExtension::Zstd),
754        PackageFileName::new(
755            "example".parse()?,
756            "1:1.0.0-1".parse()?,
757            "x86_64".parse()?,
758            Some(CompressionAlgorithmFileExtension::Zstd),
759        ))]
760    fn package_file_name_set_compression(
761        #[case] initial_compression: Option<CompressionAlgorithmFileExtension>,
762        #[case] compression: Option<CompressionAlgorithmFileExtension>,
763        #[case] output_file_name: PackageFileName,
764    ) -> TestResult {
765        let mut file_name = PackageFileName::new(
766            "example".parse()?,
767            "1:1.0.0-1".parse()?,
768            "x86_64".parse()?,
769            initial_compression,
770        );
771        file_name.set_compression(compression);
772        assert_eq!(file_name, output_file_name);
773
774        Ok(())
775    }
776}