alpm_types/package/
file_name.rs

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