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 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 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 pub fn is_http_streaming_unsupported(&self) -> bool {
160 matches!(
161 self,
162 Self::AsyncZip(async_zip::error::ZipError::FeatureNotSupported(_))
163 )
164 }
165
166 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 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}