Skip to main content

uv_extract/
error.rs

1use std::{ffi::OsString, path::PathBuf};
2
3use crate::validate_archive_member_name;
4
5#[derive(Debug, thiserror::Error)]
6pub enum Error {
7    #[error("I/O operation failed during extraction")]
8    Io(#[source] std::io::Error),
9    #[error("Invalid zip file structure")]
10    AsyncZip(#[source] async_zip::error::ZipError),
11    #[error("Invalid tar file")]
12    Tar(
13        #[source]
14        #[from]
15        tokio_tar::TarError,
16    ),
17    #[error("Invalid tar file")]
18    TarCodec(
19        #[source]
20        #[from]
21        tar_codec::ExtractError<tar_codec::DecodeError>,
22    ),
23    #[error(
24        "The top-level of the archive must only contain a list directory, but it contains: {0:?}"
25    )]
26    NonSingularArchive(Vec<OsString>),
27    #[error("The top-level of the archive must only contain a list directory, but it's empty")]
28    EmptyArchive,
29    #[error("ZIP local header filename at offset {offset} does not use UTF-8 encoding")]
30    LocalHeaderNotUtf8 { offset: u64 },
31    #[error("ZIP central directory entry filename at index {index} does not use UTF-8 encoding")]
32    CentralDirectoryEntryNotUtf8 { index: u64 },
33    #[error("Bad CRC (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())]
34    BadCrc32 {
35        path: PathBuf,
36        computed: u32,
37        expected: u32,
38    },
39    #[error("Bad uncompressed size (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())]
40    BadUncompressedSize {
41        path: PathBuf,
42        computed: u64,
43        expected: u64,
44    },
45    #[error("Bad compressed size (got {computed:08x}, expected {expected:08x}) for file: {}", path.display())]
46    BadCompressedSize {
47        path: PathBuf,
48        computed: u64,
49        expected: u64,
50    },
51    #[error("ZIP file contains multiple entries with different contents for: {}", path.display())]
52    DuplicateLocalFileHeader { path: PathBuf },
53    #[error("ZIP file contains a local file header without a corresponding central-directory record entry for: {} ({offset})", path.display())]
54    MissingCentralDirectoryEntry { path: PathBuf, offset: u64 },
55    #[error("ZIP file contains an end-of-central-directory record entry, but no local file header for: {} ({offset}", path.display())]
56    MissingLocalFileHeader { path: PathBuf, offset: u64 },
57    #[error("ZIP file uses conflicting paths for the local file header at {} (got {}, expected {})", offset, local_path.display(), central_directory_path.display())]
58    ConflictingPaths {
59        offset: u64,
60        local_path: PathBuf,
61        central_directory_path: PathBuf,
62    },
63    #[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())]
64    ConflictingChecksums {
65        path: PathBuf,
66        offset: u64,
67        local_crc32: u32,
68        central_directory_crc32: u32,
69    },
70    #[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())]
71    ConflictingCompressedSizes {
72        path: PathBuf,
73        offset: u64,
74        local_compressed_size: u64,
75        central_directory_compressed_size: u64,
76    },
77    #[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())]
78    ConflictingUncompressedSizes {
79        path: PathBuf,
80        offset: u64,
81        local_uncompressed_size: u64,
82        central_directory_uncompressed_size: u64,
83    },
84    #[error("ZIP file contains trailing contents after the end-of-central-directory record")]
85    TrailingContents,
86    #[error(
87        "ZIP file reports a number of entries in the central directory that conflicts with the actual number of entries (got {actual}, expected {expected})"
88    )]
89    ConflictingNumberOfEntries { actual: u64, expected: u64 },
90    #[error("Data descriptor is missing for file: {}", path.display())]
91    MissingDataDescriptor { path: PathBuf },
92    #[error("File contains an unexpected data descriptor: {}", path.display())]
93    UnexpectedDataDescriptor { path: PathBuf },
94    #[error(
95        "ZIP file end-of-central-directory record contains a comment that appears to be an embedded ZIP file"
96    )]
97    ZipInZip,
98    #[error("ZIP64 end-of-central-directory record contains unsupported extensible data")]
99    ExtensibleData,
100    #[error("ZIP file end-of-central-directory record contains multiple entries with the same path, but conflicting modes: {}", path.display())]
101    DuplicateExecutableFileHeader { path: PathBuf },
102    #[error("Archive contains a file with an empty filename")]
103    EmptyFilename,
104    #[error("Archive contains unacceptable filename: {filename}")]
105    UnacceptableFilename { filename: String },
106    #[error(
107        "Archive contains a file with an unsupported compression method; files must be compressed with 'stored', 'DEFLATE', or 'zstd'"
108    )]
109    UnsupportedCompression,
110}
111
112impl From<async_zip::error::ZipError> for Error {
113    fn from(err: async_zip::error::ZipError) -> Self {
114        match err {
115            async_zip::error::ZipError::CompressionNotSupported(_) => Self::UnsupportedCompression,
116            async_zip::error::ZipError::FileNameContainsNul { filename } => {
117                let filename = String::from_utf8_lossy(&filename);
118                validate_archive_member_name(&filename)
119                    .expect_err("a filename containing an embedded NUL must be rejected")
120            }
121            error => Self::AsyncZip(error),
122        }
123    }
124}
125
126impl Error {
127    /// When reading from a ZIP archive, the error can either be an I/O error from the underlying
128    /// operating system, or an error with the archive. Both get wrapped into an I/O error through
129    /// e.g., `io::copy`. This method extracts ZIP errors to distinguish them from invalid archives.
130    pub(crate) fn io_or_zip(err: std::io::Error) -> Self {
131        if err.kind() != std::io::ErrorKind::Other {
132            return Self::Io(err);
133        }
134
135        let err = match err.downcast::<async_zip::error::ZipError>() {
136            Ok(zip_err) => return Self::from(zip_err),
137            Err(err) => err,
138        };
139        Self::Io(err)
140    }
141
142    /// When reading a tar archive, the error can either be an I/O error from the underlying
143    /// reader or an error with the archive. Both get wrapped into an I/O error through operations
144    /// such as `io::copy`. This method extracts tar errors to distinguish them from I/O errors.
145    pub(crate) fn io_or_tar(err: std::io::Error) -> Self {
146        if err.kind() != std::io::ErrorKind::Other {
147            return Self::Io(err);
148        }
149
150        match err.downcast::<tokio_tar::TarError>() {
151            Ok(tar_err) => Self::Tar(tar_err),
152            Err(err) => Self::Io(err),
153        }
154    }
155
156    /// Returns `true` if the error is due to the server not supporting HTTP streaming. Most
157    /// commonly, this is due to serving ZIP files with features that are incompatible with
158    /// streaming, like data descriptors.
159    pub fn is_http_streaming_unsupported(&self) -> bool {
160        matches!(
161            self,
162            Self::AsyncZip(async_zip::error::ZipError::FeatureNotSupported(_))
163        )
164    }
165
166    /// Returns `true` if the error is due to HTTP streaming request failed.
167    pub fn is_http_streaming_failed(&self) -> bool {
168        fn contains_reqwest_error(error: &(dyn std::error::Error + 'static)) -> bool {
169            if error.downcast_ref::<reqwest::Error>().is_some() {
170                return true;
171            }
172            // `std::io::Error::source` forwards to the source of its custom error and can skip the
173            // custom error itself, so inspect `get_ref` before following the standard chain.
174            if let Some(error) = error.downcast_ref::<std::io::Error>()
175                && let Some(inner) = error.get_ref()
176                && contains_reqwest_error(inner)
177            {
178                return true;
179            }
180            error.source().is_some_and(contains_reqwest_error)
181        }
182
183        if matches!(
184            self,
185            Self::AsyncZip(async_zip::error::ZipError::UpstreamReadError(_))
186        ) {
187            return true;
188        }
189        contains_reqwest_error(self)
190    }
191}