Skip to main content

vmdk/
sesparse.rs

1//! seSparse (Space-Efficient Sparse) extent reader — vSphere 6.5+ VMFS6 snapshots.
2//!
3//! Detected by `SESPARSE` extent type in a text descriptor (not by file magic).
4//! Two fixed 512-byte headers:
5//!   - Constant header: magic `0x00000000CAFEBABE`
6//!   - Volatile header: magic `0x00000000CAFECAFE`
7//!
8//! All fields are `u64` little-endian. GTEs are 8 bytes each.
9//! Grain size MUST be 8 sectors (4 KiB). Grain table size MUST be 64 sectors
10//! (= 4096 entries × 8 bytes ÷ 512 = 64 sectors per GT).
11//!
12//! Reference: QEMU `vmdk.c` `vmdk_open_se_sparse()`;
13//! strict version check: `version == 0x0000_0002_0000_0001`.
14
15use std::io::{Read, Seek, SeekFrom};
16
17use crate::error::VmdkError;
18
19/// Constant-header magic (`0x0000_0000_CAFE_BABE`, little-endian).
20pub const SE_CONST_MAGIC: u64 = 0x0000_0000_CAFE_BABE;
21
22/// Required version field in the constant header.
23pub const SE_VERSION: u64 = 0x0000_0002_0000_0001;
24
25/// Grain size in sectors — MUST be exactly 8 for seSparse.
26pub const SE_GRAIN_SECTORS: u64 = 8;
27
28/// Grain table size in sectors — MUST be exactly 64 (4096 entries × 8 B ÷ 512).
29pub const SE_GT_SECTORS: u64 = 64;
30
31/// Number of GTEs per grain table: 64 sectors × 512 bytes ÷ 8 bytes-per-GTE.
32pub const SE_GTES_PER_GT: u64 = 4096;
33
34const SECTOR_SIZE: u64 = 512;
35
36// ── Grain-entry encoding (see sesparse-encoding memory; QEMU block/vmdk.c) ────
37/// L1 (GD) allocated marker: high 32 bits of an allocated GD entry.
38pub const SE_GD_ALLOC_MASK: u64 = 0xffff_ffff_0000_0000;
39pub const SE_GD_ALLOC_FLAG: u64 = 0x1000_0000_0000_0000;
40/// L1 low 32 bits hold the grain-table index.
41pub const SE_GD_INDEX_MASK: u64 = 0x0000_0000_ffff_ffff;
42/// L2 (GTE) top-nibble type field.
43pub const SE_GTE_TYPE_MASK: u64 = 0xf000_0000_0000_0000;
44pub const SE_GTE_TYPE_ALLOCATED: u64 = 0x3000_0000_0000_0000;
45pub const SE_GTE_TYPE_UNMAPPED: u64 = 0x1000_0000_0000_0000; // read as zero
46pub const SE_GTE_TYPE_ZERO: u64 = 0x2000_0000_0000_0000; // read as zero
47
48/// Decode an allocated L2 (GTE) entry into a grain index (bit-rotated layout).
49pub fn se_gte_grain_index(gte: u64) -> u64 {
50    ((gte & 0x0fff_0000_0000_0000) >> 48) | ((gte & 0x0000_ffff_ffff_ffff) << 12)
51}
52
53/// Parsed seSparse constant header (first 512 bytes of the extent file).
54pub struct SeConstHeader {
55    pub capacity: u64,      // virtual disk size in sectors
56    pub grain_size: u64,    // must be 8
57    pub gd_offset: u64,     // grain directory sector offset
58    pub gt_offset: u64,     // start of grain tables (sectors)
59    pub grains_offset: u64, // start of grain data (sectors)
60}
61
62impl SeConstHeader {
63    /// Parse the first 512 bytes of a seSparse extent file.
64    pub fn parse(data: &[u8]) -> Result<Self, VmdkError> {
65        if data.len() < 208 {
66            return Err(VmdkError::FileTooSmall);
67        }
68        let magic = u64::from_le_bytes(data[0..8].try_into().expect("8 bytes"));
69        if magic != SE_CONST_MAGIC {
70            return Err(VmdkError::BadMagic);
71        }
72        let version = u64::from_le_bytes(data[8..16].try_into().expect("8 bytes"));
73        if version != SE_VERSION {
74            return Err(VmdkError::UnsupportedVersion(version as u32));
75        }
76        let capacity = u64::from_le_bytes(data[16..24].try_into().expect("8 bytes"));
77        let grain_size = u64::from_le_bytes(data[24..32].try_into().expect("8 bytes"));
78        if grain_size != SE_GRAIN_SECTORS {
79            return Err(VmdkError::FieldOutOfRange {
80                field: "grain_size",
81                value: grain_size,
82                reason: "must equal the seSparse fixed grain size (8 sectors)",
83            });
84        }
85        let grain_table_size = u64::from_le_bytes(data[32..40].try_into().expect("8 bytes"));
86        if grain_table_size != SE_GT_SECTORS {
87            return Err(VmdkError::FieldOutOfRange {
88                field: "grain_table_size",
89                value: grain_table_size,
90                reason: "must equal the seSparse fixed grain-table size",
91            });
92        }
93        // Grain directory offset @128; grain tables @144; grain data @192 (all sectors).
94        let gd_offset = u64::from_le_bytes(data[128..136].try_into().expect("8 bytes"));
95        let gt_offset = u64::from_le_bytes(data[144..152].try_into().expect("8 bytes"));
96        let grains_offset = u64::from_le_bytes(data[192..200].try_into().expect("8 bytes"));
97
98        Ok(SeConstHeader {
99            capacity,
100            grain_size,
101            gd_offset,
102            gt_offset,
103            grains_offset,
104        })
105    }
106}
107
108/// Open a seSparse extent file, loading the grain directory into memory.
109///
110/// Returns `(grain_dir, grain_size_bytes, grains_offset_sectors)`.
111/// `grain_dir[i]` holds the raw L1 entry (nibble-encoded) for that GD slot.
112pub(crate) fn open_sesparse<R: Read + Seek>(
113    mut reader: R,
114) -> Result<(Vec<u64>, u64, u64), VmdkError> {
115    let mut hdr_bytes = [0u8; 512];
116    reader.read_exact(&mut hdr_bytes)?;
117    let hdr = SeConstHeader::parse(&hdr_bytes)?;
118
119    let grain_size_bytes = hdr.grain_size * SECTOR_SIZE;
120
121    // The number of GD entries = ceil(num_grains / GTES_PER_GT).
122    let num_grains = hdr.capacity.div_ceil(hdr.grain_size);
123    let num_gts = num_grains.div_ceil(SE_GTES_PER_GT);
124    // At least one GD entry even for a sub-grain-table-sized disk.
125    let num_gts = num_gts.max(1);
126
127    let gd_bytes = num_gts * 8; // 8 bytes per GD entry (u64)
128    const MAX_SESP_GD: u64 = 16 * 1024 * 1024;
129    if gd_bytes > MAX_SESP_GD {
130        return Err(VmdkError::FieldOutOfRange {
131            field: "grain_directory",
132            value: gd_bytes,
133            reason: "exceeds the 16 MiB cap",
134        });
135    }
136
137    let gd_offset_bytes = hdr.gd_offset * SECTOR_SIZE;
138    reader.seek(SeekFrom::Start(gd_offset_bytes))?;
139    let mut buf = vec![0u8; gd_bytes as usize];
140    reader.read_exact(&mut buf)?;
141
142    let grain_dir = buf
143        .chunks_exact(8)
144        .map(|c| u64::from_le_bytes(c.try_into().expect("8 bytes")))
145        .collect();
146
147    Ok((grain_dir, grain_size_bytes, hdr.grains_offset))
148}
149
150// seSparse GTE lookups are handled inline in lib.rs `grain_location` /
151// `se_read_gte`: the GD entry's allocated nibble (0x1) is checked, the GT table
152// index is its low 32 bits (GT sector = gt_offset + idx*SE_GT_SECTORS), and the
153// allocated (0x3) GTE's grain index is bit-rotated via `se_gte_grain_index`.
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn make_sesparse_header(capacity: u64) -> Vec<u8> {
160        let mut h = vec![0u8; 512];
161        h[0..8].copy_from_slice(&SE_CONST_MAGIC.to_le_bytes());
162        h[8..16].copy_from_slice(&SE_VERSION.to_le_bytes());
163        h[16..24].copy_from_slice(&capacity.to_le_bytes());
164        h[24..32].copy_from_slice(&SE_GRAIN_SECTORS.to_le_bytes()); // grain_size = 8
165        h[32..40].copy_from_slice(&SE_GT_SECTORS.to_le_bytes()); // grain_table_size = 64
166                                                                 // volatile header offset (80): just put 2
167        h[80..88].copy_from_slice(&2u64.to_le_bytes());
168        // gd_offset at 128
169        h[128..136].copy_from_slice(&10u64.to_le_bytes()); // GD at sector 10
170                                                           // gd_size at 136
171        h[136..144].copy_from_slice(&1u64.to_le_bytes());
172        // gt_offset at 144
173        h[144..152].copy_from_slice(&11u64.to_le_bytes());
174        // grains_offset at 192
175        h[192..200].copy_from_slice(&75u64.to_le_bytes());
176        h
177    }
178
179    #[test]
180    fn sesparse_header_parse_ok() {
181        let h = make_sesparse_header(4096);
182        let hdr = SeConstHeader::parse(&h).expect("parse");
183        assert_eq!(hdr.capacity, 4096);
184        assert_eq!(hdr.grain_size, 8);
185        assert_eq!(hdr.gd_offset, 10);
186        assert_eq!(hdr.gt_offset, 11);
187    }
188
189    #[test]
190    fn sesparse_wrong_magic_rejected() {
191        let h = vec![0u8; 512];
192        assert!(matches!(SeConstHeader::parse(&h), Err(VmdkError::BadMagic)));
193    }
194
195    #[test]
196    fn sesparse_wrong_version_rejected() {
197        let mut h = make_sesparse_header(8);
198        h[8..16].copy_from_slice(&0u64.to_le_bytes()); // wrong version
199        assert!(matches!(
200            SeConstHeader::parse(&h),
201            Err(VmdkError::UnsupportedVersion(_))
202        ));
203    }
204
205    #[test]
206    fn sesparse_wrong_grain_size_rejected() {
207        let mut h = make_sesparse_header(8);
208        h[24..32].copy_from_slice(&16u64.to_le_bytes()); // grain_size=16, not 8
209        assert!(matches!(
210            SeConstHeader::parse(&h),
211            Err(VmdkError::FieldOutOfRange {
212                field: "grain_size",
213                ..
214            })
215        ));
216    }
217
218    #[test]
219    fn sesparse_short_buffer_rejected() {
220        assert!(matches!(
221            SeConstHeader::parse(&[0u8; 100]),
222            Err(VmdkError::FileTooSmall)
223        ));
224    }
225
226    #[test]
227    fn sesparse_wrong_grain_table_size_rejected() {
228        let mut h = make_sesparse_header(8);
229        h[32..40].copy_from_slice(&128u64.to_le_bytes()); // grain_table_size=128, not 64
230        assert!(matches!(
231            SeConstHeader::parse(&h),
232            Err(VmdkError::FieldOutOfRange {
233                field: "grain_table_size",
234                ..
235            })
236        ));
237    }
238
239    #[test]
240    fn sesparse_grain_directory_too_large_rejected() {
241        // A capacity large enough that the GD would exceed the 16 MiB cap.
242        let h = make_sesparse_header(100_000_000_000);
243        assert!(matches!(
244            open_sesparse(std::io::Cursor::new(h)),
245            Err(VmdkError::FieldOutOfRange {
246                field: "grain_directory",
247                ..
248            })
249        ));
250    }
251}