Skip to main content

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}