Skip to main content

tzap_core/
tar_model.rs

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;
47// UF_IMMUTABLE/UF_APPEND, entitlement-protected UF_DATAVAULT, and every
48// Darwin SF_SUPPORTED bit have System-class restore semantics even when this
49// reader deliberately does not register the bit for built-in application.
50const 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        // Setting system file flags is restricted to the superuser by Darwin.
76        (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    /// Permit a requested same-OS/system operation to skip unsupported
373    /// authenticated metadata with durable diagnostics.
374    pub allow_degraded: bool,
375    /// Explicit caller authorization for system-class restoration. The core
376    /// implementation still applies only system items it understands.
377    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        // Opaque registered or user-defined tags have tag-specific payloads that cannot be
1476        // decoded here. The common header and exact length were validated above; preserve the
1477        // bytes without interpreting or following the reparse point.
1478        _ => 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    // SAFETY: `token` is a valid output slot and the process pseudo-handle is live.
3735    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        // SAFETY: the one-element privilege array provides valid input/output storage.
3751        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        // SAFETY: `token` is live and the initialized one-entry structure is readable.
3759        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    // SAFETY: `token` was returned by OpenProcessToken and is closed once.
3767    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    // SAFETY: the descriptor is live and the checked range lies within the logical file.
4895    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            // SAFETY: every path is retained as a NUL-terminated UTF-16 buffer until this call.
5451            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            // SAFETY: this context was returned by OpenEncryptedFileRawW and is closed once.
5468            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    // SAFETY: the handle is live; the zero-length query returns the required UTF-16 count.
5481    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    // SAFETY: `path` provides the queried capacity and remains writable for the call.
5494    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    // SAFETY: `path` is NUL-terminated and `context` is a writable output slot.
5516    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    // SAFETY: WriteEncryptedFileRaw passes back the context pointer supplied by the caller for
5544    // the duration of the synchronous call, and provides a writable buffer of `*length` bytes.
5545    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    // SAFETY: ReadEncryptedFileRaw passes back the context pointer supplied by the caller and a
5583    // readable byte range for the duration of this synchronous callback.
5584    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    // SAFETY: the callback and its stack context remain live for this synchronous export.
5602    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    // SAFETY: the callback, staged file, and callback context remain live for this synchronous
5685    // import, and `context` is an import context returned for the resolved temporary path.
5686    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    // SAFETY: the handle is live; the zero-length query returns the required UTF-16 count.
5776    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    // SAFETY: `base_path` provides the queried capacity and remains writable for the call.
5791    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        // SAFETY: the base path comes from the pinned destination handle and the suffix passed
5838        // built-in UTF-16 alternate-stream grammar validation during archive parsing.
5839        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        // SAFETY: ownership of the newly created handle transfers to `stream` exactly once.
5869        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    // SAFETY: the source handle is live; the returned handle, if valid, receives independent
5930    // ownership and is converted to `File` exactly once.
5931    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    // SAFETY: ownership of the newly reopened handle transfers to `destination` once.
5958    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    // SAFETY: aborting with an empty buffer releases exactly this BackupWrite context.
5992    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        // SAFETY: `desired` is live and writable, and the slice covers exactly its object
6064        // representation so the authenticated 64-byte stream can be copied without alignment loss.
6065        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    // SAFETY: the destination handle is live and `desired` is a fully initialized fixed-size
6106    // FILE_OBJECTID_BUFFER retained for the duration of this synchronous control request.
6107    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    // SAFETY: the destination handle and writable `actual` output buffer remain live for this
6123    // synchronous request, with the exact structure size supplied to the kernel.
6124    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    // SAFETY: both initialized structures remain live and are viewed over their exact object
6137    // representations solely for byte-for-byte verification.
6138    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        // SAFETY: the destination and context are live, and the input slice is readable for the
6178        // exact requested byte count during this synchronous call.
6179        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    // SAFETY: `path` is writable for PATH_MAX bytes and F_GETPATH writes a NUL-terminated path.
6446    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                // SAFETY: the external form was structurally bounded above; returned ACLs are freed.
6660                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    // SAFETY: `file` owns a live descriptor and the desired value was range checked.
6884    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    // SAFETY: the original handle is live and flags preserve no-follow access to its object.
7133    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    // SAFETY: the parser-validated self-relative descriptor remains readable and every
7171    // component output points to initialized local storage for these calls.
7172    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        // SAFETY: the handle is live and the validated descriptor contains the selected fields.
7198        && 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            // SAFETY: the handle and DACL pointer remain live for automatic-inheritance apply.
7207            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            // SAFETY: the handle is live and the validated descriptor contains the DACL.
7223            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            // SAFETY: the handle and SACL pointer remain live for automatic-inheritance apply.
7233            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            // SAFETY: the handle is live and the validated descriptor contains the SACL.
7249            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    // SAFETY: the null-buffer query returns the descriptor size through `needed`.
7276    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    // SAFETY: `actual` has the queried size and remains writable for the call.
7288    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    // A self-relative descriptor does not prescribe component order or offsets. In particular,
7358    // EFS import followed by GetKernelObjectSecurity can return the same SIDs and ACLs in a
7359    // differently packed buffer than GetSecurityInfo used during capture. Compare the represented
7360    // components rather than requiring byte-identical offset fields and padding.
7361    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    // SAFETY: `handle` is live and `current` is a correctly sized writable structure.
7507    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    // SAFETY: `handle` is live and `restored` is a correctly sized initialized structure.
7602    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    // SAFETY: `handle` is live and `actual` is a correctly sized writable structure.
7631    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    // SAFETY: the handle is live and `current` is correctly sized and writable.
7686    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    // SAFETY: the handle is live, the compression-format input is initialized, and this
7709    // synchronous FSCTL has no output buffer.
7710    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    // SAFETY: the handle is live and `current` is correctly sized and writable.
7781    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    // SAFETY: the handle is live and `updated` is a correctly sized initialized structure.
7828    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    // SAFETY: the handle is live and `actual` is correctly sized and writable.
7856    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        // SAFETY: these ioctls read/write one c_long through valid pointers and
7935        // operate on the live descriptor owned by `file`.
7936        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        // SAFETY: as above, SETFLAGS reads the initialized c_long value.
7943        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    // fsxattr consists only of integer and reserved-byte fields; zero is valid initialization.
8001    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    // SAFETY: fchown only observes the valid descriptor owned by `file`; both
8275    // numeric arguments were range-checked for this host ABI.
8276    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    // Pin and validate the final leaf without following it. This deliberately differs from
8713    // `prepare_destination`: an exact Windows reparse restore has already created this leaf, and
8714    // directory finalization must address the reparse object itself rather than reject it as an
8715    // alias. Every ancestor remains subject to the ordinary no-follow traversal checks above.
8716    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    // SAFETY: `reparse` owns a live handle and `basic` is a correctly sized writable output.
8815    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            // SAFETY: the pinned parent fd and validated leaf name identify the symlink itself.
9005            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    // SAFETY: the parent directory is pinned and `leaf_c` is a validated single path component.
9139    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    // SAFETY: `openat` returned a new owned descriptor.
9150    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    // SAFETY: the file handle is live; FSCTL_SET_SPARSE accepts null input and output buffers for
9755    // the default "set sparse" operation, and the call is synchronous.
9756    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        // SAFETY: the live handle and fixed-size buffers remain valid for this synchronous call.
9808        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    // SAFETY: the file handle is live, optional outputs are null, and `name` is a writable buffer
9894    // whose capacity is passed exactly to the synchronous query.
9895    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    // SAFETY: the handle is live and the output points to a correctly sized FILE_BASIC_INFO.
9934    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        // SAFETY: the directory handle is live and `buffer` is writable for its declared length.
9983        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    // Windows' documented FILE_RENAME_INFO allocation formula includes the structure's embedded
10021    // one-unit FileName field in addition to FileNameLength. Preserve that trailing zeroed space:
10022    // on ARM64, passing only offset_of(FileName) + FileNameLength can make NTFS consume adjacent
10023    // bytes as an unintended filename suffix when the exact allocation ends on an 8-byte boundary.
10024    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    // SAFETY: `storage` is pointer-aligned and large enough for the fixed structure plus every
10033    // UTF-16 filename unit. ReplaceIfExists=false gives the required no-clobber publication.
10034    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        // libc does not expose renameat2 on every Linux libc target, so invoke the
10106        // kernel interface directly. Both names are validated single components
10107        // beneath the same pinned parent.
10108        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    // SAFETY: the parent directory is pinned and `leaf` is a validated single component.
10327    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    // Pin the newly created object without opening a device or blocking on a FIFO.
10355    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    // SAFETY: `fd` is newly owned and transferred exactly once.
10369    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        // SAFETY: the procfs magic link refers to the pinned special object.
10380        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    // SAFETY: as above, chmod follows the procfs magic link to the pinned object.
10400    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    // SAFETY: the path points to the pinned object and `times` contains two valid timespecs.
10529    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    // SAFETY: the handle is live and the authenticated payload is retained for the synchronous
10834    // control call. FSCTL_SET_REPARSE_POINT has no output buffer.
10835    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    // SAFETY: the handle is live and the output allocation remains valid for the synchronous call.
10855    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        // UF_COMPRESSED is retained but deliberately not in the recognized/settable mask;
12092        // UF_IMMUTABLE is recognized but System-class under the v45 restore policy.
12093        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        // SF_NOUNLINK is Darwin System-class but is not registered for built-in application.
12185        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}