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::{PackageFileName, SystemArchitecture};
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(), &SystemArchitecture::X86_64.into());
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    use crate::system::SystemArchitecture;
567
568    fn init_logger() -> TestResult {
569        if TermLogger::init(
570            LevelFilter::Info,
571            Config::default(),
572            TerminalMode::Mixed,
573            ColorChoice::Auto,
574        )
575        .is_err()
576        {
577            debug!("Not initializing another logger, as one is initialized already.");
578        }
579
580        Ok(())
581    }
582
583    /// Ensures that common and uncommon cases of package filenames can be created.
584    #[rstest]
585    #[case::name_with_dashes(Name::new("example-package")?, FullVersion::from_str("1.0.0-1")?, SystemArchitecture::X86_64.into(), Some(CompressionAlgorithmFileExtension::Zstd))]
586    #[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)]
587    fn succeed_to_create_package_file_name(
588        #[case] name: Name,
589        #[case] version: FullVersion,
590        #[case] architecture: Architecture,
591        #[case] compression: Option<CompressionAlgorithmFileExtension>,
592    ) -> TestResult {
593        init_logger()?;
594
595        let package_file_name =
596            PackageFileName::new(name.clone(), version.clone(), architecture, compression);
597        debug!("Package file name: {package_file_name}");
598
599        Ok(())
600    }
601
602    /// Tests that common and uncommon cases of package file names can be recognized and
603    /// round-tripped.
604    #[rstest]
605    #[case::name_with_dashes("example-pkg-1.0.0-1-x86_64.pkg.tar.zst")]
606    #[case::no_compression("example-pkg-1.0.0-1-x86_64.pkg.tar")]
607    #[case::version_as_name("1.0.0-1-1.0.0-1-x86_64.pkg.tar.zst")]
608    #[case::version_with_epoch("example-1:1.0.0-1-x86_64.pkg.tar.zst")]
609    #[case::version_with_pkgrel_sub_version("example-1.0.0-1.1-x86_64.pkg.tar.zst")]
610    fn succeed_to_parse_package_file_name(#[case] s: &str) -> TestResult {
611        init_logger()?;
612
613        match PackageFileName::from_str(s) {
614            Err(error) => {
615                panic!("The parser failed parsing {s} although it should have succeeded:\n{error}");
616            }
617            Ok(value) => {
618                let file_name_string: String = value.clone().into();
619                assert_eq!(file_name_string, s);
620                assert_eq!(value.to_string(), s);
621            }
622        };
623
624        Ok(())
625    }
626
627    /// Ensures that [`PackageFileName`] can be created from common and uncommon cases of package
628    /// file names as [`Path`].
629    #[rstest]
630    #[case::name_with_dashes("example-pkg-1.0.0-1-x86_64.pkg.tar.zst")]
631    #[case::no_compression("example-pkg-1.0.0-1-x86_64.pkg.tar")]
632    #[case::version_as_name("1.0.0-1-1.0.0-1-x86_64.pkg.tar.zst")]
633    #[case::version_with_epoch("example-1:1.0.0-1-x86_64.pkg.tar.zst")]
634    #[case::version_with_pkgrel_sub_version("example-1.0.0-1.1-x86_64.pkg.tar.zst")]
635    fn package_file_name_from_path_succeeds(#[case] path: &str) -> TestResult {
636        init_logger()?;
637        let path = PathBuf::from(path);
638
639        match PackageFileName::try_from(path.as_path()) {
640            Err(error) => {
641                panic!(
642                    "Failed creating PackageFileName from {path:?} although it should have succeeded:\n{error}"
643                );
644            }
645            Ok(value) => assert_eq!(value.to_path_buf(), path),
646        };
647
648        Ok(())
649    }
650
651    /// Tests that a matching [`Name`] can be derived from a [`PackageFileName`].
652    #[test]
653    fn package_file_name_name() -> TestResult {
654        let name = Name::new("example")?;
655        let file_name = PackageFileName::new(
656            name.clone(),
657            "1:1.0.0-1".parse()?,
658            "x86_64".parse()?,
659            Some("zst".parse()?),
660        );
661
662        assert_eq!(file_name.name(), &name);
663
664        Ok(())
665    }
666
667    /// Tests that a matching [`FullVersion`] can be derived from a [`PackageFileName`].
668    #[test]
669    fn package_file_name_version() -> TestResult {
670        let version = FullVersion::from_str("1:1.0.0-1")?;
671        let file_name = PackageFileName::new(
672            Name::new("example")?,
673            version.clone(),
674            "x86_64".parse()?,
675            Some("zst".parse()?),
676        );
677
678        assert_eq!(file_name.version(), &version);
679
680        Ok(())
681    }
682
683    /// Tests that a matching [`Architecture`] can be derived from a [`PackageFileName`].
684    #[test]
685    fn package_file_name_architecture() -> TestResult {
686        let architecture: Architecture = SystemArchitecture::X86_64.into();
687        let file_name = PackageFileName::new(
688            Name::new("example")?,
689            "1:1.0.0-1".parse()?,
690            architecture.clone(),
691            Some("zst".parse()?),
692        );
693
694        assert_eq!(file_name.architecture(), &architecture);
695
696        Ok(())
697    }
698
699    /// Tests that a matching optional [`CompressionAlgorithmFileExtension`] can be derived from a
700    /// [`PackageFileName`].
701    #[rstest]
702    #[case::with_compression(Some(CompressionAlgorithmFileExtension::Zstd))]
703    #[case::no_compression(None)]
704    fn package_file_name_compression(
705        #[case] compression: Option<CompressionAlgorithmFileExtension>,
706    ) -> TestResult {
707        let file_name = PackageFileName::new(
708            Name::new("example")?,
709            "1:1.0.0-1".parse()?,
710            "x86_64".parse()?,
711            compression,
712        );
713
714        assert_eq!(file_name.compression(), compression);
715
716        Ok(())
717    }
718
719    /// Tests that a [`PathBuf`] can be derived from a [`PackageFileName`].
720    #[rstest]
721    #[case::with_compression(Some("zst".parse()?), "example-1:1.0.0-1-x86_64.pkg.tar.zst")]
722    #[case::no_compression(None, "example-1:1.0.0-1-x86_64.pkg.tar")]
723    fn package_file_name_to_path_buf(
724        #[case] compression: Option<CompressionAlgorithmFileExtension>,
725        #[case] path: &str,
726    ) -> TestResult {
727        let file_name = PackageFileName::new(
728            "example".parse()?,
729            "1:1.0.0-1".parse()?,
730            "x86_64".parse()?,
731            compression,
732        );
733        assert_eq!(file_name.to_path_buf(), PathBuf::from(path));
734
735        Ok(())
736    }
737
738    /// Tests that an uncompressed [`PackageFileName`] representation can be derived from a
739    /// [`PackageFileName`].
740    #[rstest]
741    #[case::compression_to_no_compression(
742        Some(CompressionAlgorithmFileExtension::Zstd),
743        None,
744        PackageFileName::new(
745            "example".parse()?,
746            "1:1.0.0-1".parse()?,
747            "x86_64".parse()?,
748            None,
749        ))]
750    #[case::no_compression_to_compression(
751        None,
752        Some(CompressionAlgorithmFileExtension::Zstd),
753        PackageFileName::new(
754            "example".parse()?,
755            "1:1.0.0-1".parse()?,
756            "x86_64".parse()?,
757            Some(CompressionAlgorithmFileExtension::Zstd),
758        ))]
759    fn package_file_name_set_compression(
760        #[case] initial_compression: Option<CompressionAlgorithmFileExtension>,
761        #[case] compression: Option<CompressionAlgorithmFileExtension>,
762        #[case] output_file_name: PackageFileName,
763    ) -> TestResult {
764        let mut file_name = PackageFileName::new(
765            "example".parse()?,
766            "1:1.0.0-1".parse()?,
767            "x86_64".parse()?,
768            initial_compression,
769        );
770        file_name.set_compression(compression);
771        assert_eq!(file_name, output_file_name);
772
773        Ok(())
774    }
775}