use std::fs::{self, File};
use std::io::Write;
use std::os::unix::fs::FileExt;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WriteStats {
pub ok: usize,
pub failed: usize,
}
pub struct EntryJob {
pub data_offset: u64,
pub comp_len: usize,
pub out_size: usize,
pub is_store: bool,
pub name: String,
}
pub fn extract_entries_unordered<D>(
input: &File,
jobs: &[EntryJob],
out_dir: &Path,
n_workers: usize,
decode: D,
) -> WriteStats
where
D: Fn(&[u8], usize, &mut Vec<u8>) -> Result<(), String> + Sync,
{
let n = jobs.len();
if n == 0 {
return WriteStats::default();
}
let workers = n_workers.max(1).min(n);
let next = AtomicUsize::new(0);
let ok = AtomicUsize::new(0);
let failed = AtomicUsize::new(0);
let next_ref = &next;
let ok_ref = &ok;
let failed_ref = &failed;
let decode_ref = &decode;
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(move || {
let mut comp: Vec<u8> = Vec::new();
let mut out: Vec<u8> = Vec::new();
loop {
let i = next_ref.fetch_add(1, Ordering::Relaxed);
if i >= n {
break;
}
let job = &jobs[i];
if comp.len() < job.comp_len {
comp.resize(job.comp_len, 0);
}
if let Err(e) = input.read_exact_at(&mut comp[..job.comp_len], job.data_offset) {
eprintln!("parwrite: read {}: {e}", job.name);
failed_ref.fetch_add(1, Ordering::Relaxed);
continue;
}
let comp_slice = &comp[..job.comp_len];
out.clear();
if job.is_store {
out.extend_from_slice(comp_slice);
} else if let Err(e) = decode_ref(comp_slice, job.out_size, &mut out) {
eprintln!("parwrite: decode {}: {e}", job.name);
failed_ref.fetch_add(1, Ordering::Relaxed);
continue;
}
let dest = out_dir.join(&job.name);
if let Some(parent) = dest.parent() {
if let Err(e) = fs::create_dir_all(parent) {
eprintln!("parwrite: mkdir {}: {e}", parent.display());
failed_ref.fetch_add(1, Ordering::Relaxed);
continue;
}
}
match File::create(&dest).and_then(|mut f| f.write_all(&out)) {
Ok(()) => {
ok_ref.fetch_add(1, Ordering::Relaxed);
}
Err(e) => {
eprintln!("parwrite: write {}: {e}", dest.display());
failed_ref.fetch_add(1, Ordering::Relaxed);
}
}
}
});
}
});
WriteStats {
ok: ok.load(Ordering::Relaxed),
failed: failed.load(Ordering::Relaxed),
}
}
pub fn write_segments_positional<D>(
out: &File,
n_segments: usize,
offsets: &[u64],
n_workers: usize,
decode: D,
) -> WriteStats
where
D: Fn(usize, &mut Vec<u8>) -> Result<(), String> + Sync,
{
assert_eq!(offsets.len(), n_segments, "offsets must have one entry per segment");
if n_segments == 0 {
return WriteStats::default();
}
let workers = n_workers.max(1).min(n_segments);
let next = AtomicUsize::new(0);
let ok = AtomicUsize::new(0);
let failed = AtomicUsize::new(0);
let next_ref = &next;
let ok_ref = &ok;
let failed_ref = &failed;
let decode_ref = &decode;
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(move || {
let mut buf: Vec<u8> = Vec::new();
loop {
let i = next_ref.fetch_add(1, Ordering::Relaxed);
if i >= n_segments {
break;
}
buf.clear();
if let Err(e) = decode_ref(i, &mut buf) {
eprintln!("parwrite: segment {i} decode: {e}");
failed_ref.fetch_add(1, Ordering::Relaxed);
continue;
}
match out.write_all_at(&buf, offsets[i]) {
Ok(()) => {
ok_ref.fetch_add(1, Ordering::Relaxed);
}
Err(e) => {
eprintln!("parwrite: segment {i} pwrite: {e}");
failed_ref.fetch_add(1, Ordering::Relaxed);
}
}
}
});
}
});
WriteStats {
ok: ok.load(Ordering::Relaxed),
failed: failed.load(Ordering::Relaxed),
}
}
pub fn write_slices_positional(
out: &File,
slices: &[&[u8]],
offsets: &[u64],
n_workers: usize,
) -> WriteStats {
assert_eq!(offsets.len(), slices.len(), "offsets must have one entry per slice");
let n = slices.len();
if n == 0 {
return WriteStats::default();
}
let workers = n_workers.max(1).min(n);
let next = AtomicUsize::new(0);
let ok = AtomicUsize::new(0);
let failed = AtomicUsize::new(0);
let next_ref = &next;
let ok_ref = &ok;
let failed_ref = &failed;
std::thread::scope(|s| {
for _ in 0..workers {
s.spawn(move || loop {
let i = next_ref.fetch_add(1, Ordering::Relaxed);
if i >= n {
break;
}
match out.write_all_at(slices[i], offsets[i]) {
Ok(()) => {
ok_ref.fetch_add(1, Ordering::Relaxed);
}
Err(e) => {
eprintln!("parwrite: slice {i} pwrite: {e}");
failed_ref.fetch_add(1, Ordering::Relaxed);
}
}
});
}
});
WriteStats {
ok: ok.load(Ordering::Relaxed),
failed: failed.load(Ordering::Relaxed),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
#[test]
fn extract_writes_every_entry_concurrently() {
let dir = std::env::temp_dir().join(format!("parwrite-test-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let arc_path = dir.join("archive.bin");
let payloads: Vec<Vec<u8>> = (0..3)
.map(|k| (0..1000).map(|i| (i as u8).wrapping_add(k as u8)).collect())
.collect();
let mut arc = Vec::new();
let mut jobs = Vec::new();
for (k, p) in payloads.iter().enumerate() {
jobs.push(EntryJob {
data_offset: arc.len() as u64,
comp_len: p.len(),
out_size: p.len(),
is_store: true, name: format!("out_{k}.bin"),
});
arc.extend_from_slice(p);
}
fs::write(&arc_path, &arc).unwrap();
let f = File::open(&arc_path).unwrap();
let out_dir = dir.join("out");
let stats = extract_entries_unordered(&f, &jobs, &out_dir, 4, |_c, _s, _o| Ok(()));
assert_eq!(stats.ok, 3);
assert_eq!(stats.failed, 0);
for (k, p) in payloads.iter().enumerate() {
let got = fs::read(out_dir.join(format!("out_{k}.bin"))).unwrap();
assert_eq!(&got, p, "entry {k} bytes must round-trip");
}
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn positional_writes_reassemble_in_order() {
let dir = std::env::temp_dir().join(format!("parwrite-pos-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let out_path = dir.join("stream.bin");
let segs: Vec<Vec<u8>> = vec![
vec![1u8; 100],
vec![2u8; 250],
vec![3u8; 50],
vec![4u8; 400],
];
let mut offsets = Vec::new();
let mut total = 0u64;
for s in &segs {
offsets.push(total);
total += s.len() as u64;
}
let out = File::create(&out_path).unwrap();
out.set_len(total).unwrap();
let stats = write_segments_positional(&out, segs.len(), &offsets, 4, |i, buf| {
buf.extend_from_slice(&segs[i]);
Ok(())
});
assert_eq!(stats.ok, segs.len());
assert_eq!(stats.failed, 0);
drop(out);
let mut got = Vec::new();
File::open(&out_path).unwrap().read_to_end(&mut got).unwrap();
let mut want = Vec::new();
for s in &segs {
want.extend_from_slice(s);
}
assert_eq!(got, want, "positional segments must reassemble in stream order");
let _ = fs::remove_dir_all(&dir);
}
}