Skip to content

Commit ed97396

Browse files
committed
parse: Remove Limits::strict(), move error leniency to Parser
The strict/lenient toggle for PAX parse errors was a parser behavior flag, not a resource limit — it was misplaced on Limits. Move it to Parser as set_ignore_parsing_errors(bool), matching the pattern of set_allow_empty_path and set_verify_checksums. Remove Limits::strict() entirely. The defaults are already safe for untrusted input (1 MiB metadata cap, bounded pending/sparse counts). Callers who want tighter resource limits can set fields directly; callers who want lenient PAX parsing call set_ignore_parsing_errors. Assisted-by: OpenCode (Claude claude-opus-4-6)
1 parent 1f04dc6 commit ed97396

1 file changed

Lines changed: 51 additions & 61 deletions

File tree

src/parse.rs

Lines changed: 51 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ use crate::{
8181
/// let limits = Limits::default();
8282
///
8383
/// // Customize limits
84-
/// let strict_limits = Limits {
84+
/// let limits = Limits {
8585
/// max_metadata_size: 64 * 1024,
8686
/// // Set to libc::PATH_MAX when extracting to disk
8787
/// max_path_len: Some(4096),
@@ -133,13 +133,6 @@ pub struct Limits {
133133
///
134134
/// Default: 10000.
135135
pub max_sparse_entries: usize,
136-
137-
/// When true, PAX extension values that fail to parse (invalid UTF-8,
138-
/// invalid integer for numeric fields like `uid`, `gid`, `size`, `mtime`)
139-
/// cause errors instead of being silently ignored.
140-
///
141-
/// Default: `true`.
142-
pub strict: bool,
143136
}
144137

145138
impl Default for Limits {
@@ -149,7 +142,6 @@ impl Default for Limits {
149142
max_path_len: None,
150143
max_pending_entries: 16,
151144
max_sparse_entries: 10_000,
152-
strict: true,
153145
}
154146
}
155147
}
@@ -172,22 +164,6 @@ impl Limits {
172164
max_path_len: None,
173165
max_pending_entries: usize::MAX,
174166
max_sparse_entries: 1_000_000,
175-
strict: false,
176-
}
177-
}
178-
179-
/// Create strict limits suitable for untrusted archives.
180-
///
181-
/// This sets conservative limits to minimize resource consumption
182-
/// from potentially malicious archives.
183-
#[must_use]
184-
pub fn strict() -> Self {
185-
Self {
186-
max_metadata_size: 64 * 1024, // 64 KiB
187-
max_path_len: Some(4096),
188-
max_pending_entries: 8,
189-
max_sparse_entries: 1000,
190-
strict: true,
191167
}
192168
}
193169

@@ -292,7 +268,7 @@ pub enum ParseError {
292268
#[error("invalid PAX sparse map: {0}")]
293269
InvalidPaxSparseMap(Cow<'static, str>),
294270

295-
/// A PAX extension value failed to parse in strict mode.
271+
/// A PAX extension value failed to parse.
296272
#[error("invalid PAX {key} value: {value:?}")]
297273
InvalidPaxValue {
298274
/// The PAX key (e.g. "uid", "size").
@@ -605,23 +581,23 @@ impl PendingMetadata<'_> {
605581
///
606582
/// Returns `Some((major, minor))` if `GNU.sparse.major` and
607583
/// `GNU.sparse.minor` are both present and parseable, `None` if
608-
/// the keys are absent. In strict mode, malformed values produce
609-
/// errors instead of being silently ignored.
610-
fn pax_sparse_version(pax: &[u8], strict: bool) -> Result<Option<(u64, u64)>> {
584+
/// the keys are absent. When `ignore_errors` is true, malformed values
585+
/// are silently skipped instead of producing errors.
586+
fn pax_sparse_version(pax: &[u8], ignore_errors: bool) -> Result<Option<(u64, u64)>> {
611587
let mut major = None;
612588
let mut minor = None;
613589
for ext in PaxExtensions::new(pax) {
614590
let ext = ext?;
615591
let key = match ext.key() {
616592
Ok(k) => k,
617-
Err(_) if !strict => continue,
593+
Err(_) if ignore_errors => continue,
618594
Err(e) => return Err(ParseError::from(e)),
619595
};
620596
match key {
621597
PAX_GNU_SPARSE_MAJOR => {
622598
let s = match ext.value() {
623599
Ok(s) => s,
624-
Err(_) if !strict => continue,
600+
Err(_) if ignore_errors => continue,
625601
Err(_) => {
626602
return Err(ParseError::InvalidPaxValue {
627603
key: PAX_GNU_SPARSE_MAJOR,
@@ -631,7 +607,7 @@ fn pax_sparse_version(pax: &[u8], strict: bool) -> Result<Option<(u64, u64)>> {
631607
};
632608
match s.parse::<u64>() {
633609
Ok(v) => major = Some(v),
634-
Err(_) if !strict => {}
610+
Err(_) if ignore_errors => {}
635611
Err(_) => {
636612
return Err(ParseError::InvalidPaxValue {
637613
key: PAX_GNU_SPARSE_MAJOR,
@@ -643,7 +619,7 @@ fn pax_sparse_version(pax: &[u8], strict: bool) -> Result<Option<(u64, u64)>> {
643619
PAX_GNU_SPARSE_MINOR => {
644620
let s = match ext.value() {
645621
Ok(s) => s,
646-
Err(_) if !strict => continue,
622+
Err(_) if ignore_errors => continue,
647623
Err(_) => {
648624
return Err(ParseError::InvalidPaxValue {
649625
key: PAX_GNU_SPARSE_MINOR,
@@ -653,7 +629,7 @@ fn pax_sparse_version(pax: &[u8], strict: bool) -> Result<Option<(u64, u64)>> {
653629
};
654630
match s.parse::<u64>() {
655631
Ok(v) => minor = Some(v),
656-
Err(_) if !strict => {}
632+
Err(_) if ignore_errors => {}
657633
Err(_) => {
658634
return Err(ParseError::InvalidPaxValue {
659635
key: PAX_GNU_SPARSE_MINOR,
@@ -726,6 +702,13 @@ pub struct Parser {
726702
///
727703
/// Default: `true`.
728704
verify_checksums: bool,
705+
/// When true, malformed PAX extension values (invalid UTF-8, unparseable
706+
/// integers for uid/gid/size/mtime) are silently skipped instead of
707+
/// producing errors. This matches the behavior of many real-world tar
708+
/// implementations.
709+
///
710+
/// Default: `false`.
711+
ignore_pax_errors: bool,
729712
}
730713

731714
impl Parser {
@@ -737,6 +720,7 @@ impl Parser {
737720
state: State::ReadHeader,
738721
allow_empty_path: false,
739722
verify_checksums: true,
723+
ignore_pax_errors: false,
740724
}
741725
}
742726

@@ -759,6 +743,18 @@ impl Parser {
759743
self.verify_checksums = verify;
760744
}
761745

746+
/// Control whether malformed PAX extension values are silently ignored.
747+
///
748+
/// When set to `true`, PAX values that fail to parse (invalid UTF-8,
749+
/// unparseable integers for `uid`, `gid`, `size`, `mtime`) are skipped
750+
/// instead of producing [`ParseError::InvalidPaxValue`] errors. This
751+
/// matches the lenient behavior of many real-world tar implementations.
752+
///
753+
/// Default: `false` (malformed values produce errors).
754+
pub fn set_ignore_pax_errors(&mut self, ignore: bool) {
755+
self.ignore_pax_errors = ignore;
756+
}
757+
762758
/// Create a new parser with default limits.
763759
#[must_use]
764760
pub fn with_defaults() -> Self {
@@ -919,7 +915,7 @@ impl Parser {
919915
// Check for PAX v1.0 sparse before emitting — it requires
920916
// reading the sparse map from the data stream.
921917
let sparse_version = if let Some(pax) = slices.pax_extensions {
922-
pax_sparse_version(pax, self.limits.strict)?
918+
pax_sparse_version(pax, self.ignore_pax_errors)?
923919
} else {
924920
None
925921
};
@@ -1040,21 +1036,21 @@ impl Parser {
10401036
"missing PAX extensions",
10411037
)))?;
10421038

1043-
let strict = self.limits.strict;
1039+
let ignore_errors = self.ignore_pax_errors;
10441040
let mut real_size = None;
10451041
let mut sparse_name = None;
10461042
for ext in PaxExtensions::new(pax) {
10471043
let ext = ext?;
10481044
let key = match ext.key() {
10491045
Ok(k) => k,
1050-
Err(_) if !strict => continue,
1046+
Err(_) if ignore_errors => continue,
10511047
Err(e) => return Err(ParseError::from(e)),
10521048
};
10531049
match key {
10541050
PAX_GNU_SPARSE_REALSIZE | PAX_GNU_SPARSE_SIZE => {
10551051
let s = match ext.value() {
10561052
Ok(s) => s,
1057-
Err(_) if !strict => continue,
1053+
Err(_) if ignore_errors => continue,
10581054
Err(_) => {
10591055
return Err(ParseError::InvalidPaxValue {
10601056
key: PAX_GNU_SPARSE_REALSIZE,
@@ -1064,7 +1060,7 @@ impl Parser {
10641060
};
10651061
match s.parse::<u64>() {
10661062
Ok(v) => real_size = Some(v),
1067-
Err(_) if !strict => {}
1063+
Err(_) if ignore_errors => {}
10681064
Err(_) => {
10691065
return Err(ParseError::InvalidPaxValue {
10701066
key: PAX_GNU_SPARSE_REALSIZE,
@@ -1346,16 +1342,16 @@ impl Parser {
13461342
let mut pax_sparse_pending_offset: Option<u64> = None;
13471343

13481344
if let Some(pax) = raw_pax {
1349-
let strict = self.limits.strict;
1345+
let ignore_errors = self.ignore_pax_errors;
13501346
let extensions = PaxExtensions::new(pax);
13511347

1352-
// Helper: parse a PAX numeric value, returning Err in strict mode
1353-
// or Ok(None) in lenient mode when the value is unparseable.
1348+
// Helper: parse a PAX numeric value, returning Ok(None) when
1349+
// ignore_pax_errors is set and the value is unparseable.
13541350
let parse_pax_u64 =
13551351
|ext: &crate::PaxExtension<'_>, key: &'static str| -> Result<Option<u64>> {
13561352
let s = match ext.value() {
13571353
Ok(s) => s,
1358-
Err(_) if !strict => return Ok(None),
1354+
Err(_) if ignore_errors => return Ok(None),
13591355
Err(_) => {
13601356
return Err(ParseError::InvalidPaxValue {
13611357
key,
@@ -1365,7 +1361,7 @@ impl Parser {
13651361
};
13661362
match s.parse::<u64>() {
13671363
Ok(v) => Ok(Some(v)),
1368-
Err(_) if !strict => Ok(None),
1364+
Err(_) if ignore_errors => Ok(None),
13691365
Err(_) => Err(ParseError::InvalidPaxValue {
13701366
key,
13711367
value: s.to_owned().into(),
@@ -1407,7 +1403,7 @@ impl Parser {
14071403
// parse only the integer part.
14081404
let s = match ext.value() {
14091405
Ok(s) => s,
1410-
Err(_) if !strict => continue,
1406+
Err(_) if ignore_errors => continue,
14111407
Err(_) => {
14121408
return Err(ParseError::InvalidPaxValue {
14131409
key: PAX_MTIME,
@@ -1418,7 +1414,7 @@ impl Parser {
14181414
let int_part = s.split('.').next().unwrap_or(s);
14191415
match int_part.parse::<u64>() {
14201416
Ok(v) => mtime = v,
1421-
Err(_) if !strict => {}
1417+
Err(_) if ignore_errors => {}
14221418
Err(_) => {
14231419
return Err(ParseError::InvalidPaxValue {
14241420
key: PAX_MTIME,
@@ -1459,7 +1455,7 @@ impl Parser {
14591455
PAX_GNU_SPARSE_MAP => {
14601456
let s = match ext.value() {
14611457
Ok(s) => s,
1462-
Err(_) if !strict => continue,
1458+
Err(_) if ignore_errors => continue,
14631459
Err(_) => {
14641460
return Err(ParseError::InvalidPaxSparseMap(Cow::Borrowed(
14651461
"non-UTF8 sparse map",
@@ -1614,10 +1610,10 @@ mod tests {
16141610
}
16151611

16161612
#[test]
1617-
fn test_strict_limits() {
1618-
let limits = Limits::strict();
1619-
assert_eq!(limits.max_path_len, Some(4096));
1620-
assert!(limits.max_metadata_size < Limits::default().max_metadata_size);
1613+
fn test_permissive_limits_relaxed() {
1614+
let limits = Limits::permissive();
1615+
assert!(limits.max_metadata_size > Limits::default().max_metadata_size);
1616+
assert!(limits.max_pending_entries > Limits::default().max_pending_entries);
16211617
}
16221618

16231619
#[test]
@@ -2861,11 +2857,8 @@ mod tests {
28612857
#[test]
28622858
fn test_lenient_ignores_invalid_pax_uid() {
28632859
let archive = make_archive_with_pax("uid", b"notanumber");
2864-
let limits = Limits {
2865-
strict: false,
2866-
..Default::default()
2867-
};
2868-
let mut parser = Parser::new(limits);
2860+
let mut parser = Parser::new(Limits::default());
2861+
parser.set_ignore_pax_errors(true);
28692862
let event = parser.parse(&archive).unwrap();
28702863
match event {
28712864
ParseEvent::Entry { entry, .. } => {
@@ -2879,11 +2872,8 @@ mod tests {
28792872
#[test]
28802873
fn test_lenient_ignores_invalid_pax_size() {
28812874
let archive = make_archive_with_pax("size", b"xyz");
2882-
let limits = Limits {
2883-
strict: false,
2884-
..Default::default()
2885-
};
2886-
let mut parser = Parser::new(limits);
2875+
let mut parser = Parser::new(Limits::default());
2876+
parser.set_ignore_pax_errors(true);
28872877
let event = parser.parse(&archive).unwrap();
28882878
match event {
28892879
ParseEvent::Entry { entry, .. } => {

0 commit comments

Comments
 (0)