isr_dl_windows/
codeview.rs1use std::path::{Path, PathBuf};
2
3use object::{
4 FileKind, Object,
5 read::pe::{ImageNtHeaders, PeFile, PeFile32, PeFile64},
6};
7
8use super::DownloaderError;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct CodeView {
15 pub name: String,
19
20 pub guid: String,
22
23 pub age: u32,
25}
26
27impl CodeView {
28 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 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 pub fn filename(&self) -> &str {
81 match self.name.rsplit_once(['\\', '/']) {
82 Some((_, filename)) => filename,
83 None => &self.name,
84 }
85 }
86
87 pub fn hash(&self) -> String {
90 format!("{}{:x}", self.guid, self.age)
91 }
92
93 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}