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 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}