Skip to content

Commit 31f3789

Browse files
authored
Merge pull request #13 from bennyz/xz2
use multi-threaded XZ decompression via liblzma
2 parents 6565ee0 + 266d6f4 commit 31f3789

10 files changed

Lines changed: 251 additions & 439 deletions

File tree

Cargo.lock

Lines changed: 22 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ nix = { version = "0.29", features = ["ioctl"] }
2020
# Enable vendored OpenSSL for cross-compilation to musl targets
2121
# This ensures OpenSSL builds from source with musl compatibility
2222
openssl = { version = "0.10", features = ["vendored"] }
23-
xz2 = "0.1"
23+
liblzma = { version = "0.4", features = ["parallel"] }
2424

2525
[dev-dependencies]
2626
http-body = "1.0.1"

src/fls/decompress.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
1+
use crate::fls::byte_channel::ByteBoundedReceiver;
2+
use crate::fls::compression::Compression;
3+
use crate::fls::stream_utils::ChannelReader;
4+
use bytes::Bytes;
5+
use std::io::Read;
16
use tokio::io::AsyncReadExt;
27
use tokio::process::{Child, Command};
38
use tokio::sync::mpsc;
49

10+
pub(crate) fn mb_to_bytes(mb: u64) -> u64 {
11+
mb.saturating_mul(1024 * 1024)
12+
}
13+
14+
pub(crate) fn create_xz_decoder<R: Read>(
15+
reader: R,
16+
memlimit_mb: u64,
17+
) -> Result<liblzma::read::XzDecoder<R>, String> {
18+
let memlimit = mb_to_bytes(memlimit_mb);
19+
let stream = liblzma::stream::Stream::new_stream_decoder(memlimit, 0).map_err(|e| {
20+
format!(
21+
"Failed to create XZ decoder with {}MB limit: {}",
22+
memlimit_mb, e
23+
)
24+
})?;
25+
Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
26+
}
27+
28+
pub(crate) fn create_mt_xz_decoder<R: Read + Send + 'static>(
29+
reader: R,
30+
xz_memlimit_mb: u64,
31+
) -> Result<Box<dyn Read + Send>, String> {
32+
let num_threads = std::thread::available_parallelism()
33+
.map(|n| n.get() as u32)
34+
.unwrap_or(2);
35+
let memlimit = mb_to_bytes(xz_memlimit_mb);
36+
eprintln!(
37+
"XZ decompression: {} threads, memory limit {}MB",
38+
num_threads, xz_memlimit_mb
39+
);
40+
let stream = liblzma::stream::MtStreamBuilder::new()
41+
.threads(num_threads)
42+
.memlimit_threading(memlimit)
43+
.memlimit_stop(memlimit)
44+
.decoder()
45+
.map_err(|e| format!("Failed to create MT XZ decoder: {}", e))?;
46+
Ok(Box::new(liblzma::read::XzDecoder::new_stream(
47+
reader, stream,
48+
)))
49+
}
50+
551
/// Determines the appropriate decompression command based on URL extension
652
fn get_decompressor_command(url: &str) -> &'static str {
753
let extension = url.rsplit('.').next().unwrap_or("").to_lowercase();
@@ -75,6 +121,67 @@ fn spawn_decompressor(
75121
Ok((process, cmd))
76122
}
77123

124+
pub(crate) fn get_compression_from_url(url: &str) -> Compression {
125+
let path = url.split('?').next().unwrap_or(url);
126+
let path = path.split('#').next().unwrap_or(path);
127+
let extension = path.rsplit('.').next().unwrap_or("").to_lowercase();
128+
match extension.as_str() {
129+
"gz" => Compression::Gzip,
130+
"xz" => Compression::Xz,
131+
"zst" | "zstd" => Compression::Zstd,
132+
_ => Compression::None,
133+
}
134+
}
135+
136+
type DecompressorResult = (
137+
mpsc::Receiver<Vec<u8>>,
138+
std::thread::JoinHandle<Result<(), String>>,
139+
);
140+
141+
pub(crate) fn start_inprocess_decompressor(
142+
buffer_rx: ByteBoundedReceiver<Bytes>,
143+
compression: Compression,
144+
consumed_progress_tx: mpsc::UnboundedSender<u64>,
145+
xz_memlimit_mb: u64,
146+
) -> Result<DecompressorResult, Box<dyn std::error::Error>> {
147+
let (decompressed_tx, decompressed_rx) = mpsc::channel::<Vec<u8>>(8);
148+
149+
let handle = std::thread::Builder::new()
150+
.name("decompressor".to_string())
151+
.spawn(move || {
152+
let channel_reader =
153+
ChannelReader::new_byte_bounded(buffer_rx).with_progress(consumed_progress_tx);
154+
155+
let mut decoder: Box<dyn Read + Send> = match compression {
156+
Compression::Xz => create_mt_xz_decoder(channel_reader, xz_memlimit_mb)?,
157+
Compression::Gzip => Box::new(flate2::read::GzDecoder::new(channel_reader)),
158+
Compression::None => Box::new(channel_reader),
159+
Compression::Zstd => {
160+
return Err("Zstd in-process decompression is not supported".to_string());
161+
}
162+
};
163+
164+
let mut buf = vec![0u8; 8 * 1024 * 1024];
165+
loop {
166+
let n = decoder
167+
.read(&mut buf)
168+
.map_err(|e| format!("Decompression error: {}", e))?;
169+
if n == 0 {
170+
break;
171+
}
172+
if decompressed_tx.blocking_send(buf[..n].to_vec()).is_err() {
173+
return Err("Writer task closed, stopping decompression".to_string());
174+
}
175+
}
176+
Ok(())
177+
})
178+
.map_err(|e| -> Box<dyn std::error::Error> {
179+
format!("Failed to spawn decompressor thread: {}", e).into()
180+
})?;
181+
182+
Ok((decompressed_rx, handle))
183+
}
184+
78185
pub(crate) async fn spawn_stderr_reader(
79186
mut stderr: tokio::process::ChildStderr,
80187
error_tx: mpsc::UnboundedSender<String>,

0 commit comments

Comments
 (0)