Skip to content

Commit 266d6f4

Browse files
committed
oci: add MT xz support
Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com> Assisted-by: claude-opus-4.6
1 parent 28668fb commit 266d6f4

2 files changed

Lines changed: 50 additions & 207 deletions

File tree

src/fls/decompress.rs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use tokio::io::AsyncReadExt;
77
use tokio::process::{Child, Command};
88
use tokio::sync::mpsc;
99

10-
fn mb_to_bytes(mb: u64) -> u64 {
10+
pub(crate) fn mb_to_bytes(mb: u64) -> u64 {
1111
mb.saturating_mul(1024 * 1024)
1212
}
1313

@@ -25,6 +25,29 @@ pub(crate) fn create_xz_decoder<R: Read>(
2525
Ok(liblzma::read::XzDecoder::new_stream(reader, stream))
2626
}
2727

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+
2851
/// Determines the appropriate decompression command based on URL extension
2952
fn get_decompressor_command(url: &str) -> &'static str {
3053
let extension = url.rsplit('.').next().unwrap_or("").to_lowercase();
@@ -130,23 +153,7 @@ pub(crate) fn start_inprocess_decompressor(
130153
ChannelReader::new_byte_bounded(buffer_rx).with_progress(consumed_progress_tx);
131154

132155
let mut decoder: Box<dyn Read + Send> = match compression {
133-
Compression::Xz => {
134-
let num_threads = std::thread::available_parallelism()
135-
.map(|n| n.get() as u32)
136-
.unwrap_or(2);
137-
let memlimit = mb_to_bytes(xz_memlimit_mb);
138-
eprintln!(
139-
"XZ decompression: {} threads, memory limit {}MB",
140-
num_threads, xz_memlimit_mb
141-
);
142-
let stream = liblzma::stream::MtStreamBuilder::new()
143-
.threads(num_threads)
144-
.memlimit_threading(memlimit)
145-
.memlimit_stop(memlimit)
146-
.decoder()
147-
.map_err(|e| format!("Failed to create MT XZ decoder: {}", e))?;
148-
Box::new(liblzma::read::XzDecoder::new_stream(channel_reader, stream))
149-
}
156+
Compression::Xz => create_mt_xz_decoder(channel_reader, xz_memlimit_mb)?,
150157
Compression::Gzip => Box::new(flate2::read::GzDecoder::new(channel_reader)),
151158
Compression::None => Box::new(channel_reader),
152159
Compression::Zstd => {

src/fls/oci/from_oci.rs

Lines changed: 25 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ struct DownloadContext {
5656
struct RawDiskDownloadParams {
5757
http_tx: ByteBoundedSender<bytes::Bytes>,
5858
writer_handle: tokio::task::JoinHandle<Result<u64, std::io::Error>>,
59-
external_decompressor: Option<tokio::process::Child>,
59+
is_compressed: bool,
6060
decompressed_progress_rx: mpsc::UnboundedReceiver<u64>,
6161
raw_written_progress_rx: mpsc::UnboundedReceiver<u64>,
6262
}
@@ -69,12 +69,6 @@ struct ProcessingHandles {
6969
tar_extractor_handle: tokio::task::JoinHandle<Result<(), String>>,
7070
}
7171

72-
/// Components returned by external decompressor pipeline setup
73-
struct ExternalDecompressorPipeline {
74-
writer_handle: tokio::task::JoinHandle<Result<u64, std::io::Error>>,
75-
decompressor: tokio::process::Child,
76-
}
77-
7872
/// Components returned by pipeline setup
7973
struct TarPipelineComponents {
8074
http_tx: ByteBoundedSender<bytes::Bytes>,
@@ -1423,159 +1417,36 @@ async fn coordinate_download_and_processing(
14231417
Ok(())
14241418
}
14251419

1426-
/// Setup external decompressor pipeline for XZ compression
1427-
async fn setup_external_decompressor_pipeline(
1428-
http_rx: ByteBoundedReceiver<bytes::Bytes>,
1429-
block_writer: AsyncBlockWriter,
1430-
decompressed_progress_tx: mpsc::UnboundedSender<u64>,
1431-
debug: bool,
1432-
) -> Result<ExternalDecompressorPipeline, Box<dyn std::error::Error>> {
1433-
// XZ: Use external xzcat process
1434-
let (mut decompressor, decompressor_name) = start_decompressor_process("disk.img.xz").await?;
1435-
1436-
let decompressor_stdin = decompressor.stdin.take().unwrap();
1437-
let decompressor_stdout = decompressor.stdout.take().unwrap();
1438-
let decompressor_stderr = decompressor.stderr.take().unwrap();
1439-
1440-
let (error_tx, error_rx) = mpsc::unbounded_channel::<String>();
1441-
1442-
// Spawn stderr reader
1443-
tokio::spawn(spawn_stderr_reader(
1444-
decompressor_stderr,
1445-
error_tx.clone(),
1446-
decompressor_name,
1447-
));
1448-
1449-
// Spawn error processor
1450-
tokio::spawn(process_error_messages(error_rx));
1451-
1452-
// Spawn blocking task: read from channel and write to xzcat stdin
1453-
// First, create a sync file handle from the async stdin
1454-
#[cfg(unix)]
1455-
let stdin_fd = {
1456-
use std::os::unix::io::{AsRawFd, FromRawFd};
1457-
let raw_fd = decompressor_stdin.as_raw_fd();
1458-
// Duplicate the fd so we can use it in blocking context
1459-
let dup_fd = unsafe { libc::dup(raw_fd) };
1460-
if dup_fd == -1 {
1461-
return Err(std::io::Error::last_os_error().into());
1462-
}
1463-
// SAFETY: dup_fd is a valid file descriptor (we checked above)
1464-
unsafe { std::fs::File::from_raw_fd(dup_fd) }
1465-
};
1466-
#[cfg(not(unix))]
1467-
let stdin_fd: std::fs::File = {
1468-
return Err("XZ streaming decompression is not supported on non-unix platforms".into());
1469-
};
1470-
1471-
// Drop the original async stdin (the dup'd fd still points to the pipe)
1472-
drop(decompressor_stdin);
1473-
1474-
let stdin_writer_handle = {
1475-
tokio::task::spawn_blocking(move || {
1476-
use std::io::Write as _;
1477-
let reader = ChannelReader::new_byte_bounded(http_rx);
1478-
let mut reader = reader;
1479-
let mut stdin = stdin_fd;
1480-
let mut buffer = vec![0u8; 1024 * 1024]; // 1MB chunks
1481-
1482-
loop {
1483-
match reader.read(&mut buffer) {
1484-
Ok(0) => break, // EOF
1485-
Ok(n) => {
1486-
if let Err(e) = stdin.write_all(&buffer[..n]) {
1487-
return Err(format!("Error writing to xzcat: {}", e));
1488-
}
1489-
}
1490-
Err(e) => return Err(format!("Error reading stream: {}", e)),
1491-
}
1492-
}
1493-
1494-
drop(stdin);
1495-
Ok::<(), String>(())
1496-
})
1497-
};
1498-
1499-
// Spawn task: xzcat stdout -> block writer with sparse detection
1500-
let writer = block_writer;
1501-
let progress_tx = decompressed_progress_tx;
1502-
let writer_handle = tokio::spawn(async move {
1503-
let mut stdout = decompressor_stdout;
1504-
let mut buffer = vec![0u8; 8 * 1024 * 1024]; // 8MB buffer
1505-
1506-
// Auto-detect sparse image format from initial data
1507-
let mut detector = FormatDetector::new();
1508-
let mut parser: Option<SparseParser> = None;
1509-
let mut format_determined = false;
1510-
1511-
loop {
1512-
let n = match tokio::io::AsyncReadExt::read(&mut stdout, &mut buffer).await {
1513-
Ok(0) => {
1514-
finalize_format_at_eof(&mut detector, format_determined, &writer, debug)
1515-
.await?;
1516-
break;
1517-
}
1518-
Ok(n) => n,
1519-
Err(e) => return Err(e),
1520-
};
1521-
1522-
let _ = progress_tx.send(n as u64);
1523-
1524-
process_buffer_with_format_detection(
1525-
&buffer,
1526-
n,
1527-
&mut detector,
1528-
&mut parser,
1529-
&mut format_determined,
1530-
&writer,
1531-
debug,
1532-
)
1533-
.await?;
1534-
}
1535-
1536-
// Wait for stdin writer to finish and propagate any errors
1537-
stdin_writer_handle
1538-
.await
1539-
.map_err(|e| std::io::Error::other(format!("Stdin writer task failed: {}", e)))?
1540-
.map_err(|e| std::io::Error::other(format!("Stdin writer error: {}", e)))?;
1541-
1542-
writer.close().await
1543-
});
1544-
1545-
Ok(ExternalDecompressorPipeline {
1546-
writer_handle,
1547-
decompressor,
1548-
})
1549-
}
1550-
1551-
/// Setup in-process decompression pipeline for Gzip or None compression
15521420
async fn setup_inprocess_decompression_pipeline(
15531421
http_rx: ByteBoundedReceiver<bytes::Bytes>,
15541422
block_writer: AsyncBlockWriter,
15551423
decompressed_progress_tx: mpsc::UnboundedSender<u64>,
15561424
compression_type: Compression,
15571425
debug: bool,
1426+
xz_memlimit_mb: u64,
15581427
) -> Result<tokio::task::JoinHandle<Result<u64, std::io::Error>>, Box<dyn std::error::Error>> {
1559-
// Gzip or None: decompress in-process and write directly to block writer
15601428
let writer = block_writer;
15611429
let progress_tx = decompressed_progress_tx;
15621430

1563-
// Create an async channel for decompressed data
15641431
let (data_tx, mut data_rx) = mpsc::channel::<Vec<u8>>(16);
15651432

1566-
// Spawn blocking task: read, decompress, send to async channel
15671433
let reader_handle = tokio::task::spawn_blocking(move || {
15681434
let reader = ChannelReader::new_byte_bounded(http_rx);
15691435

1570-
// Apply in-process gzip decompression if needed
15711436
let processed_reader: Box<dyn std::io::Read + Send> = match compression_type {
15721437
Compression::Gzip => {
15731438
if debug {
15741439
eprintln!("[DEBUG] Applying in-process gzip decompression");
15751440
}
15761441
Box::new(flate2::read::GzDecoder::new(reader))
15771442
}
1578-
_ => Box::new(reader),
1443+
Compression::Xz => {
1444+
crate::fls::decompress::create_mt_xz_decoder(reader, xz_memlimit_mb)?
1445+
}
1446+
Compression::None => Box::new(reader),
1447+
Compression::Zstd => {
1448+
return Err("Zstd in-process decompression is not supported".to_string());
1449+
}
15791450
};
15801451

15811452
let mut reader = processed_reader;
@@ -1657,7 +1528,7 @@ async fn coordinate_raw_disk_download(
16571528
let mut progress =
16581529
ProgressTracker::new(options.common.newline_progress, options.common.show_memory);
16591530
progress.set_content_length(Some(layer_size));
1660-
progress.set_is_compressed(params.external_decompressor.is_some());
1531+
progress.set_is_compressed(params.is_compressed);
16611532
progress.bytes_received = initial_buffer.len() as u64;
16621533
let update_interval = Duration::from_secs_f64(options.common.progress_interval_secs);
16631534

@@ -1772,24 +1643,6 @@ async fn coordinate_raw_disk_download(
17721643
progress.decompress_duration = Some(elapsed);
17731644
progress.write_duration = Some(elapsed);
17741645

1775-
// Wait for external decompressor process and check exit status
1776-
if let Some(mut decompressor) = params.external_decompressor {
1777-
match decompressor.wait().await {
1778-
Ok(status) => {
1779-
if !status.success() {
1780-
return Err(format!(
1781-
"Decompressor process failed with exit code: {:?}",
1782-
status.code()
1783-
)
1784-
.into());
1785-
}
1786-
}
1787-
Err(e) => {
1788-
return Err(format!("Failed to wait for decompressor process: {}", e).into());
1789-
}
1790-
}
1791-
}
1792-
17931646
// Final progress update
17941647
let _ = progress.update_progress(Some(layer_size), update_interval, true);
17951648

@@ -2520,48 +2373,31 @@ async fn flash_raw_disk_image_directly(
25202373
byte_bounded_channel::<bytes::Bytes>(max_buffer_bytes, buffer_capacity);
25212374
let (decompressed_progress_tx, decompressed_progress_rx) = mpsc::unbounded_channel::<u64>();
25222375

2523-
// For gzip and none, we can decompress in-process and write directly to block writer
2524-
// For XZ, we need the external xzcat process
2525-
let needs_external_decompressor = compression_type == Compression::Xz;
2376+
if compression_type == Compression::Zstd {
2377+
return Err("Zstd in-process decompression is not supported".into());
2378+
}
25262379

25272380
if options.common.debug {
25282381
eprintln!(
2529-
"[DEBUG] Compression type: {:?}, using external decompressor: {}",
2530-
compression_type, needs_external_decompressor
2382+
"[DEBUG] Compression type: {:?}, using in-process decompressor",
2383+
compression_type
25312384
);
25322385
}
25332386

2534-
// Spawn the processing pipeline based on compression type
2535-
let (writer_handle, external_decompressor) = if needs_external_decompressor {
2536-
// XZ: Use external xzcat process
2537-
let pipeline = setup_external_decompressor_pipeline(
2538-
http_rx,
2539-
block_writer,
2540-
decompressed_progress_tx,
2541-
options.common.debug,
2542-
)
2543-
.await?;
2544-
2545-
(pipeline.writer_handle, Some(pipeline.decompressor))
2546-
} else {
2547-
// Gzip or None: decompress in-process and write directly to block writer
2548-
let handle = setup_inprocess_decompression_pipeline(
2549-
http_rx,
2550-
block_writer,
2551-
decompressed_progress_tx,
2552-
compression_type,
2553-
options.common.debug,
2554-
)
2555-
.await?;
2556-
2557-
(handle, None)
2558-
};
2387+
let writer_handle = setup_inprocess_decompression_pipeline(
2388+
http_rx,
2389+
block_writer,
2390+
decompressed_progress_tx,
2391+
compression_type,
2392+
options.common.debug,
2393+
options.common.xz_memlimit_mb,
2394+
)
2395+
.await?;
25592396

2560-
// Coordinate download and processing
25612397
let params = RawDiskDownloadParams {
25622398
http_tx,
25632399
writer_handle,
2564-
external_decompressor,
2400+
is_compressed: compression_type != Compression::None,
25652401
decompressed_progress_rx,
25662402
raw_written_progress_rx,
25672403
};

0 commit comments

Comments
 (0)