1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! Description of a Windows system useful to compute the lookup path

#[cfg(windows)]
extern crate winapi;
use crate::apiset;
use crate::common::LookupError;
#[cfg(windows)]
use crate::knowndlls;
use fs_err as fs;
use std::collections::HashMap;
#[cfg(windows)]
use std::ffi::OsString;
#[cfg(windows)]
use std::os::windows::ffi::OsStringExt;
use std::path::{Path, PathBuf};

/// List of DLLs provided by the operating system and hardcoded into the loader
/// If a DLL with this name is required, the OS will not perform any further lookup but load the
/// copy distributed with Windows
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct KnownDLLList {
    pub entries: HashMap<String, PathBuf>,
}

impl KnownDLLList {
    /// look for a DLL by name among the entries
    pub fn search_dll_in_known_dlls(&self, library: &str) -> Result<Option<PathBuf>, LookupError> {
        if let Some(lp) = self.entries.get(&library.to_ascii_lowercase()) {
            Ok(Some(lp.clone()))
        } else {
            // DLL not found among the KnownDLLs
            Ok(None)
        }
    }
}

// supported DLL search modes: standard for desktop application, safe or unsafe, as specified by the registry (if running on Windows)
// TODO: read HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode  and pick mode accordingly
// the other modes are activated programmatically, and there is no hope to be able to handle that properly
// https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-desktop-applications

/// Description of a Windows system
/// If running from within Windows we extract the available information from the registry, the
/// environment variables and the Windows API.
/// If running in another OS we can only guess the directories, and can't do anything about the PATH
#[derive(Debug, Clone)]
pub struct WindowsSystem {
    pub safe_dll_search_mode_on: Option<bool>,
    pub apiset_map: Option<apiset::ApisetMap>,
    pub known_dlls: Option<KnownDLLList>,
    pub win_dir: PathBuf,
    pub sys_dir: PathBuf,
    // sys16_dir ignored, since it is not supported on 64-bit systems
    pub system_path: Option<Vec<PathBuf>>,
}

impl WindowsSystem {
    /// Collect information about the host operating system
    #[cfg(windows)]
    pub fn current() -> Result<Self, LookupError> {
        // TODO: read dll safe mode on/off from HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode (if it doesn't exist, it's 1)
        let win_dir = get_windows_directory()?;
        let sys_dir = get_system_directory()?;
        let apiset = match apiset::parse_apiset(sys_dir.join("apisetschema.dll")) {
            Ok(apiset) => Some(apiset),
            Err(e) => {
                eprintln!("{:?}", e);
                None
            }
        };

        let path_str = std::env::var("PATH");
        let path = path_str
            .and_then(|s| {
                Ok(s.split(";")
                    .filter_map(|subs| fs::canonicalize(subs).ok())
                    .collect())
            })
            .ok();
        let known_dlls = knowndlls::get_known_dlls().ok().map(|v| KnownDLLList {
            entries: v
                .iter()
                .map(|kd| (kd.to_lowercase(), sys_dir.join(kd)))
                .collect(),
        });
        Ok(Self {
            safe_dll_search_mode_on: None,
            apiset_map: apiset,
            known_dlls,
            win_dir,
            sys_dir,
            system_path: path,
        })
    }

    /// Collect information about the Windows operating system installed on the partition the target
    /// executable lies into
    #[cfg(not(windows))]
    pub fn from_exe_location<P: AsRef<Path>>(p: P) -> Result<Option<Self>, LookupError> {
        if let Some(root) = Self::find_root(&p) {
            Ok(Self::from_root(root))
        } else {
            Ok(None)
        }
    }

    /// Try finding a Windows installation along the path to the target executable
    /// Rationale: the user may have mounted a Windows partition at an unknown depth in the filesystem
    #[cfg(not(windows))]
    fn find_root<P: AsRef<Path>>(p: P) -> Option<PathBuf> {
        for a in p.as_ref().parent()?.ancestors() {
            if Self::from_root(a).is_some() {
                return Some(a.to_owned());
            }
        }
        None
    }

    /// Collect information about the Windows installation at the given path
    /// The path should point to the C:\ partition
    pub fn from_root<P: AsRef<Path>>(root_path: P) -> Option<Self> {
        // TODO: wrap hivex?
        // read known dlls from HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs,
        // and mark their dependencies (which are not listed there) as known DLLs as well
        // https://lucasg.github.io/2017/06/07/listing-known-dlls/
        // read dll safe mode on/off from HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode (if it doesn't exist, it's 1)
        // read system path from HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment ?
        // read user path from C:\Users\<username>\NTUSER.DAT \Environment ?
        let win_dir = root_path.as_ref().join("Windows");
        let sys_dir = win_dir.join("System32");
        if sys_dir.exists() {
            Some(Self {
                safe_dll_search_mode_on: None,
                apiset_map: apiset::parse_apiset(sys_dir.join("apisetschema.dll")).ok(),
                known_dlls: None,
                win_dir,
                sys_dir,
                system_path: None,
            })
        } else {
            None
        }
    }
}

impl PartialEq for WindowsSystem {
    fn eq(&self, other: &Self) -> bool {
        self.sys_dir == other.sys_dir
            && self.win_dir == other.win_dir
            && self.safe_dll_search_mode_on == other.safe_dll_search_mode_on
            && self.known_dlls == other.known_dlls
            && self.system_path == other.system_path
    }
}

/// Fetch the path to a system directory through the Windows API
#[cfg(windows)]
fn get_winapi_directory(
    a: unsafe extern "system" fn(
        winapi::um::winnt::LPWSTR,
        winapi::shared::minwindef::UINT,
    ) -> winapi::shared::minwindef::UINT,
) -> Result<PathBuf, std::io::Error> {
    use std::io::Error;

    const BFR_SIZE: usize = 512;
    let mut bfr: [u16; BFR_SIZE] = [0; BFR_SIZE];

    let ret: u32 = unsafe { a(bfr.as_mut_ptr(), BFR_SIZE as u32) };
    if ret == 0 {
        Err(Error::last_os_error())
    } else {
        let valid_bfr = &bfr[..ret as usize];
        fs::canonicalize(OsString::from_wide(valid_bfr))
    }
}

/// Get the path to the System directory (typically C:\Windows\System32)
#[cfg(windows)]
fn get_system_directory() -> Result<PathBuf, std::io::Error> {
    return get_winapi_directory(winapi::um::sysinfoapi::GetSystemDirectoryW);
}

/// Get the path to the Windows directory (typically C:\Windows)
#[cfg(windows)]
fn get_windows_directory() -> Result<PathBuf, std::io::Error> {
    return get_winapi_directory(winapi::um::sysinfoapi::GetWindowsDirectoryW);
}

/// Caches the content of already scanned directories, to avoid repeated expensive filesystem access
pub(crate) struct WinFileSystemCache {
    files_in_dirs: HashMap<String, HashMap<String, PathBuf>>,
}

impl WinFileSystemCache {
    pub(crate) fn new() -> Self {
        Self {
            files_in_dirs: HashMap::new(),
        }
    }

    pub(crate) fn test_file_in_folder_case_insensitive<P: AsRef<Path>, Q: AsRef<Path>>(
        &mut self,
        filename: P,
        folder: Q,
    ) -> Result<Option<PathBuf>, LookupError> {
        let folder_str: String = folder
            .as_ref()
            .to_str()
            .ok_or_else(|| {
                LookupError::ScanError(format!(
                    "Could not scan directory {:?}",
                    &folder.as_ref().to_str()
                ))
            })?
            .to_owned();
        if !self.files_in_dirs.contains_key(&folder_str) {
            self.scan_folder(&folder)?;
        }
        let dir = self.files_in_dirs.get(&folder_str).ok_or_else(|| {
            LookupError::ScanError(format!(
                "Could not scan directory {:?}",
                &folder.as_ref().to_str()
            ))
        })?;
        Ok(dir
            .get(&filename.as_ref().to_str().unwrap().to_lowercase())
            .map(|p| folder.as_ref().join(p)))
    }

    pub(crate) fn scan_folder<P: AsRef<Path>>(&mut self, folder: P) -> Result<(), LookupError> {
        let folder_str: String = folder
            .as_ref()
            .to_str()
            .ok_or_else(|| {
                LookupError::ScanError(format!(
                    "Could not scan directory {:?}",
                    &folder.as_ref().to_str()
                ))
            })?
            .to_owned();
        if let std::collections::hash_map::Entry::Vacant(e) = self.files_in_dirs.entry(folder_str) {
            let matching_entries: HashMap<String, PathBuf> = fs::read_dir(folder.as_ref())?
                .filter_map(|entry| entry.ok())
                .filter(|entry| entry.metadata().map_or_else(|_| false, |m| m.is_file()))
                .filter_map(|entry| {
                    entry
                        .file_name()
                        .to_str()
                        .map(|s| (s.to_lowercase(), entry.file_name().into()))
                })
                .collect();
            e.insert(matching_entries);
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::common::LookupError;
    use crate::system::WinFileSystemCache;

    #[cfg(windows)]
    #[test]
    fn context_win10() -> Result<(), LookupError> {
        use super::WindowsSystem;
        use fs_err as fs;
        let ctx = WindowsSystem::current()?;
        assert_eq!(ctx.win_dir, fs::canonicalize("C:\\Windows")?);
        assert_eq!(ctx.sys_dir, fs::canonicalize("C:\\Windows\\System32")?);

        // TODO: once implemented, document that it can fail if system is set otherwise
        // assert_eq!(ctx.safe_dll_search_mode_on, Some(true));

        // this changes from computer to computer, but we should get something
        let user_path = ctx.system_path;
        assert!(user_path.is_some());
        assert!(user_path
            .unwrap()
            .contains(&fs::canonicalize("C:\\Windows")?));
        Ok(())
    }

    #[test]
    fn fscache() -> Result<(), LookupError> {
        let d = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let test_file_path =
            d.join("test_data/test_project1/DepRunTest/build-same-output/bin/Debug/DepRunTest.exe");
        assert!(test_file_path.exists());
        let folder = std::fs::canonicalize(test_file_path.parent().unwrap())?;

        let mut fscache = WinFileSystemCache::new();
        let expected_res = Some(folder.join("DepRunTest.exe"));
        assert_eq!(
            fscache.test_file_in_folder_case_insensitive("depruntest.exe", &folder)?,
            expected_res
        );
        assert_eq!(
            fscache.test_file_in_folder_case_insensitive("Depruntest.exe", &folder)?,
            expected_res
        );
        assert_eq!(
            fscache.test_file_in_folder_case_insensitive("somerandomstring.txt", &folder)?,
            None
        );
        Ok(())
    }
}