Skip to content

Commit 32a9bbb

Browse files
authored
tests: Add RandomReader to exercise partial-read resilience (#436)
Wrap in-memory archive readers in tests with a RandomReader that returns 1..=N bytes per read() call using a deterministic seeded RNG. This ensures the tar parser correctly handles partial reads rather than assuming read() fills the entire buffer. Inspired by tar-core's test utilities. Assisted-by: OpenCode (Claude claude-opus-4-6)
1 parent 9c5df0b commit 32a9bbb

2 files changed

Lines changed: 85 additions & 32 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ contents are never required to be entirely resident in memory all at once.
2323
filetime = "0.2.8"
2424

2525
[dev-dependencies]
26+
rand = { version = "0.8", features = ["small_rng"] }
2627
tempfile = "3"
2728

2829
[target."cfg(unix)".dependencies]

tests/all.rs

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,60 @@ use std::iter::repeat;
1111
use std::path::{Path, PathBuf};
1212

1313
use filetime::FileTime;
14+
use rand::rngs::SmallRng;
15+
use rand::{Rng, SeedableRng};
1416
use tar::{Archive, Builder, Entries, Entry, EntryType, Header, HeaderMode};
1517
use tempfile::{Builder as TempBuilder, TempDir};
1618

19+
/// A reader wrapper that returns partial results from `read()` to exercise
20+
/// parsers that might assume `read()` fills the entire buffer.
21+
///
22+
/// Each call returns between 1 and buf.len() bytes, biased toward small
23+
/// reads by taking the minimum of two uniform samples. This gives roughly
24+
/// quadratic density toward 1, so small reads (1-10 bytes) occur frequently
25+
/// while large reads still happen. Uses a deterministic seeded RNG so
26+
/// tests remain reproducible.
27+
struct RandomReader<R> {
28+
inner: R,
29+
rng: SmallRng,
30+
}
31+
32+
impl<R> RandomReader<R> {
33+
fn new(inner: R) -> Self {
34+
RandomReader {
35+
inner,
36+
rng: SmallRng::seed_from_u64(0),
37+
}
38+
}
39+
}
40+
41+
impl<R: Read> Read for RandomReader<R> {
42+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
43+
if buf.is_empty() {
44+
return self.inner.read(buf);
45+
}
46+
// Take the min of two uniform samples to bias toward small reads.
47+
let a = self.rng.gen_range(1..=buf.len());
48+
let b = self.rng.gen_range(1..=buf.len());
49+
self.inner.read(&mut buf[..a.min(b)])
50+
}
51+
}
52+
53+
/// Convenience: wrap a byte slice in a RandomReader<Cursor<_>>.
54+
///
55+
/// The RNG is seeded from a hash of the data, so different archives
56+
/// exercise different read-size sequences while remaining deterministic.
57+
fn random_cursor_reader<D: AsRef<[u8]>>(data: D) -> RandomReader<Cursor<D>> {
58+
use std::hash::{Hash, Hasher};
59+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
60+
data.as_ref().hash(&mut hasher);
61+
let seed = hasher.finish();
62+
RandomReader {
63+
inner: Cursor::new(data),
64+
rng: SmallRng::seed_from_u64(seed),
65+
}
66+
}
67+
1768
macro_rules! tar {
1869
($e:expr) => {
1970
&include_bytes!(concat!("archives/", $e))[..]
@@ -30,14 +81,15 @@ fn simple_concat() {
3081
let mut archive_bytes = Vec::new();
3182
archive_bytes.extend(bytes);
3283

33-
let original_names: Vec<String> = decode_names(&mut Archive::new(Cursor::new(&archive_bytes)));
84+
let original_names: Vec<String> =
85+
decode_names(&mut Archive::new(random_cursor_reader(&archive_bytes)));
3486
let expected: Vec<&str> = original_names.iter().map(|n| n.as_str()).collect();
3587

3688
// concat two archives (with null in-between);
3789
archive_bytes.extend(bytes);
3890

3991
// test now that when we read the archive, it stops processing at the first zero header.
40-
let actual = decode_names(&mut Archive::new(Cursor::new(&archive_bytes)));
92+
let actual = decode_names(&mut Archive::new(random_cursor_reader(&archive_bytes)));
4193
assert_eq!(expected, actual);
4294

4395
// extend expected by itself.
@@ -48,7 +100,7 @@ fn simple_concat() {
48100
o
49101
};
50102

51-
let mut ar = Archive::new(Cursor::new(&archive_bytes));
103+
let mut ar = Archive::new(random_cursor_reader(&archive_bytes));
52104
ar.set_ignore_zeros(true);
53105

54106
let actual = decode_names(&mut ar);
@@ -71,7 +123,7 @@ fn simple_concat() {
71123

72124
#[test]
73125
fn header_impls() {
74-
let mut ar = Archive::new(Cursor::new(tar!("simple.tar")));
126+
let mut ar = Archive::new(random_cursor_reader(tar!("simple.tar")));
75127
let hn = Header::new_old();
76128
let hnb = hn.as_bytes();
77129
for file in ar.entries().unwrap() {
@@ -86,7 +138,7 @@ fn header_impls() {
86138

87139
#[test]
88140
fn header_impls_missing_last_header() {
89-
let mut ar = Archive::new(Cursor::new(tar!("simple_missing_last_header.tar")));
141+
let mut ar = Archive::new(random_cursor_reader(tar!("simple_missing_last_header.tar")));
90142
let hn = Header::new_old();
91143
let hnb = hn.as_bytes();
92144
for file in ar.entries().unwrap() {
@@ -101,7 +153,7 @@ fn header_impls_missing_last_header() {
101153

102154
#[test]
103155
fn reading_files() {
104-
let rdr = Cursor::new(tar!("reading_files.tar"));
156+
let rdr = random_cursor_reader(tar!("reading_files.tar"));
105157
let mut ar = Archive::new(rdr);
106158
let mut entries = ar.entries().unwrap();
107159

@@ -247,7 +299,7 @@ fn reading_entries_common<R: Read>(mut entries: Entries<R>) {
247299

248300
#[test]
249301
fn reading_entries() {
250-
let rdr = Cursor::new(tar!("reading_files.tar"));
302+
let rdr = random_cursor_reader(tar!("reading_files.tar"));
251303
let mut ar = Archive::new(rdr);
252304
reading_entries_common(ar.entries().unwrap());
253305
}
@@ -321,7 +373,7 @@ fn check_dirtree(td: &TempDir) {
321373
#[test]
322374
fn extracting_directories() {
323375
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
324-
let rdr = Cursor::new(tar!("directory.tar"));
376+
let rdr = random_cursor_reader(tar!("directory.tar"));
325377
let mut ar = Archive::new(rdr);
326378
ar.unpack(td.path()).unwrap();
327379
check_dirtree(&td);
@@ -333,7 +385,7 @@ fn extracting_duplicate_file_fail() {
333385
let path_present = td.path().join("a");
334386
File::create(path_present).unwrap();
335387

336-
let rdr = Cursor::new(tar!("reading_files.tar"));
388+
let rdr = random_cursor_reader(tar!("reading_files.tar"));
337389
let mut ar = Archive::new(rdr);
338390
ar.set_overwrite(false);
339391
if let Err(err) = ar.unpack(td.path()) {
@@ -355,7 +407,7 @@ fn extracting_duplicate_file_succeed() {
355407
let path_present = td.path().join("a");
356408
File::create(path_present).unwrap();
357409

358-
let rdr = Cursor::new(tar!("reading_files.tar"));
410+
let rdr = random_cursor_reader(tar!("reading_files.tar"));
359411
let mut ar = Archive::new(rdr);
360412
ar.set_overwrite(true);
361413
ar.unpack(td.path()).unwrap();
@@ -368,7 +420,7 @@ fn extracting_duplicate_link_fail() {
368420
let path_present = td.path().join("lnk");
369421
std::os::unix::fs::symlink("file", path_present).unwrap();
370422

371-
let rdr = Cursor::new(tar!("link.tar"));
423+
let rdr = random_cursor_reader(tar!("link.tar"));
372424
let mut ar = Archive::new(rdr);
373425
ar.set_overwrite(false);
374426
if let Err(err) = ar.unpack(td.path()) {
@@ -391,7 +443,7 @@ fn extracting_duplicate_link_succeed() {
391443
let path_present = td.path().join("lnk");
392444
std::os::unix::fs::symlink("file", path_present).unwrap();
393445

394-
let rdr = Cursor::new(tar!("link.tar"));
446+
let rdr = random_cursor_reader(tar!("link.tar"));
395447
let mut ar = Archive::new(rdr);
396448
ar.set_overwrite(true);
397449
ar.unpack(td.path()).unwrap();
@@ -406,7 +458,7 @@ fn xattrs() {
406458
.prefix("tar-rs")
407459
.tempdir_in("/var/tmp")
408460
.unwrap();
409-
let rdr = Cursor::new(tar!("xattrs.tar"));
461+
let rdr = random_cursor_reader(tar!("xattrs.tar"));
410462
let mut ar = Archive::new(rdr);
411463
ar.set_unpack_xattrs(true);
412464
ar.unpack(td.path()).unwrap();
@@ -424,7 +476,7 @@ fn no_xattrs() {
424476
.prefix("tar-rs")
425477
.tempdir_in("/var/tmp")
426478
.unwrap();
427-
let rdr = Cursor::new(tar!("xattrs.tar"));
479+
let rdr = random_cursor_reader(tar!("xattrs.tar"));
428480
let mut ar = Archive::new(rdr);
429481
ar.set_unpack_xattrs(false);
430482
ar.unpack(td.path()).unwrap();
@@ -580,7 +632,7 @@ fn append_dir_all_does_not_work_on_non_directory() {
580632
#[test]
581633
fn extracting_duplicate_dirs() {
582634
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
583-
let rdr = Cursor::new(tar!("duplicate_dirs.tar"));
635+
let rdr = random_cursor_reader(tar!("duplicate_dirs.tar"));
584636
let mut ar = Archive::new(rdr);
585637
ar.unpack(td.path()).unwrap();
586638

@@ -738,7 +790,7 @@ fn extracting_malicious_tarball() {
738790

739791
#[test]
740792
fn octal_spaces() {
741-
let rdr = Cursor::new(tar!("spaces.tar"));
793+
let rdr = random_cursor_reader(tar!("spaces.tar"));
742794
let mut ar = Archive::new(rdr);
743795

744796
let entry = ar.entries().unwrap().next().unwrap().unwrap();
@@ -778,15 +830,15 @@ fn extracting_malformed_tar_null_blocks() {
778830
#[test]
779831
fn empty_filename() {
780832
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
781-
let rdr = Cursor::new(tar!("empty_filename.tar"));
833+
let rdr = random_cursor_reader(tar!("empty_filename.tar"));
782834
let mut ar = Archive::new(rdr);
783835
assert!(ar.unpack(td.path()).is_ok());
784836
}
785837

786838
#[test]
787839
fn file_times() {
788840
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
789-
let rdr = Cursor::new(tar!("file_times.tar"));
841+
let rdr = random_cursor_reader(tar!("file_times.tar"));
790842
let mut ar = Archive::new(rdr);
791843
ar.unpack(td.path()).unwrap();
792844

@@ -857,7 +909,7 @@ fn backslash_treated_well() {
857909
#[test]
858910
#[cfg(unix)]
859911
fn set_mask() {
860-
use ::std::os::unix::fs::PermissionsExt;
912+
use std::os::unix::fs::PermissionsExt;
861913
let mut ar = tar::Builder::new(Vec::new());
862914

863915
let mut header = tar::Header::new_gnu();
@@ -903,7 +955,7 @@ fn nul_bytes_in_path() {
903955

904956
#[test]
905957
fn links() {
906-
let mut ar = Archive::new(Cursor::new(tar!("link.tar")));
958+
let mut ar = Archive::new(random_cursor_reader(tar!("link.tar")));
907959
let mut entries = ar.entries().unwrap();
908960
let link = entries.next().unwrap().unwrap();
909961
assert_eq!(
@@ -918,7 +970,7 @@ fn links() {
918970
#[cfg(unix)] // making symlinks on windows is hard
919971
fn unpack_links() {
920972
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
921-
let mut ar = Archive::new(Cursor::new(tar!("link.tar")));
973+
let mut ar = Archive::new(random_cursor_reader(tar!("link.tar")));
922974
ar.unpack(td.path()).unwrap();
923975

924976
let md = fs::symlink_metadata(td.path().join("lnk")).unwrap();
@@ -936,7 +988,7 @@ fn unpack_links() {
936988

937989
#[test]
938990
fn pax_size() {
939-
let mut ar = Archive::new(tar!("pax_size.tar"));
991+
let mut ar = Archive::new(random_cursor_reader(tar!("pax_size.tar")));
940992
let mut entries = ar.entries().unwrap();
941993
let mut entry = entries.next().unwrap().unwrap();
942994
let mut attributes = entry.pax_extensions().unwrap().unwrap();
@@ -956,7 +1008,7 @@ fn pax_size() {
9561008

9571009
#[test]
9581010
fn pax_simple() {
959-
let mut ar = Archive::new(tar!("pax.tar"));
1011+
let mut ar = Archive::new(random_cursor_reader(tar!("pax.tar")));
9601012
let mut entries = ar.entries().unwrap();
9611013

9621014
let mut first = entries.next().unwrap().unwrap();
@@ -1011,7 +1063,7 @@ fn pax_simple_write() {
10111063

10121064
#[test]
10131065
fn pax_path() {
1014-
let mut ar = Archive::new(tar!("pax2.tar"));
1066+
let mut ar = Archive::new(random_cursor_reader(tar!("pax2.tar")));
10151067
let mut entries = ar.entries().unwrap();
10161068

10171069
let first = entries.next().unwrap().unwrap();
@@ -1020,7 +1072,7 @@ fn pax_path() {
10201072

10211073
#[test]
10221074
fn pax_linkpath() {
1023-
let mut ar = Archive::new(tar!("pax2.tar"));
1075+
let mut ar = Archive::new(random_cursor_reader(tar!("pax2.tar")));
10241076
let mut links = ar.entries().unwrap().skip(3).take(2);
10251077

10261078
let long_symlink = links.next().unwrap().unwrap();
@@ -1192,7 +1244,7 @@ fn encoded_long_name_has_trailing_nul() {
11921244

11931245
#[test]
11941246
fn reading_sparse() {
1195-
let rdr = Cursor::new(tar!("sparse.tar"));
1247+
let rdr = random_cursor_reader(tar!("sparse.tar"));
11961248
let mut ar = Archive::new(rdr);
11971249
let mut entries = ar.entries().unwrap();
11981250

@@ -1242,7 +1294,7 @@ fn reading_sparse() {
12421294

12431295
#[test]
12441296
fn extract_sparse() {
1245-
let rdr = Cursor::new(tar!("sparse.tar"));
1297+
let rdr = random_cursor_reader(tar!("sparse.tar"));
12461298
let mut ar = Archive::new(rdr);
12471299
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
12481300
ar.unpack(td.path()).unwrap();
@@ -1295,7 +1347,7 @@ fn extract_sparse() {
12951347

12961348
#[test]
12971349
fn large_sparse() {
1298-
let rdr = Cursor::new(tar!("sparse-large.tar"));
1350+
let rdr = random_cursor_reader(tar!("sparse-large.tar"));
12991351
let mut ar = Archive::new(rdr);
13001352
let mut entries = ar.entries().unwrap();
13011353
// Only check the header info without extracting, as the file is very large,
@@ -1307,7 +1359,7 @@ fn large_sparse() {
13071359

13081360
#[test]
13091361
fn sparse_with_trailing() {
1310-
let rdr = Cursor::new(tar!("sparse-1.tar"));
1362+
let rdr = random_cursor_reader(tar!("sparse-1.tar"));
13111363
let mut ar = Archive::new(rdr);
13121364
let mut entries = ar.entries().unwrap();
13131365
let mut a = entries.next().unwrap().unwrap();
@@ -1556,7 +1608,7 @@ fn tar_directory_containing_symlink_to_directory() {
15561608
#[test]
15571609
fn long_path() {
15581610
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
1559-
let rdr = Cursor::new(tar!("7z_long_path.tar"));
1611+
let rdr = random_cursor_reader(tar!("7z_long_path.tar"));
15601612
let mut ar = Archive::new(rdr);
15611613
assert!(ar.unpack(td.path()).is_ok());
15621614
}
@@ -1571,7 +1623,7 @@ fn unpack_path_larger_than_windows_max_path() {
15711623
.tempdir()
15721624
.unwrap();
15731625
// directory in 7z_long_path.tar is over 100 chars
1574-
let rdr = Cursor::new(tar!("7z_long_path.tar"));
1626+
let rdr = random_cursor_reader(tar!("7z_long_path.tar"));
15751627
let mut ar = Archive::new(rdr);
15761628
// should unpack path greater than windows MAX_PATH length of 260 characters
15771629
assert!(ar.unpack(td.path()).is_ok());
@@ -1768,7 +1820,7 @@ fn pax_and_gnu_uid_gid() {
17681820

17691821
for file in &tarlist {
17701822
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
1771-
let rdr = Cursor::new(file);
1823+
let rdr = random_cursor_reader(file);
17721824
let mut ar = Archive::new(rdr);
17731825
ar.set_preserve_ownerships(true);
17741826

0 commit comments

Comments
 (0)