igvmfilegen/
firmware_dll.rs1use anyhow::Context;
16use std::path::Path;
17
18const VMFW_RESOURCE_TYPE: &str = "VMFW";
21
22pub fn read_igvm_image(path: &Path) -> anyhow::Result<Vec<u8>> {
30 let bytes = fs_err::read(path).context("reading input file")?;
31 if bytes.starts_with(b"MZ") {
32 extract_vmfw_resource(&bytes).with_context(|| {
33 format!(
34 "extracting embedded IGVM ({VMFW_RESOURCE_TYPE} resource) from resource DLL {}",
35 path.display()
36 )
37 })
38 } else {
39 Ok(bytes)
40 }
41}
42
43fn extract_vmfw_resource(pe_bytes: &[u8]) -> anyhow::Result<Vec<u8>> {
45 use object::read::pe::PeFile64;
46 use object::read::pe::ResourceDirectoryEntryData;
47 use object::read::pe::ResourceNameOrId;
48
49 let pe =
50 PeFile64::parse(pe_bytes).context("parsing PE image (expected a 64-bit resource DLL)")?;
51 let sections = pe.section_table();
52 let dir = pe
53 .data_directories()
54 .resource_directory(pe_bytes, §ions)
55 .context("reading resource directory")?
56 .context("PE image has no resource directory")?;
57 let root = dir.root().context("reading resource directory root")?;
58
59 for type_entry in root.entries {
63 let ResourceNameOrId::Name(name) = type_entry.name_or_id() else {
64 continue;
66 };
67 let raw_name = name.raw_data(dir).context("reading resource type name")?;
68 if !utf16le_eq(raw_name, VMFW_RESOURCE_TYPE) {
69 continue;
70 }
71
72 let ResourceDirectoryEntryData::Table(id_table) = type_entry
73 .data(dir)
74 .context("reading VMFW resource id table")?
75 else {
76 anyhow::bail!("VMFW resource type entry is not a subdirectory");
77 };
78 let id_entry = id_table
79 .entries
80 .first()
81 .context("VMFW resource type has no entries")?;
82
83 let ResourceDirectoryEntryData::Table(lang_table) = id_entry
84 .data(dir)
85 .context("reading VMFW resource language table")?
86 else {
87 anyhow::bail!("VMFW resource id entry is not a subdirectory");
88 };
89 let lang_entry = lang_table
90 .entries
91 .first()
92 .context("VMFW resource has no language entries")?;
93
94 let ResourceDirectoryEntryData::Data(data) = lang_entry
95 .data(dir)
96 .context("reading VMFW resource data entry")?
97 else {
98 anyhow::bail!("VMFW resource language entry is not a data entry");
99 };
100
101 let rva = data.offset_to_data.get(object::LittleEndian);
104 let size = data.size.get(object::LittleEndian) as usize;
105 let payload = sections
106 .pe_data_at(pe_bytes, rva)
107 .context("resolving VMFW resource RVA in section table")?
108 .get(..size)
109 .context("VMFW resource size extends past its section")?;
110 return Ok(payload.to_vec());
111 }
112
113 anyhow::bail!(
114 "no '{VMFW_RESOURCE_TYPE}' resource found; input does not look like a vmfirmwareigvm resource DLL"
115 )
116}
117
118fn utf16le_eq(utf16le: &[u8], ascii: &str) -> bool {
120 if utf16le.len() != ascii.len() * 2 {
121 return false;
122 }
123 utf16le
124 .chunks_exact(2)
125 .zip(ascii.chars())
126 .all(|(pair, c)| u16::from_le_bytes([pair[0], pair[1]]) == c as u16)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::utf16le_eq;
132
133 fn utf16le(s: &str) -> Vec<u8> {
134 s.encode_utf16().flat_map(u16::to_le_bytes).collect()
135 }
136
137 #[test]
138 fn utf16le_eq_matches() {
139 assert!(utf16le_eq(&utf16le("VMFW"), "VMFW"));
140 }
141
142 #[test]
143 fn utf16le_eq_rejects_mismatch_and_length() {
144 assert!(!utf16le_eq(&utf16le("VMFX"), "VMFW"));
145 assert!(!utf16le_eq(&utf16le("VMF"), "VMFW"));
146 assert!(!utf16le_eq(&utf16le("VMFWX"), "VMFW"));
147 assert!(!utf16le_eq(&[0x56, 0x00, 0x4d], "VM"));
149 }
150}