1use std::collections::HashSet;
9
10use archive_trait::{
11 Archive as ArchiveTrait, Member, MemberMetadata, MemberPayload as MemberPayloadTrait,
12 SpecialKind,
13};
14use tar_framing::{
15 ArchiveFormat, FrameError, PaxKeyword, PaxKind, PaxRecord, PaxValue, StreamPolicy, UstarKind,
16 logical::{MemberExtensions, MemberFrame, MemberPayload as FramingMemberPayload, TarReader},
17};
18use thiserror::Error;
19use tokio::io::AsyncRead;
20
21pub use tar_framing::{
22 DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE, DEFAULT_MAX_GNU_EXTENSION_SIZE,
23 DEFAULT_MAX_PAX_EXTENSION_SIZE,
24};
25
26pub struct TarArchive<R> {
35 reader: TarReader<R>,
36 policy: DecodePolicy,
37 fused: bool,
38}
39
40impl<R> TarArchive<R> {
41 pub fn new(reader: R) -> Self {
43 Self {
44 reader: TarReader::new(reader),
45 policy: DecodePolicy::default(),
46 fused: false,
47 }
48 }
49
50 pub fn with_policy(mut self, policy: DecodePolicy) -> Self {
54 let stream_policy = StreamPolicy::default()
55 .max_pax_extension_size(policy.pax_policy.max_extension_size)
56 .max_global_pax_extensions_size(policy.pax_policy.max_global_extensions_size)
57 .allow_all_nul_numeric_fields(policy.allow_all_nul_numeric_fields)
58 .max_gnu_extension_size(policy.max_gnu_extension_size);
59 self.reader = self.reader.with_policy(stream_policy);
60 self.policy = policy;
61 self
62 }
63}
64
65#[derive(Clone, Debug)]
69pub struct DecodePolicy {
70 allow_gnu: bool,
71 allow_all_nul_numeric_fields: bool,
72 max_gnu_extension_size: u64,
73 pax_policy: PaxDecodePolicy,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct PaxDecodePolicy {
81 max_extension_size: u64,
82 max_global_extensions_size: u64,
83 allow_non_utf8_pax_vendor_values: bool,
84 allow_global_pax_extensions: bool,
85 vendor_extension_policy: PaxVendorExtensionPolicy,
86 allow_duplicate_pax_records: bool,
87 allow_global_pax_member_metadata: bool,
88}
89
90#[derive(Clone, Debug, Default, Eq, PartialEq)]
92pub enum PaxVendorExtensionPolicy {
93 #[default]
95 RejectUnknown,
96 Ignore(PaxVendorAllowlist),
100 AllowUnknown,
104}
105
106impl PaxVendorExtensionPolicy {
107 pub fn ignore(vendors: impl IntoIterator<Item = &'static str>) -> Self {
111 Self::Ignore(PaxVendorAllowlist {
112 vendors: vendors.into_iter().collect(),
113 })
114 }
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct PaxVendorAllowlist {
122 vendors: HashSet<&'static str>,
123}
124
125impl Default for PaxDecodePolicy {
126 fn default() -> Self {
127 Self {
128 max_extension_size: DEFAULT_MAX_PAX_EXTENSION_SIZE,
129 max_global_extensions_size: DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE,
130 allow_non_utf8_pax_vendor_values: true,
131 allow_global_pax_extensions: true,
132 vendor_extension_policy: PaxVendorExtensionPolicy::default(),
133 allow_duplicate_pax_records: false,
134 allow_global_pax_member_metadata: false,
135 }
136 }
137}
138
139impl Default for DecodePolicy {
140 fn default() -> Self {
141 Self {
142 allow_gnu: true,
143 allow_all_nul_numeric_fields: true,
144 max_gnu_extension_size: DEFAULT_MAX_GNU_EXTENSION_SIZE,
145 pax_policy: PaxDecodePolicy::default(),
146 }
147 }
148}
149
150impl DecodePolicy {
151 pub fn allow_gnu(mut self, allow: bool) -> Self {
158 self.allow_gnu = allow;
159 self
160 }
161
162 pub fn allow_all_nul_numeric_fields(mut self, allow: bool) -> Self {
170 self.allow_all_nul_numeric_fields = allow;
171 self
172 }
173
174 pub fn max_gnu_extension_size(mut self, max_gnu_extension_size: u64) -> Self {
182 self.max_gnu_extension_size = max_gnu_extension_size;
183 self
184 }
185
186 pub fn pax_policy(mut self, policy: PaxDecodePolicy) -> Self {
188 self.pax_policy = policy;
189 self
190 }
191
192 fn check_format(&self, position: u64, format: ArchiveFormat) -> Result<(), DecodeError> {
193 if format == ArchiveFormat::Gnu && !self.allow_gnu {
194 return Err(DecodeError::policy_violation(
195 position,
196 DecodePolicyViolation::GnuArchive,
197 ));
198 }
199 Ok(())
200 }
201
202 fn check_global_pax(&self, position: u64, records: &[PaxRecord]) -> Result<(), DecodeError> {
203 self.pax_policy.check_global_pax_extension(position)?;
204 self.pax_policy
205 .check_pax_records(position, PaxKind::Global, records)
206 }
207
208 fn check_member<R>(&self, frame: &MemberFrame<'_, R>) -> Result<(), DecodeError> {
209 if let MemberExtensions::Pax(state) = &frame.extensions {
210 for extension in state
211 .extensions()
212 .filter(|extension| extension.kind == PaxKind::Global)
213 {
214 self.check_global_pax(extension.position, extension.records())?;
215 }
216 }
217 let format_position = match &frame.extensions {
218 MemberExtensions::Pax(_) => frame.header.position,
219 MemberExtensions::Gnu {
220 long_name,
221 long_link,
222 } => long_name
223 .iter()
224 .chain(long_link.iter())
225 .map(|header| header.position)
226 .min()
227 .unwrap_or(frame.header.position),
228 };
229 self.check_format(format_position, frame.header.format)?;
230 if let MemberExtensions::Pax(state) = &frame.extensions {
231 for extension in state
232 .extensions()
233 .filter(|extension| extension.kind == PaxKind::Local)
234 {
235 self.pax_policy.check_pax_records(
236 extension.position,
237 PaxKind::Local,
238 extension.records(),
239 )?;
240 }
241 }
242 Ok(())
243 }
244}
245
246impl PaxDecodePolicy {
247 pub fn max_extension_size(mut self, max_extension_size: u64) -> Self {
258 self.max_extension_size = max_extension_size;
259 self
260 }
261
262 pub fn max_global_extensions_size(mut self, max_global_extensions_size: u64) -> Self {
271 self.max_global_extensions_size = max_global_extensions_size;
272 self
273 }
274
275 pub fn allow_non_utf8_pax_vendor_values(mut self, allow: bool) -> Self {
285 self.allow_non_utf8_pax_vendor_values = allow;
286 self
287 }
288
289 pub fn allow_global_pax_extensions(mut self, allow: bool) -> Self {
298 self.allow_global_pax_extensions = allow;
299 self
300 }
301
302 pub fn vendor_extension_policy(mut self, policy: PaxVendorExtensionPolicy) -> Self {
319 self.vendor_extension_policy = policy;
320 self
321 }
322
323 pub fn allow_duplicate_pax_records(mut self, allow: bool) -> Self {
330 self.allow_duplicate_pax_records = allow;
331 self
332 }
333
334 pub fn allow_global_pax_member_metadata(mut self, allow: bool) -> Self {
342 self.allow_global_pax_member_metadata = allow;
343 self
344 }
345
346 fn check_global_pax_extension(&self, position: u64) -> Result<(), DecodeError> {
347 if !self.allow_global_pax_extensions {
348 return Err(DecodeError::policy_violation(
349 position,
350 DecodePolicyViolation::GlobalPaxExtension,
351 ));
352 }
353 Ok(())
354 }
355
356 fn check_pax_records(
357 &self,
358 position: u64,
359 kind: PaxKind,
360 records: &[PaxRecord],
361 ) -> Result<(), DecodeError> {
362 for record in records {
363 if let PaxRecord::Vendor {
364 vendor,
365 name,
366 value,
367 } = record
368 {
369 let allowed = match &self.vendor_extension_policy {
370 PaxVendorExtensionPolicy::RejectUnknown => false,
371 PaxVendorExtensionPolicy::Ignore(allowed) => {
372 allowed.vendors.contains(vendor.as_ref())
373 }
374 PaxVendorExtensionPolicy::AllowUnknown => true,
375 };
376 if !allowed {
377 return Err(DecodeError::policy_violation(
378 position,
379 DecodePolicyViolation::PaxVendorExtension {
380 vendor: vendor.to_string(),
381 name: name.to_string(),
382 },
383 ));
384 }
385
386 if !self.allow_non_utf8_pax_vendor_values
387 && let PaxValue::Value(value) = value
388 && std::str::from_utf8(value).is_err()
389 {
390 return Err(DecodeError::policy_violation(
391 position,
392 DecodePolicyViolation::NonUtf8PaxVendorValue {
393 vendor: vendor.to_string(),
394 name: name.to_string(),
395 },
396 ));
397 }
398 }
399 }
400
401 if kind == PaxKind::Global && !self.allow_global_pax_member_metadata {
402 for record in records {
403 let keyword = match record.keyword() {
404 PaxKeyword::Path => Some("path"),
405 PaxKeyword::LinkPath => Some("linkpath"),
406 PaxKeyword::Size => Some("size"),
407 _ => None,
408 };
409 if let Some(keyword) = keyword {
410 return Err(DecodeError::policy_violation(
411 position,
412 DecodePolicyViolation::GlobalPaxMemberMetadata { keyword },
413 ));
414 }
415 }
416 }
417
418 if !self.allow_duplicate_pax_records {
419 let mut keywords = HashSet::new();
420 for record in records {
421 let keyword = record.keyword();
422 if !keywords.insert(keyword.clone()) {
423 return Err(DecodeError::policy_violation(
424 position,
425 DecodePolicyViolation::DuplicatePaxRecord {
426 keyword: keyword.to_string(),
427 },
428 ));
429 }
430 }
431 }
432
433 Ok(())
434 }
435}
436
437#[derive(Clone, Debug, Eq, PartialEq, Error)]
439pub enum DecodePolicyViolation {
440 #[error("GNU archives are not allowed")]
442 GnuArchive,
443 #[error("global pax extended headers are not allowed")]
445 GlobalPaxExtension,
446 #[error("pax vendor extension {vendor}.{name} is not allowed")]
448 PaxVendorExtension {
449 vendor: String,
451 name: String,
453 },
454 #[error("pax vendor extension {vendor}.{name} contains a non-UTF-8 value")]
456 NonUtf8PaxVendorValue {
457 vendor: String,
459 name: String,
461 },
462 #[error("pax extended header contains duplicate record {keyword}")]
464 DuplicatePaxRecord {
465 keyword: String,
467 },
468 #[error("global pax extended header contains restricted member metadata {keyword}")]
470 GlobalPaxMemberMetadata {
471 keyword: &'static str,
473 },
474}
475
476#[derive(Debug, Error)]
478pub enum DecodeError {
479 #[error(transparent)]
481 Framing(#[from] FrameError),
482 #[error("at byte {position}: {field} is not valid UTF-8")]
484 InvalidUtf8 {
485 position: u64,
487 field: &'static str,
489 },
490 #[error("at byte {position}: decode policy rejected input: {violation}")]
492 PolicyViolation {
493 position: u64,
495 violation: DecodePolicyViolation,
497 },
498}
499
500impl DecodeError {
501 fn policy_violation(position: u64, violation: DecodePolicyViolation) -> Self {
502 Self::PolicyViolation {
503 position,
504 violation,
505 }
506 }
507}
508
509pub struct TarMemberPayload<'a, R> {
511 payload: FramingMemberPayload<'a, R>,
512}
513
514impl<R: AsyncRead + Unpin> MemberPayloadTrait for TarMemberPayload<'_, R> {
515 type Error = DecodeError;
516
517 async fn next_chunk(
518 &mut self,
519 buffer: &mut Vec<u8>,
520 target_len: usize,
521 ) -> Result<bool, Self::Error> {
522 self.payload
523 .next_chunk(buffer, target_len)
524 .await
525 .map_err(Into::into)
526 }
527
528 async fn skip(self) -> Result<(), Self::Error> {
529 self.payload.skip().await.map_err(Into::into)
530 }
531}
532
533impl<R: AsyncRead + Unpin> ArchiveTrait for TarArchive<R> {
534 type Error = DecodeError;
535 type Payload<'a>
536 = TarMemberPayload<'a, R>
537 where
538 Self: 'a;
539
540 async fn next_member<'a>(
541 &'a mut self,
542 ) -> Result<Option<Member<Self::Payload<'a>>>, Self::Error> {
543 if self.fused {
544 return Ok(None);
545 }
546
547 let frame = match self.reader.next_frame().await {
548 Ok(Some(frame)) => frame,
549 Ok(None) => {
550 self.fused = true;
551 return Ok(None);
552 }
553 Err(error) => {
554 self.fused = true;
555 return Err(error.into());
556 }
557 };
558
559 if let Err(error) = self.policy.check_member(&frame) {
560 self.fused = true;
561 return Err(error);
562 }
563
564 match project_member(frame) {
565 Ok(member) => Ok(Some(member)),
566 Err(error) => {
567 self.fused = true;
568 Err(error)
569 }
570 }
571 }
572}
573
574fn project_member<'a, R>(
575 frame: MemberFrame<'a, R>,
576) -> Result<Member<TarMemberPayload<'a, R>>, DecodeError> {
577 let position = frame.header.position;
578 let kind = frame.header.kind;
579 let size = frame.header.effective_size;
580 let executable = frame.header.mode.unwrap_or_default() & 0o111 != 0;
581 let path = std::str::from_utf8(frame.effective_path()?.as_ref())
582 .map(str::to_owned)
583 .map_err(|_| DecodeError::InvalidUtf8 {
584 position,
585 field: "path",
586 })?;
587 let target = if matches!(kind, UstarKind::HardLink | UstarKind::SymbolicLink) {
588 std::str::from_utf8(frame.effective_link_path()?.as_ref())
589 .map(str::to_owned)
590 .map_err(|_| DecodeError::InvalidUtf8 {
591 position,
592 field: "linkpath",
593 })?
594 } else {
595 String::new()
596 };
597 let metadata = MemberMetadata { path, position };
598
599 Ok(match kind {
600 UstarKind::Regular | UstarKind::Contiguous => Member::File {
601 metadata,
602 size,
603 executable,
604 payload: TarMemberPayload {
605 payload: frame.payload,
606 },
607 },
608 UstarKind::Directory => Member::Directory { metadata },
609 UstarKind::SymbolicLink => Member::SymbolicLink { metadata, target },
610 UstarKind::HardLink => Member::HardLink {
611 metadata,
612 target,
613 size,
614 payload: TarMemberPayload {
615 payload: frame.payload,
616 },
617 },
618 UstarKind::CharacterDevice => Member::Special {
619 metadata,
620 kind: SpecialKind::CharacterDevice,
621 },
622 UstarKind::BlockDevice => Member::Special {
623 metadata,
624 kind: SpecialKind::BlockDevice,
625 },
626 UstarKind::Fifo => Member::Special {
627 metadata,
628 kind: SpecialKind::Fifo,
629 },
630 })
631}