1use std::collections::HashSet;
4
5use archive_trait::{
6 Archive as ArchiveTrait, Member, MemberMetadata, MemberPayload as MemberPayloadTrait,
7 SpecialKind,
8};
9use tar_framing::{
10 ArchiveFormat, FrameError, PaxKeyword, PaxKind, PaxRecord, PaxValue, StreamPolicy, UstarKind,
11 logical::{MemberExtensions, MemberFrame, MemberPayload as FramingMemberPayload, TarReader},
12};
13use thiserror::Error;
14use tokio::io::AsyncRead;
15
16pub use tar_framing::{
17 DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE, DEFAULT_MAX_GNU_EXTENSION_SIZE,
18 DEFAULT_MAX_PAX_EXTENSION_SIZE,
19};
20
21pub struct TarArchive<R> {
26 reader: TarReader<R>,
27 policy: DecodePolicy,
28 fused: bool,
29}
30
31impl<R> TarArchive<R> {
32 pub fn new(reader: R) -> Self {
34 Self {
35 reader: TarReader::new(reader),
36 policy: DecodePolicy::default(),
37 fused: false,
38 }
39 }
40
41 pub fn with_policy(mut self, policy: DecodePolicy) -> Self {
45 let stream_policy = StreamPolicy::default()
46 .max_pax_extension_size(policy.pax_policy.max_extension_size)
47 .max_global_pax_extensions_size(policy.pax_policy.max_global_extensions_size)
48 .allow_all_nul_numeric_fields(policy.allow_all_nul_numeric_fields)
49 .max_gnu_extension_size(policy.max_gnu_extension_size);
50 self.reader = self.reader.with_policy(stream_policy);
51 self.policy = policy;
52 self
53 }
54}
55
56#[derive(Clone, Debug)]
60pub struct DecodePolicy {
61 allow_gnu: bool,
62 allow_all_nul_numeric_fields: bool,
63 max_gnu_extension_size: u64,
64 pax_policy: PaxDecodePolicy,
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct PaxDecodePolicy {
72 max_extension_size: u64,
73 max_global_extensions_size: u64,
74 allow_non_utf8_pax_vendor_values: bool,
75 allow_global_pax_extensions: bool,
76 vendor_extension_policy: PaxVendorExtensionPolicy,
77 allow_duplicate_pax_records: bool,
78 allow_global_pax_member_metadata: bool,
79}
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
83pub enum PaxVendorExtensionPolicy {
84 #[default]
86 RejectUnknown,
87 Ignore(PaxVendorAllowlist),
91 AllowUnknown,
95}
96
97impl PaxVendorExtensionPolicy {
98 pub fn ignore(vendors: impl IntoIterator<Item = &'static str>) -> Self {
102 Self::Ignore(PaxVendorAllowlist {
103 vendors: vendors.into_iter().collect(),
104 })
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct PaxVendorAllowlist {
113 vendors: HashSet<&'static str>,
114}
115
116impl Default for PaxDecodePolicy {
117 fn default() -> Self {
118 Self {
119 max_extension_size: DEFAULT_MAX_PAX_EXTENSION_SIZE,
120 max_global_extensions_size: DEFAULT_MAX_GLOBAL_PAX_EXTENSIONS_SIZE,
121 allow_non_utf8_pax_vendor_values: true,
122 allow_global_pax_extensions: true,
123 vendor_extension_policy: PaxVendorExtensionPolicy::default(),
124 allow_duplicate_pax_records: false,
125 allow_global_pax_member_metadata: false,
126 }
127 }
128}
129
130impl Default for DecodePolicy {
131 fn default() -> Self {
132 Self {
133 allow_gnu: true,
134 allow_all_nul_numeric_fields: true,
135 max_gnu_extension_size: DEFAULT_MAX_GNU_EXTENSION_SIZE,
136 pax_policy: PaxDecodePolicy::default(),
137 }
138 }
139}
140
141impl DecodePolicy {
142 pub fn allow_gnu(mut self, allow: bool) -> Self {
149 self.allow_gnu = allow;
150 self
151 }
152
153 pub fn allow_all_nul_numeric_fields(mut self, allow: bool) -> Self {
161 self.allow_all_nul_numeric_fields = allow;
162 self
163 }
164
165 pub fn max_gnu_extension_size(mut self, max_gnu_extension_size: u64) -> Self {
173 self.max_gnu_extension_size = max_gnu_extension_size;
174 self
175 }
176
177 pub fn pax_policy(mut self, policy: PaxDecodePolicy) -> Self {
179 self.pax_policy = policy;
180 self
181 }
182
183 fn check_format(&self, position: u64, format: ArchiveFormat) -> Result<(), DecodeError> {
184 if format == ArchiveFormat::Gnu && !self.allow_gnu {
185 return Err(DecodeError::policy_violation(
186 position,
187 DecodePolicyViolation::GnuArchive,
188 ));
189 }
190 Ok(())
191 }
192
193 fn check_global_pax(&self, position: u64, records: &[PaxRecord]) -> Result<(), DecodeError> {
194 self.pax_policy.check_global_pax_extension(position)?;
195 self.pax_policy
196 .check_pax_records(position, PaxKind::Global, records)
197 }
198
199 fn check_member<R>(&self, frame: &MemberFrame<'_, R>) -> Result<(), DecodeError> {
200 if let MemberExtensions::Pax(state) = &frame.extensions {
201 for extension in state
202 .extensions()
203 .filter(|extension| extension.kind == PaxKind::Global)
204 {
205 self.check_global_pax(extension.position, extension.records())?;
206 }
207 }
208 let format_position = match &frame.extensions {
209 MemberExtensions::Pax(_) => frame.header.position,
210 MemberExtensions::Gnu {
211 long_name,
212 long_link,
213 } => long_name
214 .iter()
215 .chain(long_link.iter())
216 .map(|header| header.position)
217 .min()
218 .unwrap_or(frame.header.position),
219 };
220 self.check_format(format_position, frame.header.format)?;
221 if let MemberExtensions::Pax(state) = &frame.extensions {
222 for extension in state
223 .extensions()
224 .filter(|extension| extension.kind == PaxKind::Local)
225 {
226 self.pax_policy.check_pax_records(
227 extension.position,
228 PaxKind::Local,
229 extension.records(),
230 )?;
231 }
232 }
233 Ok(())
234 }
235}
236
237impl PaxDecodePolicy {
238 pub fn max_extension_size(mut self, max_extension_size: u64) -> Self {
249 self.max_extension_size = max_extension_size;
250 self
251 }
252
253 pub fn max_global_extensions_size(mut self, max_global_extensions_size: u64) -> Self {
263 self.max_global_extensions_size = max_global_extensions_size;
264 self
265 }
266
267 pub fn allow_non_utf8_pax_vendor_values(mut self, allow: bool) -> Self {
277 self.allow_non_utf8_pax_vendor_values = allow;
278 self
279 }
280
281 pub fn allow_global_pax_extensions(mut self, allow: bool) -> Self {
290 self.allow_global_pax_extensions = allow;
291 self
292 }
293
294 pub fn vendor_extension_policy(mut self, policy: PaxVendorExtensionPolicy) -> Self {
311 self.vendor_extension_policy = policy;
312 self
313 }
314
315 pub fn allow_duplicate_pax_records(mut self, allow: bool) -> Self {
322 self.allow_duplicate_pax_records = allow;
323 self
324 }
325
326 pub fn allow_global_pax_member_metadata(mut self, allow: bool) -> Self {
334 self.allow_global_pax_member_metadata = allow;
335 self
336 }
337
338 fn check_global_pax_extension(&self, position: u64) -> Result<(), DecodeError> {
339 if !self.allow_global_pax_extensions {
340 return Err(DecodeError::policy_violation(
341 position,
342 DecodePolicyViolation::GlobalPaxExtension,
343 ));
344 }
345 Ok(())
346 }
347
348 fn check_pax_records(
349 &self,
350 position: u64,
351 kind: PaxKind,
352 records: &[PaxRecord],
353 ) -> Result<(), DecodeError> {
354 for record in records {
355 if let PaxRecord::Vendor {
356 vendor,
357 name,
358 value,
359 } = record
360 {
361 let allowed = match &self.vendor_extension_policy {
362 PaxVendorExtensionPolicy::RejectUnknown => false,
363 PaxVendorExtensionPolicy::Ignore(allowed) => {
364 allowed.vendors.contains(vendor.as_ref())
365 }
366 PaxVendorExtensionPolicy::AllowUnknown => true,
367 };
368 if !allowed {
369 return Err(DecodeError::policy_violation(
370 position,
371 DecodePolicyViolation::PaxVendorExtension {
372 vendor: vendor.to_string(),
373 name: name.to_string(),
374 },
375 ));
376 }
377
378 if !self.allow_non_utf8_pax_vendor_values
379 && let PaxValue::Value(value) = value
380 && std::str::from_utf8(value).is_err()
381 {
382 return Err(DecodeError::policy_violation(
383 position,
384 DecodePolicyViolation::NonUtf8PaxVendorValue {
385 vendor: vendor.to_string(),
386 name: name.to_string(),
387 },
388 ));
389 }
390 }
391 }
392
393 if kind == PaxKind::Global && !self.allow_global_pax_member_metadata {
394 for record in records {
395 let keyword = match record.keyword() {
396 PaxKeyword::Path => Some("path"),
397 PaxKeyword::LinkPath => Some("linkpath"),
398 PaxKeyword::Size => Some("size"),
399 _ => None,
400 };
401 if let Some(keyword) = keyword {
402 return Err(DecodeError::policy_violation(
403 position,
404 DecodePolicyViolation::GlobalPaxMemberMetadata { keyword },
405 ));
406 }
407 }
408 }
409
410 if !self.allow_duplicate_pax_records {
411 let mut keywords = HashSet::new();
412 for record in records {
413 let keyword = record.keyword();
414 if !keywords.insert(keyword.clone()) {
415 return Err(DecodeError::policy_violation(
416 position,
417 DecodePolicyViolation::DuplicatePaxRecord {
418 keyword: keyword.to_string(),
419 },
420 ));
421 }
422 }
423 }
424
425 Ok(())
426 }
427}
428
429#[derive(Clone, Debug, Eq, PartialEq, Error)]
431pub enum DecodePolicyViolation {
432 #[error("GNU archives are not allowed")]
434 GnuArchive,
435 #[error("global pax extended headers are not allowed")]
437 GlobalPaxExtension,
438 #[error("pax vendor extension {vendor}.{name} is not allowed")]
440 PaxVendorExtension {
441 vendor: String,
443 name: String,
445 },
446 #[error("pax vendor extension {vendor}.{name} contains a non-UTF-8 value")]
448 NonUtf8PaxVendorValue {
449 vendor: String,
451 name: String,
453 },
454 #[error("pax extended header contains duplicate record {keyword}")]
456 DuplicatePaxRecord {
457 keyword: String,
459 },
460 #[error("global pax extended header contains restricted member metadata {keyword}")]
462 GlobalPaxMemberMetadata {
463 keyword: &'static str,
465 },
466}
467
468#[derive(Debug, Error)]
470pub enum DecodeError {
471 #[error(transparent)]
473 Framing(#[from] FrameError),
474 #[error("at byte {position}: {field} is not valid UTF-8")]
476 InvalidUtf8 {
477 position: u64,
479 field: &'static str,
481 },
482 #[error("at byte {position}: decode policy rejected input: {violation}")]
484 PolicyViolation {
485 position: u64,
487 violation: DecodePolicyViolation,
489 },
490}
491
492impl DecodeError {
493 fn policy_violation(position: u64, violation: DecodePolicyViolation) -> Self {
494 Self::PolicyViolation {
495 position,
496 violation,
497 }
498 }
499}
500
501pub struct TarMemberPayload<'a, R> {
503 payload: FramingMemberPayload<'a, R>,
504}
505
506impl<R: AsyncRead + Unpin> MemberPayloadTrait for TarMemberPayload<'_, R> {
507 type Error = DecodeError;
508
509 async fn next_chunk(
510 &mut self,
511 buffer: &mut Vec<u8>,
512 target_len: usize,
513 ) -> Result<bool, Self::Error> {
514 self.payload
515 .next_chunk(buffer, target_len)
516 .await
517 .map_err(Into::into)
518 }
519
520 async fn skip(self) -> Result<(), Self::Error> {
521 self.payload.skip().await.map_err(Into::into)
522 }
523}
524
525impl<R: AsyncRead + Unpin> ArchiveTrait for TarArchive<R> {
526 type Error = DecodeError;
527 type Payload<'a>
528 = TarMemberPayload<'a, R>
529 where
530 Self: 'a;
531
532 async fn next_member<'a>(
533 &'a mut self,
534 ) -> Result<Option<Member<Self::Payload<'a>>>, Self::Error> {
535 if self.fused {
536 return Ok(None);
537 }
538
539 let frame = match self.reader.next_frame().await {
540 Ok(Some(frame)) => frame,
541 Ok(None) => {
542 self.fused = true;
543 return Ok(None);
544 }
545 Err(error) => {
546 self.fused = true;
547 return Err(error.into());
548 }
549 };
550
551 if let Err(error) = self.policy.check_member(&frame) {
552 self.fused = true;
553 return Err(error);
554 }
555
556 match project_member(frame) {
557 Ok(member) => Ok(Some(member)),
558 Err(error) => {
559 self.fused = true;
560 Err(error)
561 }
562 }
563 }
564}
565
566fn project_member<'a, R>(
567 frame: MemberFrame<'a, R>,
568) -> Result<Member<TarMemberPayload<'a, R>>, DecodeError> {
569 let position = frame.header.position;
570 let kind = frame.header.kind;
571 let size = frame.header.effective_size;
572 let executable = frame.header.mode.unwrap_or_default() & 0o111 != 0;
573 let path = std::str::from_utf8(frame.effective_path()?.as_ref())
574 .map(str::to_owned)
575 .map_err(|_| DecodeError::InvalidUtf8 {
576 position,
577 field: "path",
578 })?;
579 let target = if matches!(kind, UstarKind::HardLink | UstarKind::SymbolicLink) {
580 std::str::from_utf8(frame.effective_link_path()?.as_ref())
581 .map(str::to_owned)
582 .map_err(|_| DecodeError::InvalidUtf8 {
583 position,
584 field: "linkpath",
585 })?
586 } else {
587 String::new()
588 };
589 let metadata = MemberMetadata { path, position };
590
591 Ok(match kind {
592 UstarKind::Regular | UstarKind::Contiguous => Member::File {
593 metadata,
594 size,
595 executable,
596 payload: TarMemberPayload {
597 payload: frame.payload,
598 },
599 },
600 UstarKind::Directory => Member::Directory { metadata },
601 UstarKind::SymbolicLink => Member::SymbolicLink { metadata, target },
602 UstarKind::HardLink => Member::HardLink {
603 metadata,
604 target,
605 size,
606 payload: TarMemberPayload {
607 payload: frame.payload,
608 },
609 },
610 UstarKind::CharacterDevice => Member::Special {
611 metadata,
612 kind: SpecialKind::CharacterDevice,
613 },
614 UstarKind::BlockDevice => Member::Special {
615 metadata,
616 kind: SpecialKind::BlockDevice,
617 },
618 UstarKind::Fifo => Member::Special {
619 metadata,
620 kind: SpecialKind::Fifo,
621 },
622 })
623}