Skip to main content

tzap_core/
streaming_writer.rs

1use std::io::{self, ErrorKind, Read};
2
3use crate::crypto::{KdfParams, MasterKey};
4use crate::entry_metadata::{ArchiveTimestamp, SparseExtent, MAX_SPARSE_EXTENTS};
5use crate::format::{ArchiveWriteError, FormatError};
6use crate::metadata::{
7    normalize_lookup_file_path, validate_directory_path_bytes, validate_file_path_bytes,
8};
9use crate::writer::{
10    write_ordered_parallel_stream_archive_to_sink, ArchiveWriteSink, MemoryArchiveSink,
11    PortableFileMetadata, PortableModeOrigin, PortablePosixOwner, RootAuthAuthenticator,
12    RootAuthWriterConfig, SourceEntryKind, StreamingRegularMember, WriterOptions, WrittenArchive,
13    WrittenArchiveSummary,
14};
15
16const TAR_BLOCK_LEN: usize = 512;
17const MAX_TAR_STDIN_METADATA_PAYLOAD_BYTES: usize = 1024 * 1024;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct StreamingTarWriterSummary {
21    pub archive: WrittenArchiveSummary,
22    pub input_member_count: u64,
23    pub input_tar_bytes: u64,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct StreamingRawWriterSummary {
28    pub archive: WrittenArchiveSummary,
29    pub input_bytes: u64,
30}
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
33struct TarStdinInputSummary {
34    regular_file_count: u64,
35    directory_count: u64,
36    symlink_count: u64,
37    input_tar_bytes: u64,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41struct TarStdinRegularMember {
42    path: Vec<u8>,
43    entry_kind: SourceEntryKind,
44    link_target: Option<Vec<u8>>,
45    mode: u32,
46    mtime: ArchiveTimestamp,
47    logical_size: u64,
48    sparse_extents: Option<Vec<SparseExtent>>,
49    portable_metadata: PortableFileMetadata,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53struct LocalTarMetadata {
54    pending_header: bool,
55    pax_path: Option<Vec<u8>>,
56    pax_linkpath: Option<Vec<u8>>,
57    pax_size: Option<u64>,
58    pax_mode: Option<u32>,
59    pax_mtime: Option<ArchiveTimestamp>,
60    pax_uid: Option<u64>,
61    pax_gid: Option<u64>,
62    pax_uname: Option<Vec<u8>>,
63    pax_gname: Option<Vec<u8>>,
64    gnu_long_name: Option<Vec<u8>>,
65    gnu_long_link: Option<Vec<u8>>,
66    gnu_sparse_major: Option<u64>,
67    gnu_sparse_minor: Option<u64>,
68    gnu_sparse_name: Option<Vec<u8>>,
69    gnu_sparse_realsize: Option<u64>,
70}
71
72impl LocalTarMetadata {
73    fn has_pending(&self) -> bool {
74        self.pending_header
75            || self.pax_path.is_some()
76            || self.pax_linkpath.is_some()
77            || self.pax_size.is_some()
78            || self.pax_mode.is_some()
79            || self.pax_mtime.is_some()
80            || self.pax_uid.is_some()
81            || self.pax_gid.is_some()
82            || self.pax_uname.is_some()
83            || self.pax_gname.is_some()
84            || self.gnu_long_name.is_some()
85            || self.gnu_long_link.is_some()
86            || self.gnu_sparse_major.is_some()
87            || self.gnu_sparse_minor.is_some()
88            || self.gnu_sparse_name.is_some()
89            || self.gnu_sparse_realsize.is_some()
90    }
91}
92
93pub fn write_tar_stream_archive<R: Read>(
94    reader: R,
95    master_key: &MasterKey,
96    options: WriterOptions,
97) -> Result<WrittenArchive, FormatError> {
98    let mut sink = MemoryArchiveSink::default();
99    let summary = write_tar_stream_archive_to_sink(reader, master_key, options, &mut sink)
100        .map_err(format_error_from_archive_write_error)?;
101    Ok(WrittenArchive {
102        bytes: sink
103            .volumes
104            .first()
105            .cloned()
106            .ok_or(FormatError::WriterInvariant("no volumes emitted"))?,
107        volumes: sink.volumes,
108        bootstrap_sidecar: sink.bootstrap_sidecar,
109        archive_uuid: summary.archive.archive_uuid,
110        session_id: summary.archive.session_id,
111        timings: summary.archive.timings,
112    })
113}
114
115pub fn write_tar_stream_archive_to_sink<R, O>(
116    reader: R,
117    master_key: &MasterKey,
118    options: WriterOptions,
119    sink: &mut O,
120) -> Result<StreamingTarWriterSummary, ArchiveWriteError>
121where
122    R: Read,
123    O: ArchiveWriteSink,
124{
125    write_tar_stream_archive_to_sink_with_kdf_and_root_auth(
126        reader,
127        master_key,
128        options,
129        &KdfParams::Raw,
130        None,
131        None,
132        sink,
133    )
134}
135
136#[allow(clippy::too_many_arguments)]
137pub fn write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth<R, O>(
138    mut reader: R,
139    archive_path: &str,
140    input_size: u64,
141    master_key: &MasterKey,
142    options: WriterOptions,
143    kdf_params: &KdfParams,
144    root_auth: Option<RootAuthWriterConfig<'_>>,
145    authenticator: Option<&mut RootAuthAuthenticator<'_>>,
146    sink: &mut O,
147) -> Result<StreamingRawWriterSummary, ArchiveWriteError>
148where
149    R: Read,
150    O: ArchiveWriteSink,
151{
152    validate_streaming_create_writer_options(options)?;
153    let archive_path = normalize_lookup_file_path(archive_path, options.max_path_length)?;
154    let archive = write_ordered_parallel_stream_archive_to_sink(
155        master_key,
156        options,
157        kdf_params,
158        root_auth,
159        authenticator,
160        None,
161        sink,
162        None,
163        |writer| {
164            let mut payload = SizedRawPayloadReader {
165                reader: &mut reader,
166                remaining: input_size,
167            };
168            writer.write_regular_member_from_reader(
169                StreamingRegularMember {
170                    archive_path,
171                    entry_kind: SourceEntryKind::Regular,
172                    link_target: None,
173                    file_data_size: input_size,
174                    sparse_extents: None,
175                    mode: 0o644,
176                    mtime: ArchiveTimestamp::UNIX_EPOCH,
177                    portable_metadata: PortableFileMetadata::default(),
178                },
179                &mut payload,
180            )?;
181            if payload.remaining != 0 {
182                return Err(FormatError::WriterInvariant(
183                    "raw stdin payload was not fully consumed",
184                )
185                .into());
186            }
187            reject_trailing_raw_stdin_bytes(&mut reader)?;
188            Ok(())
189        },
190    )?;
191    Ok(StreamingRawWriterSummary {
192        archive,
193        input_bytes: input_size,
194    })
195}
196
197#[allow(clippy::too_many_arguments)]
198pub fn write_tar_stream_archive_to_sink_with_kdf_and_root_auth<R, O>(
199    mut reader: R,
200    master_key: &MasterKey,
201    options: WriterOptions,
202    kdf_params: &KdfParams,
203    root_auth: Option<RootAuthWriterConfig<'_>>,
204    authenticator: Option<&mut RootAuthAuthenticator<'_>>,
205    sink: &mut O,
206) -> Result<StreamingTarWriterSummary, ArchiveWriteError>
207where
208    R: Read,
209    O: ArchiveWriteSink,
210{
211    validate_streaming_create_writer_options(options)?;
212    let mut input_summary = None;
213    let archive = write_ordered_parallel_stream_archive_to_sink(
214        master_key,
215        options,
216        kdf_params,
217        root_auth,
218        authenticator,
219        None,
220        sink,
221        None,
222        |writer| {
223            let summary = stream_tar_stdin_regulars(
224                &mut reader,
225                options.max_path_length,
226                |member, payload| {
227                    let mut empty = io::empty();
228                    let payload: &mut dyn Read = match payload {
229                        Some(payload) => payload,
230                        None => &mut empty,
231                    };
232                    writer.write_regular_member_from_reader(
233                        StreamingRegularMember {
234                            archive_path: member.path,
235                            entry_kind: member.entry_kind,
236                            link_target: member.link_target,
237                            file_data_size: member.logical_size,
238                            sparse_extents: member.sparse_extents,
239                            mode: member.mode,
240                            mtime: member.mtime,
241                            portable_metadata: member.portable_metadata,
242                        },
243                        payload,
244                    )
245                },
246            )?;
247            input_summary = Some(summary);
248            Ok(())
249        },
250    )?;
251    let input_summary = input_summary.ok_or(FormatError::WriterInvariant(
252        "streaming tar parser did not return a summary",
253    ))?;
254    Ok(StreamingTarWriterSummary {
255        archive,
256        input_member_count: input_summary
257            .regular_file_count
258            .checked_add(input_summary.directory_count)
259            .and_then(|count| count.checked_add(input_summary.symlink_count))
260            .ok_or(FormatError::WriterUnsupported(
261                "tar input member count overflow",
262            ))?,
263        input_tar_bytes: input_summary.input_tar_bytes,
264    })
265}
266
267fn format_error_from_archive_write_error(error: ArchiveWriteError) -> FormatError {
268    match error {
269        ArchiveWriteError::Format(error) => error,
270        ArchiveWriteError::Io(_) => FormatError::WriterInvariant("in-memory archive writer failed"),
271    }
272}
273
274fn validate_streaming_create_writer_options(options: WriterOptions) -> Result<(), FormatError> {
275    if options.volume_loss_tolerance != 0 {
276        return Err(FormatError::WriterUnsupported(
277            "streaming create cannot tolerate volume loss",
278        ));
279    }
280    if options.target_volume_size.is_some() {
281        return Err(FormatError::WriterUnsupported(
282            "streaming create does not support target volume sizing",
283        ));
284    }
285    Ok(())
286}
287
288struct SizedRawPayloadReader<'a, R: Read> {
289    reader: &'a mut R,
290    remaining: u64,
291}
292
293impl<R: Read> Read for SizedRawPayloadReader<'_, R> {
294    fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
295        if out.is_empty() || self.remaining == 0 {
296            return Ok(0);
297        }
298        let max_read = out
299            .len()
300            .min(usize::try_from(self.remaining).unwrap_or(usize::MAX));
301        match self.reader.read(&mut out[..max_read]) {
302            Ok(read) => {
303                self.remaining -= read as u64;
304                Ok(read)
305            }
306            Err(error) if error.kind() == ErrorKind::Interrupted => self.read(out),
307            Err(error) => Err(error),
308        }
309    }
310}
311
312fn reject_trailing_raw_stdin_bytes<R: Read>(reader: &mut R) -> Result<(), ArchiveWriteError> {
313    let mut extra = [0u8; 1];
314    loop {
315        match reader.read(&mut extra) {
316            Ok(0) => return Ok(()),
317            Ok(_) => {
318                return Err(
319                    FormatError::InvalidArchive("raw stdin exceeds declared --stdin-size").into(),
320                )
321            }
322            Err(error) if error.kind() == ErrorKind::Interrupted => {}
323            Err(error) => return Err(ArchiveWriteError::Io(error)),
324        }
325    }
326}
327
328fn stream_tar_stdin_regulars<R, F>(
329    reader: &mut R,
330    max_path_length: u32,
331    mut on_regular: F,
332) -> Result<TarStdinInputSummary, ArchiveWriteError>
333where
334    R: Read,
335    F: for<'a> FnMut(
336        TarStdinRegularMember,
337        Option<&mut LimitedTarPayloadReader<'a, R>>,
338    ) -> Result<(), ArchiveWriteError>,
339{
340    let mut summary = TarStdinInputSummary::default();
341    let mut metadata = LocalTarMetadata::default();
342
343    loop {
344        let Some(header) = read_tar_block(reader, &mut summary.input_tar_bytes)? else {
345            if metadata.has_pending() {
346                return Err(
347                    FormatError::InvalidArchive("tar stream ended after metadata header").into(),
348                );
349            }
350            return Ok(summary);
351        };
352        if header.iter().all(|byte| *byte == 0) {
353            if metadata.has_pending() {
354                return Err(
355                    FormatError::InvalidArchive("tar stream ended after metadata header").into(),
356                );
357            }
358            let second = read_tar_block(reader, &mut summary.input_tar_bytes)?.ok_or(
359                FormatError::InvalidArchive("tar stream ended inside end-of-archive marker"),
360            )?;
361            if second.iter().any(|byte| *byte != 0) {
362                return Err(FormatError::InvalidArchive(
363                    "tar stream has non-zero bytes after end-of-archive marker",
364                )
365                .into());
366            }
367            drain_zero_blocks_to_eof(reader, &mut summary.input_tar_bytes)?;
368            return Ok(summary);
369        }
370
371        verify_tar_checksum(&header)?;
372        let typeflag = header[156];
373        let header_size = parse_tar_number(&header[124..136])?;
374        match typeflag {
375            b'x' => {
376                let payload =
377                    read_metadata_payload(reader, header_size, &mut summary.input_tar_bytes)?;
378                metadata.pending_header = true;
379                parse_pax_records(&payload, &mut metadata)?;
380            }
381            b'L' => {
382                let payload =
383                    read_metadata_payload(reader, header_size, &mut summary.input_tar_bytes)?;
384                metadata.pending_header = true;
385                metadata.gnu_long_name = Some(trimmed_metadata_payload(&payload));
386            }
387            b'K' => {
388                let payload =
389                    read_metadata_payload(reader, header_size, &mut summary.input_tar_bytes)?;
390                metadata.pending_header = true;
391                metadata.gnu_long_link = Some(trimmed_metadata_payload(&payload));
392            }
393            b'g' => {
394                return Err(
395                    FormatError::InvalidArchive("global PAX headers are not allowed").into(),
396                )
397            }
398            b'V' | b'M' | b'N' => {
399                return Err(
400                    FormatError::InvalidArchive("global GNU headers are not allowed").into(),
401                )
402            }
403            b'S' => {
404                return Err(
405                    FormatError::ReaderUnsupported("unsupported GNU sparse tar entry").into(),
406                )
407            }
408            0 | b'0' | b'5' | b'1' | b'2' | b'3' | b'4' | b'6' => {
409                let effective_size = metadata.pax_size.unwrap_or(header_size);
410                let mode = metadata
411                    .pax_mode
412                    .unwrap_or(parse_tar_number(&header[100..108])? as u32);
413                let mtime = if let Some(mtime) = metadata.pax_mtime {
414                    mtime
415                } else {
416                    ArchiveTimestamp::from_seconds(
417                        i64::try_from(parse_tar_number(&header[136..148])?).map_err(|_| {
418                            FormatError::WriterUnsupported(
419                                "input tar mtime exceeds revision-45 i64 range",
420                            )
421                        })?,
422                    )
423                };
424                let path = canonical_main_path(&header, typeflag, &metadata, max_path_length)?;
425                let link_target = if typeflag == b'2' {
426                    let target = metadata
427                        .pax_linkpath
428                        .clone()
429                        .or_else(|| metadata.gnu_long_link.clone())
430                        .unwrap_or_else(|| nul_trimmed(&header[157..257]).to_vec());
431                    crate::tar_model::validate_symlink_target(&path, &target)?;
432                    Some(target)
433                } else {
434                    None
435                };
436                let uid = metadata
437                    .pax_uid
438                    .unwrap_or(parse_tar_number(&header[108..116])?);
439                let gid = metadata
440                    .pax_gid
441                    .unwrap_or(parse_tar_number(&header[116..124])?);
442                let uname = tar_owner_name(metadata.pax_uname.as_deref(), &header[265..297])?;
443                let gname = tar_owner_name(metadata.pax_gname.as_deref(), &header[297..329])?;
444                let portable_metadata = PortableFileMetadata {
445                    source_os: "other-unix".into(),
446                    source_filesystem: "unknown".into(),
447                    mode_origin: PortableModeOrigin::Native,
448                    posix_owner: Some(PortablePosixOwner {
449                        uid,
450                        gid,
451                        uname,
452                        gname,
453                    }),
454                    attributes: None,
455                    native: Default::default(),
456                };
457                let has_sparse_declaration = metadata.gnu_sparse_major.is_some()
458                    || metadata.gnu_sparse_minor.is_some()
459                    || metadata.gnu_sparse_name.is_some()
460                    || metadata.gnu_sparse_realsize.is_some();
461                let sparse_logical_size = if has_sparse_declaration {
462                    if typeflag != 0 && typeflag != b'0' {
463                        return Err(FormatError::InvalidArchive(
464                            "GNU sparse metadata is attached to a non-regular entry",
465                        )
466                        .into());
467                    }
468                    if metadata.gnu_sparse_major != Some(1)
469                        || metadata.gnu_sparse_minor != Some(0)
470                        || metadata.gnu_sparse_name.is_none()
471                        || metadata.gnu_sparse_realsize.is_none()
472                    {
473                        return Err(FormatError::ReaderUnsupported(
474                            "tar stdin supports only complete GNU sparse PAX 1.0 declarations",
475                        )
476                        .into());
477                    }
478                    metadata.gnu_sparse_realsize
479                } else {
480                    None
481                };
482                metadata = LocalTarMetadata::default();
483
484                match typeflag {
485                    0 | b'0' => {
486                        {
487                            let mut payload = LimitedTarPayloadReader {
488                                reader,
489                                remaining: effective_size,
490                                input_tar_bytes: &mut summary.input_tar_bytes,
491                            };
492                            let sparse_extents = sparse_logical_size
493                                .map(|logical_size| {
494                                    read_gnu_sparse_1_0_map(&mut payload, logical_size)
495                                })
496                                .transpose()?;
497                            let member = TarStdinRegularMember {
498                                path,
499                                entry_kind: SourceEntryKind::Regular,
500                                link_target: None,
501                                mode,
502                                mtime,
503                                logical_size: sparse_logical_size.unwrap_or(effective_size),
504                                sparse_extents,
505                                portable_metadata,
506                            };
507                            on_regular(member, Some(&mut payload))?;
508                            if payload.remaining != 0 {
509                                return Err(FormatError::WriterInvariant(
510                                    "streaming tar payload was not fully consumed",
511                                )
512                                .into());
513                            }
514                        }
515                        drain_tar_padding(
516                            reader,
517                            padding_to_512_u64(effective_size),
518                            &mut summary.input_tar_bytes,
519                        )?;
520                        summary.regular_file_count = checked_input_add(
521                            summary.regular_file_count,
522                            1,
523                            "tar regular file count",
524                        )?;
525                    }
526                    b'5' => {
527                        if effective_size != 0 {
528                            return Err(FormatError::InvalidArchive(
529                                "non-regular tar entry has non-zero payload size",
530                            )
531                            .into());
532                        }
533                        let member = TarStdinRegularMember {
534                            path,
535                            entry_kind: SourceEntryKind::Directory,
536                            link_target: None,
537                            mode,
538                            mtime,
539                            logical_size: 0,
540                            sparse_extents: None,
541                            portable_metadata,
542                        };
543                        on_regular(member, None)?;
544                        summary.directory_count =
545                            checked_input_add(summary.directory_count, 1, "tar directory count")?;
546                    }
547                    b'2' => {
548                        if effective_size != 0 {
549                            return Err(FormatError::InvalidArchive(
550                                "non-regular tar entry has non-zero payload size",
551                            )
552                            .into());
553                        }
554                        let member = TarStdinRegularMember {
555                            path,
556                            entry_kind: SourceEntryKind::Symlink,
557                            link_target,
558                            mode,
559                            mtime,
560                            logical_size: 0,
561                            sparse_extents: None,
562                            portable_metadata,
563                        };
564                        on_regular(member, None)?;
565                        summary.symlink_count =
566                            checked_input_add(summary.symlink_count, 1, "tar symlink count")?;
567                    }
568                    _ => {
569                        return Err(FormatError::WriterUnsupported(
570                            "streaming tar stdin supports regular files, directories, and symlinks only",
571                        )
572                        .into())
573                    }
574                }
575            }
576            _ => return Err(FormatError::ReaderUnsupported("unsupported tar entry type").into()),
577        }
578    }
579}
580
581struct LimitedTarPayloadReader<'a, R: Read> {
582    reader: &'a mut R,
583    remaining: u64,
584    input_tar_bytes: &'a mut u64,
585}
586
587impl<R: Read> Read for LimitedTarPayloadReader<'_, R> {
588    fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
589        if out.is_empty() || self.remaining == 0 {
590            return Ok(0);
591        }
592        let max_read = out
593            .len()
594            .min(usize::try_from(self.remaining).unwrap_or(usize::MAX));
595        match self.reader.read(&mut out[..max_read]) {
596            Ok(0) => Err(io::Error::new(
597                ErrorKind::UnexpectedEof,
598                "tar stream ended inside member payload",
599            )),
600            Ok(read) => {
601                self.remaining -= read as u64;
602                *self.input_tar_bytes =
603                    checked_input_add(*self.input_tar_bytes, read as u64, "tar input size")
604                        .map_err(io_error_from_format)?;
605                Ok(read)
606            }
607            Err(error) if error.kind() == ErrorKind::Interrupted => self.read(out),
608            Err(error) => Err(error),
609        }
610    }
611}
612
613fn read_gnu_sparse_1_0_map<R: Read>(
614    payload: &mut LimitedTarPayloadReader<'_, R>,
615    logical_size: u64,
616) -> Result<Vec<SparseExtent>, ArchiveWriteError> {
617    fn read_line<R: Read>(
618        payload: &mut LimitedTarPayloadReader<'_, R>,
619        consumed: &mut u64,
620    ) -> Result<u64, ArchiveWriteError> {
621        let mut digits = Vec::with_capacity(20);
622        loop {
623            let mut byte = [0u8; 1];
624            payload
625                .read_exact(&mut byte)
626                .map_err(ArchiveWriteError::Io)?;
627            *consumed = consumed
628                .checked_add(1)
629                .ok_or(FormatError::InvalidArchive("GNU sparse map size overflow"))?;
630            if byte[0] == b'\n' {
631                break;
632            }
633            if !byte[0].is_ascii_digit() || digits.len() == 20 {
634                return Err(FormatError::InvalidArchive(
635                    "GNU sparse map value is not canonical decimal",
636                )
637                .into());
638            }
639            digits.push(byte[0]);
640        }
641        if digits.is_empty() || (digits.len() > 1 && digits[0] == b'0') {
642            return Err(
643                FormatError::InvalidArchive("GNU sparse map value is not minimal decimal").into(),
644            );
645        }
646        Ok(parse_decimal_u64(&digits)?)
647    }
648
649    let mut map_bytes = 0u64;
650    let count_u64 = read_line(payload, &mut map_bytes)?;
651    let count =
652        usize::try_from(count_u64).map_err(|_| FormatError::ReaderResourceLimitExceeded {
653            field: "sparse extent count",
654            cap: MAX_SPARSE_EXTENTS as u64,
655            actual: count_u64,
656        })?;
657    if count > MAX_SPARSE_EXTENTS {
658        return Err(FormatError::ReaderResourceLimitExceeded {
659            field: "sparse extent count",
660            cap: MAX_SPARSE_EXTENTS as u64,
661            actual: count_u64,
662        }
663        .into());
664    }
665    let mut extents = Vec::with_capacity(count);
666    let mut previous_end = 0u64;
667    let mut extent_bytes = 0u64;
668    for index in 0..count {
669        let offset = read_line(payload, &mut map_bytes)?;
670        let length = read_line(payload, &mut map_bytes)?;
671        let end = offset
672            .checked_add(length)
673            .ok_or(FormatError::InvalidArchive("GNU sparse extent overflow"))?;
674        if length == 0 || offset < previous_end || (index != 0 && offset == previous_end) {
675            return Err(FormatError::InvalidArchive(
676                "GNU sparse extents overlap, are empty, or are not merged",
677            )
678            .into());
679        }
680        if end > logical_size {
681            return Err(
682                FormatError::InvalidArchive("GNU sparse extent exceeds logical size").into(),
683            );
684        }
685        extent_bytes = extent_bytes
686            .checked_add(length)
687            .ok_or(FormatError::InvalidArchive(
688                "GNU sparse extent byte count overflow",
689            ))?;
690        extents.push(SparseExtent { offset, length });
691        previous_end = end;
692    }
693    let padded_map_size = map_bytes
694        .checked_add(511)
695        .ok_or(FormatError::InvalidArchive("GNU sparse map size overflow"))?
696        / 512
697        * 512;
698    let mut padding = padded_map_size - map_bytes;
699    let mut zeros = [0u8; 512];
700    while padding != 0 {
701        let take = usize::try_from(padding.min(zeros.len() as u64)).unwrap();
702        payload
703            .read_exact(&mut zeros[..take])
704            .map_err(ArchiveWriteError::Io)?;
705        if zeros[..take].iter().any(|byte| *byte != 0) {
706            return Err(FormatError::InvalidArchive("GNU sparse map padding is non-zero").into());
707        }
708        padding -= take as u64;
709    }
710    if payload.remaining != extent_bytes {
711        return Err(FormatError::InvalidArchive(
712            "GNU sparse stored size does not match the canonical map",
713        )
714        .into());
715    }
716    Ok(extents)
717}
718
719fn read_tar_block<R: Read>(
720    reader: &mut R,
721    input_tar_bytes: &mut u64,
722) -> Result<Option<[u8; TAR_BLOCK_LEN]>, ArchiveWriteError> {
723    let mut block = [0u8; TAR_BLOCK_LEN];
724    let mut filled = 0usize;
725    while filled < TAR_BLOCK_LEN {
726        match reader.read(&mut block[filled..]) {
727            Ok(0) if filled == 0 => return Ok(None),
728            Ok(0) => {
729                return Err(
730                    FormatError::InvalidArchive("tar stream ended inside member group").into(),
731                )
732            }
733            Ok(read) => {
734                filled += read;
735                *input_tar_bytes =
736                    checked_input_add(*input_tar_bytes, read as u64, "tar input size")?;
737            }
738            Err(error) if error.kind() == ErrorKind::Interrupted => {}
739            Err(error) => return Err(ArchiveWriteError::Io(error)),
740        }
741    }
742    Ok(Some(block))
743}
744
745fn read_metadata_payload<R: Read>(
746    reader: &mut R,
747    size: u64,
748    input_tar_bytes: &mut u64,
749) -> Result<Vec<u8>, ArchiveWriteError> {
750    let len = usize::try_from(size)
751        .map_err(|_| FormatError::ReaderUnsupported("tar metadata payload is too large"))?;
752    if len > MAX_TAR_STDIN_METADATA_PAYLOAD_BYTES {
753        return Err(
754            FormatError::ReaderUnsupported("tar metadata payload exceeds streaming cap").into(),
755        );
756    }
757    let mut payload = vec![0u8; len];
758    read_exact_counted(reader, &mut payload, input_tar_bytes)?;
759    drain_tar_padding(reader, padding_to_512_u64(size), input_tar_bytes)?;
760    Ok(payload)
761}
762
763fn read_exact_counted<R: Read>(
764    reader: &mut R,
765    mut out: &mut [u8],
766    input_tar_bytes: &mut u64,
767) -> Result<(), ArchiveWriteError> {
768    while !out.is_empty() {
769        match reader.read(out) {
770            Ok(0) => {
771                return Err(
772                    FormatError::InvalidArchive("tar stream ended inside member group").into(),
773                )
774            }
775            Ok(read) => {
776                *input_tar_bytes =
777                    checked_input_add(*input_tar_bytes, read as u64, "tar input size")?;
778                let remaining = out;
779                out = &mut remaining[read..];
780            }
781            Err(error) if error.kind() == ErrorKind::Interrupted => {}
782            Err(error) => return Err(ArchiveWriteError::Io(error)),
783        }
784    }
785    Ok(())
786}
787
788fn drain_tar_padding<R: Read>(
789    reader: &mut R,
790    padding: u64,
791    input_tar_bytes: &mut u64,
792) -> Result<(), ArchiveWriteError> {
793    let mut remaining = padding;
794    let mut buf = [0u8; TAR_BLOCK_LEN];
795    while remaining > 0 {
796        let take = buf
797            .len()
798            .min(usize::try_from(remaining).unwrap_or(usize::MAX));
799        read_exact_counted(reader, &mut buf[..take], input_tar_bytes)?;
800        if buf[..take].iter().any(|byte| *byte != 0) {
801            return Err(FormatError::InvalidArchive("tar member padding is non-zero").into());
802        }
803        remaining -= take as u64;
804    }
805    Ok(())
806}
807
808fn drain_zero_blocks_to_eof<R: Read>(
809    reader: &mut R,
810    input_tar_bytes: &mut u64,
811) -> Result<(), ArchiveWriteError> {
812    while let Some(block) = read_tar_block(reader, input_tar_bytes)? {
813        if block.iter().any(|byte| *byte != 0) {
814            return Err(FormatError::InvalidArchive(
815                "tar stream has non-zero bytes after end-of-archive marker",
816            )
817            .into());
818        }
819    }
820    Ok(())
821}
822
823fn canonical_main_path(
824    header: &[u8],
825    typeflag: u8,
826    metadata: &LocalTarMetadata,
827    max_path_length: u32,
828) -> Result<Vec<u8>, FormatError> {
829    let mut path = metadata
830        .gnu_sparse_name
831        .clone()
832        .or_else(|| metadata.pax_path.clone())
833        .or_else(|| metadata.gnu_long_name.clone())
834        .unwrap_or_else(|| ustar_path(header));
835    while path.starts_with(b"./") {
836        path.drain(..2);
837    }
838    if typeflag == b'5' && path.ends_with(b"/") && !path.ends_with(b"//") {
839        path.pop();
840    }
841    if typeflag == b'5' {
842        validate_directory_path_bytes(&path, max_path_length)?;
843    } else {
844        validate_file_path_bytes(&path, max_path_length)?;
845    }
846    Ok(path)
847}
848
849fn ustar_path(header: &[u8]) -> Vec<u8> {
850    let name = nul_trimmed(&header[0..100]);
851    let prefix = nul_trimmed(&header[345..500]);
852    if prefix.is_empty() {
853        return name.to_vec();
854    }
855    let mut path = Vec::with_capacity(prefix.len() + 1 + name.len());
856    path.extend_from_slice(prefix);
857    path.push(b'/');
858    path.extend_from_slice(name);
859    path
860}
861
862fn parse_pax_records(payload: &[u8], metadata: &mut LocalTarMetadata) -> Result<(), FormatError> {
863    let mut cursor = 0usize;
864    while cursor < payload.len() {
865        let len_digits_start = cursor;
866        while cursor < payload.len() && payload[cursor].is_ascii_digit() {
867            cursor += 1;
868        }
869        if cursor == len_digits_start || cursor >= payload.len() || payload[cursor] != b' ' {
870            return Err(FormatError::InvalidArchive("malformed PAX record"));
871        }
872        let len = parse_decimal_usize(&payload[len_digits_start..cursor])?;
873        let record_start = len_digits_start;
874        let record_end = record_start
875            .checked_add(len)
876            .ok_or(FormatError::InvalidArchive("malformed PAX record"))?;
877        if record_end > payload.len() || len < 4 {
878            return Err(FormatError::InvalidArchive("malformed PAX record"));
879        }
880        let body_start = cursor + 1;
881        let record = &payload[body_start..record_end];
882        if record.last().copied() != Some(b'\n') {
883            return Err(FormatError::InvalidArchive("malformed PAX record"));
884        }
885        let body = &record[..record.len() - 1];
886        let eq = body
887            .iter()
888            .position(|byte| *byte == b'=')
889            .ok_or(FormatError::InvalidArchive("malformed PAX record"))?;
890        let key = std::str::from_utf8(&body[..eq])
891            .map_err(|_| FormatError::InvalidArchive("malformed PAX key"))?;
892        let value = &body[eq + 1..];
893        match key {
894            "path" => metadata.pax_path = Some(value.to_vec()),
895            "linkpath" => metadata.pax_linkpath = Some(value.to_vec()),
896            "size" => metadata.pax_size = Some(parse_decimal_u64(value)?),
897            "mode" => {
898                metadata.pax_mode = Some(
899                    u32::try_from(parse_decimal_u64(value)?)
900                        .map_err(|_| FormatError::WriterUnsupported("tar mode overflow"))?,
901                )
902            }
903            "mtime" => metadata.pax_mtime = Some(parse_pax_mtime(value)?),
904            "uid" => metadata.pax_uid = Some(parse_decimal_u64(value)?),
905            "gid" => metadata.pax_gid = Some(parse_decimal_u64(value)?),
906            "uname" => metadata.pax_uname = Some(value.to_vec()),
907            "gname" => metadata.pax_gname = Some(value.to_vec()),
908            "GNU.sparse.major" => metadata.gnu_sparse_major = Some(parse_decimal_u64(value)?),
909            "GNU.sparse.minor" => metadata.gnu_sparse_minor = Some(parse_decimal_u64(value)?),
910            "GNU.sparse.name" => metadata.gnu_sparse_name = Some(value.to_vec()),
911            "GNU.sparse.realsize" => metadata.gnu_sparse_realsize = Some(parse_decimal_u64(value)?),
912            key if key.starts_with("GNU.sparse.") => {
913                return Err(FormatError::ReaderUnsupported(
914                    "unsupported GNU sparse PAX key",
915                ));
916            }
917            _ => {}
918        }
919        cursor = record_end;
920    }
921    Ok(())
922}
923
924fn verify_tar_checksum(header: &[u8]) -> Result<(), FormatError> {
925    let expected = parse_tar_number(&header[148..156])?;
926    let actual = header[..148]
927        .iter()
928        .chain(header[156..].iter())
929        .fold(8u64 * 32, |sum, byte| sum + u64::from(*byte));
930    if actual != expected {
931        return Err(FormatError::InvalidArchive("tar header checksum mismatch"));
932    }
933    Ok(())
934}
935
936fn parse_tar_number(field: &[u8]) -> Result<u64, FormatError> {
937    if field.first().is_some_and(|byte| byte & 0x80 != 0) {
938        return parse_tar_base256(field);
939    }
940    parse_tar_octal(field)
941}
942
943fn parse_tar_base256(field: &[u8]) -> Result<u64, FormatError> {
944    let Some(first) = field.first() else {
945        return Err(FormatError::InvalidArchive("empty tar numeric field"));
946    };
947    if first & 0x40 != 0 {
948        return Err(FormatError::ReaderUnsupported(
949            "negative tar base-256 numeric fields are not supported",
950        ));
951    }
952    let mut value = u128::from(first & 0x7f);
953    for byte in &field[1..] {
954        value = value
955            .checked_mul(256)
956            .and_then(|current| current.checked_add(u128::from(*byte)))
957            .ok_or(FormatError::InvalidArchive("tar base-256 field overflow"))?;
958    }
959    u64::try_from(value).map_err(|_| FormatError::ReaderUnsupported("tar numeric field too large"))
960}
961
962fn parse_tar_octal(field: &[u8]) -> Result<u64, FormatError> {
963    let field = field
964        .split(|byte| *byte == 0)
965        .next()
966        .unwrap_or(field)
967        .iter()
968        .copied()
969        .skip_while(|byte| *byte == b' ' || *byte == b'0')
970        .take_while(|byte| *byte != b' ')
971        .collect::<Vec<_>>();
972    if field.is_empty() {
973        return Ok(0);
974    }
975    let mut value = 0u64;
976    for byte in field {
977        if !(b'0'..=b'7').contains(&byte) {
978            return Err(FormatError::InvalidArchive("invalid tar octal field"));
979        }
980        value = value
981            .checked_mul(8)
982            .and_then(|current| current.checked_add(u64::from(byte - b'0')))
983            .ok_or(FormatError::InvalidArchive("tar octal field overflow"))?;
984    }
985    Ok(value)
986}
987
988fn parse_decimal_usize(bytes: &[u8]) -> Result<usize, FormatError> {
989    usize::try_from(parse_decimal_u64(bytes)?)
990        .map_err(|_| FormatError::InvalidArchive("decimal field overflow"))
991}
992
993fn parse_decimal_u64(bytes: &[u8]) -> Result<u64, FormatError> {
994    if bytes.is_empty() {
995        return Err(FormatError::InvalidArchive("malformed decimal field"));
996    }
997    let mut value = 0u64;
998    for byte in bytes {
999        if !byte.is_ascii_digit() {
1000            return Err(FormatError::InvalidArchive("malformed decimal field"));
1001        }
1002        value = value
1003            .checked_mul(10)
1004            .and_then(|current| current.checked_add(u64::from(byte - b'0')))
1005            .ok_or(FormatError::InvalidArchive("decimal field overflow"))?;
1006    }
1007    Ok(value)
1008}
1009
1010fn parse_pax_mtime(bytes: &[u8]) -> Result<ArchiveTimestamp, FormatError> {
1011    let text = std::str::from_utf8(bytes)
1012        .map_err(|_| FormatError::InvalidArchive("PAX mtime is not ASCII"))?;
1013    let (integer, fraction) = text
1014        .split_once('.')
1015        .map_or((text, None), |(integer, fraction)| {
1016            (integer, Some(fraction))
1017        });
1018    if integer.is_empty()
1019        || integer == "+"
1020        || integer == "-"
1021        || !integer
1022            .trim_start_matches(['+', '-'])
1023            .bytes()
1024            .all(|byte| byte.is_ascii_digit())
1025    {
1026        return Err(FormatError::InvalidArchive("malformed PAX mtime"));
1027    }
1028    let seconds = integer
1029        .parse::<i64>()
1030        .map_err(|_| FormatError::InvalidArchive("PAX mtime exceeds i64"))?;
1031    let nanoseconds = match fraction {
1032        None => 0,
1033        Some(fraction)
1034            if !fraction.is_empty()
1035                && fraction.len() <= 9
1036                && fraction.bytes().all(|byte| byte.is_ascii_digit()) =>
1037        {
1038            let mut padded = fraction.to_owned();
1039            padded.extend(std::iter::repeat_n('0', 9 - fraction.len()));
1040            padded
1041                .parse::<u32>()
1042                .map_err(|_| FormatError::InvalidArchive("malformed PAX mtime fraction"))?
1043        }
1044        Some(_) => return Err(FormatError::InvalidArchive("malformed PAX mtime fraction")),
1045    };
1046    if integer.starts_with('-') && seconds == 0 && nanoseconds != 0 {
1047        return Err(FormatError::WriterUnsupported(
1048            "negative fractional PAX mtime between -1 and 0 has no canonical revision-45 encoding",
1049        ));
1050    }
1051    Ok(ArchiveTimestamp::new(seconds, nanoseconds))
1052}
1053
1054fn tar_owner_name(
1055    pax_value: Option<&[u8]>,
1056    header_field: &[u8],
1057) -> Result<Option<String>, FormatError> {
1058    let value = pax_value.unwrap_or_else(|| nul_trimmed(header_field));
1059    if value.is_empty() {
1060        return Ok(None);
1061    }
1062    let value = std::str::from_utf8(value)
1063        .map_err(|_| FormatError::WriterUnsupported("input tar owner name is not UTF-8"))?;
1064    if value.contains('\0') {
1065        return Err(FormatError::InvalidArchive(
1066            "input tar owner name contains NUL",
1067        ));
1068    }
1069    Ok(Some(value.to_owned()))
1070}
1071
1072fn trimmed_metadata_payload(payload: &[u8]) -> Vec<u8> {
1073    let mut end = payload.len();
1074    while end > 0 && payload[end - 1] == 0 {
1075        end -= 1;
1076    }
1077    payload[..end].to_vec()
1078}
1079
1080fn nul_trimmed(bytes: &[u8]) -> &[u8] {
1081    let end = bytes
1082        .iter()
1083        .position(|byte| *byte == 0)
1084        .unwrap_or(bytes.len());
1085    &bytes[..end]
1086}
1087
1088fn padding_to_512_u64(len: u64) -> u64 {
1089    let remainder = len % TAR_BLOCK_LEN as u64;
1090    if remainder == 0 {
1091        0
1092    } else {
1093        TAR_BLOCK_LEN as u64 - remainder
1094    }
1095}
1096
1097fn checked_input_add(lhs: u64, rhs: u64, field: &'static str) -> Result<u64, FormatError> {
1098    lhs.checked_add(rhs)
1099        .ok_or(FormatError::WriterUnsupported(field))
1100}
1101
1102fn io_error_from_format(error: FormatError) -> io::Error {
1103    io::Error::new(ErrorKind::InvalidData, error.to_string())
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108    use std::io;
1109
1110    use crate::crypto::MasterKey;
1111    use crate::format::{FormatError, BLOCK_RECORD_FRAMING_LEN, MASTER_KEY_LEN, VOLUME_HEADER_LEN};
1112    use crate::reader::{open_archive, open_archive_volumes};
1113    use crate::root_auth::data_block_merkle_leaf_hash;
1114    use crate::wire::{BlockRecord, CryptoHeader, VolumeHeader};
1115    use crate::writer::{write_archive, RegularFile};
1116
1117    use super::*;
1118
1119    fn master_key() -> MasterKey {
1120        MasterKey::from_raw_key(&[0x31; MASTER_KEY_LEN]).unwrap()
1121    }
1122
1123    fn tar_equivalent_regular_file<'a>(path: &'a str, contents: &'a [u8]) -> RegularFile<'a> {
1124        let mut file = RegularFile::new(path, contents);
1125        file.portable_metadata = PortableFileMetadata {
1126            source_os: "other-unix".into(),
1127            source_filesystem: "unknown".into(),
1128            mode_origin: PortableModeOrigin::Native,
1129            posix_owner: Some(PortablePosixOwner {
1130                uid: 0,
1131                gid: 0,
1132                uname: None,
1133                gname: None,
1134            }),
1135            attributes: None,
1136            native: Default::default(),
1137        };
1138        file
1139    }
1140
1141    fn options() -> WriterOptions {
1142        WriterOptions {
1143            archive_uuid: Some([0x41; 16]),
1144            session_id: Some([0x42; 16]),
1145            closed_at_ns: 987_654_321,
1146            stripe_width: 1,
1147            volume_loss_tolerance: 0,
1148            bit_rot_buffer_pct: 0,
1149            index_root_fec_parity_shards: 0,
1150            ..WriterOptions::default()
1151        }
1152    }
1153
1154    fn single_pass_equivalence_options() -> WriterOptions {
1155        WriterOptions {
1156            // The single-pass writer must commit the CryptoHeader before stdin is
1157            // consumed, so it predeclares the largest supported IndexRoot class.
1158            // Compare byte identity against the legacy writer with that same
1159            // declared class, not against the legacy default that can be raised
1160            // after payload planning.
1161            index_root_fec_data_shards: u16::MAX,
1162            ..options()
1163        }
1164    }
1165
1166    fn multi_volume_options(stripe_width: u32) -> WriterOptions {
1167        WriterOptions {
1168            stripe_width,
1169            volume_loss_tolerance: 0,
1170            ..options()
1171        }
1172    }
1173
1174    fn tar_stream(entries: &[(&str, &[u8])]) -> Vec<u8> {
1175        let mut out = Vec::new();
1176        for (path, data) in entries {
1177            out.extend_from_slice(&tar_header(path.as_bytes(), b'0', data.len() as u64));
1178            out.extend_from_slice(data);
1179            out.resize(out.len() + padding_to_512(data.len()), 0);
1180        }
1181        out.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1182        out
1183    }
1184
1185    fn pax_record(key: &str, value: &[u8]) -> Vec<u8> {
1186        let body_len = key.len() + 1 + value.len() + 1;
1187        let mut total = body_len + 2;
1188        loop {
1189            let next = body_len + total.to_string().len() + 1;
1190            if next == total {
1191                break;
1192            }
1193            total = next;
1194        }
1195        let mut out = format!("{total} {key}=").into_bytes();
1196        out.extend_from_slice(value);
1197        out.push(b'\n');
1198        out
1199    }
1200
1201    fn gnu_sparse_1_0_tar(path: &str, logical_size: u64, map: &[u8], data: &[u8]) -> Vec<u8> {
1202        let mut sparse_payload = map.to_vec();
1203        sparse_payload.resize(sparse_payload.len().div_ceil(512) * 512, 0);
1204        sparse_payload.extend_from_slice(data);
1205        let stored_size = sparse_payload.len() as u64;
1206        let mut pax = Vec::new();
1207        for (key, value) in [
1208            ("GNU.sparse.major", b"1".as_slice()),
1209            ("GNU.sparse.minor", b"0".as_slice()),
1210            ("GNU.sparse.name", path.as_bytes()),
1211            ("GNU.sparse.realsize", logical_size.to_string().as_bytes()),
1212            ("size", stored_size.to_string().as_bytes()),
1213        ] {
1214            pax.extend_from_slice(&pax_record(key, value));
1215        }
1216        let mut out = Vec::new();
1217        out.extend_from_slice(&tar_header(b"PaxHeaders/sparse", b'x', pax.len() as u64));
1218        out.extend_from_slice(&pax);
1219        out.resize(out.len() + padding_to_512(pax.len()), 0);
1220        out.extend_from_slice(&tar_header(b"GNUSparseFile.0/input", b'0', stored_size));
1221        out.extend_from_slice(&sparse_payload);
1222        out.resize(out.len() + padding_to_512(sparse_payload.len()), 0);
1223        out.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1224        out
1225    }
1226
1227    fn tar_header(path: &[u8], kind: u8, size: u64) -> [u8; TAR_BLOCK_LEN] {
1228        let mut header = [0u8; TAR_BLOCK_LEN];
1229        header[..path.len()].copy_from_slice(path);
1230        write_octal(&mut header[100..108], 0o644);
1231        write_octal(&mut header[108..116], 0);
1232        write_octal(&mut header[116..124], 0);
1233        write_octal(&mut header[124..136], size);
1234        write_octal(&mut header[136..148], 0);
1235        header[148..156].fill(b' ');
1236        header[156] = kind;
1237        header[257..263].copy_from_slice(b"ustar\0");
1238        header[263..265].copy_from_slice(b"00");
1239        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
1240        write_checksum(&mut header[148..156], checksum);
1241        header
1242    }
1243
1244    fn write_octal(field: &mut [u8], value: u64) {
1245        let digits = format!("{value:o}");
1246        field.fill(0);
1247        let start = field.len() - 1 - digits.len();
1248        field[..start].fill(b'0');
1249        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
1250    }
1251
1252    fn write_checksum(field: &mut [u8], value: u64) {
1253        let digits = format!("{value:06o}");
1254        field[0..6].copy_from_slice(digits.as_bytes());
1255        field[6] = 0;
1256        field[7] = b' ';
1257    }
1258
1259    fn write_base256(field: &mut [u8], value: u64) {
1260        field.fill(0);
1261        let mut value = value;
1262        for byte in field.iter_mut().rev() {
1263            *byte = (value & 0xff) as u8;
1264            value >>= 8;
1265        }
1266        field[0] |= 0x80;
1267    }
1268
1269    fn padding_to_512(len: usize) -> usize {
1270        let remainder = len % TAR_BLOCK_LEN;
1271        if remainder == 0 {
1272            0
1273        } else {
1274            TAR_BLOCK_LEN - remainder
1275        }
1276    }
1277
1278    fn data_leaf_hash_sequence(bytes: &[u8]) -> Vec<(u64, [u8; 32])> {
1279        let volume_header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
1280        let crypto_start = volume_header.crypto_header_offset as usize;
1281        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
1282        let crypto_header = CryptoHeader::parse(
1283            &bytes[crypto_start..crypto_end],
1284            volume_header.crypto_header_length,
1285        )
1286        .unwrap();
1287        let block_size = crypto_header.fixed.block_size as usize;
1288        let record_len = block_size + BLOCK_RECORD_FRAMING_LEN;
1289        let mut offset = crypto_end;
1290        let mut hashes = Vec::new();
1291        while bytes.get(offset..offset + 4) == Some(b"TZBK") {
1292            let record =
1293                BlockRecord::parse(&bytes[offset..offset + record_len], block_size).unwrap();
1294            if record.kind.is_data() {
1295                hashes.push((
1296                    record.block_index,
1297                    data_block_merkle_leaf_hash(
1298                        record.block_index,
1299                        record.kind,
1300                        record.flags,
1301                        &record.payload,
1302                    ),
1303                ));
1304            }
1305            offset += record_len;
1306        }
1307        hashes
1308    }
1309
1310    struct TinyReadCursor {
1311        data: Vec<u8>,
1312        cursor: usize,
1313        max_chunk: usize,
1314        reads: usize,
1315    }
1316
1317    impl TinyReadCursor {
1318        fn new(data: Vec<u8>, max_chunk: usize) -> Self {
1319            Self {
1320                data,
1321                cursor: 0,
1322                max_chunk,
1323                reads: 0,
1324            }
1325        }
1326    }
1327
1328    impl Read for TinyReadCursor {
1329        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1330            self.reads += 1;
1331            if self.cursor >= self.data.len() {
1332                return Ok(0);
1333            }
1334            let len = self
1335                .max_chunk
1336                .min(buf.len())
1337                .min(self.data.len() - self.cursor);
1338            buf[..len].copy_from_slice(&self.data[self.cursor..self.cursor + len]);
1339            self.cursor += len;
1340            Ok(len)
1341        }
1342    }
1343
1344    #[test]
1345    fn tar_stdin_single_volume_round_trips_list_verify_and_extract() {
1346        let input = tar_stream(&[
1347            ("alpha.txt", b"alpha payload".as_slice()),
1348            ("dir/beta.txt", b"beta payload".as_slice()),
1349        ]);
1350        let mut reader = TinyReadCursor::new(input, 17);
1351
1352        let archive = write_tar_stream_archive(&mut reader, &master_key(), options()).unwrap();
1353
1354        assert!(reader.reads > 10);
1355        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1356        opened.verify().unwrap();
1357        let listed = opened
1358            .list_files()
1359            .unwrap()
1360            .into_iter()
1361            .map(|entry| entry.path)
1362            .collect::<Vec<_>>();
1363        assert_eq!(listed, vec!["alpha.txt", "dir/beta.txt"]);
1364        assert_eq!(
1365            opened.extract_file("dir/beta.txt").unwrap(),
1366            Some(b"beta payload".to_vec())
1367        );
1368    }
1369
1370    #[test]
1371    fn tar_stdin_canonicalizes_gnu_sparse_1_0_without_materializing_holes() {
1372        let input = gnu_sparse_1_0_tar(
1373            "sparse.bin",
1374            16 * 1024,
1375            b"2\n1024\n3\n8192\n4\n",
1376            b"abcWXYZ",
1377        );
1378        let archive = write_tar_stream_archive(input.as_slice(), &master_key(), options()).unwrap();
1379        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1380        opened.verify().unwrap();
1381        let listed = opened.list_files().unwrap();
1382        assert_eq!(listed.len(), 1);
1383        assert_eq!(listed[0].path, "sparse.bin");
1384        assert_eq!(listed[0].file_data_size, 16 * 1024);
1385        let logical = opened.extract_file("sparse.bin").unwrap().unwrap();
1386        assert_eq!(logical.len(), 16 * 1024);
1387        assert_eq!(&logical[1024..1027], b"abc");
1388        assert_eq!(&logical[8192..8196], b"WXYZ");
1389        assert!(logical[..1024].iter().all(|byte| *byte == 0));
1390        assert!(logical[8196..].iter().all(|byte| *byte == 0));
1391    }
1392
1393    #[test]
1394    fn tar_stdin_canonicalizes_all_hole_gnu_sparse_1_0() {
1395        let input = gnu_sparse_1_0_tar("hole.bin", 1 << 20, b"0\n", b"");
1396        let archive = write_tar_stream_archive(input.as_slice(), &master_key(), options()).unwrap();
1397        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1398        opened.verify().unwrap();
1399        let logical = opened.extract_file("hole.bin").unwrap().unwrap();
1400        assert_eq!(logical.len(), 1 << 20);
1401        assert!(logical.iter().all(|byte| *byte == 0));
1402    }
1403
1404    #[test]
1405    fn pax_mtime_preserves_fraction_and_pre_epoch_value() {
1406        assert_eq!(
1407            parse_pax_mtime(b"1700000000.123456789").unwrap(),
1408            ArchiveTimestamp::new(1_700_000_000, 123_456_789)
1409        );
1410        assert_eq!(
1411            parse_pax_mtime(b"-1.5").unwrap(),
1412            ArchiveTimestamp::new(-1, 500_000_000)
1413        );
1414        assert!(matches!(
1415            parse_pax_mtime(b"-0.5"),
1416            Err(FormatError::WriterUnsupported(_))
1417        ));
1418    }
1419
1420    #[test]
1421    fn sized_raw_stdin_round_trips_as_regular_tar_member_archive() {
1422        let input = b"raw bytes from stdin";
1423        let mut sink = MemoryArchiveSink::default();
1424
1425        let summary = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1426            input.as_slice(),
1427            "raw/data.bin",
1428            input.len() as u64,
1429            &master_key(),
1430            options(),
1431            &KdfParams::Raw,
1432            None,
1433            None,
1434            &mut sink,
1435        )
1436        .unwrap();
1437
1438        assert_eq!(summary.input_bytes, input.len() as u64);
1439        let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1440        opened.verify().unwrap();
1441        assert_eq!(
1442            opened.extract_file("raw/data.bin").unwrap(),
1443            Some(input.to_vec())
1444        );
1445    }
1446
1447    #[test]
1448    fn sized_raw_stdin_known_size_multi_volume_round_trips() {
1449        let input = (0..150_000)
1450            .map(|index| (index % 251) as u8)
1451            .collect::<Vec<_>>();
1452        let mut sink = MemoryArchiveSink::default();
1453
1454        let summary = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1455            input.as_slice(),
1456            "raw/data.bin",
1457            input.len() as u64,
1458            &master_key(),
1459            multi_volume_options(3),
1460            &KdfParams::Raw,
1461            None,
1462            None,
1463            &mut sink,
1464        )
1465        .unwrap();
1466
1467        assert_eq!(summary.archive.volume_count, 3);
1468        assert_eq!(sink.volumes.len(), 3);
1469        let refs = sink.volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
1470        let opened = open_archive_volumes(&refs, &master_key()).unwrap();
1471        opened.verify().unwrap();
1472        assert_eq!(
1473            opened.extract_file("raw/data.bin").unwrap(),
1474            Some(input.to_vec())
1475        );
1476    }
1477
1478    #[test]
1479    fn sized_raw_stdin_rejects_short_input() {
1480        let mut sink = MemoryArchiveSink::default();
1481
1482        let error = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1483            b"short".as_slice(),
1484            "raw/data.bin",
1485            6,
1486            &master_key(),
1487            options(),
1488            &KdfParams::Raw,
1489            None,
1490            None,
1491            &mut sink,
1492        )
1493        .unwrap_err();
1494
1495        assert!(matches!(error, ArchiveWriteError::Io(_)));
1496    }
1497
1498    #[test]
1499    fn sized_raw_stdin_rejects_trailing_input() {
1500        let mut sink = MemoryArchiveSink::default();
1501
1502        let error = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1503            b"toolong".as_slice(),
1504            "raw/data.bin",
1505            3,
1506            &master_key(),
1507            options(),
1508            &KdfParams::Raw,
1509            None,
1510            None,
1511            &mut sink,
1512        )
1513        .unwrap_err();
1514
1515        assert!(matches!(
1516            error,
1517            ArchiveWriteError::Format(FormatError::InvalidArchive(
1518                "raw stdin exceeds declared --stdin-size"
1519            ))
1520        ));
1521    }
1522
1523    #[test]
1524    fn tar_stdin_sink_summary_reports_input_not_output_tar_size() {
1525        let input = tar_stream(&[("one.txt", b"one".as_slice())]);
1526        let mut sink = MemoryArchiveSink::default();
1527
1528        let summary =
1529            write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1530                .unwrap();
1531
1532        assert_eq!(summary.input_member_count, 1);
1533        assert_eq!(summary.input_tar_bytes, input.len() as u64);
1534        assert_eq!(summary.archive.volume_count, 1);
1535        assert_eq!(sink.volumes.len(), 1);
1536        open_archive(&sink.volumes[0], &master_key())
1537            .unwrap()
1538            .verify()
1539            .unwrap();
1540    }
1541
1542    #[test]
1543    fn tar_stdin_large_single_volume_round_trips() {
1544        let beta = (0..150_000)
1545            .map(|index| (index % 251) as u8)
1546            .collect::<Vec<_>>();
1547        let input = tar_stream(&[
1548            ("alpha.txt", b"alpha payload".as_slice()),
1549            ("dir/beta.bin", beta.as_slice()),
1550        ]);
1551
1552        let archive = write_tar_stream_archive(&input[..], &master_key(), options()).unwrap();
1553        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1554
1555        opened.verify().unwrap();
1556        assert_eq!(opened.extract_file("dir/beta.bin").unwrap(), Some(beta));
1557    }
1558
1559    #[test]
1560    fn tar_stdin_multi_volume_round_trips_list_verify_and_extract() {
1561        let beta = (0..150_000)
1562            .map(|index| (index % 251) as u8)
1563            .collect::<Vec<_>>();
1564        let input = tar_stream(&[
1565            ("alpha.txt", b"alpha payload".as_slice()),
1566            ("dir/beta.bin", beta.as_slice()),
1567        ]);
1568        let mut reader = TinyReadCursor::new(input, 31);
1569        let mut sink = MemoryArchiveSink::default();
1570
1571        let summary = write_tar_stream_archive_to_sink(
1572            &mut reader,
1573            &master_key(),
1574            multi_volume_options(4),
1575            &mut sink,
1576        )
1577        .unwrap();
1578
1579        assert!(reader.reads > 10);
1580        assert_eq!(summary.input_member_count, 2);
1581        assert_eq!(summary.archive.volume_count, 4);
1582        assert_eq!(sink.volumes.len(), 4);
1583        let refs = sink.volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
1584        let opened = open_archive_volumes(&refs, &master_key()).unwrap();
1585        assert_eq!(
1586            opened.extract_file("alpha.txt").unwrap(),
1587            Some(b"alpha payload".to_vec())
1588        );
1589        assert_eq!(
1590            opened.extract_file("dir/beta.bin").unwrap(),
1591            Some(beta.clone())
1592        );
1593        opened.verify().unwrap();
1594        let listed = opened
1595            .list_files()
1596            .unwrap()
1597            .into_iter()
1598            .map(|entry| entry.path)
1599            .collect::<Vec<_>>();
1600        assert_eq!(listed, vec!["alpha.txt", "dir/beta.bin"]);
1601    }
1602
1603    #[test]
1604    fn tar_stdin_emits_identical_bytes_to_file_list_create_with_streaming_metadata_class() {
1605        let input = tar_stream(&[
1606            ("alpha.txt", b"alpha payload".as_slice()),
1607            ("dir/beta.txt", b"beta payload".as_slice()),
1608        ]);
1609        let options = single_pass_equivalence_options();
1610
1611        let streaming = write_tar_stream_archive(&input[..], &master_key(), options).unwrap();
1612        let legacy = write_archive(
1613            &[
1614                tar_equivalent_regular_file("alpha.txt", b"alpha payload"),
1615                tar_equivalent_regular_file("dir/beta.txt", b"beta payload"),
1616            ],
1617            &master_key(),
1618            options,
1619        )
1620        .unwrap();
1621
1622        assert_eq!(streaming.bytes, legacy.bytes);
1623        assert_eq!(streaming.bootstrap_sidecar, legacy.bootstrap_sidecar);
1624    }
1625
1626    #[test]
1627    fn tar_stdin_data_leaf_hashes_match_file_list_writer_sequence() {
1628        let input = tar_stream(&[
1629            ("alpha.txt", b"alpha payload".as_slice()),
1630            ("dir/beta.txt", b"beta payload".as_slice()),
1631        ]);
1632        let options = single_pass_equivalence_options();
1633
1634        let streaming = write_tar_stream_archive(&input[..], &master_key(), options).unwrap();
1635        let legacy = write_archive(
1636            &[
1637                tar_equivalent_regular_file("alpha.txt", b"alpha payload"),
1638                tar_equivalent_regular_file("dir/beta.txt", b"beta payload"),
1639            ],
1640            &master_key(),
1641            options,
1642        )
1643        .unwrap();
1644
1645        assert_eq!(
1646            data_leaf_hash_sequence(&streaming.bytes),
1647            data_leaf_hash_sequence(&legacy.bytes)
1648        );
1649    }
1650
1651    #[test]
1652    fn tar_stdin_empty_stream_accepts_two_zero_eof_blocks() {
1653        let input = vec![0u8; TAR_BLOCK_LEN * 2];
1654        let mut sink = MemoryArchiveSink::default();
1655
1656        let summary =
1657            write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1658                .unwrap();
1659
1660        assert_eq!(summary.input_member_count, 0);
1661        assert_eq!(summary.input_tar_bytes, input.len() as u64);
1662        let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1663        opened.verify().unwrap();
1664        assert!(opened.list_files().unwrap().is_empty());
1665    }
1666
1667    #[test]
1668    fn tar_stdin_accepts_base256_numeric_size_fields() {
1669        let mut header = tar_header(b"large-format.txt", b'0', 0);
1670        write_base256(&mut header[124..136], 4);
1671        header[148..156].fill(b' ');
1672        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
1673        write_checksum(&mut header[148..156], checksum);
1674        let mut input = Vec::new();
1675        input.extend_from_slice(&header);
1676        input.extend_from_slice(b"data");
1677        input.resize(input.len() + padding_to_512(4), 0);
1678        input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1679
1680        let archive = write_tar_stream_archive(&input[..], &master_key(), options()).unwrap();
1681        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1682
1683        assert_eq!(
1684            opened.extract_file("large-format.txt").unwrap(),
1685            Some(b"data".to_vec())
1686        );
1687    }
1688
1689    #[test]
1690    fn tar_stdin_rejects_metadata_header_without_following_member() {
1691        let payload = b"13 comment=x\n";
1692        let mut input = Vec::new();
1693        input.extend_from_slice(&tar_header(
1694            b"PaxHeaders/unused",
1695            b'x',
1696            payload.len() as u64,
1697        ));
1698        input.extend_from_slice(payload);
1699        input.resize(input.len() + padding_to_512(payload.len()), 0);
1700        input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1701
1702        let error = write_tar_stream_archive(&input[..], &master_key(), options()).unwrap_err();
1703
1704        assert_eq!(
1705            error,
1706            FormatError::InvalidArchive("tar stream ended after metadata header")
1707        );
1708    }
1709
1710    #[test]
1711    fn tar_stdin_preserves_directory_entries_and_regular_children() {
1712        let mut input = Vec::new();
1713        input.extend_from_slice(&tar_header(b"dir/", b'5', 0));
1714        input.extend_from_slice(&tar_header(b"dir/file.txt", b'0', 4));
1715        input.extend_from_slice(b"data");
1716        input.resize(input.len() + padding_to_512(4), 0);
1717        input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1718        let mut sink = MemoryArchiveSink::default();
1719
1720        let summary =
1721            write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1722                .unwrap();
1723
1724        assert_eq!(summary.input_member_count, 2);
1725        let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1726        assert_eq!(
1727            opened
1728                .list_files()
1729                .unwrap()
1730                .into_iter()
1731                .map(|entry| entry.path)
1732                .collect::<Vec<_>>(),
1733            vec!["dir", "dir/file.txt"]
1734        );
1735    }
1736
1737    #[test]
1738    fn tar_stdin_preserves_symlink_target_and_mtime() {
1739        let mut input = Vec::new();
1740        let mut header = tar_header(b"link", b'2', 0);
1741        header[136..148].fill(0);
1742        write_octal(&mut header[136..148], 1_700_000_321);
1743        header[157..167].copy_from_slice(b"target.txt");
1744        header[148..156].fill(b' ');
1745        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
1746        write_checksum(&mut header[148..156], checksum);
1747        input.extend_from_slice(&header);
1748        input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1749        let mut sink = MemoryArchiveSink::default();
1750
1751        let summary =
1752            write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1753                .unwrap();
1754
1755        assert_eq!(summary.input_member_count, 1);
1756        let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1757        let member = opened.list_files().unwrap().pop().unwrap();
1758        assert_eq!(member.kind, crate::tar_model::TarEntryKind::Symlink);
1759        assert_eq!(member.mtime, ArchiveTimestamp::from_seconds(1_700_000_321));
1760    }
1761
1762    #[test]
1763    fn tar_stdin_rejects_volume_loss_tolerance() {
1764        let mut bad = options();
1765        bad.volume_loss_tolerance = 1;
1766
1767        let error = write_tar_stream_archive(
1768            &tar_stream(&[("x", b"x".as_slice())])[..],
1769            &master_key(),
1770            bad,
1771        )
1772        .unwrap_err();
1773
1774        assert_eq!(
1775            error,
1776            FormatError::WriterUnsupported("streaming create cannot tolerate volume loss")
1777        );
1778    }
1779}