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