1use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt, SystemTimeSpec};
2use cap_std::ambient_authority;
3use cap_std::fs::{Dir as CapDir, OpenOptions as CapOpenOptions};
4use std::collections::BTreeMap;
5use std::fs;
6use std::io::{Read, Seek, SeekFrom, Write};
7use std::path::{Path, PathBuf};
8use std::time::{Duration, SystemTime};
9use unicode_normalization::UnicodeNormalization;
10
11#[cfg(unix)]
12use crate::entry_metadata::canonical_base64_decode;
13#[cfg(any(windows, target_os = "macos"))]
14use crate::entry_metadata::parse_timestamp;
15#[cfg(target_os = "linux")]
16use crate::entry_metadata::schily_posix_acl_to_linux_xattr;
17#[cfg(windows)]
18use cap_std::fs::OpenOptionsExt as _;
19#[cfg(unix)]
20use std::os::unix::fs::PermissionsExt;
21#[cfg(unix)]
22use std::os::unix::io::AsRawFd;
23#[cfg(windows)]
24use std::os::windows::io::AsRawHandle;
25#[cfg(windows)]
26use windows_sys::Win32::Storage::FileSystem::{
27 FileBasicInfo, GetFileInformationByHandleEx, SetFileInformationByHandle, DELETE,
28 FILE_BASIC_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ,
29 FILE_GENERIC_WRITE, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
30 FILE_WRITE_ATTRIBUTES,
31};
32
33use crate::entry_metadata::{
34 decode_percent_name, parse_auxiliary_record, parse_canonical_pax, parse_primary_metadata,
35 parse_sparse_payload, validate_group_metadata, ArchiveTimestamp, AuxiliaryRecord,
36 AuxiliaryStreamValidator, CaptureReportRow, CaptureStatus, MemberMetadata, PaxRecords,
37 PortableMetadataMirror, PrimaryMetadata, RestoreClass, RestorePolicy, SparseExtent,
38 SparseStreamValidator, CAPTURE_REPORT_KIND, HAS_NATIVE_METADATA, HAS_SPARSE_EXTENTS,
39 MAX_AGGREGATE_PAX_PAYLOAD, MAX_LOCAL_PAX_PAYLOAD, REQUIRES_SYSTEM_RESTORE,
40};
41use crate::format::{ExtractError, FormatError};
42use crate::metadata::validate_file_path_bytes;
43
44const TAR_BLOCK_LEN: usize = 512;
45const MACOS_SETTABLE_ORDINARY_FLAGS: u32 = 0x0000_800f;
46const MACOS_SETTABLE_SYSTEM_FLAGS: u32 = 0x0007_0000;
47const MACOS_SYSTEM_CLASS_FLAGS: u32 = 0x009f_0086;
51const MACOS_KNOWN_SETTABLE_FLAGS: u32 = MACOS_SETTABLE_ORDINARY_FLAGS | MACOS_SETTABLE_SYSTEM_FLAGS;
52
53fn parse_macos_flags(encoded: &[u8]) -> Result<u32, FormatError> {
54 std::str::from_utf8(encoded)
55 .ok()
56 .and_then(|value| u64::from_str_radix(value, 16).ok())
57 .and_then(|value| u32::try_from(value).ok())
58 .ok_or(FormatError::InvalidArchive("invalid macOS file flags"))
59}
60
61fn macos_flags_supported(flags: u32) -> bool {
62 flags & !MACOS_KNOWN_SETTABLE_FLAGS == 0
63}
64
65fn macos_flags_require_system(flags: u32) -> bool {
66 flags & MACOS_SYSTEM_CLASS_FLAGS != 0
67}
68
69fn macos_system_flags_privileges_available(flags: u32) -> bool {
70 if flags & MACOS_SETTABLE_SYSTEM_FLAGS == 0 {
71 return true;
72 }
73 #[cfg(target_os = "macos")]
74 {
75 (unsafe { libc::geteuid() }) == 0
77 }
78 #[cfg(not(target_os = "macos"))]
79 false
80}
81
82fn special_object_restore_supported(kind: TarEntryKind) -> bool {
83 #[cfg(target_os = "linux")]
84 {
85 let _ = kind;
86 true
87 }
88 #[cfg(target_os = "macos")]
89 {
90 kind == TarEntryKind::Fifo || (unsafe { libc::geteuid() }) == 0
91 }
92 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
93 {
94 let _ = kind;
95 false
96 }
97}
98
99#[cfg(target_os = "macos")]
100fn validate_darwin_acl_external(value: &[u8]) -> Result<(), FormatError> {
101 const ACL_MAX_ENTRIES: usize = 128;
102 const DARWIN_EXTERNAL_ACL_HEADER: usize = 40;
103 const DARWIN_EXTERNAL_ACE_SIZE: usize = 28;
104 const DARWIN_EXTERNAL_ACL_MAGIC: [u8; 4] = [0x01, 0x2c, 0xc1, 0x6d];
105 if value.get(..4) != Some(DARWIN_EXTERNAL_ACL_MAGIC.as_slice()) {
106 return Err(FormatError::InvalidArchive(
107 "macOS ACL external form has an invalid magic value",
108 ));
109 }
110 let entry_count = value
111 .get(36..40)
112 .and_then(|bytes| bytes.try_into().ok())
113 .map(u32::from_be_bytes)
114 .ok_or(FormatError::InvalidArchive(
115 "macOS ACL external form is truncated",
116 ))? as usize;
117 let expected = DARWIN_EXTERNAL_ACL_HEADER
118 .checked_add(entry_count.checked_mul(DARWIN_EXTERNAL_ACE_SIZE).ok_or(
119 FormatError::InvalidArchive("macOS ACL entry count overflows"),
120 )?)
121 .ok_or(FormatError::InvalidArchive("macOS ACL size overflows"))?;
122 if entry_count > ACL_MAX_ENTRIES || expected != value.len() {
123 return Err(FormatError::InvalidArchive(
124 "macOS ACL external form has an invalid size",
125 ));
126 }
127 Ok(())
128}
129
130#[cfg(target_os = "linux")]
131const LINUX_KNOWN_FSFLAGS: u64 = (linux_raw_sys::general::FS_SECRM_FL
132 | linux_raw_sys::general::FS_UNRM_FL
133 | linux_raw_sys::general::FS_COMPR_FL
134 | linux_raw_sys::general::FS_SYNC_FL
135 | linux_raw_sys::general::FS_IMMUTABLE_FL
136 | linux_raw_sys::general::FS_APPEND_FL
137 | linux_raw_sys::general::FS_NODUMP_FL
138 | linux_raw_sys::general::FS_NOATIME_FL
139 | linux_raw_sys::general::FS_DIRTY_FL
140 | linux_raw_sys::general::FS_COMPRBLK_FL
141 | linux_raw_sys::general::FS_NOCOMP_FL
142 | linux_raw_sys::general::FS_ENCRYPT_FL
143 | linux_raw_sys::general::FS_BTREE_FL
144 | linux_raw_sys::general::FS_IMAGIC_FL
145 | linux_raw_sys::general::FS_JOURNAL_DATA_FL
146 | linux_raw_sys::general::FS_NOTAIL_FL
147 | linux_raw_sys::general::FS_DIRSYNC_FL
148 | linux_raw_sys::general::FS_TOPDIR_FL
149 | linux_raw_sys::general::FS_HUGE_FILE_FL
150 | linux_raw_sys::general::FS_EXTENT_FL
151 | linux_raw_sys::general::FS_VERITY_FL
152 | linux_raw_sys::general::FS_EA_INODE_FL
153 | linux_raw_sys::general::FS_EOFBLOCKS_FL
154 | linux_raw_sys::general::FS_NOCOW_FL
155 | linux_raw_sys::general::FS_DAX_FL
156 | linux_raw_sys::general::FS_INLINE_DATA_FL
157 | linux_raw_sys::general::FS_PROJINHERIT_FL
158 | linux_raw_sys::general::FS_CASEFOLD_FL) as u64;
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum TarEntryKind {
162 Regular,
163 Directory,
164 Symlink,
165 Hardlink,
166 CharacterDevice,
167 BlockDevice,
168 Fifo,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub enum MetadataOperation {
173 Capture,
174 Parse,
175 Verify,
176 Plan,
177 Restore,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum MetadataDiagnosticStatus {
182 Partial,
183 Unsupported,
184 Skipped,
185 Materialized,
186 Failed,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct MetadataDiagnostic {
191 pub path: Vec<u8>,
192 pub profile: String,
193 pub metadata_class: String,
194 pub operation: MetadataOperation,
195 pub status: MetadataDiagnosticStatus,
196 pub message: String,
197 pub restore_policy: Option<RestorePolicy>,
198 pub restore_phase: Option<u8>,
199 pub native_host_error: Option<String>,
200 pub bytes_staged: Option<u64>,
201 pub bytes_committed: Option<u64>,
202}
203
204impl MetadataDiagnostic {
205 fn new(
206 path: &[u8],
207 profile: impl Into<String>,
208 metadata_class: impl Into<String>,
209 operation: MetadataOperation,
210 status: MetadataDiagnosticStatus,
211 message: impl Into<String>,
212 ) -> Self {
213 Self {
214 path: path.to_vec(),
215 profile: profile.into(),
216 metadata_class: metadata_class.into(),
217 operation,
218 status,
219 message: message.into(),
220 restore_policy: None,
221 restore_phase: None,
222 native_host_error: None,
223 bytes_staged: None,
224 bytes_committed: None,
225 }
226 }
227
228 fn for_restore(mut self, policy: RestorePolicy, phase: u8) -> Self {
229 self.restore_policy = Some(policy);
230 self.restore_phase = Some(phase);
231 self
232 }
233
234 fn with_native_error(mut self, error: &std::io::Error) -> Self {
235 self.native_host_error = Some(error.to_string());
236 self
237 }
238
239 fn with_bytes(mut self, staged: u64, committed: u64) -> Self {
240 self.bytes_staged = Some(staged);
241 self.bytes_committed = Some(committed);
242 self
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct RestorePolicyCapability {
248 pub policy: RestorePolicy,
249 pub policy_complete: bool,
250 pub degraded_restore_available: bool,
251 pub reason: Option<&'static str>,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct EntryMetadataVerification {
256 pub path: Vec<u8>,
257 pub capture_status: CaptureStatus,
258 pub required_profiles: Vec<String>,
259 pub optional_profiles: Vec<String>,
260 pub auxiliary_kinds: Vec<String>,
261 pub policy_capabilities: Vec<RestorePolicyCapability>,
262 pub full_fidelity_possible: bool,
263 pub diagnostics: Vec<MetadataDiagnostic>,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct MetadataVerificationReport {
268 pub all_capture_complete: bool,
269 pub full_fidelity_possible: bool,
270 pub profiles_present: Vec<String>,
271 pub auxiliary_kinds_present: Vec<String>,
272 pub entries: Vec<EntryMetadataVerification>,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct OwnedTarMember {
277 pub path: Vec<u8>,
278 pub kind: TarEntryKind,
279 pub data: Vec<u8>,
280 pub link_target: Option<Vec<u8>>,
281 pub mode: u32,
282 pub mtime: ArchiveTimestamp,
283 pub logical_size: u64,
284 pub reparse_placeholder: bool,
285 pub v45_metadata: Option<MemberMetadata>,
286 pub diagnostics: Vec<MetadataDiagnostic>,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct ParsedTarMember<'a> {
291 pub path: Vec<u8>,
292 pub kind: TarEntryKind,
293 pub data: &'a [u8],
294 pub link_target: Option<Vec<u8>>,
295 pub mode: u32,
296 pub mtime: ArchiveTimestamp,
297 pub logical_size: u64,
298 pub reparse_placeholder: bool,
299 pub diagnostics: Vec<MetadataDiagnostic>,
300 pub v45_metadata: MemberMetadata,
301}
302
303impl ParsedTarMember<'_> {
304 pub fn to_owned_member(&self) -> Result<OwnedTarMember, FormatError> {
305 let data = if let Some(layout) = &self.v45_metadata.sparse_layout {
306 let logical_len = usize::try_from(layout.logical_size).map_err(|_| {
307 FormatError::ReaderUnsupported("sparse logical size exceeds platform limits")
308 })?;
309 let mut logical = vec![0u8; logical_len];
310 let mut stored_cursor = layout.map_and_padding_size;
311 for extent in &layout.extents {
312 let extent_len = usize::try_from(extent.length).map_err(|_| {
313 FormatError::ReaderUnsupported("sparse extent exceeds platform limits")
314 })?;
315 let stored_end = stored_cursor
316 .checked_add(extent_len)
317 .ok_or(FormatError::InvalidArchive("sparse stored range overflow"))?;
318 let logical_start = usize::try_from(extent.offset).map_err(|_| {
319 FormatError::ReaderUnsupported("sparse offset exceeds platform limits")
320 })?;
321 let logical_end = logical_start
322 .checked_add(extent_len)
323 .ok_or(FormatError::InvalidArchive("sparse logical range overflow"))?;
324 logical
325 .get_mut(logical_start..logical_end)
326 .ok_or(FormatError::InvalidArchive(
327 "sparse logical range is invalid",
328 ))?
329 .copy_from_slice(self.data.get(stored_cursor..stored_end).ok_or(
330 FormatError::InvalidArchive("sparse stored range is invalid"),
331 )?);
332 stored_cursor = stored_end;
333 }
334 logical
335 } else {
336 self.data.to_vec()
337 };
338 Ok(OwnedTarMember {
339 path: self.path.clone(),
340 kind: self.kind,
341 data,
342 link_target: self.link_target.clone(),
343 mode: self.mode,
344 mtime: self.mtime,
345 logical_size: self.logical_size,
346 reparse_placeholder: self.reparse_placeholder,
347 v45_metadata: Some(self.v45_metadata.clone()),
348 diagnostics: self.diagnostics.clone(),
349 })
350 }
351
352 pub(crate) fn to_owned_metadata(&self) -> OwnedTarMember {
353 OwnedTarMember {
354 path: self.path.clone(),
355 kind: self.kind,
356 data: Vec::new(),
357 link_target: self.link_target.clone(),
358 mode: self.mode,
359 mtime: self.mtime,
360 logical_size: self.logical_size,
361 reparse_placeholder: self.reparse_placeholder,
362 v45_metadata: Some(self.v45_metadata.clone()),
363 diagnostics: self.diagnostics.clone(),
364 }
365 }
366}
367
368#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
369pub struct SafeExtractionOptions {
370 pub overwrite_existing: bool,
371 pub restore_policy: RestorePolicy,
372 pub allow_degraded: bool,
375 pub system_authorized: bool,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
381pub(crate) struct StreamedTarMemberMetadata {
382 pub path: Vec<u8>,
383 pub kind: TarEntryKind,
384 pub link_target: Option<Vec<u8>>,
385 pub mode: u32,
386 pub mtime: ArchiveTimestamp,
387 pub logical_size: u64,
388 pub file_entry_flags: u32,
389 pub reparse_placeholder: bool,
390 pub v45_metadata: MemberMetadata,
391 pub diagnostics: Vec<MetadataDiagnostic>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub(crate) struct TarStreamMemberSummary {
396 pub path: Vec<u8>,
397 pub kind: TarEntryKind,
398 pub link_target: Option<Vec<u8>>,
399 pub mode: u32,
400 pub mtime: ArchiveTimestamp,
401 pub logical_size: u64,
402 pub file_entry_flags: u32,
403 pub reparse_placeholder: bool,
404 pub v45_metadata: MemberMetadata,
405 pub diagnostics: Vec<MetadataDiagnostic>,
406 pub group_start: u64,
407 pub group_size: u64,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub(crate) struct TarStreamSummary {
412 pub members: Vec<TarStreamMemberSummary>,
413 pub tar_total_size: u64,
414 pub total_extraction_size: u64,
415}
416
417pub(crate) trait TarMemberGroupReader {
418 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError>;
419
420 fn read_exact_member_bytes(&mut self, mut buf: &mut [u8]) -> Result<(), ExtractError> {
421 while !buf.is_empty() {
422 let read = self.read_some_member_bytes(buf)?;
423 if read == 0 {
424 return Err(
425 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
426 );
427 }
428 let (_, rest) = buf.split_at_mut(read);
429 buf = rest;
430 }
431 Ok(())
432 }
433}
434
435trait TarMemberStreamHandler {
436 fn on_member(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), ExtractError>;
437 fn write_regular_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError>;
438 fn begin_auxiliary_payload(&mut self, _record: &AuxiliaryRecord) -> Result<bool, ExtractError> {
439 Ok(false)
440 }
441 fn write_auxiliary_payload(&mut self, _bytes: &[u8]) -> Result<(), ExtractError> {
442 Ok(())
443 }
444 fn finish_auxiliary_payload(&mut self, _record: &AuxiliaryRecord) -> Result<(), ExtractError> {
445 Ok(())
446 }
447 fn begin_sparse_payload(
448 &mut self,
449 _logical_size: u64,
450 _extents: &[SparseExtent],
451 ) -> Result<bool, ExtractError> {
452 Ok(false)
453 }
454 fn write_sparse_extent(&mut self, _offset: u64, _bytes: &[u8]) -> Result<(), ExtractError> {
455 Err(FormatError::InvalidArchive("sparse output was not initialized").into())
456 }
457 fn finish_sparse_payload(&mut self) -> Result<(), ExtractError> {
458 Ok(())
459 }
460}
461
462pub(crate) trait TarStreamObserver {
463 fn on_member_start(&mut self, _member: &StreamedTarMemberMetadata) -> Result<(), FormatError> {
464 Ok(())
465 }
466
467 fn on_regular_payload(&mut self, _bytes: &[u8]) -> Result<(), FormatError> {
468 Ok(())
469 }
470
471 fn on_auxiliary_start(&mut self, _record: &AuxiliaryRecord) -> Result<bool, FormatError> {
472 Ok(false)
473 }
474
475 fn on_auxiliary_payload(&mut self, _bytes: &[u8]) -> Result<(), FormatError> {
476 Ok(())
477 }
478
479 fn on_auxiliary_complete(&mut self, _record: &AuxiliaryRecord) -> Result<(), FormatError> {
480 Ok(())
481 }
482
483 fn on_sparse_layout(
484 &mut self,
485 _logical_size: u64,
486 _extents: &[SparseExtent],
487 ) -> Result<bool, FormatError> {
488 Ok(false)
489 }
490
491 fn on_sparse_extent(&mut self, _offset: u64, _bytes: &[u8]) -> Result<(), FormatError> {
492 Err(FormatError::InvalidArchive(
493 "sparse observer output was not initialized",
494 ))
495 }
496
497 fn on_sparse_complete(&mut self) -> Result<(), FormatError> {
498 Ok(())
499 }
500
501 fn on_member_complete(
502 &mut self,
503 member: &StreamedTarMemberMetadata,
504 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
505 Ok(member.diagnostics.clone())
506 }
507
508 fn on_archive_complete(&mut self) -> Result<Vec<MetadataDiagnostic>, FormatError> {
509 Ok(Vec::new())
510 }
511}
512
513pub(crate) struct NoopTarStreamObserver;
514
515impl TarStreamObserver for NoopTarStreamObserver {}
516
517pub(crate) struct TarStreamFilesystemRestoreObserver<'a> {
518 handler: FilesystemRestoreHandler<'a>,
519}
520
521impl<'a> TarStreamFilesystemRestoreObserver<'a> {
522 pub(crate) fn new(root: &'a Path, options: SafeExtractionOptions) -> Self {
523 Self {
524 handler: FilesystemRestoreHandler::new_deferred(root, options),
525 }
526 }
527}
528
529impl TarStreamObserver for TarStreamFilesystemRestoreObserver<'_> {
530 fn on_auxiliary_start(&mut self, record: &AuxiliaryRecord) -> Result<bool, FormatError> {
531 self.handler
532 .begin_auxiliary_payload(record)
533 .map_err(format_error_from_extract_error)
534 }
535
536 fn on_auxiliary_payload(&mut self, bytes: &[u8]) -> Result<(), FormatError> {
537 self.handler
538 .write_auxiliary_payload(bytes)
539 .map_err(format_error_from_extract_error)
540 }
541
542 fn on_auxiliary_complete(&mut self, record: &AuxiliaryRecord) -> Result<(), FormatError> {
543 self.handler
544 .finish_auxiliary_payload(record)
545 .map_err(format_error_from_extract_error)
546 }
547
548 fn on_member_start(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), FormatError> {
549 self.handler
550 .on_member(member)
551 .map_err(format_error_from_extract_error)
552 }
553
554 fn on_regular_payload(&mut self, bytes: &[u8]) -> Result<(), FormatError> {
555 self.handler
556 .write_regular_payload(bytes)
557 .map_err(format_error_from_extract_error)
558 }
559
560 fn on_sparse_layout(
561 &mut self,
562 logical_size: u64,
563 extents: &[SparseExtent],
564 ) -> Result<bool, FormatError> {
565 self.handler
566 .begin_sparse_payload(logical_size, extents)
567 .map_err(format_error_from_extract_error)
568 }
569
570 fn on_sparse_extent(&mut self, offset: u64, bytes: &[u8]) -> Result<(), FormatError> {
571 self.handler
572 .write_sparse_extent(offset, bytes)
573 .map_err(format_error_from_extract_error)
574 }
575
576 fn on_sparse_complete(&mut self) -> Result<(), FormatError> {
577 self.handler
578 .finish_sparse_payload()
579 .map_err(format_error_from_extract_error)
580 }
581
582 fn on_member_complete(
583 &mut self,
584 member: &StreamedTarMemberMetadata,
585 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
586 self.handler
587 .finish(member)
588 .map_err(format_error_from_extract_error)
589 }
590
591 fn on_archive_complete(&mut self) -> Result<Vec<MetadataDiagnostic>, FormatError> {
592 self.handler.finish_archive()
593 }
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
597enum V45PaxKind {
598 Primary,
599 Auxiliary(u32),
600}
601
602#[derive(Default)]
603struct V45StreamingGroup {
604 pending: Option<(V45PaxKind, PaxRecords)>,
605 auxiliary: Vec<AuxiliaryRecord>,
606 aggregate_pax_bytes: usize,
607}
608
609struct StreamingSparsePrimary {
610 validator: SparseStreamValidator,
611 layout: Option<crate::entry_metadata::SparseLayout>,
612 extent_index: usize,
613 extent_consumed: u64,
614 logical_cursor: u64,
615 native_output: Option<bool>,
616}
617
618impl StreamingSparsePrimary {
619 fn new(logical_size: u64) -> Self {
620 Self {
621 validator: SparseStreamValidator::new(logical_size),
622 layout: None,
623 extent_index: 0,
624 extent_consumed: 0,
625 logical_cursor: 0,
626 native_output: None,
627 }
628 }
629
630 fn observe<O: TarStreamObserver>(
631 &mut self,
632 bytes: &[u8],
633 observer: &mut O,
634 ) -> Result<(), FormatError> {
635 let before = self.validator.position();
636 self.validator.observe(bytes)?;
637 if self.layout.is_none() {
638 self.layout = self.validator.layout_if_map_complete();
639 }
640 let Some(layout) = &self.layout else {
641 return Ok(());
642 };
643 let native_output = match self.native_output {
644 Some(native_output) => native_output,
645 None => {
646 let native_output =
647 observer.on_sparse_layout(layout.logical_size, &layout.extents)?;
648 self.native_output = Some(native_output);
649 native_output
650 }
651 };
652 let padded = layout.map_and_padding_size as u64;
653 let data_offset = if before >= padded {
654 0
655 } else {
656 usize::try_from((padded - before).min(bytes.len() as u64))
657 .map_err(|_| FormatError::InvalidArchive("sparse offset exceeds usize"))?
658 };
659 let mut data = &bytes[data_offset..];
660 while !data.is_empty() {
661 let extent =
662 layout
663 .extents
664 .get(self.extent_index)
665 .ok_or(FormatError::InvalidArchive(
666 "sparse primary has trailing extent bytes",
667 ))?;
668 if self.extent_consumed == 0 && !native_output {
669 observer_write_zeros(observer, extent.offset - self.logical_cursor)?;
670 }
671 let available = extent.length - self.extent_consumed;
672 let take = usize::try_from(available.min(data.len() as u64))
673 .map_err(|_| FormatError::InvalidArchive("sparse extent exceeds usize"))?;
674 if native_output {
675 observer.on_sparse_extent(extent.offset + self.extent_consumed, &data[..take])?;
676 } else {
677 observer.on_regular_payload(&data[..take])?;
678 }
679 self.extent_consumed += take as u64;
680 data = &data[take..];
681 if self.extent_consumed == extent.length {
682 self.logical_cursor = extent.offset + extent.length;
683 self.extent_index += 1;
684 self.extent_consumed = 0;
685 }
686 }
687 Ok(())
688 }
689
690 fn finish<O: TarStreamObserver>(self, observer: &mut O) -> Result<(), FormatError> {
691 let layout = self.validator.finish()?;
692 if self.extent_index != layout.extents.len() || self.extent_consumed != 0 {
693 return Err(FormatError::InvalidArchive(
694 "sparse primary extent data is incomplete",
695 ));
696 }
697 let native_output = match self.native_output {
698 Some(native_output) => native_output,
699 None => observer.on_sparse_layout(layout.logical_size, &layout.extents)?,
700 };
701 if native_output {
702 observer.on_sparse_complete()
703 } else {
704 observer_write_zeros(observer, layout.logical_size - self.logical_cursor)
705 }
706 }
707}
708
709fn observer_write_zeros<O: TarStreamObserver>(
710 observer: &mut O,
711 mut len: u64,
712) -> Result<(), FormatError> {
713 let zeros = [0u8; 64 * 1024];
714 while len > 0 {
715 let take = len.min(zeros.len() as u64) as usize;
716 observer.on_regular_payload(&zeros[..take])?;
717 len -= take as u64;
718 }
719 Ok(())
720}
721
722pub fn parse_tar_member_group<'a>(
723 group: &'a [u8],
724 max_path_length: u32,
725) -> Result<ParsedTarMember<'a>, FormatError> {
726 if group.len() < TAR_BLOCK_LEN * 3 || group.len() % TAR_BLOCK_LEN != 0 {
727 return Err(FormatError::InvalidArchive(
728 "tar member group is not block aligned",
729 ));
730 }
731
732 let mut cursor = 0usize;
733 let mut pending: Option<(V45PaxKind, PaxRecords)> = None;
734 let mut auxiliary = Vec::<AuxiliaryRecord>::new();
735 let mut aggregate_pax_bytes = 0usize;
736
737 loop {
738 let header = slice(group, cursor, TAR_BLOCK_LEN)?;
739 if header.iter().all(|byte| *byte == 0) {
740 return Err(FormatError::InvalidArchive("tar member header is empty"));
741 }
742 verify_tar_checksum(header)?;
743 let typeflag = header[156];
744 let header_size = parse_tar_octal(&header[124..136])?;
745 let effective_size = pending
746 .as_ref()
747 .and_then(|(_, records)| records.get("size"))
748 .map(|value| parse_minimal_decimal_u64(value, "PAX size"))
749 .transpose()?
750 .unwrap_or(header_size);
751 let payload_start = checked_add(cursor, TAR_BLOCK_LEN)?;
752 let payload_len = to_usize(effective_size)?;
753 let payload_end = checked_add(payload_start, payload_len)?;
754 let padded_end = checked_add(payload_end, padding_to_512(payload_len))?;
755 let payload = slice(group, payload_start, payload_len)?;
756 if padded_end > group.len() {
757 return Err(FormatError::InvalidArchive(
758 "tar member payload exceeds group",
759 ));
760 }
761 if group[payload_end..padded_end].iter().any(|byte| *byte != 0) {
762 return Err(FormatError::InvalidArchive(
763 "tar member padding is non-zero",
764 ));
765 }
766
767 match typeflag {
768 b'x' => {
769 if pending.is_some() {
770 return Err(FormatError::InvalidArchive(
771 "PAX header is not immediately consumed",
772 ));
773 }
774 validate_v45_metadata_header(header)?;
775 aggregate_pax_bytes = aggregate_pax_bytes
776 .checked_add(payload.len())
777 .ok_or(FormatError::InvalidArchive("aggregate PAX size overflow"))?;
778 if aggregate_pax_bytes > MAX_AGGREGATE_PAX_PAYLOAD {
779 return Err(FormatError::ReaderResourceLimitExceeded {
780 field: "aggregate local PAX payload bytes per member group",
781 cap: MAX_AGGREGATE_PAX_PAYLOAD as u64,
782 actual: aggregate_pax_bytes as u64,
783 });
784 }
785 let records = parse_canonical_pax(payload)?;
786 let label = ustar_path(header);
787 let kind = if label == b"TZAP-PAX/PRIMARY" {
788 V45PaxKind::Primary
789 } else if let Some(ordinal) = parse_auxiliary_pax_label(&label) {
790 if ordinal != auxiliary.len() as u32 {
791 return Err(FormatError::InvalidArchive(
792 "auxiliary PAX ordinal is not contiguous",
793 ));
794 }
795 V45PaxKind::Auxiliary(ordinal)
796 } else {
797 return Err(FormatError::InvalidArchive(
798 "revision-45 PAX header has a non-canonical internal name",
799 ));
800 };
801 pending = Some((kind, records));
802 cursor = padded_end;
803 }
804 b'Z' => {
805 let Some((V45PaxKind::Auxiliary(ordinal), records)) = pending.take() else {
806 return Err(FormatError::InvalidArchive(
807 "auxiliary entry is missing its local PAX header",
808 ));
809 };
810 validate_v45_auxiliary_header(header, ordinal, header_size, effective_size)?;
811 auxiliary.push(parse_auxiliary_record(
812 &records,
813 ordinal,
814 effective_size,
815 payload,
816 )?);
817 cursor = padded_end;
818 }
819 b'g' | b'L' | b'K' | b'V' | b'M' | b'N' | b'S' => {
820 return Err(FormatError::InvalidArchive(
821 "global or GNU tar metadata is forbidden in revision 45",
822 ));
823 }
824 0 | b'0' | b'5' | b'2' | b'1' | b'3' | b'4' | b'6' => {
825 let Some((V45PaxKind::Primary, records)) = pending.take() else {
826 return Err(FormatError::InvalidArchive(
827 "primary entry is missing its canonical local PAX header",
828 ));
829 };
830 if padded_end != group.len() {
831 return Err(FormatError::InvalidArchive(
832 "tar member group has bytes after main entry",
833 ));
834 }
835 let kind = match typeflag {
836 b'5' => TarEntryKind::Directory,
837 b'2' => TarEntryKind::Symlink,
838 b'1' => TarEntryKind::Hardlink,
839 b'3' => TarEntryKind::CharacterDevice,
840 b'4' => TarEntryKind::BlockDevice,
841 b'6' => TarEntryKind::Fifo,
842 _ => TarEntryKind::Regular,
843 };
844 let primary = parse_primary_metadata(&records)?;
845 validate_v45_primary_header(
846 header,
847 kind,
848 header_size,
849 effective_size,
850 &primary,
851 &records,
852 )?;
853 let path = v45_primary_path(header, kind, &records, &primary, max_path_length)?;
854 let link_target =
855 v45_primary_link_target(header, kind, &path, &primary, max_path_length)?;
856 let is_sparse = primary.sparse_logical_size.is_some();
857 let reparse_placeholder = records.contains_key("TZAP.windows.reparse-placeholder");
858 if kind != TarEntryKind::Regular && effective_size != 0 {
859 return Err(FormatError::InvalidArchive(
860 "non-regular tar entry has non-zero payload size",
861 ));
862 }
863 if reparse_placeholder && effective_size != 0 {
864 return Err(FormatError::InvalidArchive(
865 "reparse placeholder has non-zero primary payload",
866 ));
867 }
868 let sparse_layout = if let Some(logical_size) = primary.sparse_logical_size {
869 if kind != TarEntryKind::Regular || reparse_placeholder {
870 return Err(FormatError::InvalidArchive(
871 "sparse metadata is not valid for this primary type",
872 ));
873 }
874 Some(parse_sparse_payload(payload, logical_size)?)
875 } else {
876 None
877 };
878 let logical_size = if kind == TarEntryKind::Regular && !reparse_placeholder {
879 primary.sparse_logical_size.unwrap_or(effective_size)
880 } else {
881 0
882 };
883 let (file_entry_flags, capture_report) =
884 v45_group_flags(&primary, &auxiliary, kind)?;
885 validate_v45_primary_cross_fields(
886 kind,
887 &records,
888 &primary,
889 &auxiliary,
890 V45PrimaryLink {
891 path: &path,
892 target: link_target.as_deref(),
893 },
894 is_sparse,
895 capture_report.as_deref(),
896 )?;
897 let diagnostics = Vec::new();
898 let mtime = decoded_mtime(&primary, header)?;
899 let v45_metadata = MemberMetadata {
900 declaration: primary.declaration.clone(),
901 primary_records: records.clone(),
902 auxiliary,
903 file_entry_flags,
904 sparse_layout,
905 capture_report,
906 primary_has_native_scalar: primary.has_native_scalar,
907 primary_requires_system_restore: primary.requires_system_restore,
908 portable_mirror: portable_metadata_mirror(header, &records, &primary)?,
909 };
910 return Ok(ParsedTarMember {
911 path,
912 kind,
913 data: if kind == TarEntryKind::Regular {
914 payload
915 } else {
916 &[]
917 },
918 mode: primary.declaration.portable_mode,
919 mtime,
920 link_target,
921 logical_size,
922 reparse_placeholder,
923 diagnostics,
924 v45_metadata,
925 });
926 }
927 _ => {
928 return Err(FormatError::InvalidArchive(
929 "unsupported revision-45 tar entry type",
930 ));
931 }
932 }
933
934 if cursor >= group.len() {
935 return Err(FormatError::InvalidArchive(
936 "tar member group has metadata records but no main entry",
937 ));
938 }
939 }
940}
941
942fn validate_v45_metadata_header(header: &[u8]) -> Result<(), FormatError> {
943 validate_ustar_header(header)?;
944 if parse_tar_octal(&header[100..108])? != 0
945 || parse_tar_octal(&header[108..116])? != 0
946 || parse_tar_octal(&header[116..124])? != 0
947 || parse_tar_octal(&header[136..148])? != 0
948 || !nul_trimmed(&header[157..257]).is_empty()
949 || !nul_trimmed(&header[265..297]).is_empty()
950 || !nul_trimmed(&header[297..329]).is_empty()
951 || parse_tar_octal(&header[329..337])? != 0
952 || parse_tar_octal(&header[337..345])? != 0
953 || !nul_trimmed(&header[345..500]).is_empty()
954 {
955 return Err(FormatError::InvalidArchive(
956 "revision-45 local PAX header has non-zero metadata fields",
957 ));
958 }
959 Ok(())
960}
961
962fn validate_ustar_header(header: &[u8]) -> Result<(), FormatError> {
963 if &header[257..263] != b"ustar\0" || &header[263..265] != b"00" {
964 return Err(FormatError::InvalidArchive(
965 "tar header is not canonical ustar",
966 ));
967 }
968 for field in [
969 &header[0..100],
970 &header[157..257],
971 &header[265..297],
972 &header[297..329],
973 &header[345..500],
974 ] {
975 validate_nul_terminated_field(field)?;
976 }
977 if header[500..512].iter().any(|byte| *byte != 0) {
978 return Err(FormatError::InvalidArchive(
979 "tar header has non-zero reserved bytes",
980 ));
981 }
982 Ok(())
983}
984
985fn validate_nul_terminated_field(field: &[u8]) -> Result<(), FormatError> {
986 if let Some(nul) = field.iter().position(|byte| *byte == 0) {
987 if field[nul..].iter().any(|byte| *byte != 0) {
988 return Err(FormatError::InvalidArchive(
989 "ustar string field has bytes after NUL",
990 ));
991 }
992 }
993 Ok(())
994}
995
996fn parse_auxiliary_pax_label(label: &[u8]) -> Option<u32> {
997 let suffix = label.strip_prefix(b"TZAP-PAX/AUX/")?;
998 if suffix.len() != 8
999 || !suffix
1000 .iter()
1001 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
1002 {
1003 return None;
1004 }
1005 u32::from_str_radix(std::str::from_utf8(suffix).ok()?, 16).ok()
1006}
1007
1008fn validate_v45_auxiliary_header(
1009 header: &[u8],
1010 ordinal: u32,
1011 header_size: u64,
1012 effective_size: u64,
1013) -> Result<(), FormatError> {
1014 validate_ustar_header(header)?;
1015 let expected = format!("TZAP-AUX/{ordinal:08x}");
1016 if ustar_path(header) != expected.as_bytes()
1017 || parse_tar_octal(&header[100..108])? != 0
1018 || parse_tar_octal(&header[108..116])? != 0
1019 || parse_tar_octal(&header[116..124])? != 0
1020 || parse_tar_octal(&header[136..148])? != 0
1021 || !nul_trimmed(&header[157..257]).is_empty()
1022 || !nul_trimmed(&header[265..297]).is_empty()
1023 || !nul_trimmed(&header[297..329]).is_empty()
1024 || parse_tar_octal(&header[329..337])? != 0
1025 || parse_tar_octal(&header[337..345])? != 0
1026 || !nul_trimmed(&header[345..500]).is_empty()
1027 || (header_size != effective_size && header_size != 0)
1028 {
1029 return Err(FormatError::InvalidArchive(
1030 "revision-45 auxiliary tar header is not canonical",
1031 ));
1032 }
1033 Ok(())
1034}
1035
1036fn validate_v45_primary_header(
1037 header: &[u8],
1038 kind: TarEntryKind,
1039 header_size: u64,
1040 effective_size: u64,
1041 primary: &PrimaryMetadata,
1042 records: &PaxRecords,
1043) -> Result<(), FormatError> {
1044 validate_ustar_header(header)?;
1045 if parse_tar_octal(&header[100..108])? != primary.declaration.portable_mode as u64 {
1046 return Err(FormatError::InvalidArchive(
1047 "ustar mode does not match TZAP.portable.mode",
1048 ));
1049 }
1050 if primary.stored_size.is_some() {
1051 if header_size != 0 && header_size != effective_size {
1052 return Err(FormatError::InvalidArchive(
1053 "ustar size conflicts with PAX size",
1054 ));
1055 }
1056 } else if header_size != effective_size {
1057 return Err(FormatError::InvalidArchive("ustar size is inconsistent"));
1058 }
1059 if !primary.declaration.owner_kind_posix
1060 && (parse_tar_octal(&header[108..116])? != 0
1061 || parse_tar_octal(&header[116..124])? != 0
1062 || !nul_trimmed(&header[265..297]).is_empty()
1063 || !nul_trimmed(&header[297..329]).is_empty())
1064 {
1065 return Err(FormatError::InvalidArchive(
1066 "owner-kind none has non-zero ustar ownership fields",
1067 ));
1068 }
1069 if primary.declaration.owner_kind_posix {
1070 validate_numeric_pax_header_match(records, "uid", &header[108..116], "UID")?;
1071 validate_numeric_pax_header_match(records, "gid", &header[116..124], "GID")?;
1072 validate_string_pax_header_match(records, "uname", &header[265..297], "user name")?;
1073 validate_string_pax_header_match(records, "gname", &header[297..329], "group name")?;
1074 }
1075 if let Some((seconds, _)) = primary.mtime {
1076 let header_mtime = parse_tar_octal(&header[136..148])?;
1077 if header_mtime != 0 && (seconds < 0 || u64::try_from(seconds).ok() != Some(header_mtime)) {
1078 return Err(FormatError::InvalidArchive(
1079 "ustar mtime conflicts with PAX mtime",
1080 ));
1081 }
1082 }
1083 let is_device = matches!(
1084 kind,
1085 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice
1086 );
1087 if !is_device
1088 && (parse_tar_octal(&header[329..337])? != 0 || parse_tar_octal(&header[337..345])? != 0)
1089 {
1090 return Err(FormatError::InvalidArchive(
1091 "non-device primary has device numbers",
1092 ));
1093 }
1094 if is_device {
1095 validate_numeric_pax_header_match(
1096 records,
1097 "TZAP.posix.device-major",
1098 &header[329..337],
1099 "device major",
1100 )?;
1101 validate_numeric_pax_header_match(
1102 records,
1103 "TZAP.posix.device-minor",
1104 &header[337..345],
1105 "device minor",
1106 )?;
1107 }
1108 Ok(())
1109}
1110
1111fn decoded_mtime(
1112 primary: &PrimaryMetadata,
1113 header: &[u8],
1114) -> Result<ArchiveTimestamp, FormatError> {
1115 let (seconds, nanoseconds) = match primary.mtime {
1116 Some(value) => value,
1117 None => (
1118 i64::try_from(parse_tar_octal(&header[136..148])?)
1119 .map_err(|_| FormatError::InvalidArchive("ustar mtime exceeds i64"))?,
1120 0,
1121 ),
1122 };
1123 Ok(ArchiveTimestamp::new(seconds, nanoseconds))
1124}
1125
1126fn portable_metadata_mirror(
1127 header: &[u8],
1128 records: &PaxRecords,
1129 primary: &PrimaryMetadata,
1130) -> Result<PortableMetadataMirror, FormatError> {
1131 let numeric = |key: &'static str, field: &[u8]| -> Result<Option<u64>, FormatError> {
1132 if !primary.declaration.owner_kind_posix {
1133 return Ok(None);
1134 }
1135 if let Some(value) = records.get(key) {
1136 Ok(Some(parse_minimal_decimal_u64(value, key)?))
1137 } else {
1138 Ok(Some(parse_tar_octal(field)?))
1139 }
1140 };
1141 let string = |key: &str, field: &[u8]| -> Option<Vec<u8>> {
1142 if !primary.declaration.owner_kind_posix {
1143 return None;
1144 }
1145 let value = records
1146 .get(key)
1147 .map(Vec::as_slice)
1148 .unwrap_or_else(|| nul_trimmed(field));
1149 (!value.is_empty()).then(|| value.to_vec())
1150 };
1151 let mtime = if let Some(value) = primary.mtime {
1152 value
1153 } else {
1154 (
1155 i64::try_from(parse_tar_octal(&header[136..148])?)
1156 .map_err(|_| FormatError::InvalidArchive("ustar mtime exceeds i64"))?,
1157 0,
1158 )
1159 };
1160 Ok(PortableMetadataMirror {
1161 owner_kind_posix: primary.declaration.owner_kind_posix,
1162 mode_origin_native: primary.declaration.mode_origin_native,
1163 mode: primary.declaration.portable_mode,
1164 attributes: primary.declaration.portable_attributes,
1165 uid: numeric("uid", &header[108..116])?,
1166 gid: numeric("gid", &header[116..124])?,
1167 uname: string("uname", &header[265..297]),
1168 gname: string("gname", &header[297..329]),
1169 mtime,
1170 })
1171}
1172
1173fn validate_numeric_pax_header_match(
1174 records: &PaxRecords,
1175 key: &'static str,
1176 header_field: &[u8],
1177 label: &'static str,
1178) -> Result<(), FormatError> {
1179 let Some(value) = records.get(key) else {
1180 return Ok(());
1181 };
1182 let pax = parse_minimal_decimal_u64(value, key)?;
1183 let header = parse_tar_octal(header_field)?;
1184 if header != 0 && header != pax {
1185 return Err(FormatError::InvalidMetadata {
1186 structure: label,
1187 reason: "ustar field conflicts with PAX value",
1188 });
1189 }
1190 Ok(())
1191}
1192
1193fn validate_string_pax_header_match(
1194 records: &PaxRecords,
1195 key: &'static str,
1196 header_field: &[u8],
1197 label: &'static str,
1198) -> Result<(), FormatError> {
1199 if let Some(value) = records.get(key) {
1200 let header = nul_trimmed(header_field);
1201 if !header.is_empty() && header != value {
1202 return Err(FormatError::InvalidMetadata {
1203 structure: label,
1204 reason: "ustar field conflicts with PAX value",
1205 });
1206 }
1207 }
1208 Ok(())
1209}
1210
1211fn v45_primary_path(
1212 header: &[u8],
1213 kind: TarEntryKind,
1214 records: &PaxRecords,
1215 primary: &PrimaryMetadata,
1216 max_path_length: u32,
1217) -> Result<Vec<u8>, FormatError> {
1218 let sparse_name = records.get("GNU.sparse.name");
1219 let mut path = if let Some(name) = sparse_name {
1220 if primary.path.is_some() || ustar_path(header) != b"GNUSparseFile.0/TZAP" {
1221 return Err(FormatError::InvalidArchive(
1222 "GNU sparse primary path framing is not canonical",
1223 ));
1224 }
1225 name.clone()
1226 } else if let Some(path) = &primary.path {
1227 if ustar_path(header) != b"TZAP-PRIMARY" {
1228 return Err(FormatError::InvalidArchive(
1229 "PAX path override lacks canonical ustar placeholder",
1230 ));
1231 }
1232 path.clone()
1233 } else {
1234 ustar_path(header)
1235 };
1236 if kind == TarEntryKind::Directory && path.ends_with(b"/") {
1237 path.pop();
1238 }
1239 validate_file_path_bytes(&path, max_path_length)?;
1240 Ok(path)
1241}
1242
1243fn v45_primary_link_target(
1244 header: &[u8],
1245 kind: TarEntryKind,
1246 path: &[u8],
1247 primary: &PrimaryMetadata,
1248 max_path_length: u32,
1249) -> Result<Option<Vec<u8>>, FormatError> {
1250 let header_target = nul_trimmed(&header[157..257]);
1251 match kind {
1252 TarEntryKind::Symlink | TarEntryKind::Hardlink => {
1253 let target = if let Some(target) = &primary.linkpath {
1254 if !header_target.is_empty() {
1255 return Err(FormatError::InvalidArchive(
1256 "PAX linkpath override has non-empty ustar linkname",
1257 ));
1258 }
1259 target.clone()
1260 } else {
1261 header_target.to_vec()
1262 };
1263 if target.is_empty() || target.contains(&0) {
1264 return Err(FormatError::InvalidArchive("tar link target is empty"));
1265 }
1266 if kind == TarEntryKind::Hardlink {
1267 validate_file_path_bytes(&target, max_path_length)?;
1268 } else {
1269 validate_symlink_target(path, &target)?;
1270 }
1271 Ok(Some(target))
1272 }
1273 _ => {
1274 if primary.linkpath.is_some() || !header_target.is_empty() {
1275 return Err(FormatError::InvalidArchive(
1276 "non-link primary has a link target",
1277 ));
1278 }
1279 Ok(None)
1280 }
1281 }
1282}
1283
1284#[derive(Clone, Copy)]
1285struct V45PrimaryLink<'a> {
1286 path: &'a [u8],
1287 target: Option<&'a [u8]>,
1288}
1289
1290fn validate_v45_primary_cross_fields(
1291 kind: TarEntryKind,
1292 records: &PaxRecords,
1293 primary: &PrimaryMetadata,
1294 auxiliary: &[AuxiliaryRecord],
1295 link: V45PrimaryLink<'_>,
1296 sparse: bool,
1297 capture_report: Option<&[CaptureReportRow]>,
1298) -> Result<(), FormatError> {
1299 let is_device = matches!(
1300 kind,
1301 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice
1302 );
1303 let has_device_major = records.contains_key("TZAP.posix.device-major");
1304 let has_device_minor = records.contains_key("TZAP.posix.device-minor");
1305 if is_device != (has_device_major && has_device_minor) {
1306 return Err(FormatError::InvalidArchive(
1307 "device primary and device-number metadata disagree",
1308 ));
1309 }
1310 if (kind == TarEntryKind::Fifo || is_device)
1311 && !primary.declaration.profile_selected("posix-backup-v1")
1312 {
1313 return Err(FormatError::InvalidArchive(
1314 "special POSIX primary lacks posix-backup-v1",
1315 ));
1316 }
1317 if records.contains_key("TZAP.linux.whiteout") {
1318 let major = records
1319 .get("TZAP.posix.device-major")
1320 .map(|value| parse_minimal_decimal_u64(value, "device major"))
1321 .transpose()?;
1322 let minor = records
1323 .get("TZAP.posix.device-minor")
1324 .map(|value| parse_minimal_decimal_u64(value, "device minor"))
1325 .transpose()?;
1326 if kind != TarEntryKind::CharacterDevice || major != Some(0) || minor != Some(0) {
1327 return Err(FormatError::InvalidArchive(
1328 "Linux whiteout is not a character device with major/minor zero",
1329 ));
1330 }
1331 }
1332 if sparse && kind != TarEntryKind::Regular {
1333 return Err(FormatError::InvalidArchive(
1334 "non-regular primary carries sparse metadata",
1335 ));
1336 }
1337 if kind == TarEntryKind::Hardlink {
1338 if primary.declaration.required_profiles != ["portable-v1"]
1339 || !primary.declaration.optional_profiles.is_empty()
1340 || sparse
1341 || auxiliary
1342 .iter()
1343 .any(|record| record.kind != CAPTURE_REPORT_KIND)
1344 {
1345 return Err(FormatError::InvalidArchive(
1346 "hardlink alias carries forbidden native or inode metadata",
1347 ));
1348 }
1349 if link.target == Some(link.path) {
1350 return Err(FormatError::InvalidArchive("hardlink aliases itself"));
1351 }
1352 }
1353 if records.contains_key("TZAP.windows.directory-case-sensitive")
1354 && kind != TarEntryKind::Directory
1355 {
1356 return Err(FormatError::InvalidArchive(
1357 "Windows directory case-sensitive state is attached to a non-directory",
1358 ));
1359 }
1360 if records.contains_key("SCHILY.acl.default") && kind != TarEntryKind::Directory {
1361 return Err(FormatError::InvalidArchive(
1362 "default POSIX ACL is attached to a non-directory",
1363 ));
1364 }
1365 if records.contains_key("TZAP.macos.clone-group") && kind != TarEntryKind::Regular {
1366 return Err(FormatError::InvalidArchive(
1367 "macOS clone group is attached to a non-regular primary",
1368 ));
1369 }
1370 validate_windows_cross_fields(kind, records, primary, auxiliary, sparse, capture_report)?;
1371 let has_textual_acl = records.contains_key("SCHILY.acl.access")
1372 || records.contains_key("SCHILY.acl.default")
1373 || records.contains_key("SCHILY.acl.ace");
1374 let has_native_macos_acl = auxiliary
1375 .iter()
1376 .any(|record| record.kind == "macos.acl-native");
1377 let acl_projection_none = records
1378 .get("TZAP.acl.projection")
1379 .is_some_and(|value| value == b"none");
1380 if (!has_textual_acl && has_native_macos_acl) != acl_projection_none {
1381 return Err(FormatError::InvalidArchive(
1382 "native-only ACL declaration and projection=none disagree",
1383 ));
1384 }
1385 if auxiliary.iter().any(|record| {
1386 record.kind == "generic.xattr"
1387 && primary
1388 .xattr_names
1389 .iter()
1390 .any(|name| name == &record.decoded_name)
1391 }) {
1392 return Err(FormatError::InvalidArchive(
1393 "xattr is duplicated in primary and auxiliary metadata",
1394 ));
1395 }
1396 if has_textual_acl
1397 && (primary.xattr_names.iter().any(|name| {
1398 matches!(
1399 name.as_slice(),
1400 b"system.posix_acl_access"
1401 | b"system.posix_acl_default"
1402 | b"com.apple.system.Security"
1403 )
1404 }) || auxiliary.iter().any(|record| {
1405 record.kind == "generic.xattr"
1406 && matches!(
1407 record.decoded_name.as_slice(),
1408 b"system.posix_acl_access"
1409 | b"system.posix_acl_default"
1410 | b"com.apple.system.Security"
1411 )
1412 }))
1413 {
1414 return Err(FormatError::InvalidArchive(
1415 "filesystem ACL backing xattr duplicates declared ACL metadata",
1416 ));
1417 }
1418 Ok(())
1419}
1420
1421const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
1422const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001;
1423const FILE_ATTRIBUTE_HIDDEN: u32 = 0x0000_0002;
1424const FILE_ATTRIBUTE_SYSTEM: u32 = 0x0000_0004;
1425const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x0000_0020;
1426const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
1427const FILE_ATTRIBUTE_TEMPORARY: u32 = 0x0000_0100;
1428const FILE_ATTRIBUTE_SPARSE_FILE: u32 = 0x0000_0200;
1429const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
1430const FILE_ATTRIBUTE_COMPRESSED: u32 = 0x0000_0800;
1431const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x0000_2000;
1432const FILE_ATTRIBUTE_ENCRYPTED: u32 = 0x0000_4000;
1433const WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES: u32 = FILE_ATTRIBUTE_READONLY
1434 | FILE_ATTRIBUTE_HIDDEN
1435 | FILE_ATTRIBUTE_SYSTEM
1436 | FILE_ATTRIBUTE_ARCHIVE
1437 | FILE_ATTRIBUTE_TEMPORARY
1438 | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
1439const WINDOWS_ESSENTIAL_INTRINSIC_ATTRIBUTES: u32 = FILE_ATTRIBUTE_DIRECTORY
1440 | FILE_ATTRIBUTE_SPARSE_FILE
1441 | FILE_ATTRIBUTE_REPARSE_POINT
1442 | FILE_ATTRIBUTE_COMPRESSED
1443 | FILE_ATTRIBUTE_ENCRYPTED;
1444const STREAM_MODIFIED_WHEN_READ: u32 = 0x0000_0001;
1445const STREAM_CONTAINS_SECURITY: u32 = 0x0000_0002;
1446const STREAM_SPARSE_ATTRIBUTE: u32 = 0x0000_0008;
1447
1448fn validate_windows_essential_reparse_data(data: &[u8]) -> Result<u32, FormatError> {
1449 const IO_REPARSE_TAG_MOUNT_POINT: u32 = 0xA000_0003;
1450 const IO_REPARSE_TAG_SYMLINK: u32 = 0xA000_000C;
1451 if data.len() < 8 {
1452 return Err(FormatError::InvalidArchive("reparse buffer is truncated"));
1453 }
1454 let tag = u32::from_le_bytes(data[0..4].try_into().unwrap());
1455 let payload_len = usize::from(u16::from_le_bytes(data[4..6].try_into().unwrap()));
1456 let header_len = if tag & 0x8000_0000 == 0 { 24 } else { 8 };
1457 if payload_len + header_len != data.len() {
1458 return Err(FormatError::InvalidArchive(
1459 "reparse buffer length is inconsistent",
1460 ));
1461 }
1462 let fixed_len = match tag {
1463 IO_REPARSE_TAG_SYMLINK if payload_len >= 12 => {
1464 if u32::from_le_bytes(data[16..20].try_into().unwrap()) != 1 {
1465 return Err(FormatError::InvalidArchive(
1466 "only relative Windows symbolic links are supported",
1467 ));
1468 }
1469 12
1470 }
1471 IO_REPARSE_TAG_MOUNT_POINT if payload_len >= 8 => 8,
1472 IO_REPARSE_TAG_SYMLINK | IO_REPARSE_TAG_MOUNT_POINT => {
1473 return Err(FormatError::InvalidArchive("reparse payload is truncated"));
1474 }
1475 _ => return Ok(tag),
1479 };
1480 let substitute_offset = usize::from(u16::from_le_bytes(data[8..10].try_into().unwrap()));
1481 let substitute_len = usize::from(u16::from_le_bytes(data[10..12].try_into().unwrap()));
1482 let print_offset = usize::from(u16::from_le_bytes(data[12..14].try_into().unwrap()));
1483 let print_len = usize::from(u16::from_le_bytes(data[14..16].try_into().unwrap()));
1484 if [substitute_offset, substitute_len, print_offset, print_len]
1485 .iter()
1486 .any(|value| value % 2 != 0)
1487 {
1488 return Err(FormatError::InvalidArchive(
1489 "reparse path fields are not UTF-16 aligned",
1490 ));
1491 }
1492 let path_buffer = &data[8 + fixed_len..];
1493 let decode = |offset: usize, len: usize| -> Result<String, FormatError> {
1494 let end = offset
1495 .checked_add(len)
1496 .ok_or(FormatError::InvalidArchive("reparse path range overflows"))?;
1497 let bytes = path_buffer
1498 .get(offset..end)
1499 .ok_or(FormatError::InvalidArchive(
1500 "reparse path range exceeds payload",
1501 ))?;
1502 let units = bytes
1503 .chunks_exact(2)
1504 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
1505 .collect::<Vec<_>>();
1506 let text = String::from_utf16(&units)
1507 .map_err(|_| FormatError::InvalidArchive("reparse path is not valid UTF-16"))?;
1508 if text.contains('\0') {
1509 return Err(FormatError::InvalidArchive("reparse path contains NUL"));
1510 }
1511 Ok(text)
1512 };
1513 let substitute = decode(substitute_offset, substitute_len)?;
1514 let print = decode(print_offset, print_len)?;
1515 if substitute.is_empty() {
1516 return Err(FormatError::InvalidArchive(
1517 "reparse substitute name is empty",
1518 ));
1519 }
1520 if tag == IO_REPARSE_TAG_SYMLINK {
1521 let target = if print.is_empty() {
1522 &substitute
1523 } else {
1524 &print
1525 };
1526 let target = target.replace('\\', "/");
1527 if target.is_empty() || target.starts_with('/') || target.contains(':') {
1528 return Err(FormatError::UnsafeArchivePath);
1529 }
1530 } else if !substitute.starts_with("\\??\\") || print.is_empty() {
1531 return Err(FormatError::InvalidArchive(
1532 "junction path fields are not canonical",
1533 ));
1534 }
1535 Ok(tag)
1536}
1537
1538fn validate_windows_cross_fields(
1539 kind: TarEntryKind,
1540 records: &PaxRecords,
1541 primary: &PrimaryMetadata,
1542 auxiliary: &[AuxiliaryRecord],
1543 sparse: bool,
1544 capture_report: Option<&[CaptureReportRow]>,
1545) -> Result<(), FormatError> {
1546 let selected = primary.declaration.profile_selected("windows-backup-v1");
1547 let file_attributes = records
1548 .get("TZAP.windows.file-attributes")
1549 .map(|value| parse_lower_hex_u32(value, "Windows file attributes"))
1550 .transpose()?;
1551 let stream_attributes = records
1552 .get("TZAP.windows.data-stream-attributes")
1553 .map(|value| parse_lower_hex_u32(value, "Windows data-stream attributes"))
1554 .transpose()?;
1555 let placeholder = records.contains_key("TZAP.windows.reparse-placeholder");
1556 let reparse_count = auxiliary
1557 .iter()
1558 .filter(|record| record.kind == "windows.reparse-data")
1559 .count();
1560 let security_descriptor_count = auxiliary
1561 .iter()
1562 .filter(|record| record.kind == "windows.security-descriptor")
1563 .count();
1564 let efs_count = auxiliary
1565 .iter()
1566 .filter(|record| record.kind == "windows.efs-raw")
1567 .count();
1568
1569 if !selected {
1570 if file_attributes.is_some()
1571 || stream_attributes.is_some()
1572 || placeholder
1573 || reparse_count != 0
1574 || security_descriptor_count != 0
1575 || efs_count != 0
1576 {
1577 return Err(FormatError::InvalidArchive(
1578 "Windows metadata is present without windows-backup-v1",
1579 ));
1580 }
1581 return Ok(());
1582 }
1583
1584 let complete = primary.declaration.capture_status == CaptureStatus::Complete;
1585 if file_attributes.is_none()
1586 && (complete
1587 || !has_capture_omission(capture_report, "windows-backup-v1", "file-attributes"))
1588 {
1589 return Err(FormatError::InvalidArchive(
1590 "windows-backup-v1 lacks exact file attributes or a matching omission",
1591 ));
1592 }
1593 if security_descriptor_count == 0
1594 && (complete
1595 || !has_capture_omission(capture_report, "windows-backup-v1", "security-descriptor"))
1596 {
1597 return Err(FormatError::InvalidArchive(
1598 "windows-backup-v1 lacks a security descriptor or a matching omission",
1599 ));
1600 }
1601 if let Some(attributes) = file_attributes {
1602 let is_directory = kind == TarEntryKind::Directory;
1603 if kind != TarEntryKind::Symlink
1604 && (attributes & FILE_ATTRIBUTE_DIRECTORY != 0) != is_directory
1605 {
1606 return Err(FormatError::InvalidArchive(
1607 "Windows directory attribute disagrees with primary type",
1608 ));
1609 }
1610 let is_reparse = attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0;
1611 if reparse_count != 0 && !is_reparse {
1612 return Err(FormatError::InvalidArchive(
1613 "Windows reparse data lacks FILE_ATTRIBUTE_REPARSE_POINT",
1614 ));
1615 }
1616 if is_reparse
1617 && reparse_count == 0
1618 && (complete
1619 || !has_capture_omission(capture_report, "windows-backup-v1", "reparse-data")
1620 || (kind != TarEntryKind::Symlink && !placeholder))
1621 {
1622 return Err(FormatError::InvalidArchive(
1623 "Windows reparse attribute lacks exact data or a safe partial placeholder",
1624 ));
1625 }
1626 if placeholder
1627 && (!is_reparse || !matches!(kind, TarEntryKind::Regular | TarEntryKind::Directory))
1628 {
1629 return Err(FormatError::InvalidArchive(
1630 "Windows reparse placeholder has invalid attributes or primary type",
1631 ));
1632 }
1633 if attributes & FILE_ATTRIBUTE_ENCRYPTED != 0
1634 && efs_count == 0
1635 && (complete || !has_capture_omission(capture_report, "windows-backup-v1", "efs-raw"))
1636 {
1637 return Err(FormatError::InvalidArchive(
1638 "encrypted Windows entry lacks raw EFS data or a matching omission",
1639 ));
1640 }
1641 } else if placeholder || reparse_count != 0 || efs_count != 0 {
1642 return Err(FormatError::InvalidArchive(
1643 "Windows native records cannot be checked without file attributes",
1644 ));
1645 }
1646
1647 let ordinary_regular = kind == TarEntryKind::Regular && !placeholder;
1648 if !ordinary_regular && stream_attributes.is_some() {
1649 return Err(FormatError::InvalidArchive(
1650 "Windows default-data-stream attributes disagree with primary type",
1651 ));
1652 }
1653 if ordinary_regular
1654 && stream_attributes.is_none()
1655 && (complete
1656 || !has_capture_omission(
1657 capture_report,
1658 "windows-backup-v1",
1659 "data-stream-attributes",
1660 ))
1661 {
1662 return Err(FormatError::InvalidArchive(
1663 "Windows regular primary lacks default-data-stream attributes or an omission",
1664 ));
1665 }
1666 if let Some(attributes) = stream_attributes {
1667 if (attributes & STREAM_SPARSE_ATTRIBUTE != 0) != sparse {
1668 let fallback = !sparse
1669 && primary.declaration.capture_status == CaptureStatus::Partial
1670 && has_capture_omission(capture_report, "windows-backup-v1", "sparse-layout");
1671 if !fallback {
1672 return Err(FormatError::InvalidArchive(
1673 "Windows primary sparse attribute disagrees with sparse framing",
1674 ));
1675 }
1676 }
1677 let _requires_system = attributes & STREAM_CONTAINS_SECURITY != 0;
1678 } else if sparse
1679 && !has_capture_omission(
1680 capture_report,
1681 "windows-backup-v1",
1682 "data-stream-attributes",
1683 )
1684 {
1685 return Err(FormatError::InvalidArchive(
1686 "sparse Windows primary lacks default-stream attributes",
1687 ));
1688 }
1689 Ok(())
1690}
1691
1692fn has_capture_omission(
1693 report: Option<&[CaptureReportRow]>,
1694 profile: &str,
1695 metadata_class: &str,
1696) -> bool {
1697 report.is_some_and(|rows| {
1698 rows.iter()
1699 .any(|row| row.profile == profile && row.metadata_class == metadata_class)
1700 })
1701}
1702
1703fn parse_lower_hex_u32(value: &[u8], structure: &'static str) -> Result<u32, FormatError> {
1704 if value.len() != 8
1705 || !value
1706 .iter()
1707 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
1708 {
1709 return Err(FormatError::InvalidMetadata {
1710 structure,
1711 reason: "value is not eight lowercase hexadecimal digits",
1712 });
1713 }
1714 std::str::from_utf8(value)
1715 .ok()
1716 .and_then(|text| u32::from_str_radix(text, 16).ok())
1717 .ok_or(FormatError::InvalidMetadata {
1718 structure,
1719 reason: "hexadecimal value exceeds u32",
1720 })
1721}
1722
1723fn v45_group_flags(
1724 primary: &PrimaryMetadata,
1725 auxiliary: &[AuxiliaryRecord],
1726 kind: TarEntryKind,
1727) -> Result<(u32, Option<Vec<crate::entry_metadata::CaptureReportRow>>), FormatError> {
1728 let (mut flags, capture_report) = validate_group_metadata(primary, auxiliary)?;
1729 if matches!(
1730 kind,
1731 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo
1732 ) {
1733 flags |= REQUIRES_SYSTEM_RESTORE;
1734 }
1735 Ok((flags, capture_report))
1736}
1737
1738fn parse_minimal_decimal_u64(value: &[u8], structure: &'static str) -> Result<u64, FormatError> {
1739 if value.is_empty()
1740 || !value.iter().all(u8::is_ascii_digit)
1741 || (value.len() > 1 && value[0] == b'0')
1742 {
1743 return Err(FormatError::InvalidMetadata {
1744 structure,
1745 reason: "value is not minimal unsigned decimal",
1746 });
1747 }
1748 std::str::from_utf8(value)
1749 .ok()
1750 .and_then(|text| text.parse().ok())
1751 .ok_or(FormatError::InvalidMetadata {
1752 structure,
1753 reason: "value exceeds u64",
1754 })
1755}
1756
1757pub fn validate_tar_stream_total_extraction_size(
1758 stream: &[u8],
1759 max_path_length: u32,
1760 cap: u64,
1761) -> Result<(), FormatError> {
1762 if stream.len() % TAR_BLOCK_LEN != 0 {
1763 return Err(FormatError::InvalidArchive(
1764 "tar stream is not block aligned",
1765 ));
1766 }
1767
1768 let mut cursor = 0usize;
1769 let mut total = 0u64;
1770 while cursor < stream.len() {
1771 let group_end = tar_member_group_end(stream, cursor)?;
1772 let member = parse_tar_member_group(&stream[cursor..group_end], max_path_length)?;
1773 if member.kind == TarEntryKind::Regular {
1774 total = total
1775 .checked_add(member.logical_size)
1776 .ok_or(FormatError::InvalidArchive(
1777 "total extraction size overflow",
1778 ))?;
1779 if total > cap {
1780 return Err(FormatError::ReaderUnsupported(
1781 "total extraction size exceeds configured cap",
1782 ));
1783 }
1784 }
1785 cursor = group_end;
1786 }
1787 Ok(())
1788}
1789
1790pub(crate) struct TarStreamTotalExtractionSizeValidator {
1791 cursor: usize,
1792 total: u64,
1793 max_path_length: u32,
1794 cap: u64,
1795}
1796
1797impl TarStreamTotalExtractionSizeValidator {
1798 pub(crate) fn new(max_path_length: u32, cap: u64) -> Self {
1799 Self {
1800 cursor: 0,
1801 total: 0,
1802 max_path_length,
1803 cap,
1804 }
1805 }
1806
1807 pub(crate) fn observe(&mut self, stream: &[u8]) -> Result<(), FormatError> {
1808 while self.cursor < stream.len() {
1809 let Some(group_end) = try_tar_member_group_end(stream, self.cursor)? else {
1810 return Ok(());
1811 };
1812 let member =
1813 parse_tar_member_group(&stream[self.cursor..group_end], self.max_path_length)?;
1814 if member.kind == TarEntryKind::Regular {
1815 self.total = self.total.checked_add(member.logical_size).ok_or(
1816 FormatError::InvalidArchive("total extraction size overflow"),
1817 )?;
1818 if self.total > self.cap {
1819 return Err(FormatError::ReaderUnsupported(
1820 "total extraction size exceeds configured cap",
1821 ));
1822 }
1823 }
1824 self.cursor = group_end;
1825 }
1826 Ok(())
1827 }
1828}
1829
1830pub(crate) struct TarStreamSummaryValidator<O = NoopTarStreamObserver> {
1831 state: StreamingTarState,
1832 max_path_length: u32,
1833 total_extraction_size: u64,
1834 extraction_cap: u64,
1835 max_metadata_payload_bytes: usize,
1836 max_member_count: u64,
1837 members: Vec<TarStreamMemberSummary>,
1838 observer: O,
1839}
1840
1841impl<O: TarStreamObserver> TarStreamSummaryValidator<O> {
1842 pub(crate) fn with_observer(
1843 max_path_length: u32,
1844 extraction_cap: u64,
1845 max_metadata_payload_bytes: usize,
1846 max_member_count: u64,
1847 observer: O,
1848 ) -> Self {
1849 Self {
1850 state: StreamingTarState::new_member(0),
1851 max_path_length,
1852 total_extraction_size: 0,
1853 extraction_cap,
1854 max_metadata_payload_bytes,
1855 max_member_count,
1856 members: Vec::new(),
1857 observer,
1858 }
1859 }
1860
1861 pub(crate) fn observe(&mut self, mut input: &[u8]) -> Result<(), FormatError> {
1862 while !input.is_empty() {
1863 let state = std::mem::replace(&mut self.state, StreamingTarState::new_member(0));
1864 let (consumed, next) = self.consume_state(state, input)?;
1865 self.state = self.resolve_ready_state(next)?;
1866 input = &input[consumed..];
1867 }
1868 Ok(())
1869 }
1870
1871 fn consume_state(
1872 &mut self,
1873 state: StreamingTarState,
1874 input: &[u8],
1875 ) -> Result<(usize, StreamingTarState), FormatError> {
1876 match state {
1877 StreamingTarState::Header {
1878 metadata,
1879 group_start,
1880 mut group_size,
1881 mut header,
1882 } => {
1883 let needed = TAR_BLOCK_LEN - header.len();
1884 let take = needed.min(input.len());
1885 header.extend_from_slice(&input[..take]);
1886 group_size = checked_u64_add(group_size, take as u64)?;
1887 checked_u64_add(group_start, group_size)?;
1888 let next = if header.len() == TAR_BLOCK_LEN {
1889 let mut header_bytes = [0u8; TAR_BLOCK_LEN];
1890 header_bytes.copy_from_slice(&header);
1891 self.state_after_header(metadata, group_start, group_size, header_bytes)?
1892 } else {
1893 StreamingTarState::Header {
1894 metadata,
1895 group_start,
1896 group_size,
1897 header,
1898 }
1899 };
1900 Ok((take, next))
1901 }
1902 StreamingTarState::Payload {
1903 metadata,
1904 group_start,
1905 mut group_size,
1906 mut entry,
1907 mut remaining,
1908 padding_remaining,
1909 } => {
1910 let take = remaining.min(input.len() as u64) as usize;
1911 match &mut entry {
1912 PendingTarEntry::LocalPax { payload, .. } => {
1913 let next_len = checked_add(payload.len(), take)?;
1914 let cap = self.max_metadata_payload_bytes.min(MAX_LOCAL_PAX_PAYLOAD);
1915 if next_len > cap {
1916 return Err(FormatError::ReaderUnsupported(
1917 "tar metadata payload exceeds configured streaming cap",
1918 ));
1919 }
1920 payload.extend_from_slice(&input[..take]);
1921 }
1922 PendingTarEntry::Auxiliary {
1923 validator,
1924 stream_to_observer,
1925 } => {
1926 validator.observe(&input[..take])?;
1927 if *stream_to_observer {
1928 self.observer.on_auxiliary_payload(&input[..take])?;
1929 }
1930 }
1931 PendingTarEntry::Main { member, sparse, .. }
1932 if take > 0 && member.kind == TarEntryKind::Regular =>
1933 {
1934 if let Some(sparse) = sparse {
1935 sparse.observe(&input[..take], &mut self.observer)?;
1936 } else {
1937 self.observer.on_regular_payload(&input[..take])?;
1938 }
1939 }
1940 PendingTarEntry::Main { .. } => {}
1941 }
1942 remaining -= take as u64;
1943 group_size = checked_u64_add(group_size, take as u64)?;
1944 checked_u64_add(group_start, group_size)?;
1945 let next = if remaining == 0 {
1946 StreamingTarState::Padding {
1947 metadata,
1948 group_start,
1949 group_size,
1950 entry,
1951 remaining: padding_remaining,
1952 }
1953 } else {
1954 StreamingTarState::Payload {
1955 metadata,
1956 group_start,
1957 group_size,
1958 entry,
1959 remaining,
1960 padding_remaining,
1961 }
1962 };
1963 Ok((take, next))
1964 }
1965 StreamingTarState::Padding {
1966 metadata,
1967 group_start,
1968 mut group_size,
1969 entry,
1970 mut remaining,
1971 } => {
1972 let take = remaining.min(input.len() as u64) as usize;
1973 if input[..take].iter().any(|byte| *byte != 0) {
1974 return Err(FormatError::InvalidArchive(
1975 "tar member padding is non-zero",
1976 ));
1977 }
1978 remaining -= take as u64;
1979 group_size = checked_u64_add(group_size, take as u64)?;
1980 checked_u64_add(group_start, group_size)?;
1981 let next = if remaining == 0 {
1982 self.finish_entry_parts(metadata, group_start, group_size, entry)?
1983 } else {
1984 StreamingTarState::Padding {
1985 metadata,
1986 group_start,
1987 group_size,
1988 entry,
1989 remaining,
1990 }
1991 };
1992 Ok((take, next))
1993 }
1994 }
1995 }
1996
1997 fn resolve_ready_state(
1998 &mut self,
1999 mut state: StreamingTarState,
2000 ) -> Result<StreamingTarState, FormatError> {
2001 loop {
2002 state = match state {
2003 StreamingTarState::Payload {
2004 metadata,
2005 group_start,
2006 group_size,
2007 entry,
2008 remaining: 0,
2009 padding_remaining,
2010 } => StreamingTarState::Padding {
2011 metadata,
2012 group_start,
2013 group_size,
2014 entry,
2015 remaining: padding_remaining,
2016 },
2017 StreamingTarState::Padding {
2018 metadata,
2019 group_start,
2020 group_size,
2021 entry,
2022 remaining: 0,
2023 } => self.finish_entry_parts(metadata, group_start, group_size, entry)?,
2024 other => return Ok(other),
2025 };
2026 }
2027 }
2028
2029 pub(crate) fn tar_total_size(&self) -> u64 {
2030 match &self.state {
2031 StreamingTarState::Header {
2032 group_start,
2033 group_size,
2034 ..
2035 }
2036 | StreamingTarState::Payload {
2037 group_start,
2038 group_size,
2039 ..
2040 }
2041 | StreamingTarState::Padding {
2042 group_start,
2043 group_size,
2044 ..
2045 } => group_start + group_size,
2046 }
2047 }
2048
2049 pub(crate) fn finish(mut self) -> Result<TarStreamSummary, FormatError> {
2050 let tar_total_size = self.tar_total_size();
2051 match self.state {
2052 StreamingTarState::Header {
2053 header, group_size, ..
2054 } if header.is_empty() && group_size == 0 => {
2055 validate_v45_member_graph(&self.members)?;
2056 let late_diagnostics = self.observer.on_archive_complete()?;
2057 for diagnostic in late_diagnostics {
2058 let member = self
2059 .members
2060 .iter_mut()
2061 .find(|member| member.path == diagnostic.path)
2062 .ok_or(FormatError::InvalidArchive(
2063 "archive-finalization diagnostic path is missing",
2064 ))?;
2065 member.diagnostics.push(diagnostic);
2066 }
2067 Ok(TarStreamSummary {
2068 members: self.members,
2069 tar_total_size,
2070 total_extraction_size: self.total_extraction_size,
2071 })
2072 }
2073 _ => Err(FormatError::InvalidArchive(
2074 "tar stream ended inside member group",
2075 )),
2076 }
2077 }
2078
2079 fn state_after_header(
2080 &mut self,
2081 mut metadata: V45StreamingGroup,
2082 group_start: u64,
2083 group_size: u64,
2084 header: [u8; TAR_BLOCK_LEN],
2085 ) -> Result<StreamingTarState, FormatError> {
2086 if header.iter().all(|byte| *byte == 0) {
2087 return Err(FormatError::InvalidArchive("tar member header is empty"));
2088 }
2089 verify_tar_checksum(&header)?;
2090 let typeflag = header[156];
2091 let header_size = parse_tar_octal(&header[124..136])?;
2092 let effective_size = metadata
2093 .pending
2094 .as_ref()
2095 .and_then(|(_, records)| records.get("size"))
2096 .map(|value| parse_minimal_decimal_u64(value, "PAX size"))
2097 .transpose()?
2098 .unwrap_or(header_size);
2099 let padding_remaining = padding_to_512_u64(effective_size);
2100
2101 let entry = match typeflag {
2102 b'x' => {
2103 if metadata.pending.is_some() {
2104 return Err(FormatError::InvalidArchive(
2105 "PAX header is not immediately consumed",
2106 ));
2107 }
2108 validate_v45_metadata_header(&header)?;
2109 if effective_size > MAX_LOCAL_PAX_PAYLOAD as u64
2110 || effective_size > self.max_metadata_payload_bytes as u64
2111 {
2112 return Err(FormatError::ReaderUnsupported(
2113 "tar metadata payload exceeds configured streaming cap",
2114 ));
2115 }
2116 let label = ustar_path(&header);
2117 let kind = if label == b"TZAP-PAX/PRIMARY" {
2118 V45PaxKind::Primary
2119 } else if let Some(ordinal) = parse_auxiliary_pax_label(&label) {
2120 if ordinal != metadata.auxiliary.len() as u32 {
2121 return Err(FormatError::InvalidArchive(
2122 "auxiliary PAX ordinal is not contiguous",
2123 ));
2124 }
2125 V45PaxKind::Auxiliary(ordinal)
2126 } else {
2127 return Err(FormatError::InvalidArchive(
2128 "revision-45 PAX header has a non-canonical internal name",
2129 ));
2130 };
2131 PendingTarEntry::LocalPax {
2132 kind,
2133 payload: Vec::new(),
2134 }
2135 }
2136 b'Z' => {
2137 let Some((V45PaxKind::Auxiliary(ordinal), records)) = metadata.pending.take()
2138 else {
2139 return Err(FormatError::InvalidArchive(
2140 "auxiliary entry is missing its local PAX header",
2141 ));
2142 };
2143 validate_v45_auxiliary_header(&header, ordinal, header_size, effective_size)?;
2144 let validator = AuxiliaryStreamValidator::new(&records, ordinal, effective_size)?;
2145 let stream_to_observer =
2146 self.observer.on_auxiliary_start(validator.declaration())?;
2147 PendingTarEntry::Auxiliary {
2148 validator,
2149 stream_to_observer,
2150 }
2151 }
2152 b'g' | b'L' | b'K' | b'V' | b'M' | b'N' | b'S' => {
2153 return Err(FormatError::InvalidArchive(
2154 "global or GNU tar metadata is forbidden in revision 45",
2155 ));
2156 }
2157 0 | b'0' | b'5' | b'2' | b'1' | b'3' | b'4' | b'6' => {
2158 let Some((V45PaxKind::Primary, records)) = metadata.pending.take() else {
2159 return Err(FormatError::InvalidArchive(
2160 "primary entry is missing its canonical local PAX header",
2161 ));
2162 };
2163 let kind = match typeflag {
2164 b'5' => TarEntryKind::Directory,
2165 b'2' => TarEntryKind::Symlink,
2166 b'1' => TarEntryKind::Hardlink,
2167 b'3' => TarEntryKind::CharacterDevice,
2168 b'4' => TarEntryKind::BlockDevice,
2169 b'6' => TarEntryKind::Fifo,
2170 _ => TarEntryKind::Regular,
2171 };
2172 let primary = parse_primary_metadata(&records)?;
2173 validate_v45_primary_header(
2174 &header,
2175 kind,
2176 header_size,
2177 effective_size,
2178 &primary,
2179 &records,
2180 )?;
2181 let path =
2182 v45_primary_path(&header, kind, &records, &primary, self.max_path_length)?;
2183 let link_target =
2184 v45_primary_link_target(&header, kind, &path, &primary, self.max_path_length)?;
2185 let is_sparse = primary.sparse_logical_size.is_some();
2186 let reparse_placeholder = records.contains_key("TZAP.windows.reparse-placeholder");
2187 if kind != TarEntryKind::Regular && effective_size != 0 {
2188 return Err(FormatError::InvalidArchive(
2189 "non-regular tar entry has non-zero payload size",
2190 ));
2191 }
2192 if reparse_placeholder && effective_size != 0 {
2193 return Err(FormatError::InvalidArchive(
2194 "reparse placeholder has non-zero primary payload",
2195 ));
2196 }
2197 let logical_size = if kind == TarEntryKind::Regular && !reparse_placeholder {
2198 primary.sparse_logical_size.unwrap_or(effective_size)
2199 } else {
2200 0
2201 };
2202 let (file_entry_flags, capture_report) =
2203 v45_group_flags(&primary, &metadata.auxiliary, kind)?;
2204 validate_v45_primary_cross_fields(
2205 kind,
2206 &records,
2207 &primary,
2208 &metadata.auxiliary,
2209 V45PrimaryLink {
2210 path: &path,
2211 target: link_target.as_deref(),
2212 },
2213 is_sparse,
2214 capture_report.as_deref(),
2215 )?;
2216 if kind == TarEntryKind::Regular {
2217 self.total_extraction_size =
2218 self.total_extraction_size.checked_add(logical_size).ok_or(
2219 FormatError::InvalidArchive("total extraction size overflow"),
2220 )?;
2221 if self.total_extraction_size > self.extraction_cap {
2222 return Err(FormatError::ReaderUnsupported(
2223 "total extraction size exceeds configured cap",
2224 ));
2225 }
2226 }
2227 let diagnostics = Vec::new();
2228 let mtime = decoded_mtime(&primary, &header)?;
2229 let member = StreamedTarMemberMetadata {
2230 path,
2231 kind,
2232 link_target,
2233 mode: primary.declaration.portable_mode,
2234 mtime,
2235 logical_size,
2236 file_entry_flags,
2237 reparse_placeholder,
2238 v45_metadata: MemberMetadata {
2239 declaration: primary.declaration.clone(),
2240 primary_records: records.clone(),
2241 auxiliary: metadata.auxiliary.clone(),
2242 file_entry_flags,
2243 sparse_layout: None,
2244 capture_report,
2245 primary_has_native_scalar: primary.has_native_scalar,
2246 primary_requires_system_restore: primary.requires_system_restore,
2247 portable_mirror: portable_metadata_mirror(&header, &records, &primary)?,
2248 },
2249 diagnostics,
2250 };
2251 self.observer.on_member_start(&member)?;
2252 PendingTarEntry::Main {
2253 member,
2254 group_start,
2255 sparse: primary.sparse_logical_size.map(StreamingSparsePrimary::new),
2256 }
2257 }
2258 _ => {
2259 return Err(FormatError::InvalidArchive(
2260 "unsupported revision-45 tar entry type",
2261 ));
2262 }
2263 };
2264
2265 self.resolve_ready_state(StreamingTarState::Payload {
2266 metadata,
2267 group_start,
2268 group_size,
2269 entry,
2270 remaining: effective_size,
2271 padding_remaining,
2272 })
2273 }
2274
2275 fn finish_entry_parts(
2276 &mut self,
2277 mut metadata: V45StreamingGroup,
2278 group_start: u64,
2279 group_size: u64,
2280 entry: PendingTarEntry,
2281 ) -> Result<StreamingTarState, FormatError> {
2282 match entry {
2283 PendingTarEntry::LocalPax { kind, payload } => {
2284 metadata.aggregate_pax_bytes = metadata
2285 .aggregate_pax_bytes
2286 .checked_add(payload.len())
2287 .ok_or(FormatError::InvalidArchive("aggregate PAX size overflow"))?;
2288 if metadata.aggregate_pax_bytes > MAX_AGGREGATE_PAX_PAYLOAD {
2289 return Err(FormatError::ReaderResourceLimitExceeded {
2290 field: "aggregate local PAX payload bytes per member group",
2291 cap: MAX_AGGREGATE_PAX_PAYLOAD as u64,
2292 actual: metadata.aggregate_pax_bytes as u64,
2293 });
2294 }
2295 metadata.pending = Some((kind, parse_canonical_pax(&payload)?));
2296 Ok(StreamingTarState::Header {
2297 metadata,
2298 group_start,
2299 group_size,
2300 header: Vec::new(),
2301 })
2302 }
2303 PendingTarEntry::Auxiliary {
2304 validator,
2305 stream_to_observer,
2306 } => {
2307 let record = validator.finish()?;
2308 if stream_to_observer {
2309 self.observer.on_auxiliary_complete(&record)?;
2310 }
2311 metadata.auxiliary.push(record);
2312 Ok(StreamingTarState::Header {
2313 metadata,
2314 group_start,
2315 group_size,
2316 header: Vec::new(),
2317 })
2318 }
2319 PendingTarEntry::Main {
2320 member,
2321 group_start,
2322 sparse,
2323 } => {
2324 if self.members.len() as u64 >= self.max_member_count {
2325 return Err(FormatError::ReaderUnsupported(
2326 "tar member count exceeds configured streaming cap",
2327 ));
2328 }
2329 if let Some(sparse) = sparse {
2330 sparse.finish(&mut self.observer)?;
2331 }
2332 let diagnostics = self.observer.on_member_complete(&member)?;
2333 self.members.push(TarStreamMemberSummary {
2334 path: member.path,
2335 kind: member.kind,
2336 link_target: member.link_target,
2337 mode: member.mode,
2338 mtime: member.mtime,
2339 logical_size: member.logical_size,
2340 file_entry_flags: member.file_entry_flags,
2341 reparse_placeholder: member.reparse_placeholder,
2342 v45_metadata: member.v45_metadata,
2343 diagnostics,
2344 group_start,
2345 group_size,
2346 });
2347 Ok(StreamingTarState::new_member(checked_u64_add(
2348 group_start,
2349 group_size,
2350 )?))
2351 }
2352 }
2353 }
2354}
2355
2356pub(crate) fn validate_v45_member_graph(
2357 members: &[TarStreamMemberSummary],
2358) -> Result<(), FormatError> {
2359 let mut selected = BTreeMap::<&[u8], &TarStreamMemberSummary>::new();
2360 for member in members {
2361 let replace = selected
2362 .get(member.path.as_slice())
2363 .is_none_or(|existing| existing.group_start < member.group_start);
2364 if replace {
2365 selected.insert(member.path.as_slice(), member);
2366 }
2367 }
2368 for member in selected.values() {
2369 if member.kind == TarEntryKind::Hardlink {
2370 let target_path = member
2371 .link_target
2372 .as_deref()
2373 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
2374 let target = selected
2375 .get(target_path)
2376 .ok_or(FormatError::InvalidArchive(
2377 "hardlink target is not present in the selected archive graph",
2378 ))?;
2379 if target.kind != TarEntryKind::Regular || target.reparse_placeholder {
2380 return Err(FormatError::InvalidArchive(
2381 "hardlink target is not a canonical regular primary",
2382 ));
2383 }
2384 if member.v45_metadata.portable_mirror != target.v45_metadata.portable_mirror {
2385 return Err(FormatError::InvalidArchive(
2386 "hardlink portable metadata mirror differs from canonical target",
2387 ));
2388 }
2389 }
2390
2391 let mut ancestor = Vec::new();
2392 let components: Vec<_> = member.path.split(|byte| *byte == b'/').collect();
2393 for component in components.iter().take(components.len().saturating_sub(1)) {
2394 if !ancestor.is_empty() {
2395 ancestor.push(b'/');
2396 }
2397 ancestor.extend_from_slice(component);
2398 if let Some(parent) = selected.get(ancestor.as_slice()) {
2399 if parent.reparse_placeholder || parent.kind == TarEntryKind::Symlink {
2400 return Err(FormatError::InvalidArchive(
2401 "selected path graph traverses a symlink or reparse ancestor",
2402 ));
2403 }
2404 if parent.kind != TarEntryKind::Directory {
2405 return Err(FormatError::InvalidArchive(
2406 "selected path graph traverses a non-directory ancestor",
2407 ));
2408 }
2409 }
2410 }
2411 }
2412 Ok(())
2413}
2414
2415pub(crate) fn validate_owned_restore_plan(
2416 members: &[&OwnedTarMember],
2417 options: SafeExtractionOptions,
2418) -> Result<(), FormatError> {
2419 let mut selected = BTreeMap::<&[u8], &OwnedTarMember>::new();
2420 for &member in members {
2421 if selected.insert(member.path.as_slice(), member).is_some() {
2422 return Err(FormatError::InvalidArchive(
2423 "restore plan contains duplicate selected paths",
2424 ));
2425 }
2426 plan_owned_member_restore(member, options)?;
2427 }
2428 for member in selected.values() {
2429 if member.kind == TarEntryKind::Hardlink {
2430 let target_path = member
2431 .link_target
2432 .as_deref()
2433 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
2434 let target = selected
2435 .get(target_path)
2436 .ok_or(FormatError::InvalidArchive(
2437 "hardlink target is not present in the selected restore graph",
2438 ))?;
2439 if target.kind != TarEntryKind::Regular || target.reparse_placeholder {
2440 return Err(FormatError::InvalidArchive(
2441 "hardlink target is not a canonical regular primary",
2442 ));
2443 }
2444 let alias_metadata = member.v45_metadata.as_ref().expect("checked above");
2445 let target_metadata = target.v45_metadata.as_ref().expect("checked above");
2446 if alias_metadata.portable_mirror != target_metadata.portable_mirror {
2447 return Err(FormatError::InvalidArchive(
2448 "hardlink portable metadata mirror differs from canonical target",
2449 ));
2450 }
2451 }
2452
2453 let mut ancestor = Vec::new();
2454 let components: Vec<_> = member.path.split(|byte| *byte == b'/').collect();
2455 for component in components.iter().take(components.len().saturating_sub(1)) {
2456 if !ancestor.is_empty() {
2457 ancestor.push(b'/');
2458 }
2459 ancestor.extend_from_slice(component);
2460 if let Some(parent) = selected.get(ancestor.as_slice()) {
2461 if parent.reparse_placeholder || parent.kind == TarEntryKind::Symlink {
2462 return Err(FormatError::InvalidArchive(
2463 "restore path traverses a selected symlink or reparse ancestor",
2464 ));
2465 }
2466 if parent.kind != TarEntryKind::Directory {
2467 return Err(FormatError::InvalidArchive(
2468 "restore path traverses a selected non-directory ancestor",
2469 ));
2470 }
2471 }
2472 }
2473 }
2474 Ok(())
2475}
2476
2477pub(crate) fn plan_owned_member_restore(
2478 member: &OwnedTarMember,
2479 options: SafeExtractionOptions,
2480) -> Result<Vec<MetadataDiagnostic>, FormatError> {
2481 let metadata = member
2482 .v45_metadata
2483 .as_ref()
2484 .ok_or(FormatError::InvalidArchive(
2485 "revision-45 member metadata is missing",
2486 ))?;
2487 plan_restore(
2488 &member.path,
2489 metadata,
2490 member.kind,
2491 member.reparse_placeholder,
2492 options,
2493 )
2494}
2495
2496pub(crate) fn restore_phase(member: &OwnedTarMember) -> u8 {
2497 restore_phase_for_kind(member.kind, member.reparse_placeholder)
2498}
2499
2500fn restore_phase_for_kind(kind: TarEntryKind, reparse_placeholder: bool) -> u8 {
2501 if reparse_placeholder {
2502 return 3;
2503 }
2504 match kind {
2505 TarEntryKind::Directory => 4,
2506 TarEntryKind::Regular => 1,
2507 TarEntryKind::Symlink
2508 | TarEntryKind::CharacterDevice
2509 | TarEntryKind::BlockDevice
2510 | TarEntryKind::Fifo => 2,
2511 TarEntryKind::Hardlink => 3,
2512 }
2513}
2514
2515enum StreamingTarState {
2516 Header {
2517 metadata: V45StreamingGroup,
2518 group_start: u64,
2519 group_size: u64,
2520 header: Vec<u8>,
2521 },
2522 Payload {
2523 metadata: V45StreamingGroup,
2524 group_start: u64,
2525 group_size: u64,
2526 entry: PendingTarEntry,
2527 remaining: u64,
2528 padding_remaining: u64,
2529 },
2530 Padding {
2531 metadata: V45StreamingGroup,
2532 group_start: u64,
2533 group_size: u64,
2534 entry: PendingTarEntry,
2535 remaining: u64,
2536 },
2537}
2538
2539impl StreamingTarState {
2540 fn new_member(group_start: u64) -> Self {
2541 Self::Header {
2542 metadata: V45StreamingGroup::default(),
2543 group_start,
2544 group_size: 0,
2545 header: Vec::new(),
2546 }
2547 }
2548}
2549
2550enum PendingTarEntry {
2551 LocalPax {
2552 kind: V45PaxKind,
2553 payload: Vec<u8>,
2554 },
2555 Auxiliary {
2556 validator: AuxiliaryStreamValidator,
2557 stream_to_observer: bool,
2558 },
2559 Main {
2560 member: StreamedTarMemberMetadata,
2561 group_start: u64,
2562 sparse: Option<StreamingSparsePrimary>,
2563 },
2564}
2565
2566fn checked_u64_add(lhs: u64, rhs: u64) -> Result<u64, FormatError> {
2567 lhs.checked_add(rhs).ok_or(FormatError::InvalidArchive(
2568 "tar member arithmetic overflow",
2569 ))
2570}
2571
2572pub(crate) fn try_tar_member_group_end(
2573 stream: &[u8],
2574 start: usize,
2575) -> Result<Option<usize>, FormatError> {
2576 let mut cursor = start;
2577 let mut pending: Option<(V45PaxKind, PaxRecords)> = None;
2578 let mut auxiliary_count = 0u32;
2579 let mut aggregate_pax_bytes = 0usize;
2580
2581 loop {
2582 let Some(header) = try_slice(stream, cursor, TAR_BLOCK_LEN)? else {
2583 return Ok(None);
2584 };
2585 if header.iter().all(|byte| *byte == 0) {
2586 return Err(FormatError::InvalidArchive("tar member header is empty"));
2587 }
2588 verify_tar_checksum(header)?;
2589 let typeflag = header[156];
2590 let header_size = parse_tar_octal(&header[124..136])?;
2591 let effective_size = pending
2592 .as_ref()
2593 .and_then(|(_, records)| records.get("size"))
2594 .map(|value| parse_minimal_decimal_u64(value, "PAX size"))
2595 .transpose()?
2596 .unwrap_or(header_size);
2597 let payload_start = checked_add(cursor, TAR_BLOCK_LEN)?;
2598 let payload_len = to_usize(effective_size)?;
2599 let payload_end = checked_add(payload_start, payload_len)?;
2600 let padded_end = checked_add(payload_end, padding_to_512(payload_len))?;
2601 let Some(payload) = try_slice(stream, payload_start, payload_len)? else {
2602 return Ok(None);
2603 };
2604 if padded_end > stream.len() {
2605 return Ok(None);
2606 }
2607 if stream[payload_end..padded_end]
2608 .iter()
2609 .any(|byte| *byte != 0)
2610 {
2611 return Err(FormatError::InvalidArchive(
2612 "tar member padding is non-zero",
2613 ));
2614 }
2615
2616 match typeflag {
2617 b'x' => {
2618 if pending.is_some() {
2619 return Err(FormatError::InvalidArchive(
2620 "PAX header is not immediately consumed",
2621 ));
2622 }
2623 validate_v45_metadata_header(header)?;
2624 aggregate_pax_bytes = aggregate_pax_bytes
2625 .checked_add(payload.len())
2626 .ok_or(FormatError::InvalidArchive("aggregate PAX size overflow"))?;
2627 if aggregate_pax_bytes > MAX_AGGREGATE_PAX_PAYLOAD {
2628 return Err(FormatError::ReaderResourceLimitExceeded {
2629 field: "aggregate local PAX payload bytes per member group",
2630 cap: MAX_AGGREGATE_PAX_PAYLOAD as u64,
2631 actual: aggregate_pax_bytes as u64,
2632 });
2633 }
2634 let records = parse_canonical_pax(payload)?;
2635 let label = ustar_path(header);
2636 let kind = if label == b"TZAP-PAX/PRIMARY" {
2637 V45PaxKind::Primary
2638 } else if let Some(ordinal) = parse_auxiliary_pax_label(&label) {
2639 if ordinal != auxiliary_count {
2640 return Err(FormatError::InvalidArchive(
2641 "auxiliary PAX ordinal is not contiguous",
2642 ));
2643 }
2644 V45PaxKind::Auxiliary(ordinal)
2645 } else {
2646 return Err(FormatError::InvalidArchive(
2647 "revision-45 PAX header has a non-canonical internal name",
2648 ));
2649 };
2650 pending = Some((kind, records));
2651 cursor = padded_end;
2652 }
2653 b'Z' => {
2654 let Some((V45PaxKind::Auxiliary(ordinal), _)) = pending.take() else {
2655 return Err(FormatError::InvalidArchive(
2656 "auxiliary entry is missing its local PAX header",
2657 ));
2658 };
2659 validate_v45_auxiliary_header(header, ordinal, header_size, effective_size)?;
2660 auxiliary_count = auxiliary_count
2661 .checked_add(1)
2662 .ok_or(FormatError::InvalidArchive("auxiliary count overflow"))?;
2663 cursor = padded_end;
2664 }
2665 b'g' | b'L' | b'K' | b'V' | b'M' | b'N' | b'S' => {
2666 return Err(FormatError::InvalidArchive(
2667 "global or GNU tar metadata is forbidden in revision 45",
2668 ));
2669 }
2670 0 | b'0' | b'5' | b'2' | b'1' | b'3' | b'4' | b'6' => {
2671 if !matches!(pending, Some((V45PaxKind::Primary, _))) {
2672 return Err(FormatError::InvalidArchive(
2673 "primary entry is missing its canonical local PAX header",
2674 ));
2675 }
2676 return Ok(Some(padded_end));
2677 }
2678 _ => {
2679 return Err(FormatError::InvalidArchive(
2680 "unsupported revision-45 tar entry type",
2681 ));
2682 }
2683 }
2684
2685 if cursor >= stream.len() {
2686 return Ok(None);
2687 }
2688 }
2689}
2690
2691fn try_slice(stream: &[u8], offset: usize, len: usize) -> Result<Option<&[u8]>, FormatError> {
2692 let end = checked_add(offset, len)?;
2693 if end > stream.len() {
2694 return Ok(None);
2695 }
2696 Ok(Some(&stream[offset..end]))
2697}
2698
2699pub(crate) fn stream_regular_tar_member_group_to_writer<R, W>(
2700 reader: &mut R,
2701 expected_path: &[u8],
2702 expected_file_data_size: u64,
2703 expected_file_flags: u32,
2704 group_len: u64,
2705 max_path_length: u32,
2706 writer: &mut W,
2707) -> Result<Vec<MetadataDiagnostic>, ExtractError>
2708where
2709 R: TarMemberGroupReader,
2710 W: Write,
2711{
2712 let mut handler = RegularWriterHandler { writer };
2713 let member = stream_tar_member_group(
2714 reader,
2715 expected_path,
2716 expected_file_data_size,
2717 expected_file_flags,
2718 group_len,
2719 max_path_length,
2720 &mut handler,
2721 )?;
2722 Ok(member.diagnostics)
2723}
2724
2725#[derive(Debug, Clone, Copy)]
2726pub(crate) struct StreamingMemberExpectation<'a> {
2727 pub path: &'a [u8],
2728 pub file_data_size: u64,
2729 pub file_flags: u32,
2730 pub group_len: u64,
2731 pub max_path_length: u32,
2732}
2733
2734pub(crate) fn restore_streaming_tar_member_group<R>(
2735 root: &Path,
2736 expected: StreamingMemberExpectation<'_>,
2737 options: SafeExtractionOptions,
2738 reader: &mut R,
2739) -> Result<Vec<MetadataDiagnostic>, ExtractError>
2740where
2741 R: TarMemberGroupReader,
2742{
2743 let mut handler = FilesystemRestoreHandler::new(root, options);
2744 let member = stream_tar_member_group(
2745 reader,
2746 expected.path,
2747 expected.file_data_size,
2748 expected.file_flags,
2749 expected.group_len,
2750 expected.max_path_length,
2751 &mut handler,
2752 )?;
2753 handler.finish(&member)
2754}
2755
2756fn stream_tar_member_group<R, H>(
2757 reader: &mut R,
2758 expected_path: &[u8],
2759 expected_file_data_size: u64,
2760 expected_file_flags: u32,
2761 group_len: u64,
2762 max_path_length: u32,
2763 handler: &mut H,
2764) -> Result<StreamedTarMemberMetadata, ExtractError>
2765where
2766 R: TarMemberGroupReader,
2767 H: TarMemberStreamHandler,
2768{
2769 if group_len < (TAR_BLOCK_LEN * 3) as u64 || group_len % TAR_BLOCK_LEN as u64 != 0 {
2770 return Err(FormatError::InvalidArchive("tar member group is not block aligned").into());
2771 }
2772
2773 let mut remaining = group_len;
2774 let mut pending: Option<(V45PaxKind, PaxRecords)> = None;
2775 let mut auxiliary = Vec::<AuxiliaryRecord>::new();
2776 let mut aggregate_pax_bytes = 0usize;
2777
2778 loop {
2779 let mut header = [0u8; TAR_BLOCK_LEN];
2780 read_member_bytes(reader, &mut header, &mut remaining)?;
2781 if header.iter().all(|byte| *byte == 0) {
2782 return Err(FormatError::InvalidArchive("tar member header is empty").into());
2783 }
2784 verify_tar_checksum(&header)?;
2785
2786 let typeflag = header[156];
2787 let header_size = parse_tar_octal(&header[124..136])?;
2788 let effective_size = pending
2789 .as_ref()
2790 .and_then(|(_, records)| records.get("size"))
2791 .map(|value| parse_minimal_decimal_u64(value, "PAX size"))
2792 .transpose()?
2793 .unwrap_or(header_size);
2794 let padding_len = padding_to_512_u64(effective_size);
2795 let entry_payload_len =
2796 effective_size
2797 .checked_add(padding_len)
2798 .ok_or(FormatError::InvalidArchive(
2799 "tar member arithmetic overflow",
2800 ))?;
2801 if entry_payload_len > remaining {
2802 return Err(FormatError::InvalidArchive("tar member payload exceeds group").into());
2803 }
2804
2805 match typeflag {
2806 b'x' => {
2807 if pending.is_some() {
2808 return Err(FormatError::InvalidArchive(
2809 "PAX header is not immediately consumed",
2810 )
2811 .into());
2812 }
2813 validate_v45_metadata_header(&header)?;
2814 if effective_size > MAX_LOCAL_PAX_PAYLOAD as u64 {
2815 return Err(FormatError::ReaderResourceLimitExceeded {
2816 field: "local PAX payload bytes",
2817 cap: MAX_LOCAL_PAX_PAYLOAD as u64,
2818 actual: effective_size,
2819 }
2820 .into());
2821 }
2822 let payload = read_member_vec(reader, effective_size, &mut remaining)?;
2823 read_zero_padding(reader, padding_len, &mut remaining)?;
2824 aggregate_pax_bytes = aggregate_pax_bytes
2825 .checked_add(payload.len())
2826 .ok_or(FormatError::InvalidArchive("aggregate PAX size overflow"))?;
2827 if aggregate_pax_bytes > MAX_AGGREGATE_PAX_PAYLOAD {
2828 return Err(FormatError::ReaderResourceLimitExceeded {
2829 field: "aggregate local PAX payload bytes per member group",
2830 cap: MAX_AGGREGATE_PAX_PAYLOAD as u64,
2831 actual: aggregate_pax_bytes as u64,
2832 }
2833 .into());
2834 }
2835 let records = parse_canonical_pax(&payload)?;
2836 let label = ustar_path(&header);
2837 let kind = if label == b"TZAP-PAX/PRIMARY" {
2838 V45PaxKind::Primary
2839 } else if let Some(ordinal) = parse_auxiliary_pax_label(&label) {
2840 if ordinal != auxiliary.len() as u32 {
2841 return Err(FormatError::InvalidArchive(
2842 "auxiliary PAX ordinal is not contiguous",
2843 )
2844 .into());
2845 }
2846 V45PaxKind::Auxiliary(ordinal)
2847 } else {
2848 return Err(FormatError::InvalidArchive(
2849 "revision-45 PAX header has a non-canonical internal name",
2850 )
2851 .into());
2852 };
2853 pending = Some((kind, records));
2854 }
2855 b'Z' => {
2856 let Some((V45PaxKind::Auxiliary(ordinal), records)) = pending.take() else {
2857 return Err(FormatError::InvalidArchive(
2858 "auxiliary entry is missing its local PAX header",
2859 )
2860 .into());
2861 };
2862 validate_v45_auxiliary_header(&header, ordinal, header_size, effective_size)?;
2863 let mut validator =
2864 AuxiliaryStreamValidator::new(&records, ordinal, effective_size)?;
2865 let stream_to_handler = handler.begin_auxiliary_payload(validator.declaration())?;
2866 stream_auxiliary_payload(
2867 reader,
2868 effective_size,
2869 &mut remaining,
2870 &mut validator,
2871 stream_to_handler.then_some(handler),
2872 )?;
2873 read_zero_padding(reader, padding_len, &mut remaining)?;
2874 let record = validator.finish()?;
2875 if stream_to_handler {
2876 handler.finish_auxiliary_payload(&record)?;
2877 }
2878 auxiliary.push(record);
2879 }
2880 b'g' | b'L' | b'K' | b'V' | b'M' | b'N' | b'S' => {
2881 return Err(FormatError::InvalidArchive(
2882 "global or GNU tar metadata is forbidden in revision 45",
2883 )
2884 .into());
2885 }
2886 0 | b'0' | b'5' | b'2' | b'1' | b'3' | b'4' | b'6' => {
2887 let Some((V45PaxKind::Primary, records)) = pending.take() else {
2888 return Err(FormatError::InvalidArchive(
2889 "primary entry is missing its canonical local PAX header",
2890 )
2891 .into());
2892 };
2893 let kind = match typeflag {
2894 b'5' => TarEntryKind::Directory,
2895 b'2' => TarEntryKind::Symlink,
2896 b'1' => TarEntryKind::Hardlink,
2897 b'3' => TarEntryKind::CharacterDevice,
2898 b'4' => TarEntryKind::BlockDevice,
2899 b'6' => TarEntryKind::Fifo,
2900 _ => TarEntryKind::Regular,
2901 };
2902 let primary = parse_primary_metadata(&records)?;
2903 validate_v45_primary_header(
2904 &header,
2905 kind,
2906 header_size,
2907 effective_size,
2908 &primary,
2909 &records,
2910 )?;
2911 let path = v45_primary_path(&header, kind, &records, &primary, max_path_length)?;
2912 let link_target =
2913 v45_primary_link_target(&header, kind, &path, &primary, max_path_length)?;
2914 let sparse = primary.sparse_logical_size.is_some();
2915 let reparse_placeholder = records.contains_key("TZAP.windows.reparse-placeholder");
2916 if kind != TarEntryKind::Regular && effective_size != 0 {
2917 return Err(FormatError::InvalidArchive(
2918 "non-regular tar entry has non-zero payload size",
2919 )
2920 .into());
2921 }
2922 if reparse_placeholder && effective_size != 0 {
2923 return Err(FormatError::InvalidArchive(
2924 "reparse placeholder has non-zero primary payload",
2925 )
2926 .into());
2927 }
2928 let logical_size = if kind == TarEntryKind::Regular && !reparse_placeholder {
2929 primary.sparse_logical_size.unwrap_or(effective_size)
2930 } else {
2931 0
2932 };
2933 let (file_entry_flags, capture_report) =
2934 v45_group_flags(&primary, &auxiliary, kind)?;
2935 if file_entry_flags != expected_file_flags {
2936 return Err(FormatError::InvalidArchive(
2937 "tar member metadata flags do not match FileEntry flags",
2938 )
2939 .into());
2940 }
2941 validate_v45_primary_cross_fields(
2942 kind,
2943 &records,
2944 &primary,
2945 &auxiliary,
2946 V45PrimaryLink {
2947 path: &path,
2948 target: link_target.as_deref(),
2949 },
2950 sparse,
2951 capture_report.as_deref(),
2952 )?;
2953 let diagnostics = Vec::new();
2954 let mtime = decoded_mtime(&primary, &header)?;
2955 let member = StreamedTarMemberMetadata {
2956 path,
2957 kind,
2958 link_target,
2959 mode: primary.declaration.portable_mode,
2960 mtime,
2961 logical_size,
2962 file_entry_flags,
2963 reparse_placeholder,
2964 v45_metadata: MemberMetadata {
2965 declaration: primary.declaration.clone(),
2966 primary_records: records.clone(),
2967 auxiliary: auxiliary.clone(),
2968 file_entry_flags,
2969 sparse_layout: None,
2970 capture_report,
2971 primary_has_native_scalar: primary.has_native_scalar,
2972 primary_requires_system_restore: primary.requires_system_restore,
2973 portable_mirror: portable_metadata_mirror(&header, &records, &primary)?,
2974 },
2975 diagnostics,
2976 };
2977 if member.path != expected_path {
2978 return Err(FormatError::InvalidArchive(
2979 "tar member path does not match FileEntry path",
2980 )
2981 .into());
2982 }
2983 if member.logical_size != expected_file_data_size {
2984 return Err(FormatError::InvalidArchive(
2985 "tar member size does not match FileEntry file_data_size",
2986 )
2987 .into());
2988 }
2989 handler.on_member(&member)?;
2990 if member.kind == TarEntryKind::Regular {
2991 if let Some(logical_size) = primary.sparse_logical_size {
2992 stream_sparse_primary_payload(
2993 reader,
2994 effective_size,
2995 logical_size,
2996 &mut remaining,
2997 handler,
2998 )?;
2999 } else {
3000 stream_regular_payload(reader, effective_size, &mut remaining, handler)?;
3001 }
3002 }
3003 read_zero_padding(reader, padding_len, &mut remaining)?;
3004 if remaining != 0 {
3005 return Err(FormatError::InvalidArchive(
3006 "tar member group has bytes after main entry",
3007 )
3008 .into());
3009 }
3010 return Ok(member);
3011 }
3012 _ => {
3013 return Err(
3014 FormatError::InvalidArchive("unsupported revision-45 tar entry type").into(),
3015 );
3016 }
3017 }
3018
3019 if remaining == 0 {
3020 return Err(FormatError::InvalidArchive(
3021 "tar member group has metadata records but no main entry",
3022 )
3023 .into());
3024 }
3025 }
3026}
3027
3028fn plan_restore(
3029 path: &[u8],
3030 metadata: &MemberMetadata,
3031 kind: TarEntryKind,
3032 reparse_placeholder: bool,
3033 options: SafeExtractionOptions,
3034) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3035 if options.restore_policy == RestorePolicy::System && !options.system_authorized {
3036 return Err(FormatError::ReaderUnsupported(
3037 "system restore policy requires explicit caller authorization",
3038 ));
3039 }
3040
3041 let mut diagnostics = Vec::new();
3042 if metadata.declaration.capture_status == CaptureStatus::Partial {
3043 diagnostics.push(
3044 MetadataDiagnostic::new(
3045 path,
3046 "tzap-core-v1",
3047 "capture-completeness",
3048 MetadataOperation::Plan,
3049 MetadataDiagnosticStatus::Partial,
3050 "entry capture is partial; full-fidelity restoration is impossible",
3051 )
3052 .for_restore(
3053 options.restore_policy,
3054 restore_phase_for_kind(kind, reparse_placeholder),
3055 ),
3056 );
3057 if let Some(rows) = &metadata.capture_report {
3058 diagnostics.extend(rows.iter().map(|row| {
3059 let message = if row.encoded_detail.is_empty() {
3060 format!("capture omission: {}", row.reason)
3061 } else {
3062 format!(
3063 "capture omission: {}; detail={}",
3064 row.reason, row.encoded_detail
3065 )
3066 };
3067 MetadataDiagnostic::new(
3068 path,
3069 &row.profile,
3070 &row.metadata_class,
3071 MetadataOperation::Capture,
3072 MetadataDiagnosticStatus::Partial,
3073 message,
3074 )
3075 .for_restore(
3076 options.restore_policy,
3077 restore_phase_for_kind(kind, reparse_placeholder),
3078 )
3079 }));
3080 }
3081 let required_omission = metadata.capture_report.as_ref().is_some_and(|rows| {
3082 rows.iter().any(|row| {
3083 metadata
3084 .declaration
3085 .required_profiles
3086 .binary_search(&row.profile)
3087 .is_ok()
3088 })
3089 });
3090 if required_omission && !options.allow_degraded {
3091 return Err(FormatError::ReaderUnsupported(
3092 "required-profile capture omission needs explicit degraded restore",
3093 ));
3094 }
3095 }
3096 let unknown_required_profiles = metadata
3097 .declaration
3098 .unknown_required_profiles()
3099 .collect::<Vec<_>>();
3100 if !unknown_required_profiles.is_empty() {
3101 if !options.allow_degraded {
3102 return Err(FormatError::ReaderUnsupported(
3103 "requested restore policy requires an unsupported required profile",
3104 ));
3105 }
3106 diagnostics.extend(unknown_required_profiles.into_iter().map(|profile| {
3107 MetadataDiagnostic::new(
3108 path,
3109 profile,
3110 "required-profile",
3111 MetadataOperation::Plan,
3112 MetadataDiagnosticStatus::Unsupported,
3113 "unsupported required profile was preserved but not restored",
3114 )
3115 .for_restore(
3116 options.restore_policy,
3117 restore_phase_for_kind(kind, reparse_placeholder),
3118 )
3119 }));
3120 }
3121 diagnostics.extend(
3122 metadata
3123 .declaration
3124 .unknown_optional_profiles()
3125 .map(|profile| {
3126 MetadataDiagnostic::new(
3127 path,
3128 profile,
3129 "optional-profile",
3130 MetadataOperation::Plan,
3131 MetadataDiagnosticStatus::Skipped,
3132 "unsupported optional profile was preserved but not restored",
3133 )
3134 .for_restore(
3135 options.restore_policy,
3136 restore_phase_for_kind(kind, reparse_placeholder),
3137 )
3138 }),
3139 );
3140
3141 if options.restore_policy == RestorePolicy::Content {
3142 for (metadata_class, message) in [
3143 ("mode", "portable mode is outside content restore policy"),
3144 (
3145 "mtime",
3146 "modification time is outside content restore policy",
3147 ),
3148 ] {
3149 diagnostics.push(
3150 MetadataDiagnostic::new(
3151 path,
3152 "portable-v1",
3153 metadata_class,
3154 MetadataOperation::Plan,
3155 MetadataDiagnosticStatus::Skipped,
3156 message,
3157 )
3158 .for_restore(options.restore_policy, 4),
3159 );
3160 }
3161 }
3162
3163 if options.restore_policy == RestorePolicy::Content && kind == TarEntryKind::Symlink {
3164 diagnostics.push(
3165 MetadataDiagnostic::new(
3166 path,
3167 "portable-v1",
3168 "symlink",
3169 MetadataOperation::Plan,
3170 MetadataDiagnosticStatus::Skipped,
3171 "symlink skipped by content restore policy",
3172 )
3173 .for_restore(options.restore_policy, 2),
3174 );
3175 }
3176 if reparse_placeholder
3177 && !(cfg!(windows)
3178 && options.restore_policy == RestorePolicy::System
3179 && windows_reparse_metadata_supported(metadata))
3180 {
3181 diagnostics.push(
3182 MetadataDiagnostic::new(
3183 path,
3184 "windows-backup-v1",
3185 "reparse-data",
3186 MetadataOperation::Plan,
3187 MetadataDiagnosticStatus::Skipped,
3188 if options.restore_policy == RestorePolicy::System {
3189 "reparse placeholder restoration is unsupported on this host"
3190 } else {
3191 "reparse placeholder is outside the selected restore policy"
3192 },
3193 )
3194 .for_restore(options.restore_policy, 3),
3195 );
3196 }
3197 if matches!(
3198 kind,
3199 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo
3200 ) && !(cfg!(any(target_os = "linux", target_os = "macos"))
3201 && options.restore_policy == RestorePolicy::System
3202 && options.system_authorized)
3203 {
3204 diagnostics.push(
3205 MetadataDiagnostic::new(
3206 path,
3207 "posix-backup-v1",
3208 "special-object",
3209 MetadataOperation::Plan,
3210 MetadataDiagnosticStatus::Skipped,
3211 if options.restore_policy == RestorePolicy::System {
3212 "special object restoration is unsupported on this host"
3213 } else {
3214 "special object is outside the selected restore policy"
3215 },
3216 )
3217 .for_restore(options.restore_policy, 2),
3218 );
3219 }
3220 if metadata.file_entry_flags & HAS_SPARSE_EXTENTS != 0 {
3221 let native_sparse_supported = cfg!(any(windows, target_os = "linux"));
3222 if options.restore_policy != RestorePolicy::Content
3223 && !native_sparse_supported
3224 && !options.allow_degraded
3225 {
3226 return Err(FormatError::ReaderUnsupported(
3227 "sparse layout materialization needs explicit degraded restore",
3228 ));
3229 }
3230 if options.restore_policy == RestorePolicy::Content || !native_sparse_supported {
3231 diagnostics.push(
3232 MetadataDiagnostic::new(
3233 path,
3234 "portable-v1",
3235 "sparse-layout",
3236 MetadataOperation::Plan,
3237 MetadataDiagnosticStatus::Materialized,
3238 if options.restore_policy == RestorePolicy::Content {
3239 "sparse layout is outside content policy; logical bytes will be materialized"
3240 } else {
3241 "sparse layout will be materialized as logical zero bytes"
3242 },
3243 )
3244 .for_restore(options.restore_policy, 1),
3245 );
3246 }
3247 }
3248
3249 if options.restore_policy != RestorePolicy::Content
3250 && !cfg!(unix)
3251 && metadata.declaration.mode_origin_native
3252 && !matches!(metadata.declaration.portable_mode & 0o1777, 0o444 | 0o666)
3253 {
3254 if !options.allow_degraded {
3255 return Err(FormatError::ReaderUnsupported(
3256 "portable mode cannot be represented exactly on this host",
3257 ));
3258 }
3259 diagnostics.push(
3260 MetadataDiagnostic::new(
3261 path,
3262 "portable-v1",
3263 "mode",
3264 MetadataOperation::Plan,
3265 MetadataDiagnosticStatus::Partial,
3266 "portable mode can only be projected to host readonly state",
3267 )
3268 .for_restore(options.restore_policy, 4),
3269 );
3270 }
3271
3272 if metadata.declaration.owner_kind_posix && options.restore_policy != RestorePolicy::System {
3273 diagnostics.push(
3274 MetadataDiagnostic::new(
3275 path,
3276 "portable-v1",
3277 "numeric-ownership",
3278 MetadataOperation::Plan,
3279 MetadataDiagnosticStatus::Skipped,
3280 "numeric ownership is outside the selected restore policy",
3281 )
3282 .for_restore(options.restore_policy, 4),
3283 );
3284 } else if metadata.declaration.owner_kind_posix && !numeric_ownership_supported(metadata) {
3285 if !options.allow_degraded {
3286 return Err(FormatError::ReaderUnsupported(
3287 "numeric ownership cannot be represented on this host",
3288 ));
3289 }
3290 diagnostics.push(
3291 MetadataDiagnostic::new(
3292 path,
3293 "portable-v1",
3294 "numeric-ownership",
3295 MetadataOperation::Plan,
3296 MetadataDiagnosticStatus::Unsupported,
3297 "numeric ownership cannot be represented on this host",
3298 )
3299 .for_restore(options.restore_policy, 4),
3300 );
3301 }
3302 if metadata.declaration.portable_mode & 0o6000 != 0
3303 && options.restore_policy != RestorePolicy::System
3304 {
3305 diagnostics.push(
3306 MetadataDiagnostic::new(
3307 path,
3308 "portable-v1",
3309 "setid-mode",
3310 MetadataOperation::Plan,
3311 MetadataDiagnosticStatus::Skipped,
3312 "setuid/setgid mode bits are outside the selected restore policy",
3313 )
3314 .for_restore(options.restore_policy, 4),
3315 );
3316 }
3317 if let Some(attributes) = metadata.declaration.portable_attributes {
3318 let portable_bits = attributes & 0x03;
3319 let same_os_bits = attributes & 0x0c;
3320 let unsupported_requested = match options.restore_policy {
3321 RestorePolicy::Content => false,
3322 RestorePolicy::Portable => {
3323 portable_bits != 0 && (!cfg!(windows) || portable_bits & !1 != 0)
3324 }
3325 RestorePolicy::SameOs | RestorePolicy::System => {
3326 (portable_bits != 0
3327 && !(cfg!(windows) && metadata.declaration.source_os == "windows")
3328 && (!cfg!(windows) || portable_bits & !1 != 0))
3329 || (same_os_bits != 0
3330 && !(cfg!(windows) && metadata.declaration.source_os == "windows"))
3331 }
3332 };
3333 if unsupported_requested && !options.allow_degraded {
3334 return Err(FormatError::ReaderUnsupported(
3335 "requested portable attribute projection needs explicit degraded restore",
3336 ));
3337 }
3338 if options.restore_policy == RestorePolicy::Content
3339 || unsupported_requested
3340 || (options.restore_policy == RestorePolicy::Portable && same_os_bits != 0)
3341 {
3342 diagnostics.push(
3343 MetadataDiagnostic::new(
3344 path,
3345 "portable-v1",
3346 "portable-attributes",
3347 MetadataOperation::Plan,
3348 MetadataDiagnosticStatus::Skipped,
3349 "portable attribute projection was wholly or partly outside host policy capability",
3350 )
3351 .for_restore(options.restore_policy, 4),
3352 );
3353 }
3354 }
3355
3356 let requests_same_os = matches!(
3357 options.restore_policy,
3358 RestorePolicy::SameOs | RestorePolicy::System
3359 );
3360 let requests_system = options.restore_policy == RestorePolicy::System;
3361 if metadata.primary_records.contains_key("atime") && metadata.declaration.source_os != "windows"
3362 {
3363 diagnostics.push(
3364 MetadataDiagnostic::new(
3365 path,
3366 "posix-backup-v1",
3367 "atime",
3368 MetadataOperation::Plan,
3369 MetadataDiagnosticStatus::Skipped,
3370 "access time restoration was not explicitly requested",
3371 )
3372 .for_restore(options.restore_policy, 4),
3373 );
3374 }
3375 if requests_same_os && !requests_system {
3376 for key in metadata
3377 .primary_records
3378 .keys()
3379 .filter(|key| key.starts_with("LIBARCHIVE.xattr."))
3380 {
3381 let name = decode_percent_name(&key.as_bytes()["LIBARCHIVE.xattr.".len()..])?;
3382 if system_xattr_name(&name, &metadata.declaration.source_os) {
3383 diagnostics.push(
3384 MetadataDiagnostic::new(
3385 path,
3386 "linux-backup-v1",
3387 "system-extended-attribute",
3388 MetadataOperation::Plan,
3389 MetadataDiagnosticStatus::Skipped,
3390 "system-class extended attribute is outside same-os restore policy",
3391 )
3392 .for_restore(options.restore_policy, 4),
3393 );
3394 }
3395 }
3396 if metadata
3397 .primary_records
3398 .get("TZAP.linux.fsflags")
3399 .and_then(|value| std::str::from_utf8(value).ok())
3400 .and_then(|value| u64::from_str_radix(value, 16).ok())
3401 .is_some_and(|flags| flags & 0x30 != 0)
3402 {
3403 diagnostics.push(
3404 MetadataDiagnostic::new(
3405 path,
3406 "linux-backup-v1",
3407 "no-change-inode-flags",
3408 MetadataOperation::Plan,
3409 MetadataDiagnosticStatus::Skipped,
3410 "immutable/append-only inode flags are outside same-os restore policy",
3411 )
3412 .for_restore(options.restore_policy, 4),
3413 );
3414 }
3415 if metadata
3416 .primary_records
3417 .get("TZAP.macos.st-flags")
3418 .and_then(|value| parse_macos_flags(value).ok())
3419 .is_some_and(macos_flags_require_system)
3420 {
3421 diagnostics.push(
3422 MetadataDiagnostic::new(
3423 path,
3424 "macos-backup-v1",
3425 "system-file-flags",
3426 MetadataOperation::Plan,
3427 MetadataDiagnosticStatus::Skipped,
3428 "system-class macOS file flags are outside same-os restore policy",
3429 )
3430 .for_restore(options.restore_policy, 4),
3431 );
3432 }
3433 }
3434 if requests_same_os
3435 && metadata
3436 .primary_records
3437 .get("TZAP.macos.st-flags")
3438 .and_then(|value| parse_macos_flags(value).ok())
3439 .is_some_and(|flags| !macos_flags_supported(flags))
3440 {
3441 diagnostics.push(
3442 MetadataDiagnostic::new(
3443 path,
3444 "macos-backup-v1",
3445 "unrecognized-file-flags",
3446 MetadataOperation::Plan,
3447 MetadataDiagnosticStatus::Skipped,
3448 "unrecognized macOS file flags were preserved but will not be applied",
3449 )
3450 .for_restore(options.restore_policy, 4),
3451 );
3452 }
3453 let profile_is_required = |profile: &str| {
3454 metadata
3455 .declaration
3456 .required_profiles
3457 .binary_search_by(|candidate| candidate.as_str().cmp(profile))
3458 .is_ok()
3459 };
3460 let native_profile = metadata
3461 .auxiliary
3462 .iter()
3463 .find(|record| record.native || record.restore_class >= RestoreClass::SameOs)
3464 .map(|record| record.profile.as_str())
3465 .or_else(|| {
3466 metadata
3467 .declaration
3468 .required_profiles
3469 .iter()
3470 .chain(&metadata.declaration.optional_profiles)
3471 .find(|profile| profile.as_str() != "portable-v1")
3472 .map(String::as_str)
3473 })
3474 .unwrap_or("portable-v1");
3475 let required_native_scalar = metadata.primary_has_native_scalar
3476 && metadata
3477 .declaration
3478 .required_profiles
3479 .iter()
3480 .any(|profile| profile != "portable-v1");
3481 let required_native_profile = metadata
3482 .declaration
3483 .required_profiles
3484 .iter()
3485 .any(|profile| profile != "portable-v1");
3486 let native_source_matches_host =
3487 source_os_matches_current_host(&metadata.declaration.source_os);
3488 let unsupported_primary_same_os = native_primary_restore_unsupported(metadata, false);
3489 let unsupported_primary_system = native_primary_restore_unsupported(metadata, true);
3490 let unsupported_same_os = metadata.auxiliary.iter().any(|record| {
3491 record.restore_class == RestoreClass::SameOs
3492 && profile_is_required(&record.profile)
3493 && !native_auxiliary_restore_supported(record, false, Some(kind))
3494 }) || (required_native_scalar && unsupported_primary_same_os)
3495 || (required_native_profile && !native_source_matches_host);
3496 let unsupported_system = metadata.auxiliary.iter().any(|record| {
3497 record.restore_class == RestoreClass::System
3498 && profile_is_required(&record.profile)
3499 && !native_auxiliary_restore_supported(record, true, Some(kind))
3500 }) || (metadata.declaration.owner_kind_posix
3501 && !numeric_ownership_supported(metadata))
3502 || (metadata.declaration.portable_mode & 0o6000 != 0 && !cfg!(unix))
3503 || (required_native_scalar && unsupported_primary_system)
3504 || (reparse_placeholder && !windows_reparse_metadata_supported(metadata))
3505 || (matches!(
3506 kind,
3507 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo
3508 ) && !special_object_restore_supported(kind))
3509 || (required_native_profile && !native_source_matches_host);
3510
3511 if (!requests_system && requests_same_os && unsupported_same_os)
3512 || (requests_system && unsupported_system)
3513 {
3514 if !options.allow_degraded {
3515 return Err(FormatError::ReaderUnsupported(
3516 "requested native metadata is not supported by this conformance class",
3517 ));
3518 }
3519 diagnostics.push(
3520 MetadataDiagnostic::new(
3521 path,
3522 native_profile,
3523 "native-metadata",
3524 MetadataOperation::Plan,
3525 MetadataDiagnosticStatus::Skipped,
3526 "requested native metadata was skipped under explicit degraded restore",
3527 )
3528 .for_restore(
3529 options.restore_policy,
3530 restore_phase_for_kind(kind, reparse_placeholder),
3531 ),
3532 );
3533 }
3534
3535 if metadata.file_entry_flags & HAS_NATIVE_METADATA != 0 && !requests_same_os {
3536 diagnostics.push(
3537 MetadataDiagnostic::new(
3538 path,
3539 native_profile,
3540 "native-metadata",
3541 MetadataOperation::Plan,
3542 MetadataDiagnosticStatus::Skipped,
3543 "authenticated native metadata is outside the selected restore policy",
3544 )
3545 .for_restore(
3546 options.restore_policy,
3547 restore_phase_for_kind(kind, reparse_placeholder),
3548 ),
3549 );
3550 }
3551 if requests_same_os
3552 && metadata.primary_has_native_scalar
3553 && !required_native_scalar
3554 && (native_primary_restore_unsupported(metadata, requests_system)
3555 || !native_source_matches_host)
3556 {
3557 diagnostics.push(
3558 MetadataDiagnostic::new(
3559 path,
3560 native_profile,
3561 "optional-native-scalar",
3562 MetadataOperation::Plan,
3563 MetadataDiagnosticStatus::Skipped,
3564 "optional native scalar metadata is unsupported on this host",
3565 )
3566 .for_restore(
3567 options.restore_policy,
3568 restore_phase_for_kind(kind, reparse_placeholder),
3569 ),
3570 );
3571 }
3572 for record in &metadata.auxiliary {
3573 let requested = match options.restore_policy {
3574 RestorePolicy::Content => record.restore_class == RestoreClass::None,
3575 RestorePolicy::Portable => record.restore_class <= RestoreClass::Portable,
3576 RestorePolicy::SameOs => record.restore_class <= RestoreClass::SameOs,
3577 RestorePolicy::System => true,
3578 };
3579 if requested
3580 && record.restore_class != RestoreClass::None
3581 && !profile_is_required(&record.profile)
3582 {
3583 diagnostics.push(
3584 MetadataDiagnostic::new(
3585 path,
3586 &record.profile,
3587 &record.kind,
3588 MetadataOperation::Plan,
3589 MetadataDiagnosticStatus::Skipped,
3590 "optional auxiliary record is unsupported on this host",
3591 )
3592 .for_restore(
3593 options.restore_policy,
3594 restore_phase_for_kind(kind, reparse_placeholder),
3595 ),
3596 );
3597 } else if !requested && record.restore_class != RestoreClass::None {
3598 diagnostics.push(
3599 MetadataDiagnostic::new(
3600 path,
3601 &record.profile,
3602 &record.kind,
3603 MetadataOperation::Plan,
3604 MetadataDiagnosticStatus::Skipped,
3605 "authenticated auxiliary record is outside the selected restore policy",
3606 )
3607 .for_restore(
3608 options.restore_policy,
3609 restore_phase_for_kind(kind, reparse_placeholder),
3610 ),
3611 );
3612 }
3613 }
3614 Ok(diagnostics)
3615}
3616
3617fn native_auxiliary_restore_supported(
3618 record: &AuxiliaryRecord,
3619 include_system: bool,
3620 kind: Option<TarEntryKind>,
3621) -> bool {
3622 if cfg!(target_os = "macos") {
3623 return match record.kind.as_str() {
3624 "macos.resource-fork" => {
3625 record.restore_class == RestoreClass::SameOs
3626 && match kind {
3627 Some(TarEntryKind::Symlink) => record.logical_size <= u64::from(u32::MAX),
3628 Some(TarEntryKind::Regular | TarEntryKind::Directory) | None => true,
3629 Some(_) => false,
3630 }
3631 }
3632 "macos.finder-info" => record.restore_class == RestoreClass::SameOs,
3633 "macos.acl-native" => {
3634 record.restore_class == RestoreClass::SameOs
3635 && record
3636 .meta
3637 .get("TZAP.aux.meta.acl-format")
3638 .is_some_and(|value| value == b"darwin-acl-external-v1")
3639 }
3640 "generic.xattr" => {
3641 record.restore_class == RestoreClass::SameOs
3642 || include_system && record.restore_class == RestoreClass::System
3643 }
3644 _ => false,
3645 };
3646 }
3647 if cfg!(target_os = "linux") && record.kind == "generic.xattr" {
3648 return record.restore_class == RestoreClass::SameOs
3649 || (include_system && record.restore_class == RestoreClass::System);
3650 }
3651 if !cfg!(windows) {
3652 return false;
3653 }
3654 if record.kind == "windows.alternate-data" {
3655 return record.restore_class == RestoreClass::SameOs
3656 && record
3657 .meta
3658 .get("TZAP.aux.meta.stream-attributes")
3659 .is_some_and(|value| {
3660 value == b"00000000" && record.flags == 0
3661 || value == b"00000008" && record.flags == 1
3662 });
3663 }
3664 if matches!(
3665 record.kind.as_str(),
3666 "windows.ea-data" | "windows.property-data" | "windows.object-id"
3667 ) {
3668 let expected_type = match record.kind.as_str() {
3669 "windows.ea-data" => b"00000002".as_slice(),
3670 "windows.property-data" => b"00000006".as_slice(),
3671 "windows.object-id" => b"00000007".as_slice(),
3672 _ => unreachable!(),
3673 };
3674 return (record.restore_class == RestoreClass::SameOs
3675 || include_system && record.restore_class == RestoreClass::System)
3676 && (record.restore_class != RestoreClass::System
3677 || windows_security_restore_privileges_available(0))
3678 && record.flags == 0
3679 && record.name_encoding == "none"
3680 && record.decoded_name.is_empty()
3681 && record
3682 .meta
3683 .get("TZAP.aux.meta.stream-type")
3684 .is_some_and(|value| value == expected_type)
3685 && record
3686 .meta
3687 .get("TZAP.aux.meta.stream-attributes")
3688 .and_then(|value| parse_lower_hex_u32(value, "Windows stream attributes").ok())
3689 .is_some_and(|attributes| {
3690 attributes & !(STREAM_MODIFIED_WHEN_READ | STREAM_CONTAINS_SECURITY) == 0
3691 && (record.kind == "windows.object-id"
3692 || attributes & STREAM_CONTAINS_SECURITY != 0)
3693 == (record.restore_class == RestoreClass::System)
3694 });
3695 }
3696 if !include_system {
3697 return false;
3698 }
3699 if record.kind == "windows.efs-raw" {
3700 return record.restore_class == RestoreClass::System
3701 && record
3702 .meta
3703 .get("TZAP.aux.meta.efs-version")
3704 .is_some_and(|value| value == b"1");
3705 }
3706 if record.kind == "windows.reparse-data" {
3707 return record
3708 .capture_report_payload
3709 .as_deref()
3710 .is_some_and(|payload| validate_windows_essential_reparse_data(payload).is_ok());
3711 }
3712 if record.kind == "windows.security-descriptor" {
3713 return record.capture_report_payload.is_some()
3714 && record
3715 .meta
3716 .get("TZAP.aux.meta.security-information")
3717 .and_then(|value| parse_lower_hex_u32(value, "Windows security information").ok())
3718 .is_some_and(windows_security_restore_privileges_available);
3719 }
3720 false
3721}
3722
3723#[cfg(windows)]
3724fn windows_security_restore_privileges_available(security_information: u32) -> bool {
3725 use std::ptr;
3726 use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, SetLastError, ERROR_SUCCESS};
3727 use windows_sys::Win32::Security::{
3728 AdjustTokenPrivileges, LookupPrivilegeValueW, SE_PRIVILEGE_ENABLED, SE_RESTORE_NAME,
3729 SE_SECURITY_NAME, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES, TOKEN_QUERY,
3730 };
3731 use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
3732
3733 let mut token = ptr::null_mut();
3734 if unsafe {
3736 OpenProcessToken(
3737 GetCurrentProcess(),
3738 TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES,
3739 &mut token,
3740 )
3741 } == 0
3742 {
3743 return false;
3744 }
3745 let enable = |name| {
3746 let mut privileges = TOKEN_PRIVILEGES {
3747 PrivilegeCount: 1,
3748 ..Default::default()
3749 };
3750 if unsafe { LookupPrivilegeValueW(ptr::null(), name, &mut privileges.Privileges[0].Luid) }
3752 == 0
3753 {
3754 return false;
3755 }
3756 privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
3757 unsafe { SetLastError(ERROR_SUCCESS) };
3758 unsafe {
3760 AdjustTokenPrivileges(token, 0, &privileges, 0, ptr::null_mut(), ptr::null_mut()) != 0
3761 && GetLastError() == ERROR_SUCCESS
3762 }
3763 };
3764 let available = enable(SE_RESTORE_NAME)
3765 && (security_information & 0x0000_0008 == 0 || enable(SE_SECURITY_NAME));
3766 unsafe { CloseHandle(token) };
3768 available
3769}
3770
3771#[cfg(not(windows))]
3772fn windows_security_restore_privileges_available(_security_information: u32) -> bool {
3773 false
3774}
3775
3776fn windows_reparse_metadata_supported(metadata: &MemberMetadata) -> bool {
3777 metadata.declaration.source_os == "windows"
3778 && metadata
3779 .auxiliary
3780 .iter()
3781 .find(|record| record.kind == "windows.reparse-data")
3782 .is_some_and(|record| native_auxiliary_restore_supported(record, true, None))
3783}
3784
3785fn native_primary_restore_unsupported(metadata: &MemberMetadata, include_system: bool) -> bool {
3786 metadata.primary_records.keys().any(|key| {
3787 let native = key.starts_with("TZAP.linux.")
3788 || key.starts_with("TZAP.macos.")
3789 || key.starts_with("TZAP.windows.")
3790 || key.starts_with("TZAP.posix.")
3791 || key.starts_with("LIBARCHIVE.")
3792 || key.starts_with("SCHILY.")
3793 || key == "TZAP.unix.ctime-observed";
3794 if !native {
3795 return false;
3796 }
3797 if key == "TZAP.unix.ctime-observed" {
3798 return false;
3799 }
3800 if key == "TZAP.linux.fsflags" {
3801 return linux_inode_flags_restore_unsupported(
3802 metadata.primary_records.get(key).map(Vec::as_slice),
3803 );
3804 }
3805 if key == "TZAP.linux.project-id" {
3806 return !cfg!(target_os = "linux") || !include_system;
3807 }
3808 if key == "TZAP.linux.whiteout" {
3809 return !cfg!(target_os = "linux") || !include_system;
3810 }
3811 if key.starts_with("TZAP.posix.device-") {
3812 return !cfg!(any(target_os = "linux", target_os = "macos")) || !include_system;
3813 }
3814 if key == "TZAP.windows.file-attributes" {
3815 if !cfg!(windows) || metadata.declaration.source_os != "windows" {
3816 return true;
3817 }
3818 return metadata
3819 .primary_records
3820 .get(key)
3821 .and_then(|value| parse_lower_hex_u32(value, "Windows file attributes").ok())
3822 .is_none_or(|attributes| {
3823 attributes
3824 & !(WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES
3825 | WINDOWS_ESSENTIAL_INTRINSIC_ATTRIBUTES
3826 | FILE_ATTRIBUTE_NORMAL)
3827 != 0
3828 });
3829 }
3830 if key == "TZAP.windows.change-time" {
3831 return !cfg!(windows) || metadata.declaration.source_os != "windows";
3832 }
3833 if key == "TZAP.windows.data-stream-attributes" {
3834 return !cfg!(windows)
3835 || metadata.declaration.source_os != "windows"
3836 || metadata
3837 .primary_records
3838 .get(key)
3839 .is_none_or(|value| value != b"00000000" && value != b"00000008");
3840 }
3841 if key == "TZAP.windows.reparse-placeholder" {
3842 return !cfg!(windows)
3843 || !include_system
3844 || !windows_reparse_metadata_supported(metadata);
3845 }
3846 if key == "TZAP.windows.directory-case-sensitive" {
3847 return include_system
3848 && (!cfg!(windows) || metadata.declaration.source_os != "windows");
3849 }
3850 if key == "LIBARCHIVE.creationtime" && metadata.declaration.source_os == "windows" {
3851 return !cfg!(windows);
3852 }
3853 if key == "LIBARCHIVE.creationtime" && metadata.declaration.source_os == "macos" {
3854 return !cfg!(target_os = "macos");
3855 }
3856 if key == "TZAP.macos.st-flags" {
3857 let flags = metadata
3858 .primary_records
3859 .get(key)
3860 .and_then(|value| parse_macos_flags(value).ok());
3861 return !cfg!(target_os = "macos")
3862 || metadata.declaration.source_os != "macos"
3863 || flags.is_none_or(|flags| {
3864 if macos_flags_require_system(flags) && !include_system {
3865 false
3866 } else {
3867 !macos_flags_supported(flags)
3868 || include_system && !macos_system_flags_privileges_available(flags)
3869 }
3870 });
3871 }
3872 if key.starts_with("SCHILY.acl.") || key.starts_with("TZAP.acl.") {
3873 return !cfg!(target_os = "linux");
3874 }
3875 if let Some(encoded_name) = key.strip_prefix("LIBARCHIVE.xattr.") {
3876 let system = decode_percent_name(encoded_name.as_bytes())
3877 .ok()
3878 .is_some_and(|name| system_xattr_name(&name, &metadata.declaration.source_os));
3879 return !cfg!(unix) && (!system || include_system);
3880 }
3881 true
3882 })
3883}
3884
3885#[cfg(target_os = "linux")]
3886fn linux_inode_flags_restore_unsupported(encoded: Option<&[u8]>) -> bool {
3887 encoded
3888 .and_then(|value| std::str::from_utf8(value).ok())
3889 .and_then(|value| u64::from_str_radix(value, 16).ok())
3890 .is_none_or(|flags| flags & !LINUX_KNOWN_FSFLAGS != 0)
3891}
3892
3893#[cfg(not(target_os = "linux"))]
3894fn linux_inode_flags_restore_unsupported(_encoded: Option<&[u8]>) -> bool {
3895 true
3896}
3897
3898fn source_os_matches_current_host(source_os: &str) -> bool {
3899 source_os == current_host_os()
3900}
3901
3902#[cfg(target_os = "linux")]
3903fn current_host_os() -> &'static str {
3904 "linux"
3905}
3906
3907#[cfg(target_os = "macos")]
3908fn current_host_os() -> &'static str {
3909 "macos"
3910}
3911
3912#[cfg(target_os = "windows")]
3913fn current_host_os() -> &'static str {
3914 "windows"
3915}
3916
3917#[cfg(target_os = "freebsd")]
3918fn current_host_os() -> &'static str {
3919 "freebsd"
3920}
3921
3922#[cfg(target_os = "netbsd")]
3923fn current_host_os() -> &'static str {
3924 "netbsd"
3925}
3926
3927#[cfg(target_os = "openbsd")]
3928fn current_host_os() -> &'static str {
3929 "openbsd"
3930}
3931
3932#[cfg(target_os = "solaris")]
3933fn current_host_os() -> &'static str {
3934 "solaris"
3935}
3936
3937#[cfg(all(
3938 unix,
3939 not(any(
3940 target_os = "linux",
3941 target_os = "macos",
3942 target_os = "freebsd",
3943 target_os = "netbsd",
3944 target_os = "openbsd",
3945 target_os = "solaris"
3946 ))
3947))]
3948fn current_host_os() -> &'static str {
3949 "other-unix"
3950}
3951
3952#[cfg(not(any(unix, windows)))]
3953fn current_host_os() -> &'static str {
3954 "other"
3955}
3956
3957#[cfg(unix)]
3958fn numeric_ownership_supported(metadata: &MemberMetadata) -> bool {
3959 metadata
3960 .portable_mirror
3961 .uid
3962 .and_then(|uid| libc::uid_t::try_from(uid).ok())
3963 .is_some()
3964 && metadata
3965 .portable_mirror
3966 .gid
3967 .and_then(|gid| libc::gid_t::try_from(gid).ok())
3968 .is_some()
3969}
3970
3971#[cfg(not(unix))]
3972fn numeric_ownership_supported(_metadata: &MemberMetadata) -> bool {
3973 false
3974}
3975
3976pub(crate) fn metadata_verification_report(
3977 members: &[TarStreamMemberSummary],
3978) -> Result<MetadataVerificationReport, FormatError> {
3979 let mut profiles_present = std::collections::BTreeSet::new();
3980 let mut auxiliary_kinds_present = std::collections::BTreeSet::new();
3981 let mut entries = Vec::with_capacity(members.len());
3982
3983 for member in members {
3984 let metadata = &member.v45_metadata;
3985 profiles_present.extend(metadata.declaration.required_profiles.iter().cloned());
3986 profiles_present.extend(metadata.declaration.optional_profiles.iter().cloned());
3987 let mut auxiliary_kinds = metadata
3988 .auxiliary
3989 .iter()
3990 .map(|record| record.kind.clone())
3991 .collect::<Vec<_>>();
3992 auxiliary_kinds.sort();
3993 auxiliary_kinds.dedup();
3994 auxiliary_kinds_present.extend(auxiliary_kinds.iter().cloned());
3995
3996 let mut policy_capabilities = Vec::with_capacity(4);
3997 for policy in [
3998 RestorePolicy::Content,
3999 RestorePolicy::Portable,
4000 RestorePolicy::SameOs,
4001 RestorePolicy::System,
4002 ] {
4003 let strict = SafeExtractionOptions {
4004 restore_policy: policy,
4005 allow_degraded: false,
4006 system_authorized: policy == RestorePolicy::System,
4007 ..SafeExtractionOptions::default()
4008 };
4009 let (policy_complete, reason) = match plan_restore(
4010 &member.path,
4011 metadata,
4012 member.kind,
4013 member.reparse_placeholder,
4014 strict,
4015 ) {
4016 Ok(_) => (true, None),
4017 Err(FormatError::ReaderUnsupported(reason)) => (false, Some(reason)),
4018 Err(error) => return Err(error),
4019 };
4020 let degraded_restore_available = if policy_complete {
4021 true
4022 } else {
4023 plan_restore(
4024 &member.path,
4025 metadata,
4026 member.kind,
4027 member.reparse_placeholder,
4028 SafeExtractionOptions {
4029 allow_degraded: true,
4030 ..strict
4031 },
4032 )
4033 .is_ok()
4034 };
4035 policy_capabilities.push(RestorePolicyCapability {
4036 policy,
4037 policy_complete,
4038 degraded_restore_available,
4039 reason,
4040 });
4041 }
4042
4043 let mut diagnostics = member.diagnostics.clone();
4044 diagnostics.extend(plan_restore(
4045 &member.path,
4046 metadata,
4047 member.kind,
4048 member.reparse_placeholder,
4049 SafeExtractionOptions {
4050 allow_degraded: true,
4051 ..SafeExtractionOptions::default()
4052 },
4053 )?);
4054 let system_complete = policy_capabilities
4055 .iter()
4056 .find(|capability| capability.policy == RestorePolicy::System)
4057 .is_some_and(|capability| capability.policy_complete);
4058 let full_fidelity_possible = metadata.declaration.capture_status == CaptureStatus::Complete
4059 && system_complete
4060 && !diagnostics.iter().any(|diagnostic| {
4061 matches!(
4062 diagnostic.status,
4063 MetadataDiagnosticStatus::Materialized
4064 | MetadataDiagnosticStatus::Unsupported
4065 | MetadataDiagnosticStatus::Failed
4066 )
4067 });
4068 entries.push(EntryMetadataVerification {
4069 path: member.path.clone(),
4070 capture_status: metadata.declaration.capture_status,
4071 required_profiles: metadata.declaration.required_profiles.clone(),
4072 optional_profiles: metadata.declaration.optional_profiles.clone(),
4073 auxiliary_kinds,
4074 policy_capabilities,
4075 full_fidelity_possible,
4076 diagnostics,
4077 });
4078 }
4079
4080 Ok(MetadataVerificationReport {
4081 all_capture_complete: entries
4082 .iter()
4083 .all(|entry| entry.capture_status == CaptureStatus::Complete),
4084 full_fidelity_possible: entries.iter().all(|entry| entry.full_fidelity_possible),
4085 profiles_present: profiles_present.into_iter().collect(),
4086 auxiliary_kinds_present: auxiliary_kinds_present.into_iter().collect(),
4087 entries,
4088 })
4089}
4090
4091struct RegularWriterHandler<'a, W> {
4092 writer: &'a mut W,
4093}
4094
4095impl<W: Write> TarMemberStreamHandler for RegularWriterHandler<'_, W> {
4096 fn on_member(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), ExtractError> {
4097 if member.kind != TarEntryKind::Regular || member.reparse_placeholder {
4098 return Err(FormatError::ReaderUnsupported(
4099 "extract_file_to_writer returns only regular file payloads",
4100 )
4101 .into());
4102 }
4103 Ok(())
4104 }
4105
4106 fn write_regular_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError> {
4107 self.writer.write_all(bytes).map_err(ExtractError::Output)
4108 }
4109}
4110
4111struct FilesystemRestoreHandler<'a> {
4112 root: &'a Path,
4113 options: SafeExtractionOptions,
4114 destination: Option<PreparedDestination>,
4115 temp_leaf: Option<PathBuf>,
4116 file: Option<fs::File>,
4117 skipped_reparse_placeholder: bool,
4118 skipped_by_policy: bool,
4119 materialized_hardlink: bool,
4120 native_sparse_active: bool,
4121 sparse_logical_size: u64,
4122 sparse_extents: Vec<SparseExtent>,
4123 planned_diagnostics: Vec<MetadataDiagnostic>,
4124 defer_hardlinks: bool,
4125 deferred_hardlinks: Vec<(Vec<u8>, Vec<u8>)>,
4126 defer_directories: bool,
4127 deferred_directories: Vec<(Vec<u8>, MemberMetadata, Vec<StagedAuxiliary>)>,
4128 active_auxiliary: Option<StagedAuxiliary>,
4129 staged_auxiliary: Vec<StagedAuxiliary>,
4130}
4131
4132struct StagedAuxiliary {
4133 record: AuxiliaryRecord,
4134 file: fs::File,
4135}
4136
4137impl<'a> FilesystemRestoreHandler<'a> {
4138 fn new(root: &'a Path, options: SafeExtractionOptions) -> Self {
4139 Self {
4140 root,
4141 options,
4142 destination: None,
4143 temp_leaf: None,
4144 file: None,
4145 skipped_reparse_placeholder: false,
4146 skipped_by_policy: false,
4147 materialized_hardlink: false,
4148 native_sparse_active: false,
4149 sparse_logical_size: 0,
4150 sparse_extents: Vec::new(),
4151 planned_diagnostics: Vec::new(),
4152 defer_hardlinks: false,
4153 deferred_hardlinks: Vec::new(),
4154 defer_directories: false,
4155 deferred_directories: Vec::new(),
4156 active_auxiliary: None,
4157 staged_auxiliary: Vec::new(),
4158 }
4159 }
4160
4161 fn new_deferred(root: &'a Path, options: SafeExtractionOptions) -> Self {
4162 let mut handler = Self::new(root, options);
4163 handler.defer_hardlinks = true;
4164 handler.defer_directories = true;
4165 handler
4166 }
4167
4168 fn finish_archive(&mut self) -> Result<Vec<MetadataDiagnostic>, FormatError> {
4169 if self.active_auxiliary.is_some() || !self.staged_auxiliary.is_empty() {
4170 return Err(FormatError::InvalidArchive(
4171 "native auxiliary payload was not attached to an archive member",
4172 ));
4173 }
4174 let mut diagnostics = Vec::new();
4175 for (path, target) in std::mem::take(&mut self.deferred_hardlinks) {
4176 let destination =
4177 prepare_destination(self.root, &path, TarEntryKind::Hardlink, self.options)?;
4178 let target_path = existing_safe_regular_path(self.root, &target)?;
4179 if self.options.restore_policy == RestorePolicy::Content {
4180 let (temp_leaf, mut output) = create_temp_regular_file(&destination)?;
4181 let mut input = open_existing_regular_file(&target_path)?;
4182 if std::io::copy(&mut input, &mut output).is_err() {
4183 let _ = destination.parent.remove_file_or_symlink(&temp_leaf);
4184 return Err(FormatError::FilesystemExtractionFailed(
4185 "failed to materialize hardlink target",
4186 ));
4187 }
4188 output.flush().map_err(|_| {
4189 FormatError::FilesystemExtractionFailed(
4190 "failed to write materialized hardlink target",
4191 )
4192 })?;
4193 publish_regular_file(&destination, &temp_leaf, output, self.options)?;
4194 } else {
4195 create_hardlink(&destination, &target_path, self.options)?;
4196 }
4197 }
4198 let mut directories = std::mem::take(&mut self.deferred_directories);
4199 directories.sort_by(|left, right| {
4200 right
4201 .0
4202 .iter()
4203 .filter(|byte| **byte == b'/')
4204 .count()
4205 .cmp(&left.0.iter().filter(|byte| **byte == b'/').count())
4206 .then_with(|| left.0.cmp(&right.0))
4207 });
4208 if self.options.restore_policy != RestorePolicy::Content {
4209 for (path, metadata, mut staged) in directories {
4210 apply_restored_directory_metadata(
4211 self.root,
4212 &path,
4213 &metadata,
4214 Some(&mut staged),
4215 self.options,
4216 &mut diagnostics,
4217 )?;
4218 if !staged.is_empty() {
4219 return Err(FormatError::InvalidArchive(
4220 "native auxiliary payload was not restored for its directory member",
4221 ));
4222 }
4223 }
4224 }
4225 Ok(diagnostics)
4226 }
4227
4228 fn finish(
4229 &mut self,
4230 member: &StreamedTarMemberMetadata,
4231 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
4232 let mut diagnostics = member.diagnostics.clone();
4233 for diagnostic in &mut diagnostics {
4234 if diagnostic.operation == MetadataOperation::Restore
4235 && diagnostic.restore_policy.is_none()
4236 {
4237 diagnostic.restore_policy = Some(self.options.restore_policy);
4238 diagnostic.restore_phase = Some(restore_phase_for_kind(
4239 member.kind,
4240 member.reparse_placeholder,
4241 ));
4242 }
4243 }
4244 diagnostics.append(&mut self.planned_diagnostics);
4245 if self.skipped_reparse_placeholder || self.skipped_by_policy {
4246 self.staged_auxiliary.clear();
4247 return Ok(diagnostics);
4248 }
4249 if !matches!(member.kind, TarEntryKind::Regular | TarEntryKind::Directory)
4250 && !self.staged_auxiliary.is_empty()
4251 {
4252 return Err(FormatError::InvalidArchive(
4253 "native auxiliary payload was not restored for its archive member",
4254 )
4255 .into());
4256 }
4257 if member.reparse_placeholder {
4258 return Ok(diagnostics);
4259 }
4260 if member.kind == TarEntryKind::Directory {
4261 if !self.defer_directories && self.options.restore_policy != RestorePolicy::Content {
4262 apply_restored_directory_metadata(
4263 self.root,
4264 &member.path,
4265 &member.v45_metadata,
4266 Some(&mut self.staged_auxiliary),
4267 self.options,
4268 &mut diagnostics,
4269 )?;
4270 if !self.staged_auxiliary.is_empty() {
4271 return Err(FormatError::InvalidArchive(
4272 "native auxiliary payload was not restored for its directory member",
4273 )
4274 .into());
4275 }
4276 }
4277 return Ok(diagnostics);
4278 }
4279 if member.kind != TarEntryKind::Regular && !self.materialized_hardlink {
4280 return Ok(diagnostics);
4281 }
4282
4283 let mut file = self.file.take().ok_or(FormatError::InvalidArchive(
4284 "regular file output is missing",
4285 ))?;
4286 file.flush()
4287 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to write regular file"))?;
4288
4289 let destination = self.destination.take().ok_or(FormatError::InvalidArchive(
4290 "regular file destination is missing",
4291 ))?;
4292 let temp_leaf = self.temp_leaf.take().ok_or(FormatError::InvalidArchive(
4293 "regular file temp path is missing",
4294 ))?;
4295 let file = match restore_windows_efs_temp(
4296 &destination,
4297 &temp_leaf,
4298 file,
4299 &mut self.staged_auxiliary,
4300 self.options,
4301 ) {
4302 Ok(file) => file,
4303 Err(error) => {
4304 let _ = destination.parent.remove_file_or_symlink(&temp_leaf);
4305 return Err(error.into());
4306 }
4307 };
4308 let file = publish_regular_file(&destination, &temp_leaf, file, self.options)?;
4309 if self.options.restore_policy != RestorePolicy::Content {
4310 if let Err(error) = apply_windows_alternate_streams(
4311 &file,
4312 &member.path,
4313 &mut self.staged_auxiliary,
4314 self.options,
4315 &mut diagnostics,
4316 ) {
4317 drop(file);
4318 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
4319 return Err(error.into());
4320 }
4321 if let Err(error) = apply_restored_regular_file_metadata_parts(
4322 &file,
4323 &member.path,
4324 RestoredRegularMetadata::from(&member.v45_metadata.portable_mirror),
4325 Some(&member.v45_metadata),
4326 Some(&mut self.staged_auxiliary),
4327 self.options,
4328 &mut diagnostics,
4329 ) {
4330 drop(file);
4331 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
4332 return Err(error.into());
4333 }
4334 if !self.staged_auxiliary.is_empty() {
4335 drop(file);
4336 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
4337 return Err(FormatError::InvalidArchive(
4338 "native auxiliary payload was not restored for its regular-file member",
4339 )
4340 .into());
4341 }
4342 }
4343 Ok(diagnostics)
4344 }
4345}
4346
4347impl Drop for FilesystemRestoreHandler<'_> {
4348 fn drop(&mut self) {
4349 if let (Some(destination), Some(temp_leaf)) =
4350 (self.destination.as_ref(), self.temp_leaf.take())
4351 {
4352 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
4353 }
4354 }
4355}
4356
4357impl TarMemberStreamHandler for FilesystemRestoreHandler<'_> {
4358 fn begin_auxiliary_payload(&mut self, record: &AuxiliaryRecord) -> Result<bool, ExtractError> {
4359 if self.active_auxiliary.is_some() {
4360 return Err(FormatError::InvalidArchive(
4361 "previous auxiliary payload was not finalized",
4362 )
4363 .into());
4364 }
4365 let requested = match self.options.restore_policy {
4366 RestorePolicy::Content | RestorePolicy::Portable => false,
4367 RestorePolicy::SameOs => record.restore_class <= RestoreClass::SameOs,
4368 RestorePolicy::System => true,
4369 };
4370 if !requested
4371 || !native_auxiliary_restore_supported(
4372 record,
4373 self.options.restore_policy == RestorePolicy::System,
4374 None,
4375 )
4376 || !matches!(
4377 record.kind.as_str(),
4378 "windows.alternate-data"
4379 | "windows.ea-data"
4380 | "windows.property-data"
4381 | "windows.object-id"
4382 | "windows.efs-raw"
4383 | "macos.resource-fork"
4384 | "macos.finder-info"
4385 | "macos.acl-native"
4386 | "generic.xattr"
4387 )
4388 {
4389 return Ok(false);
4390 }
4391 let file = tempfile::tempfile().map_err(|_| {
4392 FormatError::FilesystemExtractionFailed("failed to stage native auxiliary payload")
4393 })?;
4394 self.active_auxiliary = Some(StagedAuxiliary {
4395 record: record.clone(),
4396 file,
4397 });
4398 Ok(true)
4399 }
4400
4401 fn write_auxiliary_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError> {
4402 self.active_auxiliary
4403 .as_mut()
4404 .ok_or(FormatError::InvalidArchive(
4405 "auxiliary staging output is missing",
4406 ))?
4407 .file
4408 .write_all(bytes)
4409 .map_err(|_| {
4410 FormatError::FilesystemExtractionFailed("failed to stage native auxiliary payload")
4411 .into()
4412 })
4413 }
4414
4415 fn finish_auxiliary_payload(&mut self, record: &AuxiliaryRecord) -> Result<(), ExtractError> {
4416 let mut staged = self
4417 .active_auxiliary
4418 .take()
4419 .ok_or(FormatError::InvalidArchive(
4420 "auxiliary staging output is missing",
4421 ))?;
4422 if staged.record.ordinal != record.ordinal || staged.record.kind != record.kind {
4423 return Err(FormatError::InvalidArchive(
4424 "staged auxiliary declaration changed during validation",
4425 )
4426 .into());
4427 }
4428 staged.file.flush().map_err(|_| {
4429 FormatError::FilesystemExtractionFailed("failed to flush native auxiliary staging")
4430 })?;
4431 staged.file.seek(SeekFrom::Start(0)).map_err(|_| {
4432 FormatError::FilesystemExtractionFailed("failed to rewind native auxiliary staging")
4433 })?;
4434 staged.record = record.clone();
4435 self.staged_auxiliary.push(staged);
4436 Ok(())
4437 }
4438
4439 fn on_member(&mut self, member: &StreamedTarMemberMetadata) -> Result<(), ExtractError> {
4440 if self.destination.is_some()
4441 || self.temp_leaf.is_some()
4442 || self.file.is_some()
4443 || self.active_auxiliary.is_some()
4444 {
4445 return Err(FormatError::InvalidArchive(
4446 "previous streamed restore member was not finalized",
4447 )
4448 .into());
4449 }
4450 self.skipped_reparse_placeholder = false;
4451 self.skipped_by_policy = false;
4452 self.materialized_hardlink = false;
4453 self.native_sparse_active = false;
4454 self.sparse_logical_size = 0;
4455 self.sparse_extents.clear();
4456 self.planned_diagnostics.clear();
4457 self.planned_diagnostics = plan_restore(
4458 &member.path,
4459 &member.v45_metadata,
4460 member.kind,
4461 member.reparse_placeholder,
4462 self.options,
4463 )?;
4464 self.staged_auxiliary.retain(|item| {
4465 native_auxiliary_restore_supported(
4466 &item.record,
4467 self.options.restore_policy == RestorePolicy::System,
4468 Some(member.kind),
4469 )
4470 });
4471 let restore_exact_windows_reparse = cfg!(windows)
4472 && self.options.restore_policy == RestorePolicy::System
4473 && self.options.system_authorized
4474 && windows_reparse_metadata_supported(&member.v45_metadata);
4475 if member.reparse_placeholder && !restore_exact_windows_reparse {
4476 self.skipped_reparse_placeholder = true;
4477 return Ok(());
4478 }
4479 if member.kind == TarEntryKind::Symlink
4480 && self.options.restore_policy == RestorePolicy::Content
4481 {
4482 self.skipped_by_policy = true;
4483 return Ok(());
4484 }
4485 let restore_posix_special = cfg!(any(target_os = "linux", target_os = "macos"))
4486 && self.options.restore_policy == RestorePolicy::System
4487 && self.options.system_authorized;
4488 if matches!(
4489 member.kind,
4490 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo
4491 ) && !restore_posix_special
4492 {
4493 self.skipped_by_policy = true;
4494 return Ok(());
4495 }
4496 let destination = prepare_destination(self.root, &member.path, member.kind, self.options)?;
4497 match member.kind {
4498 TarEntryKind::Regular => {
4499 if member.reparse_placeholder {
4500 #[cfg(windows)]
4501 {
4502 create_windows_reparse_object(
4503 &destination,
4504 &member.path,
4505 member.kind,
4506 &member.v45_metadata,
4507 &mut self.staged_auxiliary,
4508 self.options,
4509 &mut self.planned_diagnostics,
4510 )?;
4511 if !self.staged_auxiliary.is_empty() {
4512 let reparse = open_existing_windows_reparse(&destination)?;
4513 apply_windows_alternate_streams(
4514 &reparse,
4515 &member.path,
4516 &mut self.staged_auxiliary,
4517 self.options,
4518 &mut self.planned_diagnostics,
4519 )?;
4520 }
4521 }
4522 #[cfg(not(windows))]
4523 unreachable!("exact Windows reparse restore is Windows-only");
4524 } else {
4525 let (temp_leaf, file) = create_temp_regular_file(&destination)?;
4526 self.destination = Some(destination);
4527 self.temp_leaf = Some(temp_leaf);
4528 self.file = Some(file);
4529 }
4530 }
4531 TarEntryKind::Directory => {
4532 if member.reparse_placeholder {
4533 #[cfg(windows)]
4534 create_windows_reparse_object(
4535 &destination,
4536 &member.path,
4537 member.kind,
4538 &member.v45_metadata,
4539 &mut self.staged_auxiliary,
4540 self.options,
4541 &mut self.planned_diagnostics,
4542 )?;
4543 #[cfg(not(windows))]
4544 unreachable!("exact Windows reparse restore is Windows-only");
4545 } else {
4546 create_directory(&destination)?;
4547 }
4548 #[cfg(windows)]
4549 if !self.staged_auxiliary.is_empty() {
4550 let directory = if member.reparse_placeholder {
4551 open_existing_windows_reparse(&destination)?
4552 } else {
4553 open_existing_directory(&destination)?
4554 };
4555 apply_generic_xattr_auxiliaries(
4556 &directory,
4557 &member.path,
4558 &mut self.staged_auxiliary,
4559 self.options,
4560 &mut self.planned_diagnostics,
4561 )?;
4562 apply_windows_alternate_streams(
4563 &directory,
4564 &member.path,
4565 &mut self.staged_auxiliary,
4566 self.options,
4567 &mut self.planned_diagnostics,
4568 )?;
4569 }
4570 if self.defer_directories {
4571 self.deferred_directories.push((
4572 member.path.clone(),
4573 member.v45_metadata.clone(),
4574 std::mem::take(&mut self.staged_auxiliary),
4575 ));
4576 }
4577 }
4578 TarEntryKind::Symlink => {
4579 let target = member
4580 .link_target
4581 .as_deref()
4582 .ok_or(FormatError::InvalidArchive("symlink target is missing"))?;
4583 validate_symlink_target(&member.path, target)?;
4584 if restore_exact_windows_reparse {
4585 #[cfg(windows)]
4586 create_windows_reparse_object(
4587 &destination,
4588 &member.path,
4589 member.kind,
4590 &member.v45_metadata,
4591 &mut self.staged_auxiliary,
4592 self.options,
4593 &mut self.planned_diagnostics,
4594 )?;
4595 #[cfg(not(windows))]
4596 unreachable!("exact Windows reparse restore is Windows-only");
4597 } else {
4598 create_symlink(&destination, target, self.options)?;
4599 let result = (|| {
4600 if !self.staged_auxiliary.is_empty() {
4601 #[cfg(windows)]
4602 {
4603 let reparse = open_existing_windows_reparse(&destination)?;
4604 apply_windows_alternate_streams(
4605 &reparse,
4606 &member.path,
4607 &mut self.staged_auxiliary,
4608 self.options,
4609 &mut self.planned_diagnostics,
4610 )?;
4611 }
4612 #[cfg(all(
4613 not(windows),
4614 not(target_os = "linux"),
4615 not(target_os = "macos")
4616 ))]
4617 self.staged_auxiliary.clear();
4618 }
4619 if self.options.restore_policy != RestorePolicy::Content {
4620 apply_restored_linux_symlink_metadata(
4621 &destination,
4622 &member.path,
4623 &member.v45_metadata,
4624 self.options,
4625 &mut self.planned_diagnostics,
4626 )?;
4627 #[cfg(target_os = "linux")]
4628 if !self.staged_auxiliary.is_empty() {
4629 let mut proc_path = PathBuf::from(format!(
4630 "/proc/self/fd/{}",
4631 destination.parent.as_raw_fd()
4632 ));
4633 proc_path.push(&destination.leaf);
4634 apply_generic_xattr_auxiliaries_to_path(
4635 &proc_path,
4636 false,
4637 &member.path,
4638 &mut self.staged_auxiliary,
4639 self.options,
4640 &mut self.planned_diagnostics,
4641 )?;
4642 }
4643 apply_restored_macos_symlink_metadata(
4644 &destination,
4645 &member.path,
4646 &member.v45_metadata,
4647 &mut self.staged_auxiliary,
4648 self.options,
4649 &mut self.planned_diagnostics,
4650 )?;
4651 if member.v45_metadata.declaration.source_os != "macos"
4652 || !matches!(
4653 self.options.restore_policy,
4654 RestorePolicy::SameOs | RestorePolicy::System
4655 )
4656 {
4657 apply_restored_symlink_mtime(
4658 &destination,
4659 &member.path,
4660 member.v45_metadata.portable_mirror.mtime,
4661 self.options,
4662 &mut self.planned_diagnostics,
4663 )?;
4664 }
4665 }
4666 #[cfg(windows)]
4667 if member.v45_metadata.declaration.source_os == "windows"
4668 && matches!(
4669 self.options.restore_policy,
4670 RestorePolicy::SameOs | RestorePolicy::System
4671 )
4672 {
4673 let reparse = open_existing_windows_reparse(&destination)?;
4674 apply_windows_basic_metadata(
4675 &reparse,
4676 &member.path,
4677 &member.v45_metadata,
4678 self.options,
4679 &mut self.planned_diagnostics,
4680 )?;
4681 }
4682 Ok(())
4683 })();
4684 if let Err(error) = result {
4685 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
4686 return Err(error);
4687 }
4688 }
4689 }
4690 TarEntryKind::Hardlink => {
4691 let target = member
4692 .link_target
4693 .as_deref()
4694 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
4695 if self.defer_hardlinks {
4696 self.deferred_hardlinks
4697 .push((member.path.clone(), target.to_vec()));
4698 self.skipped_by_policy = true;
4699 if self.options.restore_policy == RestorePolicy::Content {
4700 self.planned_diagnostics.push(
4701 MetadataDiagnostic::new(
4702 &member.path,
4703 "portable-v1",
4704 "hardlink-topology",
4705 MetadataOperation::Restore,
4706 MetadataDiagnosticStatus::Materialized,
4707 "hardlink topology was materialized by content restore policy",
4708 )
4709 .for_restore(self.options.restore_policy, 3),
4710 );
4711 }
4712 return Ok(());
4713 }
4714 let target_path = existing_safe_regular_path(self.root, target)?;
4715 if self.options.restore_policy == RestorePolicy::Content {
4716 let (temp_leaf, mut output) = create_temp_regular_file(&destination)?;
4717 let mut input = open_existing_regular_file(&target_path)?;
4718 let materialized_bytes =
4719 std::io::copy(&mut input, &mut output).map_err(|_| {
4720 FormatError::FilesystemExtractionFailed(
4721 "failed to materialize hardlink target",
4722 )
4723 })?;
4724 self.destination = Some(destination);
4725 self.temp_leaf = Some(temp_leaf);
4726 self.file = Some(output);
4727 self.materialized_hardlink = true;
4728 self.planned_diagnostics.push(
4729 MetadataDiagnostic::new(
4730 &member.path,
4731 "portable-v1",
4732 "hardlink-topology",
4733 MetadataOperation::Restore,
4734 MetadataDiagnosticStatus::Materialized,
4735 "hardlink topology was materialized by content restore policy",
4736 )
4737 .for_restore(self.options.restore_policy, 3)
4738 .with_bytes(materialized_bytes, materialized_bytes),
4739 );
4740 } else {
4741 create_hardlink(&destination, &target_path, self.options)?;
4742 }
4743 }
4744 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo => {
4745 if self.options.restore_policy != RestorePolicy::System {
4746 return Ok(());
4747 }
4748 if let Err(error) = create_posix_special_object(
4749 &destination,
4750 &member.path,
4751 member.kind,
4752 &member.v45_metadata,
4753 &mut self.staged_auxiliary,
4754 self.options,
4755 &mut self.planned_diagnostics,
4756 ) {
4757 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
4758 return Err(error.into());
4759 }
4760 }
4761 }
4762 Ok(())
4763 }
4764
4765 fn write_regular_payload(&mut self, bytes: &[u8]) -> Result<(), ExtractError> {
4766 let file = self.file.as_mut().ok_or(FormatError::InvalidArchive(
4767 "regular file output is missing",
4768 ))?;
4769 file.write_all(bytes)
4770 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to write regular file"))?;
4771 Ok(())
4772 }
4773
4774 fn begin_sparse_payload(
4775 &mut self,
4776 logical_size: u64,
4777 extents: &[SparseExtent],
4778 ) -> Result<bool, ExtractError> {
4779 #[cfg(windows)]
4780 {
4781 if self.options.restore_policy == RestorePolicy::Content {
4782 return Ok(false);
4783 }
4784 let file = self.file.as_mut().ok_or(FormatError::InvalidArchive(
4785 "regular file output is missing",
4786 ))?;
4787 prepare_windows_sparse_file(file, logical_size)?;
4788 self.native_sparse_active = true;
4789 self.sparse_logical_size = logical_size;
4790 self.sparse_extents = extents.to_vec();
4791 Ok(true)
4792 }
4793 #[cfg(target_os = "linux")]
4794 {
4795 let file = self.file.as_mut().ok_or(FormatError::InvalidArchive(
4796 "regular file output is missing",
4797 ))?;
4798 file.set_len(logical_size).map_err(|_| {
4799 FormatError::FilesystemExtractionFailed(
4800 "failed to set Linux sparse output logical size",
4801 )
4802 })?;
4803 self.native_sparse_active = true;
4804 self.sparse_logical_size = logical_size;
4805 self.sparse_extents = extents.to_vec();
4806 Ok(true)
4807 }
4808 #[cfg(all(not(windows), not(target_os = "linux")))]
4809 {
4810 let _ = (logical_size, extents);
4811 Ok(false)
4812 }
4813 }
4814
4815 fn write_sparse_extent(&mut self, offset: u64, bytes: &[u8]) -> Result<(), ExtractError> {
4816 if !self.native_sparse_active {
4817 return Err(FormatError::InvalidArchive("sparse output was not initialized").into());
4818 }
4819 let file = self.file.as_mut().ok_or(FormatError::InvalidArchive(
4820 "regular file output is missing",
4821 ))?;
4822 file.seek(SeekFrom::Start(offset)).map_err(|_| {
4823 FormatError::FilesystemExtractionFailed("failed to seek sparse output extent")
4824 })?;
4825 file.write_all(bytes).map_err(|_| {
4826 FormatError::FilesystemExtractionFailed("failed to write sparse output extent")
4827 })?;
4828 Ok(())
4829 }
4830
4831 fn finish_sparse_payload(&mut self) -> Result<(), ExtractError> {
4832 if !self.native_sparse_active {
4833 return Ok(());
4834 }
4835 let file = self.file.as_mut().ok_or(FormatError::InvalidArchive(
4836 "regular file output is missing",
4837 ))?;
4838 file.flush().map_err(|_| {
4839 FormatError::FilesystemExtractionFailed("failed to flush sparse output")
4840 })?;
4841 if file
4842 .metadata()
4843 .map_err(|_| {
4844 FormatError::FilesystemExtractionFailed("failed to inspect sparse output")
4845 })?
4846 .len()
4847 != self.sparse_logical_size
4848 {
4849 return Err(FormatError::FilesystemExtractionFailed(
4850 "sparse output logical size does not match archive",
4851 )
4852 .into());
4853 }
4854 #[cfg(windows)]
4855 verify_windows_sparse_file(file, self.sparse_logical_size, &self.sparse_extents)?;
4856 #[cfg(target_os = "linux")]
4857 punch_linux_sparse_holes(file, self.sparse_logical_size, &self.sparse_extents)?;
4858 self.native_sparse_active = false;
4859 Ok(())
4860 }
4861}
4862
4863#[cfg(target_os = "linux")]
4864fn punch_linux_sparse_holes(
4865 file: &fs::File,
4866 logical_size: u64,
4867 extents: &[SparseExtent],
4868) -> Result<(), FormatError> {
4869 let mut cursor = 0u64;
4870 for extent in extents {
4871 if extent.offset > cursor {
4872 punch_linux_sparse_hole(file, cursor, extent.offset - cursor)?;
4873 }
4874 cursor = extent
4875 .offset
4876 .checked_add(extent.length)
4877 .ok_or(FormatError::InvalidArchive("sparse extent overflow"))?;
4878 }
4879 if cursor < logical_size {
4880 punch_linux_sparse_hole(file, cursor, logical_size - cursor)?;
4881 }
4882 Ok(())
4883}
4884
4885#[cfg(target_os = "linux")]
4886fn punch_linux_sparse_hole(file: &fs::File, offset: u64, length: u64) -> Result<(), FormatError> {
4887 if length == 0 {
4888 return Ok(());
4889 }
4890 let offset = libc::off_t::try_from(offset)
4891 .map_err(|_| FormatError::ReaderUnsupported("sparse offset exceeds Linux off_t"))?;
4892 let length = libc::off_t::try_from(length)
4893 .map_err(|_| FormatError::ReaderUnsupported("sparse length exceeds Linux off_t"))?;
4894 if unsafe {
4896 libc::fallocate(
4897 file.as_raw_fd(),
4898 libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE,
4899 offset,
4900 length,
4901 )
4902 } != 0
4903 {
4904 return Err(FormatError::FilesystemExtractionFailed(
4905 "failed to preserve Linux sparse holes",
4906 ));
4907 }
4908 Ok(())
4909}
4910
4911fn format_error_from_extract_error(error: ExtractError) -> FormatError {
4912 match error {
4913 ExtractError::Format(error) => error,
4914 ExtractError::Output(_) => {
4915 FormatError::FilesystemExtractionFailed("failed to write regular file")
4916 }
4917 }
4918}
4919
4920fn read_member_bytes<R: TarMemberGroupReader>(
4921 reader: &mut R,
4922 buf: &mut [u8],
4923 remaining: &mut u64,
4924) -> Result<(), ExtractError> {
4925 if buf.len() as u64 > *remaining {
4926 return Err(FormatError::InvalidArchive("tar member payload exceeds group").into());
4927 }
4928 reader.read_exact_member_bytes(buf)?;
4929 *remaining -= buf.len() as u64;
4930 Ok(())
4931}
4932
4933fn read_member_vec<R: TarMemberGroupReader>(
4934 reader: &mut R,
4935 len: u64,
4936 remaining: &mut u64,
4937) -> Result<Vec<u8>, ExtractError> {
4938 let mut out = vec![0u8; to_usize(len)?];
4939 read_member_bytes(reader, &mut out, remaining)?;
4940 Ok(out)
4941}
4942
4943fn read_zero_padding<R: TarMemberGroupReader>(
4944 reader: &mut R,
4945 len: u64,
4946 remaining: &mut u64,
4947) -> Result<(), ExtractError> {
4948 let mut pending = len;
4949 let mut buf = [0u8; 8192];
4950 while pending > 0 {
4951 let chunk_len = pending.min(buf.len() as u64) as usize;
4952 read_member_bytes(reader, &mut buf[..chunk_len], remaining)?;
4953 if buf[..chunk_len].iter().any(|byte| *byte != 0) {
4954 return Err(FormatError::InvalidArchive("tar member padding is non-zero").into());
4955 }
4956 pending -= chunk_len as u64;
4957 }
4958 Ok(())
4959}
4960
4961fn stream_regular_payload<R, H>(
4962 reader: &mut R,
4963 len: u64,
4964 remaining: &mut u64,
4965 handler: &mut H,
4966) -> Result<(), ExtractError>
4967where
4968 R: TarMemberGroupReader,
4969 H: TarMemberStreamHandler,
4970{
4971 let mut pending = len;
4972 let mut buf = [0u8; 64 * 1024];
4973 while pending > 0 {
4974 let chunk_len = pending.min(buf.len() as u64).min(*remaining) as usize;
4975 let read = reader.read_some_member_bytes(&mut buf[..chunk_len])?;
4976 if read == 0 {
4977 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
4978 }
4979 *remaining -= read as u64;
4980 pending -= read as u64;
4981 handler.write_regular_payload(&buf[..read])?;
4982 }
4983 Ok(())
4984}
4985
4986fn stream_auxiliary_payload<R: TarMemberGroupReader, H: TarMemberStreamHandler>(
4987 reader: &mut R,
4988 len: u64,
4989 remaining: &mut u64,
4990 validator: &mut AuxiliaryStreamValidator,
4991 mut handler: Option<&mut H>,
4992) -> Result<(), ExtractError> {
4993 let mut pending = len;
4994 let mut buf = [0u8; 64 * 1024];
4995 while pending > 0 {
4996 let chunk_len = pending.min(buf.len() as u64).min(*remaining) as usize;
4997 let read = reader.read_some_member_bytes(&mut buf[..chunk_len])?;
4998 if read == 0 {
4999 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
5000 }
5001 *remaining -= read as u64;
5002 pending -= read as u64;
5003 validator.observe(&buf[..read])?;
5004 if let Some(handler) = handler.as_deref_mut() {
5005 handler.write_auxiliary_payload(&buf[..read])?;
5006 }
5007 }
5008 Ok(())
5009}
5010
5011fn stream_sparse_primary_payload<R, H>(
5012 reader: &mut R,
5013 stored_size: u64,
5014 logical_size: u64,
5015 remaining: &mut u64,
5016 handler: &mut H,
5017) -> Result<(), ExtractError>
5018where
5019 R: TarMemberGroupReader,
5020 H: TarMemberStreamHandler,
5021{
5022 if stored_size < TAR_BLOCK_LEN as u64 {
5023 return Err(FormatError::InvalidArchive("sparse primary map is truncated").into());
5024 }
5025 let mut validator = SparseStreamValidator::new(logical_size);
5026 let mut consumed = 0u64;
5027 let layout = loop {
5028 if consumed
5029 .checked_add(TAR_BLOCK_LEN as u64)
5030 .is_none_or(|value| value > stored_size)
5031 {
5032 return Err(FormatError::InvalidArchive("sparse primary map is truncated").into());
5033 }
5034 let mut block = [0u8; TAR_BLOCK_LEN];
5035 read_member_bytes(reader, &mut block, remaining)?;
5036 consumed += TAR_BLOCK_LEN as u64;
5037 validator.observe(&block)?;
5038 if let Some(layout) = validator.layout_if_map_complete() {
5039 if layout.map_and_padding_size as u64 == consumed {
5040 break layout;
5041 }
5042 }
5043 };
5044 let extent_bytes = layout.extents.iter().try_fold(0u64, |sum, extent| {
5045 sum.checked_add(extent.length)
5046 .ok_or(FormatError::InvalidArchive(
5047 "sparse extent byte count overflow",
5048 ))
5049 })?;
5050 if consumed
5051 .checked_add(extent_bytes)
5052 .is_none_or(|value| value != stored_size)
5053 {
5054 return Err(FormatError::InvalidArchive(
5055 "sparse primary stored size does not match its map",
5056 )
5057 .into());
5058 }
5059
5060 let native_output = handler.begin_sparse_payload(logical_size, &layout.extents)?;
5061 let zeros = [0u8; 64 * 1024];
5062 let mut logical_cursor = 0u64;
5063 let mut buf = [0u8; 64 * 1024];
5064 for extent in &layout.extents {
5065 if !native_output {
5066 write_zero_run(handler, &zeros, extent.offset - logical_cursor)?;
5067 }
5068 let mut extent_remaining = extent.length;
5069 let mut extent_consumed = 0u64;
5070 while extent_remaining > 0 {
5071 let chunk_len = extent_remaining.min(buf.len() as u64) as usize;
5072 read_member_bytes(reader, &mut buf[..chunk_len], remaining)?;
5073 validator.observe(&buf[..chunk_len])?;
5074 if native_output {
5075 handler.write_sparse_extent(extent.offset + extent_consumed, &buf[..chunk_len])?;
5076 } else {
5077 handler.write_regular_payload(&buf[..chunk_len])?;
5078 }
5079 extent_remaining -= chunk_len as u64;
5080 extent_consumed += chunk_len as u64;
5081 }
5082 logical_cursor = extent.offset + extent.length;
5083 }
5084 if native_output {
5085 handler.finish_sparse_payload()?;
5086 } else {
5087 write_zero_run(handler, &zeros, logical_size - logical_cursor)?;
5088 }
5089 validator.finish()?;
5090 Ok(())
5091}
5092
5093fn write_zero_run<H: TarMemberStreamHandler>(
5094 handler: &mut H,
5095 zeros: &[u8],
5096 mut len: u64,
5097) -> Result<(), ExtractError> {
5098 while len > 0 {
5099 let chunk_len = len.min(zeros.len() as u64) as usize;
5100 handler.write_regular_payload(&zeros[..chunk_len])?;
5101 len -= chunk_len as u64;
5102 }
5103 Ok(())
5104}
5105
5106fn tar_member_group_end(stream: &[u8], start: usize) -> Result<usize, FormatError> {
5107 try_tar_member_group_end(stream, start)?.ok_or(FormatError::InvalidArchive(
5108 "tar member payload exceeds stream",
5109 ))
5110}
5111
5112#[cfg(test)]
5113fn restore_tar_member(
5114 root: &Path,
5115 member: &OwnedTarMember,
5116 options: SafeExtractionOptions,
5117) -> Result<Vec<MetadataDiagnostic>, FormatError> {
5118 let mut diagnostics = member.diagnostics.clone();
5119 if let Some(metadata) = &member.v45_metadata {
5120 diagnostics.extend(plan_restore(
5121 &member.path,
5122 metadata,
5123 member.kind,
5124 member.reparse_placeholder,
5125 options,
5126 )?);
5127 }
5128 if member.reparse_placeholder {
5129 diagnostics.push(
5130 MetadataDiagnostic::new(
5131 &member.path,
5132 "windows-backup-v1",
5133 "reparse-data",
5134 MetadataOperation::Restore,
5135 MetadataDiagnosticStatus::Skipped,
5136 "reparse placeholder skipped by portable restore policy",
5137 )
5138 .for_restore(options.restore_policy, 3),
5139 );
5140 return Ok(diagnostics);
5141 }
5142 if member.kind == TarEntryKind::Symlink && options.restore_policy == RestorePolicy::Content {
5143 return Ok(diagnostics);
5144 }
5145 if matches!(
5146 member.kind,
5147 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo
5148 ) {
5149 diagnostics.push(
5150 MetadataDiagnostic::new(
5151 &member.path,
5152 "posix-backup-v1",
5153 "special-object",
5154 MetadataOperation::Restore,
5155 MetadataDiagnosticStatus::Skipped,
5156 "special object skipped by portable restore policy",
5157 )
5158 .for_restore(
5159 options.restore_policy,
5160 restore_phase_for_kind(member.kind, member.reparse_placeholder),
5161 ),
5162 );
5163 return Ok(diagnostics);
5164 }
5165 let destination = prepare_destination(root, &member.path, member.kind, options)?;
5166 match member.kind {
5167 TarEntryKind::Regular => {
5168 let (temp_leaf, mut file) = create_temp_regular_file(&destination)?;
5169 file.write_all(&member.data).map_err(|_| {
5170 FormatError::FilesystemExtractionFailed("failed to write regular file")
5171 })?;
5172 file.flush().map_err(|_| {
5173 FormatError::FilesystemExtractionFailed("failed to write regular file")
5174 })?;
5175 let file = publish_regular_file(&destination, &temp_leaf, file, options)?;
5176 if options.restore_policy != RestorePolicy::Content {
5177 if let Err(error) =
5178 apply_restored_regular_file_metadata(&file, member, options, &mut diagnostics)
5179 {
5180 drop(file);
5181 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
5182 return Err(error);
5183 }
5184 }
5185 }
5186 TarEntryKind::Directory => {
5187 create_directory(&destination)?;
5188 if options.restore_policy != RestorePolicy::Content {
5189 let metadata = member
5190 .v45_metadata
5191 .as_ref()
5192 .ok_or(FormatError::InvalidArchive(
5193 "revision-45 member metadata is missing",
5194 ))?;
5195 apply_restored_directory_metadata(
5196 root,
5197 &member.path,
5198 metadata,
5199 None,
5200 options,
5201 &mut diagnostics,
5202 )?;
5203 }
5204 }
5205 TarEntryKind::Symlink => {
5206 let target = member
5207 .link_target
5208 .as_deref()
5209 .ok_or(FormatError::InvalidArchive("symlink target is missing"))?;
5210 validate_symlink_target(&member.path, target)?;
5211 create_symlink(&destination, target, options)?;
5212 if options.restore_policy != RestorePolicy::Content {
5213 let metadata = member
5214 .v45_metadata
5215 .as_ref()
5216 .ok_or(FormatError::InvalidArchive(
5217 "revision-45 member metadata is missing",
5218 ))?;
5219 apply_restored_linux_symlink_metadata(
5220 &destination,
5221 &member.path,
5222 metadata,
5223 options,
5224 &mut diagnostics,
5225 )?;
5226 let mut staged = Vec::new();
5227 apply_restored_macos_symlink_metadata(
5228 &destination,
5229 &member.path,
5230 metadata,
5231 &mut staged,
5232 options,
5233 &mut diagnostics,
5234 )?;
5235 if metadata.declaration.source_os != "macos"
5236 || !matches!(
5237 options.restore_policy,
5238 RestorePolicy::SameOs | RestorePolicy::System
5239 )
5240 {
5241 apply_restored_symlink_mtime(
5242 &destination,
5243 &member.path,
5244 metadata.portable_mirror.mtime,
5245 options,
5246 &mut diagnostics,
5247 )?;
5248 }
5249 }
5250 }
5251 TarEntryKind::Hardlink => {
5252 let target = member
5253 .link_target
5254 .as_deref()
5255 .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
5256 let target_path = existing_safe_regular_path(root, target)?;
5257 if options.restore_policy == RestorePolicy::Content {
5258 let (temp_leaf, mut output) = create_temp_regular_file(&destination)?;
5259 let mut input = open_existing_regular_file(&target_path)?;
5260 let materialized_bytes = std::io::copy(&mut input, &mut output).map_err(|_| {
5261 FormatError::FilesystemExtractionFailed("failed to materialize hardlink target")
5262 })?;
5263 output.flush().map_err(|_| {
5264 FormatError::FilesystemExtractionFailed("failed to materialize hardlink target")
5265 })?;
5266 publish_regular_file(&destination, &temp_leaf, output, options)?;
5267 diagnostics.push(
5268 MetadataDiagnostic::new(
5269 &member.path,
5270 "portable-v1",
5271 "hardlink-topology",
5272 MetadataOperation::Restore,
5273 MetadataDiagnosticStatus::Materialized,
5274 "hardlink topology was materialized by content restore policy",
5275 )
5276 .for_restore(options.restore_policy, 3)
5277 .with_bytes(materialized_bytes, materialized_bytes),
5278 );
5279 } else {
5280 create_hardlink(&destination, &target_path, options)?;
5281 }
5282 }
5283 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice | TarEntryKind::Fifo => {
5284 unreachable!("special objects return before destination preparation")
5285 }
5286 }
5287 Ok(diagnostics)
5288}
5289
5290pub(crate) fn restore_regular_file_metadata_to_open_file(
5291 file: &fs::File,
5292 member: &OwnedTarMember,
5293 options: SafeExtractionOptions,
5294) -> Result<Vec<MetadataDiagnostic>, FormatError> {
5295 if member.kind != TarEntryKind::Regular {
5296 return Err(FormatError::ReaderUnsupported(
5297 "open-file metadata restore requires a regular archive member",
5298 ));
5299 }
5300 let metadata = member
5301 .v45_metadata
5302 .as_ref()
5303 .ok_or(FormatError::InvalidArchive(
5304 "revision-45 member metadata is missing",
5305 ))?;
5306 let mut diagnostics = plan_owned_member_restore(member, options)?;
5307 if options.restore_policy != RestorePolicy::Content {
5308 apply_restored_regular_file_metadata_parts(
5309 file,
5310 &member.path,
5311 RestoredRegularMetadata::from(&metadata.portable_mirror),
5312 Some(metadata),
5313 None,
5314 options,
5315 &mut diagnostics,
5316 )?;
5317 }
5318 Ok(diagnostics)
5319}
5320
5321#[cfg(test)]
5322fn apply_restored_regular_file_metadata(
5323 file: &fs::File,
5324 member: &OwnedTarMember,
5325 options: SafeExtractionOptions,
5326 diagnostics: &mut Vec<MetadataDiagnostic>,
5327) -> Result<(), FormatError> {
5328 if member.v45_metadata.is_some() {
5329 diagnostics.extend(restore_regular_file_metadata_to_open_file(
5330 file, member, options,
5331 )?);
5332 return Ok(());
5333 }
5334 apply_restored_regular_file_metadata_parts(
5335 file,
5336 &member.path,
5337 RestoredRegularMetadata {
5338 mode: member.mode,
5339 mtime: (member.mtime.seconds, member.mtime.nanoseconds),
5340 attributes: None,
5341 mode_origin_native: false,
5342 uid: None,
5343 gid: None,
5344 },
5345 None,
5346 None,
5347 options,
5348 diagnostics,
5349 )
5350}
5351
5352#[derive(Clone, Copy)]
5353struct RestoredRegularMetadata {
5354 mode: u32,
5355 mtime: (i64, u32),
5356 attributes: Option<u32>,
5357 mode_origin_native: bool,
5358 uid: Option<u64>,
5359 gid: Option<u64>,
5360}
5361
5362impl From<&PortableMetadataMirror> for RestoredRegularMetadata {
5363 fn from(metadata: &PortableMetadataMirror) -> Self {
5364 Self {
5365 mode: metadata.mode,
5366 mtime: metadata.mtime,
5367 attributes: metadata.attributes,
5368 mode_origin_native: metadata.mode_origin_native,
5369 uid: metadata.uid,
5370 gid: metadata.gid,
5371 }
5372 }
5373}
5374
5375fn apply_restored_regular_file_metadata_parts(
5376 file: &fs::File,
5377 path: &[u8],
5378 metadata: RestoredRegularMetadata,
5379 member_metadata: Option<&MemberMetadata>,
5380 staged_auxiliary: Option<&mut Vec<StagedAuxiliary>>,
5381 options: SafeExtractionOptions,
5382 diagnostics: &mut Vec<MetadataDiagnostic>,
5383) -> Result<(), FormatError> {
5384 let RestoredRegularMetadata {
5385 mode,
5386 mtime,
5387 attributes,
5388 mode_origin_native,
5389 uid,
5390 gid,
5391 } = metadata;
5392 apply_regular_file_ownership(file, path, uid, gid, options, diagnostics)?;
5393 let mode = if options.restore_policy == RestorePolicy::System && options.system_authorized {
5394 mode
5395 } else {
5396 mode & !0o6000
5397 };
5398 apply_regular_file_mode(file, path, mode, mode_origin_native, options, diagnostics)?;
5399 if let Some(member_metadata) = member_metadata {
5400 apply_regular_file_posix_acl(file, path, member_metadata, options, diagnostics)?;
5401 if let Some(staged) = staged_auxiliary {
5402 apply_macos_native_metadata(file, path, member_metadata, staged, options, diagnostics)?;
5403 apply_generic_xattr_auxiliaries(file, path, staged, options, diagnostics)?;
5404 }
5405 apply_regular_file_xattrs(file, path, member_metadata, options, diagnostics)?;
5406 }
5407 if member_metadata.is_some_and(|metadata| {
5408 metadata.declaration.source_os == "macos"
5409 && matches!(
5410 options.restore_policy,
5411 RestorePolicy::SameOs | RestorePolicy::System
5412 )
5413 }) {
5414 apply_macos_file_timestamps(
5415 file,
5416 path,
5417 member_metadata.unwrap(),
5418 mtime,
5419 options,
5420 diagnostics,
5421 )?;
5422 } else {
5423 apply_regular_file_mtime(file, path, mtime, options, diagnostics)?;
5424 }
5425 apply_regular_file_attributes(file, path, attributes, options, diagnostics)?;
5426 if let Some(member_metadata) = member_metadata {
5427 apply_windows_security_descriptor(file, path, member_metadata, options, diagnostics)?;
5428 apply_windows_basic_metadata(file, path, member_metadata, options, diagnostics)?;
5429 apply_linux_project_id(file, path, member_metadata, options, diagnostics)?;
5430 apply_linux_inode_flags(file, path, member_metadata, options, diagnostics)?;
5431 apply_macos_file_flags(file, path, member_metadata, options, diagnostics)?;
5432 }
5433 Ok(())
5434}
5435
5436#[cfg(windows)]
5437struct WindowsAlternateStreamRollback {
5438 paths: Vec<Vec<u16>>,
5439 committed: bool,
5440}
5441
5442#[cfg(windows)]
5443impl Drop for WindowsAlternateStreamRollback {
5444 fn drop(&mut self) {
5445 if self.committed {
5446 return;
5447 }
5448 use windows_sys::Win32::Storage::FileSystem::DeleteFileW;
5449 for path in self.paths.iter().rev() {
5450 unsafe {
5452 DeleteFileW(path.as_ptr());
5453 }
5454 }
5455 }
5456}
5457
5458#[cfg(windows)]
5459struct WindowsRawEfsContext(*mut std::ffi::c_void);
5460
5461#[cfg(windows)]
5462impl Drop for WindowsRawEfsContext {
5463 fn drop(&mut self) {
5464 use windows_sys::Win32::Storage::FileSystem::CloseEncryptedFileRaw;
5465
5466 if !self.0.is_null() {
5467 unsafe { CloseEncryptedFileRaw(self.0) };
5469 }
5470 }
5471}
5472
5473#[cfg(windows)]
5474fn windows_final_path(file: &fs::File, description: &'static str) -> Result<Vec<u16>, FormatError> {
5475 use windows_sys::Win32::Storage::FileSystem::{
5476 GetFinalPathNameByHandleW, FILE_NAME_NORMALIZED, VOLUME_NAME_DOS,
5477 };
5478
5479 let handle = file.as_raw_handle().cast();
5480 let required = unsafe {
5482 GetFinalPathNameByHandleW(
5483 handle,
5484 std::ptr::null_mut(),
5485 0,
5486 FILE_NAME_NORMALIZED | VOLUME_NAME_DOS,
5487 )
5488 };
5489 if required == 0 {
5490 return Err(FormatError::FilesystemExtractionFailed(description));
5491 }
5492 let mut path = vec![0u16; required as usize + 1];
5493 let written = unsafe {
5495 GetFinalPathNameByHandleW(
5496 handle,
5497 path.as_mut_ptr(),
5498 path.len() as u32,
5499 FILE_NAME_NORMALIZED | VOLUME_NAME_DOS,
5500 )
5501 };
5502 if written == 0 || written as usize >= path.len() {
5503 return Err(FormatError::FilesystemExtractionFailed(description));
5504 }
5505 path.truncate(written as usize);
5506 path.push(0);
5507 Ok(path)
5508}
5509
5510#[cfg(windows)]
5511fn open_windows_raw_efs(path: &[u16], flags: u32) -> Result<WindowsRawEfsContext, FormatError> {
5512 use windows_sys::Win32::Storage::FileSystem::OpenEncryptedFileRawW;
5513
5514 let mut context = std::ptr::null_mut();
5515 let status = unsafe { OpenEncryptedFileRawW(path.as_ptr(), flags, &mut context) };
5517 if status != 0 {
5518 return Err(FormatError::FilesystemExtractionFailed(
5519 "failed to open Windows raw EFS stream",
5520 ));
5521 }
5522 Ok(WindowsRawEfsContext(context))
5523}
5524
5525#[cfg(windows)]
5526struct WindowsRawEfsImport<'a> {
5527 file: &'a mut fs::File,
5528 bytes: u64,
5529 error: Option<std::io::Error>,
5530}
5531
5532#[cfg(windows)]
5533unsafe extern "system" fn windows_raw_efs_import_callback(
5534 buffer: *mut u8,
5535 context: *const std::ffi::c_void,
5536 length: *mut u32,
5537) -> u32 {
5538 use windows_sys::Win32::Foundation::{ERROR_READ_FAULT, ERROR_SUCCESS};
5539
5540 if buffer.is_null() || context.is_null() || length.is_null() {
5541 return ERROR_READ_FAULT;
5542 }
5543 let state = unsafe { &mut *context.cast_mut().cast::<WindowsRawEfsImport<'_>>() };
5546 let requested = unsafe { *length } as usize;
5547 let output = unsafe { std::slice::from_raw_parts_mut(buffer, requested) };
5548 match state.file.read(output) {
5549 Ok(count) => {
5550 unsafe { *length = count as u32 };
5551 state.bytes = state.bytes.saturating_add(count as u64);
5552 ERROR_SUCCESS
5553 }
5554 Err(error) => {
5555 state.error = Some(error);
5556 unsafe { *length = 0 };
5557 ERROR_READ_FAULT
5558 }
5559 }
5560}
5561
5562#[cfg(windows)]
5563struct WindowsRawEfsDigest {
5564 hasher: sha2::Sha256,
5565 bytes: u64,
5566}
5567
5568#[cfg(windows)]
5569unsafe extern "system" fn windows_raw_efs_digest_callback(
5570 bytes: *const u8,
5571 context: *const std::ffi::c_void,
5572 length: u32,
5573) -> u32 {
5574 use windows_sys::Win32::Foundation::{ERROR_READ_FAULT, ERROR_SUCCESS};
5575
5576 if length == 0 {
5577 return ERROR_SUCCESS;
5578 }
5579 if context.is_null() || bytes.is_null() {
5580 return ERROR_READ_FAULT;
5581 }
5582 let state = unsafe { &mut *context.cast_mut().cast::<WindowsRawEfsDigest>() };
5585 let input = unsafe { std::slice::from_raw_parts(bytes, length as usize) };
5586 sha2::Digest::update(&mut state.hasher, input);
5587 state.bytes = state.bytes.saturating_add(length as u64);
5588 ERROR_SUCCESS
5589}
5590
5591#[cfg(windows)]
5592fn verify_windows_raw_efs(path: &[u16], record: &AuxiliaryRecord) -> Result<(), FormatError> {
5593 use sha2::Digest as _;
5594 use windows_sys::Win32::Storage::FileSystem::ReadEncryptedFileRaw;
5595
5596 let context = open_windows_raw_efs(path, 0)?;
5597 let mut digest = WindowsRawEfsDigest {
5598 hasher: sha2::Sha256::new(),
5599 bytes: 0,
5600 };
5601 let status = unsafe {
5603 ReadEncryptedFileRaw(
5604 Some(windows_raw_efs_digest_callback),
5605 (&mut digest as *mut WindowsRawEfsDigest).cast(),
5606 context.0,
5607 )
5608 };
5609 if status != 0 {
5610 return Err(FormatError::FilesystemExtractionFailed(
5611 "failed to verify restored Windows raw EFS stream",
5612 ));
5613 }
5614 if digest.bytes != record.stored_size || digest.hasher.finalize().as_slice() != record.sha256 {
5615 return Err(FormatError::FilesystemExtractionFailed(
5616 "restored Windows raw EFS stream did not verify",
5617 ));
5618 }
5619 Ok(())
5620}
5621
5622#[cfg(windows)]
5623fn restore_windows_efs_temp(
5624 destination: &PreparedDestination,
5625 temp_leaf: &Path,
5626 mut output: fs::File,
5627 staged: &mut Vec<StagedAuxiliary>,
5628 options: SafeExtractionOptions,
5629) -> Result<fs::File, FormatError> {
5630 use std::os::windows::fs::MetadataExt as _;
5631 use windows_sys::Win32::Storage::FileSystem::WriteEncryptedFileRaw;
5632 use windows_sys::Win32::System::WindowsProgramming::CREATE_FOR_IMPORT;
5633
5634 let Some(index) = staged
5635 .iter()
5636 .position(|item| item.record.kind == "windows.efs-raw")
5637 else {
5638 return Ok(output);
5639 };
5640 if options.restore_policy != RestorePolicy::System || !options.system_authorized {
5641 return Err(FormatError::FilesystemExtractionFailed(
5642 "Windows raw EFS restoration requires authorized system policy",
5643 ));
5644 }
5645 output.flush().map_err(|_| {
5646 FormatError::FilesystemExtractionFailed("failed to flush Windows raw EFS temporary file")
5647 })?;
5648 let raw_path = windows_final_path(&output, "failed to resolve Windows raw EFS temporary file")?;
5649 drop(output);
5650 destination
5651 .parent
5652 .remove_file_or_symlink(temp_leaf)
5653 .map_err(|_| {
5654 FormatError::FilesystemExtractionFailed(
5655 "failed to replace temporary file with Windows raw EFS data",
5656 )
5657 })?;
5658
5659 let StagedAuxiliary {
5660 record,
5661 file: mut staged_file,
5662 } = staged.remove(index);
5663 let staged_len = staged_file
5664 .metadata()
5665 .map_err(|_| {
5666 FormatError::FilesystemExtractionFailed("failed to inspect staged Windows raw EFS data")
5667 })?
5668 .len();
5669 if staged_len != record.stored_size {
5670 return Err(FormatError::InvalidArchive(
5671 "staged Windows raw EFS size is inconsistent",
5672 ));
5673 }
5674 staged_file.seek(SeekFrom::Start(0)).map_err(|_| {
5675 FormatError::FilesystemExtractionFailed("failed to rewind staged Windows raw EFS data")
5676 })?;
5677
5678 let context = open_windows_raw_efs(&raw_path, CREATE_FOR_IMPORT)?;
5679 let mut import = WindowsRawEfsImport {
5680 file: &mut staged_file,
5681 bytes: 0,
5682 error: None,
5683 };
5684 let status = unsafe {
5687 WriteEncryptedFileRaw(
5688 Some(windows_raw_efs_import_callback),
5689 (&mut import as *mut WindowsRawEfsImport<'_>).cast(),
5690 context.0,
5691 )
5692 };
5693 if status != 0 || import.error.is_some() || import.bytes != record.stored_size {
5694 return Err(FormatError::FilesystemExtractionFailed(
5695 "failed to restore Windows raw EFS data",
5696 ));
5697 }
5698 drop(context);
5699 verify_windows_raw_efs(&raw_path, &record)?;
5700
5701 let mut reopen = CapOpenOptions::new();
5702 reopen
5703 .read(true)
5704 .write(true)
5705 .access_mode(FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE)
5706 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
5707 .follow(FollowSymlinks::No);
5708 let output = destination
5709 .parent
5710 .open_with(temp_leaf, &reopen)
5711 .map(cap_std::fs::File::into_std)
5712 .map_err(|_| {
5713 FormatError::FilesystemExtractionFailed(
5714 "failed to reopen restored Windows raw EFS temporary file",
5715 )
5716 })?;
5717 let metadata = output.metadata().map_err(|_| {
5718 FormatError::FilesystemExtractionFailed("failed to inspect restored Windows raw EFS file")
5719 })?;
5720 if !metadata.is_file()
5721 || metadata.file_type().is_symlink()
5722 || metadata.file_attributes() & FILE_ATTRIBUTE_ENCRYPTED == 0
5723 {
5724 return Err(FormatError::FilesystemExtractionFailed(
5725 "restored Windows raw EFS file is not encrypted",
5726 ));
5727 }
5728 Ok(output)
5729}
5730
5731#[cfg(not(windows))]
5732fn restore_windows_efs_temp(
5733 _destination: &PreparedDestination,
5734 _temp_leaf: &Path,
5735 output: fs::File,
5736 staged: &mut [StagedAuxiliary],
5737 _options: SafeExtractionOptions,
5738) -> Result<fs::File, FormatError> {
5739 if staged
5740 .iter()
5741 .any(|item| item.record.kind == "windows.efs-raw")
5742 {
5743 return Err(FormatError::FilesystemExtractionFailed(
5744 "Windows raw EFS restore is unavailable on this host",
5745 ));
5746 }
5747 Ok(output)
5748}
5749
5750#[cfg(windows)]
5751fn apply_windows_alternate_streams(
5752 base_file: &fs::File,
5753 path: &[u8],
5754 staged: &mut Vec<StagedAuxiliary>,
5755 options: SafeExtractionOptions,
5756 diagnostics: &mut Vec<MetadataDiagnostic>,
5757) -> Result<(), FormatError> {
5758 use std::os::windows::io::FromRawHandle as _;
5759 use windows_sys::Win32::Storage::FileSystem::{
5760 CreateFileW, GetFinalPathNameByHandleW, CREATE_NEW, FILE_ATTRIBUTE_NORMAL,
5761 FILE_NAME_NORMALIZED, VOLUME_NAME_DOS,
5762 };
5763
5764 if staged.is_empty() {
5765 return Ok(());
5766 }
5767 if !matches!(
5768 options.restore_policy,
5769 RestorePolicy::SameOs | RestorePolicy::System
5770 ) {
5771 staged.clear();
5772 return Ok(());
5773 }
5774 let handle = base_file.as_raw_handle().cast();
5775 let required = unsafe {
5777 GetFinalPathNameByHandleW(
5778 handle,
5779 std::ptr::null_mut(),
5780 0,
5781 FILE_NAME_NORMALIZED | VOLUME_NAME_DOS,
5782 )
5783 };
5784 if required == 0 {
5785 return Err(FormatError::FilesystemExtractionFailed(
5786 "failed to resolve restored object for alternate-stream creation",
5787 ));
5788 }
5789 let mut base_path = vec![0u16; required as usize + 1];
5790 let written = unsafe {
5792 GetFinalPathNameByHandleW(
5793 handle,
5794 base_path.as_mut_ptr(),
5795 base_path.len() as u32,
5796 FILE_NAME_NORMALIZED | VOLUME_NAME_DOS,
5797 )
5798 };
5799 if written == 0 || written as usize >= base_path.len() {
5800 return Err(FormatError::FilesystemExtractionFailed(
5801 "failed to resolve restored object for alternate-stream creation",
5802 ));
5803 }
5804 base_path.truncate(written as usize);
5805 let mut rollback = WindowsAlternateStreamRollback {
5806 paths: Vec::new(),
5807 committed: false,
5808 };
5809
5810 for staged_record in std::mem::take(staged) {
5811 let StagedAuxiliary { record, mut file } = staged_record;
5812 if record.kind != "windows.alternate-data" {
5813 restore_windows_backup_metadata_stream(
5814 base_file,
5815 path,
5816 &record,
5817 &mut file,
5818 options,
5819 diagnostics,
5820 )?;
5821 continue;
5822 }
5823 if record.decoded_name.len() % 2 != 0 {
5824 return Err(FormatError::InvalidArchive(
5825 "Windows alternate stream name is not UTF-16LE",
5826 ));
5827 }
5828 let stream_name = record
5829 .decoded_name
5830 .chunks_exact(2)
5831 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
5832 .collect::<Vec<_>>();
5833 let mut stream_path = Vec::with_capacity(base_path.len() + stream_name.len() + 1);
5834 stream_path.extend_from_slice(&base_path);
5835 stream_path.extend_from_slice(&stream_name);
5836 stream_path.push(0);
5837 let stream_handle = unsafe {
5840 CreateFileW(
5841 stream_path.as_ptr(),
5842 FILE_GENERIC_READ | FILE_GENERIC_WRITE,
5843 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
5844 std::ptr::null(),
5845 CREATE_NEW,
5846 FILE_ATTRIBUTE_NORMAL,
5847 std::ptr::null_mut(),
5848 )
5849 };
5850 if stream_handle.is_null() || stream_handle as isize == -1 {
5851 let error = std::io::Error::last_os_error();
5852 return record_metadata_application_failure(
5853 diagnostics,
5854 MetadataDiagnostic::new(
5855 path,
5856 "windows-backup-v1",
5857 "alternate-data",
5858 MetadataOperation::Restore,
5859 MetadataDiagnosticStatus::Failed,
5860 "failed to create Windows alternate data stream",
5861 )
5862 .for_restore(options.restore_policy, 2)
5863 .with_native_error(&error),
5864 options,
5865 "failed to create Windows alternate data stream",
5866 );
5867 }
5868 let mut stream = unsafe { fs::File::from_raw_handle(stream_handle.cast()) };
5870 rollback.paths.push(stream_path);
5871 restore_windows_alternate_stream_payload(&mut file, &mut stream, &record)?;
5872 }
5873 rollback.committed = true;
5874 Ok(())
5875}
5876
5877#[cfg(windows)]
5878fn restore_windows_backup_metadata_stream(
5879 base_file: &fs::File,
5880 path: &[u8],
5881 record: &AuxiliaryRecord,
5882 payload: &mut fs::File,
5883 options: SafeExtractionOptions,
5884 diagnostics: &mut Vec<MetadataDiagnostic>,
5885) -> Result<(), FormatError> {
5886 use std::os::windows::io::{AsRawHandle, FromRawHandle};
5887 use std::ptr;
5888 use windows_sys::Win32::Storage::FileSystem::{
5889 BackupWrite, ReOpenFile, FILE_FLAG_BACKUP_SEMANTICS, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
5890 FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
5891 };
5892
5893 let stream_type = record
5894 .meta
5895 .get("TZAP.aux.meta.stream-type")
5896 .ok_or(FormatError::InvalidArchive(
5897 "Windows backup metadata stream type is missing",
5898 ))
5899 .and_then(|value| parse_lower_hex_u32(value, "Windows backup stream type"))?;
5900 let stream_attributes = record
5901 .meta
5902 .get("TZAP.aux.meta.stream-attributes")
5903 .ok_or(FormatError::InvalidArchive(
5904 "Windows backup metadata stream attributes are missing",
5905 ))
5906 .and_then(|value| parse_lower_hex_u32(value, "Windows backup stream attributes"))?;
5907 let expected_type = match record.kind.as_str() {
5908 "windows.ea-data" => 2,
5909 "windows.property-data" => 6,
5910 "windows.object-id" => 7,
5911 _ => {
5912 return Err(FormatError::InvalidArchive(
5913 "staged Windows backup metadata stream has unsupported framing",
5914 ));
5915 }
5916 };
5917 if stream_type != expected_type
5918 || record.flags != 0
5919 || record.logical_size != record.stored_size
5920 || !record.decoded_name.is_empty()
5921 {
5922 return Err(FormatError::InvalidArchive(
5923 "Windows backup metadata stream declaration is inconsistent",
5924 ));
5925 }
5926 if record.kind == "windows.object-id" {
5927 return restore_windows_object_id(base_file, path, record, payload, options, diagnostics);
5928 }
5929 let reopened = unsafe {
5932 ReOpenFile(
5933 base_file.as_raw_handle().cast(),
5934 FILE_GENERIC_READ | FILE_GENERIC_WRITE,
5935 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
5936 FILE_FLAG_BACKUP_SEMANTICS,
5937 )
5938 };
5939 if reopened.is_null() || reopened as isize == -1 {
5940 let error = std::io::Error::last_os_error();
5941 return record_metadata_application_failure(
5942 diagnostics,
5943 MetadataDiagnostic::new(
5944 path,
5945 "windows-backup-v1",
5946 &record.kind,
5947 MetadataOperation::Restore,
5948 MetadataDiagnosticStatus::Failed,
5949 "failed to reopen Windows object for backup-stream restoration",
5950 )
5951 .for_restore(options.restore_policy, 2)
5952 .with_native_error(&error),
5953 options,
5954 "failed to reopen Windows object for backup-stream restoration",
5955 );
5956 }
5957 let destination = unsafe { fs::File::from_raw_handle(reopened.cast()) };
5959 let mut context = ptr::null_mut();
5960 let signed_size = i64::try_from(record.logical_size).map_err(|_| {
5961 FormatError::ReaderUnsupported("Windows backup metadata stream exceeds i64")
5962 })?;
5963 let mut header = [0u8; 20];
5964 header[0..4].copy_from_slice(&stream_type.to_le_bytes());
5965 header[4..8].copy_from_slice(&stream_attributes.to_le_bytes());
5966 header[8..16].copy_from_slice(&signed_size.to_le_bytes());
5967 let result = (|| {
5968 windows_backup_write_all(&destination, &mut context, &header)?;
5969 payload.seek(SeekFrom::Start(0)).map_err(|_| {
5970 FormatError::FilesystemExtractionFailed(
5971 "failed to rewind staged Windows backup metadata stream",
5972 )
5973 })?;
5974 let mut buffer = [0u8; 64 * 1024];
5975 let mut remaining = record.logical_size;
5976 while remaining != 0 {
5977 let count = buffer
5978 .len()
5979 .min(usize::try_from(remaining).unwrap_or(usize::MAX));
5980 payload.read_exact(&mut buffer[..count]).map_err(|_| {
5981 FormatError::FilesystemExtractionFailed(
5982 "staged Windows backup metadata stream ended early",
5983 )
5984 })?;
5985 windows_backup_write_all(&destination, &mut context, &buffer[..count])?;
5986 remaining -= count as u64;
5987 }
5988 Ok(())
5989 })();
5990 let mut ignored = 0u32;
5991 let abort_ok = unsafe {
5993 BackupWrite(
5994 destination.as_raw_handle().cast(),
5995 ptr::null(),
5996 0,
5997 &mut ignored,
5998 1,
5999 0,
6000 &mut context,
6001 )
6002 } != 0;
6003 let result = if result.is_ok() && !abort_ok {
6004 Err(FormatError::FilesystemExtractionFailed(
6005 "failed to finalize Windows backup metadata stream restoration",
6006 ))
6007 } else {
6008 result
6009 };
6010 match result {
6011 Ok(()) => Ok(()),
6012 Err(error @ FormatError::FilesystemExtractionFailed(_)) => {
6013 record_metadata_application_failure(
6014 diagnostics,
6015 MetadataDiagnostic::new(
6016 path,
6017 "windows-backup-v1",
6018 &record.kind,
6019 MetadataOperation::Restore,
6020 MetadataDiagnosticStatus::Failed,
6021 error.to_string(),
6022 )
6023 .for_restore(options.restore_policy, 2),
6024 options,
6025 "failed to restore Windows backup metadata stream",
6026 )
6027 }
6028 Err(error) => Err(error),
6029 }
6030}
6031
6032#[cfg(windows)]
6033fn restore_windows_object_id(
6034 destination: &fs::File,
6035 path: &[u8],
6036 record: &AuxiliaryRecord,
6037 payload: &mut fs::File,
6038 options: SafeExtractionOptions,
6039 diagnostics: &mut Vec<MetadataDiagnostic>,
6040) -> Result<(), FormatError> {
6041 use std::mem::size_of;
6042 use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _};
6043 use windows_sys::Win32::Storage::FileSystem::{
6044 ReOpenFile, FILE_FLAG_BACKUP_SEMANTICS, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
6045 FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
6046 };
6047 use windows_sys::Win32::System::Ioctl::{
6048 FILE_OBJECTID_BUFFER, FSCTL_GET_OBJECT_ID, FSCTL_SET_OBJECT_ID,
6049 };
6050 use windows_sys::Win32::System::IO::DeviceIoControl;
6051
6052 let size = size_of::<FILE_OBJECTID_BUFFER>();
6053 if record.logical_size != size as u64 {
6054 return Err(FormatError::InvalidArchive(
6055 "Windows object-ID backup stream is not exactly 64 bytes",
6056 ));
6057 }
6058 let mut desired = FILE_OBJECTID_BUFFER::default();
6059 payload.seek(SeekFrom::Start(0)).map_err(|_| {
6060 FormatError::FilesystemExtractionFailed("failed to rewind staged Windows object ID")
6061 })?;
6062 {
6063 let desired_bytes = unsafe {
6066 std::slice::from_raw_parts_mut(
6067 (&mut desired as *mut FILE_OBJECTID_BUFFER).cast::<u8>(),
6068 size,
6069 )
6070 };
6071 payload.read_exact(desired_bytes).map_err(|_| {
6072 FormatError::FilesystemExtractionFailed("staged Windows object ID ended early")
6073 })?;
6074 }
6075
6076 let reopened_handle = unsafe {
6077 ReOpenFile(
6078 destination.as_raw_handle().cast(),
6079 FILE_GENERIC_READ | FILE_GENERIC_WRITE,
6080 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
6081 FILE_FLAG_BACKUP_SEMANTICS,
6082 )
6083 };
6084 if reopened_handle.is_null() || reopened_handle as isize == -1 {
6085 let error = std::io::Error::last_os_error();
6086 return record_metadata_application_failure(
6087 diagnostics,
6088 MetadataDiagnostic::new(
6089 path,
6090 "windows-backup-v1",
6091 &record.kind,
6092 MetadataOperation::Restore,
6093 MetadataDiagnosticStatus::Failed,
6094 "failed to reopen Windows object for object-ID restoration",
6095 )
6096 .for_restore(options.restore_policy, 2)
6097 .with_native_error(&error),
6098 options,
6099 "failed to reopen Windows object for object-ID restoration",
6100 );
6101 }
6102 let reopened = unsafe { fs::File::from_raw_handle(reopened_handle.cast()) };
6103
6104 let mut returned = 0u32;
6105 let set_ok = unsafe {
6108 DeviceIoControl(
6109 reopened.as_raw_handle().cast(),
6110 FSCTL_SET_OBJECT_ID,
6111 (&mut desired as *mut FILE_OBJECTID_BUFFER).cast(),
6112 size as u32,
6113 std::ptr::null_mut(),
6114 0,
6115 &mut returned,
6116 std::ptr::null_mut(),
6117 )
6118 } != 0;
6119 let set_error = (!set_ok).then(std::io::Error::last_os_error);
6120 let mut actual = FILE_OBJECTID_BUFFER::default();
6121 returned = 0;
6122 let get_ok = unsafe {
6125 DeviceIoControl(
6126 reopened.as_raw_handle().cast(),
6127 FSCTL_GET_OBJECT_ID,
6128 std::ptr::null(),
6129 0,
6130 (&mut actual as *mut FILE_OBJECTID_BUFFER).cast(),
6131 size as u32,
6132 &mut returned,
6133 std::ptr::null_mut(),
6134 )
6135 } != 0;
6136 let actual_bytes = unsafe {
6139 std::slice::from_raw_parts((&actual as *const FILE_OBJECTID_BUFFER).cast::<u8>(), size)
6140 };
6141 let desired_bytes = unsafe {
6142 std::slice::from_raw_parts((&desired as *const FILE_OBJECTID_BUFFER).cast::<u8>(), size)
6143 };
6144 if get_ok && returned as usize == size && actual_bytes == desired_bytes {
6145 return Ok(());
6146 }
6147 let error = set_error.unwrap_or_else(std::io::Error::last_os_error);
6148 record_metadata_application_failure(
6149 diagnostics,
6150 MetadataDiagnostic::new(
6151 path,
6152 "windows-backup-v1",
6153 "windows.object-id",
6154 MetadataOperation::Restore,
6155 MetadataDiagnosticStatus::Failed,
6156 "failed to restore and verify Windows object ID",
6157 )
6158 .for_restore(options.restore_policy, 2)
6159 .with_native_error(&error),
6160 options,
6161 "failed to restore and verify Windows object ID",
6162 )
6163}
6164
6165#[cfg(windows)]
6166fn windows_backup_write_all(
6167 destination: &fs::File,
6168 context: &mut *mut std::ffi::c_void,
6169 mut bytes: &[u8],
6170) -> Result<(), FormatError> {
6171 use std::os::windows::io::AsRawHandle;
6172 use windows_sys::Win32::Storage::FileSystem::BackupWrite;
6173
6174 while !bytes.is_empty() {
6175 let count = bytes.len().min(u32::MAX as usize);
6176 let mut written = 0u32;
6177 if unsafe {
6180 BackupWrite(
6181 destination.as_raw_handle().cast(),
6182 bytes.as_ptr(),
6183 count as u32,
6184 &mut written,
6185 0,
6186 0,
6187 context,
6188 )
6189 } == 0
6190 {
6191 return Err(FormatError::FilesystemExtractionFailed(
6192 "failed to restore Windows backup metadata stream",
6193 ));
6194 }
6195 if written == 0 || written as usize > count {
6196 return Err(FormatError::FilesystemExtractionFailed(
6197 "Windows BackupWrite made no progress",
6198 ));
6199 }
6200 bytes = &bytes[written as usize..];
6201 }
6202 Ok(())
6203}
6204
6205#[cfg(windows)]
6206fn restore_windows_alternate_stream_payload(
6207 staged: &mut fs::File,
6208 stream: &mut fs::File,
6209 record: &AuxiliaryRecord,
6210) -> Result<(), FormatError> {
6211 let sparse_layout = record.sparse_layout.as_ref();
6212 let extents = sparse_layout.map(|layout| layout.extents.as_slice());
6213 let extent_bytes = extents
6214 .unwrap_or_default()
6215 .iter()
6216 .try_fold(0u64, |sum, extent| sum.checked_add(extent.length))
6217 .ok_or(FormatError::InvalidArchive(
6218 "sparse Windows alternate stream extent size overflow",
6219 ))?;
6220 let data_offset = if let Some(extents) = extents {
6221 let map_size = sparse_layout
6222 .expect("sparse extents require a layout")
6223 .map_and_padding_size as u64;
6224 if map_size.checked_add(extent_bytes) != Some(record.stored_size) {
6225 return Err(FormatError::InvalidArchive(
6226 "sparse Windows alternate stream stored size is inconsistent",
6227 ));
6228 }
6229 prepare_windows_sparse_file(stream, record.logical_size)?;
6230 staged.seek(SeekFrom::Start(map_size)).map_err(|_| {
6231 FormatError::FilesystemExtractionFailed("failed to seek staged sparse alternate stream")
6232 })?;
6233 for extent in extents {
6234 stream.seek(SeekFrom::Start(extent.offset)).map_err(|_| {
6235 FormatError::FilesystemExtractionFailed("failed to seek sparse alternate stream")
6236 })?;
6237 copy_exact_bytes(
6238 staged,
6239 stream,
6240 extent.length,
6241 "Windows sparse alternate stream",
6242 )?;
6243 }
6244 map_size
6245 } else {
6246 staged.seek(SeekFrom::Start(0)).map_err(|_| {
6247 FormatError::FilesystemExtractionFailed("failed to rewind staged alternate stream")
6248 })?;
6249 copy_exact_bytes(
6250 staged,
6251 stream,
6252 record.logical_size,
6253 "Windows alternate stream",
6254 )?;
6255 0
6256 };
6257 stream.flush().map_err(|_| {
6258 FormatError::FilesystemExtractionFailed("failed to flush Windows alternate stream")
6259 })?;
6260 if stream
6261 .metadata()
6262 .map_err(|_| {
6263 FormatError::FilesystemExtractionFailed("failed to inspect Windows alternate stream")
6264 })?
6265 .len()
6266 != record.logical_size
6267 {
6268 return Err(FormatError::FilesystemExtractionFailed(
6269 "Windows alternate stream logical size did not verify",
6270 ));
6271 }
6272 if let Some(extents) = extents {
6273 let actual_extents = query_windows_sparse_ranges(stream, record.logical_size)?;
6274 if actual_extents != extents && !windows_file_system_is_refs(stream)? {
6275 return Err(FormatError::FilesystemExtractionFailed(
6276 "Windows sparse alternate stream ranges did not verify",
6277 ));
6278 }
6279 }
6280 staged.seek(SeekFrom::Start(data_offset)).map_err(|_| {
6281 FormatError::FilesystemExtractionFailed("failed to rewind staged alternate stream data")
6282 })?;
6283 if let Some(extents) = extents {
6284 for extent in extents {
6285 stream.seek(SeekFrom::Start(extent.offset)).map_err(|_| {
6286 FormatError::FilesystemExtractionFailed(
6287 "failed to seek restored sparse alternate stream",
6288 )
6289 })?;
6290 compare_exact_bytes(
6291 staged,
6292 stream,
6293 extent.length,
6294 "Windows sparse alternate stream",
6295 )?;
6296 }
6297 } else {
6298 stream.seek(SeekFrom::Start(0)).map_err(|_| {
6299 FormatError::FilesystemExtractionFailed("failed to rewind Windows alternate stream")
6300 })?;
6301 compare_exact_bytes(
6302 staged,
6303 stream,
6304 record.logical_size,
6305 "Windows alternate stream",
6306 )?;
6307 }
6308 Ok(())
6309}
6310
6311#[cfg(windows)]
6312fn copy_exact_bytes(
6313 input: &mut fs::File,
6314 output: &mut fs::File,
6315 mut remaining: u64,
6316 description: &'static str,
6317) -> Result<(), FormatError> {
6318 let mut buffer = [0u8; 64 * 1024];
6319 while remaining > 0 {
6320 let count = buffer
6321 .len()
6322 .min(usize::try_from(remaining).unwrap_or(usize::MAX));
6323 input.read_exact(&mut buffer[..count]).map_err(|_| {
6324 FormatError::FilesystemExtractionFailed("staged auxiliary payload ended early")
6325 })?;
6326 output
6327 .write_all(&buffer[..count])
6328 .map_err(|_| FormatError::FilesystemExtractionFailed(description))?;
6329 remaining -= count as u64;
6330 }
6331 Ok(())
6332}
6333
6334#[cfg(windows)]
6335fn compare_exact_bytes(
6336 expected: &mut fs::File,
6337 actual: &mut fs::File,
6338 mut remaining: u64,
6339 description: &'static str,
6340) -> Result<(), FormatError> {
6341 let mut expected_buffer = [0u8; 64 * 1024];
6342 let mut actual_buffer = [0u8; 64 * 1024];
6343 while remaining > 0 {
6344 let count = expected_buffer
6345 .len()
6346 .min(usize::try_from(remaining).unwrap_or(usize::MAX));
6347 expected
6348 .read_exact(&mut expected_buffer[..count])
6349 .map_err(|_| {
6350 FormatError::FilesystemExtractionFailed("failed to read staged auxiliary payload")
6351 })?;
6352 actual
6353 .read_exact(&mut actual_buffer[..count])
6354 .map_err(|_| {
6355 FormatError::FilesystemExtractionFailed("failed to read restored auxiliary payload")
6356 })?;
6357 if expected_buffer[..count] != actual_buffer[..count] {
6358 return Err(FormatError::FilesystemExtractionFailed(description));
6359 }
6360 remaining -= count as u64;
6361 }
6362 Ok(())
6363}
6364
6365#[cfg(unix)]
6366fn apply_generic_xattr_auxiliaries(
6367 base_file: &fs::File,
6368 path: &[u8],
6369 staged: &mut Vec<StagedAuxiliary>,
6370 options: SafeExtractionOptions,
6371 diagnostics: &mut Vec<MetadataDiagnostic>,
6372) -> Result<(), FormatError> {
6373 use std::ffi::OsStr;
6374 use std::os::unix::ffi::OsStrExt;
6375 use xattr::FileExt as _;
6376
6377 let mut remaining = Vec::new();
6378 for mut item in std::mem::take(staged) {
6379 if item.record.kind != "generic.xattr" {
6380 remaining.push(item);
6381 continue;
6382 }
6383 if item.record.restore_class == RestoreClass::System
6384 && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
6385 {
6386 continue;
6387 }
6388 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
6389 FormatError::FilesystemExtractionFailed("failed to rewind staged extended attribute")
6390 })?;
6391 let value_len = usize::try_from(item.record.logical_size).map_err(|_| {
6392 FormatError::ReaderUnsupported("extended attribute exceeds platform limits")
6393 })?;
6394 let mut value = vec![0u8; value_len];
6395 item.file.read_exact(&mut value).map_err(|_| {
6396 FormatError::FilesystemExtractionFailed("failed to read staged extended attribute")
6397 })?;
6398 let name = OsStr::from_bytes(&item.record.decoded_name);
6399 if let Err(error) = base_file.set_xattr(name, &value) {
6400 record_metadata_application_failure(
6401 diagnostics,
6402 MetadataDiagnostic::new(
6403 path,
6404 &item.record.profile,
6405 "extended-attribute",
6406 MetadataOperation::Restore,
6407 MetadataDiagnosticStatus::Failed,
6408 "failed to apply auxiliary extended attribute",
6409 )
6410 .for_restore(options.restore_policy, 4)
6411 .with_native_error(&error),
6412 options,
6413 "failed to apply auxiliary extended attribute",
6414 )?;
6415 continue;
6416 }
6417 if base_file.get_xattr(name).ok().flatten().as_deref() != Some(value.as_slice()) {
6418 record_metadata_application_failure(
6419 diagnostics,
6420 MetadataDiagnostic::new(
6421 path,
6422 &item.record.profile,
6423 "extended-attribute",
6424 MetadataOperation::Restore,
6425 MetadataDiagnosticStatus::Failed,
6426 "auxiliary extended attribute did not verify after restoration",
6427 )
6428 .for_restore(options.restore_policy, 4),
6429 options,
6430 "auxiliary extended attribute did not verify after restoration",
6431 )?;
6432 }
6433 }
6434 *staged = remaining;
6435 Ok(())
6436}
6437
6438#[cfg(target_os = "macos")]
6439fn open_macos_resource_fork(file: &fs::File, write: bool) -> std::io::Result<fs::File> {
6440 use std::ffi::OsString;
6441 use std::os::unix::ffi::OsStringExt as _;
6442 use std::os::unix::fs::MetadataExt as _;
6443
6444 let mut path = vec![0u8; libc::PATH_MAX as usize];
6445 if unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETPATH, path.as_mut_ptr()) } != 0 {
6447 return Err(std::io::Error::last_os_error());
6448 }
6449 let length = path.iter().position(|byte| *byte == 0).ok_or_else(|| {
6450 std::io::Error::new(
6451 std::io::ErrorKind::InvalidData,
6452 "macOS returned an unterminated descriptor path",
6453 )
6454 })?;
6455 path.truncate(length);
6456 path.extend_from_slice(b"/..namedfork/rsrc");
6457 let path = PathBuf::from(OsString::from_vec(path));
6458 let mut options = fs::OpenOptions::new();
6459 options.read(true);
6460 if write {
6461 options.write(true).truncate(true).create(true);
6462 }
6463 let fork = options.open(path)?;
6464 let owner = file.metadata()?;
6465 let fork_metadata = fork.metadata()?;
6466 #[allow(clippy::unnecessary_cast)]
6467 if owner.dev() != fork_metadata.dev() || owner.ino() != fork_metadata.ino() {
6468 return Err(std::io::Error::other(
6469 "resource fork path no longer identifies the pinned file",
6470 ));
6471 }
6472 Ok(fork)
6473}
6474
6475#[cfg(target_os = "macos")]
6476fn apply_macos_native_metadata(
6477 file: &fs::File,
6478 path: &[u8],
6479 metadata: &MemberMetadata,
6480 staged: &mut Vec<StagedAuxiliary>,
6481 options: SafeExtractionOptions,
6482 diagnostics: &mut Vec<MetadataDiagnostic>,
6483) -> Result<(), FormatError> {
6484 use std::ffi::{c_int, c_void, OsStr};
6485 use std::os::unix::ffi::OsStrExt as _;
6486 use xattr::FileExt as _;
6487
6488 if metadata.declaration.source_os != "macos"
6489 || !matches!(
6490 options.restore_policy,
6491 RestorePolicy::SameOs | RestorePolicy::System
6492 )
6493 {
6494 return Ok(());
6495 }
6496
6497 extern "C" {
6498 fn acl_copy_int(buffer: *const c_void) -> *mut c_void;
6499 fn acl_copy_ext(
6500 buffer: *mut c_void,
6501 acl: *mut c_void,
6502 size: libc::ssize_t,
6503 ) -> libc::ssize_t;
6504 fn acl_size(acl: *mut c_void) -> libc::ssize_t;
6505 fn acl_set_fd_np(fd: c_int, acl: *mut c_void, acl_type: c_int) -> c_int;
6506 fn acl_get_fd_np(fd: c_int, acl_type: c_int) -> *mut c_void;
6507 fn acl_free(object: *mut c_void) -> c_int;
6508 }
6509
6510 const ACL_TYPE_EXTENDED: c_int = 0x0000_0100;
6511
6512 let fail = |diagnostics: &mut Vec<MetadataDiagnostic>,
6513 class: &'static str,
6514 message: &'static str,
6515 error: Option<&std::io::Error>| {
6516 let mut diagnostic = MetadataDiagnostic::new(
6517 path,
6518 "macos-backup-v1",
6519 class,
6520 MetadataOperation::Restore,
6521 MetadataDiagnosticStatus::Failed,
6522 message,
6523 )
6524 .for_restore(options.restore_policy, 4);
6525 if let Some(error) = error {
6526 diagnostic = diagnostic.with_native_error(error);
6527 }
6528 record_metadata_application_failure(diagnostics, diagnostic, options, message)
6529 };
6530
6531 let mut items = std::mem::take(staged);
6532 items.sort_by_key(|item| match item.record.kind.as_str() {
6533 "macos.resource-fork" => 0,
6534 "macos.acl-native" => 1,
6535 "macos.finder-info" => 2,
6536 _ => 3,
6537 });
6538 let mut remaining = Vec::new();
6539 for mut item in items {
6540 match item.record.kind.as_str() {
6541 "macos.finder-info" => {
6542 if item.record.logical_size != 32 {
6543 return Err(FormatError::InvalidArchive(
6544 "macOS FinderInfo is not exactly 32 bytes",
6545 ));
6546 }
6547 let mut value = [0u8; 32];
6548 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
6549 FormatError::FilesystemExtractionFailed(
6550 "failed to rewind staged macOS FinderInfo",
6551 )
6552 })?;
6553 item.file.read_exact(&mut value).map_err(|_| {
6554 FormatError::FilesystemExtractionFailed(
6555 "failed to read staged macOS FinderInfo",
6556 )
6557 })?;
6558 let name = OsStr::from_bytes(b"com.apple.FinderInfo");
6559 if let Err(error) = file.set_xattr(name, &value) {
6560 fail(
6561 diagnostics,
6562 "finder-info",
6563 "failed to apply macOS FinderInfo",
6564 Some(&error),
6565 )?;
6566 } else if file.get_xattr(name).ok().flatten().as_deref() != Some(value.as_slice()) {
6567 fail(
6568 diagnostics,
6569 "finder-info",
6570 "macOS FinderInfo did not verify after restoration",
6571 None,
6572 )?;
6573 }
6574 }
6575 "macos.resource-fork" => {
6576 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
6577 FormatError::FilesystemExtractionFailed(
6578 "failed to rewind staged macOS resource fork",
6579 )
6580 })?;
6581 let mut fork = match open_macos_resource_fork(file, true) {
6582 Ok(fork) => fork,
6583 Err(error) => {
6584 fail(
6585 diagnostics,
6586 "resource-fork",
6587 "failed to open macOS resource fork",
6588 Some(&error),
6589 )?;
6590 continue;
6591 }
6592 };
6593 if std::io::copy(&mut item.file, &mut fork)
6594 .ok()
6595 .is_none_or(|copied| copied != item.record.logical_size)
6596 || fork.sync_all().is_err()
6597 {
6598 fail(
6599 diagnostics,
6600 "resource-fork",
6601 "failed to write macOS resource fork",
6602 None,
6603 )?;
6604 } else {
6605 drop(fork);
6606 let mut fork = open_macos_resource_fork(file, false).map_err(|_| {
6607 FormatError::FilesystemExtractionFailed(
6608 "failed to reopen macOS resource fork for verification",
6609 )
6610 })?;
6611 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
6612 FormatError::FilesystemExtractionFailed(
6613 "failed to rewind staged macOS resource fork",
6614 )
6615 })?;
6616 let mut expected = vec![0u8; 1024 * 1024];
6617 let mut actual = vec![0u8; 1024 * 1024];
6618 let mut remaining = item.record.logical_size;
6619 let mut verified = true;
6620 while remaining > 0 {
6621 let count = expected
6622 .len()
6623 .min(usize::try_from(remaining).unwrap_or(usize::MAX));
6624 if item.file.read_exact(&mut expected[..count]).is_err()
6625 || fork.read_exact(&mut actual[..count]).is_err()
6626 || expected[..count] != actual[..count]
6627 {
6628 verified = false;
6629 break;
6630 }
6631 remaining -= count as u64;
6632 }
6633 let mut trailing = [0u8; 1];
6634 if verified && fork.read(&mut trailing).ok() != Some(0) {
6635 verified = false;
6636 }
6637 if !verified {
6638 fail(
6639 diagnostics,
6640 "resource-fork",
6641 "macOS resource fork content did not verify after restoration",
6642 None,
6643 )?;
6644 }
6645 }
6646 }
6647 "macos.acl-native" => {
6648 let size = usize::try_from(item.record.logical_size).map_err(|_| {
6649 FormatError::ReaderUnsupported("macOS ACL exceeds platform limits")
6650 })?;
6651 let mut value = vec![0u8; size];
6652 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
6653 FormatError::FilesystemExtractionFailed("failed to rewind staged macOS ACL")
6654 })?;
6655 item.file.read_exact(&mut value).map_err(|_| {
6656 FormatError::FilesystemExtractionFailed("failed to read staged macOS ACL")
6657 })?;
6658 validate_darwin_acl_external(&value)?;
6659 let acl = unsafe { acl_copy_int(value.as_ptr().cast()) };
6661 if acl.is_null() || unsafe { acl_size(acl) } != size as libc::ssize_t {
6662 if !acl.is_null() {
6663 unsafe { acl_free(acl) };
6664 }
6665 return Err(FormatError::InvalidArchive(
6666 "macOS ACL external form is invalid",
6667 ));
6668 }
6669 if unsafe { acl_set_fd_np(file.as_raw_fd(), acl, ACL_TYPE_EXTENDED) } != 0 {
6670 let error = std::io::Error::last_os_error();
6671 unsafe { acl_free(acl) };
6672 fail(
6673 diagnostics,
6674 "acl-native",
6675 "failed to apply native macOS ACL",
6676 Some(&error),
6677 )?;
6678 continue;
6679 }
6680 unsafe { acl_free(acl) };
6681 let restored = unsafe { acl_get_fd_np(file.as_raw_fd(), ACL_TYPE_EXTENDED) };
6682 if restored.is_null() || unsafe { acl_size(restored) } != size as libc::ssize_t {
6683 if !restored.is_null() {
6684 unsafe { acl_free(restored) };
6685 }
6686 fail(
6687 diagnostics,
6688 "acl-native",
6689 "native macOS ACL did not verify after restoration",
6690 None,
6691 )?;
6692 continue;
6693 }
6694 let mut actual = vec![0u8; size];
6695 let copied = unsafe {
6696 acl_copy_ext(actual.as_mut_ptr().cast(), restored, size as libc::ssize_t)
6697 };
6698 unsafe { acl_free(restored) };
6699 if copied != size as libc::ssize_t || actual != value {
6700 fail(
6701 diagnostics,
6702 "acl-native",
6703 "native macOS ACL did not verify after restoration",
6704 None,
6705 )?;
6706 }
6707 }
6708 _ => remaining.push(item),
6709 }
6710 }
6711 *staged = remaining;
6712
6713 Ok(())
6714}
6715
6716#[cfg(not(target_os = "macos"))]
6717fn apply_macos_native_metadata(
6718 _file: &fs::File,
6719 _path: &[u8],
6720 _metadata: &MemberMetadata,
6721 _staged: &mut Vec<StagedAuxiliary>,
6722 _options: SafeExtractionOptions,
6723 _diagnostics: &mut Vec<MetadataDiagnostic>,
6724) -> Result<(), FormatError> {
6725 Ok(())
6726}
6727
6728#[cfg(target_os = "macos")]
6729fn apply_macos_file_timestamps(
6730 file: &fs::File,
6731 path: &[u8],
6732 metadata: &MemberMetadata,
6733 mtime: (i64, u32),
6734 options: SafeExtractionOptions,
6735 diagnostics: &mut Vec<MetadataDiagnostic>,
6736) -> Result<(), FormatError> {
6737 use std::ffi::{c_int, c_void};
6738 use std::os::macos::fs::MetadataExt as _;
6739
6740 #[repr(C)]
6741 struct AttrList {
6742 bitmap_count: u16,
6743 reserved: u16,
6744 common_attr: u32,
6745 volume_attr: u32,
6746 directory_attr: u32,
6747 file_attr: u32,
6748 fork_attr: u32,
6749 }
6750 extern "C" {
6751 fn fsetattrlist(
6752 fd: c_int,
6753 attributes: *const c_void,
6754 buffer: *const c_void,
6755 size: usize,
6756 options: u32,
6757 ) -> c_int;
6758 }
6759 let mut common_attr = 0x0000_0400;
6760 let mut times = Vec::<libc::timespec>::new();
6761 let creation_time = metadata
6762 .primary_records
6763 .get("LIBARCHIVE.creationtime")
6764 .map(|encoded| parse_timestamp(encoded))
6765 .transpose()?;
6766 if let Some((seconds, nanoseconds)) = creation_time {
6767 common_attr |= 0x0000_0200;
6768 times.push(libc::timespec {
6769 tv_sec: seconds,
6770 tv_nsec: i64::from(nanoseconds),
6771 });
6772 }
6773 times.push(libc::timespec {
6774 tv_sec: mtime.0,
6775 tv_nsec: i64::from(mtime.1),
6776 });
6777 let attributes = AttrList {
6778 bitmap_count: 5,
6779 reserved: 0,
6780 common_attr,
6781 volume_attr: 0,
6782 directory_attr: 0,
6783 file_attr: 0,
6784 fork_attr: 0,
6785 };
6786 if unsafe {
6787 fsetattrlist(
6788 file.as_raw_fd(),
6789 (&attributes as *const AttrList).cast(),
6790 times.as_ptr().cast(),
6791 times.len() * std::mem::size_of::<libc::timespec>(),
6792 0,
6793 )
6794 } != 0
6795 {
6796 let error = std::io::Error::last_os_error();
6797 return record_metadata_application_failure(
6798 diagnostics,
6799 MetadataDiagnostic::new(
6800 path,
6801 "macos-backup-v1",
6802 "timestamps",
6803 MetadataOperation::Restore,
6804 MetadataDiagnosticStatus::Failed,
6805 "failed to apply macOS timestamps",
6806 )
6807 .for_restore(options.restore_policy, 4)
6808 .with_native_error(&error),
6809 options,
6810 "failed to apply macOS timestamps",
6811 );
6812 }
6813 let actual = file.metadata().map_err(|_| {
6814 FormatError::FilesystemExtractionFailed("failed to inspect restored macOS timestamps")
6815 })?;
6816 if (actual.st_mtime(), actual.st_mtime_nsec() as u32) != mtime
6817 || creation_time.is_some_and(|creation| {
6818 (actual.st_birthtime(), actual.st_birthtime_nsec() as u32) != creation
6819 })
6820 {
6821 return record_metadata_application_failure(
6822 diagnostics,
6823 MetadataDiagnostic::new(
6824 path,
6825 "macos-backup-v1",
6826 "timestamps",
6827 MetadataOperation::Restore,
6828 MetadataDiagnosticStatus::Failed,
6829 "macOS timestamps did not verify after restoration",
6830 )
6831 .for_restore(options.restore_policy, 4),
6832 options,
6833 "macOS timestamps did not verify after restoration",
6834 );
6835 }
6836 Ok(())
6837}
6838
6839#[cfg(not(target_os = "macos"))]
6840fn apply_macos_file_timestamps(
6841 _file: &fs::File,
6842 _path: &[u8],
6843 _metadata: &MemberMetadata,
6844 _mtime: (i64, u32),
6845 _options: SafeExtractionOptions,
6846 _diagnostics: &mut Vec<MetadataDiagnostic>,
6847) -> Result<(), FormatError> {
6848 Ok(())
6849}
6850
6851#[cfg(target_os = "macos")]
6852fn apply_macos_file_flags(
6853 file: &fs::File,
6854 path: &[u8],
6855 metadata: &MemberMetadata,
6856 options: SafeExtractionOptions,
6857 diagnostics: &mut Vec<MetadataDiagnostic>,
6858) -> Result<(), FormatError> {
6859 use std::os::macos::fs::MetadataExt as _;
6860
6861 if metadata.declaration.source_os != "macos"
6862 || !matches!(
6863 options.restore_policy,
6864 RestorePolicy::SameOs | RestorePolicy::System
6865 )
6866 {
6867 return Ok(());
6868 }
6869 let Some(encoded) = metadata.primary_records.get("TZAP.macos.st-flags") else {
6870 return Ok(());
6871 };
6872 let desired = parse_macos_flags(encoded)? & MACOS_KNOWN_SETTABLE_FLAGS;
6873 if macos_flags_require_system(desired)
6874 && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
6875 {
6876 return Ok(());
6877 }
6878 let retained_unknown = file
6879 .metadata()
6880 .map(|value| value.st_flags() & !MACOS_KNOWN_SETTABLE_FLAGS)
6881 .unwrap_or(0);
6882 let applied = retained_unknown | desired;
6883 if unsafe { libc::fchflags(file.as_raw_fd(), applied) } != 0 {
6885 let error = std::io::Error::last_os_error();
6886 return record_metadata_application_failure(
6887 diagnostics,
6888 MetadataDiagnostic::new(
6889 path,
6890 "macos-backup-v1",
6891 "file-flags",
6892 MetadataOperation::Restore,
6893 MetadataDiagnosticStatus::Failed,
6894 "failed to apply macOS file flags",
6895 )
6896 .for_restore(options.restore_policy, 4)
6897 .with_native_error(&error),
6898 options,
6899 "failed to apply macOS file flags",
6900 );
6901 }
6902 if file
6903 .metadata()
6904 .map(|value| value.st_flags() & MACOS_KNOWN_SETTABLE_FLAGS)
6905 .ok()
6906 != Some(desired)
6907 {
6908 return record_metadata_application_failure(
6909 diagnostics,
6910 MetadataDiagnostic::new(
6911 path,
6912 "macos-backup-v1",
6913 "file-flags",
6914 MetadataOperation::Restore,
6915 MetadataDiagnosticStatus::Failed,
6916 "macOS file flags did not verify after restoration",
6917 )
6918 .for_restore(options.restore_policy, 4),
6919 options,
6920 "macOS file flags did not verify after restoration",
6921 );
6922 }
6923 Ok(())
6924}
6925
6926#[cfg(not(target_os = "macos"))]
6927fn apply_macos_file_flags(
6928 _file: &fs::File,
6929 _path: &[u8],
6930 _metadata: &MemberMetadata,
6931 _options: SafeExtractionOptions,
6932 _diagnostics: &mut Vec<MetadataDiagnostic>,
6933) -> Result<(), FormatError> {
6934 Ok(())
6935}
6936
6937#[cfg(target_os = "linux")]
6938fn apply_generic_xattr_auxiliaries_to_path(
6939 base_path: &Path,
6940 dereference: bool,
6941 path: &[u8],
6942 staged: &mut Vec<StagedAuxiliary>,
6943 options: SafeExtractionOptions,
6944 diagnostics: &mut Vec<MetadataDiagnostic>,
6945) -> Result<(), FormatError> {
6946 use std::ffi::OsStr;
6947 use std::os::unix::ffi::OsStrExt;
6948
6949 let mut remaining = Vec::new();
6950 for mut item in std::mem::take(staged) {
6951 if item.record.kind != "generic.xattr" {
6952 remaining.push(item);
6953 continue;
6954 }
6955 if item.record.restore_class == RestoreClass::System
6956 && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
6957 {
6958 continue;
6959 }
6960 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
6961 FormatError::FilesystemExtractionFailed("failed to rewind staged extended attribute")
6962 })?;
6963 let value_len = usize::try_from(item.record.logical_size).map_err(|_| {
6964 FormatError::ReaderUnsupported("extended attribute exceeds platform limits")
6965 })?;
6966 let mut value = vec![0u8; value_len];
6967 item.file.read_exact(&mut value).map_err(|_| {
6968 FormatError::FilesystemExtractionFailed("failed to read staged extended attribute")
6969 })?;
6970 let name = OsStr::from_bytes(&item.record.decoded_name);
6971 let set_result = if dereference {
6972 xattr::set_deref(base_path, name, &value)
6973 } else {
6974 xattr::set(base_path, name, &value)
6975 };
6976 if let Err(error) = set_result {
6977 record_metadata_application_failure(
6978 diagnostics,
6979 MetadataDiagnostic::new(
6980 path,
6981 &item.record.profile,
6982 "extended-attribute",
6983 MetadataOperation::Restore,
6984 MetadataDiagnosticStatus::Failed,
6985 "failed to apply auxiliary extended attribute",
6986 )
6987 .for_restore(options.restore_policy, 4)
6988 .with_native_error(&error),
6989 options,
6990 "failed to apply auxiliary extended attribute",
6991 )?;
6992 continue;
6993 }
6994 let restored = if dereference {
6995 xattr::get_deref(base_path, name)
6996 } else {
6997 xattr::get(base_path, name)
6998 };
6999 if restored.ok().flatten().as_deref() != Some(value.as_slice()) {
7000 record_metadata_application_failure(
7001 diagnostics,
7002 MetadataDiagnostic::new(
7003 path,
7004 &item.record.profile,
7005 "extended-attribute",
7006 MetadataOperation::Restore,
7007 MetadataDiagnosticStatus::Failed,
7008 "auxiliary extended attribute did not verify after restoration",
7009 )
7010 .for_restore(options.restore_policy, 4),
7011 options,
7012 "auxiliary extended attribute did not verify after restoration",
7013 )?;
7014 }
7015 }
7016 *staged = remaining;
7017 Ok(())
7018}
7019
7020#[cfg(not(unix))]
7021fn apply_generic_xattr_auxiliaries(
7022 _base_file: &fs::File,
7023 _path: &[u8],
7024 _staged: &mut Vec<StagedAuxiliary>,
7025 _options: SafeExtractionOptions,
7026 _diagnostics: &mut Vec<MetadataDiagnostic>,
7027) -> Result<(), FormatError> {
7028 Ok(())
7029}
7030
7031#[cfg(not(windows))]
7032fn apply_windows_alternate_streams(
7033 _base_file: &fs::File,
7034 _path: &[u8],
7035 _staged: &mut Vec<StagedAuxiliary>,
7036 _options: SafeExtractionOptions,
7037 _diagnostics: &mut Vec<MetadataDiagnostic>,
7038) -> Result<(), FormatError> {
7039 Ok(())
7040}
7041
7042#[cfg(windows)]
7043fn apply_windows_security_descriptor(
7044 file: &fs::File,
7045 path: &[u8],
7046 metadata: &MemberMetadata,
7047 options: SafeExtractionOptions,
7048 diagnostics: &mut Vec<MetadataDiagnostic>,
7049) -> Result<(), FormatError> {
7050 use std::ptr;
7051 use windows_sys::Win32::Foundation::{CloseHandle, ERROR_INSUFFICIENT_BUFFER};
7052 use windows_sys::Win32::Security::Authorization::{SetSecurityInfo, SE_FILE_OBJECT};
7053 use windows_sys::Win32::Security::{
7054 GetKernelObjectSecurity, GetSecurityDescriptorDacl, GetSecurityDescriptorGroup,
7055 GetSecurityDescriptorOwner, GetSecurityDescriptorSacl, SetKernelObjectSecurity,
7056 };
7057 use windows_sys::Win32::Storage::FileSystem::{
7058 ReOpenFile, READ_CONTROL, WRITE_DAC, WRITE_OWNER,
7059 };
7060 use windows_sys::Win32::System::SystemServices::ACCESS_SYSTEM_SECURITY;
7061
7062 if metadata.declaration.source_os != "windows"
7063 || options.restore_policy != RestorePolicy::System
7064 || !options.system_authorized
7065 {
7066 return Ok(());
7067 }
7068 let Some(record) = metadata
7069 .auxiliary
7070 .iter()
7071 .find(|record| record.kind == "windows.security-descriptor")
7072 else {
7073 return Ok(());
7074 };
7075 let payload = record
7076 .capture_report_payload
7077 .as_deref()
7078 .ok_or(FormatError::InvalidArchive(
7079 "Windows security descriptor was not retained",
7080 ))?;
7081 let security_information = record
7082 .meta
7083 .get("TZAP.aux.meta.security-information")
7084 .map(|value| parse_lower_hex_u32(value, "Windows security information"))
7085 .transpose()?
7086 .ok_or(FormatError::InvalidArchive(
7087 "Windows security descriptor lacks its information mask",
7088 ))?;
7089 let query_security_information = security_information & 0x0000_000f;
7090 let control = u16::from_le_bytes([payload[2], payload[3]]);
7091 let mut application_security_information = security_information;
7092 if security_information & 0x0000_0004 != 0 && security_information & 0xa000_0000 == 0 {
7093 application_security_information |= if control & 0x1000 != 0 {
7094 0x8000_0000
7095 } else {
7096 0x2000_0000
7097 };
7098 }
7099 if security_information & 0x0000_0008 != 0 && security_information & 0x5000_0000 == 0 {
7100 application_security_information |= if control & 0x2000 != 0 {
7101 0x4000_0000
7102 } else {
7103 0x1000_0000
7104 };
7105 }
7106 if !windows_security_restore_privileges_available(security_information) {
7107 let diagnostic = MetadataDiagnostic::new(
7108 path,
7109 "windows-backup-v1",
7110 "security-descriptor",
7111 MetadataOperation::Restore,
7112 MetadataDiagnosticStatus::Unsupported,
7113 "required Windows restore privilege is unavailable",
7114 )
7115 .for_restore(options.restore_policy, 4);
7116 if options.allow_degraded {
7117 diagnostics.push(diagnostic);
7118 return Ok(());
7119 }
7120 return Err(FormatError::ReaderUnsupported(
7121 "Windows security restoration requires SeRestorePrivilege and optional SeSecurityPrivilege",
7122 ));
7123 }
7124 let desired_access = READ_CONTROL
7125 | WRITE_DAC
7126 | WRITE_OWNER
7127 | if security_information & 0x0000_0008 != 0 {
7128 ACCESS_SYSTEM_SECURITY
7129 } else {
7130 0
7131 };
7132 let security_handle = unsafe {
7134 ReOpenFile(
7135 file.as_raw_handle().cast(),
7136 desired_access,
7137 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
7138 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
7139 )
7140 };
7141 if security_handle.is_null() || security_handle as isize == -1 {
7142 let error = std::io::Error::last_os_error();
7143 return record_metadata_application_failure(
7144 diagnostics,
7145 MetadataDiagnostic::new(
7146 path,
7147 "windows-backup-v1",
7148 "security-descriptor",
7149 MetadataOperation::Restore,
7150 MetadataDiagnosticStatus::Failed,
7151 "failed to open object for Windows security restoration",
7152 )
7153 .for_restore(options.restore_policy, 4)
7154 .with_native_error(&error),
7155 options,
7156 "failed to open object for Windows security restoration",
7157 );
7158 }
7159 let descriptor = payload.as_ptr().cast_mut().cast();
7160 let mut owner = ptr::null_mut();
7161 let mut group = ptr::null_mut();
7162 let mut dacl = ptr::null_mut();
7163 let mut sacl = ptr::null_mut();
7164 let mut owner_defaulted = 0;
7165 let mut group_defaulted = 0;
7166 let mut dacl_present = 0;
7167 let mut dacl_defaulted = 0;
7168 let mut sacl_present = 0;
7169 let mut sacl_defaulted = 0;
7170 let descriptor_components_ok = unsafe {
7173 GetSecurityDescriptorOwner(descriptor, &mut owner, &mut owner_defaulted) != 0
7174 && GetSecurityDescriptorGroup(descriptor, &mut group, &mut group_defaulted) != 0
7175 && GetSecurityDescriptorDacl(
7176 descriptor,
7177 &mut dacl_present,
7178 &mut dacl,
7179 &mut dacl_defaulted,
7180 ) != 0
7181 && GetSecurityDescriptorSacl(
7182 descriptor,
7183 &mut sacl_present,
7184 &mut sacl,
7185 &mut sacl_defaulted,
7186 ) != 0
7187 };
7188 if !descriptor_components_ok {
7189 unsafe { CloseHandle(security_handle) };
7190 return Err(FormatError::InvalidArchive(
7191 "Windows security descriptor components are invalid",
7192 ));
7193 }
7194 let mut set_error = None;
7195 let owner_group_information = application_security_information & 0x0000_0003;
7196 if owner_group_information != 0
7197 && unsafe { SetKernelObjectSecurity(security_handle, owner_group_information, descriptor) }
7199 == 0
7200 {
7201 set_error = Some(std::io::Error::last_os_error());
7202 }
7203 let dacl_information = application_security_information & 0xa000_0004;
7204 if set_error.is_none() && dacl_information & 0x0000_0004 != 0 {
7205 if dacl_present == 0 || control & 0x0400 != 0 {
7206 let status = unsafe {
7208 SetSecurityInfo(
7209 security_handle,
7210 SE_FILE_OBJECT,
7211 dacl_information,
7212 ptr::null_mut(),
7213 ptr::null_mut(),
7214 dacl,
7215 ptr::null_mut(),
7216 )
7217 };
7218 if status != 0 {
7219 set_error = Some(std::io::Error::from_raw_os_error(status as i32));
7220 }
7221 } else if unsafe {
7222 SetKernelObjectSecurity(security_handle, dacl_information, descriptor)
7224 } == 0
7225 {
7226 set_error = Some(std::io::Error::last_os_error());
7227 }
7228 }
7229 let sacl_information = application_security_information & 0x5000_0008;
7230 if set_error.is_none() && sacl_information & 0x0000_0008 != 0 {
7231 if sacl_present == 0 || control & 0x0800 != 0 {
7232 let status = unsafe {
7234 SetSecurityInfo(
7235 security_handle,
7236 SE_FILE_OBJECT,
7237 sacl_information,
7238 ptr::null_mut(),
7239 ptr::null_mut(),
7240 ptr::null_mut(),
7241 sacl,
7242 )
7243 };
7244 if status != 0 {
7245 set_error = Some(std::io::Error::from_raw_os_error(status as i32));
7246 }
7247 } else if unsafe {
7248 SetKernelObjectSecurity(security_handle, sacl_information, descriptor)
7250 } == 0
7251 {
7252 set_error = Some(std::io::Error::last_os_error());
7253 }
7254 }
7255 if let Some(set_error) = set_error {
7256 unsafe { CloseHandle(security_handle) };
7257 return record_metadata_application_failure(
7258 diagnostics,
7259 MetadataDiagnostic::new(
7260 path,
7261 "windows-backup-v1",
7262 "security-descriptor",
7263 MetadataOperation::Restore,
7264 MetadataDiagnosticStatus::Failed,
7265 "failed to apply Windows security descriptor",
7266 )
7267 .for_restore(options.restore_policy, 4)
7268 .with_native_error(&set_error),
7269 options,
7270 "failed to apply Windows security descriptor",
7271 );
7272 }
7273
7274 let mut needed = 0u32;
7275 let first = unsafe {
7277 GetKernelObjectSecurity(
7278 security_handle,
7279 query_security_information,
7280 ptr::null_mut(),
7281 0,
7282 &mut needed,
7283 )
7284 };
7285 let first_error = std::io::Error::last_os_error();
7286 let mut actual = vec![0u8; needed as usize];
7287 let get_ok = first == 0
7289 && first_error.raw_os_error() == Some(ERROR_INSUFFICIENT_BUFFER as i32)
7290 && needed != 0
7291 && unsafe {
7292 GetKernelObjectSecurity(
7293 security_handle,
7294 query_security_information,
7295 actual.as_mut_ptr().cast(),
7296 needed,
7297 &mut needed,
7298 )
7299 } != 0;
7300 unsafe { CloseHandle(security_handle) };
7301 if get_ok && actual != payload && windows_security_descriptors_equivalent(payload, &actual) {
7302 diagnostics.push(
7303 MetadataDiagnostic::new(
7304 path,
7305 "windows-backup-v1",
7306 "security-descriptor",
7307 MetadataOperation::Restore,
7308 MetadataDiagnosticStatus::Materialized,
7309 "Windows returned a semantically equivalent security descriptor with normalized self-relative layout or absent-ACL protection; all represented components verified",
7310 )
7311 .for_restore(options.restore_policy, 4),
7312 );
7313 return Ok(());
7314 }
7315 if !get_ok || actual != payload {
7316 return record_metadata_application_failure(
7317 diagnostics,
7318 MetadataDiagnostic::new(
7319 path,
7320 "windows-backup-v1",
7321 "security-descriptor",
7322 MetadataOperation::Restore,
7323 MetadataDiagnosticStatus::Failed,
7324 "Windows security descriptor did not verify after restoration",
7325 )
7326 .for_restore(options.restore_policy, 4),
7327 options,
7328 "Windows security descriptor did not verify after restoration",
7329 );
7330 }
7331 Ok(())
7332}
7333
7334#[cfg(windows)]
7335fn windows_security_descriptors_equivalent(expected: &[u8], actual: &[u8]) -> bool {
7336 const DACL_PRESENT: u16 = 0x0004;
7337 const SACL_PRESENT: u16 = 0x0010;
7338 const DACL_PROTECTED: u16 = 0x1000;
7339 const SACL_PROTECTED: u16 = 0x2000;
7340
7341 if expected.len() < 20 || actual.len() < 20 || expected[..2] != actual[..2] {
7342 return false;
7343 }
7344 let expected_control = u16::from_le_bytes([expected[2], expected[3]]);
7345 let actual_control = u16::from_le_bytes([actual[2], actual[3]]);
7346 let mut ignorable = 0u16;
7347 if expected_control & DACL_PRESENT == 0 && actual_control & DACL_PRESENT == 0 {
7348 ignorable |= DACL_PROTECTED;
7349 }
7350 if expected_control & SACL_PRESENT == 0 && actual_control & SACL_PRESENT == 0 {
7351 ignorable |= SACL_PROTECTED;
7352 }
7353 if (expected_control ^ actual_control) & !ignorable != 0 {
7354 return false;
7355 }
7356
7357 for (offset_field, acl, represented) in [
7362 (4usize, false, true),
7363 (8, false, true),
7364 (12, true, expected_control & SACL_PRESENT != 0),
7365 (16, true, expected_control & DACL_PRESENT != 0),
7366 ] {
7367 if represented {
7368 let Some(expected_component) =
7369 security_descriptor_component(expected, offset_field, acl)
7370 else {
7371 return false;
7372 };
7373 let Some(actual_component) = security_descriptor_component(actual, offset_field, acl)
7374 else {
7375 return false;
7376 };
7377 if expected_component != actual_component {
7378 return false;
7379 }
7380 }
7381 }
7382 true
7383}
7384
7385#[cfg(windows)]
7386fn security_descriptor_component(
7387 descriptor: &[u8],
7388 offset_field: usize,
7389 acl: bool,
7390) -> Option<&[u8]> {
7391 let offset_bytes = descriptor.get(offset_field..offset_field.checked_add(4)?)?;
7392 let offset = u32::from_le_bytes(offset_bytes.try_into().ok()?) as usize;
7393 if offset == 0 {
7394 return Some(&[]);
7395 }
7396 let length = if acl {
7397 let header = descriptor.get(offset..offset.checked_add(4)?)?;
7398 u16::from_le_bytes([header[2], header[3]]) as usize
7399 } else {
7400 let header = descriptor.get(offset..offset.checked_add(8)?)?;
7401 8usize.checked_add(usize::from(header[1]).checked_mul(4)?)?
7402 };
7403 descriptor.get(offset..offset.checked_add(length)?)
7404}
7405
7406#[cfg(not(windows))]
7407fn apply_windows_security_descriptor(
7408 _file: &fs::File,
7409 _path: &[u8],
7410 _metadata: &MemberMetadata,
7411 _options: SafeExtractionOptions,
7412 _diagnostics: &mut Vec<MetadataDiagnostic>,
7413) -> Result<(), FormatError> {
7414 Ok(())
7415}
7416
7417#[cfg(windows)]
7418fn pax_timestamp_to_windows_filetime(timestamp: (i64, u32)) -> Result<i64, FormatError> {
7419 const WINDOWS_TO_UNIX_EPOCH_100NS: i128 = 116_444_736_000_000_000;
7420 let (seconds, nanoseconds) = timestamp;
7421 if nanoseconds % 100 != 0 {
7422 return Err(FormatError::FilesystemExtractionFailed(
7423 "Windows timestamp is not representable at 100-nanosecond precision",
7424 ));
7425 }
7426 let ticks = i128::from(seconds)
7427 .checked_mul(10_000_000)
7428 .and_then(|value| value.checked_add(i128::from(nanoseconds / 100)))
7429 .and_then(|value| value.checked_add(WINDOWS_TO_UNIX_EPOCH_100NS))
7430 .and_then(|value| i64::try_from(value).ok())
7431 .ok_or(FormatError::FilesystemExtractionFailed(
7432 "Windows timestamp is outside the FILETIME range",
7433 ))?;
7434 if ticks < 0 {
7435 return Err(FormatError::FilesystemExtractionFailed(
7436 "Windows timestamp predates the FILETIME epoch",
7437 ));
7438 }
7439 Ok(ticks)
7440}
7441
7442#[cfg(windows)]
7443fn apply_windows_basic_metadata(
7444 file: &fs::File,
7445 path: &[u8],
7446 metadata: &MemberMetadata,
7447 options: SafeExtractionOptions,
7448 diagnostics: &mut Vec<MetadataDiagnostic>,
7449) -> Result<(), FormatError> {
7450 if metadata.declaration.source_os != "windows"
7451 || !matches!(
7452 options.restore_policy,
7453 RestorePolicy::SameOs | RestorePolicy::System
7454 )
7455 {
7456 return Ok(());
7457 }
7458
7459 apply_windows_directory_case_sensitive(file, path, metadata, options, diagnostics)?;
7460
7461 let desired_attributes = metadata
7462 .primary_records
7463 .get("TZAP.windows.file-attributes")
7464 .map(|value| parse_lower_hex_u32(value, "Windows file attributes"))
7465 .transpose()?;
7466 let compression_exact = if let Some(desired) = desired_attributes {
7467 apply_windows_compression(
7468 file,
7469 path,
7470 desired & FILE_ATTRIBUTE_COMPRESSED != 0,
7471 options,
7472 diagnostics,
7473 )?
7474 } else {
7475 true
7476 };
7477 let intrinsic_verification_mask = WINDOWS_ESSENTIAL_INTRINSIC_ATTRIBUTES
7478 & if options.restore_policy == RestorePolicy::System {
7479 u32::MAX
7480 } else {
7481 !FILE_ATTRIBUTE_ENCRYPTED
7482 }
7483 & if compression_exact {
7484 u32::MAX
7485 } else {
7486 !FILE_ATTRIBUTE_COMPRESSED
7487 };
7488 let attribute_verification_mask =
7489 WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES | intrinsic_verification_mask;
7490 let parse_optional_timestamp = |key: &str| {
7491 metadata
7492 .primary_records
7493 .get(key)
7494 .map(|value| parse_timestamp(value).and_then(pax_timestamp_to_windows_filetime))
7495 .transpose()
7496 };
7497 let creation_time = parse_optional_timestamp("LIBARCHIVE.creationtime")?;
7498 let access_time = parse_optional_timestamp("atime")?;
7499 let write_time = Some(pax_timestamp_to_windows_filetime(
7500 metadata.portable_mirror.mtime,
7501 )?);
7502 let change_time = parse_optional_timestamp("TZAP.windows.change-time")?;
7503
7504 let mut current = FILE_BASIC_INFO::default();
7505 let handle = file.as_raw_handle().cast();
7506 if unsafe {
7508 GetFileInformationByHandleEx(
7509 handle,
7510 FileBasicInfo,
7511 (&mut current as *mut FILE_BASIC_INFO).cast(),
7512 std::mem::size_of::<FILE_BASIC_INFO>() as u32,
7513 )
7514 } == 0
7515 {
7516 let error = std::io::Error::last_os_error();
7517 return record_metadata_application_failure(
7518 diagnostics,
7519 MetadataDiagnostic::new(
7520 path,
7521 "windows-backup-v1",
7522 "basic-metadata",
7523 MetadataOperation::Restore,
7524 MetadataDiagnosticStatus::Failed,
7525 "failed to inspect Windows basic metadata",
7526 )
7527 .for_restore(options.restore_policy, 4)
7528 .with_native_error(&error),
7529 options,
7530 "failed to inspect Windows basic metadata",
7531 );
7532 }
7533
7534 let mut restored = current;
7535 if let Some(value) = creation_time {
7536 restored.CreationTime = value;
7537 }
7538 if let Some(value) = access_time {
7539 restored.LastAccessTime = value;
7540 }
7541 if let Some(value) = write_time {
7542 restored.LastWriteTime = value;
7543 }
7544 if let Some(value) = change_time {
7545 restored.ChangeTime = value;
7546 }
7547 if let Some(desired) = desired_attributes {
7548 let unsupported = desired
7549 & !(WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES
7550 | WINDOWS_ESSENTIAL_INTRINSIC_ATTRIBUTES
7551 | FILE_ATTRIBUTE_NORMAL);
7552 if unsupported != 0 {
7553 let diagnostic = MetadataDiagnostic::new(
7554 path,
7555 "windows-backup-v1",
7556 "file-attributes",
7557 MetadataOperation::Restore,
7558 MetadataDiagnosticStatus::Unsupported,
7559 format!("unsupported Windows attribute bits were not applied: {unsupported:08x}"),
7560 )
7561 .for_restore(options.restore_policy, 4);
7562 if options.allow_degraded {
7563 diagnostics.push(diagnostic);
7564 } else {
7565 return Err(FormatError::ReaderUnsupported(
7566 "Windows file attributes contain unsupported bits",
7567 ));
7568 }
7569 }
7570 let intrinsic_mismatch = (current.FileAttributes ^ desired) & intrinsic_verification_mask;
7571 if intrinsic_mismatch != 0 {
7572 record_metadata_application_failure(
7573 diagnostics,
7574 MetadataDiagnostic::new(
7575 path,
7576 "windows-backup-v1",
7577 "file-attributes",
7578 MetadataOperation::Restore,
7579 MetadataDiagnosticStatus::Failed,
7580 format!(
7581 "restored Windows object has mismatched intrinsic attributes: {intrinsic_mismatch:08x}"
7582 ),
7583 )
7584 .for_restore(options.restore_policy, 4),
7585 options,
7586 "restored Windows object has mismatched intrinsic attributes",
7587 )?;
7588 }
7589 restored.FileAttributes = (current.FileAttributes & !WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES)
7590 | (desired & WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES);
7591 if restored.FileAttributes
7592 & (WINDOWS_ESSENTIAL_SETTABLE_ATTRIBUTES | WINDOWS_ESSENTIAL_INTRINSIC_ATTRIBUTES)
7593 == 0
7594 {
7595 restored.FileAttributes |= FILE_ATTRIBUTE_NORMAL;
7596 } else {
7597 restored.FileAttributes &= !FILE_ATTRIBUTE_NORMAL;
7598 }
7599 }
7600
7601 if unsafe {
7603 SetFileInformationByHandle(
7604 handle,
7605 FileBasicInfo,
7606 (&restored as *const FILE_BASIC_INFO).cast(),
7607 std::mem::size_of::<FILE_BASIC_INFO>() as u32,
7608 )
7609 } == 0
7610 {
7611 let error = std::io::Error::last_os_error();
7612 return record_metadata_application_failure(
7613 diagnostics,
7614 MetadataDiagnostic::new(
7615 path,
7616 "windows-backup-v1",
7617 "basic-metadata",
7618 MetadataOperation::Restore,
7619 MetadataDiagnosticStatus::Failed,
7620 "failed to apply Windows basic metadata",
7621 )
7622 .for_restore(options.restore_policy, 4)
7623 .with_native_error(&error),
7624 options,
7625 "failed to apply Windows basic metadata",
7626 );
7627 }
7628
7629 let mut actual = FILE_BASIC_INFO::default();
7630 if unsafe {
7632 GetFileInformationByHandleEx(
7633 handle,
7634 FileBasicInfo,
7635 (&mut actual as *mut FILE_BASIC_INFO).cast(),
7636 std::mem::size_of::<FILE_BASIC_INFO>() as u32,
7637 )
7638 } == 0
7639 || actual.CreationTime != restored.CreationTime
7640 || actual.LastAccessTime != restored.LastAccessTime
7641 || actual.LastWriteTime != restored.LastWriteTime
7642 || actual.ChangeTime != restored.ChangeTime
7643 || actual.FileAttributes & attribute_verification_mask
7644 != restored.FileAttributes & attribute_verification_mask
7645 {
7646 let error = std::io::Error::last_os_error();
7647 return record_metadata_application_failure(
7648 diagnostics,
7649 MetadataDiagnostic::new(
7650 path,
7651 "windows-backup-v1",
7652 "basic-metadata",
7653 MetadataOperation::Restore,
7654 MetadataDiagnosticStatus::Failed,
7655 "Windows basic metadata did not verify after restoration",
7656 )
7657 .for_restore(options.restore_policy, 4)
7658 .with_native_error(&error),
7659 options,
7660 "Windows basic metadata did not verify after restoration",
7661 );
7662 }
7663 Ok(())
7664}
7665
7666#[cfg(windows)]
7667fn apply_windows_compression(
7668 file: &fs::File,
7669 path: &[u8],
7670 compressed: bool,
7671 options: SafeExtractionOptions,
7672 diagnostics: &mut Vec<MetadataDiagnostic>,
7673) -> Result<bool, FormatError> {
7674 use std::os::windows::io::AsRawHandle;
7675 use std::ptr;
7676 use windows_sys::Win32::Storage::FileSystem::{
7677 FileBasicInfo, GetFileInformationByHandleEx, COMPRESSION_FORMAT_DEFAULT,
7678 COMPRESSION_FORMAT_NONE, FILE_BASIC_INFO,
7679 };
7680 use windows_sys::Win32::System::Ioctl::FSCTL_SET_COMPRESSION;
7681 use windows_sys::Win32::System::IO::DeviceIoControl;
7682
7683 let handle = file.as_raw_handle().cast();
7684 let mut current = FILE_BASIC_INFO::default();
7685 if unsafe {
7687 GetFileInformationByHandleEx(
7688 handle,
7689 FileBasicInfo,
7690 (&mut current as *mut FILE_BASIC_INFO).cast(),
7691 std::mem::size_of::<FILE_BASIC_INFO>() as u32,
7692 )
7693 } == 0
7694 {
7695 return Err(FormatError::FilesystemExtractionFailed(
7696 "failed to inspect Windows compression state",
7697 ));
7698 }
7699 if (current.FileAttributes & FILE_ATTRIBUTE_COMPRESSED != 0) == compressed {
7700 return Ok(true);
7701 }
7702 let mut format = if compressed {
7703 COMPRESSION_FORMAT_DEFAULT
7704 } else {
7705 COMPRESSION_FORMAT_NONE
7706 };
7707 let mut ignored = 0u32;
7708 if unsafe {
7711 DeviceIoControl(
7712 handle,
7713 FSCTL_SET_COMPRESSION,
7714 (&mut format as *mut u16).cast(),
7715 std::mem::size_of::<u16>() as u32,
7716 ptr::null_mut(),
7717 0,
7718 &mut ignored,
7719 ptr::null_mut(),
7720 )
7721 } == 0
7722 {
7723 let error = std::io::Error::last_os_error();
7724 record_metadata_application_failure(
7725 diagnostics,
7726 MetadataDiagnostic::new(
7727 path,
7728 "windows-backup-v1",
7729 "compression-layout",
7730 MetadataOperation::Restore,
7731 MetadataDiagnosticStatus::Materialized,
7732 if compressed {
7733 "native Windows compression could not be recreated"
7734 } else {
7735 "native Windows compression could not be removed"
7736 },
7737 )
7738 .for_restore(options.restore_policy, 4)
7739 .with_native_error(&error),
7740 options,
7741 "failed to apply native Windows compression state",
7742 )?;
7743 return Ok(false);
7744 }
7745 Ok(true)
7746}
7747
7748#[cfg(windows)]
7749fn apply_windows_directory_case_sensitive(
7750 file: &fs::File,
7751 path: &[u8],
7752 metadata: &MemberMetadata,
7753 options: SafeExtractionOptions,
7754 diagnostics: &mut Vec<MetadataDiagnostic>,
7755) -> Result<(), FormatError> {
7756 use std::os::windows::io::AsRawHandle;
7757 use windows_sys::Win32::Storage::FileSystem::{
7758 FileCaseSensitiveInfo, GetFileInformationByHandleEx, SetFileInformationByHandle,
7759 FILE_CASE_SENSITIVE_INFO,
7760 };
7761 use windows_sys::Win32::System::SystemServices::FILE_CS_FLAG_CASE_SENSITIVE_DIR;
7762
7763 let Some(encoded) = metadata
7764 .primary_records
7765 .get("TZAP.windows.directory-case-sensitive")
7766 else {
7767 return Ok(());
7768 };
7769 let desired = match encoded.as_slice() {
7770 b"0" => 0,
7771 b"1" => FILE_CS_FLAG_CASE_SENSITIVE_DIR,
7772 _ => {
7773 return Err(FormatError::InvalidArchive(
7774 "invalid Windows directory case-sensitivity state",
7775 ));
7776 }
7777 };
7778 let handle = file.as_raw_handle().cast();
7779 let mut current = FILE_CASE_SENSITIVE_INFO::default();
7780 if unsafe {
7782 GetFileInformationByHandleEx(
7783 handle,
7784 FileCaseSensitiveInfo,
7785 (&mut current as *mut FILE_CASE_SENSITIVE_INFO).cast(),
7786 std::mem::size_of::<FILE_CASE_SENSITIVE_INFO>() as u32,
7787 )
7788 } == 0
7789 {
7790 let error = std::io::Error::last_os_error();
7791 return record_metadata_application_failure(
7792 diagnostics,
7793 MetadataDiagnostic::new(
7794 path,
7795 "windows-backup-v1",
7796 "directory-case-sensitive",
7797 MetadataOperation::Restore,
7798 MetadataDiagnosticStatus::Failed,
7799 "failed to inspect Windows directory case-sensitivity state",
7800 )
7801 .for_restore(options.restore_policy, 4)
7802 .with_native_error(&error),
7803 options,
7804 "failed to inspect Windows directory case-sensitivity state",
7805 );
7806 }
7807 if current.Flags == desired {
7808 return Ok(());
7809 }
7810 if options.restore_policy != RestorePolicy::System || !options.system_authorized {
7811 return record_metadata_application_failure(
7812 diagnostics,
7813 MetadataDiagnostic::new(
7814 path,
7815 "windows-backup-v1",
7816 "directory-case-sensitive",
7817 MetadataOperation::Restore,
7818 MetadataDiagnosticStatus::Unsupported,
7819 "changing Windows directory case-sensitivity requires authorized System restore",
7820 )
7821 .for_restore(options.restore_policy, 4),
7822 options,
7823 "Windows directory case-sensitivity state requires authorized System restore",
7824 );
7825 }
7826 let updated = FILE_CASE_SENSITIVE_INFO { Flags: desired };
7827 if unsafe {
7829 SetFileInformationByHandle(
7830 handle,
7831 FileCaseSensitiveInfo,
7832 (&updated as *const FILE_CASE_SENSITIVE_INFO).cast(),
7833 std::mem::size_of::<FILE_CASE_SENSITIVE_INFO>() as u32,
7834 )
7835 } == 0
7836 {
7837 let error = std::io::Error::last_os_error();
7838 return record_metadata_application_failure(
7839 diagnostics,
7840 MetadataDiagnostic::new(
7841 path,
7842 "windows-backup-v1",
7843 "directory-case-sensitive",
7844 MetadataOperation::Restore,
7845 MetadataDiagnosticStatus::Failed,
7846 "failed to apply Windows directory case-sensitivity state",
7847 )
7848 .for_restore(options.restore_policy, 4)
7849 .with_native_error(&error),
7850 options,
7851 "failed to apply Windows directory case-sensitivity state",
7852 );
7853 }
7854 let mut actual = FILE_CASE_SENSITIVE_INFO::default();
7855 if unsafe {
7857 GetFileInformationByHandleEx(
7858 handle,
7859 FileCaseSensitiveInfo,
7860 (&mut actual as *mut FILE_CASE_SENSITIVE_INFO).cast(),
7861 std::mem::size_of::<FILE_CASE_SENSITIVE_INFO>() as u32,
7862 )
7863 } == 0
7864 || actual.Flags != desired
7865 {
7866 let error = std::io::Error::last_os_error();
7867 return record_metadata_application_failure(
7868 diagnostics,
7869 MetadataDiagnostic::new(
7870 path,
7871 "windows-backup-v1",
7872 "directory-case-sensitive",
7873 MetadataOperation::Restore,
7874 MetadataDiagnosticStatus::Failed,
7875 "Windows directory case-sensitivity state did not verify after restoration",
7876 )
7877 .for_restore(options.restore_policy, 4)
7878 .with_native_error(&error),
7879 options,
7880 "Windows directory case-sensitivity state did not verify after restoration",
7881 );
7882 }
7883 Ok(())
7884}
7885
7886#[cfg(not(windows))]
7887fn apply_windows_basic_metadata(
7888 _file: &fs::File,
7889 _path: &[u8],
7890 _metadata: &MemberMetadata,
7891 _options: SafeExtractionOptions,
7892 _diagnostics: &mut Vec<MetadataDiagnostic>,
7893) -> Result<(), FormatError> {
7894 Ok(())
7895}
7896
7897#[cfg(target_os = "linux")]
7898fn apply_linux_inode_flags(
7899 file: &fs::File,
7900 path: &[u8],
7901 metadata: &MemberMetadata,
7902 options: SafeExtractionOptions,
7903 diagnostics: &mut Vec<MetadataDiagnostic>,
7904) -> Result<(), FormatError> {
7905 if !source_os_matches_current_host(&metadata.declaration.source_os) {
7906 return Ok(());
7907 }
7908 let Some(encoded) = metadata.primary_records.get("TZAP.linux.fsflags") else {
7909 return Ok(());
7910 };
7911 let text = std::str::from_utf8(encoded)
7912 .map_err(|_| FormatError::InvalidArchive("Linux inode flags are not ASCII"))?;
7913 let desired = u64::from_str_radix(text, 16)
7914 .map_err(|_| FormatError::InvalidArchive("Linux inode flags are invalid"))?;
7915 let no_change = desired
7916 & u64::from(linux_raw_sys::general::FS_IMMUTABLE_FL | linux_raw_sys::general::FS_APPEND_FL)
7917 != 0;
7918 if !matches!(
7919 options.restore_policy,
7920 RestorePolicy::SameOs | RestorePolicy::System
7921 ) || (no_change
7922 && !(options.restore_policy == RestorePolicy::System && options.system_authorized))
7923 {
7924 return Ok(());
7925 }
7926 let apply_result = (|| -> std::io::Result<()> {
7927 if desired & !LINUX_KNOWN_FSFLAGS != 0 {
7928 return Err(std::io::Error::new(
7929 std::io::ErrorKind::Unsupported,
7930 "archive contains unrecognized Linux inode flag bits",
7931 ));
7932 }
7933 let mut current: libc::c_long = 0;
7934 if unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut current) } != 0 {
7937 return Err(std::io::Error::last_os_error());
7938 }
7939 let modifiable = u64::from(linux_raw_sys::general::FS_FL_USER_MODIFIABLE);
7940 let mut restored =
7941 ((current as u64 & !modifiable) | (desired & modifiable)) as libc::c_long;
7942 if unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_SETFLAGS, &mut restored) } != 0 {
7944 return Err(std::io::Error::last_os_error());
7945 }
7946 let mut actual: libc::c_long = 0;
7947 if unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut actual) } != 0 {
7948 return Err(std::io::Error::last_os_error());
7949 }
7950 if actual as u64 != desired {
7951 return Err(std::io::Error::other(format!(
7952 "Linux inode flags did not verify: wanted {desired:016x}, got {:016x}",
7953 actual as u64
7954 )));
7955 }
7956 Ok(())
7957 })();
7958 if apply_result.is_ok() {
7959 return Ok(());
7960 }
7961 let error = apply_result.unwrap_err();
7962 record_metadata_application_failure(
7963 diagnostics,
7964 MetadataDiagnostic::new(
7965 path,
7966 "linux-backup-v1",
7967 "inode-flags",
7968 MetadataOperation::Restore,
7969 MetadataDiagnosticStatus::Failed,
7970 "failed to apply Linux inode flags",
7971 )
7972 .for_restore(options.restore_policy, 4)
7973 .with_native_error(&error),
7974 options,
7975 "failed to apply Linux inode flags",
7976 )
7977}
7978
7979#[cfg(target_os = "linux")]
7980fn apply_linux_project_id(
7981 file: &fs::File,
7982 path: &[u8],
7983 metadata: &MemberMetadata,
7984 options: SafeExtractionOptions,
7985 diagnostics: &mut Vec<MetadataDiagnostic>,
7986) -> Result<(), FormatError> {
7987 if metadata.declaration.source_os != "linux"
7988 || options.restore_policy != RestorePolicy::System
7989 || !options.system_authorized
7990 {
7991 return Ok(());
7992 }
7993 let Some(encoded) = metadata.primary_records.get("TZAP.linux.project-id") else {
7994 return Ok(());
7995 };
7996 let desired = std::str::from_utf8(encoded)
7997 .ok()
7998 .and_then(|value| value.parse::<u32>().ok())
7999 .ok_or(FormatError::InvalidArchive("Linux project ID is invalid"))?;
8000 let mut attributes: linux_raw_sys::general::fsxattr = unsafe { std::mem::zeroed() };
8002 let get_result = unsafe {
8003 libc::ioctl(
8004 file.as_raw_fd(),
8005 linux_raw_sys::ioctl::FS_IOC_FSGETXATTR as libc::Ioctl,
8006 &mut attributes,
8007 )
8008 };
8009 if get_result == 0 {
8010 attributes.fsx_projid = desired;
8011 if unsafe {
8012 libc::ioctl(
8013 file.as_raw_fd(),
8014 linux_raw_sys::ioctl::FS_IOC_FSSETXATTR as libc::Ioctl,
8015 &attributes,
8016 )
8017 } == 0
8018 {
8019 let mut actual: linux_raw_sys::general::fsxattr = unsafe { std::mem::zeroed() };
8020 if unsafe {
8021 libc::ioctl(
8022 file.as_raw_fd(),
8023 linux_raw_sys::ioctl::FS_IOC_FSGETXATTR as libc::Ioctl,
8024 &mut actual,
8025 )
8026 } == 0
8027 && actual.fsx_projid == desired
8028 {
8029 return Ok(());
8030 }
8031 }
8032 }
8033 let error = std::io::Error::last_os_error();
8034 record_metadata_application_failure(
8035 diagnostics,
8036 MetadataDiagnostic::new(
8037 path,
8038 "linux-backup-v1",
8039 "project-id",
8040 MetadataOperation::Restore,
8041 MetadataDiagnosticStatus::Failed,
8042 "failed to apply Linux project ID",
8043 )
8044 .for_restore(options.restore_policy, 4)
8045 .with_native_error(&error),
8046 options,
8047 "failed to apply Linux project ID",
8048 )
8049}
8050
8051#[cfg(not(target_os = "linux"))]
8052fn apply_linux_project_id(
8053 _file: &fs::File,
8054 _path: &[u8],
8055 _metadata: &MemberMetadata,
8056 _options: SafeExtractionOptions,
8057 _diagnostics: &mut Vec<MetadataDiagnostic>,
8058) -> Result<(), FormatError> {
8059 Ok(())
8060}
8061
8062#[cfg(not(target_os = "linux"))]
8063fn apply_linux_inode_flags(
8064 _file: &fs::File,
8065 _path: &[u8],
8066 _metadata: &MemberMetadata,
8067 _options: SafeExtractionOptions,
8068 _diagnostics: &mut Vec<MetadataDiagnostic>,
8069) -> Result<(), FormatError> {
8070 Ok(())
8071}
8072
8073#[cfg(target_os = "linux")]
8074fn apply_regular_file_posix_acl(
8075 file: &fs::File,
8076 path: &[u8],
8077 metadata: &MemberMetadata,
8078 options: SafeExtractionOptions,
8079 diagnostics: &mut Vec<MetadataDiagnostic>,
8080) -> Result<(), FormatError> {
8081 use xattr::FileExt as _;
8082
8083 if !source_os_matches_current_host(&metadata.declaration.source_os)
8084 || !matches!(
8085 options.restore_policy,
8086 RestorePolicy::SameOs | RestorePolicy::System
8087 )
8088 {
8089 return Ok(());
8090 }
8091 for (key, name) in [
8092 ("SCHILY.acl.access", "system.posix_acl_access"),
8093 ("SCHILY.acl.default", "system.posix_acl_default"),
8094 ] {
8095 let Some(text) = metadata.primary_records.get(key) else {
8096 continue;
8097 };
8098 let value = schily_posix_acl_to_linux_xattr(text)?;
8099 if let Err(error) = file.set_xattr(name, &value) {
8100 record_metadata_application_failure(
8101 diagnostics,
8102 MetadataDiagnostic::new(
8103 path,
8104 "posix-backup-v1",
8105 "posix-acl",
8106 MetadataOperation::Restore,
8107 MetadataDiagnosticStatus::Failed,
8108 "failed to apply POSIX ACL",
8109 )
8110 .for_restore(options.restore_policy, 4)
8111 .with_native_error(&error),
8112 options,
8113 "failed to apply POSIX ACL",
8114 )?;
8115 continue;
8116 }
8117 if file.get_xattr(name).ok().flatten().as_deref() != Some(value.as_slice()) {
8118 record_metadata_application_failure(
8119 diagnostics,
8120 MetadataDiagnostic::new(
8121 path,
8122 "posix-backup-v1",
8123 "posix-acl",
8124 MetadataOperation::Restore,
8125 MetadataDiagnosticStatus::Failed,
8126 "POSIX ACL did not verify after restoration",
8127 )
8128 .for_restore(options.restore_policy, 4),
8129 options,
8130 "POSIX ACL did not verify after restoration",
8131 )?;
8132 }
8133 }
8134 Ok(())
8135}
8136
8137#[cfg(not(target_os = "linux"))]
8138fn apply_regular_file_posix_acl(
8139 _file: &fs::File,
8140 _path: &[u8],
8141 _metadata: &MemberMetadata,
8142 _options: SafeExtractionOptions,
8143 _diagnostics: &mut Vec<MetadataDiagnostic>,
8144) -> Result<(), FormatError> {
8145 Ok(())
8146}
8147
8148#[cfg(unix)]
8149fn apply_regular_file_xattrs(
8150 file: &fs::File,
8151 path: &[u8],
8152 metadata: &MemberMetadata,
8153 options: SafeExtractionOptions,
8154 diagnostics: &mut Vec<MetadataDiagnostic>,
8155) -> Result<(), FormatError> {
8156 use std::ffi::OsStr;
8157 use std::os::unix::ffi::OsStrExt;
8158 use xattr::FileExt as _;
8159
8160 if !source_os_matches_current_host(&metadata.declaration.source_os)
8161 || !matches!(
8162 options.restore_policy,
8163 RestorePolicy::SameOs | RestorePolicy::System
8164 )
8165 {
8166 return Ok(());
8167 }
8168 for (key, encoded) in metadata
8169 .primary_records
8170 .iter()
8171 .filter(|(key, _)| key.starts_with("LIBARCHIVE.xattr."))
8172 {
8173 let name = decode_percent_name(&key.as_bytes()["LIBARCHIVE.xattr.".len()..])?;
8174 let system = system_xattr_name(&name, &metadata.declaration.source_os);
8175 if system && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
8176 {
8177 continue;
8178 }
8179 let value = canonical_base64_decode(encoded)?;
8180 if let Err(error) = file.set_xattr(OsStr::from_bytes(&name), &value) {
8181 record_metadata_application_failure(
8182 diagnostics,
8183 MetadataDiagnostic::new(
8184 path,
8185 if system && metadata.declaration.source_os == "macos" {
8186 "macos-backup-v1"
8187 } else if system {
8188 "linux-backup-v1"
8189 } else {
8190 "posix-backup-v1"
8191 },
8192 "extended-attribute",
8193 MetadataOperation::Restore,
8194 MetadataDiagnosticStatus::Failed,
8195 "failed to apply extended attribute",
8196 )
8197 .for_restore(options.restore_policy, 4)
8198 .with_native_error(&error),
8199 options,
8200 "failed to apply extended attribute",
8201 )?;
8202 continue;
8203 }
8204 if file
8205 .get_xattr(OsStr::from_bytes(&name))
8206 .ok()
8207 .flatten()
8208 .as_deref()
8209 != Some(value.as_slice())
8210 {
8211 record_metadata_application_failure(
8212 diagnostics,
8213 MetadataDiagnostic::new(
8214 path,
8215 if system && metadata.declaration.source_os == "macos" {
8216 "macos-backup-v1"
8217 } else if system {
8218 "linux-backup-v1"
8219 } else {
8220 "posix-backup-v1"
8221 },
8222 "extended-attribute",
8223 MetadataOperation::Restore,
8224 MetadataDiagnosticStatus::Failed,
8225 "extended attribute did not verify after restoration",
8226 )
8227 .for_restore(options.restore_policy, 4),
8228 options,
8229 "extended attribute did not verify after restoration",
8230 )?;
8231 }
8232 }
8233 Ok(())
8234}
8235
8236#[cfg(not(unix))]
8237fn apply_regular_file_xattrs(
8238 _file: &fs::File,
8239 _path: &[u8],
8240 _metadata: &MemberMetadata,
8241 _options: SafeExtractionOptions,
8242 _diagnostics: &mut Vec<MetadataDiagnostic>,
8243) -> Result<(), FormatError> {
8244 Ok(())
8245}
8246
8247fn system_xattr_name(name: &[u8], source_os: &str) -> bool {
8248 name.starts_with(b"security.")
8249 || name.starts_with(b"trusted.")
8250 || name.starts_with(b"system.")
8251 || (source_os == "linux" && !name.starts_with(b"user.") && !name.starts_with(b"com.apple."))
8252}
8253
8254#[cfg(unix)]
8255fn apply_regular_file_ownership(
8256 file: &fs::File,
8257 path: &[u8],
8258 uid: Option<u64>,
8259 gid: Option<u64>,
8260 options: SafeExtractionOptions,
8261 diagnostics: &mut Vec<MetadataDiagnostic>,
8262) -> Result<(), FormatError> {
8263 if options.restore_policy != RestorePolicy::System || !options.system_authorized {
8264 return Ok(());
8265 }
8266 let (Some(uid), Some(gid)) = (uid, gid) else {
8267 return Ok(());
8268 };
8269 let uid = libc::uid_t::try_from(uid)
8270 .map_err(|_| FormatError::FilesystemExtractionFailed("archived UID exceeds host uid_t"))?;
8271 let gid = libc::gid_t::try_from(gid)
8272 .map_err(|_| FormatError::FilesystemExtractionFailed("archived GID exceeds host gid_t"))?;
8273
8274 if unsafe { libc::fchown(file.as_raw_fd(), uid, gid) } != 0 {
8277 let error = std::io::Error::last_os_error();
8278 return record_metadata_application_failure(
8279 diagnostics,
8280 MetadataDiagnostic::new(
8281 path,
8282 "portable-v1",
8283 "numeric-ownership",
8284 MetadataOperation::Restore,
8285 MetadataDiagnosticStatus::Failed,
8286 "failed to apply numeric ownership",
8287 )
8288 .for_restore(options.restore_policy, 4)
8289 .with_native_error(&error),
8290 options,
8291 "failed to apply numeric ownership",
8292 );
8293 }
8294 Ok(())
8295}
8296
8297#[cfg(not(unix))]
8298fn apply_regular_file_ownership(
8299 _file: &fs::File,
8300 _path: &[u8],
8301 _uid: Option<u64>,
8302 _gid: Option<u64>,
8303 _options: SafeExtractionOptions,
8304 _diagnostics: &mut Vec<MetadataDiagnostic>,
8305) -> Result<(), FormatError> {
8306 Ok(())
8307}
8308
8309#[cfg(windows)]
8310fn apply_regular_file_attributes(
8311 file: &fs::File,
8312 path: &[u8],
8313 attributes: Option<u32>,
8314 options: SafeExtractionOptions,
8315 diagnostics: &mut Vec<MetadataDiagnostic>,
8316) -> Result<(), FormatError> {
8317 let Some(attributes) = attributes else {
8318 return Ok(());
8319 };
8320 let metadata = match file.metadata() {
8321 Ok(metadata) => metadata,
8322 Err(error) => {
8323 return record_metadata_application_failure(
8324 diagnostics,
8325 MetadataDiagnostic::new(
8326 path,
8327 "portable-v1",
8328 "portable-attributes",
8329 MetadataOperation::Restore,
8330 MetadataDiagnosticStatus::Failed,
8331 "failed to inspect file before applying readonly attribute projection",
8332 )
8333 .for_restore(options.restore_policy, 4)
8334 .with_native_error(&error),
8335 options,
8336 "failed to inspect file before applying readonly attribute projection",
8337 );
8338 }
8339 };
8340 let mut permissions = metadata.permissions();
8341 permissions.set_readonly(attributes & 1 != 0);
8342 if let Err(error) = file.set_permissions(permissions) {
8343 return record_metadata_application_failure(
8344 diagnostics,
8345 MetadataDiagnostic::new(
8346 path,
8347 "portable-v1",
8348 "portable-attributes",
8349 MetadataOperation::Restore,
8350 MetadataDiagnosticStatus::Failed,
8351 "failed to apply readonly attribute projection",
8352 )
8353 .for_restore(options.restore_policy, 4)
8354 .with_native_error(&error),
8355 options,
8356 "failed to apply readonly attribute projection",
8357 );
8358 }
8359 Ok(())
8360}
8361
8362#[cfg(not(windows))]
8363fn apply_regular_file_attributes(
8364 _file: &fs::File,
8365 _path: &[u8],
8366 _attributes: Option<u32>,
8367 _options: SafeExtractionOptions,
8368 _diagnostics: &mut Vec<MetadataDiagnostic>,
8369) -> Result<(), FormatError> {
8370 Ok(())
8371}
8372
8373#[cfg(unix)]
8374fn apply_regular_file_mode(
8375 file: &fs::File,
8376 path: &[u8],
8377 mode: u32,
8378 _mode_origin_native: bool,
8379 options: SafeExtractionOptions,
8380 diagnostics: &mut Vec<MetadataDiagnostic>,
8381) -> Result<(), FormatError> {
8382 match file.metadata() {
8383 Ok(metadata) => {
8384 let mut permissions = metadata.permissions();
8385 permissions.set_mode(mode & 0o7777);
8386 if let Err(error) = file.set_permissions(permissions) {
8387 return record_metadata_application_failure(
8388 diagnostics,
8389 MetadataDiagnostic::new(
8390 path,
8391 "portable-v1",
8392 "mode",
8393 MetadataOperation::Restore,
8394 MetadataDiagnosticStatus::Failed,
8395 "failed to apply mode metadata",
8396 )
8397 .for_restore(options.restore_policy, 4)
8398 .with_native_error(&error),
8399 options,
8400 "failed to apply mode metadata",
8401 );
8402 }
8403 }
8404 Err(error) => {
8405 return record_metadata_application_failure(
8406 diagnostics,
8407 MetadataDiagnostic::new(
8408 path,
8409 "portable-v1",
8410 "mode",
8411 MetadataOperation::Restore,
8412 MetadataDiagnosticStatus::Failed,
8413 "failed to inspect file before applying mode metadata",
8414 )
8415 .for_restore(options.restore_policy, 4)
8416 .with_native_error(&error),
8417 options,
8418 "failed to inspect file before applying mode metadata",
8419 );
8420 }
8421 }
8422 Ok(())
8423}
8424
8425#[cfg(not(unix))]
8426fn apply_regular_file_mode(
8427 file: &fs::File,
8428 path: &[u8],
8429 mode: u32,
8430 mode_origin_native: bool,
8431 options: SafeExtractionOptions,
8432 diagnostics: &mut Vec<MetadataDiagnostic>,
8433) -> Result<(), FormatError> {
8434 match file.metadata() {
8435 Ok(metadata) => {
8436 let mut permissions = metadata.permissions();
8437 permissions.set_readonly(mode & 0o222 == 0);
8438 if let Err(error) = file.set_permissions(permissions) {
8439 return record_metadata_application_failure(
8440 diagnostics,
8441 MetadataDiagnostic::new(
8442 path,
8443 "portable-v1",
8444 "mode",
8445 MetadataOperation::Restore,
8446 MetadataDiagnosticStatus::Failed,
8447 "failed to apply mode metadata",
8448 )
8449 .for_restore(options.restore_policy, 4)
8450 .with_native_error(&error),
8451 options,
8452 "failed to apply mode metadata",
8453 );
8454 }
8455 if mode_origin_native && mode & 0o777 != 0o444 && mode & 0o777 != 0o666 {
8456 let diagnostic = MetadataDiagnostic::new(
8457 path,
8458 "portable-v1",
8459 "mode",
8460 MetadataOperation::Restore,
8461 MetadataDiagnosticStatus::Partial,
8462 "mode metadata was only partially applied on this platform",
8463 )
8464 .for_restore(options.restore_policy, 4);
8465 if options.allow_degraded {
8466 diagnostics.push(diagnostic);
8467 } else {
8468 return Err(FormatError::FilesystemExtractionFailed(
8469 "portable mode cannot be represented exactly on this host",
8470 ));
8471 }
8472 }
8473 }
8474 Err(error) => {
8475 return record_metadata_application_failure(
8476 diagnostics,
8477 MetadataDiagnostic::new(
8478 path,
8479 "portable-v1",
8480 "mode",
8481 MetadataOperation::Restore,
8482 MetadataDiagnosticStatus::Failed,
8483 "failed to inspect file before applying mode metadata",
8484 )
8485 .for_restore(options.restore_policy, 4)
8486 .with_native_error(&error),
8487 options,
8488 "failed to inspect file before applying mode metadata",
8489 );
8490 }
8491 }
8492 Ok(())
8493}
8494
8495fn apply_regular_file_mtime(
8496 file: &fs::File,
8497 path: &[u8],
8498 (seconds, nanoseconds): (i64, u32),
8499 options: SafeExtractionOptions,
8500 diagnostics: &mut Vec<MetadataDiagnostic>,
8501) -> Result<(), FormatError> {
8502 let duration = Duration::new(seconds.unsigned_abs(), nanoseconds);
8503 let modified = if seconds < 0 {
8504 SystemTime::UNIX_EPOCH.checked_sub(duration)
8505 } else {
8506 SystemTime::UNIX_EPOCH.checked_add(duration)
8507 };
8508 let Some(modified) = modified else {
8509 return record_metadata_application_failure(
8510 diagnostics,
8511 MetadataDiagnostic::new(
8512 path,
8513 "portable-v1",
8514 "mtime",
8515 MetadataOperation::Restore,
8516 MetadataDiagnosticStatus::Failed,
8517 "failed to apply mtime metadata",
8518 )
8519 .for_restore(options.restore_policy, 4),
8520 options,
8521 "mtime cannot be represented on this host",
8522 );
8523 };
8524 let times = fs::FileTimes::new().set_modified(modified);
8525 if let Err(error) = file.set_times(times) {
8526 return record_metadata_application_failure(
8527 diagnostics,
8528 MetadataDiagnostic::new(
8529 path,
8530 "portable-v1",
8531 "mtime",
8532 MetadataOperation::Restore,
8533 MetadataDiagnosticStatus::Failed,
8534 "failed to apply mtime metadata",
8535 )
8536 .for_restore(options.restore_policy, 4)
8537 .with_native_error(&error),
8538 options,
8539 "failed to apply mtime metadata",
8540 );
8541 }
8542 Ok(())
8543}
8544
8545fn record_metadata_application_failure(
8546 diagnostics: &mut Vec<MetadataDiagnostic>,
8547 diagnostic: MetadataDiagnostic,
8548 options: SafeExtractionOptions,
8549 strict_error: &'static str,
8550) -> Result<(), FormatError> {
8551 if options.allow_degraded {
8552 diagnostics.push(diagnostic);
8553 Ok(())
8554 } else {
8555 Err(FormatError::FilesystemExtractionFailed(strict_error))
8556 }
8557}
8558
8559pub(crate) fn validate_symlink_target(link_path: &[u8], target: &[u8]) -> Result<(), FormatError> {
8560 if target.is_empty()
8561 || target.contains(&0)
8562 || target.contains(&b'\\')
8563 || target.contains(&b':')
8564 || target[0] == b'/'
8565 {
8566 return Err(FormatError::UnsafeArchivePath);
8567 }
8568 let target = std::str::from_utf8(target).map_err(|_| FormatError::UnsafeArchivePath)?;
8569 let link_path = std::str::from_utf8(link_path).map_err(|_| FormatError::UnsafeArchivePath)?;
8570 if target.nfc().collect::<String>() != target {
8571 return Err(FormatError::UnsafeArchivePath);
8572 }
8573 let mut stack = link_path
8574 .split('/')
8575 .take(link_path.split('/').count().saturating_sub(1))
8576 .map(str::to_owned)
8577 .collect::<Vec<_>>();
8578 for component in target.split('/') {
8579 if component.is_empty() || component == "." {
8580 return Err(FormatError::UnsafeArchivePath);
8581 }
8582 if component == ".." {
8583 if stack.pop().is_none() {
8584 return Err(FormatError::UnsafeArchivePath);
8585 }
8586 } else {
8587 validate_file_path_bytes(component.as_bytes(), u32::MAX)?;
8588 stack.push(component.to_owned());
8589 }
8590 }
8591 Ok(())
8592}
8593
8594struct PreparedDestination {
8595 parent: CapDir,
8596 leaf: PathBuf,
8597}
8598
8599fn prepare_destination(
8600 root: &Path,
8601 archive_path: &[u8],
8602 kind: TarEntryKind,
8603 options: SafeExtractionOptions,
8604) -> Result<PreparedDestination, FormatError> {
8605 let components = path_components(archive_path)?;
8606 let mut parent = open_extraction_root(root)?;
8607 for component in &components[..components.len().saturating_sub(1)] {
8608 parent = open_or_create_safe_child_dir(&parent, component)?;
8609 }
8610
8611 let leaf = PathBuf::from(components.last().ok_or(FormatError::UnsafeArchivePath)?);
8612 match parent.symlink_metadata(&leaf) {
8613 Ok(metadata) => {
8614 let file_type = metadata.file_type();
8615 if file_type.is_symlink() {
8616 return Err(FormatError::UnsafeArchivePath);
8617 }
8618 if kind == TarEntryKind::Directory {
8619 if file_type.is_dir() {
8620 return Ok(PreparedDestination { parent, leaf });
8621 }
8622 return Err(FormatError::UnsafeOverwrite);
8623 }
8624 if file_type.is_dir() {
8625 return Err(FormatError::UnsafeOverwrite);
8626 }
8627 if !options.overwrite_existing {
8628 return Err(FormatError::UnsafeOverwrite);
8629 }
8630 }
8631 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
8632 Err(_) => {
8633 return Err(FormatError::FilesystemExtractionFailed(
8634 "failed to inspect destination",
8635 ));
8636 }
8637 }
8638 Ok(PreparedDestination { parent, leaf })
8639}
8640
8641fn open_extraction_root(root: &Path) -> Result<CapDir, FormatError> {
8642 let metadata = fs::symlink_metadata(root).map_err(|_| {
8643 FormatError::FilesystemExtractionFailed("extraction root must already exist")
8644 })?;
8645 if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
8646 return Err(FormatError::UnsafeArchivePath);
8647 }
8648 CapDir::open_ambient_dir(root, ambient_authority())
8649 .map_err(|_| FormatError::FilesystemExtractionFailed("extraction root must already exist"))
8650}
8651
8652fn open_or_create_safe_child_dir(parent: &CapDir, component: &str) -> Result<CapDir, FormatError> {
8653 match parent.open_dir_nofollow(component) {
8654 Ok(child) => return Ok(child),
8655 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
8656 Err(_) => return Err(FormatError::UnsafeArchivePath),
8657 }
8658
8659 match parent.create_dir(component) {
8660 Ok(()) => {}
8661 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
8662 Err(_) => {
8663 return Err(FormatError::FilesystemExtractionFailed(
8664 "failed to create parent directory",
8665 ));
8666 }
8667 }
8668 parent
8669 .open_dir_nofollow(component)
8670 .map_err(|_| FormatError::UnsafeArchivePath)
8671}
8672
8673fn existing_safe_regular_path(
8674 root: &Path,
8675 archive_path: &[u8],
8676) -> Result<PreparedDestination, FormatError> {
8677 validate_file_path_bytes(archive_path, u32::MAX)?;
8678 let components = path_components(archive_path)?;
8679 let mut parent = open_extraction_root(root)?;
8680 for component in &components[..components.len().saturating_sub(1)] {
8681 parent = parent
8682 .open_dir_nofollow(component)
8683 .map_err(|_| FormatError::UnsafeArchivePath)?;
8684 }
8685
8686 let leaf = PathBuf::from(components.last().ok_or(FormatError::UnsafeArchivePath)?);
8687 let metadata = parent
8688 .symlink_metadata(&leaf)
8689 .map_err(|_| FormatError::UnsafeArchivePath)?;
8690 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
8691 return Err(FormatError::UnsafeArchivePath);
8692 }
8693 Ok(PreparedDestination { parent, leaf })
8694}
8695
8696#[cfg(windows)]
8697fn existing_safe_windows_reparse_path(
8698 root: &Path,
8699 archive_path: &[u8],
8700) -> Result<PreparedDestination, FormatError> {
8701 validate_file_path_bytes(archive_path, u32::MAX)?;
8702 let components = path_components(archive_path)?;
8703 let mut parent = open_extraction_root(root)?;
8704 for component in &components[..components.len().saturating_sub(1)] {
8705 parent = parent
8706 .open_dir_nofollow(component)
8707 .map_err(|_| FormatError::UnsafeArchivePath)?;
8708 }
8709
8710 let leaf = PathBuf::from(components.last().ok_or(FormatError::UnsafeArchivePath)?);
8711 let destination = PreparedDestination { parent, leaf };
8712 drop(open_existing_windows_reparse(&destination)?);
8717 Ok(destination)
8718}
8719
8720fn create_new_file_options() -> CapOpenOptions {
8721 let mut options = CapOpenOptions::new();
8722 options
8723 .read(true)
8724 .write(true)
8725 .create_new(true)
8726 .follow(FollowSymlinks::No);
8727 #[cfg(windows)]
8728 options.access_mode(FILE_GENERIC_READ | FILE_GENERIC_WRITE | DELETE);
8729 options
8730}
8731
8732fn open_existing_regular_file(target: &PreparedDestination) -> Result<fs::File, FormatError> {
8733 let mut options = CapOpenOptions::new();
8734 options.read(true).follow(FollowSymlinks::No);
8735 target
8736 .parent
8737 .open_with(&target.leaf, &options)
8738 .map(cap_std::fs::File::into_std)
8739 .map_err(|_| {
8740 FormatError::FilesystemExtractionFailed(
8741 "failed to open hardlink target for materialization",
8742 )
8743 })
8744}
8745
8746fn open_existing_directory(target: &PreparedDestination) -> Result<fs::File, FormatError> {
8747 #[cfg(windows)]
8748 {
8749 let mut options = CapOpenOptions::new();
8750 options
8751 .access_mode(FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES)
8752 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
8753 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
8754 .follow(FollowSymlinks::No);
8755 let directory = target
8756 .parent
8757 .open_with(&target.leaf, &options)
8758 .map(cap_std::fs::File::into_std)
8759 .map_err(|_| {
8760 FormatError::FilesystemExtractionFailed(
8761 "failed to open directory for metadata restoration",
8762 )
8763 })?;
8764 let metadata = directory.metadata().map_err(|_| {
8765 FormatError::FilesystemExtractionFailed(
8766 "failed to inspect directory for metadata restoration",
8767 )
8768 })?;
8769 if !metadata.is_dir() || metadata.file_type().is_symlink() {
8770 return Err(FormatError::UnsafeArchivePath);
8771 }
8772 Ok(directory)
8773 }
8774
8775 #[cfg(not(windows))]
8776 let directory = target.parent.open_dir_nofollow(&target.leaf).map_err(|_| {
8777 FormatError::FilesystemExtractionFailed("failed to open directory for metadata restoration")
8778 })?;
8779 #[cfg(unix)]
8780 {
8781 directory
8782 .open(".")
8783 .map(cap_std::fs::File::into_std)
8784 .map_err(|_| {
8785 FormatError::FilesystemExtractionFailed(
8786 "failed to reopen directory for metadata restoration",
8787 )
8788 })
8789 }
8790 #[cfg(all(not(unix), not(windows)))]
8791 {
8792 Ok(directory.into_std_file())
8793 }
8794}
8795
8796#[cfg(windows)]
8797fn open_existing_windows_reparse(target: &PreparedDestination) -> Result<fs::File, FormatError> {
8798 let mut options = CapOpenOptions::new();
8799 options
8800 .access_mode(FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES)
8801 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
8802 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
8803 .follow(FollowSymlinks::No);
8804 let reparse = target
8805 .parent
8806 .open_with(&target.leaf, &options)
8807 .map(cap_std::fs::File::into_std)
8808 .map_err(|_| {
8809 FormatError::FilesystemExtractionFailed(
8810 "failed to open Windows reparse object for metadata restoration",
8811 )
8812 })?;
8813 let mut basic = FILE_BASIC_INFO::default();
8814 if unsafe {
8816 GetFileInformationByHandleEx(
8817 reparse.as_raw_handle().cast(),
8818 FileBasicInfo,
8819 (&mut basic as *mut FILE_BASIC_INFO).cast(),
8820 std::mem::size_of::<FILE_BASIC_INFO>() as u32,
8821 )
8822 } == 0
8823 || basic.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == 0
8824 {
8825 return Err(FormatError::UnsafeArchivePath);
8826 }
8827 Ok(reparse)
8828}
8829
8830fn apply_restored_directory_metadata(
8831 root: &Path,
8832 path: &[u8],
8833 metadata: &MemberMetadata,
8834 staged_auxiliary: Option<&mut Vec<StagedAuxiliary>>,
8835 options: SafeExtractionOptions,
8836 diagnostics: &mut Vec<MetadataDiagnostic>,
8837) -> Result<(), FormatError> {
8838 #[cfg(windows)]
8839 let exact_reparse = options.restore_policy == RestorePolicy::System
8840 && options.system_authorized
8841 && windows_reparse_metadata_supported(metadata);
8842 #[cfg(windows)]
8843 let destination = if exact_reparse {
8844 existing_safe_windows_reparse_path(root, path)?
8845 } else {
8846 prepare_destination(root, path, TarEntryKind::Directory, options)?
8847 };
8848 #[cfg(not(windows))]
8849 let destination = prepare_destination(root, path, TarEntryKind::Directory, options)?;
8850 #[cfg(windows)]
8851 let directory = if exact_reparse {
8852 open_existing_windows_reparse(&destination)?
8853 } else {
8854 open_existing_directory(&destination)?
8855 };
8856 #[cfg(not(windows))]
8857 let directory = open_existing_directory(&destination)?;
8858 apply_restored_regular_file_metadata_parts(
8859 &directory,
8860 path,
8861 RestoredRegularMetadata::from(&metadata.portable_mirror),
8862 Some(metadata),
8863 staged_auxiliary,
8864 options,
8865 diagnostics,
8866 )
8867}
8868
8869pub(crate) fn finalize_committed_directory_metadata(
8870 root: &Path,
8871 members: &mut [TarStreamMemberSummary],
8872 merged_directory_paths: &[Vec<u8>],
8873 options: SafeExtractionOptions,
8874) -> Result<(), FormatError> {
8875 if options.restore_policy == RestorePolicy::Content {
8876 return Ok(());
8877 }
8878 let mut directory_indices = members
8879 .iter()
8880 .enumerate()
8881 .filter_map(|(index, member)| {
8882 (member.kind == TarEntryKind::Directory
8883 && merged_directory_paths.contains(&member.path))
8884 .then_some(index)
8885 })
8886 .collect::<Vec<_>>();
8887 directory_indices.sort_by(|left, right| {
8888 let left_path = &members[*left].path;
8889 let right_path = &members[*right].path;
8890 right_path
8891 .iter()
8892 .filter(|byte| **byte == b'/')
8893 .count()
8894 .cmp(&left_path.iter().filter(|byte| **byte == b'/').count())
8895 .then_with(|| left_path.cmp(right_path))
8896 });
8897 for index in directory_indices {
8898 let member = &mut members[index];
8899 apply_restored_directory_metadata(
8900 root,
8901 &member.path,
8902 &member.v45_metadata,
8903 None,
8904 options,
8905 &mut member.diagnostics,
8906 )?;
8907 }
8908 Ok(())
8909}
8910
8911fn apply_restored_symlink_mtime(
8912 destination: &PreparedDestination,
8913 path: &[u8],
8914 (seconds, nanoseconds): (i64, u32),
8915 options: SafeExtractionOptions,
8916 diagnostics: &mut Vec<MetadataDiagnostic>,
8917) -> Result<(), FormatError> {
8918 let duration = Duration::new(seconds.unsigned_abs(), nanoseconds);
8919 let modified = if seconds < 0 {
8920 SystemTime::UNIX_EPOCH.checked_sub(duration)
8921 } else {
8922 SystemTime::UNIX_EPOCH.checked_add(duration)
8923 };
8924 let Some(modified) = modified else {
8925 return record_metadata_application_failure(
8926 diagnostics,
8927 MetadataDiagnostic::new(
8928 path,
8929 "portable-v1",
8930 "mtime",
8931 MetadataOperation::Restore,
8932 MetadataDiagnosticStatus::Failed,
8933 "failed to apply symlink mtime metadata",
8934 )
8935 .for_restore(options.restore_policy, 4),
8936 options,
8937 "symlink mtime cannot be represented on this host",
8938 );
8939 };
8940 if let Err(error) = destination.parent.set_symlink_times(
8941 &destination.leaf,
8942 None,
8943 Some(SystemTimeSpec::Absolute(
8944 cap_std::time::SystemTime::from_std(modified),
8945 )),
8946 ) {
8947 return record_metadata_application_failure(
8948 diagnostics,
8949 MetadataDiagnostic::new(
8950 path,
8951 "portable-v1",
8952 "mtime",
8953 MetadataOperation::Restore,
8954 MetadataDiagnosticStatus::Failed,
8955 "failed to apply symlink mtime metadata",
8956 )
8957 .for_restore(options.restore_policy, 4)
8958 .with_native_error(&error),
8959 options,
8960 "failed to apply symlink mtime metadata",
8961 );
8962 }
8963 Ok(())
8964}
8965
8966#[cfg(target_os = "linux")]
8967fn apply_restored_linux_symlink_metadata(
8968 destination: &PreparedDestination,
8969 path: &[u8],
8970 metadata: &MemberMetadata,
8971 options: SafeExtractionOptions,
8972 diagnostics: &mut Vec<MetadataDiagnostic>,
8973) -> Result<(), FormatError> {
8974 use std::ffi::{CString, OsStr};
8975 use std::os::unix::ffi::OsStrExt;
8976
8977 if metadata.declaration.source_os != "linux"
8978 || !matches!(
8979 options.restore_policy,
8980 RestorePolicy::SameOs | RestorePolicy::System
8981 )
8982 {
8983 return Ok(());
8984 }
8985 let leaf = destination.leaf.as_os_str().as_bytes();
8986 let leaf_c = CString::new(leaf).map_err(|_| FormatError::UnsafeArchivePath)?;
8987 let current = destination
8988 .parent
8989 .symlink_metadata(&destination.leaf)
8990 .map_err(|_| FormatError::UnsafeArchivePath)?;
8991 if !current.file_type().is_symlink() {
8992 return Err(FormatError::UnsafeArchivePath);
8993 }
8994
8995 if options.restore_policy == RestorePolicy::System && options.system_authorized {
8996 if let (Some(uid), Some(gid)) = (metadata.portable_mirror.uid, metadata.portable_mirror.gid)
8997 {
8998 let uid = libc::uid_t::try_from(uid).map_err(|_| {
8999 FormatError::FilesystemExtractionFailed("archived UID exceeds host uid_t")
9000 })?;
9001 let gid = libc::gid_t::try_from(gid).map_err(|_| {
9002 FormatError::FilesystemExtractionFailed("archived GID exceeds host gid_t")
9003 })?;
9004 if unsafe {
9006 libc::fchownat(
9007 destination.parent.as_raw_fd(),
9008 leaf_c.as_ptr(),
9009 uid,
9010 gid,
9011 libc::AT_SYMLINK_NOFOLLOW,
9012 )
9013 } != 0
9014 {
9015 let error = std::io::Error::last_os_error();
9016 record_metadata_application_failure(
9017 diagnostics,
9018 MetadataDiagnostic::new(
9019 path,
9020 "portable-v1",
9021 "numeric-ownership",
9022 MetadataOperation::Restore,
9023 MetadataDiagnosticStatus::Failed,
9024 "failed to apply symlink numeric ownership",
9025 )
9026 .for_restore(options.restore_policy, 4)
9027 .with_native_error(&error),
9028 options,
9029 "failed to apply symlink numeric ownership",
9030 )?;
9031 }
9032 }
9033 }
9034
9035 let mut proc_path = PathBuf::from(format!("/proc/self/fd/{}", destination.parent.as_raw_fd()));
9036 proc_path.push(&destination.leaf);
9037 for (key, encoded) in metadata
9038 .primary_records
9039 .iter()
9040 .filter(|(key, _)| key.starts_with("LIBARCHIVE.xattr."))
9041 {
9042 let name = decode_percent_name(&key.as_bytes()["LIBARCHIVE.xattr.".len()..])?;
9043 let system = system_xattr_name(&name, "linux");
9044 if system && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
9045 {
9046 continue;
9047 }
9048 let value = canonical_base64_decode(encoded)?;
9049 let name = OsStr::from_bytes(&name);
9050 if let Err(error) = xattr::set(&proc_path, name, &value) {
9051 record_metadata_application_failure(
9052 diagnostics,
9053 MetadataDiagnostic::new(
9054 path,
9055 if system {
9056 "linux-backup-v1"
9057 } else {
9058 "posix-backup-v1"
9059 },
9060 "extended-attribute",
9061 MetadataOperation::Restore,
9062 MetadataDiagnosticStatus::Failed,
9063 "failed to apply symlink extended attribute",
9064 )
9065 .for_restore(options.restore_policy, 4)
9066 .with_native_error(&error),
9067 options,
9068 "failed to apply symlink extended attribute",
9069 )?;
9070 continue;
9071 }
9072 if xattr::get(&proc_path, name).ok().flatten().as_deref() != Some(value.as_slice()) {
9073 record_metadata_application_failure(
9074 diagnostics,
9075 MetadataDiagnostic::new(
9076 path,
9077 if system {
9078 "linux-backup-v1"
9079 } else {
9080 "posix-backup-v1"
9081 },
9082 "extended-attribute",
9083 MetadataOperation::Restore,
9084 MetadataDiagnosticStatus::Failed,
9085 "symlink extended attribute did not verify after restoration",
9086 )
9087 .for_restore(options.restore_policy, 4),
9088 options,
9089 "symlink extended attribute did not verify after restoration",
9090 )?;
9091 }
9092 }
9093 Ok(())
9094}
9095
9096#[cfg(not(target_os = "linux"))]
9097fn apply_restored_linux_symlink_metadata(
9098 _destination: &PreparedDestination,
9099 _path: &[u8],
9100 _metadata: &MemberMetadata,
9101 _options: SafeExtractionOptions,
9102 _diagnostics: &mut Vec<MetadataDiagnostic>,
9103) -> Result<(), FormatError> {
9104 Ok(())
9105}
9106
9107#[cfg(target_os = "macos")]
9108fn apply_restored_macos_symlink_metadata(
9109 destination: &PreparedDestination,
9110 path: &[u8],
9111 metadata: &MemberMetadata,
9112 staged: &mut Vec<StagedAuxiliary>,
9113 options: SafeExtractionOptions,
9114 diagnostics: &mut Vec<MetadataDiagnostic>,
9115) -> Result<(), FormatError> {
9116 use std::ffi::{c_char, c_int, c_void, CString};
9117 use std::os::fd::{FromRawFd as _, OwnedFd};
9118 use std::os::unix::ffi::OsStrExt as _;
9119
9120 if metadata.declaration.source_os != "macos"
9121 || !matches!(
9122 options.restore_policy,
9123 RestorePolicy::SameOs | RestorePolicy::System
9124 )
9125 {
9126 return Ok(());
9127 }
9128 let current = destination
9129 .parent
9130 .symlink_metadata(&destination.leaf)
9131 .map_err(|_| FormatError::UnsafeArchivePath)?;
9132 if !current.file_type().is_symlink() {
9133 return Err(FormatError::UnsafeArchivePath);
9134 }
9135 let leaf = destination.leaf.as_os_str().as_bytes();
9136 let leaf_c = CString::new(leaf).map_err(|_| FormatError::UnsafeArchivePath)?;
9137 const O_SYMLINK: c_int = 0x0020_0000;
9138 let link_fd = unsafe {
9140 libc::openat(
9141 destination.parent.as_raw_fd(),
9142 leaf_c.as_ptr(),
9143 libc::O_RDONLY | libc::O_CLOEXEC | O_SYMLINK | 0x0000_1000,
9144 )
9145 };
9146 if link_fd < 0 {
9147 return Err(FormatError::UnsafeArchivePath);
9148 }
9149 let link_fd = unsafe { OwnedFd::from_raw_fd(link_fd) };
9151 let mut pinned_stat = std::mem::MaybeUninit::<libc::stat>::uninit();
9152 if unsafe { libc::fstat(link_fd.as_raw_fd(), pinned_stat.as_mut_ptr()) } != 0
9153 || unsafe { pinned_stat.assume_init() }.st_mode & libc::S_IFMT != libc::S_IFLNK
9154 {
9155 return Err(FormatError::UnsafeArchivePath);
9156 }
9157
9158 extern "C" {
9159 fn fgetxattr(
9160 fd: c_int,
9161 name: *const c_char,
9162 value: *mut c_void,
9163 size: usize,
9164 position: u32,
9165 options: c_int,
9166 ) -> libc::ssize_t;
9167 fn fsetxattr(
9168 fd: c_int,
9169 name: *const c_char,
9170 value: *const c_void,
9171 size: usize,
9172 position: u32,
9173 options: c_int,
9174 ) -> c_int;
9175 fn fremovexattr(fd: c_int, name: *const c_char, options: c_int) -> c_int;
9176 fn acl_copy_int(buffer: *const c_void) -> *mut c_void;
9177 fn acl_copy_ext(
9178 buffer: *mut c_void,
9179 acl: *mut c_void,
9180 size: libc::ssize_t,
9181 ) -> libc::ssize_t;
9182 fn acl_size(acl: *mut c_void) -> libc::ssize_t;
9183 fn acl_set_fd_np(fd: c_int, acl: *mut c_void, acl_type: c_int) -> c_int;
9184 fn acl_get_fd_np(fd: c_int, acl_type: c_int) -> *mut c_void;
9185 fn acl_free(object: *mut c_void) -> c_int;
9186 fn fsetattrlist(
9187 fd: c_int,
9188 attributes: *const c_void,
9189 buffer: *const c_void,
9190 size: usize,
9191 options: u32,
9192 ) -> c_int;
9193 fn fchflags(fd: c_int, flags: u32) -> c_int;
9194 }
9195 const XATTR_CREATE: c_int = 0x0002;
9196 const ACL_TYPE_EXTENDED: c_int = 0x0000_0100;
9197 const RESOURCE_FORK: &[u8] = b"com.apple.ResourceFork\0";
9198 const FINDER_INFO: &[u8] = b"com.apple.FinderInfo\0";
9199
9200 let fail = |diagnostics: &mut Vec<MetadataDiagnostic>,
9201 class: &'static str,
9202 message: &'static str,
9203 error: Option<&std::io::Error>| {
9204 let mut diagnostic = MetadataDiagnostic::new(
9205 path,
9206 "macos-backup-v1",
9207 class,
9208 MetadataOperation::Restore,
9209 MetadataDiagnosticStatus::Failed,
9210 message,
9211 )
9212 .for_restore(options.restore_policy, 4);
9213 if let Some(error) = error {
9214 diagnostic = diagnostic.with_native_error(error);
9215 }
9216 record_metadata_application_failure(diagnostics, diagnostic, options, message)
9217 };
9218
9219 if options.restore_policy == RestorePolicy::System && options.system_authorized {
9220 if let (Some(uid), Some(gid)) = (metadata.portable_mirror.uid, metadata.portable_mirror.gid)
9221 {
9222 let uid = libc::uid_t::try_from(uid).map_err(|_| {
9223 FormatError::FilesystemExtractionFailed("archived UID exceeds host uid_t")
9224 })?;
9225 let gid = libc::gid_t::try_from(gid).map_err(|_| {
9226 FormatError::FilesystemExtractionFailed("archived GID exceeds host gid_t")
9227 })?;
9228 if unsafe { libc::fchown(link_fd.as_raw_fd(), uid, gid) } != 0 {
9229 let error = std::io::Error::last_os_error();
9230 fail(
9231 diagnostics,
9232 "numeric-ownership",
9233 "failed to apply macOS symlink ownership",
9234 Some(&error),
9235 )?;
9236 }
9237 }
9238 }
9239
9240 let mut items = std::mem::take(staged);
9241 items.sort_by_key(|item| match item.record.kind.as_str() {
9242 "macos.resource-fork" => 0,
9243 "macos.acl-native" => 1,
9244 "macos.finder-info" => 2,
9245 "generic.xattr" => 3,
9246 _ => 4,
9247 });
9248 let mut remaining = Vec::new();
9249 for mut item in items {
9250 if item.record.restore_class == RestoreClass::System
9251 && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
9252 {
9253 continue;
9254 }
9255 match item.record.kind.as_str() {
9256 "macos.resource-fork" => {
9257 let name = RESOURCE_FORK.as_ptr().cast::<c_char>();
9258 if unsafe { fremovexattr(link_fd.as_raw_fd(), name, 0) } != 0 {
9259 let error = std::io::Error::last_os_error();
9260 if error.raw_os_error() != Some(libc::ENOATTR) {
9261 fail(
9262 diagnostics,
9263 "resource-fork",
9264 "failed to replace macOS symlink resource fork",
9265 Some(&error),
9266 )?;
9267 continue;
9268 }
9269 }
9270 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
9271 FormatError::FilesystemExtractionFailed(
9272 "failed to rewind staged macOS symlink resource fork",
9273 )
9274 })?;
9275 let mut offset = 0u64;
9276 let mut buffer = vec![0u8; 1024 * 1024];
9277 if item.record.logical_size == 0
9278 && unsafe {
9279 fsetxattr(
9280 link_fd.as_raw_fd(),
9281 name,
9282 std::ptr::null(),
9283 0,
9284 0,
9285 XATTR_CREATE,
9286 )
9287 } != 0
9288 {
9289 let error = std::io::Error::last_os_error();
9290 fail(
9291 diagnostics,
9292 "resource-fork",
9293 "failed to create macOS symlink resource fork",
9294 Some(&error),
9295 )?;
9296 continue;
9297 }
9298 while offset < item.record.logical_size {
9299 let count = usize::try_from(
9300 (item.record.logical_size - offset).min(buffer.len() as u64),
9301 )
9302 .unwrap();
9303 item.file.read_exact(&mut buffer[..count]).map_err(|_| {
9304 FormatError::FilesystemExtractionFailed(
9305 "failed to read staged macOS symlink resource fork",
9306 )
9307 })?;
9308 if unsafe {
9309 fsetxattr(
9310 link_fd.as_raw_fd(),
9311 name,
9312 buffer.as_ptr().cast(),
9313 count,
9314 u32::try_from(offset).map_err(|_| {
9315 FormatError::ReaderUnsupported(
9316 "macOS resource fork exceeds Darwin xattr position range",
9317 )
9318 })?,
9319 if offset == 0 { XATTR_CREATE } else { 0 },
9320 )
9321 } != 0
9322 {
9323 let error = std::io::Error::last_os_error();
9324 fail(
9325 diagnostics,
9326 "resource-fork",
9327 "failed to write macOS symlink resource fork",
9328 Some(&error),
9329 )?;
9330 break;
9331 }
9332 offset += count as u64;
9333 }
9334 let actual =
9335 unsafe { fgetxattr(link_fd.as_raw_fd(), name, std::ptr::null_mut(), 0, 0, 0) };
9336 if actual < 0 || actual as u64 != item.record.logical_size {
9337 fail(
9338 diagnostics,
9339 "resource-fork",
9340 "macOS symlink resource fork did not verify after restoration",
9341 None,
9342 )?;
9343 } else {
9344 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
9345 FormatError::FilesystemExtractionFailed(
9346 "failed to rewind staged macOS symlink resource fork",
9347 )
9348 })?;
9349 let mut expected = vec![0u8; 1024 * 1024];
9350 let mut restored = vec![0u8; 1024 * 1024];
9351 let mut verify_offset = 0u64;
9352 while verify_offset < item.record.logical_size {
9353 let count = usize::try_from(
9354 (item.record.logical_size - verify_offset).min(expected.len() as u64),
9355 )
9356 .unwrap();
9357 item.file.read_exact(&mut expected[..count]).map_err(|_| {
9358 FormatError::FilesystemExtractionFailed(
9359 "failed to read staged macOS symlink resource fork",
9360 )
9361 })?;
9362 let copied = unsafe {
9363 fgetxattr(
9364 link_fd.as_raw_fd(),
9365 name,
9366 restored.as_mut_ptr().cast(),
9367 count,
9368 u32::try_from(verify_offset).map_err(|_| {
9369 FormatError::ReaderUnsupported(
9370 "macOS resource fork exceeds Darwin xattr position range",
9371 )
9372 })?,
9373 0,
9374 )
9375 };
9376 if copied != count as libc::ssize_t
9377 || restored[..count] != expected[..count]
9378 {
9379 fail(
9380 diagnostics,
9381 "resource-fork",
9382 "macOS symlink resource fork did not verify after restoration",
9383 None,
9384 )?;
9385 break;
9386 }
9387 verify_offset += count as u64;
9388 }
9389 }
9390 }
9391 "macos.acl-native" => {
9392 let size = usize::try_from(item.record.logical_size).map_err(|_| {
9393 FormatError::ReaderUnsupported("macOS ACL exceeds platform limits")
9394 })?;
9395 let mut value = vec![0u8; size];
9396 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
9397 FormatError::FilesystemExtractionFailed("failed to rewind staged macOS ACL")
9398 })?;
9399 item.file.read_exact(&mut value).map_err(|_| {
9400 FormatError::FilesystemExtractionFailed("failed to read staged macOS ACL")
9401 })?;
9402 validate_darwin_acl_external(&value)?;
9403 let acl = unsafe { acl_copy_int(value.as_ptr().cast()) };
9404 if acl.is_null() {
9405 return Err(FormatError::InvalidArchive(
9406 "macOS ACL external form is invalid",
9407 ));
9408 }
9409 if unsafe { acl_set_fd_np(link_fd.as_raw_fd(), acl, ACL_TYPE_EXTENDED) } != 0 {
9410 let error = std::io::Error::last_os_error();
9411 unsafe { acl_free(acl) };
9412 fail(
9413 diagnostics,
9414 "acl-native",
9415 "failed to apply native macOS symlink ACL",
9416 Some(&error),
9417 )?;
9418 continue;
9419 }
9420 unsafe { acl_free(acl) };
9421 let restored = unsafe { acl_get_fd_np(link_fd.as_raw_fd(), ACL_TYPE_EXTENDED) };
9422 if restored.is_null() || unsafe { acl_size(restored) } != size as libc::ssize_t {
9423 if !restored.is_null() {
9424 unsafe { acl_free(restored) };
9425 }
9426 fail(
9427 diagnostics,
9428 "acl-native",
9429 "native macOS symlink ACL did not verify after restoration",
9430 None,
9431 )?;
9432 continue;
9433 }
9434 let mut actual = vec![0u8; size];
9435 let copied = unsafe {
9436 acl_copy_ext(actual.as_mut_ptr().cast(), restored, size as libc::ssize_t)
9437 };
9438 unsafe { acl_free(restored) };
9439 if copied != size as libc::ssize_t || actual != value {
9440 fail(
9441 diagnostics,
9442 "acl-native",
9443 "native macOS symlink ACL did not verify after restoration",
9444 None,
9445 )?;
9446 }
9447 }
9448 "macos.finder-info" | "generic.xattr" => {
9449 let (name, class) = if item.record.kind == "macos.finder-info" {
9450 (FINDER_INFO.to_vec(), "finder-info")
9451 } else {
9452 let mut name = item.record.decoded_name.clone();
9453 name.push(0);
9454 (name, "extended-attribute")
9455 };
9456 let value_len = usize::try_from(item.record.logical_size).map_err(|_| {
9457 FormatError::ReaderUnsupported("extended attribute exceeds platform limits")
9458 })?;
9459 let mut value = vec![0u8; value_len];
9460 item.file.seek(SeekFrom::Start(0)).map_err(|_| {
9461 FormatError::FilesystemExtractionFailed(
9462 "failed to rewind staged macOS symlink xattr",
9463 )
9464 })?;
9465 item.file.read_exact(&mut value).map_err(|_| {
9466 FormatError::FilesystemExtractionFailed(
9467 "failed to read staged macOS symlink xattr",
9468 )
9469 })?;
9470 if item.record.kind == "macos.finder-info" && value.len() != 32 {
9471 return Err(FormatError::InvalidArchive(
9472 "macOS FinderInfo is not exactly 32 bytes",
9473 ));
9474 }
9475 if unsafe {
9476 fsetxattr(
9477 link_fd.as_raw_fd(),
9478 name.as_ptr().cast(),
9479 value.as_ptr().cast(),
9480 value.len(),
9481 0,
9482 0,
9483 )
9484 } != 0
9485 {
9486 let error = std::io::Error::last_os_error();
9487 fail(
9488 diagnostics,
9489 class,
9490 "failed to apply macOS symlink extended attribute",
9491 Some(&error),
9492 )?;
9493 continue;
9494 }
9495 let actual_len = unsafe {
9496 fgetxattr(
9497 link_fd.as_raw_fd(),
9498 name.as_ptr().cast(),
9499 std::ptr::null_mut(),
9500 0,
9501 0,
9502 0,
9503 )
9504 };
9505 let mut actual = vec![0u8; value.len()];
9506 let copied = if actual_len == value.len() as libc::ssize_t {
9507 unsafe {
9508 fgetxattr(
9509 link_fd.as_raw_fd(),
9510 name.as_ptr().cast(),
9511 actual.as_mut_ptr().cast(),
9512 actual.len(),
9513 0,
9514 0,
9515 )
9516 }
9517 } else {
9518 -1
9519 };
9520 if copied != value.len() as libc::ssize_t || actual != value {
9521 fail(
9522 diagnostics,
9523 class,
9524 "macOS symlink extended attribute did not verify after restoration",
9525 None,
9526 )?;
9527 }
9528 }
9529 _ => remaining.push(item),
9530 }
9531 }
9532 *staged = remaining;
9533
9534 for (key, encoded) in metadata
9535 .primary_records
9536 .iter()
9537 .filter(|(key, _)| key.starts_with("LIBARCHIVE.xattr."))
9538 {
9539 let name = decode_percent_name(&key.as_bytes()["LIBARCHIVE.xattr.".len()..])?;
9540 let system = system_xattr_name(&name, "macos");
9541 if system && !(options.restore_policy == RestorePolicy::System && options.system_authorized)
9542 {
9543 continue;
9544 }
9545 let value = canonical_base64_decode(encoded)?;
9546 let name = CString::new(name)
9547 .map_err(|_| FormatError::InvalidArchive("xattr name contains NUL"))?;
9548 if unsafe {
9549 fsetxattr(
9550 link_fd.as_raw_fd(),
9551 name.as_ptr(),
9552 value.as_ptr().cast(),
9553 value.len(),
9554 0,
9555 0,
9556 )
9557 } != 0
9558 {
9559 let error = std::io::Error::last_os_error();
9560 fail(
9561 diagnostics,
9562 "extended-attribute",
9563 "failed to apply macOS symlink extended attribute",
9564 Some(&error),
9565 )?;
9566 continue;
9567 }
9568 let mut actual = vec![0u8; value.len()];
9569 let copied = unsafe {
9570 fgetxattr(
9571 link_fd.as_raw_fd(),
9572 name.as_ptr(),
9573 actual.as_mut_ptr().cast(),
9574 actual.len(),
9575 0,
9576 0,
9577 )
9578 };
9579 if copied != value.len() as libc::ssize_t || actual != value {
9580 fail(
9581 diagnostics,
9582 "extended-attribute",
9583 "macOS symlink extended attribute did not verify after restoration",
9584 None,
9585 )?;
9586 }
9587 }
9588
9589 #[repr(C)]
9590 struct AttrList {
9591 bitmap_count: u16,
9592 reserved: u16,
9593 common_attr: u32,
9594 volume_attr: u32,
9595 directory_attr: u32,
9596 file_attr: u32,
9597 fork_attr: u32,
9598 }
9599 let mut common_attr = 0x0000_0400;
9600 let mut times = Vec::<libc::timespec>::new();
9601 if let Some(encoded) = metadata.primary_records.get("LIBARCHIVE.creationtime") {
9602 let (seconds, nanoseconds) = parse_timestamp(encoded)?;
9603 common_attr |= 0x0000_0200;
9604 times.push(libc::timespec {
9605 tv_sec: seconds,
9606 tv_nsec: i64::from(nanoseconds),
9607 });
9608 }
9609 let (seconds, nanoseconds) = metadata.portable_mirror.mtime;
9610 times.push(libc::timespec {
9611 tv_sec: seconds,
9612 tv_nsec: i64::from(nanoseconds),
9613 });
9614 let attributes = AttrList {
9615 bitmap_count: 5,
9616 reserved: 0,
9617 common_attr,
9618 volume_attr: 0,
9619 directory_attr: 0,
9620 file_attr: 0,
9621 fork_attr: 0,
9622 };
9623 if unsafe {
9624 fsetattrlist(
9625 link_fd.as_raw_fd(),
9626 (&attributes as *const AttrList).cast(),
9627 times.as_ptr().cast(),
9628 times.len() * std::mem::size_of::<libc::timespec>(),
9629 0,
9630 )
9631 } != 0
9632 {
9633 let error = std::io::Error::last_os_error();
9634 fail(
9635 diagnostics,
9636 "timestamps",
9637 "failed to apply macOS symlink timestamps",
9638 Some(&error),
9639 )?;
9640 } else {
9641 let mut actual = std::mem::MaybeUninit::<libc::stat>::uninit();
9642 let status = unsafe { libc::fstat(link_fd.as_raw_fd(), actual.as_mut_ptr()) };
9643 let verified = if status == 0 {
9644 let actual = unsafe { actual.assume_init() };
9645 actual.st_mtime == seconds
9646 && actual.st_mtime_nsec == i64::from(nanoseconds)
9647 && metadata
9648 .primary_records
9649 .get("LIBARCHIVE.creationtime")
9650 .map(|encoded| parse_timestamp(encoded))
9651 .transpose()?
9652 .is_none_or(|(birth_seconds, birth_nanoseconds)| {
9653 actual.st_birthtime == birth_seconds
9654 && actual.st_birthtime_nsec == i64::from(birth_nanoseconds)
9655 })
9656 } else {
9657 false
9658 };
9659 if !verified {
9660 fail(
9661 diagnostics,
9662 "timestamps",
9663 "macOS symlink timestamps did not verify after restoration",
9664 None,
9665 )?;
9666 }
9667 }
9668
9669 if let Some(encoded) = metadata.primary_records.get("TZAP.macos.st-flags") {
9670 let desired = parse_macos_flags(encoded)? & MACOS_KNOWN_SETTABLE_FLAGS;
9671 if !macos_flags_require_system(desired)
9672 || options.restore_policy == RestorePolicy::System && options.system_authorized
9673 {
9674 let mut before = std::mem::MaybeUninit::<libc::stat>::uninit();
9675 let retained_unknown =
9676 if unsafe { libc::fstat(link_fd.as_raw_fd(), before.as_mut_ptr()) } == 0 {
9677 unsafe { before.assume_init() }.st_flags & !MACOS_KNOWN_SETTABLE_FLAGS
9678 } else {
9679 0
9680 };
9681 if unsafe { fchflags(link_fd.as_raw_fd(), retained_unknown | desired) } != 0 {
9682 let error = std::io::Error::last_os_error();
9683 fail(
9684 diagnostics,
9685 "file-flags",
9686 "failed to apply macOS symlink flags",
9687 Some(&error),
9688 )?;
9689 } else {
9690 let mut actual = std::mem::MaybeUninit::<libc::stat>::uninit();
9691 let status = unsafe { libc::fstat(link_fd.as_raw_fd(), actual.as_mut_ptr()) };
9692 let verified = status == 0
9693 && unsafe { actual.assume_init() }.st_flags & MACOS_KNOWN_SETTABLE_FLAGS
9694 == desired;
9695 if !verified {
9696 fail(
9697 diagnostics,
9698 "file-flags",
9699 "macOS symlink flags did not verify after restoration",
9700 None,
9701 )?;
9702 }
9703 }
9704 }
9705 }
9706 Ok(())
9707}
9708
9709#[cfg(not(target_os = "macos"))]
9710fn apply_restored_macos_symlink_metadata(
9711 _destination: &PreparedDestination,
9712 _path: &[u8],
9713 _metadata: &MemberMetadata,
9714 _staged: &mut Vec<StagedAuxiliary>,
9715 _options: SafeExtractionOptions,
9716 _diagnostics: &mut Vec<MetadataDiagnostic>,
9717) -> Result<(), FormatError> {
9718 Ok(())
9719}
9720
9721fn create_temp_regular_file(
9722 destination: &PreparedDestination,
9723) -> Result<(PathBuf, fs::File), FormatError> {
9724 for _ in 0..1000u32 {
9725 let mut candidate = destination.leaf.as_os_str().to_os_string();
9726 candidate.push(format!(".tzap-tmp-{}", uuid::Uuid::new_v4()));
9727 let leaf = PathBuf::from(candidate);
9728 match destination
9729 .parent
9730 .open_with(&leaf, &create_new_file_options())
9731 {
9732 Ok(file) => return Ok((leaf, file.into_std())),
9733 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
9734 Err(_) => {
9735 return Err(FormatError::FilesystemExtractionFailed(
9736 "failed to create regular file",
9737 ));
9738 }
9739 }
9740 }
9741 Err(FormatError::FilesystemExtractionFailed(
9742 "failed to create regular file",
9743 ))
9744}
9745
9746#[cfg(windows)]
9747fn prepare_windows_sparse_file(file: &fs::File, logical_size: u64) -> Result<(), FormatError> {
9748 use std::os::windows::io::AsRawHandle;
9749 use std::ptr;
9750 use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE;
9751 use windows_sys::Win32::System::IO::DeviceIoControl;
9752
9753 let mut bytes_returned = 0u32;
9754 if unsafe {
9757 DeviceIoControl(
9758 file.as_raw_handle().cast(),
9759 FSCTL_SET_SPARSE,
9760 ptr::null(),
9761 0,
9762 ptr::null_mut(),
9763 0,
9764 &mut bytes_returned,
9765 ptr::null_mut(),
9766 )
9767 } == 0
9768 {
9769 return Err(FormatError::FilesystemExtractionFailed(
9770 "destination filesystem cannot mark sparse output",
9771 ));
9772 }
9773 file.set_len(logical_size)
9774 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to size sparse output"))
9775}
9776
9777#[cfg(windows)]
9778fn query_windows_sparse_ranges(
9779 file: &fs::File,
9780 logical_size: u64,
9781) -> Result<Vec<SparseExtent>, FormatError> {
9782 use std::mem::size_of;
9783 use std::os::windows::io::AsRawHandle;
9784 use std::ptr;
9785 use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
9786 use windows_sys::Win32::System::Ioctl::{
9787 FILE_ALLOCATED_RANGE_BUFFER, FSCTL_QUERY_ALLOCATED_RANGES,
9788 };
9789 use windows_sys::Win32::System::IO::DeviceIoControl;
9790
9791 const QUERY_BATCH: usize = 1024;
9792 if logical_size == 0 {
9793 return Ok(Vec::new());
9794 }
9795 let logical_size_i64 = i64::try_from(logical_size).map_err(|_| {
9796 FormatError::FilesystemExtractionFailed("sparse logical size exceeds Windows range API")
9797 })?;
9798 let mut query_start = 0u64;
9799 let mut extents = Vec::<SparseExtent>::new();
9800 while query_start < logical_size {
9801 let mut query = FILE_ALLOCATED_RANGE_BUFFER {
9802 FileOffset: query_start as i64,
9803 Length: logical_size_i64 - query_start as i64,
9804 };
9805 let mut output = [FILE_ALLOCATED_RANGE_BUFFER::default(); QUERY_BATCH];
9806 let mut bytes_returned = 0u32;
9807 let success = unsafe {
9809 DeviceIoControl(
9810 file.as_raw_handle().cast(),
9811 FSCTL_QUERY_ALLOCATED_RANGES,
9812 (&mut query as *mut FILE_ALLOCATED_RANGE_BUFFER).cast(),
9813 size_of::<FILE_ALLOCATED_RANGE_BUFFER>() as u32,
9814 output.as_mut_ptr().cast(),
9815 size_of::<[FILE_ALLOCATED_RANGE_BUFFER; QUERY_BATCH]>() as u32,
9816 &mut bytes_returned,
9817 ptr::null_mut(),
9818 )
9819 };
9820 let error = std::io::Error::last_os_error();
9821 if success == 0 && error.raw_os_error() != Some(ERROR_MORE_DATA as i32) {
9822 return Err(FormatError::FilesystemExtractionFailed(
9823 "failed to query restored sparse ranges",
9824 ));
9825 }
9826 if bytes_returned as usize % size_of::<FILE_ALLOCATED_RANGE_BUFFER>() != 0 {
9827 return Err(FormatError::FilesystemExtractionFailed(
9828 "Windows returned a truncated restored sparse range",
9829 ));
9830 }
9831 let count = bytes_returned as usize / size_of::<FILE_ALLOCATED_RANGE_BUFFER>();
9832 if count > QUERY_BATCH || (success == 0 && count == 0) {
9833 return Err(FormatError::FilesystemExtractionFailed(
9834 "restored sparse range query made no progress",
9835 ));
9836 }
9837 let mut next_query_start = query_start;
9838 for range in &output[..count] {
9839 if range.FileOffset < 0 || range.Length <= 0 {
9840 return Err(FormatError::FilesystemExtractionFailed(
9841 "Windows returned an invalid restored sparse range",
9842 ));
9843 }
9844 let offset = range.FileOffset as u64;
9845 let end = offset
9846 .checked_add(range.Length as u64)
9847 .ok_or(FormatError::FilesystemExtractionFailed(
9848 "restored sparse range overflow",
9849 ))?
9850 .min(logical_size);
9851 if offset >= logical_size || end <= offset {
9852 return Err(FormatError::FilesystemExtractionFailed(
9853 "Windows returned an out-of-bounds restored sparse range",
9854 ));
9855 }
9856 if let Some(previous) = extents.last_mut() {
9857 let previous_end = previous.offset + previous.length;
9858 if offset <= previous_end {
9859 previous.length = previous_end.max(end) - previous.offset;
9860 } else {
9861 extents.push(SparseExtent {
9862 offset,
9863 length: end - offset,
9864 });
9865 }
9866 } else {
9867 extents.push(SparseExtent {
9868 offset,
9869 length: end - offset,
9870 });
9871 }
9872 next_query_start = next_query_start.max(end);
9873 }
9874 if success != 0 {
9875 break;
9876 }
9877 if next_query_start <= query_start {
9878 return Err(FormatError::FilesystemExtractionFailed(
9879 "restored sparse range query did not advance",
9880 ));
9881 }
9882 query_start = next_query_start;
9883 }
9884 Ok(extents)
9885}
9886
9887#[cfg(windows)]
9888fn windows_file_system_is_refs(file: &fs::File) -> Result<bool, FormatError> {
9889 use std::os::windows::io::AsRawHandle as _;
9890 use windows_sys::Win32::Storage::FileSystem::GetVolumeInformationByHandleW;
9891
9892 let mut name = [0u16; 32];
9893 if unsafe {
9896 GetVolumeInformationByHandleW(
9897 file.as_raw_handle().cast(),
9898 std::ptr::null_mut(),
9899 0,
9900 std::ptr::null_mut(),
9901 std::ptr::null_mut(),
9902 std::ptr::null_mut(),
9903 name.as_mut_ptr(),
9904 name.len() as u32,
9905 )
9906 } == 0
9907 {
9908 return Err(FormatError::FilesystemExtractionFailed(
9909 "failed to identify Windows destination filesystem",
9910 ));
9911 }
9912 let length = name
9913 .iter()
9914 .position(|unit| *unit == 0)
9915 .unwrap_or(name.len());
9916 Ok(String::from_utf16_lossy(&name[..length]).eq_ignore_ascii_case("refs"))
9917}
9918
9919#[cfg(windows)]
9920fn verify_windows_sparse_file(
9921 file: &fs::File,
9922 logical_size: u64,
9923 expected_extents: &[SparseExtent],
9924) -> Result<(), FormatError> {
9925 use std::mem::size_of;
9926 use std::os::windows::io::AsRawHandle;
9927 use windows_sys::Win32::Storage::FileSystem::{
9928 FileBasicInfo, GetFileInformationByHandleEx, FILE_BASIC_INFO,
9929 };
9930
9931 const FILE_ATTRIBUTE_SPARSE_FILE: u32 = 0x0000_0200;
9932 let mut basic = FILE_BASIC_INFO::default();
9933 if unsafe {
9935 GetFileInformationByHandleEx(
9936 file.as_raw_handle().cast(),
9937 FileBasicInfo,
9938 (&mut basic as *mut FILE_BASIC_INFO).cast(),
9939 size_of::<FILE_BASIC_INFO>() as u32,
9940 )
9941 } == 0
9942 || basic.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE == 0
9943 {
9944 return Err(FormatError::FilesystemExtractionFailed(
9945 "restored file is not marked sparse",
9946 ));
9947 }
9948 if query_windows_sparse_ranges(file, logical_size)? != expected_extents
9949 && !windows_file_system_is_refs(file)?
9950 {
9951 return Err(FormatError::FilesystemExtractionFailed(
9952 "restored sparse ranges do not match archive",
9953 ));
9954 }
9955 Ok(())
9956}
9957
9958#[cfg(windows)]
9959fn rename_open_file_noreplace(
9960 file: &fs::File,
9961 destination_parent: &CapDir,
9962 destination_leaf: &Path,
9963) -> Result<(), FormatError> {
9964 use std::mem::size_of;
9965 use std::os::windows::ffi::OsStrExt;
9966 use std::os::windows::io::AsRawHandle;
9967 use windows_sys::Win32::Storage::FileSystem::{
9968 FileRenameInfo, GetFinalPathNameByHandleW, SetFileInformationByHandle,
9969 FILE_NAME_NORMALIZED, FILE_RENAME_INFO, VOLUME_NAME_DOS,
9970 };
9971
9972 let leaf = destination_leaf
9973 .as_os_str()
9974 .encode_wide()
9975 .collect::<Vec<_>>();
9976 if leaf.is_empty() || leaf.contains(&0) {
9977 return Err(FormatError::UnsafeArchivePath);
9978 }
9979 let mut capacity = 512usize;
9980 let mut name = loop {
9981 let mut buffer = vec![0u16; capacity];
9982 let length = unsafe {
9984 GetFinalPathNameByHandleW(
9985 destination_parent.as_raw_handle().cast(),
9986 buffer.as_mut_ptr(),
9987 u32::try_from(buffer.len()).map_err(|_| {
9988 FormatError::FilesystemExtractionFailed(
9989 "destination path buffer exceeds Windows limit",
9990 )
9991 })?,
9992 FILE_NAME_NORMALIZED | VOLUME_NAME_DOS,
9993 )
9994 } as usize;
9995 if length == 0 {
9996 return Err(FormatError::FilesystemExtractionFailed(
9997 "failed to resolve destination directory handle",
9998 ));
9999 }
10000 if length < buffer.len() {
10001 buffer.truncate(length);
10002 break buffer;
10003 }
10004 capacity = length
10005 .checked_add(1)
10006 .ok_or(FormatError::FilesystemExtractionFailed(
10007 "destination path length overflow",
10008 ))?;
10009 };
10010 if !name.ends_with(&[b'\\' as u16]) {
10011 name.push(b'\\' as u16);
10012 }
10013 name.extend_from_slice(&leaf);
10014 let name_byte_len =
10015 name.len()
10016 .checked_mul(size_of::<u16>())
10017 .ok_or(FormatError::FilesystemExtractionFailed(
10018 "destination file name is too large to publish",
10019 ))?;
10020 let byte_len = size_of::<FILE_RENAME_INFO>()
10025 .checked_add(name_byte_len)
10026 .ok_or(FormatError::FilesystemExtractionFailed(
10027 "destination rename buffer overflow",
10028 ))?;
10029 let storage_len = byte_len.div_ceil(size_of::<usize>());
10030 let mut storage = vec![0usize; storage_len];
10031 let info = storage.as_mut_ptr().cast::<FILE_RENAME_INFO>();
10032 unsafe {
10035 (*info).Anonymous.ReplaceIfExists = false;
10036 (*info).RootDirectory = std::ptr::null_mut();
10037 (*info).FileNameLength = u32::try_from(name.len() * size_of::<u16>()).map_err(|_| {
10038 FormatError::FilesystemExtractionFailed("destination filename exceeds Windows limit")
10039 })?;
10040 std::ptr::copy_nonoverlapping(
10041 name.as_ptr(),
10042 std::ptr::addr_of_mut!((*info).FileName).cast::<u16>(),
10043 name.len(),
10044 );
10045 if SetFileInformationByHandle(
10046 file.as_raw_handle().cast(),
10047 FileRenameInfo,
10048 info.cast(),
10049 u32::try_from(byte_len).map_err(|_| {
10050 FormatError::FilesystemExtractionFailed(
10051 "destination rename buffer exceeds Windows limit",
10052 )
10053 })?,
10054 ) == 0
10055 {
10056 let error = std::io::Error::last_os_error();
10057 return if matches!(error.raw_os_error(), Some(80 | 183)) {
10058 Err(FormatError::UnsafeOverwrite)
10059 } else {
10060 Err(FormatError::FilesystemExtractionFailed(
10061 "failed to publish allocation-preserving output",
10062 ))
10063 };
10064 }
10065 }
10066 Ok(())
10067}
10068
10069fn publish_regular_file(
10070 destination: &PreparedDestination,
10071 temp_leaf: &Path,
10072 mut temp_file: fs::File,
10073 options: SafeExtractionOptions,
10074) -> Result<fs::File, FormatError> {
10075 if options.overwrite_existing {
10076 remove_existing_leaf_if_needed(destination)?;
10077 }
10078
10079 #[cfg(windows)]
10080 {
10081 temp_file
10082 .flush()
10083 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to flush regular file"))?;
10084 if let Err(error) =
10085 rename_open_file_noreplace(&temp_file, &destination.parent, &destination.leaf)
10086 {
10087 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
10088 return Err(error);
10089 }
10090 Ok(temp_file)
10091 }
10092
10093 #[cfg(target_os = "linux")]
10094 {
10095 use std::ffi::CString;
10096 use std::os::unix::ffi::OsStrExt as _;
10097
10098 temp_file
10099 .flush()
10100 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to flush regular file"))?;
10101 let source = CString::new(temp_leaf.as_os_str().as_bytes())
10102 .map_err(|_| FormatError::UnsafeArchivePath)?;
10103 let target = CString::new(destination.leaf.as_os_str().as_bytes())
10104 .map_err(|_| FormatError::UnsafeArchivePath)?;
10105 if unsafe {
10109 libc::syscall(
10110 libc::SYS_renameat2,
10111 destination.parent.as_raw_fd(),
10112 source.as_ptr(),
10113 destination.parent.as_raw_fd(),
10114 target.as_ptr(),
10115 libc::RENAME_NOREPLACE,
10116 )
10117 } != 0
10118 {
10119 let error = std::io::Error::last_os_error();
10120 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
10121 return if error.raw_os_error() == Some(libc::EEXIST) {
10122 Err(FormatError::UnsafeOverwrite)
10123 } else {
10124 Err(FormatError::FilesystemExtractionFailed(
10125 "failed to publish allocation-preserving output",
10126 ))
10127 };
10128 }
10129 Ok(temp_file)
10130 }
10131
10132 #[cfg(all(not(windows), not(target_os = "linux")))]
10133 let mut output = match destination
10134 .parent
10135 .open_with(&destination.leaf, &create_new_file_options())
10136 {
10137 Ok(file) => file.into_std(),
10138 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
10139 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
10140 return Err(FormatError::UnsafeOverwrite);
10141 }
10142 Err(_) => {
10143 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
10144 return Err(FormatError::FilesystemExtractionFailed(
10145 "failed to create regular file",
10146 ));
10147 }
10148 };
10149
10150 #[cfg(all(not(windows), not(target_os = "linux")))]
10151 let copy_result = temp_file
10152 .seek(SeekFrom::Start(0))
10153 .and_then(|_| std::io::copy(&mut temp_file, &mut output))
10154 .and_then(|_| output.flush());
10155
10156 #[cfg(all(not(windows), not(target_os = "linux")))]
10157 if copy_result.is_err() {
10158 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
10159 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
10160 return Err(FormatError::FilesystemExtractionFailed(
10161 "failed to write regular file",
10162 ));
10163 }
10164
10165 #[cfg(all(not(windows), not(target_os = "linux")))]
10166 {
10167 let _ = destination.parent.remove_file_or_symlink(temp_leaf);
10168 Ok(output)
10169 }
10170}
10171
10172fn remove_existing_leaf_if_needed(destination: &PreparedDestination) -> Result<(), FormatError> {
10173 match destination.parent.symlink_metadata(&destination.leaf) {
10174 Ok(metadata) => {
10175 if metadata.file_type().is_dir() {
10176 return Err(FormatError::UnsafeOverwrite);
10177 }
10178 destination
10179 .parent
10180 .remove_file_or_symlink(&destination.leaf)
10181 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to remove old file"))
10182 }
10183 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
10184 Err(_) => Err(FormatError::FilesystemExtractionFailed(
10185 "failed to inspect destination",
10186 )),
10187 }
10188}
10189
10190fn create_directory(destination: &PreparedDestination) -> Result<(), FormatError> {
10191 match destination.parent.create_dir(&destination.leaf) {
10192 Ok(()) => Ok(()),
10193 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
10194 let metadata = destination
10195 .parent
10196 .symlink_metadata(&destination.leaf)
10197 .map_err(|_| FormatError::UnsafeOverwrite)?;
10198 let file_type = metadata.file_type();
10199 if file_type.is_symlink() {
10200 Err(FormatError::UnsafeArchivePath)
10201 } else if file_type.is_dir() {
10202 Ok(())
10203 } else {
10204 Err(FormatError::UnsafeOverwrite)
10205 }
10206 }
10207 Err(_) => Err(FormatError::FilesystemExtractionFailed(
10208 "failed to create directory",
10209 )),
10210 }
10211}
10212
10213fn create_hardlink(
10214 destination: &PreparedDestination,
10215 target: &PreparedDestination,
10216 options: SafeExtractionOptions,
10217) -> Result<(), FormatError> {
10218 if options.overwrite_existing {
10219 remove_existing_leaf_if_needed(destination)?;
10220 }
10221 match target
10222 .parent
10223 .hard_link(&target.leaf, &destination.parent, &destination.leaf)
10224 {
10225 Ok(()) => {
10226 let metadata = destination
10227 .parent
10228 .symlink_metadata(&destination.leaf)
10229 .map_err(|_| {
10230 FormatError::FilesystemExtractionFailed("failed to inspect hardlink")
10231 })?;
10232 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
10233 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
10234 return Err(FormatError::UnsafeArchivePath);
10235 }
10236 Ok(())
10237 }
10238 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
10239 Err(FormatError::UnsafeOverwrite)
10240 }
10241 Err(_) => Err(FormatError::FilesystemExtractionFailed(
10242 "failed to create hardlink",
10243 )),
10244 }
10245}
10246
10247fn create_symlink(
10248 destination: &PreparedDestination,
10249 target: &[u8],
10250 options: SafeExtractionOptions,
10251) -> Result<(), FormatError> {
10252 if options.overwrite_existing {
10253 remove_existing_leaf_if_needed(destination)?;
10254 }
10255 let target = std::str::from_utf8(target).map_err(|_| FormatError::UnsafeArchivePath)?;
10256 match destination.parent.symlink_file(target, &destination.leaf) {
10257 Ok(()) => Ok(()),
10258 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
10259 Err(FormatError::UnsafeOverwrite)
10260 }
10261 Err(_) => Err(FormatError::FilesystemExtractionFailed(
10262 "failed to create symlink",
10263 )),
10264 }
10265}
10266
10267#[cfg(target_os = "linux")]
10268fn create_posix_special_object(
10269 destination: &PreparedDestination,
10270 path: &[u8],
10271 kind: TarEntryKind,
10272 metadata: &MemberMetadata,
10273 staged: &mut Vec<StagedAuxiliary>,
10274 options: SafeExtractionOptions,
10275 diagnostics: &mut Vec<MetadataDiagnostic>,
10276) -> Result<(), FormatError> {
10277 use std::ffi::{CString, OsStr};
10278 use std::os::fd::FromRawFd as _;
10279 use std::os::unix::ffi::OsStrExt as _;
10280
10281 if options.restore_policy != RestorePolicy::System || !options.system_authorized {
10282 return Err(FormatError::ReaderUnsupported(
10283 "special POSIX objects require authorized system restore",
10284 ));
10285 }
10286 if options.overwrite_existing {
10287 remove_existing_leaf_if_needed(destination)?;
10288 }
10289 let leaf = CString::new(destination.leaf.as_os_str().as_bytes())
10290 .map_err(|_| FormatError::UnsafeArchivePath)?;
10291 let permission_mode = metadata.portable_mirror.mode & 0o7777;
10292 let (object_mode, device) = match kind {
10293 TarEntryKind::Fifo => (libc::S_IFIFO | permission_mode, 0),
10294 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice => {
10295 let major = metadata
10296 .primary_records
10297 .get("TZAP.posix.device-major")
10298 .ok_or(FormatError::InvalidArchive(
10299 "device major number is missing",
10300 ))?;
10301 let minor = metadata
10302 .primary_records
10303 .get("TZAP.posix.device-minor")
10304 .ok_or(FormatError::InvalidArchive(
10305 "device minor number is missing",
10306 ))?;
10307 let major = parse_minimal_decimal_u64(major, "device major")?;
10308 let minor = parse_minimal_decimal_u64(minor, "device minor")?;
10309 let major = libc::c_uint::try_from(major)
10310 .map_err(|_| FormatError::ReaderUnsupported("device major exceeds host ABI"))?;
10311 let minor = libc::c_uint::try_from(minor)
10312 .map_err(|_| FormatError::ReaderUnsupported("device minor exceeds host ABI"))?;
10313 let type_mode = if kind == TarEntryKind::CharacterDevice {
10314 libc::S_IFCHR
10315 } else {
10316 libc::S_IFBLK
10317 };
10318 (type_mode | permission_mode, libc::makedev(major, minor))
10319 }
10320 _ => {
10321 return Err(FormatError::WriterInvariant(
10322 "non-special member reached Linux special-object creation",
10323 ));
10324 }
10325 };
10326 if unsafe {
10328 libc::mknodat(
10329 destination.parent.as_raw_fd(),
10330 leaf.as_ptr(),
10331 object_mode as libc::mode_t,
10332 device,
10333 )
10334 } != 0
10335 {
10336 let error = std::io::Error::last_os_error();
10337 return record_metadata_application_failure(
10338 diagnostics,
10339 MetadataDiagnostic::new(
10340 path,
10341 "posix-backup-v1",
10342 "special-object",
10343 MetadataOperation::Restore,
10344 MetadataDiagnosticStatus::Failed,
10345 "failed to create Linux special object",
10346 )
10347 .for_restore(options.restore_policy, 2)
10348 .with_native_error(&error),
10349 options,
10350 "failed to create Linux special object",
10351 );
10352 }
10353
10354 let fd = unsafe {
10356 libc::openat(
10357 destination.parent.as_raw_fd(),
10358 leaf.as_ptr(),
10359 libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
10360 )
10361 };
10362 if fd < 0 {
10363 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
10364 return Err(FormatError::FilesystemExtractionFailed(
10365 "failed to pin restored Linux special object",
10366 ));
10367 }
10368 let pinned = unsafe { fs::File::from_raw_fd(fd) };
10370 let proc_path = PathBuf::from(format!("/proc/self/fd/{}", pinned.as_raw_fd()));
10371 let proc_c = CString::new(proc_path.as_os_str().as_bytes())
10372 .map_err(|_| FormatError::UnsafeArchivePath)?;
10373
10374 if let (Some(uid), Some(gid)) = (metadata.portable_mirror.uid, metadata.portable_mirror.gid) {
10375 let uid = libc::uid_t::try_from(uid)
10376 .map_err(|_| FormatError::ReaderUnsupported("archived UID exceeds host uid_t"))?;
10377 let gid = libc::gid_t::try_from(gid)
10378 .map_err(|_| FormatError::ReaderUnsupported("archived GID exceeds host gid_t"))?;
10379 if unsafe { libc::chown(proc_c.as_ptr(), uid, gid) } != 0 {
10381 let error = std::io::Error::last_os_error();
10382 record_metadata_application_failure(
10383 diagnostics,
10384 MetadataDiagnostic::new(
10385 path,
10386 "portable-v1",
10387 "numeric-ownership",
10388 MetadataOperation::Restore,
10389 MetadataDiagnosticStatus::Failed,
10390 "failed to apply special-object ownership",
10391 )
10392 .for_restore(options.restore_policy, 4)
10393 .with_native_error(&error),
10394 options,
10395 "failed to apply special-object ownership",
10396 )?;
10397 }
10398 }
10399 if unsafe { libc::chmod(proc_c.as_ptr(), permission_mode as libc::mode_t) } != 0 {
10401 let error = std::io::Error::last_os_error();
10402 record_metadata_application_failure(
10403 diagnostics,
10404 MetadataDiagnostic::new(
10405 path,
10406 "portable-v1",
10407 "mode",
10408 MetadataOperation::Restore,
10409 MetadataDiagnosticStatus::Failed,
10410 "failed to apply special-object mode",
10411 )
10412 .for_restore(options.restore_policy, 4)
10413 .with_native_error(&error),
10414 options,
10415 "failed to apply special-object mode",
10416 )?;
10417 }
10418 for (key, name) in [
10419 ("SCHILY.acl.access", "system.posix_acl_access"),
10420 ("SCHILY.acl.default", "system.posix_acl_default"),
10421 ] {
10422 let Some(text) = metadata.primary_records.get(key) else {
10423 continue;
10424 };
10425 let value = schily_posix_acl_to_linux_xattr(text)?;
10426 if let Err(error) = xattr::set_deref(&proc_path, name, &value) {
10427 record_metadata_application_failure(
10428 diagnostics,
10429 MetadataDiagnostic::new(
10430 path,
10431 "posix-backup-v1",
10432 "posix-acl",
10433 MetadataOperation::Restore,
10434 MetadataDiagnosticStatus::Failed,
10435 "failed to apply special-object POSIX ACL",
10436 )
10437 .for_restore(options.restore_policy, 4)
10438 .with_native_error(&error),
10439 options,
10440 "failed to apply special-object POSIX ACL",
10441 )?;
10442 continue;
10443 }
10444 if xattr::get_deref(&proc_path, name).ok().flatten().as_deref() != Some(value.as_slice()) {
10445 record_metadata_application_failure(
10446 diagnostics,
10447 MetadataDiagnostic::new(
10448 path,
10449 "posix-backup-v1",
10450 "posix-acl",
10451 MetadataOperation::Restore,
10452 MetadataDiagnosticStatus::Failed,
10453 "special-object POSIX ACL did not verify after restoration",
10454 )
10455 .for_restore(options.restore_policy, 4),
10456 options,
10457 "special-object POSIX ACL did not verify after restoration",
10458 )?;
10459 }
10460 }
10461 apply_generic_xattr_auxiliaries_to_path(&proc_path, true, path, staged, options, diagnostics)?;
10462 for (key, encoded) in metadata
10463 .primary_records
10464 .iter()
10465 .filter(|(key, _)| key.starts_with("LIBARCHIVE.xattr."))
10466 {
10467 let name = decode_percent_name(&key.as_bytes()["LIBARCHIVE.xattr.".len()..])?;
10468 let value = canonical_base64_decode(encoded)?;
10469 if let Err(error) = xattr::set_deref(&proc_path, OsStr::from_bytes(&name), &value) {
10470 record_metadata_application_failure(
10471 diagnostics,
10472 MetadataDiagnostic::new(
10473 path,
10474 if system_xattr_name(&name, "linux") {
10475 "linux-backup-v1"
10476 } else {
10477 "posix-backup-v1"
10478 },
10479 "extended-attribute",
10480 MetadataOperation::Restore,
10481 MetadataDiagnosticStatus::Failed,
10482 "failed to apply special-object extended attribute",
10483 )
10484 .for_restore(options.restore_policy, 4)
10485 .with_native_error(&error),
10486 options,
10487 "failed to apply special-object extended attribute",
10488 )?;
10489 continue;
10490 }
10491 if xattr::get_deref(&proc_path, OsStr::from_bytes(&name))
10492 .ok()
10493 .flatten()
10494 .as_deref()
10495 != Some(value.as_slice())
10496 {
10497 record_metadata_application_failure(
10498 diagnostics,
10499 MetadataDiagnostic::new(
10500 path,
10501 if system_xattr_name(&name, "linux") {
10502 "linux-backup-v1"
10503 } else {
10504 "posix-backup-v1"
10505 },
10506 "extended-attribute",
10507 MetadataOperation::Restore,
10508 MetadataDiagnosticStatus::Failed,
10509 "special-object extended attribute did not verify after restoration",
10510 )
10511 .for_restore(options.restore_policy, 4),
10512 options,
10513 "special-object extended attribute did not verify after restoration",
10514 )?;
10515 }
10516 }
10517 let (seconds, nanoseconds) = metadata.portable_mirror.mtime;
10518 let times = [
10519 libc::timespec {
10520 tv_sec: 0,
10521 tv_nsec: libc::UTIME_OMIT,
10522 },
10523 libc::timespec {
10524 tv_sec: seconds as _,
10525 tv_nsec: nanoseconds as libc::c_long,
10526 },
10527 ];
10528 if unsafe { libc::utimensat(libc::AT_FDCWD, proc_c.as_ptr(), times.as_ptr(), 0) } != 0 {
10530 let error = std::io::Error::last_os_error();
10531 record_metadata_application_failure(
10532 diagnostics,
10533 MetadataDiagnostic::new(
10534 path,
10535 "portable-v1",
10536 "mtime",
10537 MetadataOperation::Restore,
10538 MetadataDiagnosticStatus::Failed,
10539 "failed to apply special-object mtime",
10540 )
10541 .for_restore(options.restore_policy, 4)
10542 .with_native_error(&error),
10543 options,
10544 "failed to apply special-object mtime",
10545 )?;
10546 }
10547 if kind == TarEntryKind::Fifo {
10548 let fd = unsafe {
10549 libc::openat(
10550 destination.parent.as_raw_fd(),
10551 leaf.as_ptr(),
10552 libc::O_RDONLY | libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC,
10553 )
10554 };
10555 if fd < 0 {
10556 let error = std::io::Error::last_os_error();
10557 return record_metadata_application_failure(
10558 diagnostics,
10559 MetadataDiagnostic::new(
10560 path,
10561 "linux-backup-v1",
10562 "fifo-native-metadata",
10563 MetadataOperation::Restore,
10564 MetadataDiagnosticStatus::Failed,
10565 "failed to open restored FIFO for native metadata",
10566 )
10567 .for_restore(options.restore_policy, 4)
10568 .with_native_error(&error),
10569 options,
10570 "failed to open restored FIFO for native metadata",
10571 );
10572 }
10573 let fifo = unsafe { fs::File::from_raw_fd(fd) };
10574 apply_linux_project_id(&fifo, path, metadata, options, diagnostics)?;
10575 apply_linux_inode_flags(&fifo, path, metadata, options, diagnostics)?;
10576 }
10577 Ok(())
10578}
10579
10580#[cfg(target_os = "macos")]
10581fn create_posix_special_object(
10582 destination: &PreparedDestination,
10583 path: &[u8],
10584 kind: TarEntryKind,
10585 metadata: &MemberMetadata,
10586 staged: &mut Vec<StagedAuxiliary>,
10587 options: SafeExtractionOptions,
10588 diagnostics: &mut Vec<MetadataDiagnostic>,
10589) -> Result<(), FormatError> {
10590 use std::ffi::CString;
10591 use std::os::fd::FromRawFd as _;
10592 use std::os::unix::ffi::OsStrExt as _;
10593
10594 if options.restore_policy != RestorePolicy::System || !options.system_authorized {
10595 return Err(FormatError::ReaderUnsupported(
10596 "special POSIX objects require authorized system restore",
10597 ));
10598 }
10599 if options.overwrite_existing {
10600 remove_existing_leaf_if_needed(destination)?;
10601 }
10602 let leaf = CString::new(destination.leaf.as_os_str().as_bytes())
10603 .map_err(|_| FormatError::UnsafeArchivePath)?;
10604 let permission_mode = metadata.portable_mirror.mode & 0o7777;
10605 let (object_mode, device) = match kind {
10606 TarEntryKind::Fifo => (u32::from(libc::S_IFIFO) | permission_mode, 0),
10607 TarEntryKind::CharacterDevice | TarEntryKind::BlockDevice => {
10608 let major = metadata
10609 .primary_records
10610 .get("TZAP.posix.device-major")
10611 .ok_or(FormatError::InvalidArchive(
10612 "device major number is missing",
10613 ))?;
10614 let minor = metadata
10615 .primary_records
10616 .get("TZAP.posix.device-minor")
10617 .ok_or(FormatError::InvalidArchive(
10618 "device minor number is missing",
10619 ))?;
10620 let major = libc::c_int::try_from(parse_minimal_decimal_u64(major, "device major")?)
10621 .map_err(|_| FormatError::ReaderUnsupported("device major exceeds host ABI"))?;
10622 let minor = libc::c_int::try_from(parse_minimal_decimal_u64(minor, "device minor")?)
10623 .map_err(|_| FormatError::ReaderUnsupported("device minor exceeds host ABI"))?;
10624 let type_mode = if kind == TarEntryKind::CharacterDevice {
10625 libc::S_IFCHR
10626 } else {
10627 libc::S_IFBLK
10628 };
10629 (
10630 u32::from(type_mode) | permission_mode,
10631 libc::makedev(major, minor),
10632 )
10633 }
10634 _ => {
10635 return Err(FormatError::WriterInvariant(
10636 "non-special member reached macOS special-object creation",
10637 ));
10638 }
10639 };
10640 if unsafe {
10641 libc::mknodat(
10642 destination.parent.as_raw_fd(),
10643 leaf.as_ptr(),
10644 object_mode as libc::mode_t,
10645 device,
10646 )
10647 } != 0
10648 {
10649 let error = std::io::Error::last_os_error();
10650 return record_metadata_application_failure(
10651 diagnostics,
10652 MetadataDiagnostic::new(
10653 path,
10654 "posix-backup-v1",
10655 "special-object",
10656 MetadataOperation::Restore,
10657 MetadataDiagnosticStatus::Failed,
10658 "failed to create macOS special object",
10659 )
10660 .for_restore(options.restore_policy, 2)
10661 .with_native_error(&error),
10662 options,
10663 "failed to create macOS special object",
10664 );
10665 }
10666
10667 const O_EVTONLY: libc::c_int = 0x0000_8000;
10668 let open_flags = if kind == TarEntryKind::Fifo {
10669 libc::O_RDWR | libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC
10670 } else {
10671 libc::O_RDONLY | O_EVTONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC
10672 };
10673 let fd = unsafe { libc::openat(destination.parent.as_raw_fd(), leaf.as_ptr(), open_flags) };
10674 if fd < 0 {
10675 let _ = destination.parent.remove_file_or_symlink(&destination.leaf);
10676 return Err(FormatError::FilesystemExtractionFailed(
10677 "failed to pin restored macOS special object",
10678 ));
10679 }
10680 let pinned = unsafe { fs::File::from_raw_fd(fd) };
10681 apply_restored_regular_file_metadata_parts(
10682 &pinned,
10683 path,
10684 RestoredRegularMetadata::from(&metadata.portable_mirror),
10685 Some(metadata),
10686 Some(staged),
10687 options,
10688 diagnostics,
10689 )
10690}
10691
10692#[cfg(not(any(target_os = "linux", target_os = "macos")))]
10693fn create_posix_special_object(
10694 _destination: &PreparedDestination,
10695 _path: &[u8],
10696 _kind: TarEntryKind,
10697 _metadata: &MemberMetadata,
10698 _staged: &mut Vec<StagedAuxiliary>,
10699 _options: SafeExtractionOptions,
10700 _diagnostics: &mut Vec<MetadataDiagnostic>,
10701) -> Result<(), FormatError> {
10702 Err(FormatError::ReaderUnsupported(
10703 "POSIX special-object restore is unavailable on this host",
10704 ))
10705}
10706
10707#[cfg(windows)]
10708struct WindowsReparseRollback<'a> {
10709 destination: &'a PreparedDestination,
10710 directory: bool,
10711 armed: bool,
10712}
10713
10714#[cfg(windows)]
10715impl Drop for WindowsReparseRollback<'_> {
10716 fn drop(&mut self) {
10717 if !self.armed {
10718 return;
10719 }
10720 if self.directory {
10721 let _ = self.destination.parent.remove_dir(&self.destination.leaf);
10722 } else {
10723 let _ = self
10724 .destination
10725 .parent
10726 .remove_file_or_symlink(&self.destination.leaf);
10727 }
10728 }
10729}
10730
10731#[cfg(windows)]
10732fn create_windows_reparse_object(
10733 destination: &PreparedDestination,
10734 path: &[u8],
10735 kind: TarEntryKind,
10736 metadata: &MemberMetadata,
10737 staged_auxiliary: &mut Vec<StagedAuxiliary>,
10738 options: SafeExtractionOptions,
10739 diagnostics: &mut Vec<MetadataDiagnostic>,
10740) -> Result<(), FormatError> {
10741 use std::ptr;
10742 use windows_sys::Win32::System::Ioctl::{FSCTL_GET_REPARSE_POINT, FSCTL_SET_REPARSE_POINT};
10743 use windows_sys::Win32::System::IO::DeviceIoControl;
10744
10745 let record = metadata
10746 .auxiliary
10747 .iter()
10748 .find(|record| record.kind == "windows.reparse-data")
10749 .ok_or(FormatError::InvalidArchive(
10750 "Windows reparse object lacks exact reparse data",
10751 ))?;
10752 let payload = record
10753 .capture_report_payload
10754 .as_deref()
10755 .ok_or(FormatError::InvalidArchive(
10756 "Windows reparse data was not retained",
10757 ))?;
10758 let tag = validate_windows_essential_reparse_data(payload)?;
10759 const IO_REPARSE_TAG_SYMLINK: u32 = 0xA000_000C;
10760 if (kind == TarEntryKind::Symlink) != (tag == IO_REPARSE_TAG_SYMLINK) {
10761 return Err(FormatError::InvalidArchive(
10762 "Windows reparse tag disagrees with primary object kind",
10763 ));
10764 }
10765 let attributes = metadata
10766 .primary_records
10767 .get("TZAP.windows.file-attributes")
10768 .map(|value| parse_lower_hex_u32(value, "Windows file attributes"))
10769 .transpose()?
10770 .ok_or(FormatError::InvalidArchive(
10771 "Windows reparse object lacks file attributes",
10772 ))?;
10773 let directory_object = attributes & FILE_ATTRIBUTE_DIRECTORY != 0;
10774 if kind == TarEntryKind::Directory && !directory_object {
10775 return Err(FormatError::InvalidArchive(
10776 "Windows junction is not a directory reparse object",
10777 ));
10778 }
10779 if options.overwrite_existing {
10780 remove_existing_leaf_if_needed(destination)?;
10781 }
10782 let mut rollback = WindowsReparseRollback {
10783 destination,
10784 directory: directory_object,
10785 armed: false,
10786 };
10787
10788 let file = if directory_object {
10789 destination
10790 .parent
10791 .create_dir(&destination.leaf)
10792 .map_err(|error| {
10793 if error.kind() == std::io::ErrorKind::AlreadyExists {
10794 FormatError::UnsafeOverwrite
10795 } else {
10796 FormatError::FilesystemExtractionFailed(
10797 "failed to create Windows reparse directory",
10798 )
10799 }
10800 })?;
10801 let mut open = CapOpenOptions::new();
10802 open.access_mode(FILE_GENERIC_READ | FILE_GENERIC_WRITE)
10803 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
10804 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
10805 .follow(FollowSymlinks::No);
10806 destination
10807 .parent
10808 .open_with(&destination.leaf, &open)
10809 .map(cap_std::fs::File::into_std)
10810 .map_err(|_| {
10811 FormatError::FilesystemExtractionFailed("failed to open Windows reparse directory")
10812 })?
10813 } else {
10814 let mut open = create_new_file_options();
10815 open.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
10816 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
10817 destination
10818 .parent
10819 .open_with(&destination.leaf, &open)
10820 .map(cap_std::fs::File::into_std)
10821 .map_err(|error| {
10822 if error.kind() == std::io::ErrorKind::AlreadyExists {
10823 FormatError::UnsafeOverwrite
10824 } else {
10825 FormatError::FilesystemExtractionFailed("failed to create Windows reparse file")
10826 }
10827 })?
10828 };
10829 rollback.armed = true;
10830
10831 let handle = file.as_raw_handle().cast();
10832 let mut bytes_returned = 0u32;
10833 if unsafe {
10836 DeviceIoControl(
10837 handle,
10838 FSCTL_SET_REPARSE_POINT,
10839 payload.as_ptr().cast(),
10840 payload.len() as u32,
10841 ptr::null_mut(),
10842 0,
10843 &mut bytes_returned,
10844 ptr::null_mut(),
10845 )
10846 } == 0
10847 {
10848 return Err(FormatError::FilesystemExtractionFailed(
10849 "failed to set Windows reparse data",
10850 ));
10851 }
10852
10853 let mut actual = vec![0u8; 16 * 1024];
10854 if unsafe {
10856 DeviceIoControl(
10857 handle,
10858 FSCTL_GET_REPARSE_POINT,
10859 ptr::null(),
10860 0,
10861 actual.as_mut_ptr().cast(),
10862 actual.len() as u32,
10863 &mut bytes_returned,
10864 ptr::null_mut(),
10865 )
10866 } == 0
10867 || actual.get(..bytes_returned as usize) != Some(payload)
10868 {
10869 return Err(FormatError::FilesystemExtractionFailed(
10870 "Windows reparse data did not verify after creation",
10871 ));
10872 }
10873 apply_windows_alternate_streams(&file, path, staged_auxiliary, options, diagnostics)?;
10874 apply_windows_security_descriptor(&file, path, metadata, options, diagnostics)?;
10875 apply_windows_basic_metadata(&file, path, metadata, options, diagnostics)?;
10876 rollback.armed = false;
10877 Ok(())
10878}
10879
10880fn path_components(path: &[u8]) -> Result<Vec<String>, FormatError> {
10881 validate_file_path_bytes(path, u32::MAX)?;
10882 let path = std::str::from_utf8(path).map_err(|_| FormatError::UnsafeArchivePath)?;
10883 Ok(path.split('/').map(str::to_owned).collect())
10884}
10885
10886fn ustar_path(header: &[u8]) -> Vec<u8> {
10887 let name = nul_trimmed(&header[0..100]);
10888 let prefix = nul_trimmed(&header[345..500]);
10889 if prefix.is_empty() {
10890 name.to_vec()
10891 } else {
10892 let mut out = Vec::with_capacity(prefix.len() + 1 + name.len());
10893 out.extend_from_slice(prefix);
10894 out.push(b'/');
10895 out.extend_from_slice(name);
10896 out
10897 }
10898}
10899
10900fn verify_tar_checksum(header: &[u8]) -> Result<(), FormatError> {
10901 let stored = parse_tar_octal(&header[148..156])?;
10902 let mut sum = 0u64;
10903 for (idx, byte) in header.iter().enumerate() {
10904 if (148..156).contains(&idx) {
10905 sum += b' ' as u64;
10906 } else {
10907 sum += *byte as u64;
10908 }
10909 }
10910 if stored != sum {
10911 return Err(FormatError::InvalidArchive("tar header checksum mismatch"));
10912 }
10913 Ok(())
10914}
10915
10916fn parse_tar_octal(field: &[u8]) -> Result<u64, FormatError> {
10917 let mut value = 0u64;
10918 let mut saw_digit = false;
10919 for byte in field {
10920 match *byte {
10921 0 | b' ' if saw_digit => break,
10922 0 | b' ' => {}
10923 b'0'..=b'7' => {
10924 saw_digit = true;
10925 value = value
10926 .checked_mul(8)
10927 .and_then(|acc| acc.checked_add((*byte - b'0') as u64))
10928 .ok_or(FormatError::InvalidArchive("tar octal field overflow"))?;
10929 }
10930 _ => return Err(FormatError::InvalidArchive("malformed tar octal field")),
10931 }
10932 }
10933 Ok(value)
10934}
10935
10936fn nul_trimmed(bytes: &[u8]) -> &[u8] {
10937 let end = bytes
10938 .iter()
10939 .position(|byte| *byte == 0)
10940 .unwrap_or(bytes.len());
10941 &bytes[..end]
10942}
10943
10944fn padding_to_512(len: usize) -> usize {
10945 let remainder = len % TAR_BLOCK_LEN;
10946 if remainder == 0 {
10947 0
10948 } else {
10949 TAR_BLOCK_LEN - remainder
10950 }
10951}
10952
10953fn padding_to_512_u64(len: u64) -> u64 {
10954 let remainder = len % TAR_BLOCK_LEN as u64;
10955 if remainder == 0 {
10956 0
10957 } else {
10958 TAR_BLOCK_LEN as u64 - remainder
10959 }
10960}
10961
10962fn slice(bytes: &[u8], offset: usize, len: usize) -> Result<&[u8], FormatError> {
10963 let end = checked_add(offset, len)?;
10964 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10965 structure: "tar member",
10966 expected: end,
10967 actual: bytes.len(),
10968 })
10969}
10970
10971fn checked_add(lhs: usize, rhs: usize) -> Result<usize, FormatError> {
10972 lhs.checked_add(rhs).ok_or(FormatError::InvalidArchive(
10973 "tar member arithmetic overflow",
10974 ))
10975}
10976
10977fn to_usize(value: u64) -> Result<usize, FormatError> {
10978 usize::try_from(value).map_err(|_| FormatError::InvalidArchive("tar member size overflow"))
10979}
10980
10981#[cfg(test)]
10982mod tests {
10983 use super::*;
10984 use tempfile::tempdir;
10985
10986 fn header(path: &[u8], kind: u8, size: usize, link: &[u8]) -> [u8; TAR_BLOCK_LEN] {
10987 let mut header = [0u8; TAR_BLOCK_LEN];
10988 header[..path.len()].copy_from_slice(path);
10989 write_octal(&mut header[100..108], 0o644);
10990 write_octal(&mut header[108..116], 0);
10991 write_octal(&mut header[116..124], 0);
10992 write_octal(&mut header[124..136], size as u64);
10993 write_octal(&mut header[136..148], 0);
10994 header[148..156].fill(b' ');
10995 header[156] = kind;
10996 header[157..157 + link.len()].copy_from_slice(link);
10997 header[257..263].copy_from_slice(b"ustar\0");
10998 header[263..265].copy_from_slice(b"00");
10999 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
11000 write_checksum(&mut header[148..156], checksum);
11001 header
11002 }
11003
11004 fn member(path: &[u8], kind: u8, data: &[u8], link: &[u8]) -> Vec<u8> {
11005 member_with_declared_size(path, kind, data.len(), data, link)
11006 }
11007
11008 fn member_with_declared_size(
11009 path: &[u8],
11010 kind: u8,
11011 declared_size: usize,
11012 data: &[u8],
11013 link: &[u8],
11014 ) -> Vec<u8> {
11015 let records =
11016 crate::entry_metadata::portable_primary_pax(path, 0o644, "other", false).unwrap();
11017 let pax = crate::entry_metadata::encode_canonical_pax(&records).unwrap();
11018 let mut pax_header = header(b"TZAP-PAX/PRIMARY", b'x', pax.len(), b"");
11019 write_octal(&mut pax_header[100..108], 0);
11020 pax_header[148..156].fill(b' ');
11021 let checksum = pax_header.iter().map(|byte| *byte as u64).sum::<u64>();
11022 write_checksum(&mut pax_header[148..156], checksum);
11023 let mut out = Vec::new();
11024 out.extend_from_slice(&pax_header);
11025 out.extend_from_slice(&pax);
11026 out.resize(out.len() + padding_to_512(pax.len()), 0);
11027 out.extend_from_slice(&header(path, kind, declared_size, link));
11028 out.extend_from_slice(data);
11029 out.resize(out.len() + padding_to_512(data.len()), 0);
11030 out
11031 }
11032
11033 fn member_with_prefix(prefix: &[u8], path: &[u8], kind: u8, data: &[u8]) -> Vec<u8> {
11034 let mut full_path = prefix.to_vec();
11035 full_path.push(b'/');
11036 full_path.extend_from_slice(path);
11037 let records =
11038 crate::entry_metadata::portable_primary_pax(&full_path, 0o644, "other", false).unwrap();
11039 let pax = crate::entry_metadata::encode_canonical_pax(&records).unwrap();
11040 let mut pax_header = header(b"TZAP-PAX/PRIMARY", b'x', pax.len(), b"");
11041 write_octal(&mut pax_header[100..108], 0);
11042 pax_header[148..156].fill(b' ');
11043 let checksum = pax_header.iter().map(|byte| *byte as u64).sum::<u64>();
11044 write_checksum(&mut pax_header[148..156], checksum);
11045 let mut header = header(path, kind, data.len(), b"");
11046 header[345..345 + prefix.len()].copy_from_slice(prefix);
11047 header[148..156].fill(b' ');
11048 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
11049 write_checksum(&mut header[148..156], checksum);
11050
11051 let mut out = Vec::new();
11052 out.extend_from_slice(&pax_header);
11053 out.extend_from_slice(&pax);
11054 out.resize(out.len() + padding_to_512(pax.len()), 0);
11055 out.extend_from_slice(&header);
11056 out.extend_from_slice(data);
11057 out.resize(out.len() + padding_to_512(data.len()), 0);
11058 out
11059 }
11060
11061 fn pax_record(key: &str, value: &[u8]) -> Vec<u8> {
11062 let mut len = key.len() + value.len() + 4;
11063 loop {
11064 let candidate = len.to_string().len() + 1 + key.len() + 1 + value.len() + 1;
11065 if candidate == len {
11066 break;
11067 }
11068 len = candidate;
11069 }
11070 let mut out = Vec::new();
11071 out.extend_from_slice(len.to_string().as_bytes());
11072 out.push(b' ');
11073 out.extend_from_slice(key.as_bytes());
11074 out.push(b'=');
11075 out.extend_from_slice(value);
11076 out.push(b'\n');
11077 out
11078 }
11079
11080 fn write_octal(field: &mut [u8], value: u64) {
11081 let digits = format!("{value:o}");
11082 field.fill(0);
11083 let start = field.len() - 1 - digits.len();
11084 field[..start].fill(b'0');
11085 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
11086 }
11087
11088 fn write_checksum(field: &mut [u8], value: u64) {
11089 let digits = format!("{value:06o}");
11090 field[0..6].copy_from_slice(digits.as_bytes());
11091 field[6] = 0;
11092 field[7] = b' ';
11093 }
11094
11095 #[cfg(windows)]
11096 #[test]
11097 fn security_descriptor_equivalence_only_normalizes_protection_on_absent_acls() {
11098 let descriptor = |control: u16| {
11099 let mut bytes = vec![1, 0];
11100 bytes.extend_from_slice(&control.to_le_bytes());
11101 bytes.extend_from_slice(&[0; 16]);
11102 bytes
11103 };
11104 let base = 0x8004u16;
11105 assert!(windows_security_descriptors_equivalent(
11106 &descriptor(base | 0x2000),
11107 &descriptor(base)
11108 ));
11109 assert!(!windows_security_descriptors_equivalent(
11110 &descriptor(base | 0x1000),
11111 &descriptor(base)
11112 ));
11113 assert!(!windows_security_descriptors_equivalent(
11114 &descriptor(base),
11115 &descriptor(base | 0x0008)
11116 ));
11117 let mut changed_body = descriptor(base | 0x2000);
11118 changed_body[10] = 1;
11119 assert!(!windows_security_descriptors_equivalent(
11120 &changed_body,
11121 &descriptor(base)
11122 ));
11123 }
11124
11125 #[cfg(windows)]
11126 #[test]
11127 fn security_descriptor_equivalence_ignores_self_relative_component_layout() {
11128 let owner = [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0];
11129 let group = [1, 1, 0, 0, 0, 0, 0, 5, 32, 2, 0, 0];
11130 let dacl = [2, 0, 8, 0, 0, 0, 0, 0];
11131 let descriptor = |order: [usize; 3]| {
11132 let components: [&[u8]; 3] = [&owner, &group, &dacl];
11133 let mut bytes = vec![0u8; 20];
11134 bytes[0] = 1;
11135 bytes[2..4].copy_from_slice(&0x8004u16.to_le_bytes());
11136 for index in order {
11137 let offset = bytes.len() as u32;
11138 let field = match index {
11139 0 => 4,
11140 1 => 8,
11141 2 => 16,
11142 _ => unreachable!(),
11143 };
11144 bytes[field..field + 4].copy_from_slice(&offset.to_le_bytes());
11145 bytes.extend_from_slice(components[index]);
11146 }
11147 bytes
11148 };
11149 let expected = descriptor([0, 1, 2]);
11150 let actual = descriptor([2, 1, 0]);
11151 assert_ne!(expected, actual);
11152 assert!(windows_security_descriptors_equivalent(&expected, &actual));
11153
11154 let mut changed_dacl = actual;
11155 let dacl_offset = u32::from_le_bytes(changed_dacl[16..20].try_into().unwrap()) as usize;
11156 changed_dacl[dacl_offset] = 4;
11157 assert!(!windows_security_descriptors_equivalent(
11158 &expected,
11159 &changed_dacl
11160 ));
11161 }
11162
11163 #[test]
11164 fn parses_ustar_regular_member() {
11165 let bytes = member(b"dir/file.txt", b'0', b"hello", b"");
11166 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
11167
11168 assert_eq!(parsed.kind, TarEntryKind::Regular);
11169 assert_eq!(parsed.path, b"dir/file.txt");
11170 assert_eq!(parsed.data, b"hello");
11171 assert_eq!(parsed.logical_size, 5);
11172 }
11173
11174 #[test]
11175 fn canonicalizes_one_directory_trailing_slash_only_for_directories() {
11176 let dir = member(b"dir/", b'5', b"", b"");
11177 assert_eq!(parse_tar_member_group(&dir, 4096).unwrap().path, b"dir");
11178
11179 let file = member(b"dir/", b'0', b"", b"");
11180 assert_eq!(
11181 parse_tar_member_group(&file, 4096).unwrap_err(),
11182 FormatError::UnsafeArchivePath
11183 );
11184 }
11185
11186 #[test]
11187 fn rejects_global_pax_headers() {
11188 let bytes = member(b"pax", b'g', b"11 path=x\n", b"");
11189 assert_eq!(
11190 parse_tar_member_group(&bytes, 4096).unwrap_err(),
11191 FormatError::InvalidArchive("global or GNU tar metadata is forbidden in revision 45")
11192 );
11193 }
11194
11195 #[test]
11196 fn rejects_global_pax_before_main_entry() {
11197 let global_pax = pax_record("path", b"poisoned.txt");
11198 let mut bytes = member(b"GlobalHead/path", b'g', &global_pax, b"");
11199 bytes.extend_from_slice(&member(b"safe.txt", b'0', b"abc", b""));
11200
11201 assert_eq!(
11202 parse_tar_member_group(&bytes, 4096).unwrap_err(),
11203 FormatError::InvalidArchive("global or GNU tar metadata is forbidden in revision 45")
11204 );
11205 }
11206
11207 #[test]
11208 fn rejects_global_gnu_headers() {
11209 for typeflag in *b"VMN" {
11210 let bytes = member(b"global", typeflag, b"archive-label", b"");
11211
11212 assert_eq!(
11213 parse_tar_member_group(&bytes, 4096).unwrap_err(),
11214 FormatError::InvalidArchive(
11215 "global or GNU tar metadata is forbidden in revision 45"
11216 ),
11217 "typeflag {typeflag:?}"
11218 );
11219 }
11220 }
11221
11222 #[test]
11223 fn rejects_unsupported_gnu_sparse_entry_type() {
11224 let bytes = member(b"sparse.bin", b'S', b"", b"");
11225
11226 assert_eq!(
11227 parse_tar_member_group(&bytes, 4096).unwrap_err(),
11228 FormatError::InvalidArchive("global or GNU tar metadata is forbidden in revision 45")
11229 );
11230 }
11231
11232 #[test]
11233 fn rejects_noncanonical_extra_local_pax_path_and_size() {
11234 let pax = pax_record("path", b"long/name.txt");
11235 let mut bytes = member(b"PaxHeaders/name", b'x', &pax, b"");
11236 bytes.extend_from_slice(&member(b"short", b'0', b"abc", b""));
11237
11238 assert!(parse_tar_member_group(&bytes, 4096).is_err());
11239 }
11240
11241 #[test]
11242 fn rejects_gnu_long_name_and_link_records() {
11243 let mut named = member(b"././@LongLink", b'L', b"long/path.txt\0", b"");
11244 named.extend_from_slice(&member(b"short", b'0', b"abc", b""));
11245 assert!(parse_tar_member_group(&named, 4096).is_err());
11246
11247 let mut linked = member(b"././@LongLink", b'K', b"target/file.txt\0", b"");
11248 linked.extend_from_slice(&member(b"short-link", b'2', b"", b"fallback"));
11249 assert!(parse_tar_member_group(&linked, 4096).is_err());
11250 }
11251
11252 #[test]
11253 fn supported_tar_metadata_profile_matrix_matches_buffered_and_streaming_parsers() {
11254 struct Case {
11255 name: &'static str,
11256 bytes: Vec<u8>,
11257 expected_path: &'static [u8],
11258 expected_kind: TarEntryKind,
11259 expected_data: &'static [u8],
11260 expected_link_target: Option<&'static [u8]>,
11261 expected_logical_size: u64,
11262 }
11263
11264 let cases = vec![
11265 Case {
11266 name: "regular ustar member",
11267 bytes: member(b"dir/file.txt", b'0', b"hello", b""),
11268 expected_path: b"dir/file.txt",
11269 expected_kind: TarEntryKind::Regular,
11270 expected_data: b"hello",
11271 expected_link_target: None,
11272 expected_logical_size: 5,
11273 },
11274 Case {
11275 name: "ustar prefix plus name",
11276 bytes: member_with_prefix(b"dir/prefix", b"file.txt", b'0', b"abc"),
11277 expected_path: b"dir/prefix/file.txt",
11278 expected_kind: TarEntryKind::Regular,
11279 expected_data: b"abc",
11280 expected_link_target: None,
11281 expected_logical_size: 3,
11282 },
11283 Case {
11284 name: "directory trailing slash",
11285 bytes: member(b"dir/", b'5', b"", b""),
11286 expected_path: b"dir",
11287 expected_kind: TarEntryKind::Directory,
11288 expected_data: b"",
11289 expected_link_target: None,
11290 expected_logical_size: 0,
11291 },
11292 Case {
11293 name: "canonical symlink",
11294 bytes: member(b"links/link", b'2', b"", b"target/file.txt"),
11295 expected_path: b"links/link",
11296 expected_kind: TarEntryKind::Symlink,
11297 expected_data: b"",
11298 expected_link_target: Some(b"target/file.txt"),
11299 expected_logical_size: 0,
11300 },
11301 ];
11302
11303 for case in cases {
11304 let parsed = parse_tar_member_group(&case.bytes, 4096).unwrap_or_else(|err| {
11305 panic!("{} should parse in buffered tar parser: {err:?}", case.name)
11306 });
11307 assert_eq!(parsed.path, case.expected_path, "{}", case.name);
11308 assert_eq!(parsed.kind, case.expected_kind, "{}", case.name);
11309 assert_eq!(parsed.data, case.expected_data, "{}", case.name);
11310 assert_eq!(
11311 parsed.link_target.as_deref(),
11312 case.expected_link_target,
11313 "{}",
11314 case.name
11315 );
11316 assert_eq!(
11317 parsed.logical_size, case.expected_logical_size,
11318 "{}",
11319 case.name
11320 );
11321
11322 let mut streaming = TarStreamSummaryValidator::with_observer(
11323 4096,
11324 u64::MAX,
11325 4096,
11326 16,
11327 NoopTarStreamObserver,
11328 );
11329 streaming.observe(&case.bytes).unwrap_or_else(|err| {
11330 panic!(
11331 "{} should parse in streaming tar parser: {err:?}",
11332 case.name
11333 )
11334 });
11335 let summary = streaming.finish().unwrap_or_else(|err| {
11336 panic!(
11337 "{} should finish in streaming tar parser: {err:?}",
11338 case.name
11339 )
11340 });
11341 assert_eq!(summary.members.len(), 1, "{}", case.name);
11342 let member = &summary.members[0];
11343 assert_eq!(member.path, case.expected_path, "{}", case.name);
11344 assert_eq!(member.kind, case.expected_kind, "{}", case.name);
11345 assert_eq!(
11346 member.link_target.as_deref(),
11347 case.expected_link_target,
11348 "{}",
11349 case.name
11350 );
11351 assert_eq!(
11352 member.logical_size, case.expected_logical_size,
11353 "{}",
11354 case.name
11355 );
11356 }
11357 }
11358
11359 #[test]
11360 fn tar_metadata_rejects_unsafe_or_inconsistent_overrides_matrix() {
11361 let mut pax_absolute_path = member(
11362 b"PaxHeaders/file",
11363 b'x',
11364 &pax_record("path", b"/absolute"),
11365 b"",
11366 );
11367 pax_absolute_path.extend_from_slice(&member(b"fallback", b'0', b"abc", b""));
11368
11369 let mut pax_parent_path = member(
11370 b"PaxHeaders/file",
11371 b'x',
11372 &pax_record("path", b"../escape"),
11373 b"",
11374 );
11375 pax_parent_path.extend_from_slice(&member(b"fallback", b'0', b"abc", b""));
11376
11377 let mut pax_absolute_link = member(
11378 b"PaxHeaders/link",
11379 b'x',
11380 &pax_record("linkpath", b"/target"),
11381 b"",
11382 );
11383 pax_absolute_link.extend_from_slice(&member(b"links/link", b'2', b"", b"safe"));
11384
11385 let mut gnu_unsafe_name = member(b"././@LongLink", b'L', b"bad:name.txt\0", b"");
11386 gnu_unsafe_name.extend_from_slice(&member(b"fallback", b'0', b"abc", b""));
11387
11388 let mut gnu_parent_hardlink = member(b"././@LongLink", b'K', b"../target.txt\0", b"");
11389 gnu_parent_hardlink.extend_from_slice(&member(b"links/hard", b'1', b"", b"safe"));
11390
11391 let mut pax_size_on_directory =
11392 member(b"PaxHeaders/dir", b'x', &pax_record("size", b"1"), b"");
11393 pax_size_on_directory
11394 .extend_from_slice(&member_with_declared_size(b"dir", b'5', 0, b"x", b""));
11395
11396 for (name, bytes) in [
11397 ("pax absolute path", pax_absolute_path),
11398 ("pax parent path", pax_parent_path),
11399 ("pax absolute symlink target", pax_absolute_link),
11400 ("gnu unsafe long name", gnu_unsafe_name),
11401 ("gnu hardlink parent target", gnu_parent_hardlink),
11402 ("pax size on directory", pax_size_on_directory),
11403 ] {
11404 assert!(parse_tar_member_group(&bytes, 4096).is_err(), "{name}");
11405
11406 let mut streaming = TarStreamSummaryValidator::with_observer(
11407 4096,
11408 u64::MAX,
11409 4096,
11410 16,
11411 NoopTarStreamObserver,
11412 );
11413 assert!(streaming.observe(&bytes).is_err(), "{name}");
11414 }
11415 }
11416
11417 #[test]
11418 fn pax_size_exceeding_available_group_is_rejected_by_buffered_and_streaming_parsers() {
11419 let mut bytes = member(b"PaxHeaders/file", b'x', &pax_record("size", b"4096"), b"");
11420 bytes.extend_from_slice(&member_with_declared_size(b"file", b'0', 0, b"short", b""));
11421
11422 assert!(parse_tar_member_group(&bytes, 4096).is_err());
11423
11424 let mut streaming = TarStreamSummaryValidator::with_observer(
11425 4096,
11426 u64::MAX,
11427 4096,
11428 16,
11429 NoopTarStreamObserver,
11430 );
11431 assert!(streaming.observe(&bytes).is_err());
11432 }
11433
11434 #[test]
11435 fn malformed_pax_record_matrix_rejects_before_metadata_is_trusted() {
11436 let cases: Vec<(&str, Vec<u8>)> = vec![
11437 ("missing length", b"path=file\n".to_vec()),
11438 ("missing space", b"12path=file\n".to_vec()),
11439 ("record too short", b"3 a\n".to_vec()),
11440 ("missing newline", b"11 path=file".to_vec()),
11441 ("missing equals", b"10 pathfile\n".to_vec()),
11442 ("non utf8 key", vec![7, b' ', 0xff, b'=', b'x', b'\n']),
11443 ("bad size value", pax_record("size", b"12x")),
11444 ];
11445
11446 for (name, payload) in cases {
11447 let mut bytes = member(b"PaxHeaders/file", b'x', &payload, b"");
11448 bytes.extend_from_slice(&member(b"file", b'0', b"abc", b""));
11449
11450 assert!(
11451 matches!(
11452 parse_tar_member_group(&bytes, 4096).unwrap_err(),
11453 FormatError::InvalidArchive(_)
11454 ),
11455 "{name}"
11456 );
11457
11458 let mut streaming = TarStreamSummaryValidator::with_observer(
11459 4096,
11460 u64::MAX,
11461 4096,
11462 16,
11463 NoopTarStreamObserver,
11464 );
11465 assert!(
11466 matches!(
11467 streaming.observe(&bytes).unwrap_err(),
11468 FormatError::InvalidArchive(_)
11469 ),
11470 "{name}"
11471 );
11472 }
11473 }
11474
11475 #[test]
11476 fn rejects_unregistered_legacy_xattr_and_acl_pax_keys() {
11477 let mut pax = Vec::new();
11478 pax.extend_from_slice(&pax_record("SCHILY.xattr.user.comment", b"hello"));
11479 pax.extend_from_slice(&pax_record("LIBARCHIVE.xattr.user.comment", b"hello"));
11480 pax.extend_from_slice(&pax_record("SCHILY.acl.access", b"user::rw-"));
11481 pax.extend_from_slice(&pax_record("LIBARCHIVE.acl.access", b"user::rw-"));
11482 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
11483 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
11484
11485 assert!(parse_tar_member_group(&bytes, 4096).is_err());
11486 }
11487
11488 #[test]
11489 fn rejects_unregistered_legacy_timestamp_pax_keys() {
11490 let mut pax = Vec::new();
11491 pax.extend_from_slice(&pax_record("atime", b"1.123456789"));
11492 pax.extend_from_slice(&pax_record("ctime", b"2.123456789"));
11493 pax.extend_from_slice(&pax_record("mtime", b"3.123456789"));
11494 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
11495 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
11496
11497 assert!(parse_tar_member_group(&bytes, 4096).is_err());
11498 }
11499
11500 #[test]
11501 fn rejects_noncanonical_sparse_and_unknown_pax_keys() {
11502 let mut pax = Vec::new();
11503 pax.extend_from_slice(&pax_record("GNU.sparse.realsize", b"1024"));
11504 pax.extend_from_slice(&pax_record("GNU.sparse.map", b"0,1"));
11505 pax.extend_from_slice(&pax_record("comment", b"ignored"));
11506 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
11507 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
11508
11509 assert!(parse_tar_member_group(&bytes, 4096).is_err());
11510 }
11511
11512 #[test]
11513 fn rejects_mixed_unregistered_local_pax_keys() {
11514 let mut pax = Vec::new();
11515 pax.extend_from_slice(&pax_record("SCHILY.xattr.user.comment", b"hello"));
11516 pax.extend_from_slice(&pax_record("GNU.sparse.realsize", b"1024"));
11517 pax.extend_from_slice(&pax_record("mtime", b"1.123456789"));
11518 pax.extend_from_slice(&pax_record("comment", b"ignored"));
11519 let mut bytes = member(b"PaxHeaders/file", b'x', &pax, b"");
11520 bytes.extend_from_slice(&member(b"file.txt", b'0', b"abc", b""));
11521
11522 assert!(parse_tar_member_group(&bytes, 4096).is_err());
11523 }
11524
11525 #[test]
11526 fn rejects_platform_escape_paths() {
11527 for path in [
11528 b"/abs".as_slice(),
11529 b"../up".as_slice(),
11530 b"a//b".as_slice(),
11531 b"a\\b".as_slice(),
11532 b"a:b".as_slice(),
11533 b"CON".as_slice(),
11534 ] {
11535 let bytes = member(path, b'0', b"", b"");
11536 assert_eq!(
11537 parse_tar_member_group(&bytes, 4096).unwrap_err(),
11538 FormatError::UnsafeArchivePath
11539 );
11540 }
11541 }
11542
11543 #[cfg(unix)]
11544 #[test]
11545 fn safe_restore_rejects_symlink_parent() {
11546 let tmp = tempdir().unwrap();
11547 let outside = tempdir().unwrap();
11548 std::os::unix::fs::symlink(outside.path(), tmp.path().join("link")).unwrap();
11549
11550 let member = OwnedTarMember {
11551 path: b"link/file.txt".to_vec(),
11552 kind: TarEntryKind::Regular,
11553 data: b"blocked".to_vec(),
11554 link_target: None,
11555 mode: 0o644,
11556 mtime: ArchiveTimestamp::UNIX_EPOCH,
11557 logical_size: 7,
11558 reparse_placeholder: false,
11559 v45_metadata: None,
11560 diagnostics: Vec::new(),
11561 };
11562
11563 assert_eq!(
11564 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap_err(),
11565 FormatError::UnsafeArchivePath
11566 );
11567 }
11568
11569 #[cfg(unix)]
11570 #[test]
11571 fn prepared_regular_file_uses_open_parent_after_parent_path_swap() {
11572 let tmp = tempdir().unwrap();
11573 let outside = tempdir().unwrap();
11574 let original_parent = tmp.path().join("a");
11575 let held_parent = tmp.path().join("held");
11576 fs::create_dir(&original_parent).unwrap();
11577
11578 let destination = prepare_destination(
11579 tmp.path(),
11580 b"a/file.txt",
11581 TarEntryKind::Regular,
11582 SafeExtractionOptions::default(),
11583 )
11584 .unwrap();
11585
11586 fs::rename(&original_parent, &held_parent).unwrap();
11587 std::os::unix::fs::symlink(outside.path(), &original_parent).unwrap();
11588
11589 let (temp_leaf, mut file) = create_temp_regular_file(&destination).unwrap();
11590 file.write_all(b"inside").unwrap();
11591 publish_regular_file(
11592 &destination,
11593 &temp_leaf,
11594 file,
11595 SafeExtractionOptions::default(),
11596 )
11597 .unwrap();
11598
11599 assert_eq!(fs::read(held_parent.join("file.txt")).unwrap(), b"inside");
11600 assert!(!outside.path().join("file.txt").exists());
11601 }
11602
11603 #[cfg(windows)]
11604 #[test]
11605 fn open_file_publication_preserves_even_and_odd_length_names() {
11606 let tmp = tempdir().unwrap();
11607 for name in ["a", "bb"] {
11608 let destination = prepare_destination(
11609 tmp.path(),
11610 name.as_bytes(),
11611 TarEntryKind::Regular,
11612 SafeExtractionOptions::default(),
11613 )
11614 .unwrap();
11615 let (temp_leaf, mut file) = create_temp_regular_file(&destination).unwrap();
11616 file.write_all(name.as_bytes()).unwrap();
11617 publish_regular_file(
11618 &destination,
11619 &temp_leaf,
11620 file,
11621 SafeExtractionOptions::default(),
11622 )
11623 .unwrap();
11624 assert_eq!(fs::read(tmp.path().join(name)).unwrap(), name.as_bytes());
11625 }
11626 let mut names = fs::read_dir(tmp.path())
11627 .unwrap()
11628 .map(|entry| entry.unwrap().file_name())
11629 .collect::<Vec<_>>();
11630 names.sort();
11631 assert_eq!(names, ["a", "bb"]);
11632 }
11633
11634 #[cfg(unix)]
11635 #[test]
11636 fn create_directory_rechecks_leaf_without_following_symlink() {
11637 let tmp = tempdir().unwrap();
11638 let outside = tempdir().unwrap();
11639 let destination = prepare_destination(
11640 tmp.path(),
11641 b"dir",
11642 TarEntryKind::Directory,
11643 SafeExtractionOptions::default(),
11644 )
11645 .unwrap();
11646
11647 std::os::unix::fs::symlink(outside.path(), tmp.path().join("dir")).unwrap();
11648
11649 assert_eq!(
11650 create_directory(&destination).unwrap_err(),
11651 FormatError::UnsafeArchivePath
11652 );
11653 assert!(outside.path().read_dir().unwrap().next().is_none());
11654 }
11655
11656 #[test]
11657 fn safe_restore_requires_hardlink_target_to_be_existing_regular_file() {
11658 let tmp = tempdir().unwrap();
11659 fs::write(tmp.path().join("target.txt"), b"target").unwrap();
11660 let member = OwnedTarMember {
11661 path: b"linked.txt".to_vec(),
11662 kind: TarEntryKind::Hardlink,
11663 data: Vec::new(),
11664 link_target: Some(b"target.txt".to_vec()),
11665 mode: 0o644,
11666 mtime: ArchiveTimestamp::UNIX_EPOCH,
11667 logical_size: 0,
11668 reparse_placeholder: false,
11669 v45_metadata: None,
11670 diagnostics: Vec::new(),
11671 };
11672
11673 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
11674 assert_eq!(fs::read(tmp.path().join("linked.txt")).unwrap(), b"target");
11675 }
11676
11677 #[cfg(unix)]
11678 #[test]
11679 fn restore_applies_regular_file_mode_metadata() {
11680 let tmp = tempdir().unwrap();
11681 let member = OwnedTarMember {
11682 path: b"script.sh".to_vec(),
11683 kind: TarEntryKind::Regular,
11684 data: b"#!/bin/sh\n".to_vec(),
11685 link_target: None,
11686 mode: 0o755,
11687 mtime: ArchiveTimestamp::UNIX_EPOCH,
11688 logical_size: 10,
11689 reparse_placeholder: false,
11690 v45_metadata: None,
11691 diagnostics: Vec::new(),
11692 };
11693
11694 let diagnostics =
11695 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
11696
11697 assert!(diagnostics.is_empty());
11698 let mode = fs::metadata(tmp.path().join("script.sh"))
11699 .unwrap()
11700 .permissions()
11701 .mode()
11702 & 0o777;
11703 assert_eq!(mode, 0o755);
11704 }
11705
11706 #[test]
11707 fn restore_applies_regular_file_mtime_metadata() {
11708 let tmp = tempdir().unwrap();
11709 let member = OwnedTarMember {
11710 path: b"dated.txt".to_vec(),
11711 kind: TarEntryKind::Regular,
11712 data: b"dated".to_vec(),
11713 link_target: None,
11714 mode: 0o666,
11715 mtime: ArchiveTimestamp::from_seconds(1_700_000_000),
11716 logical_size: 5,
11717 reparse_placeholder: false,
11718 v45_metadata: None,
11719 diagnostics: Vec::new(),
11720 };
11721
11722 let diagnostics =
11723 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
11724
11725 assert!(diagnostics.is_empty());
11726 let modified = fs::metadata(tmp.path().join("dated.txt"))
11727 .unwrap()
11728 .modified()
11729 .unwrap()
11730 .duration_since(SystemTime::UNIX_EPOCH)
11731 .unwrap()
11732 .as_secs();
11733 assert_eq!(modified, 1_700_000_000);
11734 }
11735
11736 #[test]
11737 fn restore_revalidates_symlink_targets_from_owned_members() {
11738 let tmp = tempdir().unwrap();
11739 let member = OwnedTarMember {
11740 path: b"link".to_vec(),
11741 kind: TarEntryKind::Symlink,
11742 data: Vec::new(),
11743 link_target: Some(b"/outside".to_vec()),
11744 mode: 0o644,
11745 mtime: ArchiveTimestamp::UNIX_EPOCH,
11746 logical_size: 0,
11747 reparse_placeholder: false,
11748 v45_metadata: None,
11749 diagnostics: Vec::new(),
11750 };
11751
11752 assert_eq!(
11753 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap_err(),
11754 FormatError::UnsafeArchivePath
11755 );
11756 assert!(!tmp.path().join("link").exists());
11757 }
11758
11759 #[test]
11760 fn skipped_entries_do_not_create_destination_parents() {
11761 let tmp = tempdir().unwrap();
11762 for (path, kind, target) in [
11763 (
11764 b"symlink-parent/link".as_slice(),
11765 TarEntryKind::Symlink,
11766 Some(b"target".to_vec()),
11767 ),
11768 (b"special-parent/fifo".as_slice(), TarEntryKind::Fifo, None),
11769 ] {
11770 let member = OwnedTarMember {
11771 path: path.to_vec(),
11772 kind,
11773 data: Vec::new(),
11774 link_target: target,
11775 mode: 0o644,
11776 mtime: ArchiveTimestamp::UNIX_EPOCH,
11777 logical_size: 0,
11778 reparse_placeholder: false,
11779 v45_metadata: None,
11780 diagnostics: Vec::new(),
11781 };
11782 restore_tar_member(
11783 tmp.path(),
11784 &member,
11785 SafeExtractionOptions {
11786 restore_policy: RestorePolicy::Content,
11787 ..SafeExtractionOptions::default()
11788 },
11789 )
11790 .unwrap();
11791 }
11792
11793 assert!(!tmp.path().join("symlink-parent").exists());
11794 assert!(!tmp.path().join("special-parent").exists());
11795 }
11796
11797 #[test]
11798 fn safe_restore_rejects_directory_over_existing_file_even_with_overwrite() {
11799 let tmp = tempdir().unwrap();
11800 let conflict = tmp.path().join("conflict");
11801 fs::write(&conflict, b"not a directory").unwrap();
11802 let member = OwnedTarMember {
11803 path: b"conflict".to_vec(),
11804 kind: TarEntryKind::Directory,
11805 data: Vec::new(),
11806 link_target: None,
11807 mode: 0o644,
11808 mtime: ArchiveTimestamp::UNIX_EPOCH,
11809 logical_size: 0,
11810 reparse_placeholder: false,
11811 v45_metadata: None,
11812 diagnostics: Vec::new(),
11813 };
11814
11815 assert_eq!(
11816 restore_tar_member(
11817 tmp.path(),
11818 &member,
11819 SafeExtractionOptions {
11820 overwrite_existing: true,
11821 ..SafeExtractionOptions::default()
11822 }
11823 )
11824 .unwrap_err(),
11825 FormatError::UnsafeOverwrite
11826 );
11827 assert!(conflict.is_file());
11828 }
11829
11830 #[test]
11831 fn hardlink_target_checks_use_component_position_not_value() {
11832 let tmp = tempdir().unwrap();
11833 fs::create_dir(tmp.path().join("a")).unwrap();
11834 fs::write(tmp.path().join("a").join("a"), b"target").unwrap();
11835 let member = OwnedTarMember {
11836 path: b"linked.txt".to_vec(),
11837 kind: TarEntryKind::Hardlink,
11838 data: Vec::new(),
11839 link_target: Some(b"a/a".to_vec()),
11840 mode: 0o644,
11841 mtime: ArchiveTimestamp::UNIX_EPOCH,
11842 logical_size: 0,
11843 reparse_placeholder: false,
11844 v45_metadata: None,
11845 diagnostics: Vec::new(),
11846 };
11847
11848 restore_tar_member(tmp.path(), &member, SafeExtractionOptions::default()).unwrap();
11849 assert_eq!(fs::read(tmp.path().join("linked.txt")).unwrap(), b"target");
11850 }
11851
11852 #[test]
11853 fn hardlink_targets_obey_max_path_length() {
11854 let bytes = member(b"link", b'1', b"", b"long/name");
11855
11856 assert_eq!(
11857 parse_tar_member_group(&bytes, 4).unwrap_err(),
11858 FormatError::UnsafeArchivePath
11859 );
11860 }
11861
11862 fn member_summary(bytes: &[u8], group_start: u64) -> TarStreamMemberSummary {
11863 let parsed = parse_tar_member_group(bytes, 4096).unwrap();
11864 TarStreamMemberSummary {
11865 path: parsed.path,
11866 kind: parsed.kind,
11867 link_target: parsed.link_target,
11868 mode: parsed.mode,
11869 mtime: parsed.mtime,
11870 logical_size: parsed.logical_size,
11871 file_entry_flags: parsed.v45_metadata.file_entry_flags,
11872 reparse_placeholder: parsed.reparse_placeholder,
11873 v45_metadata: parsed.v45_metadata,
11874 diagnostics: parsed.diagnostics,
11875 group_start,
11876 group_size: bytes.len() as u64,
11877 }
11878 }
11879
11880 #[test]
11881 fn member_graph_accepts_hardlink_target_after_alias_and_rejects_mirror_mismatch() {
11882 let alias_bytes = member(b"alias.txt", b'1', b"", b"target.txt");
11883 let target_bytes = member(b"target.txt", b'0', b"payload", b"");
11884 let alias = member_summary(&alias_bytes, 0);
11885 let target = member_summary(&target_bytes, alias_bytes.len() as u64);
11886 assert!(validate_v45_member_graph(&[alias.clone(), target.clone()]).is_ok());
11887
11888 let mut mismatched_alias = alias;
11889 mismatched_alias.v45_metadata.portable_mirror.mode = 0o600;
11890 assert_eq!(
11891 validate_v45_member_graph(&[mismatched_alias, target]).unwrap_err(),
11892 FormatError::InvalidArchive(
11893 "hardlink portable metadata mirror differs from canonical target"
11894 )
11895 );
11896 }
11897
11898 #[test]
11899 fn member_graph_rejects_writes_below_selected_symlink() {
11900 let link_bytes = member(b"dir", b'2', b"", b"target");
11901 let child_bytes = member(b"dir/file.txt", b'0', b"payload", b"");
11902 let link = member_summary(&link_bytes, 0);
11903 let child = member_summary(&child_bytes, link_bytes.len() as u64);
11904
11905 assert_eq!(
11906 validate_v45_member_graph(&[link, child]).unwrap_err(),
11907 FormatError::InvalidArchive(
11908 "selected path graph traverses a symlink or reparse ancestor"
11909 )
11910 );
11911 }
11912
11913 #[test]
11914 fn partial_capture_diagnostics_preserve_authenticated_omission_details() {
11915 let bytes = member(b"file.txt", b'0', b"payload", b"");
11916 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
11917 let mut metadata = parsed.v45_metadata;
11918 metadata.declaration.capture_status = CaptureStatus::Partial;
11919 metadata.capture_report = Some(vec![CaptureReportRow {
11920 profile: "portable-v1".into(),
11921 metadata_class: "sparse-layout".into(),
11922 reason: "changed-during-read".into(),
11923 encoded_detail: "extent%20map%20changed".into(),
11924 }]);
11925
11926 let diagnostics = plan_restore(
11927 b"file.txt",
11928 &metadata,
11929 TarEntryKind::Regular,
11930 false,
11931 SafeExtractionOptions {
11932 allow_degraded: true,
11933 ..SafeExtractionOptions::default()
11934 },
11935 )
11936 .unwrap();
11937
11938 assert!(diagnostics.iter().any(|diagnostic| {
11939 diagnostic.profile == "portable-v1"
11940 && diagnostic.metadata_class == "sparse-layout"
11941 && diagnostic.operation == MetadataOperation::Capture
11942 && diagnostic.status == MetadataDiagnosticStatus::Partial
11943 && diagnostic.message
11944 == "capture omission: changed-during-read; detail=extent%20map%20changed"
11945 }));
11946 }
11947
11948 #[test]
11949 fn content_restore_reports_portable_mode_and_mtime_as_skipped() {
11950 let bytes = member(b"file.txt", b'0', b"payload", b"");
11951 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
11952
11953 let diagnostics = plan_restore(
11954 b"file.txt",
11955 &parsed.v45_metadata,
11956 TarEntryKind::Regular,
11957 false,
11958 SafeExtractionOptions {
11959 restore_policy: RestorePolicy::Content,
11960 ..SafeExtractionOptions::default()
11961 },
11962 )
11963 .unwrap();
11964
11965 for metadata_class in ["mode", "mtime"] {
11966 assert!(diagnostics.iter().any(|diagnostic| {
11967 diagnostic.profile == "portable-v1"
11968 && diagnostic.metadata_class == metadata_class
11969 && diagnostic.status == MetadataDiagnosticStatus::Skipped
11970 && diagnostic.restore_policy == Some(RestorePolicy::Content)
11971 }));
11972 }
11973 }
11974
11975 #[test]
11976 fn unsupported_required_profile_needs_explicit_degraded_restore() {
11977 let bytes = member(b"file.txt", b'0', b"payload", b"");
11978 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
11979 let mut metadata = parsed.v45_metadata;
11980 metadata
11981 .declaration
11982 .required_profiles
11983 .push("x.com.example.test-v1".into());
11984 metadata
11985 .declaration
11986 .optional_profiles
11987 .push("x.com.example.optional-v1".into());
11988
11989 assert_eq!(
11990 plan_restore(
11991 b"file.txt",
11992 &metadata,
11993 TarEntryKind::Regular,
11994 false,
11995 SafeExtractionOptions::default(),
11996 )
11997 .unwrap_err(),
11998 FormatError::ReaderUnsupported(
11999 "requested restore policy requires an unsupported required profile"
12000 )
12001 );
12002 let diagnostics = plan_restore(
12003 b"file.txt",
12004 &metadata,
12005 TarEntryKind::Regular,
12006 false,
12007 SafeExtractionOptions {
12008 allow_degraded: true,
12009 ..SafeExtractionOptions::default()
12010 },
12011 )
12012 .unwrap();
12013 assert!(diagnostics.iter().any(|diagnostic| {
12014 diagnostic.profile == "x.com.example.test-v1"
12015 && diagnostic.metadata_class == "required-profile"
12016 && diagnostic.status == MetadataDiagnosticStatus::Unsupported
12017 }));
12018 assert!(diagnostics.iter().any(|diagnostic| {
12019 diagnostic.profile == "x.com.example.optional-v1"
12020 && diagnostic.metadata_class == "optional-profile"
12021 && diagnostic.status == MetadataDiagnosticStatus::Skipped
12022 }));
12023 }
12024
12025 #[test]
12026 fn portable_directory_metadata_is_supported_without_degradation() {
12027 let bytes = member(b"dir", b'5', b"", b"");
12028 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12029
12030 let diagnostics = plan_restore(
12031 b"dir",
12032 &parsed.v45_metadata,
12033 TarEntryKind::Directory,
12034 false,
12035 SafeExtractionOptions::default(),
12036 )
12037 .unwrap();
12038 assert!(diagnostics.is_empty());
12039 }
12040
12041 #[cfg(target_os = "linux")]
12042 #[test]
12043 fn exact_linux_restore_rejects_unrecognized_inode_flag_bits() {
12044 let bytes = member(b"file.txt", b'0', b"payload", b"");
12045 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12046 let mut metadata = parsed.v45_metadata;
12047 metadata.declaration.source_os = "linux".into();
12048 metadata
12049 .declaration
12050 .required_profiles
12051 .push("linux-backup-v1".into());
12052 metadata.declaration.required_profiles.sort();
12053 metadata.primary_has_native_scalar = true;
12054 metadata
12055 .primary_records
12056 .insert("TZAP.linux.fsflags".into(), b"0000000080000000".to_vec());
12057
12058 assert_eq!(
12059 plan_restore(
12060 b"file.txt",
12061 &metadata,
12062 TarEntryKind::Regular,
12063 false,
12064 SafeExtractionOptions {
12065 restore_policy: RestorePolicy::System,
12066 system_authorized: true,
12067 ..SafeExtractionOptions::default()
12068 },
12069 )
12070 .unwrap_err(),
12071 FormatError::ReaderUnsupported(
12072 "requested native metadata is not supported by this conformance class"
12073 )
12074 );
12075 }
12076
12077 #[cfg(target_os = "macos")]
12078 #[test]
12079 fn macos_restore_plans_unknown_and_system_flags_without_silently_applying_them() {
12080 let bytes = member(b"file.txt", b'0', b"payload", b"");
12081 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12082 let mut metadata = parsed.v45_metadata;
12083 metadata.declaration.source_os = "macos".into();
12084 metadata
12085 .declaration
12086 .required_profiles
12087 .extend(["macos-backup-v1".into(), "posix-backup-v1".into()]);
12088 metadata.declaration.required_profiles.sort();
12089 metadata.declaration.required_profiles.dedup();
12090 metadata.primary_has_native_scalar = true;
12091 metadata
12094 .primary_records
12095 .insert("TZAP.macos.st-flags".into(), b"0000000000000022".to_vec());
12096
12097 let diagnostics = plan_restore(
12098 b"file.txt",
12099 &metadata,
12100 TarEntryKind::Regular,
12101 false,
12102 SafeExtractionOptions {
12103 restore_policy: RestorePolicy::SameOs,
12104 ..SafeExtractionOptions::default()
12105 },
12106 )
12107 .unwrap();
12108 assert!(diagnostics.iter().any(|diagnostic| {
12109 diagnostic.metadata_class == "unrecognized-file-flags"
12110 && diagnostic.status == MetadataDiagnosticStatus::Skipped
12111 }));
12112 assert!(diagnostics.iter().any(|diagnostic| {
12113 diagnostic.metadata_class == "system-file-flags"
12114 && diagnostic.status == MetadataDiagnosticStatus::Skipped
12115 }));
12116 }
12117
12118 #[cfg(target_os = "macos")]
12119 #[test]
12120 fn macos_required_unknown_ordinary_flag_needs_explicit_degraded_restore() {
12121 let bytes = member(b"file.txt", b'0', b"payload", b"");
12122 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12123 let mut metadata = parsed.v45_metadata;
12124 metadata.declaration.source_os = "macos".into();
12125 metadata
12126 .declaration
12127 .required_profiles
12128 .extend(["macos-backup-v1".into(), "posix-backup-v1".into()]);
12129 metadata.declaration.required_profiles.sort();
12130 metadata.declaration.required_profiles.dedup();
12131 metadata.primary_has_native_scalar = true;
12132 metadata
12133 .primary_records
12134 .insert("TZAP.macos.st-flags".into(), b"0000000000000020".to_vec());
12135
12136 let strict = plan_restore(
12137 b"file.txt",
12138 &metadata,
12139 TarEntryKind::Regular,
12140 false,
12141 SafeExtractionOptions {
12142 restore_policy: RestorePolicy::SameOs,
12143 ..SafeExtractionOptions::default()
12144 },
12145 );
12146 assert_eq!(
12147 strict.unwrap_err(),
12148 FormatError::ReaderUnsupported(
12149 "requested native metadata is not supported by this conformance class"
12150 )
12151 );
12152 let degraded = plan_restore(
12153 b"file.txt",
12154 &metadata,
12155 TarEntryKind::Regular,
12156 false,
12157 SafeExtractionOptions {
12158 restore_policy: RestorePolicy::SameOs,
12159 allow_degraded: true,
12160 ..SafeExtractionOptions::default()
12161 },
12162 )
12163 .unwrap();
12164 assert!(degraded.iter().any(|diagnostic| {
12165 diagnostic.metadata_class == "unrecognized-file-flags"
12166 && diagnostic.status == MetadataDiagnosticStatus::Skipped
12167 }));
12168 }
12169
12170 #[cfg(target_os = "macos")]
12171 #[test]
12172 fn macos_unregistered_superuser_flag_stays_system_class() {
12173 let bytes = member(b"file.txt", b'0', b"payload", b"");
12174 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12175 let mut metadata = parsed.v45_metadata;
12176 metadata.declaration.source_os = "macos".into();
12177 metadata
12178 .declaration
12179 .required_profiles
12180 .extend(["macos-backup-v1".into(), "posix-backup-v1".into()]);
12181 metadata.declaration.required_profiles.sort();
12182 metadata.declaration.required_profiles.dedup();
12183 metadata.primary_has_native_scalar = true;
12184 metadata
12186 .primary_records
12187 .insert("TZAP.macos.st-flags".into(), b"0000000000100000".to_vec());
12188
12189 let same_os = plan_restore(
12190 b"file.txt",
12191 &metadata,
12192 TarEntryKind::Regular,
12193 false,
12194 SafeExtractionOptions {
12195 restore_policy: RestorePolicy::SameOs,
12196 ..SafeExtractionOptions::default()
12197 },
12198 )
12199 .unwrap();
12200 assert!(same_os.iter().any(|diagnostic| {
12201 diagnostic.metadata_class == "system-file-flags"
12202 && diagnostic.status == MetadataDiagnosticStatus::Skipped
12203 }));
12204 assert_eq!(
12205 plan_restore(
12206 b"file.txt",
12207 &metadata,
12208 TarEntryKind::Regular,
12209 false,
12210 SafeExtractionOptions {
12211 restore_policy: RestorePolicy::System,
12212 system_authorized: true,
12213 ..SafeExtractionOptions::default()
12214 },
12215 )
12216 .unwrap_err(),
12217 FormatError::ReaderUnsupported(
12218 "requested native metadata is not supported by this conformance class"
12219 )
12220 );
12221 }
12222
12223 #[cfg(target_os = "macos")]
12224 #[test]
12225 fn macos_system_file_flags_fail_preflight_without_superuser_privilege() {
12226 if unsafe { libc::geteuid() } == 0 {
12227 return;
12228 }
12229 let bytes = member(b"file.txt", b'0', b"payload", b"");
12230 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12231 let mut metadata = parsed.v45_metadata;
12232 metadata.declaration.source_os = "macos".into();
12233 metadata
12234 .declaration
12235 .required_profiles
12236 .extend(["macos-backup-v1".into(), "posix-backup-v1".into()]);
12237 metadata.declaration.required_profiles.sort();
12238 metadata.declaration.required_profiles.dedup();
12239 metadata.primary_has_native_scalar = true;
12240 metadata
12241 .primary_records
12242 .insert("TZAP.macos.st-flags".into(), b"0000000000020000".to_vec());
12243
12244 assert_eq!(
12245 plan_restore(
12246 b"file.txt",
12247 &metadata,
12248 TarEntryKind::Regular,
12249 false,
12250 SafeExtractionOptions {
12251 restore_policy: RestorePolicy::System,
12252 system_authorized: true,
12253 ..SafeExtractionOptions::default()
12254 },
12255 )
12256 .unwrap_err(),
12257 FormatError::ReaderUnsupported(
12258 "requested native metadata is not supported by this conformance class"
12259 )
12260 );
12261 }
12262
12263 #[cfg(target_os = "macos")]
12264 #[test]
12265 fn macos_device_restore_fails_preflight_without_superuser_privilege() {
12266 if unsafe { libc::geteuid() } == 0 {
12267 return;
12268 }
12269 let bytes = member(b"device", b'0', b"", b"");
12270 let parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12271
12272 assert_eq!(
12273 plan_restore(
12274 b"device",
12275 &parsed.v45_metadata,
12276 TarEntryKind::CharacterDevice,
12277 false,
12278 SafeExtractionOptions {
12279 restore_policy: RestorePolicy::System,
12280 system_authorized: true,
12281 ..SafeExtractionOptions::default()
12282 },
12283 )
12284 .unwrap_err(),
12285 FormatError::ReaderUnsupported(
12286 "requested native metadata is not supported by this conformance class"
12287 )
12288 );
12289 }
12290
12291 #[cfg(target_os = "macos")]
12292 #[test]
12293 fn macos_resource_fork_support_is_primary_kind_aware() {
12294 let record = AuxiliaryRecord {
12295 ordinal: 0,
12296 kind: "macos.resource-fork".into(),
12297 profile: "macos-backup-v1".into(),
12298 restore_class: RestoreClass::SameOs,
12299 native: true,
12300 name_encoding: "none".into(),
12301 decoded_name: Vec::new(),
12302 flags: 0,
12303 logical_size: u64::from(u32::MAX) + 1,
12304 stored_size: 0,
12305 sha256: [0; 32],
12306 meta: BTreeMap::new(),
12307 sparse_layout: None,
12308 capture_report_payload: None,
12309 };
12310 assert!(native_auxiliary_restore_supported(
12311 &record,
12312 false,
12313 Some(TarEntryKind::Regular)
12314 ));
12315 assert!(!native_auxiliary_restore_supported(
12316 &record,
12317 false,
12318 Some(TarEntryKind::Symlink)
12319 ));
12320 assert!(!native_auxiliary_restore_supported(
12321 &record,
12322 false,
12323 Some(TarEntryKind::Fifo)
12324 ));
12325 }
12326
12327 #[cfg(target_os = "linux")]
12328 #[test]
12329 fn generic_xattr_auxiliary_failure_is_bound_to_pinned_special_object() {
12330 use sha2::{Digest as _, Sha256};
12331 use std::ffi::CString;
12332 use std::os::unix::ffi::OsStrExt as _;
12333
12334 let temp = tempfile::tempdir().unwrap();
12335 let fifo = temp.path().join("events.fifo");
12336 let fifo_c = CString::new(fifo.as_os_str().as_bytes()).unwrap();
12337 assert_eq!(unsafe { libc::mkfifo(fifo_c.as_ptr(), 0o600) }, 0);
12338 let value = b"member-bound auxiliary value";
12339 let mut staged_file = tempfile::tempfile().unwrap();
12340 staged_file.write_all(value).unwrap();
12341 staged_file.seek(SeekFrom::Start(0)).unwrap();
12342 let mut staged = vec![StagedAuxiliary {
12343 record: AuxiliaryRecord {
12344 ordinal: 0,
12345 kind: "generic.xattr".into(),
12346 profile: "posix-backup-v1".into(),
12347 restore_class: RestoreClass::SameOs,
12348 native: true,
12349 name_encoding: "bytes".into(),
12350 decoded_name: b"user.tzap-aux".to_vec(),
12351 flags: 0,
12352 logical_size: value.len() as u64,
12353 stored_size: value.len() as u64,
12354 sha256: Sha256::digest(value).into(),
12355 meta: BTreeMap::new(),
12356 sparse_layout: None,
12357 capture_report_payload: None,
12358 },
12359 file: staged_file,
12360 }];
12361 let mut diagnostics = Vec::new();
12362
12363 apply_generic_xattr_auxiliaries_to_path(
12364 &fifo,
12365 true,
12366 b"events.fifo",
12367 &mut staged,
12368 SafeExtractionOptions {
12369 restore_policy: RestorePolicy::SameOs,
12370 allow_degraded: true,
12371 ..SafeExtractionOptions::default()
12372 },
12373 &mut diagnostics,
12374 )
12375 .unwrap();
12376
12377 assert!(staged.is_empty());
12378 assert!(diagnostics.iter().any(|diagnostic| {
12379 diagnostic.metadata_class == "extended-attribute"
12380 && diagnostic.status == MetadataDiagnosticStatus::Failed
12381 }));
12382 assert_eq!(xattr::get(&fifo, "user.tzap-aux").unwrap(), None);
12383 }
12384
12385 #[test]
12386 fn sparse_layout_materialization_requires_explicit_degraded_portable_restore() {
12387 let bytes = member(b"sparse.bin", b'0', b"data", b"");
12388 let mut parsed = parse_tar_member_group(&bytes, 4096).unwrap();
12389 parsed.v45_metadata.file_entry_flags |= HAS_SPARSE_EXTENTS;
12390
12391 let strict = plan_restore(
12392 b"sparse.bin",
12393 &parsed.v45_metadata,
12394 TarEntryKind::Regular,
12395 false,
12396 SafeExtractionOptions::default(),
12397 );
12398 #[cfg(any(windows, target_os = "linux"))]
12399 assert!(strict.unwrap().is_empty());
12400 #[cfg(not(any(windows, target_os = "linux")))]
12401 assert_eq!(
12402 strict.unwrap_err(),
12403 FormatError::ReaderUnsupported(
12404 "sparse layout materialization needs explicit degraded restore"
12405 )
12406 );
12407
12408 let degraded = plan_restore(
12409 b"sparse.bin",
12410 &parsed.v45_metadata,
12411 TarEntryKind::Regular,
12412 false,
12413 SafeExtractionOptions {
12414 allow_degraded: true,
12415 ..SafeExtractionOptions::default()
12416 },
12417 )
12418 .unwrap();
12419 #[cfg(any(windows, target_os = "linux"))]
12420 assert!(degraded.is_empty());
12421 #[cfg(not(any(windows, target_os = "linux")))]
12422 assert!(degraded.iter().any(|diagnostic| {
12423 diagnostic.metadata_class == "sparse-layout"
12424 && diagnostic.status == MetadataDiagnosticStatus::Materialized
12425 && diagnostic.restore_policy == Some(RestorePolicy::Portable)
12426 }));
12427
12428 let content = plan_restore(
12429 b"sparse.bin",
12430 &parsed.v45_metadata,
12431 TarEntryKind::Regular,
12432 false,
12433 SafeExtractionOptions {
12434 restore_policy: RestorePolicy::Content,
12435 ..SafeExtractionOptions::default()
12436 },
12437 )
12438 .unwrap();
12439 assert!(content.iter().any(|diagnostic| {
12440 diagnostic.metadata_class == "sparse-layout"
12441 && diagnostic.restore_policy == Some(RestorePolicy::Content)
12442 }));
12443 }
12444}