1use crate::error::{Result, VmdkError};
4
5pub const MAGIC: u32 = 0x564D_444B;
6pub const VERSION: u32 = 1;
7pub const VERSION_ZEROED_GRAIN: u32 = 2;
9pub const VERSION_STREAM_OPT: u32 = 3;
10pub const SECTOR_SIZE: u64 = 512;
11
12pub const MAX_NUM_GTES_PER_GT: u32 = 512;
18
19pub const MAX_GRAIN_SIZE_SECTORS: u64 = 0x10000;
26
27pub const GD_AT_END: u64 = 0xffff_ffff_ffff_ffff;
33
34pub struct SparseExtentHeader {
36 pub version: u32, pub capacity: u64, pub grain_size: u64, pub descriptor_offset: u64, pub descriptor_size: u64, pub num_gtes_per_gt: u32,
42 pub rgd_offset: u64, pub gd_offset: u64, pub compressed: bool,
46}
47
48impl SparseExtentHeader {
49 pub fn parse(data: &[u8]) -> Result<Self> {
50 if data.len() < 512 {
51 return Err(VmdkError::FileTooSmall);
52 }
53
54 let magic = u32::from_le_bytes(data[0..4].try_into().expect("4 bytes"));
55 if magic != MAGIC {
56 return Err(VmdkError::BadMagic);
57 }
58
59 let version = u32::from_le_bytes(data[4..8].try_into().expect("4 bytes"));
60 if version != VERSION && version != VERSION_ZEROED_GRAIN && version != VERSION_STREAM_OPT {
63 return Err(VmdkError::UnsupportedVersion(version));
64 }
65
66 let capacity = u64::from_le_bytes(data[12..20].try_into().expect("8 bytes"));
67 let grain_size = u64::from_le_bytes(data[20..28].try_into().expect("8 bytes"));
68 let descriptor_offset = u64::from_le_bytes(data[28..36].try_into().expect("8 bytes"));
69 let descriptor_size = u64::from_le_bytes(data[36..44].try_into().expect("8 bytes"));
70 let num_gtes_per_gt = u32::from_le_bytes(data[44..48].try_into().expect("4 bytes"));
71 let rgd_offset = u64::from_le_bytes(data[48..56].try_into().expect("8 bytes"));
72 let gd_offset = u64::from_le_bytes(data[56..64].try_into().expect("8 bytes"));
73 let compress_algorithm = u16::from_le_bytes(data[77..79].try_into().expect("2 bytes"));
74
75 match (version, compress_algorithm) {
81 (VERSION | VERSION_ZEROED_GRAIN, 0) | (VERSION_STREAM_OPT, 1) => {}
82 _ => return Err(VmdkError::CompressedNotSupported),
83 }
84
85 if grain_size < 8 {
88 return Err(VmdkError::FieldOutOfRange {
89 field: "grain_size",
90 value: grain_size,
91 reason: "must be >= 8 sectors (VDF 1.1 §4.1)",
92 });
93 }
94 if grain_size > MAX_GRAIN_SIZE_SECTORS {
97 return Err(VmdkError::FieldOutOfRange {
98 field: "grain_size",
99 value: grain_size,
100 reason: "exceeds the maximum supported grain size (32 MiB)",
101 });
102 }
103 if num_gtes_per_gt == 0 {
104 return Err(VmdkError::FieldOutOfRange {
105 field: "num_gtes_per_gt",
106 value: u64::from(num_gtes_per_gt),
107 reason: "must be > 0",
108 });
109 }
110 if num_gtes_per_gt > MAX_NUM_GTES_PER_GT {
115 return Err(VmdkError::FieldOutOfRange {
116 field: "num_gtes_per_gt",
117 value: u64::from(num_gtes_per_gt),
118 reason: "exceeds the spec maximum of 512",
119 });
120 }
121
122 Ok(SparseExtentHeader {
123 version,
124 capacity,
125 grain_size,
126 descriptor_offset,
127 descriptor_size,
128 num_gtes_per_gt,
129 rgd_offset,
130 gd_offset,
131 compressed: compress_algorithm != 0,
132 })
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn valid_header() -> Vec<u8> {
141 let mut h = vec![0u8; 512];
142 h[0..4].copy_from_slice(&MAGIC.to_le_bytes());
143 h[4..8].copy_from_slice(&VERSION.to_le_bytes());
144 h[12..20].copy_from_slice(&8u64.to_le_bytes()); h[20..28].copy_from_slice(&8u64.to_le_bytes()); h[44..48].copy_from_slice(&512u32.to_le_bytes()); h
148 }
149
150 #[test]
151 fn parse_rejects_short_buffer() {
152 assert!(matches!(
153 SparseExtentHeader::parse(&[0u8; 100]),
154 Err(VmdkError::FileTooSmall)
155 ));
156 }
157
158 #[test]
159 fn parse_rejects_bad_magic() {
160 let h = vec![0u8; 512];
161 assert!(matches!(
162 SparseExtentHeader::parse(&h),
163 Err(VmdkError::BadMagic)
164 ));
165 }
166
167 #[test]
168 fn parse_rejects_unsupported_version() {
169 let mut h = valid_header();
170 h[4..8].copy_from_slice(&4u32.to_le_bytes()); assert!(matches!(
172 SparseExtentHeader::parse(&h),
173 Err(VmdkError::UnsupportedVersion(4))
174 ));
175 }
176
177 #[test]
178 fn parse_accepts_version_2() {
179 let mut h = valid_header();
180 h[4..8].copy_from_slice(&VERSION_ZEROED_GRAIN.to_le_bytes());
181 let hdr = SparseExtentHeader::parse(&h).expect("v2 parses");
182 assert_eq!(hdr.version, 2);
183 }
184
185 #[test]
186 fn parse_rejects_grain_size_below_minimum() {
187 let mut h = valid_header();
188 h[20..28].copy_from_slice(&4u64.to_le_bytes()); assert!(matches!(
190 SparseExtentHeader::parse(&h),
191 Err(VmdkError::FieldOutOfRange {
192 field: "grain_size",
193 value: 4,
194 ..
195 })
196 ));
197 }
198
199 #[test]
200 fn parse_rejects_grain_size_above_maximum() {
201 let mut h = valid_header();
204 let oversized = MAX_GRAIN_SIZE_SECTORS + 1;
205 h[20..28].copy_from_slice(&oversized.to_le_bytes());
206 assert!(matches!(
207 SparseExtentHeader::parse(&h),
208 Err(VmdkError::FieldOutOfRange {
209 field: "grain_size",
210 ..
211 })
212 ));
213 }
214
215 #[test]
216 fn parse_rejects_zero_num_gtes() {
217 let mut h = valid_header();
218 h[44..48].copy_from_slice(&0u32.to_le_bytes());
219 assert!(matches!(
220 SparseExtentHeader::parse(&h),
221 Err(VmdkError::FieldOutOfRange {
222 field: "num_gtes_per_gt",
223 value: 0,
224 ..
225 })
226 ));
227 }
228
229 #[test]
230 fn parse_rejects_num_gtes_above_spec_max() {
231 let mut h = valid_header();
236 h[44..48].copy_from_slice(&513u32.to_le_bytes());
237 assert!(matches!(
238 SparseExtentHeader::parse(&h),
239 Err(VmdkError::FieldOutOfRange {
240 field: "num_gtes_per_gt",
241 value: 513,
242 ..
243 })
244 ));
245
246 let mut h = valid_header();
248 h[44..48].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
249 assert!(matches!(
250 SparseExtentHeader::parse(&h),
251 Err(VmdkError::FieldOutOfRange {
252 field: "num_gtes_per_gt",
253 value: 0xFFFF_FFFF,
254 ..
255 })
256 ));
257 }
258
259 #[test]
260 fn parse_accepts_num_gtes_at_spec_max() {
261 let mut h = valid_header();
263 h[44..48].copy_from_slice(&512u32.to_le_bytes());
264 assert!(SparseExtentHeader::parse(&h).is_ok());
265 }
266
267 #[test]
268 fn parse_rejects_compressed_flag_on_v1() {
269 let mut h = valid_header();
270 h[77..79].copy_from_slice(&1u16.to_le_bytes()); assert!(matches!(
272 SparseExtentHeader::parse(&h),
273 Err(VmdkError::CompressedNotSupported)
274 ));
275 }
276}