1#[cfg(test)]
10mod tests;
11
12use std::{collections::TryReserveError, fmt, io};
13
14use crate::artifact::{self, ArtifactError, ArtifactIdentity, Sha256Digest};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct ArchiveLimits {
19 pub archive_bytes: usize,
21 pub decompressed_bytes: usize,
23 pub members: u32,
25 pub member_bytes: usize,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum ArchiveResource {
32 Members,
34 MemberBytes,
36}
37
38#[derive(Debug)]
40pub enum ArchiveError {
41 InvalidMember,
43 Archive(ArtifactError),
45 Decompression(ArtifactError),
47 TrailingCompressedData,
49 Tar(io::Error),
51 LimitExceeded {
53 resource: ArchiveResource,
55 limit: u64,
57 },
58 UnsupportedEntry {
60 entry: u32,
62 kind: u8,
64 },
65 TruncatedEntry {
67 entry: u32,
69 },
70 InvalidPadding,
72 MissingMember,
74 DuplicateMember,
76 Member(ArtifactError),
78 Allocation(TryReserveError),
80}
81
82impl fmt::Display for ArchiveError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::InvalidMember => {
86 f.write_str("archive member must be a canonical relative POSIX path")
87 }
88 Self::Archive(_) => f.write_str("compressed archive admission failed"),
89 Self::Decompression(_) => f.write_str("bounded gzip decoding failed"),
90 Self::TrailingCompressedData => f.write_str("bytes follow the single gzip stream"),
91 Self::Tar(_) => f.write_str("tar decoding failed"),
92 Self::LimitExceeded { resource, limit } => {
93 write!(f, "archive {resource:?} limit {limit} exceeded")
94 }
95 Self::UnsupportedEntry { entry, kind } => {
96 write!(f, "unsupported tar type {kind} at record {entry}")
97 }
98 Self::TruncatedEntry { entry } => write!(f, "truncated tar record {entry}"),
99 Self::InvalidPadding => f.write_str("invalid tar end markers or padding"),
100 Self::MissingMember => f.write_str("selected tar member is missing"),
101 Self::DuplicateMember => f.write_str("selected tar member is duplicated"),
102 Self::Member(_) => f.write_str("selected tar member admission failed"),
103 Self::Allocation(_) => f.write_str("selected tar member allocation failed"),
104 }
105 }
106}
107
108impl std::error::Error for ArchiveError {
109 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110 match self {
111 Self::Archive(source) | Self::Decompression(source) | Self::Member(source) => {
112 Some(source)
113 }
114 Self::Tar(source) => Some(source),
115 Self::Allocation(source) => Some(source),
116 _ => None,
117 }
118 }
119}
120
121pub struct ExtractedMember {
126 pub bytes: Vec<u8>,
128 pub archive_identity: ArtifactIdentity,
130 pub member_identity: ArtifactIdentity,
132}
133
134impl fmt::Debug for ExtractedMember {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.debug_struct("ExtractedMember")
137 .field("archive_identity", &self.archive_identity)
138 .field("member_identity", &self.member_identity)
139 .finish_non_exhaustive()
140 }
141}
142
143pub fn extract_tar_gz(
161 bytes: &[u8],
162 archive_sha256: Sha256Digest,
163 member: &str,
164 member_sha256: Sha256Digest,
165 limits: ArchiveLimits,
166) -> Result<ExtractedMember, ArchiveError> {
167 if !valid_member(member) {
168 return Err(ArchiveError::InvalidMember);
169 }
170 let archive_identity =
171 artifact::verify_reader(bytes, limits.archive_bytes as u64, archive_sha256)
172 .map_err(ArchiveError::Archive)?;
173 let mut decoder = flate2::bufread::GzDecoder::new(bytes);
174 let inflated = artifact::read_reader(&mut decoder, limits.decompressed_bytes)
175 .map_err(ArchiveError::Decompression)?;
176 if !decoder.into_inner().is_empty() {
177 return Err(ArchiveError::TrailingCompressedData);
178 }
179 let payload = select_member(&inflated, member, limits)?;
180 let member_identity =
181 artifact::verify_reader(payload, limits.member_bytes as u64, member_sha256)
182 .map_err(ArchiveError::Member)?;
183 let mut selected = Vec::new();
184 selected
185 .try_reserve_exact(payload.len())
186 .map_err(ArchiveError::Allocation)?;
187 selected.extend_from_slice(payload);
188 Ok(ExtractedMember {
189 bytes: selected,
190 archive_identity,
191 member_identity,
192 })
193}
194
195fn valid_member(member: &str) -> bool {
196 !member.is_empty()
197 && !member.bytes().any(|byte| matches!(byte, 0 | b'\\' | b':'))
198 && member
199 .split('/')
200 .all(|component| !matches!(component, "" | "." | ".."))
201}
202
203fn select_member<'a>(
204 bytes: &'a [u8],
205 member: &str,
206 limits: ArchiveLimits,
207) -> Result<&'a [u8], ArchiveError> {
208 let mut archive = tar::Archive::new(bytes);
209 let entries = archive.entries().map_err(ArchiveError::Tar)?.raw(true);
210 let mut next = 0_usize;
211 let mut selected = None;
212 for (index, entry) in entries.enumerate() {
213 let entry = entry.map_err(ArchiveError::Tar)?;
214 let count = u32::try_from(index)
215 .ok()
216 .and_then(|count| count.checked_add(1))
217 .filter(|count| *count <= limits.members)
218 .ok_or_else(|| ArchiveError::LimitExceeded {
219 resource: ArchiveResource::Members,
220 limit: u64::from(limits.members),
221 })?;
222 let kind = entry.header().entry_type();
223 if !kind.is_file() && !kind.is_dir() {
224 return Err(ArchiveError::UnsupportedEntry {
225 entry: count,
226 kind: kind.as_byte(),
227 });
228 }
229 let start = usize::try_from(entry.raw_file_position())
230 .map_err(|_| ArchiveError::TruncatedEntry { entry: count })?;
231 let size = usize::try_from(entry.size())
232 .map_err(|_| ArchiveError::TruncatedEntry { entry: count })?;
233 let end = start
234 .checked_add(size)
235 .ok_or(ArchiveError::TruncatedEntry { entry: count })?;
236 next = end
237 .checked_add(511)
238 .map(|value| value & !511)
239 .filter(|value| *value <= bytes.len())
240 .ok_or(ArchiveError::TruncatedEntry { entry: count })?;
241 if header_matches(entry.header(), member.as_bytes()) {
242 if selected.is_some() {
243 return Err(ArchiveError::DuplicateMember);
244 }
245 if !kind.is_file() {
246 return Err(ArchiveError::UnsupportedEntry {
247 entry: count,
248 kind: kind.as_byte(),
249 });
250 }
251 if size > limits.member_bytes {
252 return Err(ArchiveError::LimitExceeded {
253 resource: ArchiveResource::MemberBytes,
254 limit: limits.member_bytes as u64,
255 });
256 }
257 selected = Some(
258 bytes
259 .get(start..end)
260 .ok_or(ArchiveError::TruncatedEntry { entry: count })?,
261 );
262 }
263 }
264 let padding = &bytes[next..];
265 if padding.len() < 1024
266 || !padding.len().is_multiple_of(512)
267 || padding.iter().any(|byte| *byte != 0)
268 {
269 return Err(ArchiveError::InvalidPadding);
270 }
271 selected.ok_or(ArchiveError::MissingMember)
272}
273
274fn header_matches(header: &tar::Header, member: &[u8]) -> bool {
275 let name = header
278 .as_old()
279 .name
280 .split(|byte| *byte == 0)
281 .next()
282 .unwrap_or(&[]);
283 if let Some(ustar) = header.as_ustar() {
284 let prefix = ustar.prefix.split(|byte| *byte == 0).next().unwrap_or(&[]);
285 if !prefix.is_empty() {
286 return member
287 .strip_prefix(prefix)
288 .and_then(|rest| rest.strip_prefix(b"/"))
289 == Some(name);
290 }
291 }
292 member == name
293}