Skip to content

Commit a4bf4fb

Browse files
committed
feat: finish par2cmdline parity
Add PAR2 creation support with packet generation, volume splitting, streaming recovery generation, and par2cmdline-compatible create options. Tighten verify/repair compatibility for base paths, data filename resolution, extra scan files, purge behavior, long flags, and uppercase PAR2 extensions. Expand integration and binary coverage, including par2cmdline-turbo compatibility cases, recovery limits, and create/repair parity scenarios.
1 parent 0b80a95 commit a4bf4fb

53 files changed

Lines changed: 10889 additions & 664 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ harness = false
6565
name = "verify_performance"
6666
harness = false
6767

68+
[[bench]]
69+
name = "create_benchmark"
70+
harness = false
71+
6872
[[bench]]
6973
name = "md5_throughput"
7074
harness = false
@@ -82,4 +86,4 @@ debug = true
8286
strip = false
8387
lto = true # Link-time optimization for better performance
8488
codegen-units = 1 # Single codegen unit for better optimization
85-
overflow-checks = true # Keep overflow checks for safety with untrusted network data
89+
overflow-checks = true # Keep overflow checks for safety with untrusted network data

README.md

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ A Rust implementation of PAR2 (Parity Archive) for data recovery and verificatio
88

99
### Performance
1010

11-
par2rs achieves **1.1-2.9x speedup** over par2cmdline through:
11+
#### Verification/Repair
12+
13+
par2rs achieves **1.1-2.9x speedup** over par2cmdline for verification and repair through:
1214
- **Optimized I/O patterns** using full slice-size chunks instead of 64KB blocks (eliminates redundant reads)
1315
- **Parallel Reed-Solomon reconstruction** using Rayon for multi-threaded chunk processing
1416
- **SIMD-accelerated operations** (PSHUFB on x86_64, NEON on ARM64, portable_simd cross-platform)
@@ -17,7 +19,7 @@ par2rs achieves **1.1-2.9x speedup** over par2cmdline through:
1719

1820
**⚠️ Performance Regression Note:** These results show significantly lower speedups than previous benchmarks (which showed 2-200× improvements). This is considered a **regression** and is under investigation. The current implementation maintains correctness but has lost most of its performance advantages on Linux x86_64.
1921

20-
**Latest benchmark results:**
22+
**Latest verification/repair benchmark results:**
2123

2224
**Linux x86_64 (AMD Ryzen 9 5950X, 64GB RAM):**
2325
- 1MB: **1.23x speedup** (0.032s → 0.026s)
@@ -84,7 +86,7 @@ par2 v myfile.par2 # short form
8486
par2 repair myfile.par2
8587
par2 r myfile.par2 # short form
8688

87-
# Create recovery files (coming soon)
89+
# Create recovery files
8890
par2 create myfile.par2 file1 file2
8991
par2 c myfile.par2 file1 file2 # short form
9092
```
@@ -101,6 +103,17 @@ par2 r -p myfile.par2
101103
# Use specific number of threads
102104
par2 v -t 8 myfile.par2
103105

106+
# Create with explicit recovery settings
107+
par2 c -s65536 -r10 myfile.par2 file1 file2
108+
109+
# Store source names relative to a base path
110+
par2 c -B /data/archive myfile.par2 /data/archive/file1
111+
par2 v -B /data/archive myfile.par2
112+
113+
# Scan renamed or relocated data while verifying/repairing
114+
par2 v myfile.par2 renamed-file
115+
par2 r myfile.par2 renamed-file
116+
104117
# Disable parallel processing (single-threaded)
105118
par2 v --no-parallel myfile.par2
106119
```
@@ -265,15 +278,29 @@ Verifies the integrity of files using PAR2 archives.
265278
**Features:**
266279
- Complete PAR2 set analysis
267280
- File integrity verification
281+
- Byte-by-byte block scanning for renamed or displaced data
268282
- Progress reporting
269283
- Detailed statistics
270284

271-
### par2create (Planned)
285+
### par2create
272286
Creates PAR2 recovery files for data protection.
273287

274-
### par2repair (Planned)
288+
**Features:**
289+
- par2cmdline-style create options for block size/count, redundancy, recovery volume layout, recursion, base paths, and quiet/verbose modes
290+
- PAR2 index and recovery volume generation
291+
- Reed-Solomon recovery block generation
292+
- Compatibility coverage against par2cmdline for generated sets
293+
294+
### par2repair
275295
Repairs corrupted files using PAR2 recovery data.
276296

297+
**Features:**
298+
- Recovery set loading from main and volume PAR2 files
299+
- Corrupt or missing file reconstruction
300+
- Base path support for relocated data files
301+
- Extra file scanning for renamed or relocated protected files
302+
- Optional purge of backup and PAR2 files after successful repair
303+
277304
### split_par2 (Utility)
278305
Development utility to split PAR2 files into individual packets for analysis.
279306

@@ -327,18 +354,18 @@ This implementation follows the PAR2 specification and supports:
327354

328355
### File Scanning Strategy
329356

330-
`par2rs` uses a **block-aligned sequential scanning** approach that differs from `par2cmdline`'s sliding window scanner:
357+
`par2rs` uses a **global block scanner** modeled after `par2cmdline`:
331358

332-
- **par2cmdline**: Uses a byte-by-byte sliding window with rolling CRC32 that can find blocks at *any offset* in a file, even if displaced by inserted/deleted data. This is more thorough but slower.
359+
- **Fast path**: Aligned blocks are checked first for the common case where files are present at their expected paths and offsets.
333360

334-
- **par2rs**: Only checks blocks at their expected aligned positions using sequential reads with large buffers (128MB). This is significantly faster for normal verification but cannot find displaced blocks.
361+
- **Compatibility path**: When needed, verification and repair scan byte-by-byte with rolling CRC32 to find protected data blocks at displaced offsets or inside extra files passed on the command line.
335362

336363
**Practical Impact:**
337-
-**par2rs is faster** for standard verification/repair scenarios (files are either intact or corrupted at known positions)
338-
- ⚠️ **par2cmdline is more robust** for edge cases like files with prepended data or non-aligned block corruption
339-
- 🎯 For typical use cases (bit rot, transmission errors, filesystem corruption), both tools will perform equivalently
364+
-Intact files still take the fast aligned path.
365+
- ✅ Renamed or relocated files can be supplied as extra arguments to `verify` or `repair`.
366+
- ✅ Displaced blocks from inserted or deleted bytes are detected by the byte-scanning path.
340367

341-
This design choice optimizes for the common case where files are either intact or have corruption at expected block boundaries, delivering substantial performance improvements while maintaining correctness for standard PAR2 operations.
368+
This keeps the common case efficient while matching `par2cmdline` behavior for the recovery cases where data is present but not at the protected filename or expected block offset.
342369

343370
## Known Issues
344371

@@ -347,11 +374,15 @@ This design choice optimizes for the common case where files are either intact o
347374
## Roadmap
348375

349376
- [x] **Phase 1**: Complete packet parsing and verification
350-
- [ ] **Phase 2**: PAR2 file creation (`par2create`)
377+
- [x] **Phase 2**: PAR2 file creation (`par2create`)
351378
- [x] **Phase 3**: File repair functionality (`par2repair`)
352379
- [x] **Phase 4**: SIMD optimizations (PSHUFB, NEON, portable_simd)
353380
- [ ] **Phase 5**: Runtime SIMD dispatch
354381
- [ ] **Phase 6**: Advanced features (progress callbacks, custom block sizes)
382+
- [ ] **Performance**: Investigate the Linux x86_64 verification/repair regression and restore prior benchmark speedups
383+
- [ ] **Create Optimization**: Merge hashing and recovery generation into a single pass to avoid reading source files twice
384+
- [ ] **Repair Reliability**: Reproduce and fix the repair hang on small files within large multi-file PAR2 sets
385+
- [ ] **Benchmarks**: Re-test and refresh macOS Apple Silicon results
355386

356387
## Documentation
357388

benches/create_benchmark.rs

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
2+
use par2rs::create::{CreateContextBuilder, SilentCreateReporter};
3+
use std::fs;
4+
use std::hint::black_box;
5+
use std::path::PathBuf;
6+
use tempfile::tempdir;
7+
8+
fn create_test_file(size: usize) -> (tempfile::TempDir, PathBuf) {
9+
let temp_dir = tempdir().unwrap();
10+
let file_path = temp_dir.path().join("test.dat");
11+
12+
// Create file with pattern to avoid compression optimizations
13+
let pattern = (0..256)
14+
.cycle()
15+
.take(size)
16+
.map(|i| i as u8)
17+
.collect::<Vec<_>>();
18+
fs::write(&file_path, pattern).unwrap();
19+
20+
(temp_dir, file_path)
21+
}
22+
23+
fn bench_par2_creation(c: &mut Criterion) {
24+
let mut group = c.benchmark_group("par2_creation");
25+
26+
// Test different file sizes
27+
let sizes = vec![
28+
("1KB", 1024),
29+
("10KB", 10 * 1024),
30+
("100KB", 100 * 1024),
31+
("1MB", 1024 * 1024),
32+
("10MB", 10 * 1024 * 1024),
33+
];
34+
35+
for (size_name, size) in sizes {
36+
group.bench_with_input(
37+
BenchmarkId::new("create_file", size_name),
38+
&size,
39+
|b, &size| {
40+
b.iter_batched(
41+
|| create_test_file(size),
42+
|(temp_dir, test_file)| {
43+
let par2_file = temp_dir.path().join("test.par2");
44+
let reporter = Box::new(SilentCreateReporter);
45+
46+
let mut context = CreateContextBuilder::new()
47+
.output_name(par2_file.to_str().unwrap())
48+
.source_files(vec![test_file])
49+
.redundancy_percentage(5)
50+
.reporter(reporter)
51+
.build()
52+
.unwrap();
53+
54+
context.create().unwrap();
55+
black_box(());
56+
},
57+
criterion::BatchSize::SmallInput,
58+
);
59+
},
60+
);
61+
}
62+
63+
group.finish();
64+
}
65+
66+
fn bench_par2_creation_redundancy(c: &mut Criterion) {
67+
let mut group = c.benchmark_group("par2_creation_redundancy");
68+
69+
// Test different redundancy levels with 1MB file
70+
let redundancy_levels = vec![5, 10, 20, 50];
71+
72+
for redundancy in redundancy_levels {
73+
group.bench_with_input(
74+
BenchmarkId::new("redundancy", format!("{}%", redundancy)),
75+
&redundancy,
76+
|b, &redundancy| {
77+
b.iter_batched(
78+
|| create_test_file(1024 * 1024), // 1MB
79+
|(temp_dir, test_file)| {
80+
let par2_file = temp_dir.path().join("test.par2");
81+
let reporter = Box::new(SilentCreateReporter);
82+
83+
let mut context = CreateContextBuilder::new()
84+
.output_name(par2_file.to_str().unwrap())
85+
.source_files(vec![test_file])
86+
.redundancy_percentage(redundancy)
87+
.reporter(reporter)
88+
.build()
89+
.unwrap();
90+
91+
context.create().unwrap();
92+
black_box(());
93+
},
94+
criterion::BatchSize::SmallInput,
95+
);
96+
},
97+
);
98+
}
99+
100+
group.finish();
101+
}
102+
103+
fn bench_par2_creation_multifile(c: &mut Criterion) {
104+
let mut group = c.benchmark_group("par2_creation_multifile");
105+
106+
// Test different numbers of files (each 100KB)
107+
let file_counts = vec![1, 3, 5, 10];
108+
109+
for file_count in file_counts {
110+
group.bench_with_input(
111+
BenchmarkId::new("files", format!("{}_files", file_count)),
112+
&file_count,
113+
|b, &file_count| {
114+
b.iter_batched(
115+
|| {
116+
let temp_dir = tempdir().unwrap();
117+
let mut files = Vec::new();
118+
119+
for i in 0..file_count {
120+
let file_path = temp_dir.path().join(format!("test{}.dat", i));
121+
let pattern = (0..256)
122+
.cycle()
123+
.take(100 * 1024)
124+
.map(|x| (x + i) as u8)
125+
.collect::<Vec<_>>();
126+
fs::write(&file_path, pattern).unwrap();
127+
files.push(file_path);
128+
}
129+
130+
(temp_dir, files)
131+
},
132+
|(temp_dir, test_files)| {
133+
let par2_file = temp_dir.path().join("test.par2");
134+
let reporter = Box::new(SilentCreateReporter);
135+
136+
let mut context = CreateContextBuilder::new()
137+
.output_name(par2_file.to_str().unwrap())
138+
.source_files(test_files)
139+
.redundancy_percentage(5)
140+
.reporter(reporter)
141+
.build()
142+
.unwrap();
143+
144+
context.create().unwrap();
145+
black_box(());
146+
},
147+
criterion::BatchSize::SmallInput,
148+
);
149+
},
150+
);
151+
}
152+
153+
group.finish();
154+
}
155+
156+
fn bench_block_size_calculation(c: &mut Criterion) {
157+
let mut group = c.benchmark_group("block_size_calculation");
158+
159+
// Test block size calculation performance
160+
let sizes = vec![
161+
("1MB", 1024 * 1024),
162+
("10MB", 10 * 1024 * 1024),
163+
("100MB", 100 * 1024 * 1024),
164+
];
165+
166+
for (size_name, size) in sizes {
167+
group.bench_with_input(
168+
BenchmarkId::new("calculate", size_name),
169+
&size,
170+
|b, &size| {
171+
b.iter_batched(
172+
|| create_test_file(size),
173+
|(temp_dir, test_file)| {
174+
let par2_file = temp_dir.path().join("test.par2");
175+
let reporter = Box::new(SilentCreateReporter);
176+
177+
// This will trigger block size calculation
178+
let context = CreateContextBuilder::new()
179+
.output_name(par2_file.to_str().unwrap())
180+
.source_files(vec![test_file])
181+
.redundancy_percentage(10)
182+
.reporter(reporter)
183+
.build()
184+
.unwrap();
185+
186+
black_box(context.block_size());
187+
},
188+
criterion::BatchSize::SmallInput,
189+
);
190+
},
191+
);
192+
}
193+
194+
group.finish();
195+
}
196+
197+
criterion_group!(
198+
benches,
199+
bench_par2_creation,
200+
bench_par2_creation_redundancy,
201+
bench_par2_creation_multifile,
202+
bench_block_size_calculation
203+
);
204+
criterion_main!(benches);

0 commit comments

Comments
 (0)