1use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt};
2use cap_std::ambient_authority;
3use cap_std::fs::{Dir as CapDir, OpenOptions as CapOpenOptions};
4use std::fs;
5use std::io::{Seek, SeekFrom, Write};
6use std::path::{Path, PathBuf};
7use std::time::{Duration, SystemTime};
8
9#[cfg(unix)]
10use std::os::unix::fs::PermissionsExt;
11
12use crate::format::{ExtractError, FormatError};
13use crate::metadata::validate_file_path_bytes;
14
15const TAR_BLOCK_LEN: usize = 512;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TarEntryKind {
19 Regular,
20 Directory,
21 Symlink,
22 Hardlink,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct MetadataDiagnostic {
27 pub profile: &'static str,
28 pub message: &'static str,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct OwnedTarMember {
33 pub path: Vec<u8>,
34 pub kind: TarEntryKind,
35 pub data: Vec<u8>,
36 pub link_target: Option<Vec<u8>>,
37 pub mode: u32,
38 pub mtime: u64,
39 pub logical_size: u64,
40 pub diagnostics: Vec<MetadataDiagnostic>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParsedTarMember<'a> {
45 pub path: Vec<u8>,
46 pub kind: TarEntryKind,
47 pub data: &'a [u8],
48 pub link_target: Option<Vec<u8>>,
49 pub mode: u32,
50 pub mtime: u64,
51 pub logical_size: u64,
52 pub diagnostics: Vec<MetadataDiagnostic>,
53}
54
55impl ParsedTarMember<'_> {
56 pub fn to_owned_member(&self) -> OwnedTarMember {
57 OwnedTarMember {
58 path: self.path.clone(),
59 kind: self.kind,
60 data: self.data.to_vec(),
61 link_target: self.link_target.clone(),
62 mode: self.mode,
63 mtime: self.mtime,
64 logical_size: self.logical_size,
65 diagnostics: self.diagnostics.clone(),
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub struct SafeExtractionOptions {
72 pub overwrite_existing: bool,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub(crate) struct StreamedTarMemberMetadata {
77 pub path: Vec<u8>,
78 pub kind: TarEntryKind,
79 pub link_target: Option<Vec<u8>>,
80 pub mode: u32,
81 pub mtime: u64,
82 pub logical_size: u64,
83 pub diagnostics: Vec<MetadataDiagnostic>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub(crate) struct TarStreamMemberSummary {
88 pub path: Vec<u8>,
89 pub kind: TarEntryKind,
90 pub link_target: Option<Vec<u8>>,
91 pub mode: u32,
92 pub mtime: u64,
93 pub logical_size: u64,
94 pub diagnostics: Vec<MetadataDiagnostic>,
95 pub group_start: u64,
96 pub group_size: u64,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub(crate) struct TarStreamSummary {
101 pub members: Vec<TarStreamMemberSummary>,
102 pub tar_total_size: u64,
103 pub total_extraction_size: u64,
104}
105
106pub(crate) trait TarMemberGroupReader {
107 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError>;
108
109 fn read_exact_member_bytes(&mut self, mut buf: &mut [u8]) -> Result<(), ExtractError> {
110 while !buf.is_empty() {
111 let read = self.read_some_member_bytes(buf)?;
112 if read == 0 {
113 return Err(
114 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
115 );
116 }
117 let (_, rest) = buf.split_at_mut(read);
118 buf = rest;
119 }
120 Ok(())
121 }
122}
123
124trait TarMemberStreamHandler {
125 fn on_member(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), ExtractError>;
126 fn write_regular_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError>;
127}
128
129pub(crate) trait TarStreamObserver {
130 fn on_member_start(&mut self, _member: &StreamedTarMemberMetadata) -> Result<(), FormatError> {
131 Ok(())
132 }
133
134 fn on_regular_payload(&mut self, _bytes: &[u8]) -> Result<(), FormatError> {
135 Ok(())
136 }
137
138 fn on_member_complete(
139 &mut self,
140 member: &StreamedTarMemberMetadata,
141 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
142 Ok(member.diagnostics.clone())
143 }
144}
145
146pub(crate) struct NoopTarStreamObserver;
147
148impl TarStreamObserver for NoopTarStreamObserver {}
149
150pub(crate) struct TarStreamFilesystemRestoreObserver<'a> {
151 handler: FilesystemRestoreHandler<'a>,
152}
153
154impl<'a> TarStreamFilesystemRestoreObserver<'a> {
155 pub(crate) fn new(root: &'a Path, options: SafeExtractionOptions) -> Self {
156 Self {
157 handler: FilesystemRestoreHandler::new(root, options),
158 }
159 }
160}
161
162impl TarStreamObserver for TarStreamFilesystemRestoreObserver<'_> {
163 fn on_member_start(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), FormatError> {
164 self.handler
165 .on_member(member)
166 .map_err(format_error_from_extract_error)
167 }
168
169 fn on_regular_payload(&mut self, bytes: &[u8]) -> Result<(), FormatError> {
170 self.handler
171 .write_regular_payload(bytes)
172 .map_err(format_error_from_extract_error)
173 }
174
175 fn on_member_complete(
176 &mut self,
177 member: &StreamedTarMemberMetadata,
178 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
179 self.handler
180 .finish(member)
181 .map_err(format_error_from_extract_error)
182 }
183}
184
185#[derive(Default)]
186struct LocalMetadata {
187 pax_path: Option<Vec<u8>>,
188 pax_linkpath: Option<Vec<u8>>,
189 pax_size: Option<u64>,
190 gnu_long_name: Option<Vec<u8>>,
191 gnu_long_link: Option<Vec<u8>>,
192 diagnostics: Vec<MetadataDiagnostic>,
193}
194
195pub fn parse_tar_member_group<'a>(
196 group: &'a [u8],
197 max_path_length: u32,
198) -> Result<ParsedTarMember<'a>, FormatError> {
199 if group.len() < TAR_BLOCK_LEN || group.len() % TAR_BLOCK_LEN != 0 {
200 return Err(FormatError::InvalidArchive(
201 "tar member group is not block aligned",
202 ));
203 }
204
205 let mut cursor = 0usize;
206 let mut metadata = LocalMetadata::default();
207
208 loop {
209 let header = slice(group, cursor, TAR_BLOCK_LEN)?;
210 if header.iter().all(|byte| *byte == 0) {
211 return Err(FormatError::InvalidArchive("tar member header is empty"));
212 }
213 verify_tar_checksum(header)?;
214 let typeflag = header[156];
215 let header_size = parse_tar_octal(&header[124..136])?;
216 let is_main = matches!(typeflag, 0 | b'0' | b'5' | b'2' | b'1');
217 let effective_size = if is_main {
218 metadata.pax_size.unwrap_or(header_size)
219 } else {
220 header_size
221 };
222 let payload_start = checked_add(cursor, TAR_BLOCK_LEN)?;
223 let payload_len = to_usize(effective_size)?;
224 let payload_end = checked_add(payload_start, payload_len)?;
225 let padded_end = checked_add(payload_end, padding_to_512(payload_len))?;
226 let payload = slice(group, payload_start, payload_len)?;
227 if padded_end > group.len() {
228 return Err(FormatError::InvalidArchive(
229 "tar member payload exceeds group",
230 ));
231 }
232 if group[payload_end..padded_end].iter().any(|byte| *byte != 0) {
233 return Err(FormatError::InvalidArchive(
234 "tar member padding is non-zero",
235 ));
236 }
237
238 match typeflag {
239 b'x' => {
240 parse_pax_records(payload, &mut metadata)?;
241 cursor = padded_end;
242 }
243 b'g' => {
244 return Err(FormatError::InvalidArchive(
245 "global PAX headers are not allowed",
246 ));
247 }
248 b'V' | b'M' | b'N' => {
249 return Err(FormatError::InvalidArchive(
250 "global GNU headers are not allowed",
251 ));
252 }
253 b'L' => {
254 metadata.gnu_long_name = Some(trimmed_metadata_payload(payload));
255 cursor = padded_end;
256 }
257 b'K' => {
258 metadata.gnu_long_link = Some(trimmed_metadata_payload(payload));
259 cursor = padded_end;
260 }
261 b'S' => {
262 return Err(FormatError::ReaderUnsupported(
263 "unsupported GNU sparse tar entry",
264 ));
265 }
266 0 | b'0' | b'5' | b'2' | b'1' => {
267 if padded_end != group.len() {
268 return Err(FormatError::InvalidArchive(
269 "tar member group has bytes after main entry",
270 ));
271 }
272 let kind = match typeflag {
273 b'5' => TarEntryKind::Directory,
274 b'2' => TarEntryKind::Symlink,
275 b'1' => TarEntryKind::Hardlink,
276 _ => TarEntryKind::Regular,
277 };
278 let mode = parse_tar_octal(&header[100..108])? as u32;
279 let mtime = parse_tar_octal(&header[136..148])?;
280 let path = canonical_main_path(header, kind, &metadata, max_path_length)?;
281 let link_target =
282 canonical_link_target(header, kind, &path, &metadata, max_path_length)?;
283 if kind != TarEntryKind::Regular && effective_size != 0 {
284 return Err(FormatError::InvalidArchive(
285 "non-regular tar entry has non-zero payload size",
286 ));
287 }
288 let logical_size = if kind == TarEntryKind::Regular {
289 effective_size
290 } else {
291 0
292 };
293 return Ok(ParsedTarMember {
294 path,
295 kind,
296 data: if kind == TarEntryKind::Regular {
297 payload
298 } else {
299 &[]
300 },
301 mode,
302 mtime,
303 link_target,
304 logical_size,
305 diagnostics: metadata.diagnostics,
306 });
307 }
308 _ => {
309 return Err(FormatError::ReaderUnsupported("unsupported tar entry type"));
310 }
311 }
312
313 if cursor >= group.len() {
314 return Err(FormatError::InvalidArchive(
315 "tar member group has metadata records but no main entry",
316 ));
317 }
318 }
319}
320
321pub fn validate_tar_stream_total_extraction_size(
322 stream: &[u8],
323 max_path_length: u32,
324 cap: u64,
325) -> Result<(), FormatError> {
326 if stream.len() % TAR_BLOCK_LEN != 0 {
327 return Err(FormatError::InvalidArchive(
328 "tar stream is not block aligned",
329 ));
330 }
331
332 let mut cursor = 0usize;
333 let mut total = 0u64;
334 while cursor < stream.len() {
335 let group_end = tar_member_group_end(stream, cursor)?;
336 let member = parse_tar_member_group(&stream[cursor..group_end], max_path_length)?;
337 if member.kind == TarEntryKind::Regular {
338 total = total
339 .checked_add(member.logical_size)
340 .ok_or(FormatError::InvalidArchive(
341 "total extraction size overflow",
342 ))?;
343 if total > cap {
344 return Err(FormatError::ReaderUnsupported(
345 "total extraction size exceeds configured cap",
346 ));
347 }
348 }
349 cursor = group_end;
350 }
351 Ok(())
352}
353
354pub(crate) struct TarStreamTotalExtractionSizeValidator {
355 cursor: usize,
356 total: u64,
357 max_path_length: u32,
358 cap: u64,
359}
360
361impl TarStreamTotalExtractionSizeValidator {
362 pub(crate) fn new(max_path_length: u32, cap: u64) -> Self {
363 Self {
364 cursor: 0,
365 total: 0,
366 max_path_length,
367 cap,
368 }
369 }
370
371 pub(crate) fn observe(&mut self, stream: &[u8]) -> Result<(), FormatError> {
372 while self.cursor < stream.len() {
373 let Some(group_end) = try_tar_member_group_end(stream, self.cursor)? else {
374 return Ok(());
375 };
376 let member =
377 parse_tar_member_group(&stream[self.cursor..group_end], self.max_path_length)?;
378 if member.kind == TarEntryKind::Regular {
379 self.total = self.total.checked_add(member.logical_size).ok_or(
380 FormatError::InvalidArchive("total extraction size overflow"),
381 )?;
382 if self.total > self.cap {
383 return Err(FormatError::ReaderUnsupported(
384 "total extraction size exceeds configured cap",
385 ));
386 }
387 }
388 self.cursor = group_end;
389 }
390 Ok(())
391 }
392}
393
394pub(crate) struct TarStreamSummaryValidator<O = NoopTarStreamObserver> {
395 state: StreamingTarState,
396 max_path_length: u32,
397 total_extraction_size: u64,
398 extraction_cap: u64,
399 max_metadata_payload_bytes: usize,
400 max_member_count: u64,
401 members: Vec<TarStreamMemberSummary>,
402 observer: O,
403}
404
405impl<O: TarStreamObserver> TarStreamSummaryValidator<O> {
406 pub(crate) fn with_observer(
407 max_path_length: u32,
408 extraction_cap: u64,
409 max_metadata_payload_bytes: usize,
410 max_member_count: u64,
411 observer: O,
412 ) -> Self {
413 Self {
414 state: StreamingTarState::new_member(0),
415 max_path_length,
416 total_extraction_size: 0,
417 extraction_cap,
418 max_metadata_payload_bytes,
419 max_member_count,
420 members: Vec::new(),
421 observer,
422 }
423 }
424
425 pub(crate) fn observe(&mut self, mut input: &[u8]) -> Result<(), FormatError> {
426 while !input.is_empty() {
427 let state = std::mem::replace(&mut self.state, StreamingTarState::new_member(0));
428 let (consumed, next) = self.consume_state(state, input)?;
429 self.state = self.resolve_ready_state(next)?;
430 input = &input[consumed..];
431 }
432 Ok(())
433 }
434
435 fn consume_state(
436 &mut self,
437 state: StreamingTarState,
438 input: &[u8],
439 ) -> Result<(usize, StreamingTarState), FormatError> {
440 match state {
441 StreamingTarState::Header {
442 metadata,
443 group_start,
444 mut group_size,
445 mut header,
446 } => {
447 let needed = TAR_BLOCK_LEN - header.len();
448 let take = needed.min(input.len());
449 header.extend_from_slice(&input[..take]);
450 group_size = checked_u64_add(group_size, take as u64)?;
451 checked_u64_add(group_start, group_size)?;
452 let next = if header.len() == TAR_BLOCK_LEN {
453 let mut header_bytes = [0u8; TAR_BLOCK_LEN];
454 header_bytes.copy_from_slice(&header);
455 self.state_after_header(metadata, group_start, group_size, header_bytes)?
456 } else {
457 StreamingTarState::Header {
458 metadata,
459 group_start,
460 group_size,
461 header,
462 }
463 };
464 Ok((take, next))
465 }
466 StreamingTarState::Payload {
467 metadata,
468 group_start,
469 mut group_size,
470 mut entry,
471 mut remaining,
472 padding_remaining,
473 } => {
474 let take = remaining.min(input.len() as u64) as usize;
475 if let Some(payload) = entry.metadata_payload_mut() {
476 let next_len = checked_add(payload.len(), take)?;
477 if next_len > self.max_metadata_payload_bytes {
478 return Err(FormatError::ReaderUnsupported(
479 "tar metadata payload exceeds configured streaming cap",
480 ));
481 }
482 payload.extend_from_slice(&input[..take]);
483 } else if take > 0 && entry.is_regular_main() {
484 self.observer.on_regular_payload(&input[..take])?;
485 }
486 remaining -= take as u64;
487 group_size = checked_u64_add(group_size, take as u64)?;
488 checked_u64_add(group_start, group_size)?;
489 let next = if remaining == 0 {
490 StreamingTarState::Padding {
491 metadata,
492 group_start,
493 group_size,
494 entry,
495 remaining: padding_remaining,
496 }
497 } else {
498 StreamingTarState::Payload {
499 metadata,
500 group_start,
501 group_size,
502 entry,
503 remaining,
504 padding_remaining,
505 }
506 };
507 Ok((take, next))
508 }
509 StreamingTarState::Padding {
510 metadata,
511 group_start,
512 mut group_size,
513 entry,
514 mut remaining,
515 } => {
516 let take = remaining.min(input.len() as u64) as usize;
517 if input[..take].iter().any(|byte| *byte != 0) {
518 return Err(FormatError::InvalidArchive(
519 "tar member padding is non-zero",
520 ));
521 }
522 remaining -= take as u64;
523 group_size = checked_u64_add(group_size, take as u64)?;
524 checked_u64_add(group_start, group_size)?;
525 let next = if remaining == 0 {
526 self.finish_entry_parts(metadata, group_start, group_size, entry)?
527 } else {
528 StreamingTarState::Padding {
529 metadata,
530 group_start,
531 group_size,
532 entry,
533 remaining,
534 }
535 };
536 Ok((take, next))
537 }
538 }
539 }
540
541 fn resolve_ready_state(
542 &mut self,
543 mut state: StreamingTarState,
544 ) -> Result<StreamingTarState, FormatError> {
545 loop {
546 state = match state {
547 StreamingTarState::Payload {
548 metadata,
549 group_start,
550 group_size,
551 entry,
552 remaining: 0,
553 padding_remaining,
554 } => StreamingTarState::Padding {
555 metadata,
556 group_start,
557 group_size,
558 entry,
559 remaining: padding_remaining,
560 },
561 StreamingTarState::Padding {
562 metadata,
563 group_start,
564 group_size,
565 entry,
566 remaining: 0,
567 } => self.finish_entry_parts(metadata, group_start, group_size, entry)?,
568 other => return Ok(other),
569 };
570 }
571 }
572
573 pub(crate) fn tar_total_size(&self) -> u64 {
574 match &self.state {
575 StreamingTarState::Header {
576 group_start,
577 group_size,
578 ..
579 }
580 | StreamingTarState::Payload {
581 group_start,
582 group_size,
583 ..
584 }
585 | StreamingTarState::Padding {
586 group_start,
587 group_size,
588 ..
589 } => group_start + group_size,
590 }
591 }
592
593 pub(crate) fn finish(self) -> Result<TarStreamSummary, FormatError> {
594 let tar_total_size = self.tar_total_size();
595 match self.state {
596 StreamingTarState::Header {
597 header, group_size, ..
598 } if header.is_empty() && group_size == 0 => Ok(TarStreamSummary {
599 members: self.members,
600 tar_total_size,
601 total_extraction_size: self.total_extraction_size,
602 }),
603 _ => Err(FormatError::InvalidArchive(
604 "tar stream ended inside member group",
605 )),
606 }
607 }
608
609 fn state_after_header(
610 &mut self,
611 mut metadata: LocalMetadata,
612 group_start: u64,
613 group_size: u64,
614 header: [u8; TAR_BLOCK_LEN],
615 ) -> Result<StreamingTarState, FormatError> {
616 if header.iter().all(|byte| *byte == 0) {
617 return Err(FormatError::InvalidArchive("tar member header is empty"));
618 }
619 verify_tar_checksum(&header)?;
620 let typeflag = header[156];
621 let header_size = parse_tar_octal(&header[124..136])?;
622 let is_main = matches!(typeflag, 0 | b'0' | b'5' | b'2' | b'1');
623 let effective_size = if is_main {
624 metadata.pax_size.unwrap_or(header_size)
625 } else {
626 header_size
627 };
628 let padding_remaining = padding_to_512_u64(effective_size);
629
630 let entry = match typeflag {
631 b'x' => PendingTarEntry::LocalPax {
632 payload: Vec::new(),
633 },
634 b'L' => PendingTarEntry::GnuLongName {
635 payload: Vec::new(),
636 },
637 b'K' => PendingTarEntry::GnuLongLink {
638 payload: Vec::new(),
639 },
640 b'g' => {
641 return Err(FormatError::InvalidArchive(
642 "global PAX headers are not allowed",
643 ))
644 }
645 b'V' | b'M' | b'N' => {
646 return Err(FormatError::InvalidArchive(
647 "global GNU headers are not allowed",
648 ))
649 }
650 b'S' => {
651 return Err(FormatError::ReaderUnsupported(
652 "unsupported GNU sparse tar entry",
653 ))
654 }
655 0 | b'0' | b'5' | b'2' | b'1' => {
656 let kind = match typeflag {
657 b'5' => TarEntryKind::Directory,
658 b'2' => TarEntryKind::Symlink,
659 b'1' => TarEntryKind::Hardlink,
660 _ => TarEntryKind::Regular,
661 };
662 let mode = parse_tar_octal(&header[100..108])? as u32;
663 let mtime = parse_tar_octal(&header[136..148])?;
664 let path = canonical_main_path(&header, kind, &metadata, self.max_path_length)?;
665 let link_target =
666 canonical_link_target(&header, kind, &path, &metadata, self.max_path_length)?;
667 if kind != TarEntryKind::Regular && effective_size != 0 {
668 return Err(FormatError::InvalidArchive(
669 "non-regular tar entry has non-zero payload size",
670 ));
671 }
672 let logical_size = if kind == TarEntryKind::Regular {
673 effective_size
674 } else {
675 0
676 };
677 if kind == TarEntryKind::Regular {
678 self.total_extraction_size =
679 self.total_extraction_size.checked_add(logical_size).ok_or(
680 FormatError::InvalidArchive("total extraction size overflow"),
681 )?;
682 if self.total_extraction_size > self.extraction_cap {
683 return Err(FormatError::ReaderUnsupported(
684 "total extraction size exceeds configured cap",
685 ));
686 }
687 }
688 let member = StreamedTarMemberMetadata {
689 path,
690 kind,
691 link_target,
692 mode,
693 mtime,
694 logical_size,
695 diagnostics: std::mem::take(&mut metadata.diagnostics),
696 };
697 self.observer.on_member_start(&member)?;
698 PendingTarEntry::Main {
699 member,
700 group_start,
701 }
702 }
703 _ => return Err(FormatError::ReaderUnsupported("unsupported tar entry type")),
704 };
705
706 self.resolve_ready_state(StreamingTarState::Payload {
707 metadata,
708 group_start,
709 group_size,
710 entry,
711 remaining: effective_size,
712 padding_remaining,
713 })
714 }
715
716 fn finish_entry_parts(
717 &mut self,
718 mut metadata: LocalMetadata,
719 group_start: u64,
720 group_size: u64,
721 entry: PendingTarEntry,
722 ) -> Result<StreamingTarState, FormatError> {
723 match entry {
724 PendingTarEntry::LocalPax { payload } => {
725 parse_pax_records(&payload, &mut metadata)?;
726 Ok(StreamingTarState::Header {
727 metadata,
728 group_start,
729 group_size,
730 header: Vec::new(),
731 })
732 }
733 PendingTarEntry::GnuLongName { payload } => {
734 metadata.gnu_long_name = Some(trimmed_metadata_payload(&payload));
735 Ok(StreamingTarState::Header {
736 metadata,
737 group_start,
738 group_size,
739 header: Vec::new(),
740 })
741 }
742 PendingTarEntry::GnuLongLink { payload } => {
743 metadata.gnu_long_link = Some(trimmed_metadata_payload(&payload));
744 Ok(StreamingTarState::Header {
745 metadata,
746 group_start,
747 group_size,
748 header: Vec::new(),
749 })
750 }
751 PendingTarEntry::Main {
752 member,
753 group_start,
754 } => {
755 if self.members.len() as u64 >= self.max_member_count {
756 return Err(FormatError::ReaderUnsupported(
757 "tar member count exceeds configured streaming cap",
758 ));
759 }
760 let diagnostics = self.observer.on_member_complete(&member)?;
761 self.members.push(TarStreamMemberSummary {
762 path: member.path,
763 kind: member.kind,
764 link_target: member.link_target,
765 mode: member.mode,
766 mtime: member.mtime,
767 logical_size: member.logical_size,
768 diagnostics,
769 group_start,
770 group_size,
771 });
772 Ok(StreamingTarState::new_member(checked_u64_add(
773 group_start,
774 group_size,
775 )?))
776 }
777 }
778 }
779}
780
781enum StreamingTarState {
782 Header {
783 metadata: LocalMetadata,
784 group_start: u64,
785 group_size: u64,
786 header: Vec<u8>,
787 },
788 Payload {
789 metadata: LocalMetadata,
790 group_start: u64,
791 group_size: u64,
792 entry: PendingTarEntry,
793 remaining: u64,
794 padding_remaining: u64,
795 },
796 Padding {
797 metadata: LocalMetadata,
798 group_start: u64,
799 group_size: u64,
800 entry: PendingTarEntry,
801 remaining: u64,
802 },
803}
804
805impl StreamingTarState {
806 fn new_member(group_start: u64) -> Self {
807 Self::Header {
808 metadata: LocalMetadata::default(),
809 group_start,
810 group_size: 0,
811 header: Vec::new(),
812 }
813 }
814}
815
816enum PendingTarEntry {
817 LocalPax {
818 payload: Vec<u8>,
819 },
820 GnuLongName {
821 payload: Vec<u8>,
822 },
823 GnuLongLink {
824 payload: Vec<u8>,
825 },
826 Main {
827 member: StreamedTarMemberMetadata,
828 group_start: u64,
829 },
830}
831
832impl PendingTarEntry {
833 fn metadata_payload_mut(&mut self) -> Option<&mut Vec<u8>> {
834 match self {
835 Self::LocalPax { payload }
836 | Self::GnuLongName { payload }
837 | Self::GnuLongLink { payload } => Some(payload),
838 Self::Main { .. } => None,
839 }
840 }
841
842 fn is_regular_main(&self) -> bool {
843 matches!(
844 self,
845 Self::Main {
846 member: StreamedTarMemberMetadata {
847 kind: TarEntryKind::Regular,
848 ..
849 },
850 ..
851 }
852 )
853 }
854}
855
856fn checked_u64_add(lhs: u64, rhs: u64) -> Result<u64, FormatError> {
857 lhs.checked_add(rhs).ok_or(FormatError::InvalidArchive(
858 "tar member arithmetic overflow",
859 ))
860}
861
862pub(crate) fn try_tar_member_group_end(
863 stream: &[u8],
864 start: usize,
865) -> Result<Option<usize>, FormatError> {
866 let mut cursor = start;
867 let mut metadata = LocalMetadata::default();
868
869 loop {
870 let Some(header) = try_slice(stream, cursor, TAR_BLOCK_LEN)? else {
871 return Ok(None);
872 };
873 if header.iter().all(|byte| *byte == 0) {
874 return Err(FormatError::InvalidArchive("tar member header is empty"));
875 }
876 verify_tar_checksum(header)?;
877 let typeflag = header[156];
878 let header_size = parse_tar_octal(&header[124..136])?;
879 let is_main = matches!(typeflag, 0 | b'0' | b'5' | b'2' | b'1');
880 let effective_size = if is_main {
881 metadata.pax_size.unwrap_or(header_size)
882 } else {
883 header_size
884 };
885 let payload_start = checked_add(cursor, TAR_BLOCK_LEN)?;
886 let payload_len = to_usize(effective_size)?;
887 let payload_end = checked_add(payload_start, payload_len)?;
888 let padded_end = checked_add(payload_end, padding_to_512(payload_len))?;
889 let Some(payload) = try_slice(stream, payload_start, payload_len)? else {
890 return Ok(None);
891 };
892 if padded_end > stream.len() {
893 return Ok(None);
894 }
895 if stream[payload_end..padded_end]
896 .iter()
897 .any(|byte| *byte != 0)
898 {
899 return Err(FormatError::InvalidArchive(
900 "tar member padding is non-zero",
901 ));
902 }
903
904 match typeflag {
905 b'x' => {
906 parse_pax_records(payload, &mut metadata)?;
907 cursor = padded_end;
908 }
909 b'L' | b'K' => {
910 cursor = padded_end;
911 }
912 b'g' => {
913 return Err(FormatError::InvalidArchive(
914 "global PAX headers are not allowed",
915 ));
916 }
917 b'V' | b'M' | b'N' => {
918 return Err(FormatError::InvalidArchive(
919 "global GNU headers are not allowed",
920 ));
921 }
922 b'S' => {
923 return Err(FormatError::ReaderUnsupported(
924 "unsupported GNU sparse tar entry",
925 ));
926 }
927 0 | b'0' | b'5' | b'2' | b'1' => return Ok(Some(padded_end)),
928 _ => return Err(FormatError::ReaderUnsupported("unsupported tar entry type")),
929 }
930
931 if cursor >= stream.len() {
932 return Ok(None);
933 }
934 }
935}
936
937fn try_slice(stream: &[u8], offset: usize, len: usize) -> Result<Option<&[u8]>, FormatError> {
938 let end = checked_add(offset, len)?;
939 if end > stream.len() {
940 return Ok(None);
941 }
942 Ok(Some(&stream[offset..end]))
943}
944
945pub(crate) fn stream_regular_tar_member_group_to_writer<R, W>(
946 reader: &mut R,
947 expected_path: &[u8],
948 expected_file_data_size: u64,
949 group_len: u64,
950 max_path_length: u32,
951 writer: &mut W,
952) -> Result<Vec<MetadataDiagnostic>, ExtractError>
953where
954 R: TarMemberGroupReader,
955 W: Write,
956{
957 let mut handler = RegularWriterHandler { writer };
958 let member = stream_tar_member_group(
959 reader,
960 expected_path,
961 expected_file_data_size,
962 group_len,
963 max_path_length,
964 &mut handler,
965 )?;
966 Ok(member.diagnostics)
967}
968
969pub(crate) fn restore_streaming_tar_member_group<R>(
970 root: &Path,
971 expected_path: &[u8],
972 expected_file_data_size: u64,
973 group_len: u64,
974 max_path_length: u32,
975 options: SafeExtractionOptions,
976 reader: &mut R,
977) -> Result<Vec<MetadataDiagnostic>, ExtractError>
978where
979 R: TarMemberGroupReader,
980{
981 let mut handler = FilesystemRestoreHandler::new(root, options);
982 let member = stream_tar_member_group(
983 reader,
984 expected_path,
985 expected_file_data_size,
986 group_len,
987 max_path_length,
988 &mut handler,
989 )?;
990 handler.finish(&member)
991}
992
993fn stream_tar_member_group<R, H>(
994 reader: &mut R,
995 expected_path: &[u8],
996 expected_file_data_size: u64,
997 group_len: u64,
998 max_path_length: u32,
999 handler: &mut H,
1000) -> Result<StreamedTarMemberMetadata, ExtractError>
1001where
1002 R: TarMemberGroupReader,
1003 H: TarMemberStreamHandler,
1004{
1005 if group_len < TAR_BLOCK_LEN as u64 || group_len % TAR_BLOCK_LEN as u64 != 0 {
1006 return Err(FormatError::InvalidArchive("tar member group is not block aligned").into());
1007 }
1008
1009 let mut remaining = group_len;
1010 let mut metadata = LocalMetadata::default();
1011
1012 loop {
1013 let mut header = [0u8; TAR_BLOCK_LEN];
1014 read_member_bytes(reader, &mut header, &mut remaining)?;
1015 if header.iter().all(|byte| *byte == 0) {
1016 return Err(FormatError::InvalidArchive("tar member header is empty").into());
1017 }
1018 verify_tar_checksum(&header)?;
1019
1020 let typeflag = header[156];
1021 let header_size = parse_tar_octal(&header[124..136])?;
1022 let is_main = matches!(typeflag, 0 | b'0' | b'5' | b'2' | b'1');
1023 let effective_size = if is_main {
1024 metadata.pax_size.unwrap_or(header_size)
1025 } else {
1026 header_size
1027 };
1028 let padding_len = padding_to_512_u64(effective_size);
1029 let entry_payload_len =
1030 effective_size
1031 .checked_add(padding_len)
1032 .ok_or(FormatError::InvalidArchive(
1033 "tar member arithmetic overflow",
1034 ))?;
1035 if entry_payload_len > remaining {
1036 return Err(FormatError::InvalidArchive("tar member payload exceeds group").into());
1037 }
1038
1039 match typeflag {
1040 b'x' => {
1041 let payload = read_member_vec(reader, effective_size, &mut remaining)?;
1042 parse_pax_records(&payload, &mut metadata)?;
1043 read_zero_padding(reader, padding_len, &mut remaining)?;
1044 }
1045 b'g' => {
1046 return Err(
1047 FormatError::InvalidArchive("global PAX headers are not allowed").into(),
1048 );
1049 }
1050 b'V' | b'M' | b'N' => {
1051 return Err(
1052 FormatError::InvalidArchive("global GNU headers are not allowed").into(),
1053 );
1054 }
1055 b'L' => {
1056 let payload = read_member_vec(reader, effective_size, &mut remaining)?;
1057 metadata.gnu_long_name = Some(trimmed_metadata_payload(&payload));
1058 read_zero_padding(reader, padding_len, &mut remaining)?;
1059 }
1060 b'K' => {
1061 let payload = read_member_vec(reader, effective_size, &mut remaining)?;
1062 metadata.gnu_long_link = Some(trimmed_metadata_payload(&payload));
1063 read_zero_padding(reader, padding_len, &mut remaining)?;
1064 }
1065 b'S' => {
1066 return Err(
1067 FormatError::ReaderUnsupported("unsupported GNU sparse tar entry").into(),
1068 );
1069 }
1070 0 | b'0' | b'5' | b'2' | b'1' => {
1071 let kind = match typeflag {
1072 b'5' => TarEntryKind::Directory,
1073 b'2' => TarEntryKind::Symlink,
1074 b'1' => TarEntryKind::Hardlink,
1075 _ => TarEntryKind::Regular,
1076 };
1077 let mode = parse_tar_octal(&header[100..108])? as u32;
1078 let mtime = parse_tar_octal(&header[136..148])?;
1079 let path = canonical_main_path(&header, kind, &metadata, max_path_length)?;
1080 let link_target =
1081 canonical_link_target(&header, kind, &path, &metadata, max_path_length)?;
1082 if kind != TarEntryKind::Regular && effective_size != 0 {
1083 return Err(FormatError::InvalidArchive(
1084 "non-regular tar entry has non-zero payload size",
1085 )
1086 .into());
1087 }
1088 let logical_size = if kind == TarEntryKind::Regular {
1089 effective_size
1090 } else {
1091 0
1092 };
1093 let member = StreamedTarMemberMetadata {
1094 path,
1095 kind,
1096 link_target,
1097 mode,
1098 mtime,
1099 logical_size,
1100 diagnostics: std::mem::take(&mut metadata.diagnostics),
1101 };
1102 if member.path != expected_path {
1103 return Err(FormatError::InvalidArchive(
1104 "tar member path does not match FileEntry path",
1105 )
1106 .into());
1107 }
1108 if member.logical_size != expected_file_data_size {
1109 return Err(FormatError::InvalidArchive(
1110 "tar member size does not match FileEntry file_data_size",
1111 )
1112 .into());
1113 }
1114 handler.on_member(&member)?;
1115 if member.kind == TarEntryKind::Regular {
1116 stream_regular_payload(reader, effective_size, &mut remaining, handler)?;
1117 }
1118 read_zero_padding(reader, padding_len, &mut remaining)?;
1119 if remaining != 0 {
1120 return Err(FormatError::InvalidArchive(
1121 "tar member group has bytes after main entry",
1122 )
1123 .into());
1124 }
1125 return Ok(member);
1126 }
1127 _ => {
1128 return Err(FormatError::ReaderUnsupported("unsupported tar entry type").into());
1129 }
1130 }
1131
1132 if remaining == 0 {
1133 return Err(FormatError::InvalidArchive(
1134 "tar member group has metadata records but no main entry",
1135 )
1136 .into());
1137 }
1138 }
1139}
1140
1141struct RegularWriterHandler<'a, W> {
1142 writer: &'a mut W,
1143}
1144
1145impl<W: Write> TarMemberStreamHandler for RegularWriterHandler<'_, W> {
1146 fn on_member(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), ExtractError> {
1147 if member.kind != TarEntryKind::Regular {
1148 return Err(FormatError::ReaderUnsupported(
1149 "extract_file_to_writer returns only regular file payloads",
1150 )
1151 .into());
1152 }
1153 Ok(())
1154 }
1155
1156 fn write_regular_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError> {
1157 self.writer.write_all(bytes).map_err(ExtractError::Output)
1158 }
1159}
1160
1161struct FilesystemRestoreHandler<'a> {
1162 root: &'a Path,
1163 options: SafeExtractionOptions,
1164 destination: Option<PreparedDestination>,
1165 temp_leaf: Option<PathBuf>,
1166 file: Option<fs::File>,
1167}
1168
1169impl<'a> FilesystemRestoreHandler<'a> {
1170 fn new(root: &'a Path, options: SafeExtractionOptions) -> Self {
1171 Self {
1172 root,
1173 options,
1174 destination: None,
1175 temp_leaf: None,
1176 file: None,
1177 }
1178 }
1179
1180 fn finish(
1181 &mut self,
1182 member: &StreamedTarMemberMetadata,
1183 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
1184 let mut diagnostics = member.diagnostics.clone();
1185 if member.kind != TarEntryKind::Regular {
1186 return Ok(diagnostics);
1187 }
1188
1189 let mut file = self.file.take().ok_or(FormatError::InvalidArchive(
1190 "regular file output is missing",
1191 ))?;
1192 file.flush()
1193 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to write regular file"))?;
1194
1195 let destination = self.destination.take().ok_or(FormatError::InvalidArchive(
1196 "regular file destination is missing",
1197 ))?;
1198 let temp_leaf = self.temp_leaf.take().ok_or(FormatError::InvalidArchive(
1199 "regular file temp path is missing",
1200 ))?;
1201 let file = publish_regular_file(&destination, &temp_leaf, file, self.options)?;
1202 apply_restored_regular_file_metadata_parts(
1203 &file,
1204 member.mode,
1205 member.mtime,
1206 &mut diagnostics,
1207 );
1208 Ok(diagnostics)
1209 }
1210}
1211
1212impl Drop for FilesystemRestoreHandler<'_> {
1213 fn drop(&mut self) {
1214 if let (Some(destination), Some(temp_leaf)) =
1215 (self.destination.as_ref(), self.temp_leaf.take())
1216 {
1217 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
1218 }
1219 }
1220}
1221
1222impl TarMemberStreamHandler for FilesystemRestoreHandler<'_> {
1223 fn on_member(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), ExtractError> {
1224 let destination = prepare_destination(self.root, &member.path, member.kind, self.options)?;
1225 match member.kind {
1226 TarEntryKind::Regular => {
1227 let (temp_leaf, file) = create_temp_regular_file(&destination)?;
1228 self.destination = Some(destination);
1229 self.temp_leaf = Some(temp_leaf);
1230 self.file = Some(file);
1231 }
1232 TarEntryKind::Directory => create_directory(&destination)?,
1233 TarEntryKind::Symlink => {
1234 let target = member
1235 .link_target
1236 .as_deref()
1237 .ok_or(FormatError::InvalidArchive("symlink target is missing"))?;
1238 validate_symlink_target(&member.path, target)?;
1239 create_symlink(&destination, target, self.options)?;
1240 }
1241 TarEntryKind::Hardlink => {
1242 let target = member
1243 .link_target
1244 .as_deref()
1245 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
1246 let target_path = existing_safe_regular_path(self.root, target)?;
1247 create_hardlink(&destination, &target_path, self.options)?;
1248 }
1249 }
1250 Ok(())
1251 }
1252
1253 fn write_regular_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError> {
1254 let file = self.file.as_mut().ok_or(FormatError::InvalidArchive(
1255 "regular file output is missing",
1256 ))?;
1257 file.write_all(bytes)
1258 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to write regular file"))?;
1259 Ok(())
1260 }
1261}
1262
1263fn format_error_from_extract_error(error: ExtractError) -> FormatError {
1264 match error {
1265 ExtractError::Format(error) => error,
1266 ExtractError::Output(_) => {
1267 FormatError::FilesystemExtractionFailed("failed to write regular file")
1268 }
1269 }
1270}
1271
1272fn read_member_bytes<R: TarMemberGroupReader>(
1273 reader: &mut R,
1274 buf: &mut [u8],
1275 remaining: &mut u64,
1276) -> Result<(), ExtractError> {
1277 if buf.len() as u64 > *remaining {
1278 return Err(FormatError::InvalidArchive("tar member payload exceeds group").into());
1279 }
1280 reader.read_exact_member_bytes(buf)?;
1281 *remaining -= buf.len() as u64;
1282 Ok(())
1283}
1284
1285fn read_member_vec<R: TarMemberGroupReader>(
1286 reader: &mut R,
1287 len: u64,
1288 remaining: &mut u64,
1289) -> Result<Vec<u8>, ExtractError> {
1290 let mut out = vec![0u8; to_usize(len)?];
1291 read_member_bytes(reader, &mut out, remaining)?;
1292 Ok(out)
1293}
1294
1295fn read_zero_padding<R: TarMemberGroupReader>(
1296 reader: &mut R,
1297 len: u64,
1298 remaining: &mut u64,
1299) -> Result<(), ExtractError> {
1300 let mut pending = len;
1301 let mut buf = [0u8; 8192];
1302 while pending > 0 {
1303 let chunk_len = pending.min(buf.len() as u64) as usize;
1304 read_member_bytes(reader, &mut buf[..chunk_len], remaining)?;
1305 if buf[..chunk_len].iter().any(|byte| *byte != 0) {
1306 return Err(FormatError::InvalidArchive("tar member padding is non-zero").into());
1307 }
1308 pending -= chunk_len as u64;
1309 }
1310 Ok(())
1311}
1312
1313fn stream_regular_payload<R, H>(
1314 reader: &mut R,
1315 len: u64,
1316 remaining: &mut u64,
1317 handler: &mut H,
1318) -> Result<(), ExtractError>
1319where
1320 R: TarMemberGroupReader,
1321 H: TarMemberStreamHandler,
1322{
1323 let mut pending = len;
1324 let mut buf = [0u8; 64 * 1024];
1325 while pending > 0 {
1326 let chunk_len = pending.min(buf.len() as u64).min(*remaining) as usize;
1327 let read = reader.read_some_member_bytes(&mut buf[..chunk_len])?;
1328 if read == 0 {
1329 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
1330 }
1331 *remaining -= read as u64;
1332 pending -= read as u64;
1333 handler.write_regular_payload(&buf[..read])?;
1334 }
1335 Ok(())
1336}
1337
1338fn tar_member_group_end(stream: &[u8], start: usize) -> Result<usize, FormatError> {
1339 let mut cursor = start;
1340 let mut metadata = LocalMetadata::default();
1341
1342 loop {
1343 let header = slice(stream, cursor, TAR_BLOCK_LEN)?;
1344 if header.iter().all(|byte| *byte == 0) {
1345 return Err(FormatError::InvalidArchive("tar member header is empty"));
1346 }
1347 verify_tar_checksum(header)?;
1348 let typeflag = header[156];
1349 let header_size = parse_tar_octal(&header[124..136])?;
1350 let is_main = matches!(typeflag, 0 | b'0' | b'5' | b'2' | b'1');
1351 let effective_size = if is_main {
1352 metadata.pax_size.unwrap_or(header_size)
1353 } else {
1354 header_size
1355 };
1356 let payload_start = checked_add(cursor, TAR_BLOCK_LEN)?;
1357 let payload_len = to_usize(effective_size)?;
1358 let payload_end = checked_add(payload_start, payload_len)?;
1359 let padded_end = checked_add(payload_end, padding_to_512(payload_len))?;
1360 let payload = slice(stream, payload_start, payload_len)?;
1361 if padded_end > stream.len() {
1362 return Err(FormatError::InvalidArchive(
1363 "tar member payload exceeds stream",
1364 ));
1365 }
1366 if stream[payload_end..padded_end]
1367 .iter()
1368 .any(|byte| *byte != 0)
1369 {
1370 return Err(FormatError::InvalidArchive(
1371 "tar member padding is non-zero",
1372 ));
1373 }
1374
1375 match typeflag {
1376 b'x' => {
1377 parse_pax_records(payload, &mut metadata)?;
1378 cursor = padded_end;
1379 }
1380 b'L' | b'K' => {
1381 cursor = padded_end;
1382 }
1383 b'g' => {
1384 return Err(FormatError::InvalidArchive(
1385 "global PAX headers are not allowed",
1386 ));
1387 }
1388 b'V' | b'M' | b'N' => {
1389 return Err(FormatError::InvalidArchive(
1390 "global GNU headers are not allowed",
1391 ));
1392 }
1393 b'S' => {
1394 return Err(FormatError::ReaderUnsupported(
1395 "unsupported GNU sparse tar entry",
1396 ));
1397 }
1398 0 | b'0' | b'5' | b'2' | b'1' => return Ok(padded_end),
1399 _ => return Err(FormatError::ReaderUnsupported("unsupported tar entry type")),
1400 }
1401
1402 if cursor >= stream.len() {
1403 return Err(FormatError::InvalidArchive(
1404 "tar member group has metadata records but no main entry",
1405 ));
1406 }
1407 }
1408}
1409
1410pub fn restore_tar_member(
1411 root: &Path,
1412 member: &OwnedTarMember,
1413 options: SafeExtractionOptions,
1414) -> Result<Vec<MetadataDiagnostic>, FormatError> {
1415 let destination = prepare_destination(root, &member.path, member.kind, options)?;
1416 let mut diagnostics = member.diagnostics.clone();
1417 match member.kind {
1418 TarEntryKind::Regular => {
1419 let (temp_leaf, mut file) = create_temp_regular_file(&destination)?;
1420 file.write_all(&member.data).map_err(|_| {
1421 FormatError::FilesystemExtractionFailed("failed to write regular file")
1422 })?;
1423 file.flush().map_err(|_| {
1424 FormatError::FilesystemExtractionFailed("failed to write regular file")
1425 })?;
1426 let file = publish_regular_file(&destination, &temp_leaf, file, options)?;
1427 apply_restored_regular_file_metadata(&file, member, &mut diagnostics);
1428 }
1429 TarEntryKind::Directory => create_directory(&destination)?,
1430 TarEntryKind::Symlink => {
1431 let target = member
1432 .link_target
1433 .as_deref()
1434 .ok_or(FormatError::InvalidArchive("symlink target is missing"))?;
1435 validate_symlink_target(&member.path, target)?;
1436 create_symlink(&destination, target, options)?;
1437 }
1438 TarEntryKind::Hardlink => {
1439 let target = member
1440 .link_target
1441 .as_deref()
1442 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
1443 let target_path = existing_safe_regular_path(root, target)?;
1444 create_hardlink(&destination, &target_path, options)?;
1445 }
1446 }
1447 Ok(diagnostics)
1448}
1449
1450fn apply_restored_regular_file_metadata(
1451 file: &fs::File,
1452 member: &OwnedTarMember,
1453 diagnostics: &mut Vec<MetadataDiagnostic>,
1454) {
1455 apply_restored_regular_file_metadata_parts(file, member.mode, member.mtime, diagnostics);
1456}
1457
1458fn apply_restored_regular_file_metadata_parts(
1459 file: &fs::File,
1460 mode: u32,
1461 mtime: u64,
1462 diagnostics: &mut Vec<MetadataDiagnostic>,
1463) {
1464 apply_regular_file_mtime(file, mtime, diagnostics);
1465 apply_regular_file_mode(file, mode, diagnostics);
1466}
1467
1468#[cfg(unix)]
1469fn apply_regular_file_mode(file: &fs::File, mode: u32, diagnostics: &mut Vec<MetadataDiagnostic>) {
1470 match file.metadata() {
1471 Ok(metadata) => {
1472 let mut permissions = metadata.permissions();
1473 permissions.set_mode(mode & 0o7777);
1474 if file.set_permissions(permissions).is_err() {
1475 diagnostics.push(MetadataDiagnostic {
1476 profile: "ustar-baseline",
1477 message: "failed to apply mode metadata",
1478 });
1479 }
1480 }
1481 Err(_) => diagnostics.push(MetadataDiagnostic {
1482 profile: "ustar-baseline",
1483 message: "failed to apply mode metadata",
1484 }),
1485 }
1486}
1487
1488#[cfg(not(unix))]
1489fn apply_regular_file_mode(file: &fs::File, mode: u32, diagnostics: &mut Vec<MetadataDiagnostic>) {
1490 match file.metadata() {
1491 Ok(metadata) => {
1492 let mut permissions = metadata.permissions();
1493 permissions.set_readonly(mode & 0o222 == 0);
1494 if file.set_permissions(permissions).is_err() {
1495 diagnostics.push(MetadataDiagnostic {
1496 profile: "ustar-baseline",
1497 message: "failed to apply mode metadata",
1498 });
1499 return;
1500 }
1501 if mode & 0o777 != 0o444 && mode & 0o777 != 0o666 {
1502 diagnostics.push(MetadataDiagnostic {
1503 profile: "ustar-baseline",
1504 message: "mode metadata was only partially applied on this platform",
1505 });
1506 }
1507 }
1508 Err(_) => diagnostics.push(MetadataDiagnostic {
1509 profile: "ustar-baseline",
1510 message: "failed to apply mode metadata",
1511 }),
1512 }
1513}
1514
1515fn apply_regular_file_mtime(
1516 file: &fs::File,
1517 mtime: u64,
1518 diagnostics: &mut Vec<MetadataDiagnostic>,
1519) {
1520 let Some(modified) = SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(mtime)) else {
1521 diagnostics.push(MetadataDiagnostic {
1522 profile: "ustar-baseline",
1523 message: "failed to apply mtime metadata",
1524 });
1525 return;
1526 };
1527 let times = fs::FileTimes::new().set_modified(modified);
1528 if file.set_times(times).is_err() {
1529 diagnostics.push(MetadataDiagnostic {
1530 profile: "ustar-baseline",
1531 message: "failed to apply mtime metadata",
1532 });
1533 }
1534}
1535
1536fn canonical_main_path(
1537 header: &[u8],
1538 kind: TarEntryKind,
1539 metadata: &LocalMetadata,
1540 max_path_length: u32,
1541) -> Result<Vec<u8>, FormatError> {
1542 let mut path = metadata
1543 .pax_path
1544 .clone()
1545 .or_else(|| metadata.gnu_long_name.clone())
1546 .unwrap_or_else(|| ustar_path(header));
1547 if kind == TarEntryKind::Directory && path.ends_with(b"/") && !path.ends_with(b"//") {
1548 path.pop();
1549 }
1550 validate_file_path_bytes(&path, max_path_length)?;
1551 Ok(path)
1552}
1553
1554fn canonical_link_target(
1555 header: &[u8],
1556 kind: TarEntryKind,
1557 link_path: &[u8],
1558 metadata: &LocalMetadata,
1559 max_path_length: u32,
1560) -> Result<Option<Vec<u8>>, FormatError> {
1561 if !matches!(kind, TarEntryKind::Symlink | TarEntryKind::Hardlink) {
1562 return Ok(None);
1563 }
1564 let target = metadata
1565 .pax_linkpath
1566 .clone()
1567 .or_else(|| metadata.gnu_long_link.clone())
1568 .unwrap_or_else(|| nul_trimmed(&header[157..257]).to_vec());
1569 if target.is_empty() {
1570 return Err(FormatError::UnsafeArchivePath);
1571 }
1572 match kind {
1573 TarEntryKind::Hardlink => validate_file_path_bytes(&target, max_path_length)?,
1574 TarEntryKind::Symlink => validate_symlink_target(link_path, &target)?,
1575 _ => {}
1576 }
1577 Ok(Some(target))
1578}
1579
1580fn parse_pax_records(payload: &[u8], metadata: &mut LocalMetadata) -> Result<(), FormatError> {
1581 let mut cursor = 0usize;
1582 while cursor < payload.len() {
1583 let len_digits_start = cursor;
1584 while cursor < payload.len() && payload[cursor].is_ascii_digit() {
1585 cursor += 1;
1586 }
1587 if cursor == len_digits_start || cursor >= payload.len() || payload[cursor] != b' ' {
1588 return Err(FormatError::InvalidArchive("malformed PAX record"));
1589 }
1590 let len = parse_decimal(&payload[len_digits_start..cursor])?;
1591 let record_start = len_digits_start;
1592 let record_end = checked_add(record_start, len)?;
1593 if record_end > payload.len() || len < 4 {
1594 return Err(FormatError::InvalidArchive("malformed PAX record"));
1595 }
1596 let body_start = cursor + 1;
1597 let record = &payload[body_start..record_end];
1598 if record.last().copied() != Some(b'\n') {
1599 return Err(FormatError::InvalidArchive("malformed PAX record"));
1600 }
1601 let body = &record[..record.len() - 1];
1602 let eq = body
1603 .iter()
1604 .position(|byte| *byte == b'=')
1605 .ok_or(FormatError::InvalidArchive("malformed PAX record"))?;
1606 let key = std::str::from_utf8(&body[..eq])
1607 .map_err(|_| FormatError::InvalidArchive("malformed PAX key"))?;
1608 let value = &body[eq + 1..];
1609 match key {
1610 "path" => metadata.pax_path = Some(value.to_vec()),
1611 "linkpath" => metadata.pax_linkpath = Some(value.to_vec()),
1612 "size" => metadata.pax_size = Some(parse_decimal(value)? as u64),
1613 "atime" | "ctime" | "mtime" => metadata.diagnostics.push(MetadataDiagnostic {
1614 profile: "pax-posix-2001",
1615 message: "unsupported PAX timestamp metadata was ignored",
1616 }),
1617 key if key.starts_with("SCHILY.xattr.")
1618 || key.starts_with("LIBARCHIVE.xattr.")
1619 || key.starts_with("SCHILY.acl.")
1620 || key.starts_with("LIBARCHIVE.acl.") =>
1621 {
1622 metadata.diagnostics.push(MetadataDiagnostic {
1623 profile: "pax-xattrs-acls",
1624 message: "unsupported xattr/ACL PAX metadata was ignored",
1625 });
1626 }
1627 key if key.starts_with("GNU.sparse.") => {
1628 metadata.diagnostics.push(MetadataDiagnostic {
1629 profile: "gnu-sparse",
1630 message: "unsupported sparse-file PAX metadata was ignored",
1631 });
1632 }
1633 _ => metadata.diagnostics.push(MetadataDiagnostic {
1634 profile: "pax-posix-2001",
1635 message: "unsupported PAX key was ignored",
1636 }),
1637 }
1638 cursor = record_end;
1639 }
1640 Ok(())
1641}
1642
1643fn validate_symlink_target(link_path: &[u8], target: &[u8]) -> Result<(), FormatError> {
1644 if target.is_empty()
1645 || target.contains(&0)
1646 || target.contains(&b'\\')
1647 || target.contains(&b':')
1648 || target[0] == b'/'
1649 {
1650 return Err(FormatError::UnsafeArchivePath);
1651 }
1652 let target = std::str::from_utf8(target).map_err(|_| FormatError::UnsafeArchivePath)?;
1653 let link_path = std::str::from_utf8(link_path).map_err(|_| FormatError::UnsafeArchivePath)?;
1654 let mut stack = link_path
1655 .split('/')
1656 .take(link_path.split('/').count().saturating_sub(1))
1657 .map(str::to_owned)
1658 .collect::<Vec<_>>();
1659 for component in target.split('/') {
1660 if component.is_empty() || component == "." {
1661 return Err(FormatError::UnsafeArchivePath);
1662 }
1663 if component == ".." {
1664 if stack.pop().is_none() {
1665 return Err(FormatError::UnsafeArchivePath);
1666 }
1667 } else {
1668 validate_file_path_bytes(component.as_bytes(), u32::MAX)?;
1669 stack.push(component.to_owned());
1670 }
1671 }
1672 Ok(())
1673}
1674
1675struct PreparedDestination {
1676 parent: CapDir,
1677 leaf: PathBuf,
1678}
1679
1680fn prepare_destination(
1681 root: &Path,
1682 archive_path: &[u8],
1683 kind: TarEntryKind,
1684 options: SafeExtractionOptions,
1685) -> Result<PreparedDestination, FormatError> {
1686 let components = path_components(archive_path)?;
1687 let mut parent = open_extraction_root(root)?;
1688 for component in &components[..components.len().saturating_sub(1)] {
1689 parent = open_or_create_safe_child_dir(&parent, component)?;
1690 }
1691
1692 let leaf = PathBuf::from(components.last().ok_or(FormatError::UnsafeArchivePath)?);
1693 match parent.symlink_metadata(&leaf) {
1694 Ok(metadata) => {
1695 let file_type = metadata.file_type();
1696 if file_type.is_symlink() {
1697 return Err(FormatError::UnsafeArchivePath);
1698 }
1699 if kind == TarEntryKind::Directory {
1700 if file_type.is_dir() {
1701 return Ok(PreparedDestination { parent, leaf });
1702 }
1703 return Err(FormatError::UnsafeOverwrite);
1704 }
1705 if file_type.is_dir() {
1706 return Err(FormatError::UnsafeOverwrite);
1707 }
1708 if !options.overwrite_existing {
1709 return Err(FormatError::UnsafeOverwrite);
1710 }
1711 }
1712 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1713 Err(_) => {
1714 return Err(FormatError::FilesystemExtractionFailed(
1715 "failed to inspect destination",
1716 ));
1717 }
1718 }
1719 Ok(PreparedDestination { parent, leaf })
1720}
1721
1722fn open_extraction_root(root: &Path) -> Result<CapDir, FormatError> {
1723 let metadata = fs::symlink_metadata(root).map_err(|_| {
1724 FormatError::FilesystemExtractionFailed("extraction root must already exist")
1725 })?;
1726 if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
1727 return Err(FormatError::UnsafeArchivePath);
1728 }
1729 CapDir::open_ambient_dir(root, ambient_authority())
1730 .map_err(|_| FormatError::FilesystemExtractionFailed("extraction root must already exist"))
1731}
1732
1733fn open_or_create_safe_child_dir(parent: &CapDir, component: &str) -> Result<CapDir, FormatError> {
1734 match parent.open_dir_nofollow(component) {
1735 Ok(child) => return Ok(child),
1736 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1737 Err(_) => return Err(FormatError::UnsafeArchivePath),
1738 }
1739
1740 match parent.create_dir(component) {
1741 Ok(()) => {}
1742 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
1743 Err(_) => {
1744 return Err(FormatError::FilesystemExtractionFailed(
1745 "failed to create parent directory",
1746 ));
1747 }
1748 }
1749 parent
1750 .open_dir_nofollow(component)
1751 .map_err(|_| FormatError::UnsafeArchivePath)
1752}
1753
1754fn existing_safe_regular_path(
1755 root: &Path,
1756 archive_path: &[u8],
1757) -> Result<PreparedDestination, FormatError> {
1758 validate_file_path_bytes(archive_path, u32::MAX)?;
1759 let components = path_components(archive_path)?;
1760 let mut parent = open_extraction_root(root)?;
1761 for component in &components[..components.len().saturating_sub(1)] {
1762 parent = parent
1763 .open_dir_nofollow(component)
1764 .map_err(|_| FormatError::UnsafeArchivePath)?;
1765 }
1766
1767 let leaf = PathBuf::from(components.last().ok_or(FormatError::UnsafeArchivePath)?);
1768 let metadata = parent
1769 .symlink_metadata(&leaf)
1770 .map_err(|_| FormatError::UnsafeArchivePath)?;
1771 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
1772 return Err(FormatError::UnsafeArchivePath);
1773 }
1774 Ok(PreparedDestination { parent, leaf })
1775}
1776
1777fn create_new_file_options() -> CapOpenOptions {
1778 let mut options = CapOpenOptions::new();
1779 options
1780 .read(true)
1781 .write(true)
1782 .create_new(true)
1783 .follow(FollowSymlinks::No);
1784 options
1785}
1786
1787fn create_temp_regular_file(
1788 destination: &PreparedDestination,
1789) -> Result<(PathBuf, fs::File), FormatError> {
1790 for _ in 0..1000u32 {
1791 let mut candidate = destination.leaf.as_os_str().to_os_string();
1792 candidate.push(format!(".tzap-tmp-{}", uuid::Uuid::new_v4()));
1793 let leaf = PathBuf::from(candidate);
1794 match destination
1795 .parent
1796 .open_with(&leaf, &create_new_file_options())
1797 {
1798 Ok(file) => return Ok((leaf, file.into_std())),
1799 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
1800 Err(_) => {
1801 return Err(FormatError::FilesystemExtractionFailed(
1802 "failed to create regular file",
1803 ));
1804 }
1805 }
1806 }
1807 Err(FormatError::FilesystemExtractionFailed(
1808 "failed to create regular file",
1809 ))
1810}
1811
1812fn publish_regular_file(
1813 destination: &PreparedDestination,
1814 temp_leaf: &Path,
1815 mut temp_file: fs::File,
1816 options: SafeExtractionOptions,
1817) -> Result<fs::File, FormatError> {
1818 if options.overwrite_existing {
1819 remove_existing_leaf_if_needed(destination)?;
1820 }
1821
1822 let mut output = match destination
1823 .parent
1824 .open_with(&destination.leaf, &create_new_file_options())
1825 {
1826 Ok(file) => file.into_std(),
1827 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
1828 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
1829 return Err(FormatError::UnsafeOverwrite);
1830 }
1831 Err(_) => {
1832 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
1833 return Err(FormatError::FilesystemExtractionFailed(
1834 "failed to create regular file",
1835 ));
1836 }
1837 };
1838
1839 let copy_result = temp_file
1840 .seek(SeekFrom::Start(0))
1841 .and_then(|_| std::io::copy(&mut temp_file, &mut output))
1842 .and_then(|_| output.flush());
1843
1844 if copy_result.is_err() {
1845 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
1846 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
1847 return Err(FormatError::FilesystemExtractionFailed(
1848 "failed to write regular file",
1849 ));
1850 }
1851
1852 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
1853 Ok(output)
1854}
1855
1856fn remove_existing_leaf_if_needed(destination: &PreparedDestination) -> Result<(), FormatError> {
1857 match destination.parent.symlink_metadata(&destination.leaf) {
1858 Ok(metadata) => {
1859 if metadata.file_type().is_dir() {
1860 return Err(FormatError::UnsafeOverwrite);
1861 }
1862 destination
1863 .parent
1864 .remove_file_or_symlink(&destination.leaf)
1865 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to remove old file"))
1866 }
1867 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
1868 Err(_) => Err(FormatError::FilesystemExtractionFailed(
1869 "failed to inspect destination",
1870 )),
1871 }
1872}
1873
1874fn create_directory(destination: &PreparedDestination) -> Result<(), FormatError> {
1875 match destination.parent.create_dir(&destination.leaf) {
1876 Ok(()) => Ok(()),
1877 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
1878 let metadata = destination
1879 .parent
1880 .symlink_metadata(&destination.leaf)
1881 .map_err(|_| FormatError::UnsafeOverwrite)?;
1882 let file_type = metadata.file_type();
1883 if file_type.is_symlink() {
1884 Err(FormatError::UnsafeArchivePath)
1885 } else if file_type.is_dir() {
1886 Ok(())
1887 } else {
1888 Err(FormatError::UnsafeOverwrite)
1889 }
1890 }
1891 Err(_) => Err(FormatError::FilesystemExtractionFailed(
1892 "failed to create directory",
1893 )),
1894 }
1895}
1896
1897fn create_hardlink(
1898 destination: &PreparedDestination,
1899 target: &PreparedDestination,
1900 options: SafeExtractionOptions,
1901) -> Result<(), FormatError> {
1902 if options.overwrite_existing {
1903 remove_existing_leaf_if_needed(destination)?;
1904 }
1905 match target
1906 .parent
1907 .hard_link(&target.leaf, &destination.parent, &destination.leaf)
1908 {
1909 Ok(()) => {
1910 let metadata = destination
1911 .parent
1912 .symlink_metadata(&destination.leaf)
1913 .map_err(|_| {
1914 FormatError::FilesystemExtractionFailed("failed to inspect hardlink")
1915 })?;
1916 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
1917 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
1918 return Err(FormatError::UnsafeArchivePath);
1919 }
1920 Ok(())
1921 }
1922 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
1923 Err(FormatError::UnsafeOverwrite)
1924 }
1925 Err(_) => Err(FormatError::FilesystemExtractionFailed(
1926 "failed to create hardlink",
1927 )),
1928 }
1929}
1930
1931fn create_symlink(
1932 destination: &PreparedDestination,
1933 target: &[u8],
1934 options: SafeExtractionOptions,
1935) -> Result<(), FormatError> {
1936 if options.overwrite_existing {
1937 remove_existing_leaf_if_needed(destination)?;
1938 }
1939 let target = std::str::from_utf8(target).map_err(|_| FormatError::UnsafeArchivePath)?;
1940 match destination.parent.symlink_file(target, &destination.leaf) {
1941 Ok(()) => Ok(()),
1942 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
1943 Err(FormatError::UnsafeOverwrite)
1944 }
1945 Err(_) => Err(FormatError::FilesystemExtractionFailed(
1946 "failed to create symlink",
1947 )),
1948 }
1949}
1950
1951fn path_components(path: &[u8]) -> Result<Vec<String>, FormatError> {
1952 validate_file_path_bytes(path, u32::MAX)?;
1953 let path = std::str::from_utf8(path).map_err(|_| FormatError::UnsafeArchivePath)?;
1954 Ok(path.split('/').map(str::to_owned).collect())
1955}
1956
1957fn ustar_path(header: &[u8]) -> Vec<u8> {
1958 let name = nul_trimmed(&header[0..100]);
1959 let prefix = nul_trimmed(&header[345..500]);
1960 if prefix.is_empty() {
1961 name.to_vec()
1962 } else {
1963 let mut out = Vec::with_capacity(prefix.len() + 1 + name.len());
1964 out.extend_from_slice(prefix);
1965 out.push(b'/');
1966 out.extend_from_slice(name);
1967 out
1968 }
1969}
1970
1971fn trimmed_metadata_payload(payload: &[u8]) -> Vec<u8> {
1972 let mut end = payload.len();
1973 while end > 0 && payload[end - 1] == 0 {
1974 end -= 1;
1975 }
1976 payload[..end].to_vec()
1977}
1978
1979fn verify_tar_checksum(header: &[u8]) -> Result<(), FormatError> {
1980 let stored = parse_tar_octal(&header[148..156])?;
1981 let mut sum = 0u64;
1982 for (idx, byte) in header.iter().enumerate() {
1983 if (148..156).contains(&idx) {
1984 sum += b' ' as u64;
1985 } else {
1986 sum += *byte as u64;
1987 }
1988 }
1989 if stored != sum {
1990 return Err(FormatError::InvalidArchive("tar header checksum mismatch"));
1991 }
1992 Ok(())
1993}
1994
1995fn parse_tar_octal(field: &[u8]) -> Result<u64, FormatError> {
1996 let mut value = 0u64;
1997 let mut saw_digit = false;
1998 for byte in field {
1999 match *byte {
2000 0 | b' ' if saw_digit => break,
2001 0 | b' ' => {}
2002 b'0'..=b'7' => {
2003 saw_digit = true;
2004 value = value
2005 .checked_mul(8)
2006 .and_then(|acc| acc.checked_add((*byte - b'0') as u64))
2007 .ok_or(FormatError::InvalidArchive("tar octal field overflow"))?;
2008 }
2009 _ => return Err(FormatError::InvalidArchive("malformed tar octal field")),
2010 }
2011 }
2012 Ok(value)
2013}
2014
2015fn parse_decimal(field: &[u8]) -> Result<usize, FormatError> {
2016 let mut value = 0usize;
2017 if field.is_empty() {
2018 return Err(FormatError::InvalidArchive("malformed decimal field"));
2019 }
2020 for byte in field {
2021 if !byte.is_ascii_digit() {
2022 return Err(FormatError::InvalidArchive("malformed decimal field"));
2023 }
2024 value = value
2025 .checked_mul(10)
2026 .and_then(|acc| acc.checked_add((byte - b'0') as usize))
2027 .ok_or(FormatError::InvalidArchive("decimal field overflow"))?;
2028 }
2029 Ok(value)
2030}
2031
2032fn nul_trimmed(bytes: &[u8]) -> &[u8] {
2033 let end = bytes
2034 .iter()
2035 .position(|byte| *byte == 0)
2036 .unwrap_or(bytes.len());
2037 &bytes[..end]
2038}
2039
2040fn padding_to_512(len: usize) -> usize {
2041 let remainder = len % TAR_BLOCK_LEN;
2042 if remainder == 0 {
2043 0
2044 } else {
2045 TAR_BLOCK_LEN - remainder
2046 }
2047}
2048
2049fn padding_to_512_u64(len: u64) -> u64 {
2050 let remainder = len % TAR_BLOCK_LEN as u64;
2051 if remainder == 0 {
2052 0
2053 } else {
2054 TAR_BLOCK_LEN as u64 - remainder
2055 }
2056}
2057
2058fn slice(bytes: &[u8], offset: usize, len: usize) -> Result<&[u8], FormatError> {
2059 let end = checked_add(offset, len)?;
2060 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
2061 structure: "tar member",
2062 expected: end,
2063 actual: bytes.len(),
2064 })
2065}
2066
2067fn checked_add(lhs: usize, rhs: usize) -> Result<usize, FormatError> {
2068 lhs.checked_add(rhs).ok_or(FormatError::InvalidArchive(
2069 "tar member arithmetic overflow",
2070 ))
2071}
2072
2073fn to_usize(value: u64) -> Result<usize, FormatError> {
2074 usize::try_from(value).map_err(|_| FormatError::InvalidArchive("tar member size overflow"))
2075}
2076
2077#[cfg(test)]
2078mod tests {
2079 use super::*;
2080 use tempfile::tempdir;
2081
2082 fn header(path: &[u8], kind: u8, size: usize, link: &[u8]) -> [u8; TAR_BLOCK_LEN] {
2083 let mut header = [0u8; TAR_BLOCK_LEN];
2084 header[..path.len()].copy_from_slice(path);
2085 write_octal(&mut header[100..108], 0o644);
2086 write_octal(&mut header[108..116], 0);
2087 write_octal(&mut header[116..124], 0);
2088 write_octal(&mut header[124..136], size as u64);
2089 write_octal(&mut header[136..148], 0);
2090 header[148..156].fill(b' ');
2091 header[156] = kind;
2092 header[157..157 + link.len()].copy_from_slice(link);
2093 header[257..263].copy_from_slice(b"ustar\0");
2094 header[263..265].copy_from_slice(b"00");
2095 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
2096 write_checksum(&mut header[148..156], checksum);
2097 header
2098 }
2099
2100 fn member(path: &[u8], kind: u8, data: &[u8], link: &[u8]) -> Vec<u8> {
2101 member_with_declared_size(path, kind, data.len(), data, link)
2102 }
2103
2104 fn member_with_declared_size(
2105 path: &[u8],
2106 kind: u8,
2107 declared_size: usize,
2108 data: &[u8],
2109 link: &[u8],
2110 ) -> Vec<u8> {
2111 let mut out = Vec::new();
2112 out.extend_from_slice(&header(path, kind, declared_size, link));
2113 out.extend_from_slice(data);
2114 out.resize(out.len() + padding_to_512(data.len()), 0);
2115 out
2116 }
2117
2118 fn member_with_prefix(prefix: &[u8], path: &[u8], kind: u8, data: &[u8]) -> Vec<u8> {
2119 let mut header = header(path, kind, data.len(), b"");
2120 header[345..345 + prefix.len()].copy_from_slice(prefix);
2121 header[148..156].fill(b' ');
2122 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
2123 write_checksum(&mut header[148..156], checksum);
2124
2125 let mut out = Vec::new();
2126 out.extend_from_slice(&header);
2127 out.extend_from_slice(data);
2128 out.resize(out.len() + padding_to_512(data.len()), 0);
2129 out
2130 }
2131
2132 fn pax_record(key: &str, value: &[u8]) -> Vec<u8> {
2133 let mut len = key.len() + value.len() + 4;
2134 loop {
2135 let candidate = len.to_string().len() + 1 + key.len() + 1 + value.len() + 1;
2136 if candidate == len {
2137 break;
2138 }
2139 len = candidate;
2140 }
2141 let mut out = Vec::new();
2142 out.extend_from_slice(len.to_string().as_bytes());
2143 out.push(b' ');
2144 out.extend_from_slice(key.as_bytes());
2145 out.push(b'=');
2146 out.extend_from_slice(value);
2147 out.push(b'\n');
2148 out
2149 }
2150
2151 fn write_octal(field: &mut [u8], value: u64) {
2152 let digits = format!("{value:o}");
2153 field.fill(0);
2154 let start = field.len() - 1 - digits.len();
2155 field[..start].fill(b'0');
2156 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
2157 }
2158
2159 fn write_checksum(field: &mut [u8], value: u64) {
2160 let digits = format!("{value:06o}");
2161 field[0..6].copy_from_slice(digits.as_bytes());
2162 field[6] = 0;
2163 field[7] = b' ';
2164 }
2165
2166 #[test]
2167 fn parses_ustar_regular_member() {
2168 let bytes = member(b"dir/file.txt", b'0', b"hello", b"");
2169 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
2170
2171 assert_eq!(parsed.kind, TarEntryKind::Regular);
2172 assert_eq!(parsed.path, b"dir/file.txt");
2173 assert_eq!(parsed.data, b"hello");
2174 assert_eq!(parsed.logical_size, 5);
2175 }
2176
2177 #[test]
2178 fn canonicalizes_one_directory_trailing_slash_only_for_directories() {
2179 let dir = member(b"dir/", b'5', b"", b"");
2180 assert_eq!(parse_tar_member_group(&dir, 4096).unwrap().path, b"dir");
2181
2182 let file = member(b"dir/", b'0', b"", b"");
2183 assert_eq!(
2184 parse_tar_member_group(&file, 4096).unwrap_err(),
2185 FormatError::UnsafeArchivePath
2186 );
2187 }
2188
2189 #[test]
2190 fn rejects_global_pax_headers() {
2191 let bytes = member(b"pax", b'g', b"11 path=x\n", b"");
2192 assert_eq!(
2193 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2194 FormatError::InvalidArchive("global PAX headers are not allowed")
2195 );
2196 }
2197
2198 #[test]
2199 fn rejects_global_pax_before_main_entry() {
2200 let global_pax = pax_record("path", b"poisoned.txt");
2201 let mut bytes = member(b"GlobalHead/path", b'g', &global_pax, b"");
2202 bytes.extend_from_slice(&member(b"safe.txt", b'0', b"abc", b""));
2203
2204 assert_eq!(
2205 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2206 FormatError::InvalidArchive("global PAX headers are not allowed")
2207 );
2208 }
2209
2210 #[test]
2211 fn rejects_global_gnu_headers() {
2212 for typeflag in [b'V', b'M', b'N'] {
2213 let bytes = member(b"global", typeflag, b"archive-label", b"");
2214
2215 assert_eq!(
2216 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2217 FormatError::InvalidArchive("global GNU headers are not allowed"),
2218 "typeflag {typeflag:?}"
2219 );
2220 }
2221 }
2222
2223 #[test]
2224 fn rejects_unsupported_gnu_sparse_entry_type() {
2225 let bytes = member(b"sparse.bin", b'S', b"", b"");
2226
2227 assert_eq!(
2228 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2229 FormatError::ReaderUnsupported("unsupported GNU sparse tar entry")
2230 );
2231 }
2232
2233 #[test]
2234 fn applies_local_pax_path_and_size() {
2235 let pax = pax_record("path", b"long/name.txt");
2236 let mut bytes = member(b"PaxHeaders/name", b'x', &pax, b"");
2237 bytes.extend_from_slice(&member(b"short", b'0', b"abc", b""));
2238
2239 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
2240 assert_eq!(parsed.path, b"long/name.txt");
2241 assert_eq!(parsed.data, b"abc");
2242 }
2243
2244 #[test]
2245 fn applies_local_gnu_long_name_and_link_to_following_entry() {
2246 let mut named = member(b"././@LongLink", b'L', b"long/path.txt\0", b"");
2247 named.extend_from_slice(&member(b"short", b'0', b"abc", b""));
2248 let parsed = parse_tar_member_group(&named, 4096).unwrap();
2249 assert_eq!(parsed.path, b"long/path.txt");
2250 assert_eq!(parsed.data, b"abc");
2251
2252 let mut linked = member(b"././@LongLink", b'K', b"target/file.txt\0", b"");
2253 linked.extend_from_slice(&member(b"short-link", b'2', b"", b"fallback"));
2254 let parsed = parse_tar_member_group(&linked, 4096).unwrap();
2255 assert_eq!(parsed.path, b"short-link");
2256 assert_eq!(
2257 parsed.link_target.as_deref(),
2258 Some(b"target/file.txt".as_slice())
2259 );
2260 }
2261
2262 #[test]
2263 fn supported_tar_metadata_profile_matrix_matches_buffered_and_streaming_parsers() {
2264 struct Case {
2265 name: &'static str,
2266 bytes: Vec<u8>,
2267 expected_path: &'static [u8],
2268 expected_kind: TarEntryKind,
2269 expected_data: &'static [u8],
2270 expected_link_target: Option<&'static [u8]>,
2271 expected_logical_size: u64,
2272 }
2273
2274 let mut pax_path_and_size = member(
2275 b"PaxHeaders/pax-file",
2276 b'x',
2277 &[
2278 pax_record("path", b"deep/pax-name.txt"),
2279 pax_record("size", b"5"),
2280 ]
2281 .concat(),
2282 b"",
2283 );
2284 pax_path_and_size.extend_from_slice(&member_with_declared_size(
2285 b"fallback",
2286 b'0',
2287 0,
2288 b"hello",
2289 b"",
2290 ));
2291
2292 let mut pax_linkpath = member(
2293 b"PaxHeaders/pax-link",
2294 b'x',
2295 &pax_record("linkpath", b"target/file.txt"),
2296 b"",
2297 );
2298 pax_linkpath.extend_from_slice(&member(b"links/link", b'2', b"", b"fallback-target"));
2299
2300 let mut gnu_long_name = member(b"././@LongLink", b'L', b"long/path/name.txt\0", b"");
2301 gnu_long_name.extend_from_slice(&member(b"short", b'0', b"abc", b""));
2302
2303 let mut gnu_long_link = member(b"././@LongLink", b'K', b"target/hard.txt\0", b"");
2304 gnu_long_link.extend_from_slice(&member(b"links/hard", b'1', b"", b"fallback"));
2305
2306 let cases = vec![
2307 Case {
2308 name: "regular ustar member",
2309 bytes: member(b"dir/file.txt", b'0', b"hello", b""),
2310 expected_path: b"dir/file.txt",
2311 expected_kind: TarEntryKind::Regular,
2312 expected_data: b"hello",
2313 expected_link_target: None,
2314 expected_logical_size: 5,
2315 },
2316 Case {
2317 name: "ustar prefix plus name",
2318 bytes: member_with_prefix(b"dir/prefix", b"file.txt", b'0', b"abc"),
2319 expected_path: b"dir/prefix/file.txt",
2320 expected_kind: TarEntryKind::Regular,
2321 expected_data: b"abc",
2322 expected_link_target: None,
2323 expected_logical_size: 3,
2324 },
2325 Case {
2326 name: "directory trailing slash",
2327 bytes: member(b"dir/", b'5', b"", b""),
2328 expected_path: b"dir",
2329 expected_kind: TarEntryKind::Directory,
2330 expected_data: b"",
2331 expected_link_target: None,
2332 expected_logical_size: 0,
2333 },
2334 Case {
2335 name: "local pax path and size",
2336 bytes: pax_path_and_size,
2337 expected_path: b"deep/pax-name.txt",
2338 expected_kind: TarEntryKind::Regular,
2339 expected_data: b"hello",
2340 expected_link_target: None,
2341 expected_logical_size: 5,
2342 },
2343 Case {
2344 name: "local pax linkpath",
2345 bytes: pax_linkpath,
2346 expected_path: b"links/link",
2347 expected_kind: TarEntryKind::Symlink,
2348 expected_data: b"",
2349 expected_link_target: Some(b"target/file.txt"),
2350 expected_logical_size: 0,
2351 },
2352 Case {
2353 name: "gnu long name",
2354 bytes: gnu_long_name,
2355 expected_path: b"long/path/name.txt",
2356 expected_kind: TarEntryKind::Regular,
2357 expected_data: b"abc",
2358 expected_link_target: None,
2359 expected_logical_size: 3,
2360 },
2361 Case {
2362 name: "gnu long link",
2363 bytes: gnu_long_link,
2364 expected_path: b"links/hard",
2365 expected_kind: TarEntryKind::Hardlink,
2366 expected_data: b"",
2367 expected_link_target: Some(b"target/hard.txt"),
2368 expected_logical_size: 0,
2369 },
2370 ];
2371
2372 for case in cases {
2373 let parsed = parse_tar_member_group(&case.bytes, 4096).unwrap_or_else(|err| {
2374 panic!("{} should parse in buffered tar parser: {err:?}", case.name)
2375 });
2376 assert_eq!(parsed.path, case.expected_path, "{}", case.name);
2377 assert_eq!(parsed.kind, case.expected_kind, "{}", case.name);
2378 assert_eq!(parsed.data, case.expected_data, "{}", case.name);
2379 assert_eq!(
2380 parsed.link_target.as_deref(),
2381 case.expected_link_target,
2382 "{}",
2383 case.name
2384 );
2385 assert_eq!(
2386 parsed.logical_size, case.expected_logical_size,
2387 "{}",
2388 case.name
2389 );
2390
2391 let mut streaming = TarStreamSummaryValidator::with_observer(
2392 4096,
2393 u64::MAX,
2394 4096,
2395 16,
2396 NoopTarStreamObserver,
2397 );
2398 streaming.observe(&case.bytes).unwrap_or_else(|err| {
2399 panic!(
2400 "{} should parse in streaming tar parser: {err:?}",
2401 case.name
2402 )
2403 });
2404 let summary = streaming.finish().unwrap_or_else(|err| {
2405 panic!(
2406 "{} should finish in streaming tar parser: {err:?}",
2407 case.name
2408 )
2409 });
2410 assert_eq!(summary.members.len(), 1, "{}", case.name);
2411 let member = &summary.members[0];
2412 assert_eq!(member.path, case.expected_path, "{}", case.name);
2413 assert_eq!(member.kind, case.expected_kind, "{}", case.name);
2414 assert_eq!(
2415 member.link_target.as_deref(),
2416 case.expected_link_target,
2417 "{}",
2418 case.name
2419 );
2420 assert_eq!(
2421 member.logical_size, case.expected_logical_size,
2422 "{}",
2423 case.name
2424 );
2425 }
2426 }
2427
2428 #[test]
2429 fn tar_metadata_rejects_unsafe_or_inconsistent_overrides_matrix() {
2430 let mut pax_absolute_path = member(
2431 b"PaxHeaders/file",
2432 b'x',
2433 &pax_record("path", b"/absolute"),
2434 b"",
2435 );
2436 pax_absolute_path.extend_from_slice(&member(b"fallback", b'0', b"abc", b""));
2437
2438 let mut pax_parent_path = member(
2439 b"PaxHeaders/file",
2440 b'x',
2441 &pax_record("path", b"../escape"),
2442 b"",
2443 );
2444 pax_parent_path.extend_from_slice(&member(b"fallback", b'0', b"abc", b""));
2445
2446 let mut pax_absolute_link = member(
2447 b"PaxHeaders/link",
2448 b'x',
2449 &pax_record("linkpath", b"/target"),
2450 b"",
2451 );
2452 pax_absolute_link.extend_from_slice(&member(b"links/link", b'2', b"", b"safe"));
2453
2454 let mut gnu_unsafe_name = member(b"././@LongLink", b'L', b"bad:name.txt\0", b"");
2455 gnu_unsafe_name.extend_from_slice(&member(b"fallback", b'0', b"abc", b""));
2456
2457 let mut gnu_parent_hardlink = member(b"././@LongLink", b'K', b"../target.txt\0", b"");
2458 gnu_parent_hardlink.extend_from_slice(&member(b"links/hard", b'1', b"", b"safe"));
2459
2460 let mut pax_size_on_directory =
2461 member(b"PaxHeaders/dir", b'x', &pax_record("size", b"1"), b"");
2462 pax_size_on_directory
2463 .extend_from_slice(&member_with_declared_size(b"dir", b'5', 0, b"x", b""));
2464
2465 for (name, bytes, expected) in [
2466 (
2467 "pax absolute path",
2468 pax_absolute_path,
2469 FormatError::UnsafeArchivePath,
2470 ),
2471 (
2472 "pax parent path",
2473 pax_parent_path,
2474 FormatError::UnsafeArchivePath,
2475 ),
2476 (
2477 "pax absolute symlink target",
2478 pax_absolute_link,
2479 FormatError::UnsafeArchivePath,
2480 ),
2481 (
2482 "gnu unsafe long name",
2483 gnu_unsafe_name,
2484 FormatError::UnsafeArchivePath,
2485 ),
2486 (
2487 "gnu hardlink parent target",
2488 gnu_parent_hardlink,
2489 FormatError::UnsafeArchivePath,
2490 ),
2491 (
2492 "pax size on directory",
2493 pax_size_on_directory,
2494 FormatError::InvalidArchive("non-regular tar entry has non-zero payload size"),
2495 ),
2496 ] {
2497 assert_eq!(
2498 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2499 expected,
2500 "{name}"
2501 );
2502
2503 let mut streaming = TarStreamSummaryValidator::with_observer(
2504 4096,
2505 u64::MAX,
2506 4096,
2507 16,
2508 NoopTarStreamObserver,
2509 );
2510 assert_eq!(streaming.observe(&bytes).unwrap_err(), expected, "{name}");
2511 }
2512 }
2513
2514 #[test]
2515 fn pax_size_exceeding_available_group_is_rejected_by_buffered_and_streaming_parsers() {
2516 let mut bytes = member(b"PaxHeaders/file", b'x', &pax_record("size", b"4096"), b"");
2517 bytes.extend_from_slice(&member_with_declared_size(b"file", b'0', 0, b"short", b""));
2518
2519 assert_eq!(
2520 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2521 FormatError::InvalidLength {
2522 structure: "tar member",
2523 expected: 5632,
2524 actual: 2048,
2525 }
2526 );
2527
2528 let mut streaming = TarStreamSummaryValidator::with_observer(
2529 4096,
2530 u64::MAX,
2531 4096,
2532 16,
2533 NoopTarStreamObserver,
2534 );
2535 streaming.observe(&bytes).unwrap();
2536 assert_eq!(
2537 streaming.finish().unwrap_err(),
2538 FormatError::InvalidArchive("tar stream ended inside member group")
2539 );
2540 }
2541
2542 #[test]
2543 fn malformed_pax_record_matrix_rejects_before_metadata_is_trusted() {
2544 let cases: Vec<(&str, Vec<u8>)> = vec![
2545 ("missing length", b"path=file\n".to_vec()),
2546 ("missing space", b"12path=file\n".to_vec()),
2547 ("record too short", b"3 a\n".to_vec()),
2548 ("missing newline", b"11 path=file".to_vec()),
2549 ("missing equals", b"10 pathfile\n".to_vec()),
2550 ("non utf8 key", vec![7, b' ', 0xff, b'=', b'x', b'\n']),
2551 ("bad size value", pax_record("size", b"12x")),
2552 ];
2553
2554 for (name, payload) in cases {
2555 let mut bytes = member(b"PaxHeaders/file", b'x', &payload, b"");
2556 bytes.extend_from_slice(&member(b"file", b'0', b"abc", b""));
2557
2558 assert!(
2559 matches!(
2560 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2561 FormatError::InvalidArchive(_)
2562 ),
2563 "{name}"
2564 );
2565
2566 let mut streaming = TarStreamSummaryValidator::with_observer(
2567 4096,
2568 u64::MAX,
2569 4096,
2570 16,
2571 NoopTarStreamObserver,
2572 );
2573 assert!(
2574 matches!(
2575 streaming.observe(&bytes).unwrap_err(),
2576 FormatError::InvalidArchive(_)
2577 ),
2578 "{name}"
2579 );
2580 }
2581 }
2582
2583 #[test]
2584 fn reports_degraded_diagnostics_for_xattr_and_acl_pax_profiles() {
2585 let mut pax = Vec::new();
2586 pax.extend_from_slice(&pax_record("SCHILY.xattr.user.comment", b"hello"));
2587 pax.extend_from_slice(&pax_record("LIBARCHIVE.xattr.user.comment", b"hello"));
2588 pax.extend_from_slice(&pax_record("SCHILY.acl.access", b"user::rw-"));
2589 pax.extend_from_slice(&pax_record("LIBARCHIVE.acl.access", b"user::rw-"));
2590 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
2591 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
2592
2593 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
2594
2595 assert_eq!(parsed.path, b"file.txt");
2596 assert_eq!(parsed.data, b"abc");
2597 assert_eq!(
2598 parsed
2599 .diagnostics
2600 .iter()
2601 .filter(|diagnostic| **diagnostic
2602 == MetadataDiagnostic {
2603 profile: "pax-xattrs-acls",
2604 message: "unsupported xattr/ACL PAX metadata was ignored",
2605 })
2606 .count(),
2607 4
2608 );
2609 }
2610
2611 #[test]
2612 fn reports_degraded_diagnostics_for_pax_timestamp_precision() {
2613 let mut pax = Vec::new();
2614 pax.extend_from_slice(&pax_record("atime", b"1.123456789"));
2615 pax.extend_from_slice(&pax_record("ctime", b"2.123456789"));
2616 pax.extend_from_slice(&pax_record("mtime", b"3.123456789"));
2617 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
2618 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
2619
2620 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
2621
2622 assert_eq!(
2623 parsed
2624 .diagnostics
2625 .iter()
2626 .filter(|diagnostic| **diagnostic
2627 == MetadataDiagnostic {
2628 profile: "pax-posix-2001",
2629 message: "unsupported PAX timestamp metadata was ignored",
2630 })
2631 .count(),
2632 3
2633 );
2634 }
2635
2636 #[test]
2637 fn reports_degraded_diagnostics_for_sparse_and_unknown_pax_profiles() {
2638 let mut pax = Vec::new();
2639 pax.extend_from_slice(&pax_record("GNU.sparse.realsize", b"1024"));
2640 pax.extend_from_slice(&pax_record("GNU.sparse.map", b"0,1"));
2641 pax.extend_from_slice(&pax_record("comment", b"ignored"));
2642 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
2643 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
2644
2645 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
2646
2647 assert_eq!(
2648 parsed
2649 .diagnostics
2650 .iter()
2651 .filter(|diagnostic| **diagnostic
2652 == MetadataDiagnostic {
2653 profile: "gnu-sparse",
2654 message: "unsupported sparse-file PAX metadata was ignored",
2655 })
2656 .count(),
2657 2
2658 );
2659 assert_eq!(
2660 parsed
2661 .diagnostics
2662 .iter()
2663 .filter(|diagnostic| **diagnostic
2664 == MetadataDiagnostic {
2665 profile: "pax-posix-2001",
2666 message: "unsupported PAX key was ignored",
2667 })
2668 .count(),
2669 1
2670 );
2671 }
2672
2673 #[test]
2674 fn reports_degraded_diagnostics_for_unsupported_local_pax_profiles() {
2675 let mut pax = Vec::new();
2676 pax.extend_from_slice(&pax_record("SCHILY.xattr.user.comment", b"hello"));
2677 pax.extend_from_slice(&pax_record("GNU.sparse.realsize", b"1024"));
2678 pax.extend_from_slice(&pax_record("mtime", b"1.123456789"));
2679 pax.extend_from_slice(&pax_record("comment", b"ignored"));
2680 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
2681 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
2682
2683 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
2684
2685 assert_eq!(parsed.path, b"file.txt");
2686 assert_eq!(parsed.data, b"abc");
2687 assert!(parsed.diagnostics.contains(&MetadataDiagnostic {
2688 profile: "pax-xattrs-acls",
2689 message: "unsupported xattr/ACL PAX metadata was ignored",
2690 }));
2691 assert!(parsed.diagnostics.contains(&MetadataDiagnostic {
2692 profile: "gnu-sparse",
2693 message: "unsupported sparse-file PAX metadata was ignored",
2694 }));
2695 assert!(parsed.diagnostics.contains(&MetadataDiagnostic {
2696 profile: "pax-posix-2001",
2697 message: "unsupported PAX timestamp metadata was ignored",
2698 }));
2699 assert!(parsed.diagnostics.contains(&MetadataDiagnostic {
2700 profile: "pax-posix-2001",
2701 message: "unsupported PAX key was ignored",
2702 }));
2703 }
2704
2705 #[test]
2706 fn rejects_platform_escape_paths() {
2707 for path in [
2708 b"/abs".as_slice(),
2709 b"../up".as_slice(),
2710 b"a//b".as_slice(),
2711 b"a\\b".as_slice(),
2712 b"a:b".as_slice(),
2713 b"CON".as_slice(),
2714 ] {
2715 let bytes = member(path, b'0', b"", b"");
2716 assert_eq!(
2717 parse_tar_member_group(&bytes, 4096).unwrap_err(),
2718 FormatError::UnsafeArchivePath
2719 );
2720 }
2721 }
2722
2723 #[cfg(unix)]
2724 #[test]
2725 fn safe_restore_rejects_symlink_parent() {
2726 let tmp = tempdir().unwrap();
2727 let outside = tempdir().unwrap();
2728 std::os::unix::fs::symlink(outside.path(), tmp.path().join("link")).unwrap();
2729
2730 let member = OwnedTarMember {
2731 path: b"link/file.txt".to_vec(),
2732 kind: TarEntryKind::Regular,
2733 data: b"blocked".to_vec(),
2734 link_target: None,
2735 mode: 0o644,
2736 mtime: 0,
2737 logical_size: 7,
2738 diagnostics: Vec::new(),
2739 };
2740
2741 assert_eq!(
2742 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap_err(),
2743 FormatError::UnsafeArchivePath
2744 );
2745 }
2746
2747 #[cfg(unix)]
2748 #[test]
2749 fn prepared_regular_file_uses_open_parent_after_parent_path_swap() {
2750 let tmp = tempdir().unwrap();
2751 let outside = tempdir().unwrap();
2752 let original_parent = tmp.path().join("a");
2753 let held_parent = tmp.path().join("held");
2754 fs::create_dir(&original_parent).unwrap();
2755
2756 let destination = prepare_destination(
2757 tmp.path(),
2758 b"a/file.txt",
2759 TarEntryKind::Regular,
2760 SafeExtractionOptions::default(),
2761 )
2762 .unwrap();
2763
2764 fs::rename(&original_parent, &held_parent).unwrap();
2765 std::os::unix::fs::symlink(outside.path(), &original_parent).unwrap();
2766
2767 let (temp_leaf, mut file) = create_temp_regular_file(&destination).unwrap();
2768 file.write_all(b"inside").unwrap();
2769 publish_regular_file(
2770 &destination,
2771 &temp_leaf,
2772 file,
2773 SafeExtractionOptions::default(),
2774 )
2775 .unwrap();
2776
2777 assert_eq!(fs::read(held_parent.join("file.txt")).unwrap(), b"inside");
2778 assert!(!outside.path().join("file.txt").exists());
2779 }
2780
2781 #[cfg(unix)]
2782 #[test]
2783 fn create_directory_rechecks_leaf_without_following_symlink() {
2784 let tmp = tempdir().unwrap();
2785 let outside = tempdir().unwrap();
2786 let destination = prepare_destination(
2787 tmp.path(),
2788 b"dir",
2789 TarEntryKind::Directory,
2790 SafeExtractionOptions::default(),
2791 )
2792 .unwrap();
2793
2794 std::os::unix::fs::symlink(outside.path(), tmp.path().join("dir")).unwrap();
2795
2796 assert_eq!(
2797 create_directory(&destination).unwrap_err(),
2798 FormatError::UnsafeArchivePath
2799 );
2800 assert!(outside.path().read_dir().unwrap().next().is_none());
2801 }
2802
2803 #[test]
2804 fn safe_restore_requires_hardlink_target_to_be_existing_regular_file() {
2805 let tmp = tempdir().unwrap();
2806 fs::write(tmp.path().join("target.txt"), b"target").unwrap();
2807 let member = OwnedTarMember {
2808 path: b"linked.txt".to_vec(),
2809 kind: TarEntryKind::Hardlink,
2810 data: Vec::new(),
2811 link_target: Some(b"target.txt".to_vec()),
2812 mode: 0o644,
2813 mtime: 0,
2814 logical_size: 0,
2815 diagnostics: Vec::new(),
2816 };
2817
2818 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
2819 assert_eq!(fs::read(tmp.path().join("linked.txt")).unwrap(), b"target");
2820 }
2821
2822 #[cfg(unix)]
2823 #[test]
2824 fn restore_applies_regular_file_mode_metadata() {
2825 let tmp = tempdir().unwrap();
2826 let member = OwnedTarMember {
2827 path: b"script.sh".to_vec(),
2828 kind: TarEntryKind::Regular,
2829 data: b"#!/bin/sh\n".to_vec(),
2830 link_target: None,
2831 mode: 0o755,
2832 mtime: 0,
2833 logical_size: 10,
2834 diagnostics: Vec::new(),
2835 };
2836
2837 let diagnostics =
2838 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
2839
2840 assert!(diagnostics.is_empty());
2841 let mode = fs::metadata(tmp.path().join("script.sh"))
2842 .unwrap()
2843 .permissions()
2844 .mode()
2845 & 0o777;
2846 assert_eq!(mode, 0o755);
2847 }
2848
2849 #[test]
2850 fn restore_applies_regular_file_mtime_metadata() {
2851 let tmp = tempdir().unwrap();
2852 let member = OwnedTarMember {
2853 path: b"dated.txt".to_vec(),
2854 kind: TarEntryKind::Regular,
2855 data: b"dated".to_vec(),
2856 link_target: None,
2857 mode: 0o666,
2858 mtime: 1_700_000_000,
2859 logical_size: 5,
2860 diagnostics: Vec::new(),
2861 };
2862
2863 let diagnostics =
2864 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
2865
2866 assert!(diagnostics.is_empty());
2867 let modified = fs::metadata(tmp.path().join("dated.txt"))
2868 .unwrap()
2869 .modified()
2870 .unwrap()
2871 .duration_since(SystemTime::UNIX_EPOCH)
2872 .unwrap()
2873 .as_secs();
2874 assert_eq!(modified, 1_700_000_000);
2875 }
2876
2877 #[test]
2878 fn restore_revalidates_symlink_targets_from_owned_members() {
2879 let tmp = tempdir().unwrap();
2880 let member = OwnedTarMember {
2881 path: b"link".to_vec(),
2882 kind: TarEntryKind::Symlink,
2883 data: Vec::new(),
2884 link_target: Some(b"/outside".to_vec()),
2885 mode: 0o644,
2886 mtime: 0,
2887 logical_size: 0,
2888 diagnostics: Vec::new(),
2889 };
2890
2891 assert_eq!(
2892 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap_err(),
2893 FormatError::UnsafeArchivePath
2894 );
2895 assert!(!tmp.path().join("link").exists());
2896 }
2897
2898 #[test]
2899 fn safe_restore_rejects_directory_over_existing_file_even_with_overwrite() {
2900 let tmp = tempdir().unwrap();
2901 let conflict = tmp.path().join("conflict");
2902 fs::write(&conflict, b"not a directory").unwrap();
2903 let member = OwnedTarMember {
2904 path: b"conflict".to_vec(),
2905 kind: TarEntryKind::Directory,
2906 data: Vec::new(),
2907 link_target: None,
2908 mode: 0o644,
2909 mtime: 0,
2910 logical_size: 0,
2911 diagnostics: Vec::new(),
2912 };
2913
2914 assert_eq!(
2915 restore_tar_member(
2916 tmp.path(),
2917 &member,
2918 SafeExtractionOptions {
2919 overwrite_existing: true
2920 }
2921 )
2922 .unwrap_err(),
2923 FormatError::UnsafeOverwrite
2924 );
2925 assert!(conflict.is_file());
2926 }
2927
2928 #[test]
2929 fn hardlink_target_checks_use_component_position_not_value() {
2930 let tmp = tempdir().unwrap();
2931 fs::create_dir(tmp.path().join("a")).unwrap();
2932 fs::write(tmp.path().join("a").join("a"), b"target").unwrap();
2933 let member = OwnedTarMember {
2934 path: b"linked.txt".to_vec(),
2935 kind: TarEntryKind::Hardlink,
2936 data: Vec::new(),
2937 link_target: Some(b"a/a".to_vec()),
2938 mode: 0o644,
2939 mtime: 0,
2940 logical_size: 0,
2941 diagnostics: Vec::new(),
2942 };
2943
2944 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
2945 assert_eq!(fs::read(tmp.path().join("linked.txt")).unwrap(), b"target");
2946 }
2947
2948 #[test]
2949 fn hardlink_targets_obey_max_path_length() {
2950 let bytes = member(b"link", b'1', b"", b"long/name");
2951
2952 assert_eq!(
2953 parse_tar_member_group(&bytes, 4).unwrap_err(),
2954 FormatError::UnsafeArchivePath
2955 );
2956 }
2957}