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