Skip to main content

isr_dl_windows/
codeview.rs

1use std::path::{Path, PathBuf};
2
3use object::{
4    FileKind, Object,
5    read::pe::{ImageNtHeaders, PeFile, PeFile32, PeFile64},
6};
7
8use super::DownloaderError;
9
10/// Identifies the PDB matching a specific PE binary.
11///
12/// Parsed from the CodeView entry in the PE's debug directory.
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct CodeView {
15    /// The PDB path as recorded by the linker.
16    ///
17    /// Can be a full build path or just a filename.
18    pub name: String,
19
20    /// PDB signature GUID.
21    pub guid: String,
22
23    /// PDB age counter.
24    pub age: u32,
25}
26
27impl CodeView {
28    /// Extracts CodeView info from a parsed PE file. Returns
29    /// [`DownloaderError::MissingCodeView`] if the PE has no debug directory.
30    pub fn from_pe<Pe>(pe: &PeFile<Pe>) -> Result<Self, DownloaderError>
31    where
32        Pe: ImageNtHeaders,
33    {
34        let cv = match pe.pdb_info()? {
35            Some(cv) => cv,
36            None => return Err(DownloaderError::MissingCodeView),
37        };
38
39        let guid = cv.guid();
40        let age = cv.age();
41        let path = cv.path();
42
43        let guid0 = u32::from_le_bytes(guid[0..4].try_into().unwrap());
44        let guid1 = u16::from_le_bytes(guid[4..6].try_into().unwrap());
45        let guid2 = u16::from_le_bytes(guid[6..8].try_into().unwrap());
46        let guid3 = &guid[8..16];
47
48        Ok(Self {
49            name: String::from_utf8_lossy(path).to_string(),
50            guid: format!(
51                "{:08x}{:04x}{:04x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
52                guid0,
53                guid1,
54                guid2,
55                guid3[0],
56                guid3[1],
57                guid3[2],
58                guid3[3],
59                guid3[4],
60                guid3[5],
61                guid3[6],
62                guid3[7],
63            ),
64            age: age & 0xf,
65        })
66    }
67
68    /// Reads the PE file at `path` and extracts its CodeView info.
69    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, DownloaderError> {
70        let data = std::fs::read(path)?;
71
72        match FileKind::parse(&data[..])? {
73            FileKind::Pe32 => Self::from_pe(&PeFile32::parse(&data[..])?),
74            FileKind::Pe64 => Self::from_pe(&PeFile64::parse(&data[..])?),
75            kind => Err(DownloaderError::UnsupportedFileKind(kind)),
76        }
77    }
78
79    /// Returns the PDB filename without any directory components.
80    pub fn filename(&self) -> &str {
81        match self.name.rsplit_once(['\\', '/']) {
82            Some((_, filename)) => filename,
83            None => &self.name,
84        }
85    }
86
87    /// Returns the symbol-server lookup key `<guid><age>` used as the path
88    /// segment between the PDB name and the downloaded file.
89    pub fn hash(&self) -> String {
90        format!("{}{:x}", self.guid, self.age)
91    }
92
93    /// Returns the relative output directory for this PDB:
94    /// `<filename>/<hash>`.
95    pub fn subdirectory(&self) -> PathBuf {
96        PathBuf::from(self.filename()).join(self.hash())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn sample() -> CodeView {
105        CodeView {
106            name: "kernel32.pdb".into(),
107            guid: "1b72224d37b8179228200ed8994498b2".into(),
108            age: 1,
109        }
110    }
111
112    #[test]
113    fn hash_concatenates_guid_and_age_in_hex() {
114        assert_eq!(sample().hash(), "1b72224d37b8179228200ed8994498b21");
115    }
116
117    #[test]
118    fn hash_formats_age_as_lowercase_hex() {
119        let cv = CodeView {
120            age: 0xab,
121            ..sample()
122        };
123        assert!(cv.hash().ends_with("ab"));
124    }
125
126    #[test]
127    fn subdirectory_joins_filename_and_hash() {
128        assert_eq!(
129            sample().subdirectory(),
130            PathBuf::from("kernel32.pdb").join("1b72224d37b8179228200ed8994498b21")
131        );
132    }
133
134    #[test]
135    fn filename_strips_windows_style_path() {
136        let cv = CodeView {
137            name: r"D:\a\_work\1\s\msvcp140.amd64.pdb".into(),
138            ..sample()
139        };
140        assert_eq!(cv.filename(), "msvcp140.amd64.pdb");
141    }
142
143    #[test]
144    fn filename_strips_unix_style_path() {
145        let cv = CodeView {
146            name: "path/to/foo.pdb".into(),
147            ..sample()
148        };
149        assert_eq!(cv.filename(), "foo.pdb");
150    }
151
152    #[test]
153    fn filename_returns_name_when_no_separator() {
154        assert_eq!(sample().filename(), "kernel32.pdb");
155    }
156
157    #[test]
158    fn subdirectory_uses_filename_not_full_name() {
159        let cv = CodeView {
160            name: r"D:\build\msvcp140.amd64.pdb".into(),
161            ..sample()
162        };
163        assert_eq!(
164            cv.subdirectory(),
165            PathBuf::from("msvcp140.amd64.pdb").join("1b72224d37b8179228200ed8994498b21")
166        );
167    }
168}