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