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 .allow_header_uname_and_gname_without_terminator(
59 policy.allow_header_uname_and_gname_without_terminator,
60 )
61 .max_gnu_extension_size(policy.max_gnu_extension_size);
62 self.reader = self.reader.with_policy(stream_policy);
63 self.policy = policy;
64 self
65 }
66}
67
68#[derive(Clone, Debug)]
72pub struct DecodePolicy {
73 allow_gnu: bool,
74 allow_all_nul_numeric_fields: bool,
75 allow_header_uname_and_gname_without_terminator: bool,
76 max_gnu_extension_size: u64,
77 pax_policy: PaxDecodePolicy,
78}
79
80#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct PaxDecodePolicy {
85 max_extension_size: u64,
86 max_global_extensions_size: u64,
87 allow_non_utf8_pax_vendor_values: bool,
88 allow_global_pax_extensions: bool,
89 vendor_extension_policy: PaxVendorExtensionPolicy,
90 allow_duplicate_pax_records: bool,
91 allow_global_pax_member_metadata: bool,
92}
93
94#[derive(Clone, Debug, Default, Eq, PartialEq)]
96pub enum PaxVendorExtensionPolicy {
97 #[default]
99 RejectUnknown,
100 Ignore(PaxVendorAllowlist),
104 AllowUnknown,
108}
109
110impl PaxVendorExtensionPolicy {
111 pub fn ignore(vendors: impl IntoIterator<Item = &'static str>) -> Self {
115 Self::Ignore(PaxVendorAllowlist {
116 vendors: vendors.into_iter().collect(),
117 })
118 }
119}
120
121#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct PaxVendorAllowlist {
126 vendors: HashSet<&'static str>,
127}
128
129impl Default for PaxDecodePolicy {
130 fn default() -> Self {
131 Self {
132 max_extension_size: DEFAULT_MAX_PAX_EXTENSION_SIZE,
133 max_global_extensions_size: DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE,
134 allow_non_utf8_pax_vendor_values: true,
135 allow_global_pax_extensions: true,
136 vendor_extension_policy: PaxVendorExtensionPolicy::default(),
137 allow_duplicate_pax_records: false,
138 allow_global_pax_member_metadata: false,
139 }
140 }
141}
142
143impl Default for DecodePolicy {
144 fn default() -> Self {
145 Self {
146 allow_gnu: true,
147 allow_all_nul_numeric_fields: true,
148 allow_header_uname_and_gname_without_terminator: false,
149 max_gnu_extension_size: DEFAULT_MAX_GNU_EXTENSION_SIZE,
150 pax_policy: PaxDecodePolicy::default(),
151 }
152 }
153}
154
155impl DecodePolicy {
156 pub fn allow_gnu(mut self, allow: bool) -> Self {
163 self.allow_gnu = allow;
164 self
165 }
166
167 pub fn allow_all_nul_numeric_fields(mut self, allow: bool) -> Self {
175 self.allow_all_nul_numeric_fields = allow;
176 self
177 }
178
179 pub fn allow_header_uname_and_gname_without_terminator(mut self, allow: bool) -> Self {
185 self.allow_header_uname_and_gname_without_terminator = allow;
186 self
187 }
188
189 pub fn max_gnu_extension_size(mut self, max_gnu_extension_size: u64) -> Self {
197 self.max_gnu_extension_size = max_gnu_extension_size;
198 self
199 }
200
201 pub fn pax_policy(mut self, policy: PaxDecodePolicy) -> Self {
203 self.pax_policy = policy;
204 self
205 }
206
207 fn check_format(&self, position: u64, format: ArchiveFormat) -> Result<(), DecodeError> {
208 if format == ArchiveFormat::Gnu && !self.allow_gnu {
209 return Err(DecodeError::policy_violation(
210 position,
211 DecodePolicyViolation::GnuArchive,
212 ));
213 }
214 Ok(())
215 }
216
217 fn check_global_pax(&self, position: u64, records: &[PaxRecord]) -> Result<(), DecodeError> {
218 self.pax_policy.check_global_pax_extension(position)?;
219 self.pax_policy
220 .check_pax_records(position, PaxKind::Global, records)
221 }
222
223 fn check_member<R>(&self, frame: &MemberFrame<'_, R>) -> Result<(), DecodeError> {
224 if let MemberExtensions::Pax(state) = &frame.extensions {
225 for extension in state
226 .extensions()
227 .filter(|extension| extension.kind == PaxKind::Global)
228 {
229 self.check_global_pax(extension.position, extension.records())?;
230 }
231 }
232 let format_position = match &frame.extensions {
233 MemberExtensions::Pax(_) => frame.header.position,
234 MemberExtensions::Gnu {
235 long_name,
236 long_link,
237 } => long_name
238 .iter()
239 .chain(long_link.iter())
240 .map(|header| header.position)
241 .min()
242 .unwrap_or(frame.header.position),
243 };
244 self.check_format(format_position, frame.header.format)?;
245 if let MemberExtensions::Pax(state) = &frame.extensions {
246 for extension in state
247 .extensions()
248 .filter(|extension| extension.kind == PaxKind::Local)
249 {
250 self.pax_policy.check_pax_records(
251 extension.position,
252 PaxKind::Local,
253 extension.records(),
254 )?;
255 }
256 }
257 Ok(())
258 }
259}
260
261impl PaxDecodePolicy {
262 pub fn max_extension_size(mut self, max_extension_size: u64) -> Self {
273 self.max_extension_size = max_extension_size;
274 self
275 }
276
277 pub fn max_global_extensions_size(mut self, max_global_extensions_size: u64) -> Self {
286 self.max_global_extensions_size = max_global_extensions_size;
287 self
288 }
289
290 pub fn allow_non_utf8_pax_vendor_values(mut self, allow: bool) -> Self {
300 self.allow_non_utf8_pax_vendor_values = allow;
301 self
302 }
303
304 pub fn allow_global_pax_extensions(mut self, allow: bool) -> Self {
313 self.allow_global_pax_extensions = allow;
314 self
315 }
316
317 pub fn vendor_extension_policy(mut self, policy: PaxVendorExtensionPolicy) -> Self {
334 self.vendor_extension_policy = policy;
335 self
336 }
337
338 pub fn allow_duplicate_pax_records(mut self, allow: bool) -> Self {
345 self.allow_duplicate_pax_records = allow;
346 self
347 }
348
349 pub fn allow_global_pax_member_metadata(mut self, allow: bool) -> Self {
357 self.allow_global_pax_member_metadata = allow;
358 self
359 }
360
361 fn check_global_pax_extension(&self, position: u64) -> Result<(), DecodeError> {
362 if !self.allow_global_pax_extensions {
363 return Err(DecodeError::policy_violation(
364 position,
365 DecodePolicyViolation::GlobalPaxExtension,
366 ));
367 }
368 Ok(())
369 }
370
371 fn check_pax_records(
372 &self,
373 position: u64,
374 kind: PaxKind,
375 records: &[PaxRecord],
376 ) -> Result<(), DecodeError> {
377 for record in records {
378 if let PaxRecord::Vendor {
379 vendor,
380 name,
381 value,
382 } = record
383 {
384 let allowed = match &self.vendor_extension_policy {
385 PaxVendorExtensionPolicy::RejectUnknown => false,
386 PaxVendorExtensionPolicy::Ignore(allowed) => {
387 allowed.vendors.contains(vendor.as_ref())
388 }
389 PaxVendorExtensionPolicy::AllowUnknown => true,
390 };
391 if !allowed {
392 return Err(DecodeError::policy_violation(
393 position,
394 DecodePolicyViolation::PaxVendorExtension {
395 vendor: vendor.to_string(),
396 name: name.to_string(),
397 },
398 ));
399 }
400
401 if !self.allow_non_utf8_pax_vendor_values
402 && let PaxValue::Value(value) = value
403 && std::str::from_utf8(value).is_err()
404 {
405 return Err(DecodeError::policy_violation(
406 position,
407 DecodePolicyViolation::NonUtf8PaxVendorValue {
408 vendor: vendor.to_string(),
409 name: name.to_string(),
410 },
411 ));
412 }
413 }
414 }
415
416 if kind == PaxKind::Global && !self.allow_global_pax_member_metadata {
417 for record in records {
418 let keyword = match record.keyword() {
419 PaxKeyword::Path => Some("path"),
420 PaxKeyword::LinkPath => Some("linkpath"),
421 PaxKeyword::Size => Some("size"),
422 _ => None,
423 };
424 if let Some(keyword) = keyword {
425 return Err(DecodeError::policy_violation(
426 position,
427 DecodePolicyViolation::GlobalPaxMemberMetadata { keyword },
428 ));
429 }
430 }
431 }
432
433 if !self.allow_duplicate_pax_records {
434 let mut keywords = HashSet::new();
435 for record in records {
436 let keyword = record.keyword();
437 if !keywords.insert(keyword.clone()) {
438 return Err(DecodeError::policy_violation(
439 position,
440 DecodePolicyViolation::DuplicatePaxRecord {
441 keyword: keyword.to_string(),
442 },
443 ));
444 }
445 }
446 }
447
448 Ok(())
449 }
450}
451
452#[derive(Clone, Debug, Eq, PartialEq, Error)]
454pub enum DecodePolicyViolation {
455 #[error("GNU archives are not allowed")]
457 GnuArchive,
458 #[error("global pax extended headers are not allowed")]
460 GlobalPaxExtension,
461 #[error("pax vendor extension {vendor}.{name} is not allowed")]
463 PaxVendorExtension {
464 vendor: String,
466 name: String,
468 },
469 #[error("pax vendor extension {vendor}.{name} contains a non-UTF-8 value")]
471 NonUtf8PaxVendorValue {
472 vendor: String,
474 name: String,
476 },
477 #[error("pax extended header contains duplicate record {keyword}")]
479 DuplicatePaxRecord {
480 keyword: String,
482 },
483 #[error("global pax extended header contains restricted member metadata {keyword}")]
485 GlobalPaxMemberMetadata {
486 keyword: &'static str,
488 },
489}
490
491#[derive(Debug, Error)]
493pub enum DecodeError {
494 #[error(transparent)]
496 Framing(#[from] FrameError),
497 #[error("at byte {position}: {field} is not valid UTF-8")]
499 InvalidUtf8 {
500 position: u64,
502 field: &'static str,
504 },
505 #[error("at byte {position}: decode policy rejected input: {violation}")]
507 PolicyViolation {
508 position: u64,
510 violation: DecodePolicyViolation,
512 },
513}
514
515impl DecodeError {
516 fn policy_violation(position: u64, violation: DecodePolicyViolation) -> Self {
517 Self::PolicyViolation {
518 position,
519 violation,
520 }
521 }
522}
523
524pub struct TarMemberPayload<'a, R> {
526 payload: FramingMemberPayload<'a, R>,
527}
528
529impl<R: AsyncRead + Unpin> MemberPayloadTrait for TarMemberPayload<'_, R> {
530 type Error = DecodeError;
531
532 async fn next_chunk(
533 &mut self,
534 buffer: &mut Vec<u8>,
535 target_len: usize,
536 ) -> Result<bool, Self::Error> {
537 self.payload
538 .next_chunk(buffer, target_len)
539 .await
540 .map_err(Into::into)
541 }
542
543 async fn skip(self) -> Result<(), Self::Error> {
544 self.payload.skip().await.map_err(Into::into)
545 }
546}
547
548impl<R: AsyncRead + Unpin> ArchiveTrait for TarArchive<R> {
549 type Error = DecodeError;
550 type Payload<'a>
551 = TarMemberPayload<'a, R>
552 where
553 Self: 'a;
554
555 async fn next_member<'a>(
556 &'a mut self,
557 ) -> Result<Option<Member<Self::Payload<'a>>>, Self::Error> {
558 if self.fused {
559 return Ok(None);
560 }
561
562 let frame = match self.reader.next_frame().await {
563 Ok(Some(frame)) => frame,
564 Ok(None) => {
565 self.fused = true;
566 return Ok(None);
567 }
568 Err(error) => {
569 self.fused = true;
570 return Err(error.into());
571 }
572 };
573
574 if let Err(error) = self.policy.check_member(&frame) {
575 self.fused = true;
576 return Err(error);
577 }
578
579 match project_member(frame) {
580 Ok(member) => Ok(Some(member)),
581 Err(error) => {
582 self.fused = true;
583 Err(error)
584 }
585 }
586 }
587}
588
589fn project_member<'a, R>(
590 frame: MemberFrame<'a, R>,
591) -> Result<Member<TarMemberPayload<'a, R>>, DecodeError> {
592 let position = frame.header.position;
593 let kind = frame.header.kind;
594 let size = frame.header.effective_size;
595 let executable = frame.header.mode.unwrap_or_default() & 0o111 != 0;
596 let path = std::str::from_utf8(frame.effective_path()?.as_ref())
597 .map(str::to_owned)
598 .map_err(|_| DecodeError::InvalidUtf8 {
599 position,
600 field: "path",
601 })?;
602 let target = if matches!(kind, UstarKind::HardLink | UstarKind::SymbolicLink) {
603 std::str::from_utf8(frame.effective_link_path()?.as_ref())
604 .map(str::to_owned)
605 .map_err(|_| DecodeError::InvalidUtf8 {
606 position,
607 field: "linkpath",
608 })?
609 } else {
610 String::new()
611 };
612 let metadata = MemberMetadata { path, position };
613
614 Ok(match kind {
615 UstarKind::Regular | UstarKind::Contiguous => Member::File {
616 metadata,
617 size,
618 executable,
619 payload: TarMemberPayload {
620 payload: frame.payload,
621 },
622 },
623 UstarKind::Directory => Member::Directory { metadata },
624 UstarKind::SymbolicLink => Member::SymbolicLink { metadata, target },
625 UstarKind::HardLink => Member::HardLink {
626 metadata,
627 target,
628 size,
629 payload: TarMemberPayload {
630 payload: frame.payload,
631 },
632 },
633 UstarKind::CharacterDevice => Member::Special {
634 metadata,
635 kind: SpecialKind::CharacterDevice,
636 },
637 UstarKind::BlockDevice => Member::Special {
638 metadata,
639 kind: SpecialKind::BlockDevice,
640 },
641 UstarKind::Fifo => Member::Special {
642 metadata,
643 kind: SpecialKind::Fifo,
644 },
645 })
646}