1use std::io::{self, ErrorKind, Read};
2
3use crate::crypto::{KdfParams, MasterKey};
4use crate::format::{ArchiveWriteError, FormatError};
5use crate::metadata::{
6 normalize_lookup_file_path, validate_directory_path_bytes, validate_file_path_bytes,
7};
8use crate::writer::{
9 write_ordered_parallel_stream_archive_to_sink, ArchiveWriteSink, MemoryArchiveSink,
10 RootAuthAuthenticator, RootAuthWriterConfig, StreamingRegularMember, WriterOptions,
11 WrittenArchive, WrittenArchiveSummary,
12};
13
14const TAR_BLOCK_LEN: usize = 512;
15const MAX_TAR_STDIN_METADATA_PAYLOAD_BYTES: usize = 1024 * 1024;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct StreamingTarWriterSummary {
19 pub archive: WrittenArchiveSummary,
20 pub input_member_count: u64,
21 pub input_tar_bytes: u64,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct StreamingRawWriterSummary {
26 pub archive: WrittenArchiveSummary,
27 pub input_bytes: u64,
28}
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31struct TarStdinInputSummary {
32 regular_file_count: u64,
33 skipped_directory_count: u64,
34 input_tar_bytes: u64,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38struct TarStdinRegularMember {
39 path: Vec<u8>,
40 mode: u32,
41 mtime: u64,
42 logical_size: u64,
43}
44
45#[derive(Debug, Clone, Default, PartialEq, Eq)]
46struct LocalTarMetadata {
47 pending_header: bool,
48 pax_path: Option<Vec<u8>>,
49 pax_size: Option<u64>,
50 pax_mode: Option<u32>,
51 pax_mtime: Option<u64>,
52 gnu_long_name: Option<Vec<u8>>,
53}
54
55impl LocalTarMetadata {
56 fn has_pending(&self) -> bool {
57 self.pending_header
58 || self.pax_path.is_some()
59 || self.pax_size.is_some()
60 || self.pax_mode.is_some()
61 || self.pax_mtime.is_some()
62 || self.gnu_long_name.is_some()
63 }
64}
65
66pub fn write_tar_stream_archive<R: Read>(
67 reader: R,
68 master_key: &MasterKey,
69 options: WriterOptions,
70) -> Result<WrittenArchive, FormatError> {
71 let mut sink = MemoryArchiveSink::default();
72 let summary = write_tar_stream_archive_to_sink(reader, master_key, options, &mut sink)
73 .map_err(format_error_from_archive_write_error)?;
74 Ok(WrittenArchive {
75 bytes: sink
76 .volumes
77 .first()
78 .cloned()
79 .ok_or(FormatError::WriterInvariant("no volumes emitted"))?,
80 volumes: sink.volumes,
81 bootstrap_sidecar: sink.bootstrap_sidecar,
82 archive_uuid: summary.archive.archive_uuid,
83 session_id: summary.archive.session_id,
84 timings: summary.archive.timings,
85 })
86}
87
88pub fn write_tar_stream_archive_to_sink<R, O>(
89 reader: R,
90 master_key: &MasterKey,
91 options: WriterOptions,
92 sink: &mut O,
93) -> Result<StreamingTarWriterSummary, ArchiveWriteError>
94where
95 R: Read,
96 O: ArchiveWriteSink,
97{
98 write_tar_stream_archive_to_sink_with_kdf_and_root_auth(
99 reader,
100 master_key,
101 options,
102 &KdfParams::Raw,
103 None,
104 None,
105 sink,
106 )
107}
108
109#[allow(clippy::too_many_arguments)]
110pub fn write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth<R, O>(
111 mut reader: R,
112 archive_path: &str,
113 input_size: u64,
114 master_key: &MasterKey,
115 options: WriterOptions,
116 kdf_params: &KdfParams,
117 root_auth: Option<RootAuthWriterConfig<'_>>,
118 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
119 sink: &mut O,
120) -> Result<StreamingRawWriterSummary, ArchiveWriteError>
121where
122 R: Read,
123 O: ArchiveWriteSink,
124{
125 validate_streaming_create_writer_options(options)?;
126 let archive_path = normalize_lookup_file_path(archive_path, options.max_path_length)?;
127 let archive = write_ordered_parallel_stream_archive_to_sink(
128 master_key,
129 options,
130 kdf_params,
131 root_auth,
132 authenticator,
133 None,
134 sink,
135 |writer| {
136 let mut payload = SizedRawPayloadReader {
137 reader: &mut reader,
138 remaining: input_size,
139 };
140 writer.write_regular_member_from_reader(
141 StreamingRegularMember {
142 archive_path,
143 file_data_size: input_size,
144 mode: 0o644,
145 mtime: 0,
146 },
147 &mut payload,
148 )?;
149 if payload.remaining != 0 {
150 return Err(FormatError::WriterInvariant(
151 "raw stdin payload was not fully consumed",
152 )
153 .into());
154 }
155 reject_trailing_raw_stdin_bytes(&mut reader)?;
156 Ok(())
157 },
158 )?;
159 Ok(StreamingRawWriterSummary {
160 archive,
161 input_bytes: input_size,
162 })
163}
164
165#[allow(clippy::too_many_arguments)]
166pub fn write_tar_stream_archive_to_sink_with_kdf_and_root_auth<R, O>(
167 mut reader: R,
168 master_key: &MasterKey,
169 options: WriterOptions,
170 kdf_params: &KdfParams,
171 root_auth: Option<RootAuthWriterConfig<'_>>,
172 authenticator: Option<&mut RootAuthAuthenticator<'_>>,
173 sink: &mut O,
174) -> Result<StreamingTarWriterSummary, ArchiveWriteError>
175where
176 R: Read,
177 O: ArchiveWriteSink,
178{
179 validate_streaming_create_writer_options(options)?;
180 let mut input_summary = None;
181 let archive = write_ordered_parallel_stream_archive_to_sink(
182 master_key,
183 options,
184 kdf_params,
185 root_auth,
186 authenticator,
187 None,
188 sink,
189 |writer| {
190 let summary = stream_tar_stdin_regulars(
191 &mut reader,
192 options.max_path_length,
193 |member, payload| {
194 writer.write_regular_member_from_reader(
195 StreamingRegularMember {
196 archive_path: member.path,
197 file_data_size: member.logical_size,
198 mode: member.mode,
199 mtime: member.mtime,
200 },
201 payload,
202 )
203 },
204 )?;
205 input_summary = Some(summary);
206 Ok(())
207 },
208 )?;
209 let input_summary = input_summary.ok_or(FormatError::WriterInvariant(
210 "streaming tar parser did not return a summary",
211 ))?;
212 Ok(StreamingTarWriterSummary {
213 archive,
214 input_member_count: input_summary.regular_file_count,
215 input_tar_bytes: input_summary.input_tar_bytes,
216 })
217}
218
219fn format_error_from_archive_write_error(error: ArchiveWriteError) -> FormatError {
220 match error {
221 ArchiveWriteError::Format(error) => error,
222 ArchiveWriteError::Io(_) => FormatError::WriterInvariant("in-memory archive writer failed"),
223 }
224}
225
226fn validate_streaming_create_writer_options(options: WriterOptions) -> Result<(), FormatError> {
227 if options.volume_loss_tolerance != 0 {
228 return Err(FormatError::WriterUnsupported(
229 "streaming create cannot tolerate volume loss",
230 ));
231 }
232 if options.target_volume_size.is_some() {
233 return Err(FormatError::WriterUnsupported(
234 "streaming create does not support target volume sizing",
235 ));
236 }
237 Ok(())
238}
239
240struct SizedRawPayloadReader<'a, R: Read> {
241 reader: &'a mut R,
242 remaining: u64,
243}
244
245impl<R: Read> Read for SizedRawPayloadReader<'_, R> {
246 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
247 if out.is_empty() || self.remaining == 0 {
248 return Ok(0);
249 }
250 let max_read = out
251 .len()
252 .min(usize::try_from(self.remaining).unwrap_or(usize::MAX));
253 match self.reader.read(&mut out[..max_read]) {
254 Ok(read) => {
255 self.remaining -= read as u64;
256 Ok(read)
257 }
258 Err(error) if error.kind() == ErrorKind::Interrupted => self.read(out),
259 Err(error) => Err(error),
260 }
261 }
262}
263
264fn reject_trailing_raw_stdin_bytes<R: Read>(reader: &mut R) -> Result<(), ArchiveWriteError> {
265 let mut extra = [0u8; 1];
266 loop {
267 match reader.read(&mut extra) {
268 Ok(0) => return Ok(()),
269 Ok(_) => {
270 return Err(
271 FormatError::InvalidArchive("raw stdin exceeds declared --stdin-size").into(),
272 )
273 }
274 Err(error) if error.kind() == ErrorKind::Interrupted => {}
275 Err(error) => return Err(ArchiveWriteError::Io(error)),
276 }
277 }
278}
279
280fn stream_tar_stdin_regulars<R, F>(
281 reader: &mut R,
282 max_path_length: u32,
283 mut on_regular: F,
284) -> Result<TarStdinInputSummary, ArchiveWriteError>
285where
286 R: Read,
287 F: for<'a> FnMut(
288 TarStdinRegularMember,
289 &mut LimitedTarPayloadReader<'a, R>,
290 ) -> Result<(), ArchiveWriteError>,
291{
292 let mut summary = TarStdinInputSummary::default();
293 let mut metadata = LocalTarMetadata::default();
294
295 loop {
296 let Some(header) = read_tar_block(reader, &mut summary.input_tar_bytes)? else {
297 if metadata.has_pending() {
298 return Err(
299 FormatError::InvalidArchive("tar stream ended after metadata header").into(),
300 );
301 }
302 return Ok(summary);
303 };
304 if header.iter().all(|byte| *byte == 0) {
305 if metadata.has_pending() {
306 return Err(
307 FormatError::InvalidArchive("tar stream ended after metadata header").into(),
308 );
309 }
310 let second = read_tar_block(reader, &mut summary.input_tar_bytes)?.ok_or(
311 FormatError::InvalidArchive("tar stream ended inside end-of-archive marker"),
312 )?;
313 if second.iter().any(|byte| *byte != 0) {
314 return Err(FormatError::InvalidArchive(
315 "tar stream has non-zero bytes after end-of-archive marker",
316 )
317 .into());
318 }
319 drain_zero_blocks_to_eof(reader, &mut summary.input_tar_bytes)?;
320 return Ok(summary);
321 }
322
323 verify_tar_checksum(&header)?;
324 let typeflag = header[156];
325 let header_size = parse_tar_number(&header[124..136])?;
326 match typeflag {
327 b'x' => {
328 let payload =
329 read_metadata_payload(reader, header_size, &mut summary.input_tar_bytes)?;
330 metadata.pending_header = true;
331 parse_pax_records(&payload, &mut metadata)?;
332 }
333 b'L' => {
334 let payload =
335 read_metadata_payload(reader, header_size, &mut summary.input_tar_bytes)?;
336 metadata.pending_header = true;
337 metadata.gnu_long_name = Some(trimmed_metadata_payload(&payload));
338 }
339 b'K' => {
340 let _payload =
341 read_metadata_payload(reader, header_size, &mut summary.input_tar_bytes)?;
342 metadata.pending_header = true;
343 }
344 b'g' => {
345 return Err(
346 FormatError::InvalidArchive("global PAX headers are not allowed").into(),
347 )
348 }
349 b'V' | b'M' | b'N' => {
350 return Err(
351 FormatError::InvalidArchive("global GNU headers are not allowed").into(),
352 )
353 }
354 b'S' => {
355 return Err(
356 FormatError::ReaderUnsupported("unsupported GNU sparse tar entry").into(),
357 )
358 }
359 0 | b'0' | b'5' | b'1' | b'2' | b'3' | b'4' | b'6' => {
360 let effective_size = metadata.pax_size.unwrap_or(header_size);
361 let mode = metadata
362 .pax_mode
363 .unwrap_or(parse_tar_number(&header[100..108])? as u32);
364 let mtime = metadata
365 .pax_mtime
366 .unwrap_or(parse_tar_number(&header[136..148])?);
367 let path = canonical_main_path(&header, typeflag, &metadata, max_path_length)?;
368 metadata = LocalTarMetadata::default();
369
370 match typeflag {
371 0 | b'0' => {
372 let member = TarStdinRegularMember {
373 path,
374 mode,
375 mtime,
376 logical_size: effective_size,
377 };
378 {
379 let mut payload = LimitedTarPayloadReader {
380 reader,
381 remaining: effective_size,
382 input_tar_bytes: &mut summary.input_tar_bytes,
383 };
384 on_regular(member, &mut payload)?;
385 if payload.remaining != 0 {
386 return Err(FormatError::WriterInvariant(
387 "streaming tar payload was not fully consumed",
388 )
389 .into());
390 }
391 }
392 drain_tar_padding(
393 reader,
394 padding_to_512_u64(effective_size),
395 &mut summary.input_tar_bytes,
396 )?;
397 summary.regular_file_count = checked_input_add(
398 summary.regular_file_count,
399 1,
400 "tar regular file count",
401 )?;
402 }
403 b'5' => {
404 if effective_size != 0 {
405 return Err(FormatError::InvalidArchive(
406 "non-regular tar entry has non-zero payload size",
407 )
408 .into());
409 }
410 summary.skipped_directory_count = checked_input_add(
411 summary.skipped_directory_count,
412 1,
413 "tar directory count",
414 )?;
415 }
416 _ => {
417 return Err(FormatError::WriterUnsupported(
418 "streaming tar stdin supports regular files and directory entries only",
419 )
420 .into())
421 }
422 }
423 }
424 _ => return Err(FormatError::ReaderUnsupported("unsupported tar entry type").into()),
425 }
426 }
427}
428
429struct LimitedTarPayloadReader<'a, R: Read> {
430 reader: &'a mut R,
431 remaining: u64,
432 input_tar_bytes: &'a mut u64,
433}
434
435impl<R: Read> Read for LimitedTarPayloadReader<'_, R> {
436 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
437 if out.is_empty() || self.remaining == 0 {
438 return Ok(0);
439 }
440 let max_read = out
441 .len()
442 .min(usize::try_from(self.remaining).unwrap_or(usize::MAX));
443 match self.reader.read(&mut out[..max_read]) {
444 Ok(0) => Err(io::Error::new(
445 ErrorKind::UnexpectedEof,
446 "tar stream ended inside member payload",
447 )),
448 Ok(read) => {
449 self.remaining -= read as u64;
450 *self.input_tar_bytes =
451 checked_input_add(*self.input_tar_bytes, read as u64, "tar input size")
452 .map_err(io_error_from_format)?;
453 Ok(read)
454 }
455 Err(error) if error.kind() == ErrorKind::Interrupted => self.read(out),
456 Err(error) => Err(error),
457 }
458 }
459}
460
461fn read_tar_block<R: Read>(
462 reader: &mut R,
463 input_tar_bytes: &mut u64,
464) -> Result<Option<[u8; TAR_BLOCK_LEN]>, ArchiveWriteError> {
465 let mut block = [0u8; TAR_BLOCK_LEN];
466 let mut filled = 0usize;
467 while filled < TAR_BLOCK_LEN {
468 match reader.read(&mut block[filled..]) {
469 Ok(0) if filled == 0 => return Ok(None),
470 Ok(0) => {
471 return Err(
472 FormatError::InvalidArchive("tar stream ended inside member group").into(),
473 )
474 }
475 Ok(read) => {
476 filled += read;
477 *input_tar_bytes =
478 checked_input_add(*input_tar_bytes, read as u64, "tar input size")?;
479 }
480 Err(error) if error.kind() == ErrorKind::Interrupted => {}
481 Err(error) => return Err(ArchiveWriteError::Io(error)),
482 }
483 }
484 Ok(Some(block))
485}
486
487fn read_metadata_payload<R: Read>(
488 reader: &mut R,
489 size: u64,
490 input_tar_bytes: &mut u64,
491) -> Result<Vec<u8>, ArchiveWriteError> {
492 let len = usize::try_from(size)
493 .map_err(|_| FormatError::ReaderUnsupported("tar metadata payload is too large"))?;
494 if len > MAX_TAR_STDIN_METADATA_PAYLOAD_BYTES {
495 return Err(
496 FormatError::ReaderUnsupported("tar metadata payload exceeds streaming cap").into(),
497 );
498 }
499 let mut payload = vec![0u8; len];
500 read_exact_counted(reader, &mut payload, input_tar_bytes)?;
501 drain_tar_padding(reader, padding_to_512_u64(size), input_tar_bytes)?;
502 Ok(payload)
503}
504
505fn read_exact_counted<R: Read>(
506 reader: &mut R,
507 mut out: &mut [u8],
508 input_tar_bytes: &mut u64,
509) -> Result<(), ArchiveWriteError> {
510 while !out.is_empty() {
511 match reader.read(out) {
512 Ok(0) => {
513 return Err(
514 FormatError::InvalidArchive("tar stream ended inside member group").into(),
515 )
516 }
517 Ok(read) => {
518 *input_tar_bytes =
519 checked_input_add(*input_tar_bytes, read as u64, "tar input size")?;
520 let remaining = out;
521 out = &mut remaining[read..];
522 }
523 Err(error) if error.kind() == ErrorKind::Interrupted => {}
524 Err(error) => return Err(ArchiveWriteError::Io(error)),
525 }
526 }
527 Ok(())
528}
529
530fn drain_tar_padding<R: Read>(
531 reader: &mut R,
532 padding: u64,
533 input_tar_bytes: &mut u64,
534) -> Result<(), ArchiveWriteError> {
535 let mut remaining = padding;
536 let mut buf = [0u8; TAR_BLOCK_LEN];
537 while remaining > 0 {
538 let take = buf
539 .len()
540 .min(usize::try_from(remaining).unwrap_or(usize::MAX));
541 read_exact_counted(reader, &mut buf[..take], input_tar_bytes)?;
542 if buf[..take].iter().any(|byte| *byte != 0) {
543 return Err(FormatError::InvalidArchive("tar member padding is non-zero").into());
544 }
545 remaining -= take as u64;
546 }
547 Ok(())
548}
549
550fn drain_zero_blocks_to_eof<R: Read>(
551 reader: &mut R,
552 input_tar_bytes: &mut u64,
553) -> Result<(), ArchiveWriteError> {
554 while let Some(block) = read_tar_block(reader, input_tar_bytes)? {
555 if block.iter().any(|byte| *byte != 0) {
556 return Err(FormatError::InvalidArchive(
557 "tar stream has non-zero bytes after end-of-archive marker",
558 )
559 .into());
560 }
561 }
562 Ok(())
563}
564
565fn canonical_main_path(
566 header: &[u8],
567 typeflag: u8,
568 metadata: &LocalTarMetadata,
569 max_path_length: u32,
570) -> Result<Vec<u8>, FormatError> {
571 let mut path = metadata
572 .pax_path
573 .clone()
574 .or_else(|| metadata.gnu_long_name.clone())
575 .unwrap_or_else(|| ustar_path(header));
576 while path.starts_with(b"./") {
577 path.drain(..2);
578 }
579 if typeflag == b'5' && path.ends_with(b"/") && !path.ends_with(b"//") {
580 path.pop();
581 }
582 if typeflag == b'5' {
583 validate_directory_path_bytes(&path, max_path_length)?;
584 } else {
585 validate_file_path_bytes(&path, max_path_length)?;
586 }
587 Ok(path)
588}
589
590fn ustar_path(header: &[u8]) -> Vec<u8> {
591 let name = nul_trimmed(&header[0..100]);
592 let prefix = nul_trimmed(&header[345..500]);
593 if prefix.is_empty() {
594 return name.to_vec();
595 }
596 let mut path = Vec::with_capacity(prefix.len() + 1 + name.len());
597 path.extend_from_slice(prefix);
598 path.push(b'/');
599 path.extend_from_slice(name);
600 path
601}
602
603fn parse_pax_records(payload: &[u8], metadata: &mut LocalTarMetadata) -> Result<(), FormatError> {
604 let mut cursor = 0usize;
605 while cursor < payload.len() {
606 let len_digits_start = cursor;
607 while cursor < payload.len() && payload[cursor].is_ascii_digit() {
608 cursor += 1;
609 }
610 if cursor == len_digits_start || cursor >= payload.len() || payload[cursor] != b' ' {
611 return Err(FormatError::InvalidArchive("malformed PAX record"));
612 }
613 let len = parse_decimal_usize(&payload[len_digits_start..cursor])?;
614 let record_start = len_digits_start;
615 let record_end = record_start
616 .checked_add(len)
617 .ok_or(FormatError::InvalidArchive("malformed PAX record"))?;
618 if record_end > payload.len() || len < 4 {
619 return Err(FormatError::InvalidArchive("malformed PAX record"));
620 }
621 let body_start = cursor + 1;
622 let record = &payload[body_start..record_end];
623 if record.last().copied() != Some(b'\n') {
624 return Err(FormatError::InvalidArchive("malformed PAX record"));
625 }
626 let body = &record[..record.len() - 1];
627 let eq = body
628 .iter()
629 .position(|byte| *byte == b'=')
630 .ok_or(FormatError::InvalidArchive("malformed PAX record"))?;
631 let key = std::str::from_utf8(&body[..eq])
632 .map_err(|_| FormatError::InvalidArchive("malformed PAX key"))?;
633 let value = &body[eq + 1..];
634 match key {
635 "path" => metadata.pax_path = Some(value.to_vec()),
636 "size" => metadata.pax_size = Some(parse_decimal_u64(value)?),
637 "mode" => {
638 metadata.pax_mode = Some(
639 u32::try_from(parse_decimal_u64(value)?)
640 .map_err(|_| FormatError::WriterUnsupported("tar mode overflow"))?,
641 )
642 }
643 "mtime" => metadata.pax_mtime = Some(parse_pax_mtime(value)?),
644 key if key.starts_with("GNU.sparse.") => {
645 return Err(FormatError::ReaderUnsupported(
646 "unsupported GNU sparse tar entry",
647 ))
648 }
649 _ => {}
650 }
651 cursor = record_end;
652 }
653 Ok(())
654}
655
656fn verify_tar_checksum(header: &[u8]) -> Result<(), FormatError> {
657 let expected = parse_tar_number(&header[148..156])?;
658 let actual = header[..148]
659 .iter()
660 .chain(header[156..].iter())
661 .fold(8u64 * 32, |sum, byte| sum + u64::from(*byte));
662 if actual != expected {
663 return Err(FormatError::InvalidArchive("tar header checksum mismatch"));
664 }
665 Ok(())
666}
667
668fn parse_tar_number(field: &[u8]) -> Result<u64, FormatError> {
669 if field.first().is_some_and(|byte| byte & 0x80 != 0) {
670 return parse_tar_base256(field);
671 }
672 parse_tar_octal(field)
673}
674
675fn parse_tar_base256(field: &[u8]) -> Result<u64, FormatError> {
676 let Some(first) = field.first() else {
677 return Err(FormatError::InvalidArchive("empty tar numeric field"));
678 };
679 if first & 0x40 != 0 {
680 return Err(FormatError::ReaderUnsupported(
681 "negative tar base-256 numeric fields are not supported",
682 ));
683 }
684 let mut value = u128::from(first & 0x7f);
685 for byte in &field[1..] {
686 value = value
687 .checked_mul(256)
688 .and_then(|current| current.checked_add(u128::from(*byte)))
689 .ok_or(FormatError::InvalidArchive("tar base-256 field overflow"))?;
690 }
691 u64::try_from(value).map_err(|_| FormatError::ReaderUnsupported("tar numeric field too large"))
692}
693
694fn parse_tar_octal(field: &[u8]) -> Result<u64, FormatError> {
695 let field = field
696 .split(|byte| *byte == 0)
697 .next()
698 .unwrap_or(field)
699 .iter()
700 .copied()
701 .skip_while(|byte| *byte == b' ' || *byte == b'0')
702 .take_while(|byte| *byte != b' ')
703 .collect::<Vec<_>>();
704 if field.is_empty() {
705 return Ok(0);
706 }
707 let mut value = 0u64;
708 for byte in field {
709 if !(b'0'..=b'7').contains(&byte) {
710 return Err(FormatError::InvalidArchive("invalid tar octal field"));
711 }
712 value = value
713 .checked_mul(8)
714 .and_then(|current| current.checked_add(u64::from(byte - b'0')))
715 .ok_or(FormatError::InvalidArchive("tar octal field overflow"))?;
716 }
717 Ok(value)
718}
719
720fn parse_decimal_usize(bytes: &[u8]) -> Result<usize, FormatError> {
721 usize::try_from(parse_decimal_u64(bytes)?)
722 .map_err(|_| FormatError::InvalidArchive("decimal field overflow"))
723}
724
725fn parse_decimal_u64(bytes: &[u8]) -> Result<u64, FormatError> {
726 if bytes.is_empty() {
727 return Err(FormatError::InvalidArchive("malformed decimal field"));
728 }
729 let mut value = 0u64;
730 for byte in bytes {
731 if !byte.is_ascii_digit() {
732 return Err(FormatError::InvalidArchive("malformed decimal field"));
733 }
734 value = value
735 .checked_mul(10)
736 .and_then(|current| current.checked_add(u64::from(byte - b'0')))
737 .ok_or(FormatError::InvalidArchive("decimal field overflow"))?;
738 }
739 Ok(value)
740}
741
742fn parse_pax_mtime(bytes: &[u8]) -> Result<u64, FormatError> {
743 let whole = bytes.split(|byte| *byte == b'.').next().unwrap_or(bytes);
744 parse_decimal_u64(whole)
745}
746
747fn trimmed_metadata_payload(payload: &[u8]) -> Vec<u8> {
748 let mut end = payload.len();
749 while end > 0 && payload[end - 1] == 0 {
750 end -= 1;
751 }
752 payload[..end].to_vec()
753}
754
755fn nul_trimmed(bytes: &[u8]) -> &[u8] {
756 let end = bytes
757 .iter()
758 .position(|byte| *byte == 0)
759 .unwrap_or(bytes.len());
760 &bytes[..end]
761}
762
763fn padding_to_512_u64(len: u64) -> u64 {
764 let remainder = len % TAR_BLOCK_LEN as u64;
765 if remainder == 0 {
766 0
767 } else {
768 TAR_BLOCK_LEN as u64 - remainder
769 }
770}
771
772fn checked_input_add(lhs: u64, rhs: u64, field: &'static str) -> Result<u64, FormatError> {
773 lhs.checked_add(rhs)
774 .ok_or(FormatError::WriterUnsupported(field))
775}
776
777fn io_error_from_format(error: FormatError) -> io::Error {
778 io::Error::new(ErrorKind::InvalidData, error.to_string())
779}
780
781#[cfg(test)]
782mod tests {
783 use std::io;
784
785 use crate::crypto::MasterKey;
786 use crate::format::{FormatError, BLOCK_RECORD_FRAMING_LEN, MASTER_KEY_LEN, VOLUME_HEADER_LEN};
787 use crate::reader::{open_archive, open_archive_volumes};
788 use crate::root_auth::data_block_merkle_leaf_hash;
789 use crate::wire::{BlockRecord, CryptoHeader, VolumeHeader};
790 use crate::writer::{write_archive, RegularFile};
791
792 use super::*;
793
794 fn master_key() -> MasterKey {
795 MasterKey::from_raw_key(&[0x31; MASTER_KEY_LEN]).unwrap()
796 }
797
798 fn options() -> WriterOptions {
799 WriterOptions {
800 archive_uuid: Some([0x41; 16]),
801 session_id: Some([0x42; 16]),
802 closed_at_ns: 987_654_321,
803 stripe_width: 1,
804 volume_loss_tolerance: 0,
805 bit_rot_buffer_pct: 0,
806 index_root_fec_parity_shards: 0,
807 ..WriterOptions::default()
808 }
809 }
810
811 fn single_pass_equivalence_options() -> WriterOptions {
812 WriterOptions {
813 index_root_fec_data_shards: u16::MAX,
819 ..options()
820 }
821 }
822
823 fn multi_volume_options(stripe_width: u32) -> WriterOptions {
824 WriterOptions {
825 stripe_width,
826 volume_loss_tolerance: 0,
827 ..options()
828 }
829 }
830
831 fn tar_stream(entries: &[(&str, &[u8])]) -> Vec<u8> {
832 let mut out = Vec::new();
833 for (path, data) in entries {
834 out.extend_from_slice(&tar_header(path.as_bytes(), b'0', data.len() as u64));
835 out.extend_from_slice(data);
836 out.resize(out.len() + padding_to_512(data.len()), 0);
837 }
838 out.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
839 out
840 }
841
842 fn tar_header(path: &[u8], kind: u8, size: u64) -> [u8; TAR_BLOCK_LEN] {
843 let mut header = [0u8; TAR_BLOCK_LEN];
844 header[..path.len()].copy_from_slice(path);
845 write_octal(&mut header[100..108], 0o644);
846 write_octal(&mut header[108..116], 0);
847 write_octal(&mut header[116..124], 0);
848 write_octal(&mut header[124..136], size);
849 write_octal(&mut header[136..148], 0);
850 header[148..156].fill(b' ');
851 header[156] = kind;
852 header[257..263].copy_from_slice(b"ustar\0");
853 header[263..265].copy_from_slice(b"00");
854 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
855 write_checksum(&mut header[148..156], checksum);
856 header
857 }
858
859 fn write_octal(field: &mut [u8], value: u64) {
860 let digits = format!("{value:o}");
861 field.fill(0);
862 let start = field.len() - 1 - digits.len();
863 field[..start].fill(b'0');
864 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
865 }
866
867 fn write_checksum(field: &mut [u8], value: u64) {
868 let digits = format!("{value:06o}");
869 field[0..6].copy_from_slice(digits.as_bytes());
870 field[6] = 0;
871 field[7] = b' ';
872 }
873
874 fn write_base256(field: &mut [u8], value: u64) {
875 field.fill(0);
876 let mut value = value;
877 for byte in field.iter_mut().rev() {
878 *byte = (value & 0xff) as u8;
879 value >>= 8;
880 }
881 field[0] |= 0x80;
882 }
883
884 fn padding_to_512(len: usize) -> usize {
885 let remainder = len % TAR_BLOCK_LEN;
886 if remainder == 0 {
887 0
888 } else {
889 TAR_BLOCK_LEN - remainder
890 }
891 }
892
893 fn data_leaf_hash_sequence(bytes: &[u8]) -> Vec<(u64, [u8; 32])> {
894 let volume_header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
895 let crypto_start = volume_header.crypto_header_offset as usize;
896 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
897 let crypto_header = CryptoHeader::parse(
898 &bytes[crypto_start..crypto_end],
899 volume_header.crypto_header_length,
900 )
901 .unwrap();
902 let block_size = crypto_header.fixed.block_size as usize;
903 let record_len = block_size + BLOCK_RECORD_FRAMING_LEN;
904 let mut offset = crypto_end;
905 let mut hashes = Vec::new();
906 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
907 let record =
908 BlockRecord::parse(&bytes[offset..offset + record_len], block_size).unwrap();
909 if record.kind.is_data() {
910 hashes.push((
911 record.block_index,
912 data_block_merkle_leaf_hash(
913 record.block_index,
914 record.kind,
915 record.flags,
916 &record.payload,
917 ),
918 ));
919 }
920 offset += record_len;
921 }
922 hashes
923 }
924
925 struct TinyReadCursor {
926 data: Vec<u8>,
927 cursor: usize,
928 max_chunk: usize,
929 reads: usize,
930 }
931
932 impl TinyReadCursor {
933 fn new(data: Vec<u8>, max_chunk: usize) -> Self {
934 Self {
935 data,
936 cursor: 0,
937 max_chunk,
938 reads: 0,
939 }
940 }
941 }
942
943 impl Read for TinyReadCursor {
944 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
945 self.reads += 1;
946 if self.cursor >= self.data.len() {
947 return Ok(0);
948 }
949 let len = self
950 .max_chunk
951 .min(buf.len())
952 .min(self.data.len() - self.cursor);
953 buf[..len].copy_from_slice(&self.data[self.cursor..self.cursor + len]);
954 self.cursor += len;
955 Ok(len)
956 }
957 }
958
959 #[test]
960 fn tar_stdin_single_volume_round_trips_list_verify_and_extract() {
961 let input = tar_stream(&[
962 ("alpha.txt", b"alpha payload".as_slice()),
963 ("dir/beta.txt", b"beta payload".as_slice()),
964 ]);
965 let mut reader = TinyReadCursor::new(input, 17);
966
967 let archive = write_tar_stream_archive(&mut reader, &master_key(), options()).unwrap();
968
969 assert!(reader.reads > 10);
970 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
971 opened.verify().unwrap();
972 let listed = opened
973 .list_files()
974 .unwrap()
975 .into_iter()
976 .map(|entry| entry.path)
977 .collect::<Vec<_>>();
978 assert_eq!(listed, vec!["alpha.txt", "dir/beta.txt"]);
979 assert_eq!(
980 opened.extract_file("dir/beta.txt").unwrap(),
981 Some(b"beta payload".to_vec())
982 );
983 }
984
985 #[test]
986 fn sized_raw_stdin_round_trips_as_regular_tar_member_archive() {
987 let input = b"raw bytes from stdin";
988 let mut sink = MemoryArchiveSink::default();
989
990 let summary = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
991 input.as_slice(),
992 "raw/data.bin",
993 input.len() as u64,
994 &master_key(),
995 options(),
996 &KdfParams::Raw,
997 None,
998 None,
999 &mut sink,
1000 )
1001 .unwrap();
1002
1003 assert_eq!(summary.input_bytes, input.len() as u64);
1004 let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1005 opened.verify().unwrap();
1006 assert_eq!(
1007 opened.extract_file("raw/data.bin").unwrap(),
1008 Some(input.to_vec())
1009 );
1010 }
1011
1012 #[test]
1013 fn sized_raw_stdin_known_size_multi_volume_round_trips() {
1014 let input = (0..150_000)
1015 .map(|index| (index % 251) as u8)
1016 .collect::<Vec<_>>();
1017 let mut sink = MemoryArchiveSink::default();
1018
1019 let summary = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1020 input.as_slice(),
1021 "raw/data.bin",
1022 input.len() as u64,
1023 &master_key(),
1024 multi_volume_options(3),
1025 &KdfParams::Raw,
1026 None,
1027 None,
1028 &mut sink,
1029 )
1030 .unwrap();
1031
1032 assert_eq!(summary.archive.volume_count, 3);
1033 assert_eq!(sink.volumes.len(), 3);
1034 let refs = sink.volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
1035 let opened = open_archive_volumes(&refs, &master_key()).unwrap();
1036 opened.verify().unwrap();
1037 assert_eq!(
1038 opened.extract_file("raw/data.bin").unwrap(),
1039 Some(input.to_vec())
1040 );
1041 }
1042
1043 #[test]
1044 fn sized_raw_stdin_rejects_short_input() {
1045 let mut sink = MemoryArchiveSink::default();
1046
1047 let error = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1048 b"short".as_slice(),
1049 "raw/data.bin",
1050 6,
1051 &master_key(),
1052 options(),
1053 &KdfParams::Raw,
1054 None,
1055 None,
1056 &mut sink,
1057 )
1058 .unwrap_err();
1059
1060 assert!(matches!(error, ArchiveWriteError::Io(_)));
1061 }
1062
1063 #[test]
1064 fn sized_raw_stdin_rejects_trailing_input() {
1065 let mut sink = MemoryArchiveSink::default();
1066
1067 let error = write_sized_raw_member_archive_to_sink_with_kdf_and_root_auth(
1068 b"toolong".as_slice(),
1069 "raw/data.bin",
1070 3,
1071 &master_key(),
1072 options(),
1073 &KdfParams::Raw,
1074 None,
1075 None,
1076 &mut sink,
1077 )
1078 .unwrap_err();
1079
1080 assert!(matches!(
1081 error,
1082 ArchiveWriteError::Format(FormatError::InvalidArchive(
1083 "raw stdin exceeds declared --stdin-size"
1084 ))
1085 ));
1086 }
1087
1088 #[test]
1089 fn tar_stdin_sink_summary_reports_input_not_output_tar_size() {
1090 let input = tar_stream(&[("one.txt", b"one".as_slice())]);
1091 let mut sink = MemoryArchiveSink::default();
1092
1093 let summary =
1094 write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1095 .unwrap();
1096
1097 assert_eq!(summary.input_member_count, 1);
1098 assert_eq!(summary.input_tar_bytes, input.len() as u64);
1099 assert_eq!(summary.archive.volume_count, 1);
1100 assert_eq!(sink.volumes.len(), 1);
1101 open_archive(&sink.volumes[0], &master_key())
1102 .unwrap()
1103 .verify()
1104 .unwrap();
1105 }
1106
1107 #[test]
1108 fn tar_stdin_large_single_volume_round_trips() {
1109 let beta = (0..150_000)
1110 .map(|index| (index % 251) as u8)
1111 .collect::<Vec<_>>();
1112 let input = tar_stream(&[
1113 ("alpha.txt", b"alpha payload".as_slice()),
1114 ("dir/beta.bin", beta.as_slice()),
1115 ]);
1116
1117 let archive = write_tar_stream_archive(&input[..], &master_key(), options()).unwrap();
1118 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1119
1120 opened.verify().unwrap();
1121 assert_eq!(opened.extract_file("dir/beta.bin").unwrap(), Some(beta));
1122 }
1123
1124 #[test]
1125 fn tar_stdin_multi_volume_round_trips_list_verify_and_extract() {
1126 let beta = (0..150_000)
1127 .map(|index| (index % 251) as u8)
1128 .collect::<Vec<_>>();
1129 let input = tar_stream(&[
1130 ("alpha.txt", b"alpha payload".as_slice()),
1131 ("dir/beta.bin", beta.as_slice()),
1132 ]);
1133 let mut reader = TinyReadCursor::new(input, 31);
1134 let mut sink = MemoryArchiveSink::default();
1135
1136 let summary = write_tar_stream_archive_to_sink(
1137 &mut reader,
1138 &master_key(),
1139 multi_volume_options(4),
1140 &mut sink,
1141 )
1142 .unwrap();
1143
1144 assert!(reader.reads > 10);
1145 assert_eq!(summary.input_member_count, 2);
1146 assert_eq!(summary.archive.volume_count, 4);
1147 assert_eq!(sink.volumes.len(), 4);
1148 let refs = sink.volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
1149 let opened = open_archive_volumes(&refs, &master_key()).unwrap();
1150 assert_eq!(
1151 opened.extract_file("alpha.txt").unwrap(),
1152 Some(b"alpha payload".to_vec())
1153 );
1154 assert_eq!(
1155 opened.extract_file("dir/beta.bin").unwrap(),
1156 Some(beta.clone())
1157 );
1158 opened.verify().unwrap();
1159 let listed = opened
1160 .list_files()
1161 .unwrap()
1162 .into_iter()
1163 .map(|entry| entry.path)
1164 .collect::<Vec<_>>();
1165 assert_eq!(listed, vec!["alpha.txt", "dir/beta.bin"]);
1166 }
1167
1168 #[test]
1169 fn tar_stdin_emits_identical_bytes_to_file_list_create_with_streaming_metadata_class() {
1170 let input = tar_stream(&[
1171 ("alpha.txt", b"alpha payload".as_slice()),
1172 ("dir/beta.txt", b"beta payload".as_slice()),
1173 ]);
1174 let options = single_pass_equivalence_options();
1175
1176 let streaming = write_tar_stream_archive(&input[..], &master_key(), options).unwrap();
1177 let legacy = write_archive(
1178 &[
1179 RegularFile::new("alpha.txt", b"alpha payload"),
1180 RegularFile::new("dir/beta.txt", b"beta payload"),
1181 ],
1182 &master_key(),
1183 options,
1184 )
1185 .unwrap();
1186
1187 assert_eq!(streaming.bytes, legacy.bytes);
1188 assert_eq!(streaming.bootstrap_sidecar, legacy.bootstrap_sidecar);
1189 }
1190
1191 #[test]
1192 fn tar_stdin_data_leaf_hashes_match_file_list_writer_sequence() {
1193 let input = tar_stream(&[
1194 ("alpha.txt", b"alpha payload".as_slice()),
1195 ("dir/beta.txt", b"beta payload".as_slice()),
1196 ]);
1197 let options = single_pass_equivalence_options();
1198
1199 let streaming = write_tar_stream_archive(&input[..], &master_key(), options).unwrap();
1200 let legacy = write_archive(
1201 &[
1202 RegularFile::new("alpha.txt", b"alpha payload"),
1203 RegularFile::new("dir/beta.txt", b"beta payload"),
1204 ],
1205 &master_key(),
1206 options,
1207 )
1208 .unwrap();
1209
1210 assert_eq!(
1211 data_leaf_hash_sequence(&streaming.bytes),
1212 data_leaf_hash_sequence(&legacy.bytes)
1213 );
1214 }
1215
1216 #[test]
1217 fn tar_stdin_empty_stream_accepts_two_zero_eof_blocks() {
1218 let input = vec![0u8; TAR_BLOCK_LEN * 2];
1219 let mut sink = MemoryArchiveSink::default();
1220
1221 let summary =
1222 write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1223 .unwrap();
1224
1225 assert_eq!(summary.input_member_count, 0);
1226 assert_eq!(summary.input_tar_bytes, input.len() as u64);
1227 let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1228 opened.verify().unwrap();
1229 assert!(opened.list_files().unwrap().is_empty());
1230 }
1231
1232 #[test]
1233 fn tar_stdin_accepts_base256_numeric_size_fields() {
1234 let mut header = tar_header(b"large-format.txt", b'0', 0);
1235 write_base256(&mut header[124..136], 4);
1236 header[148..156].fill(b' ');
1237 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
1238 write_checksum(&mut header[148..156], checksum);
1239 let mut input = Vec::new();
1240 input.extend_from_slice(&header);
1241 input.extend_from_slice(b"data");
1242 input.resize(input.len() + padding_to_512(4), 0);
1243 input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1244
1245 let archive = write_tar_stream_archive(&input[..], &master_key(), options()).unwrap();
1246 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
1247
1248 assert_eq!(
1249 opened.extract_file("large-format.txt").unwrap(),
1250 Some(b"data".to_vec())
1251 );
1252 }
1253
1254 #[test]
1255 fn tar_stdin_rejects_metadata_header_without_following_member() {
1256 let payload = b"13 comment=x\n";
1257 let mut input = Vec::new();
1258 input.extend_from_slice(&tar_header(
1259 b"PaxHeaders/unused",
1260 b'x',
1261 payload.len() as u64,
1262 ));
1263 input.extend_from_slice(payload);
1264 input.resize(input.len() + padding_to_512(payload.len()), 0);
1265 input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1266
1267 let error = write_tar_stream_archive(&input[..], &master_key(), options()).unwrap_err();
1268
1269 assert_eq!(
1270 error,
1271 FormatError::InvalidArchive("tar stream ended after metadata header")
1272 );
1273 }
1274
1275 #[test]
1276 fn tar_stdin_skips_directory_entries_and_keeps_regular_children() {
1277 let mut input = Vec::new();
1278 input.extend_from_slice(&tar_header(b"dir/", b'5', 0));
1279 input.extend_from_slice(&tar_header(b"dir/file.txt", b'0', 4));
1280 input.extend_from_slice(b"data");
1281 input.resize(input.len() + padding_to_512(4), 0);
1282 input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1283 let mut sink = MemoryArchiveSink::default();
1284
1285 let summary =
1286 write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1287 .unwrap();
1288
1289 assert_eq!(summary.input_member_count, 1);
1290 let opened = open_archive(&sink.volumes[0], &master_key()).unwrap();
1291 assert_eq!(
1292 opened
1293 .list_files()
1294 .unwrap()
1295 .into_iter()
1296 .map(|entry| entry.path)
1297 .collect::<Vec<_>>(),
1298 vec!["dir/file.txt"]
1299 );
1300 }
1301
1302 #[test]
1303 fn tar_stdin_rejects_unsupported_symlink_entries() {
1304 let mut input = Vec::new();
1305 input.extend_from_slice(&tar_header(b"link", b'2', 0));
1306 input.extend_from_slice(&[0u8; TAR_BLOCK_LEN * 2]);
1307 let mut sink = MemoryArchiveSink::default();
1308
1309 let error =
1310 write_tar_stream_archive_to_sink(&input[..], &master_key(), options(), &mut sink)
1311 .unwrap_err();
1312
1313 match error {
1314 ArchiveWriteError::Format(error) => assert_eq!(
1315 error,
1316 FormatError::WriterUnsupported(
1317 "streaming tar stdin supports regular files and directory entries only"
1318 )
1319 ),
1320 ArchiveWriteError::Io(error) => panic!("unexpected I/O error: {error}"),
1321 }
1322 assert!(!sink.volumes.is_empty());
1323 }
1324
1325 #[test]
1326 fn tar_stdin_rejects_volume_loss_tolerance() {
1327 let mut bad = options();
1328 bad.volume_loss_tolerance = 1;
1329
1330 let error = write_tar_stream_archive(
1331 &tar_stream(&[("x", b"x".as_slice())])[..],
1332 &master_key(),
1333 bad,
1334 )
1335 .unwrap_err();
1336
1337 assert_eq!(
1338 error,
1339 FormatError::WriterUnsupported("streaming create cannot tolerate volume loss")
1340 );
1341 }
1342}