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}