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 GD_AT_END: u64 = 0xffff_ffff_ffff_ffff;
25
26pub struct SparseExtentHeader {
28 pub version: u32, pub capacity: u64, pub grain_size: u64, pub descriptor_offset: u64, pub descriptor_size: u64, pub num_gtes_per_gt: u32,
34 pub rgd_offset: u64, pub gd_offset: u64, pub compressed: bool,
38}
39
40impl SparseExtentHeader {
41 pub fn parse(data: &[u8]) -> Result<Self> {
42 if data.len() < 512 {
43 return Err(VmdkError::FileTooSmall);
44 }
45
46 let magic = u32::from_le_bytes(data[0..4].try_into().expect("4 bytes"));
47 if magic != MAGIC {
48 return Err(VmdkError::BadMagic);
49 }
50
51 let version = u32::from_le_bytes(data[4..8].try_into().expect("4 bytes"));
52 if version != VERSION && version != VERSION_ZEROED_GRAIN && version != VERSION_STREAM_OPT {
55 return Err(VmdkError::UnsupportedVersion(version));
56 }
57
58 let capacity = u64::from_le_bytes(data[12..20].try_into().expect("8 bytes"));
59 let grain_size = u64::from_le_bytes(data[20..28].try_into().expect("8 bytes"));
60 let descriptor_offset = u64::from_le_bytes(data[28..36].try_into().expect("8 bytes"));
61 let descriptor_size = u64::from_le_bytes(data[36..44].try_into().expect("8 bytes"));
62 let num_gtes_per_gt = u32::from_le_bytes(data[44..48].try_into().expect("4 bytes"));
63 let rgd_offset = u64::from_le_bytes(data[48..56].try_into().expect("8 bytes"));
64 let gd_offset = u64::from_le_bytes(data[56..64].try_into().expect("8 bytes"));
65 let compress_algorithm = u16::from_le_bytes(data[77..79].try_into().expect("2 bytes"));
66
67 match (version, compress_algorithm) {
73 (VERSION | VERSION_ZEROED_GRAIN, 0) | (VERSION_STREAM_OPT, 1) => {}
74 _ => return Err(VmdkError::CompressedNotSupported),
75 }
76
77 if grain_size < 8 {
80 return Err(VmdkError::FieldOutOfRange {
81 field: "grain_size",
82 value: grain_size,
83 reason: "must be >= 8 sectors (VDF 1.1 §4.1)",
84 });
85 }
86 if num_gtes_per_gt == 0 {
87 return Err(VmdkError::FieldOutOfRange {
88 field: "num_gtes_per_gt",
89 value: u64::from(num_gtes_per_gt),
90 reason: "must be > 0",
91 });
92 }
93 if num_gtes_per_gt > MAX_NUM_GTES_PER_GT {
98 return Err(VmdkError::FieldOutOfRange {
99 field: "num_gtes_per_gt",
100 value: u64::from(num_gtes_per_gt),
101 reason: "exceeds the spec maximum of 512",
102 });
103 }
104
105 Ok(SparseExtentHeader {
106 version,
107 capacity,
108 grain_size,
109 descriptor_offset,
110 descriptor_size,
111 num_gtes_per_gt,
112 rgd_offset,
113 gd_offset,
114 compressed: compress_algorithm != 0,
115 })
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn valid_header() -> Vec<u8> {
124 let mut h = vec![0u8; 512];
125 h[0..4].copy_from_slice(&MAGIC.to_le_bytes());
126 h[4..8].copy_from_slice(&VERSION.to_le_bytes());
127 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
131 }
132
133 #[test]
134 fn parse_rejects_short_buffer() {
135 assert!(matches!(
136 SparseExtentHeader::parse(&[0u8; 100]),
137 Err(VmdkError::FileTooSmall)
138 ));
139 }
140
141 #[test]
142 fn parse_rejects_bad_magic() {
143 let h = vec![0u8; 512];
144 assert!(matches!(
145 SparseExtentHeader::parse(&h),
146 Err(VmdkError::BadMagic)
147 ));
148 }
149
150 #[test]
151 fn parse_rejects_unsupported_version() {
152 let mut h = valid_header();
153 h[4..8].copy_from_slice(&4u32.to_le_bytes()); assert!(matches!(
155 SparseExtentHeader::parse(&h),
156 Err(VmdkError::UnsupportedVersion(4))
157 ));
158 }
159
160 #[test]
161 fn parse_accepts_version_2() {
162 let mut h = valid_header();
163 h[4..8].copy_from_slice(&VERSION_ZEROED_GRAIN.to_le_bytes());
164 let hdr = SparseExtentHeader::parse(&h).expect("v2 parses");
165 assert_eq!(hdr.version, 2);
166 }
167
168 #[test]
169 fn parse_rejects_grain_size_below_minimum() {
170 let mut h = valid_header();
171 h[20..28].copy_from_slice(&4u64.to_le_bytes()); assert!(matches!(
173 SparseExtentHeader::parse(&h),
174 Err(VmdkError::FieldOutOfRange {
175 field: "grain_size",
176 value: 4,
177 ..
178 })
179 ));
180 }
181
182 #[test]
183 fn parse_rejects_zero_num_gtes() {
184 let mut h = valid_header();
185 h[44..48].copy_from_slice(&0u32.to_le_bytes());
186 assert!(matches!(
187 SparseExtentHeader::parse(&h),
188 Err(VmdkError::FieldOutOfRange {
189 field: "num_gtes_per_gt",
190 value: 0,
191 ..
192 })
193 ));
194 }
195
196 #[test]
197 fn parse_rejects_num_gtes_above_spec_max() {
198 let mut h = valid_header();
203 h[44..48].copy_from_slice(&513u32.to_le_bytes());
204 assert!(matches!(
205 SparseExtentHeader::parse(&h),
206 Err(VmdkError::FieldOutOfRange {
207 field: "num_gtes_per_gt",
208 value: 513,
209 ..
210 })
211 ));
212
213 let mut h = valid_header();
215 h[44..48].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
216 assert!(matches!(
217 SparseExtentHeader::parse(&h),
218 Err(VmdkError::FieldOutOfRange {
219 field: "num_gtes_per_gt",
220 value: 0xFFFF_FFFF,
221 ..
222 })
223 ));
224 }
225
226 #[test]
227 fn parse_accepts_num_gtes_at_spec_max() {
228 let mut h = valid_header();
230 h[44..48].copy_from_slice(&512u32.to_le_bytes());
231 assert!(SparseExtentHeader::parse(&h).is_ok());
232 }
233
234 #[test]
235 fn parse_rejects_compressed_flag_on_v1() {
236 let mut h = valid_header();
237 h[77..79].copy_from_slice(&1u16.to_le_bytes()); assert!(matches!(
239 SparseExtentHeader::parse(&h),
240 Err(VmdkError::CompressedNotSupported)
241 ));
242 }
243}