Skip to main content

vhd/
footer.rs

1//! VHD footer parsing (MS-VHD §2.1).
2//!
3//! The footer is a 512-byte structure at the end of every VHD file.
4//! Fixed disks also have a copy at byte 0 of the file.
5//! Dynamic disks have a copy at byte 0 and the real footer at the very end.
6
7use crate::error::{Result, VhdError};
8use crate::read::{be_u32, be_u64};
9
10pub const FOOTER_SIZE: usize = 512;
11pub const COOKIE: &[u8; 8] = b"conectix";
12pub const CURRENT_VERSION: u32 = 0x0001_0000;
13
14/// VHD disk type codes (§2.1, DiskType field).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DiskType {
17    Fixed = 2,
18    Dynamic = 3,
19    // Differencing = 4 — rejected at open time
20}
21
22/// Parsed VHD footer fields relevant to container reading.
23#[derive(Debug, Clone)]
24pub struct VhdFooter {
25    pub disk_type: DiskType,
26    pub current_size: u64, // CurrentSize (offset 48) — current/readable virtual size
27    pub original_size: u64, // OriginalSize (offset 40) — size at creation
28    pub data_offset: u64,  // offset to dynamic header (0xFFFF... for fixed)
29}
30
31impl VhdFooter {
32    /// Parse the last 512 bytes of `data` as a VHD footer.
33    pub fn parse(data: &[u8]) -> Result<Self> {
34        if data.len() < FOOTER_SIZE {
35            return Err(VhdError::FileTooSmall);
36        }
37        let footer = &data[data.len() - FOOTER_SIZE..];
38
39        // Cookie: bytes 0–7
40        if &footer[0..8] != COOKIE {
41            return Err(VhdError::BadCookie);
42        }
43
44        // Version: bytes 12–15
45        let version = be_u32(footer, 12);
46        if version != CURRENT_VERSION {
47            return Err(VhdError::UnsupportedVersion(version));
48        }
49
50        // DataOffset: bytes 16–23
51        let data_offset = be_u64(footer, 16);
52
53        // CurrentSize (virtual disk size): bytes 48–55 (MS-VHD §2.1).
54        // OriginalSize is bytes 40–47; the two differ on a resized disk.
55        let current_size = be_u64(footer, 48);
56        // OriginalSize (offset 40) — creation size; libvhdi reports this as media size.
57        let original_size = be_u64(footer, 40);
58
59        // DiskType: bytes 60–63
60        let disk_type_raw = be_u32(footer, 60);
61        let disk_type = match disk_type_raw {
62            2 => DiskType::Fixed,
63            3 => DiskType::Dynamic,
64            4 => return Err(VhdError::DifferencingNotSupported),
65            other => return Err(VhdError::UnknownDiskType(other)),
66        };
67
68        // Checksum: bytes 64–67
69        let stored_checksum = be_u32(footer, 64);
70        let computed = checksum(footer);
71        if stored_checksum != computed {
72            return Err(VhdError::ChecksumMismatch {
73                expected: stored_checksum,
74                actual: computed,
75            });
76        }
77
78        Ok(VhdFooter {
79            disk_type,
80            current_size,
81            original_size,
82            data_offset,
83        })
84    }
85}
86
87/// One's-complement checksum over the footer with the checksum field zeroed.
88fn checksum(footer: &[u8]) -> u32 {
89    let mut sum: u32 = 0;
90    for (i, &byte) in footer.iter().enumerate() {
91        // Skip the checksum field itself (bytes 64–67)
92        if (64..68).contains(&i) {
93            continue;
94        }
95        sum = sum.wrapping_add(u32::from(byte));
96    }
97    !sum
98}
99
100// ── Test helpers ─────────────────────────────────────────────────────────────
101
102/// Build a minimal valid Fixed VHD footer for testing.
103///
104/// Sets the cookie, version, current_size, disk_type=Fixed, data_offset=0xFFFF,
105/// and a valid checksum. All other fields are zeroed.
106#[cfg(any(test, feature = "test-helpers"))]
107pub fn test_fixed_footer(virtual_size: u64) -> Vec<u8> {
108    let mut footer = vec![0u8; FOOTER_SIZE];
109
110    // Cookie
111    footer[0..8].copy_from_slice(COOKIE);
112    // Features: reserved (0x0000_0002)
113    footer[8..12].copy_from_slice(&0x0000_0002u32.to_be_bytes());
114    // FileFormatVersion
115    footer[12..16].copy_from_slice(&CURRENT_VERSION.to_be_bytes());
116    // DataOffset: 0xFFFF_FFFF_FFFF_FFFF for Fixed
117    footer[16..24].copy_from_slice(&0xFFFF_FFFF_FFFF_FFFFu64.to_be_bytes());
118    // OriginalSize (offset 40)
119    footer[40..48].copy_from_slice(&virtual_size.to_be_bytes());
120    // CurrentSize (offset 48)
121    footer[48..56].copy_from_slice(&virtual_size.to_be_bytes());
122    // DiskType: Fixed = 2
123    footer[60..64].copy_from_slice(&2u32.to_be_bytes());
124    // Checksum (computed with checksum field zeroed)
125    let cs = checksum(&footer);
126    footer[64..68].copy_from_slice(&cs.to_be_bytes());
127
128    footer
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    fn base() -> Vec<u8> {
136        test_fixed_footer(1024)
137    }
138
139    #[test]
140    fn too_small_is_file_too_small() {
141        assert!(matches!(
142            VhdFooter::parse(&[0u8; 100]),
143            Err(VhdError::FileTooSmall)
144        ));
145    }
146
147    #[test]
148    fn bad_cookie_rejected() {
149        let mut f = base();
150        f[0] = b'X';
151        assert!(matches!(VhdFooter::parse(&f), Err(VhdError::BadCookie)));
152    }
153
154    #[test]
155    fn unsupported_version_rejected() {
156        let mut f = base();
157        f[12..16].copy_from_slice(&0x0002_0000u32.to_be_bytes());
158        assert!(matches!(
159            VhdFooter::parse(&f),
160            Err(VhdError::UnsupportedVersion(0x0002_0000))
161        ));
162    }
163
164    #[test]
165    fn differencing_rejected() {
166        let mut f = base();
167        f[60..64].copy_from_slice(&4u32.to_be_bytes());
168        assert!(matches!(
169            VhdFooter::parse(&f),
170            Err(VhdError::DifferencingNotSupported)
171        ));
172    }
173
174    #[test]
175    fn unknown_disk_type_rejected() {
176        let mut f = base();
177        f[60..64].copy_from_slice(&99u32.to_be_bytes());
178        assert!(matches!(
179            VhdFooter::parse(&f),
180            Err(VhdError::UnknownDiskType(99))
181        ));
182    }
183
184    #[test]
185    fn checksum_mismatch_rejected() {
186        let mut f = base();
187        f[100] ^= 0xFF; // reserved byte — cookie/version/type still valid
188        assert!(matches!(
189            VhdFooter::parse(&f),
190            Err(VhdError::ChecksumMismatch { .. })
191        ));
192    }
193}