Skip to main content

vhdx/medium/
open.rs

1//! Open-options builder implementation.
2
3use bitvec::prelude::*;
4use crc32c::crc32c;
5use std::io::{ErrorKind, Read, Seek, Write};
6
7use super::{
8    CacheEntry, Len, LogReplayPolicy, Medium, OpenOptions, ParentResolver, ReadOnly, ReadWrite,
9    SetLen, SyncData, is_known_metadata_guid, is_known_region_guid, read_exact_at, write_all_at,
10};
11use crate::constants::{
12    HEADER_BUFFER_SIZE, HEADER_SIZE, HEADER1_OFFSET, HEADER2_OFFSET, METADATA_REGION_GUID, MIB,
13    VHDX_SIGNATURE_BYTES,
14};
15use crate::error::{Error, Result, SignaturePosition};
16use crate::header::Header;
17use crate::log::Log;
18use crate::log_replay;
19use crate::types::Guid;
20use std::sync::atomic::AtomicU64;
21use std::sync::{Arc, Mutex, RwLock};
22
23impl<T, Mode> OpenOptions<T, Mode> {
24    fn validate_policy_compatibility(write: bool, policy: LogReplayPolicy) -> Result<()> {
25        match policy {
26            LogReplayPolicy::InMemoryOnReadOnly | LogReplayPolicy::ReadOnlyNoReplay if write => {
27                Err(Error::InvalidParameter(
28                    "log replay policy incompatible with write access".into(),
29                ))
30            }
31            _ => Ok(()),
32        }
33    }
34
35    fn read_header(inner: &mut T) -> Result<Vec<u8>>
36    where
37        T: Read + Seek,
38    {
39        let mut header_buf = vec![0u8; HEADER_BUFFER_SIZE];
40        let mut signature = [0u8; 8];
41        match read_exact_at(inner, 0, &mut signature) {
42            Ok(()) => {}
43            Err(err) if err.kind() == ErrorKind::UnexpectedEof => {
44                return Err(Error::InvalidFile(
45                    "file too small to contain VHDX signature".into(),
46                ));
47            }
48            Err(err) => return Err(err.into()),
49        }
50        match read_exact_at(inner, 0, &mut header_buf) {
51            Ok(()) => {}
52            Err(err) if err.kind() == ErrorKind::UnexpectedEof => {
53                return Err(Error::InvalidFile(format!(
54                    "header section too small: need at least {HEADER_BUFFER_SIZE}"
55                )));
56            }
57            Err(err) => return Err(err.into()),
58        }
59        Ok(header_buf)
60    }
61
62    fn validate_file_signature(header_buf: &[u8]) -> Result<()> {
63        let sig = &header_buf[..VHDX_SIGNATURE_BYTES.len() / 8];
64        if sig.view_bits::<Lsb0>() == *VHDX_SIGNATURE_BYTES {
65            return Ok(());
66        }
67        let mut actual_bytes = [0u8; 8];
68        actual_bytes.copy_from_slice(sig);
69        Err(Error::InvalidSignature {
70            position: SignaturePosition::FileTypeIdentifier,
71            expected: VHDX_SIGNATURE_BYTES.into_inner().to_le_bytes(),
72            found: actual_bytes,
73        })
74    }
75
76    fn validate_current_header(current: &crate::header::HeaderStructure<'_>) -> Result<()> {
77        if current.version() != 1 {
78            return Err(Error::UnsupportedVersion {
79                version: current.version(),
80            });
81        }
82        if current.log_version() != 0 && current.log_guid() != Guid::zero() {
83            return Err(Error::UnsupportedLogVersion {
84                version: current.log_version(),
85            });
86        }
87        Ok(())
88    }
89
90    fn validate_region_table_and_metadata(
91        inner: &mut T, header: &Header, strict: bool,
92    ) -> Result<()>
93    where
94        T: Read + Seek,
95    {
96        let rt = header.region_table(0)?;
97        Self::validate_region_table_entries(&rt, strict)?;
98        Self::validate_unknown_metadata(inner, &rt, strict)
99    }
100
101    fn validate_region_table_entries(
102        rt: &crate::header::RegionTable<'_>, strict: bool,
103    ) -> Result<()> {
104        let entries: Vec<_> = rt.entries().collect();
105        for (i, entry) in entries.iter().enumerate() {
106            let file_offset = entry.file_offset();
107            let length = entry.length();
108            if file_offset % u64::from(MIB) != 0 {
109                return Err(Error::InvalidRegionTable(format!(
110                    "REGION_ENTRY_ALIGNMENT: entry {i} file_offset {file_offset:#x} not 1MB-aligned"
111                )));
112            }
113            if file_offset < u64::from(MIB) {
114                return Err(Error::InvalidRegionTable(format!(
115                    "REGION_ENTRY_OFFSET_MINIMUM: entry {i} file_offset {file_offset} < 1MB minimum"
116                )));
117            }
118            if u64::from(length) % u64::from(MIB) != 0 {
119                return Err(Error::InvalidRegionTable(format!(
120                    "REGION_ENTRY_ALIGNMENT: entry {i} length {length} not 1MB-aligned"
121                )));
122            }
123            let end = file_offset + u64::from(length);
124            for (j, prev) in entries[..i].iter().enumerate() {
125                let prev_end = prev.file_offset() + u64::from(prev.length());
126                if file_offset < prev_end && prev.file_offset() < end {
127                    return Err(Error::InvalidRegionTable(format!(
128                        "REGION_ENTRY_OVERLAP: entries {j} and {i} overlap"
129                    )));
130                }
131            }
132            if !is_known_region_guid(&entry.guid()) {
133                if entry.required() {
134                    return Err(Error::RegionRequiredUnknown { guid: entry.guid() });
135                }
136                if strict {
137                    return Err(Error::RegionOptionalUnknown { guid: entry.guid() });
138                }
139            }
140        }
141        Ok(())
142    }
143
144    fn validate_unknown_metadata(
145        inner: &mut T, rt: &crate::header::RegionTable<'_>, strict: bool,
146    ) -> Result<()>
147    where
148        T: Read + Seek,
149    {
150        for entry in rt.entries() {
151            if entry.guid() != METADATA_REGION_GUID {
152                continue;
153            }
154            let mut meta_data = vec![0u8; entry.length() as usize];
155            read_exact_at(inner, entry.file_offset(), &mut meta_data)?;
156            let meta = crate::metadata::Metadata::new(&meta_data)?;
157            for table_entry in meta.table().entries() {
158                if table_entry.flags().is_required()
159                    && !is_known_metadata_guid(&table_entry.item_id())
160                {
161                    return Err(Error::MetadataRequiredUnknown {
162                        guid: table_entry.item_id(),
163                    });
164                }
165                if strict
166                    && !table_entry.flags().is_required()
167                    && !is_known_metadata_guid(&table_entry.item_id())
168                {
169                    return Err(Error::MetadataOptionalUnknown {
170                        guid: table_entry.item_id(),
171                    });
172                }
173            }
174            break;
175        }
176        Ok(())
177    }
178
179    fn load_log_data(inner: &mut T, offset: u64, length: u32) -> Result<Vec<u8>>
180    where
181        T: Read + Seek,
182    {
183        let mut log_data = vec![0u8; length as usize];
184        read_exact_at(inner, offset, &mut log_data)?;
185        Ok(log_data)
186    }
187
188    fn apply_writable_header_update(
189        write: bool, inner: &mut T, header_buf: &mut Vec<u8>,
190    ) -> Result<()>
191    where
192        T: Write + Seek + SyncData,
193    {
194        if !write {
195            return Ok(());
196        }
197        if header_buf.len() < HEADER_BUFFER_SIZE {
198            header_buf.resize(HEADER_BUFFER_SIZE, 0);
199        }
200        let hdr = Header::new(header_buf)?;
201        let h1 = hdr.header(1)?;
202        let h2 = hdr.header(2)?;
203        let current_idx = if h1.sequence_number() > h2.sequence_number() {
204            1
205        } else {
206            2
207        };
208        let noncurrent_idx = if current_idx == 1 { 2 } else { 1 };
209        let noncurrent_offset = if noncurrent_idx == 1 {
210            u64::from(HEADER1_OFFSET)
211        } else {
212            u64::from(HEADER2_OFFSET)
213        };
214        let current_header = hdr.header(0)?;
215        let updated_header = Self::build_updated_header(&current_header);
216        write_all_at(inner, noncurrent_offset, &updated_header)?;
217        inner.sync_data()?;
218        let start = usize::try_from(noncurrent_offset).unwrap();
219        header_buf[start..start + HEADER_SIZE as usize].copy_from_slice(&updated_header);
220        Ok(())
221    }
222
223    fn build_updated_header(
224        current_header: &crate::header::HeaderStructure<'_>,
225    ) -> [u8; HEADER_SIZE as usize] {
226        let mut updated_header = [0u8; HEADER_SIZE as usize];
227        updated_header[..4].copy_from_slice(b"head");
228        updated_header[4..8].copy_from_slice(&0u32.to_le_bytes());
229        updated_header[8..16]
230            .copy_from_slice(&(current_header.sequence_number() + 1).to_le_bytes());
231        updated_header[16..32].copy_from_slice(&Guid::new_v4().to_bytes());
232        updated_header[32..48].copy_from_slice(&current_header.data_write_guid().to_bytes());
233        updated_header[48..64].copy_from_slice(&current_header.log_guid().to_bytes());
234        updated_header[64..66].copy_from_slice(&current_header.log_version().to_le_bytes());
235        updated_header[66..68].copy_from_slice(&current_header.version().to_le_bytes());
236        updated_header[68..72].copy_from_slice(&current_header.log_length().to_le_bytes());
237        updated_header[72..80].copy_from_slice(&current_header.log_offset().to_le_bytes());
238        let checksum = crc32c(&updated_header);
239        updated_header[4..8].copy_from_slice(&checksum.to_le_bytes());
240        updated_header
241    }
242
243    #[must_use]
244    pub fn strict(mut self, strict: bool) -> Self {
245        self.strict = strict;
246        self
247    }
248
249    #[must_use]
250    pub fn log_replay(mut self, policy: LogReplayPolicy) -> Self {
251        self.log_replay_policy = policy;
252        self
253    }
254
255    /// Configure the resolver used for differencing disk parent reads.
256    #[must_use]
257    pub fn with_parent_resolver<R>(mut self, resolver: R) -> Self
258    where
259        R: ParentResolver + Send + 'static,
260    {
261        self.parent_resolver = Some(Box::new(resolver));
262        self
263    }
264}
265
266impl<T> OpenOptions<T, ReadOnly> {
267    #[must_use]
268    pub fn write(self) -> OpenOptions<T, ReadWrite>
269    where
270        T: Read + Write + Seek + Len + SetLen + SyncData,
271    {
272        OpenOptions {
273            inner: self.inner,
274            strict: self.strict,
275            log_replay_policy: self.log_replay_policy,
276            parent_resolver: self.parent_resolver,
277            _mode: std::marker::PhantomData,
278        }
279    }
280
281    /// Open the medium in read-only mode.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if the medium is not a valid VHDX file, validation fails,
286    /// or the selected log replay policy cannot be satisfied.
287    pub fn finish(mut self) -> Result<Medium<T>>
288    where
289        T: Read + Seek,
290    {
291        Self::validate_policy_compatibility(false, self.log_replay_policy)?;
292        let strict = self.strict;
293        let log_replay_policy = self.log_replay_policy;
294        let mut header_buf = Self::read_header(&mut self.inner)?;
295        Self::validate_file_signature(&header_buf)?;
296        let header = Header::new(&header_buf)?;
297        let current = header.header(0)?;
298        Self::validate_current_header(&current)?;
299        let log_offset = current.log_offset();
300        let log_length = current.log_length();
301        let log_guid = current.log_guid();
302        Self::validate_region_table_and_metadata(&mut self.inner, &header, strict)?;
303        let log_data = Self::load_log_data(&mut self.inner, log_offset, log_length)?;
304
305        let replay_overlay = match log_replay_policy {
306            LogReplayPolicy::Require => {
307                let log = Log::new(&log_data)?;
308                if log_replay::has_pending_log(&log, &log_guid) {
309                    return Err(Error::LogReplayRequired);
310                }
311                None
312            }
313            LogReplayPolicy::Auto | LogReplayPolicy::InMemoryOnReadOnly => {
314                let log = Log::new(&log_data)?;
315                if log_replay::has_pending_log(&log, &log_guid) {
316                    let active = log_replay::detect_active_sequence(&log, &log_guid)?;
317                    Some(Arc::new(log_replay::build_replay_overlay(&active)?))
318                } else {
319                    None
320                }
321            }
322            LogReplayPolicy::ReadOnlyNoReplay => None,
323        };
324
325        if let Some(ref overlay) = replay_overlay {
326            if header_buf.len() < HEADER_BUFFER_SIZE {
327                header_buf.resize(HEADER_BUFFER_SIZE, 0);
328            }
329            overlay.apply_to_region(&mut header_buf, 0);
330        }
331
332        Ok(Medium {
333            inner: Mutex::new(self.inner),
334            header_buf: RwLock::new(Some(CacheEntry::new(0, Arc::from(header_buf)))),
335            bat_buf: RwLock::new(None),
336            metadata_buf: RwLock::new(None),
337            log_buf: RwLock::new(Some(CacheEntry::new(0, Arc::from(log_data)))),
338            generation: AtomicU64::new(0),
339            write: false,
340            strict,
341            log_replay_policy,
342            replay_overlay,
343            parent_resolver: Mutex::new(self.parent_resolver),
344            validator_buf: RwLock::new(None),
345        })
346    }
347}
348
349impl<T> OpenOptions<T, ReadWrite> {
350    /// Open the medium in read-write mode.
351    ///
352    /// # Errors
353    ///
354    /// Returns an error if the medium is not a valid VHDX file, validation fails,
355    /// or the selected log replay policy is incompatible with write access.
356    pub fn finish(mut self) -> Result<Medium<T>>
357    where
358        T: Read + Write + Seek + Len + SetLen + SyncData,
359    {
360        Self::validate_policy_compatibility(true, self.log_replay_policy)?;
361        let strict = self.strict;
362        let log_replay_policy = self.log_replay_policy;
363        let mut header_buf = Self::read_header(&mut self.inner)?;
364        Self::validate_file_signature(&header_buf)?;
365        let header = Header::new(&header_buf)?;
366        let current = header.header(0)?;
367        Self::validate_current_header(&current)?;
368        let log_offset = current.log_offset();
369        let log_length = current.log_length();
370        let log_guid = current.log_guid();
371        Self::validate_region_table_and_metadata(&mut self.inner, &header, strict)?;
372        let log_data = Self::load_log_data(&mut self.inner, log_offset, log_length)?;
373
374        let replay_overlay = match log_replay_policy {
375            LogReplayPolicy::Require => {
376                let log = Log::new(&log_data)?;
377                if log_replay::has_pending_log(&log, &log_guid) {
378                    return Err(Error::LogReplayRequired);
379                }
380                None
381            }
382            LogReplayPolicy::Auto => {
383                let log = Log::new(&log_data)?;
384                if log_replay::has_pending_log(&log, &log_guid) {
385                    let active = log_replay::detect_active_sequence(&log, &log_guid)?;
386                    let file_size = self.inner.len()?;
387                    if file_size < active.flushed_file_offset() {
388                        return Err(Error::CorruptedHeader(format!(
389                            "file truncated: size {} < FlushedFileOffset {}",
390                            file_size,
391                            active.flushed_file_offset()
392                        )));
393                    }
394                    log_replay::replay_to_file(&mut self.inner, &active)?;
395                }
396                None
397            }
398            LogReplayPolicy::InMemoryOnReadOnly | LogReplayPolicy::ReadOnlyNoReplay => {
399                unreachable!()
400            }
401        };
402
403        Self::apply_writable_header_update(true, &mut self.inner, &mut header_buf)?;
404
405        Ok(Medium {
406            inner: Mutex::new(self.inner),
407            header_buf: RwLock::new(Some(CacheEntry::new(0, Arc::from(header_buf)))),
408            bat_buf: RwLock::new(None),
409            metadata_buf: RwLock::new(None),
410            log_buf: RwLock::new(Some(CacheEntry::new(0, Arc::from(log_data)))),
411            generation: AtomicU64::new(0),
412            write: true,
413            strict,
414            log_replay_policy,
415            replay_overlay,
416            parent_resolver: Mutex::new(self.parent_resolver),
417            validator_buf: RwLock::new(None),
418        })
419    }
420}