imago/qcow2/compressed.rs
1//! Support for compressed clusters.
2
3use super::*;
4use crate::io_buffers::IoBuffer;
5use crate::sync_primitives::RwLockEitherGuard;
6use miniz_oxide::inflate::core::{decompress as inflate, DecompressorOxide};
7use miniz_oxide::inflate::TINFLStatus;
8use std::num::NonZero;
9
10/// Read guard for a compressed cluster.
11///
12/// Readers generally do not need to lock L2 tables: Modifications replace mappings atomically, and
13/// allocations referenced by a (data) mapping are not freed while the image can be accessed
14/// concurrently. However, allocations referenced by compressed mappings may be freed when they
15/// are replaced (by concurrent writes), so the L2 table must not be modified from looking up the
16/// compressed mapping until its data has been read. This guard ensures that.
17#[derive(Debug)]
18pub(super) struct CompressedClusterReadGuard<'a> {
19 /// Offset of the compressed data.
20 host_offset: HostOffset,
21
22 /// Upper limit on the number of bytes that comprise the compressed data.
23 length: NonZero<u64>,
24
25 /// log2 of the cluster size.
26 cluster_bits: u32,
27
28 /// Held lock on the L2 table, preventing modifications.
29 _lock: RwLockEitherGuard<'a, ()>,
30}
31
32#[maybe_async]
33impl<'a> CompressedClusterReadGuard<'a> {
34 /// Create a new guard for reading compressed data from the given cluster, under `lock`, which
35 /// prevents concurrent modifications of that cluster.
36 ///
37 /// `lock` must be a read or write lock on `L2Table.writer_lock`.
38 ///
39 /// Return `None` if `cluster` is not compressed (checked under the guard).
40 pub fn new(
41 l2_table: &L2Table,
42 cluster: GuestCluster,
43 lock: RwLockEitherGuard<'a, ()>,
44 ) -> io::Result<Option<Self>> {
45 let L2Mapping::Compressed {
46 host_offset,
47 length,
48 } = l2_table.get_mapping(cluster)?
49 else {
50 return Ok(None);
51 };
52
53 Ok(Some(CompressedClusterReadGuard {
54 host_offset,
55 length,
56 cluster_bits: l2_table.cluster_bits(),
57 _lock: lock,
58 }))
59 }
60
61 /// Read the compressed cluster.
62 ///
63 /// Read the compressed data from `storage` (which must be the image’s data file) and
64 /// decompress it into `buf` (which must have a length of exactly one cluster).
65 pub async fn read_compressed<S: Storage>(&self, storage: &S, buf: &mut [u8]) -> io::Result<()> {
66 debug_assert!(buf.len() == 1 << self.cluster_bits);
67
68 // Must fit (really shouldn’t be compressed if this exceeds the cluster size anyway)
69 let length = self.length.get().try_into().map_err(io::Error::other)?;
70 let mut compressed_buf = IoBuffer::new(length, storage.mem_align())?;
71 storage
72 .read(&mut compressed_buf, self.host_offset.0)
73 .await?;
74
75 let mut dec_ox = DecompressorOxide::new();
76 let (status, _read, written) =
77 inflate(&mut dec_ox, compressed_buf.as_ref().into_slice(), buf, 0, 0);
78
79 // Because `length` will generally exceed the actual length, `HasMoreOutput` is expected
80 // and can be ignored
81 if status != TINFLStatus::Done && status != TINFLStatus::HasMoreOutput {
82 return Err(io::Error::other(format!(
83 "Failed to decompress cluster (host offset {}+{length}): {status:?}",
84 self.host_offset.0,
85 )));
86 }
87 if written < buf.len() {
88 return Err(io::Error::other(format!(
89 "Failed to decompress cluster (host offset {}+{length}): Decompressed {written} bytes, expected {}",
90 self.host_offset.0,
91 buf.len(),
92 )));
93 }
94
95 Ok(())
96 }
97}