hvlite_pcat_locator/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Helper code for finding PCAT binaries.
5
6#![forbid(unsafe_code)]
7
8use anyhow::Context;
9use mesh::MeshPayload;
10use std::fs::File;
11use std::path::Path;
12use std::path::PathBuf;
13use std::process::Command;
14
15mod resource_dll_parser;
16
17#[derive(Debug, MeshPayload)]
18/// The location of discovered ROM data.
19pub struct RomFileLocation {
20    /// The opened file containing the data.
21    pub file: File,
22    /// The starting byte offset of the data.
23    pub start: u64,
24    /// The length of the data.
25    pub len: usize,
26}
27
28/// Returns path to the "Windows\System32" directory.
29fn system32_path() -> String {
30    // Other approaches could be using the WinDir env. variable,
31    // or the unsafe `GetWindowsDirectoryW` function from the `windows` crate.
32    let windows_dir = std::env::var("SystemRoot").unwrap_or(String::from(r"C:\Windows"));
33    format!("{windows_dir}\\System32")
34}
35
36/// Attempt to automatically find and open the PCAT BIOS. Will always prefer
37/// the more recently updated vmfirmwarepcat.dll over vmfirmware.dll.
38pub fn find_pcat_bios(command_line_path: Option<&Path>) -> anyhow::Result<RomFileLocation> {
39    const NAME: &str = "pcat_firmware";
40    const BIOS_DESCRIPTOR: DllResourceDescriptor = DllResourceDescriptor::new(b"VMFW", 13500);
41    const EXPECTED_BIOS_SIZE: usize = 256 * 1024;
42
43    if let Some(p) = command_line_path {
44        parse_rom_file(p, NAME, BIOS_DESCRIPTOR, EXPECTED_BIOS_SIZE)
45    } else {
46        let system32_path = system32_path();
47        // Newer windows hosts have a specific file for pcat firmware.
48        let default_pcat_firmware_file = format!(r"{system32_path}\vmfirmwarepcat.dll");
49
50        let result = match parse_rom_file(
51            &translate_path(Path::new(default_pcat_firmware_file.as_str()))?,
52            NAME,
53            BIOS_DESCRIPTOR,
54            EXPECTED_BIOS_SIZE,
55        ) {
56            Ok(r) => r,
57            Err(_) => {
58                // Older hosts have a single file for both pcat and uefi.
59                let legacy_pcat_firmware_file = format!(r"{system32_path}\vmfirmware.dll");
60
61                parse_rom_file(
62                    &translate_path(Path::new(legacy_pcat_firmware_file.as_str()))?,
63                    NAME,
64                    BIOS_DESCRIPTOR,
65                    EXPECTED_BIOS_SIZE,
66                )?
67            }
68        };
69        Ok(result)
70    }
71}
72
73/// Attempt to automatically find and open the SVGA video device BIOS from
74/// vmemulateddevices.dll.
75pub fn find_svga_bios(command_line_path: Option<&Path>) -> anyhow::Result<RomFileLocation> {
76    const SVGA_BIOS_DESCRIPTOR: DllResourceDescriptor = DllResourceDescriptor::new(b"BIOS", 13501);
77    const NAME: &str = "vga_firmware";
78    const EXPECTED_SIZE: usize = 48 * 1024;
79
80    if let Some(p) = command_line_path {
81        parse_rom_file(p, NAME, SVGA_BIOS_DESCRIPTOR, EXPECTED_SIZE)
82    } else {
83        let system32_path = system32_path();
84        // TODO: Also load the splash screen from the same dll?
85        let default_svga_firmware_file = format!(r"{system32_path}\vmemulateddevices.dll");
86
87        parse_rom_file(
88            &translate_path(Path::new(default_svga_firmware_file.as_str()))?,
89            NAME,
90            SVGA_BIOS_DESCRIPTOR,
91            EXPECTED_SIZE,
92        )
93    }
94}
95
96/// Translate a Windows path to an OS-appropriate path.
97fn translate_path(path: &Path) -> anyhow::Result<PathBuf> {
98    let file_path = if cfg!(windows) {
99        path.into()
100    } else if cfg!(target_os = "linux") {
101        // WSL
102        let output = Command::new("wslpath")
103            .arg(path)
104            .output()
105            .context("Failed to translate path to windows. Are you not on WSL?")?;
106
107        String::from_utf8_lossy(&output.stdout).trim().into()
108    } else {
109        anyhow::bail!("No path specified for firmware and no default is configured for this OS.")
110    };
111
112    Ok(file_path)
113}
114
115/// Reads the ROM data from the given file and returns it.
116/// Autodetects if the file is a dll or not, and attempts to parse appropriately.
117fn parse_rom_file(
118    file_path: &Path,
119    firmware_name: &str,
120    descriptor: DllResourceDescriptor,
121    expected_len: usize,
122) -> anyhow::Result<RomFileLocation> {
123    tracing::debug!(
124        ?file_path,
125        ?firmware_name,
126        "Attempting to load firmware file."
127    );
128
129    let file = fs_err::File::open(file_path)?;
130
131    let (start, len) = if let Some(maybe_resource) =
132        resource_dll_parser::try_find_resource_from_dll(&file, &descriptor)?
133    {
134        maybe_resource
135    } else {
136        (0, file.metadata()?.len() as usize)
137    };
138
139    if len != expected_len {
140        tracing::warn!(
141            firmware_name,
142            len,
143            expected_len,
144            "ROM data length does not match expected length, trying anyways",
145        );
146    }
147
148    Ok(RomFileLocation {
149        file: file.into(),
150        start,
151        len,
152    })
153}
154
155pub(crate) struct DllResourceDescriptor {
156    /// 4 characters encoded in LE UTF-16
157    resource_type: [u8; 8],
158    id: u32,
159}
160
161impl DllResourceDescriptor {
162    const fn new(resource_type: &[u8; 4], id: u32) -> Self {
163        Self {
164            id,
165            // Convert to LE UTF-16, only support ASCII names today
166            resource_type: [
167                resource_type[0],
168                0,
169                resource_type[1],
170                0,
171                resource_type[2],
172                0,
173                resource_type[3],
174                0,
175            ],
176        }
177    }
178}