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