1const BLOCK_SIZE: usize = 512;
4const NAME_RANGE: core::ops::Range<usize> = 0..100;
5const SIZE_RANGE: core::ops::Range<usize> = 124..136;
6const CHECKSUM_RANGE: core::ops::Range<usize> = 148..156;
7const TYPE_OFFSET: usize = 156;
8const PREFIX_RANGE: core::ops::Range<usize> = 345..500;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11enum WriteState {
12 Ready,
13 File { remaining: u64, size: u64 },
14 Finished,
15}
16
17pub struct TarWriter {
19 state: WriteState,
20}
21
22impl TarWriter {
23 pub const fn new() -> Self {
24 Self {
25 state: WriteState::Ready,
26 }
27 }
28
29 pub fn begin_file<E>(
30 &mut self,
31 path: &[u8],
32 size: u64,
33 mode: u32,
34 mut output: impl FnMut(&[u8]) -> Result<(), E>,
35 ) -> Result<(), TarWriteError<E>> {
36 if self.state != WriteState::Ready {
37 return Err(TarWriteError::InvalidState);
38 }
39 if path.is_empty()
40 || path.len() > NAME_RANGE.len()
41 || path[0] == b'/'
42 || path.contains(&0)
43 || path
44 .split(|byte| *byte == b'/')
45 .any(|component| component == b"..")
46 {
47 return Err(TarWriteError::InvalidPath);
48 }
49
50 let mut header = [0u8; BLOCK_SIZE];
51 header[..path.len()].copy_from_slice(path);
52 write_octal(&mut header[100..108], mode as u64)
53 .map_err(|_| TarWriteError::ValueTooLarge)?;
54 write_octal(&mut header[SIZE_RANGE], size).map_err(|_| TarWriteError::ValueTooLarge)?;
55 for digits in [108..115, 116..123, 136..147] {
58 header[digits].fill(b'0');
59 }
60 header[CHECKSUM_RANGE].fill(b' ');
61 header[TYPE_OFFSET] = b'0';
62 header[257..263].copy_from_slice(b"ustar\0");
63 header[263..265].copy_from_slice(b"00");
64 let checksum = header.iter().map(|byte| *byte as u64).sum();
65 let checksum_field = &mut header[CHECKSUM_RANGE];
66 write_octal(&mut checksum_field[..7], checksum)
67 .map_err(|_| TarWriteError::ValueTooLarge)?;
68 checksum_field[7] = b' ';
69 output(&header).map_err(TarWriteError::Output)?;
70 self.state = WriteState::File {
71 remaining: size,
72 size,
73 };
74 Ok(())
75 }
76
77 pub fn write_file_data<E>(
78 &mut self,
79 bytes: &[u8],
80 mut output: impl FnMut(&[u8]) -> Result<(), E>,
81 ) -> Result<(), TarWriteError<E>> {
82 let WriteState::File { remaining, size } = self.state else {
83 return Err(TarWriteError::InvalidState);
84 };
85 if bytes.len() as u64 > remaining {
86 return Err(TarWriteError::TooMuchData);
87 }
88 output(bytes).map_err(TarWriteError::Output)?;
89 self.state = WriteState::File {
90 remaining: remaining - bytes.len() as u64,
91 size,
92 };
93 Ok(())
94 }
95
96 pub fn end_file<E>(
97 &mut self,
98 mut output: impl FnMut(&[u8]) -> Result<(), E>,
99 ) -> Result<(), TarWriteError<E>> {
100 let WriteState::File { remaining, size } = self.state else {
101 return Err(TarWriteError::InvalidState);
102 };
103 if remaining != 0 {
104 return Err(TarWriteError::SizeMismatch { remaining });
105 }
106 let padding = padding_for(size);
107 if padding != 0 {
108 output(&[0; BLOCK_SIZE][..padding]).map_err(TarWriteError::Output)?;
109 }
110 self.state = WriteState::Ready;
111 Ok(())
112 }
113
114 pub fn finish<E>(
115 &mut self,
116 mut output: impl FnMut(&[u8]) -> Result<(), E>,
117 ) -> Result<(), TarWriteError<E>> {
118 if self.state != WriteState::Ready {
119 return Err(TarWriteError::InvalidState);
120 }
121 output(&[0; BLOCK_SIZE * 2]).map_err(TarWriteError::Output)?;
122 self.state = WriteState::Finished;
123 Ok(())
124 }
125}
126
127impl Default for TarWriter {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
134pub enum TarWriteError<E> {
135 #[display("invalid tar writer state")]
136 InvalidState,
137 #[display("invalid tar member path")]
138 InvalidPath,
139 #[display("tar header value is too large")]
140 ValueTooLarge,
141 #[display("tar member received too much data")]
142 TooMuchData,
143 #[display("tar member is missing {remaining} bytes")]
144 SizeMismatch { remaining: u64 },
145 #[display("tar writer output failed: {_0}")]
146 Output(E),
147}
148
149fn write_octal(field: &mut [u8], mut value: u64) -> Result<(), ()> {
150 field.fill(b'0');
151 let digits = field.len().checked_sub(1).ok_or(())?;
152 field[digits] = 0;
153 for index in (0..digits).rev() {
154 field[index] = b'0' + (value & 7) as u8;
155 value >>= 3;
156 }
157 if value != 0 {
158 return Err(());
159 }
160 Ok(())
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
165pub enum ExtractError<E> {
166 #[display("invalid tar header checksum")]
168 InvalidChecksum,
169 #[display("invalid tar entry size")]
171 InvalidSize,
172 #[display("invalid tar end marker")]
174 InvalidEndMarker,
175 #[display("tar output failed: {_0}")]
177 Output(E),
178}
179
180#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
182pub enum FinishError {
183 #[display("unexpected end of tar stream")]
185 UnexpectedEof,
186 #[display("tar entry not found")]
188 NotFound,
189}
190
191pub struct EntryExtractor<'target> {
201 target: &'target [u8],
202 header: [u8; BLOCK_SIZE],
203 header_len: usize,
204 entry_remaining: u64,
205 padding_remaining: usize,
206 selected: bool,
207 found: bool,
208 zero_blocks: u8,
209}
210
211impl<'target> EntryExtractor<'target> {
212 pub const fn new(target: &'target [u8]) -> Self {
214 Self {
215 target,
216 header: [0; BLOCK_SIZE],
217 header_len: 0,
218 entry_remaining: 0,
219 padding_remaining: 0,
220 selected: false,
221 found: false,
222 zero_blocks: 0,
223 }
224 }
225
226 pub fn push<E>(
232 &mut self,
233 mut input: &[u8],
234 mut output: impl FnMut(&[u8]) -> Result<(), E>,
235 ) -> Result<(), ExtractError<E>> {
236 while !input.is_empty() {
237 if self.zero_blocks == 2 {
238 return Ok(());
239 }
240
241 if self.entry_remaining != 0 {
242 let length = input.len().min(u64_to_usize(self.entry_remaining));
243 let bytes = &input[..length];
244 if self.selected {
245 output(bytes).map_err(ExtractError::Output)?;
246 }
247 self.entry_remaining -= length as u64;
248 input = &input[length..];
249 continue;
250 }
251
252 if self.padding_remaining != 0 {
253 let length = input.len().min(self.padding_remaining);
254 self.padding_remaining -= length;
255 input = &input[length..];
256 continue;
257 }
258
259 let length = input.len().min(BLOCK_SIZE - self.header_len);
260 self.header[self.header_len..self.header_len + length]
261 .copy_from_slice(&input[..length]);
262 self.header_len += length;
263 input = &input[length..];
264
265 if self.header_len == BLOCK_SIZE {
266 self.consume_header()?;
267 self.header_len = 0;
268 }
269 }
270
271 Ok(())
272 }
273
274 pub const fn found(&self) -> bool {
276 self.found
277 }
278
279 pub fn finish(&self) -> Result<(), FinishError> {
281 if self.zero_blocks != 2
282 || self.header_len != 0
283 || self.entry_remaining != 0
284 || self.padding_remaining != 0
285 {
286 return Err(FinishError::UnexpectedEof);
287 }
288 if !self.found {
289 return Err(FinishError::NotFound);
290 }
291 Ok(())
292 }
293
294 fn consume_header<E>(&mut self) -> Result<(), ExtractError<E>> {
295 if self.header.iter().all(|byte| *byte == 0) {
296 self.zero_blocks += 1;
297 return Ok(());
298 }
299 if self.zero_blocks != 0 {
300 return Err(ExtractError::InvalidEndMarker);
301 }
302
303 verify_checksum(&self.header)?;
304 let size = parse_number(&self.header[SIZE_RANGE]).ok_or(ExtractError::InvalidSize)?;
305 let is_regular = matches!(self.header[TYPE_OFFSET], 0 | b'0');
306 self.selected = !self.found && is_regular && path_matches(&self.header, self.target);
307 self.found |= self.selected;
308 self.entry_remaining = size;
309 self.padding_remaining = padding_for(size);
310 Ok(())
311 }
312}
313
314fn verify_checksum<E>(header: &[u8; BLOCK_SIZE]) -> Result<(), ExtractError<E>> {
315 let expected = parse_octal(&header[CHECKSUM_RANGE]).ok_or(ExtractError::InvalidChecksum)?;
316 let actual = header
317 .iter()
318 .enumerate()
319 .map(|(index, byte)| {
320 if CHECKSUM_RANGE.contains(&index) {
321 u64::from(b' ')
322 } else {
323 u64::from(*byte)
324 }
325 })
326 .sum::<u64>();
327 if actual == expected {
328 Ok(())
329 } else {
330 Err(ExtractError::InvalidChecksum)
331 }
332}
333
334fn parse_number(field: &[u8]) -> Option<u64> {
335 if field.first().copied()? & 0x80 == 0 {
336 return parse_octal(field);
337 }
338
339 let mut value = u64::from(field[0] & 0x7f);
340 for byte in &field[1..] {
341 value = value.checked_mul(256)?.checked_add(u64::from(*byte))?;
342 }
343 Some(value)
344}
345
346fn parse_octal(field: &[u8]) -> Option<u64> {
347 let mut value = 0u64;
348 let mut saw_digit = false;
349 let mut saw_terminator = false;
350
351 for byte in field {
352 match *byte {
353 b'0'..=b'7' if !saw_terminator => {
354 saw_digit = true;
355 value = value.checked_mul(8)?.checked_add(u64::from(*byte - b'0'))?;
356 }
357 b' ' if !saw_digit => {}
358 0 | b' ' if saw_digit => saw_terminator = true,
359 _ => return None,
360 }
361 }
362
363 saw_digit.then_some(value)
364}
365
366fn path_matches(header: &[u8; BLOCK_SIZE], target: &[u8]) -> bool {
367 let name = nul_terminated(&header[NAME_RANGE]);
368 let prefix = nul_terminated(&header[PREFIX_RANGE]);
369 if prefix.is_empty() {
370 return target == name;
371 }
372
373 target.len() == prefix.len() + 1 + name.len()
374 && target.starts_with(prefix)
375 && target[prefix.len()] == b'/'
376 && &target[prefix.len() + 1..] == name
377}
378
379fn nul_terminated(field: &[u8]) -> &[u8] {
380 let length = field
381 .iter()
382 .position(|byte| *byte == 0)
383 .unwrap_or(field.len());
384 &field[..length]
385}
386
387fn padding_for(size: u64) -> usize {
388 ((BLOCK_SIZE as u64 - size % BLOCK_SIZE as u64) % BLOCK_SIZE as u64) as usize
389}
390
391fn u64_to_usize(value: u64) -> usize {
392 usize::try_from(value).unwrap_or(usize::MAX)
393}
394
395pub struct ArchiveBuffers<'a> {
397 pub path: &'a mut [u8],
399 pub link: &'a mut [u8],
401 pub pax: &'a mut [u8],
403}
404
405#[derive(Clone, Copy, Debug, Eq, PartialEq)]
407pub enum EntryKind {
408 Regular,
409 HardLink,
410 SymbolicLink,
411 CharacterDevice,
412 BlockDevice,
413 Directory,
414 Fifo,
415 Contiguous,
416 Other(u8),
417}
418
419#[derive(Clone, Copy)]
421pub struct Entry<'a> {
422 pub path: &'a [u8],
423 pub kind: EntryKind,
424 pub size: u64,
425 pub mode: u64,
426 pub uid: u64,
427 pub gid: u64,
428 pub mtime: u64,
429 pub link_target: Option<&'a [u8]>,
430 pub device_major: Option<u64>,
431 pub device_minor: Option<u64>,
432 pax: &'a [u8],
433}
434
435impl<'a> Entry<'a> {
436 pub fn pax_records(&self) -> PaxRecords<'a> {
438 PaxRecords::new(self.pax)
439 }
440}
441
442#[derive(Clone, Copy, Debug, Eq, PartialEq)]
444pub struct PaxRecord<'a> {
445 pub key: &'a [u8],
446 pub value: &'a [u8],
447}
448
449pub struct PaxRecords<'a> {
451 bytes: &'a [u8],
452}
453
454impl<'a> PaxRecords<'a> {
455 const fn new(bytes: &'a [u8]) -> Self {
456 Self { bytes }
457 }
458}
459
460impl<'a> Iterator for PaxRecords<'a> {
461 type Item = Result<PaxRecord<'a>, PaxError>;
462
463 fn next(&mut self) -> Option<Self::Item> {
464 if self.bytes.is_empty() {
465 return None;
466 }
467 let result = parse_pax_record(self.bytes);
468 self.bytes = match &result {
469 Ok((_, length)) => &self.bytes[*length..],
470 Err(_) => &[],
471 };
472 Some(result.map(|(record, _)| record))
473 }
474}
475
476pub trait LayerEventSink {
478 type Error;
479
480 fn begin_entry(&mut self, entry: Entry<'_>) -> Result<(), Self::Error>;
481 fn entry_data(&mut self, bytes: &[u8]) -> Result<(), Self::Error>;
482 fn end_entry(&mut self) -> Result<(), Self::Error>;
483 fn whiteout(&mut self, path: &[u8]) -> Result<(), Self::Error>;
484 fn opaque_directory(&mut self, path: &[u8]) -> Result<(), Self::Error>;
485}
486
487pub trait TransactionalLayerSink: LayerEventSink {
489 fn begin_layer(&mut self) -> Result<(), Self::Error>;
490 fn commit_layer(&mut self) -> Result<(), Self::Error>;
491 fn abort_layer(&mut self);
492}
493
494#[derive(Clone, Copy, Debug, Eq, PartialEq)]
495enum ArchiveEntry {
496 None,
497 Normal,
498 LongPath,
499 LongLink,
500 Pax,
501}
502
503pub struct Archive<'a> {
505 header: [u8; BLOCK_SIZE],
506 header_len: usize,
507 entry_remaining: u64,
508 padding_remaining: usize,
509 current: ArchiveEntry,
510 entry_open: bool,
511 zero_blocks: u8,
512 path: &'a mut [u8],
513 path_len: usize,
514 link: &'a mut [u8],
515 link_len: usize,
516 pax: &'a mut [u8],
517 pax_len: usize,
518 pending_long_path: bool,
519 pending_long_link: bool,
520 pending_pax: bool,
521}
522
523impl<'a> Archive<'a> {
524 pub fn new(buffers: ArchiveBuffers<'a>) -> Self {
525 Self {
526 header: [0; BLOCK_SIZE],
527 header_len: 0,
528 entry_remaining: 0,
529 padding_remaining: 0,
530 current: ArchiveEntry::None,
531 entry_open: false,
532 zero_blocks: 0,
533 path: buffers.path,
534 path_len: 0,
535 link: buffers.link,
536 link_len: 0,
537 pax: buffers.pax,
538 pax_len: 0,
539 pending_long_path: false,
540 pending_long_link: false,
541 pending_pax: false,
542 }
543 }
544
545 pub fn push<S: LayerEventSink>(
546 &mut self,
547 mut input: &[u8],
548 sink: &mut S,
549 ) -> Result<(), ArchiveError<S::Error>> {
550 while !input.is_empty() {
551 if self.zero_blocks == 2 {
552 return Ok(());
553 }
554 if self.entry_remaining != 0 {
555 let length = input.len().min(u64_to_usize(self.entry_remaining));
556 let bytes = &input[..length];
557 match self.current {
558 ArchiveEntry::Normal => sink.entry_data(bytes).map_err(ArchiveError::Sink)?,
559 ArchiveEntry::LongPath => {
560 append_scratch(self.path, &mut self.path_len, bytes, Scratch::Path)?
561 }
562 ArchiveEntry::LongLink => {
563 append_scratch(self.link, &mut self.link_len, bytes, Scratch::Link)?
564 }
565 ArchiveEntry::Pax => {
566 append_scratch(self.pax, &mut self.pax_len, bytes, Scratch::Pax)?
567 }
568 ArchiveEntry::None => return Err(ArchiveError::InvalidState),
569 }
570 self.entry_remaining -= length as u64;
571 input = &input[length..];
572 if self.entry_remaining == 0 {
573 self.finish_content(sink)?;
574 }
575 continue;
576 }
577 if self.padding_remaining != 0 {
578 let length = input.len().min(self.padding_remaining);
579 self.padding_remaining -= length;
580 input = &input[length..];
581 if self.padding_remaining == 0 {
582 self.current = ArchiveEntry::None;
583 }
584 continue;
585 }
586
587 let length = input.len().min(BLOCK_SIZE - self.header_len);
588 self.header[self.header_len..self.header_len + length]
589 .copy_from_slice(&input[..length]);
590 self.header_len += length;
591 input = &input[length..];
592 if self.header_len == BLOCK_SIZE {
593 self.consume_header(sink)?;
594 self.header_len = 0;
595 }
596 }
597 Ok(())
598 }
599
600 pub fn finish(&self) -> Result<(), ArchiveFinishError> {
601 if self.zero_blocks != 2
602 || self.header_len != 0
603 || self.entry_remaining != 0
604 || self.padding_remaining != 0
605 || self.entry_open
606 || self.current != ArchiveEntry::None
607 {
608 return Err(ArchiveFinishError::UnexpectedEof);
609 }
610 if self.pending_long_path || self.pending_long_link || self.pending_pax {
611 return Err(ArchiveFinishError::DanglingExtension);
612 }
613 Ok(())
614 }
615
616 fn consume_header<S: LayerEventSink>(
617 &mut self,
618 sink: &mut S,
619 ) -> Result<(), ArchiveError<S::Error>> {
620 if self.header.iter().all(|byte| *byte == 0) {
621 if self.current != ArchiveEntry::None {
622 return Err(ArchiveError::InvalidState);
623 }
624 self.zero_blocks += 1;
625 return Ok(());
626 }
627 if self.zero_blocks != 0 {
628 return Err(ArchiveError::InvalidEndMarker);
629 }
630 verify_checksum::<()>(&self.header).map_err(|_| ArchiveError::InvalidChecksum)?;
631
632 let mut size = parse_number(&self.header[SIZE_RANGE]).ok_or(ArchiveError::InvalidSize)?;
633 let type_flag = self.header[TYPE_OFFSET];
634 if self.start_extension(type_flag, size, sink)? {
635 return Ok(());
636 }
637
638 let pax = if self.pending_pax {
639 &self.pax[..self.pax_len]
640 } else {
641 &[]
642 };
643 let [pax_size, pax_path, pax_link] = pax_overrides(pax)?;
644 if let Some(value) = pax_size {
645 size = decimal(value).ok_or(ArchiveError::InvalidSize)?;
646 }
647
648 if let Some(value) = pax_path {
649 self.path_len = copy_scratch(self.path, value, Scratch::Path)?;
650 } else if !self.pending_long_path {
651 self.path_len = header_path(&self.header, self.path)?;
652 }
653 trim_nul(self.path, &mut self.path_len);
654 self.path_len = normalize_path(self.path, self.path_len)?;
655
656 if let Some(value) = pax_link {
657 self.link_len = copy_scratch(self.link, value, Scratch::Link)?;
658 } else if !self.pending_long_link {
659 self.link_len = copy_scratch(
660 self.link,
661 nul_terminated(&self.header[157..257]),
662 Scratch::Link,
663 )?;
664 }
665 trim_nul(self.link, &mut self.link_len);
666
667 let kind = entry_kind(type_flag);
668 match kind {
669 EntryKind::HardLink if self.link_len != 0 => {
670 self.link_len = normalize_path(self.link, self.link_len)?;
671 }
672 EntryKind::SymbolicLink if self.link_len != 0 => {
673 validate_symbolic_link(&self.path[..self.path_len], &self.link[..self.link_len])?;
674 }
675 _ => {}
676 }
677
678 self.pending_long_path = false;
679 self.pending_long_link = false;
680 self.current = ArchiveEntry::Normal;
681 self.entry_remaining = size;
682 self.padding_remaining = padding_for(size);
683
684 if let Some(whiteout) = whiteout(self.path, &mut self.path_len) {
685 if kind != EntryKind::Regular || size != 0 {
686 return Err(ArchiveError::InvalidWhiteout);
687 }
688 match whiteout {
689 Whiteout::Remove => sink
690 .whiteout(&self.path[..self.path_len])
691 .map_err(ArchiveError::Sink)?,
692 Whiteout::Opaque => sink
693 .opaque_directory(&self.path[..self.path_len])
694 .map_err(ArchiveError::Sink)?,
695 }
696 self.pending_pax = false;
697 self.pax_len = 0;
698 self.current = ArchiveEntry::None;
699 return Ok(());
700 }
701
702 let number = |range| parse_number(&self.header[range]).ok_or(ArchiveError::InvalidMetadata);
703 let (device_major, device_minor) = match kind {
704 EntryKind::CharacterDevice | EntryKind::BlockDevice => {
705 (Some(number(329..337)?), Some(number(337..345)?))
706 }
707 _ => (None, None),
708 };
709 let link_target = matches!(kind, EntryKind::HardLink | EntryKind::SymbolicLink)
710 .then_some(&self.link[..self.link_len]);
711 sink.begin_entry(Entry {
712 path: &self.path[..self.path_len],
713 kind,
714 size,
715 mode: number(100..108)?,
716 uid: number(108..116)?,
717 gid: number(116..124)?,
718 mtime: number(136..148)?,
719 link_target,
720 device_major,
721 device_minor,
722 pax,
723 })
724 .map_err(ArchiveError::Sink)?;
725 self.entry_open = true;
726 self.pending_pax = false;
727 self.pax_len = 0;
728 if size == 0 {
729 self.finish_content(sink)?;
730 }
731 Ok(())
732 }
733
734 fn start_extension<S: LayerEventSink>(
735 &mut self,
736 type_flag: u8,
737 size: u64,
738 sink: &mut S,
739 ) -> Result<bool, ArchiveError<S::Error>> {
740 self.current = match type_flag {
741 b'L' => {
742 self.path_len = 0;
743 ArchiveEntry::LongPath
744 }
745 b'K' => {
746 self.link_len = 0;
747 ArchiveEntry::LongLink
748 }
749 b'x' => {
750 self.pax_len = 0;
751 ArchiveEntry::Pax
752 }
753 b'g' => return Err(ArchiveError::UnsupportedGlobalPax),
754 _ => return Ok(false),
755 };
756 self.entry_remaining = size;
757 self.padding_remaining = padding_for(size);
758 if size == 0 {
759 self.finish_content(sink)?;
760 }
761 Ok(true)
762 }
763
764 fn finish_content<S: LayerEventSink>(
765 &mut self,
766 sink: &mut S,
767 ) -> Result<(), ArchiveError<S::Error>> {
768 match self.current {
769 ArchiveEntry::Normal => {
770 if self.entry_open {
771 sink.end_entry().map_err(ArchiveError::Sink)?;
772 self.entry_open = false;
773 }
774 }
775 ArchiveEntry::LongPath => {
776 trim_nul(self.path, &mut self.path_len);
777 self.pending_long_path = true;
778 }
779 ArchiveEntry::LongLink => {
780 trim_nul(self.link, &mut self.link_len);
781 self.pending_long_link = true;
782 }
783 ArchiveEntry::Pax => {
784 validate_pax(&self.pax[..self.pax_len])?;
785 self.pending_pax = true;
786 }
787 ArchiveEntry::None => return Err(ArchiveError::InvalidState),
788 }
789 if self.padding_remaining == 0 {
790 self.current = ArchiveEntry::None;
791 }
792 Ok(())
793 }
794}
795
796#[derive(Clone, Copy, Debug, Eq, PartialEq)]
797pub enum Scratch {
798 Path,
799 Link,
800 Pax,
801}
802
803#[derive(Debug, Eq, PartialEq, derive_more::Display, derive_more::From)]
804pub enum ArchiveError<E> {
805 #[display("invalid tar header checksum")]
806 InvalidChecksum,
807 #[display("invalid tar entry size")]
808 InvalidSize,
809 #[display("invalid tar entry metadata")]
810 InvalidMetadata,
811 #[display("invalid tar end marker")]
812 InvalidEndMarker,
813 #[display("unsafe or invalid tar path")]
814 InvalidPath,
815 #[display("unsafe tar link target")]
816 InvalidLink,
817 #[display("invalid OCI whiteout entry")]
818 InvalidWhiteout,
819 #[display("invalid PAX header: {_0}")]
820 #[from]
821 InvalidPax(PaxError),
822 #[display("global PAX headers are not supported")]
823 UnsupportedGlobalPax,
824 #[display("{_0:?} buffer is too small")]
825 BufferTooSmall(Scratch),
826 #[display("invalid tar parser state")]
827 InvalidState,
828 #[display("layer sink failed: {_0}")]
829 Sink(E),
830}
831
832#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
833pub enum ArchiveFinishError {
834 #[display("unexpected end of tar archive")]
835 UnexpectedEof,
836 #[display("tar archive ended after an extension header")]
837 DanglingExtension,
838}
839
840#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
841pub enum PaxError {
842 #[display("invalid record length")]
843 InvalidLength,
844 #[display("invalid key/value record")]
845 InvalidRecord,
846}
847
848#[derive(Clone, Copy)]
849enum Whiteout {
850 Remove,
851 Opaque,
852}
853
854fn entry_kind(type_flag: u8) -> EntryKind {
855 match type_flag {
856 0 | b'0' => EntryKind::Regular,
857 b'1' => EntryKind::HardLink,
858 b'2' => EntryKind::SymbolicLink,
859 b'3' => EntryKind::CharacterDevice,
860 b'4' => EntryKind::BlockDevice,
861 b'5' => EntryKind::Directory,
862 b'6' => EntryKind::Fifo,
863 b'7' => EntryKind::Contiguous,
864 other => EntryKind::Other(other),
865 }
866}
867
868fn header_path<E>(
869 header: &[u8; BLOCK_SIZE],
870 destination: &mut [u8],
871) -> Result<usize, ArchiveError<E>> {
872 let name = nul_terminated(&header[NAME_RANGE]);
873 let prefix = nul_terminated(&header[PREFIX_RANGE]);
874 let needed = name.len() + usize::from(!prefix.is_empty()) + prefix.len();
875 if needed > destination.len() {
876 return Err(ArchiveError::BufferTooSmall(Scratch::Path));
877 }
878 let mut length = 0;
879 if !prefix.is_empty() {
880 destination[..prefix.len()].copy_from_slice(prefix);
881 length += prefix.len();
882 destination[length] = b'/';
883 length += 1;
884 }
885 destination[length..length + name.len()].copy_from_slice(name);
886 Ok(length + name.len())
887}
888
889fn append_scratch<E>(
890 destination: &mut [u8],
891 length: &mut usize,
892 bytes: &[u8],
893 kind: Scratch,
894) -> Result<(), ArchiveError<E>> {
895 crate::buffer::append(destination, length, bytes)
896 .map_err(|_| ArchiveError::BufferTooSmall(kind))
897}
898
899fn copy_scratch<E>(
900 destination: &mut [u8],
901 bytes: &[u8],
902 kind: Scratch,
903) -> Result<usize, ArchiveError<E>> {
904 if bytes.len() > destination.len() {
905 return Err(ArchiveError::BufferTooSmall(kind));
906 }
907 destination[..bytes.len()].copy_from_slice(bytes);
908 Ok(bytes.len())
909}
910
911fn trim_nul(bytes: &[u8], length: &mut usize) {
912 while *length != 0 && bytes[*length - 1] == 0 {
913 *length -= 1;
914 }
915}
916
917fn normalize_path<E>(bytes: &mut [u8], length: usize) -> Result<usize, ArchiveError<E>> {
918 if length == 0 || bytes[0] == b'/' || bytes[..length].contains(&0) {
919 return Err(ArchiveError::InvalidPath);
920 }
921 let mut read = 0;
922 let mut write = 0;
923 while read < length {
924 let start = read;
925 let end = bytes[start..length]
926 .iter()
927 .position(|byte| *byte == b'/')
928 .map_or(length, |offset| start + offset);
929 read = end + 1;
930 match &bytes[start..end] {
931 b"" | b"." => continue,
932 b".." => return Err(ArchiveError::InvalidPath),
933 _ => {}
934 }
935 if write != 0 {
936 bytes[write] = b'/';
937 write += 1;
938 }
939 bytes.copy_within(start..end, write);
940 write += end - start;
941 }
942 if write == 0 {
943 bytes[0] = b'.';
944 write = 1;
945 }
946 Ok(write)
947}
948
949fn validate_symbolic_link<E>(path: &[u8], target: &[u8]) -> Result<(), ArchiveError<E>> {
950 if target.is_empty() || target[0] == b'/' || target.contains(&0) {
951 return Err(ArchiveError::InvalidLink);
952 }
953 let mut depth = path.split(|byte| *byte == b'/').count().saturating_sub(1);
954 for component in target.split(|byte| *byte == b'/') {
955 match component {
956 b"" | b"." => {}
957 b".." => {
958 depth = depth.checked_sub(1).ok_or(ArchiveError::InvalidLink)?;
959 }
960 _ => depth += 1,
961 }
962 }
963 Ok(())
964}
965
966fn whiteout(path: &mut [u8], length: &mut usize) -> Option<Whiteout> {
967 let basename = path[..*length]
968 .iter()
969 .rposition(|byte| *byte == b'/')
970 .map_or(0, |position| position + 1);
971 let name = &path[basename..*length];
972 if name == b".wh..wh..opq" {
973 if basename == 0 {
974 path[0] = b'.';
975 *length = 1;
976 } else {
977 *length = basename - 1;
978 }
979 return Some(Whiteout::Opaque);
980 }
981 let target = name.strip_prefix(b".wh.")?;
982 if target.is_empty() {
983 return None;
984 }
985 path.copy_within(basename + 4..*length, basename);
986 *length -= 4;
987 Some(Whiteout::Remove)
988}
989
990fn validate_pax<E>(bytes: &[u8]) -> Result<(), ArchiveError<E>> {
991 for record in PaxRecords::new(bytes) {
992 record?;
993 }
994 Ok(())
995}
996
997fn pax_overrides<E>(bytes: &[u8]) -> Result<[Option<&[u8]>; 3], ArchiveError<E>> {
998 let mut values = [None; 3];
999 for record in PaxRecords::new(bytes) {
1000 let record = record?;
1001 let index = match record.key {
1002 b"size" => 0,
1003 b"path" => 1,
1004 b"linkpath" => 2,
1005 _ => continue,
1006 };
1007 if values[index].replace(record.value).is_some() {
1008 return Err(ArchiveError::InvalidPax(PaxError::InvalidRecord));
1009 }
1010 }
1011 Ok(values)
1012}
1013
1014fn parse_pax_record(bytes: &[u8]) -> Result<(PaxRecord<'_>, usize), PaxError> {
1015 let space = bytes
1016 .iter()
1017 .position(|byte| *byte == b' ')
1018 .ok_or(PaxError::InvalidLength)?;
1019 let length = decimal(&bytes[..space]).ok_or(PaxError::InvalidLength)?;
1020 let length = usize::try_from(length).map_err(|_| PaxError::InvalidLength)?;
1021 let end = length;
1022 let record = bytes.get(space + 1..end).ok_or(PaxError::InvalidLength)?;
1023 let record = record.strip_suffix(b"\n").ok_or(PaxError::InvalidRecord)?;
1024 let equals = record
1025 .iter()
1026 .position(|byte| *byte == b'=')
1027 .ok_or(PaxError::InvalidRecord)?;
1028 if equals == 0 {
1029 return Err(PaxError::InvalidRecord);
1030 }
1031 Ok((
1032 PaxRecord {
1033 key: &record[..equals],
1034 value: &record[equals + 1..],
1035 },
1036 end,
1037 ))
1038}
1039
1040fn decimal(bytes: &[u8]) -> Option<u64> {
1041 if !bytes.iter().all(u8::is_ascii_digit) {
1042 return None;
1043 }
1044 core::str::from_utf8(bytes).ok()?.parse().ok()
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use super::{
1050 header_path, normalize_path, pax_overrides, validate_pax, validate_symbolic_link, Archive,
1051 ArchiveBuffers, ArchiveError, Entry, EntryExtractor, EntryKind, ExtractError, FinishError,
1052 LayerEventSink, PaxError, TarWriteError, TarWriter, BLOCK_SIZE,
1053 };
1054 use std::{format, string::ToString, vec::Vec};
1055
1056 type OwnedPaxRecord = (Vec<u8>, Vec<u8>);
1057 type OwnedEntry = (Vec<u8>, EntryKind, Vec<OwnedPaxRecord>);
1058
1059 #[derive(Default)]
1060 struct Events {
1061 entries: Vec<OwnedEntry>,
1062 contents: Vec<u8>,
1063 whiteouts: Vec<Vec<u8>>,
1064 opaque: Vec<Vec<u8>>,
1065 }
1066
1067 #[test]
1068 fn writes_deterministic_ustar_files() {
1069 let mut bytes = Vec::new();
1070 let mut writer = TarWriter::new();
1071 writer
1072 .begin_file(b"blobs/sha256/abc", 5, 0o644, |chunk| {
1073 bytes.extend_from_slice(chunk);
1074 Ok::<_, ()>(())
1075 })
1076 .unwrap();
1077 writer
1078 .write_file_data(b"hello", |chunk| {
1079 bytes.extend_from_slice(chunk);
1080 Ok::<_, ()>(())
1081 })
1082 .unwrap();
1083 writer
1084 .end_file(|chunk| {
1085 bytes.extend_from_slice(chunk);
1086 Ok::<_, ()>(())
1087 })
1088 .unwrap();
1089 writer
1090 .finish(|chunk| {
1091 bytes.extend_from_slice(chunk);
1092 Ok::<_, ()>(())
1093 })
1094 .unwrap();
1095
1096 assert_eq!(&bytes[..18], b"blobs/sha256/abc\0\0");
1097 assert_eq!(&bytes[BLOCK_SIZE..BLOCK_SIZE + 5], b"hello");
1098 assert_eq!(bytes.len(), BLOCK_SIZE * 4);
1099
1100 let mut invalid = TarWriter::new();
1101 assert_eq!(
1102 invalid.begin_file(b"../escape", 0, 0o644, |_| Ok::<_, ()>(())),
1103 Err(TarWriteError::InvalidPath)
1104 );
1105 }
1106
1107 impl LayerEventSink for Events {
1108 type Error = ();
1109
1110 fn begin_entry(&mut self, entry: Entry<'_>) -> Result<(), Self::Error> {
1111 let pax = entry
1112 .pax_records()
1113 .map(|record| {
1114 let record = record.unwrap();
1115 (record.key.to_vec(), record.value.to_vec())
1116 })
1117 .collect();
1118 self.entries.push((entry.path.to_vec(), entry.kind, pax));
1119 Ok(())
1120 }
1121
1122 fn entry_data(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
1123 self.contents.extend_from_slice(bytes);
1124 Ok(())
1125 }
1126
1127 fn end_entry(&mut self) -> Result<(), Self::Error> {
1128 Ok(())
1129 }
1130
1131 fn whiteout(&mut self, path: &[u8]) -> Result<(), Self::Error> {
1132 self.whiteouts.push(path.to_vec());
1133 Ok(())
1134 }
1135
1136 fn opaque_directory(&mut self, path: &[u8]) -> Result<(), Self::Error> {
1137 self.opaque.push(path.to_vec());
1138 Ok(())
1139 }
1140 }
1141
1142 #[test]
1143 fn streams_layer_events_and_whiteouts_at_every_fragment_size() {
1144 let mut bytes = Vec::new();
1145 append_entry(&mut bytes, b"./etc//config", b"value", b'0', b"");
1146 append_entry(&mut bytes, b"etc/.wh.deleted", b"", b'0', b"");
1147 append_entry(&mut bytes, b"var/lib/.wh..wh..opq", b"", b'0', b"");
1148 finish_archive(&mut bytes);
1149
1150 for fragment in 1..=BLOCK_SIZE + 1 {
1151 let mut path = [0; 256];
1152 let mut link = [0; 256];
1153 let mut pax = [0; 512];
1154 let mut archive = Archive::new(ArchiveBuffers {
1155 path: &mut path,
1156 link: &mut link,
1157 pax: &mut pax,
1158 });
1159 let mut events = Events::default();
1160 for chunk in bytes.chunks(fragment) {
1161 archive.push(chunk, &mut events).unwrap();
1162 }
1163 archive.finish().unwrap();
1164 assert_eq!(events.entries[0].0, b"etc/config");
1165 assert_eq!(events.contents, b"value");
1166 assert_eq!(events.whiteouts, [b"etc/deleted".to_vec()]);
1167 assert_eq!(events.opaque, [b"var/lib".to_vec()]);
1168 }
1169 }
1170
1171 #[test]
1172 fn applies_pax_and_gnu_long_path_overrides_without_allocating() {
1173 let mut bytes = Vec::new();
1174 let record = pax_record("path", "pax/overridden/file");
1175 append_entry(&mut bytes, b"PaxHeaders.0", &record, b'x', b"");
1176 append_entry(&mut bytes, b"ignored", b"pax", b'0', b"");
1177
1178 let long = [b'a'; 140];
1179 let mut long_nul = long.to_vec();
1180 long_nul.push(0);
1181 append_entry(&mut bytes, b"LongLink", &long_nul, b'L', b"");
1182 append_entry(&mut bytes, b"ignored", b"gnu", b'0', b"");
1183 finish_archive(&mut bytes);
1184
1185 let mut path = [0; 256];
1186 let mut link = [0; 256];
1187 let mut pax = [0; 512];
1188 let mut archive = Archive::new(ArchiveBuffers {
1189 path: &mut path,
1190 link: &mut link,
1191 pax: &mut pax,
1192 });
1193 let mut events = Events::default();
1194 archive.push(&bytes, &mut events).unwrap();
1195 archive.finish().unwrap();
1196 assert_eq!(events.entries[0].0, b"pax/overridden/file");
1197 assert_eq!(events.entries[0].2[0].0, b"path");
1198 assert_eq!(events.entries[1].0, long);
1199 assert_eq!(events.contents, b"paxgnu");
1200 }
1201
1202 #[test]
1203 fn rejects_paths_that_escape_the_root() {
1204 let mut bytes = Vec::new();
1205 append_entry(&mut bytes, b"../escape", b"", b'0', b"");
1206 let mut path = [0; 64];
1207 let mut link = [0; 64];
1208 let mut pax = [0; 64];
1209 let mut archive = Archive::new(ArchiveBuffers {
1210 path: &mut path,
1211 link: &mut link,
1212 pax: &mut pax,
1213 });
1214 assert!(archive.push(&bytes, &mut Events::default()).is_err());
1215 }
1216
1217 #[test]
1218 fn rejects_absolute_symbolic_link_targets() {
1219 let mut bytes = Vec::new();
1220 append_link(&mut bytes, b"link", b"/etc/passwd", b'2');
1221 let mut path = [0; 64];
1222 let mut link = [0; 64];
1223 let mut pax = [0; 64];
1224 let mut archive = Archive::new(ArchiveBuffers {
1225 path: &mut path,
1226 link: &mut link,
1227 pax: &mut pax,
1228 });
1229 assert_eq!(
1230 archive.push(&bytes, &mut Events::default()),
1231 Err(ArchiveError::InvalidLink)
1232 );
1233 }
1234
1235 #[test]
1236 fn rejects_symbolic_link_targets_that_escape_the_root() {
1237 let mut bytes = Vec::new();
1238 append_link(&mut bytes, b"dir/link", b"../../escape", b'2');
1239 let mut path = [0; 64];
1240 let mut link = [0; 64];
1241 let mut pax = [0; 64];
1242 let mut archive = Archive::new(ArchiveBuffers {
1243 path: &mut path,
1244 link: &mut link,
1245 pax: &mut pax,
1246 });
1247 assert_eq!(
1248 archive.push(&bytes, &mut Events::default()),
1249 Err(ArchiveError::InvalidLink)
1250 );
1251 }
1252
1253 #[test]
1254 fn resolves_symbolic_link_target_components_before_checking_depth() {
1255 assert_eq!(
1256 validate_symbolic_link::<()>(b"link", b"dir/../target"),
1257 Ok(())
1258 );
1259 assert_eq!(
1260 validate_symbolic_link::<()>(b"dir/link", b"./../../escape"),
1261 Err(ArchiveError::InvalidLink)
1262 );
1263 assert_eq!(
1264 validate_symbolic_link::<()>(b"dir/link", b"nested//../../../escape"),
1265 Err(ArchiveError::InvalidLink)
1266 );
1267 }
1268
1269 #[test]
1270 fn joins_ustar_name_and_prefix_with_exact_buffer_bounds() {
1271 let mut header = [0; BLOCK_SIZE];
1272 header[..4].copy_from_slice(b"name");
1273 header[345..351].copy_from_slice(b"prefix");
1274
1275 let mut exact = [0; 11];
1276 assert_eq!(header_path::<()>(&header, &mut exact), Ok(11));
1277 assert_eq!(&exact, b"prefix/name");
1278
1279 let mut too_small = [0; 10];
1280 assert!(header_path::<()>(&header, &mut too_small).is_err());
1281
1282 header[345..351].fill(0);
1283 let mut name_only = [0; 4];
1284 assert_eq!(header_path::<()>(&header, &mut name_only), Ok(4));
1285 assert_eq!(&name_only, b"name");
1286 }
1287
1288 #[test]
1289 fn normalizes_paths_and_rejects_invalid_inputs() {
1290 let mut path = *b"./a//b/./";
1291 let length = normalize_path::<()>(&mut path, 9).unwrap();
1292 assert_eq!(&path[..length], b"a/b");
1293
1294 let mut dot = *b".";
1295 let length = normalize_path::<()>(&mut dot, 1).unwrap();
1296 assert_eq!(&dot[..length], b".");
1297
1298 for invalid in [b"".as_slice(), b"/root", b"a\0b", b"../root"] {
1299 let mut path = [0; 16];
1300 path[..invalid.len()].copy_from_slice(invalid);
1301 assert_eq!(
1302 normalize_path::<()>(&mut path, invalid.len()),
1303 Err(ArchiveError::InvalidPath)
1304 );
1305 }
1306 }
1307
1308 #[test]
1309 fn rejects_malformed_pax_records() {
1310 assert!(validate_pax::<()>(b"garbage").is_err());
1311 for key in ["size", "path", "linkpath"] {
1312 let duplicate = pax_record(key, "first").repeat(2);
1313 assert_eq!(
1314 pax_overrides::<()>(&duplicate),
1315 Err(ArchiveError::InvalidPax(PaxError::InvalidRecord))
1316 );
1317 }
1318 assert_eq!(
1319 pax_overrides::<()>(&pax_record("vendor.key", "value").repeat(2)),
1320 Ok([None; 3])
1321 );
1322 }
1323
1324 #[test]
1325 fn extracts_a_regular_file_from_arbitrary_fragments() {
1326 let mut archive = Vec::new();
1327 append_entry(&mut archive, b"first", b"skip", b'0', b"");
1328 append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1329 append_entry(&mut archive, b"wanted", b"later", b'0', b"");
1330 finish_archive(&mut archive);
1331
1332 for fragment_size in 1..=BLOCK_SIZE + 1 {
1333 let mut extractor = EntryExtractor::new(b"wanted");
1334 let mut output = Vec::new();
1335 for fragment in archive.chunks(fragment_size) {
1336 extractor
1337 .push(fragment, |bytes| {
1338 output.extend_from_slice(bytes);
1339 Ok::<_, ()>(())
1340 })
1341 .unwrap();
1342 }
1343 extractor.finish().unwrap();
1344 assert_eq!(output, b"contents");
1345 }
1346 }
1347
1348 #[test]
1349 fn matches_a_ustar_prefix_without_allocating_a_path() {
1350 let mut archive = Vec::new();
1351 append_entry(
1352 &mut archive,
1353 b"settings.yaml",
1354 b"value",
1355 b'0',
1356 b"etc/datadog-agent",
1357 );
1358 finish_archive(&mut archive);
1359 let mut extractor = EntryExtractor::new(b"etc/datadog-agent/settings.yaml");
1360 let mut output = Vec::new();
1361 extractor
1362 .push(&archive, |bytes| {
1363 output.extend_from_slice(bytes);
1364 Ok::<_, ()>(())
1365 })
1366 .unwrap();
1367 extractor.finish().unwrap();
1368 assert_eq!(output, b"value");
1369 }
1370
1371 #[test]
1372 fn recognizes_an_empty_regular_file() {
1373 let mut archive = Vec::new();
1374 append_entry(&mut archive, b"empty", b"", b'0', b"");
1375 finish_archive(&mut archive);
1376 let mut extractor = EntryExtractor::new(b"empty");
1377 extractor.push(&archive, |_| Ok::<_, ()>(())).unwrap();
1378 assert!(extractor.found());
1379 extractor.finish().unwrap();
1380 }
1381
1382 #[test]
1383 fn does_not_extract_a_directory_with_the_requested_name() {
1384 let mut archive = Vec::new();
1385 append_entry(&mut archive, b"wanted", b"", b'5', b"");
1386 finish_archive(&mut archive);
1387 let mut extractor = EntryExtractor::new(b"wanted");
1388 extractor.push(&archive, |_| Ok::<_, ()>(())).unwrap();
1389 assert_eq!(extractor.finish(), Err(FinishError::NotFound));
1390 }
1391
1392 #[test]
1393 fn rejects_an_invalid_checksum() {
1394 let mut archive = Vec::new();
1395 append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1396 archive[0] ^= 1;
1397 let mut extractor = EntryExtractor::new(b"wanted");
1398 assert_eq!(
1399 extractor.push(&archive, |_| Ok::<_, ()>(())),
1400 Err(ExtractError::InvalidChecksum)
1401 );
1402 }
1403
1404 #[test]
1405 fn reports_a_truncated_stream() {
1406 let mut archive = Vec::new();
1407 append_entry(&mut archive, b"wanted", b"contents", b'0', b"");
1408 let mut extractor = EntryExtractor::new(b"wanted");
1409 extractor
1410 .push(&archive[..BLOCK_SIZE + 2], |_| Ok::<_, ()>(()))
1411 .unwrap();
1412 assert_eq!(extractor.finish(), Err(FinishError::UnexpectedEof));
1413 }
1414
1415 fn append_entry(archive: &mut Vec<u8>, name: &[u8], contents: &[u8], kind: u8, prefix: &[u8]) {
1416 let mut header = [0u8; BLOCK_SIZE];
1417 header[..name.len()].copy_from_slice(name);
1418 write_octal(&mut header[100..108], 0o644);
1419 write_octal(&mut header[108..116], 0);
1420 write_octal(&mut header[116..124], 0);
1421 write_octal(&mut header[124..136], contents.len() as u64);
1422 write_octal(&mut header[136..148], 0);
1423 header[148..156].fill(b' ');
1424 header[156] = kind;
1425 header[257..263].copy_from_slice(b"ustar\0");
1426 header[263..265].copy_from_slice(b"00");
1427 header[345..345 + prefix.len()].copy_from_slice(prefix);
1428 let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
1429 write_octal(&mut header[148..156], checksum);
1430
1431 archive.extend_from_slice(&header);
1432 archive.extend_from_slice(contents);
1433 archive.resize(archive.len() + super::padding_for(contents.len() as u64), 0);
1434 }
1435
1436 fn append_link(archive: &mut Vec<u8>, name: &[u8], target: &[u8], kind: u8) {
1437 let start = archive.len();
1438 append_entry(archive, name, b"", kind, b"");
1439 let header = &mut archive[start..start + BLOCK_SIZE];
1440 header[157..157 + target.len()].copy_from_slice(target);
1441 header[148..156].fill(b' ');
1442 let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
1443 write_octal(&mut header[148..156], checksum);
1444 }
1445
1446 fn finish_archive(archive: &mut Vec<u8>) {
1447 archive.resize(archive.len() + 2 * BLOCK_SIZE, 0);
1448 }
1449
1450 fn pax_record(key: &str, value: &str) -> Vec<u8> {
1451 let payload = key.len() + 1 + value.len() + 1;
1452 let mut digits = 1;
1453 loop {
1454 let length = digits + 1 + payload;
1455 let actual_digits = length.to_string().len();
1456 if actual_digits == digits {
1457 return format!("{length} {key}={value}\n").into_bytes();
1458 }
1459 digits = actual_digits;
1460 }
1461 }
1462
1463 fn write_octal(field: &mut [u8], mut value: u64) {
1464 field.fill(b'0');
1465 *field.last_mut().unwrap() = 0;
1466 let digits = field.len() - 1;
1467 for byte in field[..digits].iter_mut().rev() {
1468 *byte = b'0' + (value % 8) as u8;
1469 value /= 8;
1470 }
1471 assert_eq!(value, 0);
1472 }
1473}