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