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}