Skip to main content

uv_extract/
error.rs

1use std::{ffi::OsString, path::PathBuf};
2
3#[derive(Debug, thiserror::Error)]
4pub enum Error {
5    #[error("I/O operation failed during extraction")]
6    Io(#[source] std::io::Error),
7    #[error("Invalid zip file structure")]
8    AsyncZip(#[from] async_zip::error::ZipError),
9    #[error("Invalid tar file")]
10    Tar(#[from] tokio_tar::TarError),
11    #[error(
12        "The top-level of the archive must only contain a list directory, but it contains: {0:?}"
13    )]
14    NonSingularArchive(Vec<OsString>),
15    #[error("The top-level of the archive must only contain a list directory, but it's empty")]
16    EmptyArchive,
17    #[error("ZIP local header filename at offset {offset} does not use UTF-8 encoding")]
18    LocalHeaderNotUtf8 { offset: u64 },
19    #[error("ZIP central directory entry filename at index {index} does not use UTF-8 encoding")]
20    CentralDirectoryEntryNotUtf8 { index: u64 },
21    #[error("Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())]
22    BadCrc32 {
23        path: PathBuf,
24        computed: u32,
25        expected: u32,
26    },
27    #[error("Bad uncompressed size (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())]
28    BadUncompressedSize {
29        path: PathBuf,
30        computed: u64,
31        expected: u64,
32    },
33    #[error("Bad compressed size (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())]
34    BadCompressedSize {
35        path: PathBuf,
36        computed: u64,
37        expected: u64,
38    },
39    #[error("ZIP file contains multiple entries with different contents for: {}", path.display())]
40    DuplicateLocalFileHeader { path: PathBuf },
41    #[error("ZIP file contains a local file header without a corresponding central-directory record entry for: {} ({offset})", path.display())]
42    MissingCentralDirectoryEntry { path: PathBuf, offset: u64 },
43    #[error("ZIP file contains an end-of-central-directory record entry, but no local file header for: {} ({offset}", path.display())]
44    MissingLocalFileHeader { path: PathBuf, offset: u64 },
45    #[error("ZIP file uses conflicting paths for the local file header at {} (got {}, expected {})", offset, local_path.display(), central_directory_path.display())]
46    ConflictingPaths {
47        offset: u64,
48        local_path: PathBuf,
49        central_directory_path: PathBuf,
50    },
51    #[error("ZIP file uses conflicting checksums for the local file header and central-directory record (got {local_crc32}, expected {central_directory_crc32}) for: {} ({offset})", path.display())]
52    ConflictingChecksums {
53        path: PathBuf,
54        offset: u64,
55        local_crc32: u32,
56        central_directory_crc32: u32,
57    },
58    #[error("ZIP file uses conflicting compressed sizes for the local file header and central-directory record (got {local_compressed_size}, expected {central_directory_compressed_size}) for: {} ({offset})", path.display())]
59    ConflictingCompressedSizes {
60        path: PathBuf,
61        offset: u64,
62        local_compressed_size: u64,
63        central_directory_compressed_size: u64,
64    },
65    #[error("ZIP file uses conflicting uncompressed sizes for the local file header and central-directory record (got {local_uncompressed_size}, expected {central_directory_uncompressed_size}) for: {} ({offset})", path.display())]
66    ConflictingUncompressedSizes {
67        path: PathBuf,
68        offset: u64,
69        local_uncompressed_size: u64,
70        central_directory_uncompressed_size: u64,
71    },
72    #[error("ZIP file contains trailing contents after the end-of-central-directory record")]
73    TrailingContents,
74    #[error(
75        "ZIP file reports a number of entries in the central directory that conflicts with the actual number of entries (got {actual}, expected {expected})"
76    )]
77    ConflictingNumberOfEntries { actual: u64, expected: u64 },
78    #[error("Data descriptor is missing for file: {}", path.display())]
79    MissingDataDescriptor { path: PathBuf },
80    #[error("File contains an unexpected data descriptor: {}", path.display())]
81    UnexpectedDataDescriptor { path: PathBuf },
82    #[error(
83        "ZIP file end-of-central-directory record contains a comment that appears to be an embedded ZIP file"
84    )]
85    ZipInZip,
86    #[error("ZIP64 end-of-central-directory record contains unsupported extensible data")]
87    ExtensibleData,
88    #[error("ZIP file end-of-central-directory record contains multiple entries with the same path, but conflicting modes: {}", path.display())]
89    DuplicateExecutableFileHeader { path: PathBuf },
90    #[error("Archive contains a file with an empty filename")]
91    EmptyFilename,
92    #[error("Archive contains unacceptable filename: {filename}")]
93    UnacceptableFilename { filename: String },
94}
95
96impl Error {
97    /// When reading from an archive, the error can either be an IO error from the underlying
98    /// operating system, or an error with the archive. Both get wrapper into an IO error through
99    /// e.g., `io::copy`. This method extracts zip and tar errors, to distinguish them from invalid
100    /// archives.
101    pub(crate) fn io_or_compression(err: std::io::Error) -> Self {
102        if err.kind() != std::io::ErrorKind::Other {
103            return Self::Io(err);
104        }
105
106        let err = match err.downcast::<tokio_tar::TarError>() {
107            Ok(tar_err) => return Self::Tar(tar_err),
108            Err(err) => err,
109        };
110        let err = match err.downcast::<async_zip::error::ZipError>() {
111            Ok(zip_err) => return Self::AsyncZip(zip_err),
112            Err(err) => err,
113        };
114        Self::Io(err)
115    }
116
117    /// Returns `true` if the error is due to the server not supporting HTTP streaming. Most
118    /// commonly, this is due to serving ZIP files with features that are incompatible with
119    /// streaming, like data descriptors.
120    pub fn is_http_streaming_unsupported(&self) -> bool {
121        matches!(
122            self,
123            Self::AsyncZip(async_zip::error::ZipError::FeatureNotSupported(_))
124        )
125    }
126
127    /// Returns `true` if the error is due to HTTP streaming request failed.
128    pub fn is_http_streaming_failed(&self) -> bool {
129        match self {
130            Self::AsyncZip(async_zip::error::ZipError::UpstreamReadError(_)) => true,
131            Self::Io(err) => {
132                if let Some(inner) = err.get_ref() {
133                    inner.downcast_ref::<reqwest::Error>().is_some()
134                } else {
135                    false
136                }
137            }
138            _ => false,
139        }
140    }
141}