Skip to main content

windows_erg/process/
types.rs

1//! Process-related types and enumerations.
2
3use std::collections::HashMap;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::sync::OnceLock;
7use windows::Win32::System::Threading::PROCESS_ACCESS_RIGHTS;
8
9// Re-export common types
10pub use crate::types::{ProcessId, ThreadId};
11
12/// Case-insensitive string key for HashMap lookups.
13/// Hashes and compares strings case-insensitively without allocating lowercase strings.
14#[derive(Debug, Clone, Copy)]
15struct CaseInsensitiveKey<'a>(&'a str);
16
17impl<'a> Hash for CaseInsensitiveKey<'a> {
18    fn hash<H: Hasher>(&self, state: &mut H) {
19        // Hash each character converted to lowercase on-the-fly
20        for ch in self.0.chars() {
21            ch.to_ascii_lowercase().hash(state);
22        }
23    }
24}
25
26impl<'a> PartialEq for CaseInsensitiveKey<'a> {
27    fn eq(&self, other: &Self) -> bool {
28        self.0.eq_ignore_ascii_case(other.0)
29    }
30}
31
32impl<'a> Eq for CaseInsensitiveKey<'a> {}
33
34/// Static path cache for common image paths.
35/// Uses a HashMap with case-insensitive hashing for O(1) lookups.
36static PATH_CACHE: OnceLock<HashMap<CaseInsensitiveKey<'static>, &'static str>> = OnceLock::new();
37
38/// Common system paths to cache (static strings, no allocation)
39const COMMON_PATHS: &[&str] = &[
40    // Core kernel DLLs (System32)
41    "C:\\Windows\\System32\\kernel32.dll",
42    "C:\\Windows\\System32\\ntdll.dll",
43    "C:\\Windows\\System32\\msvcrt.dll",
44    "C:\\Windows\\System32\\advapi32.dll",
45    "C:\\Windows\\System32\\user32.dll",
46    "C:\\Windows\\System32\\gdi32.dll",
47    "C:\\Windows\\System32\\ws2_32.dll",
48    "C:\\Windows\\System32\\shell32.dll",
49    "C:\\Windows\\System32\\ole32.dll",
50    "C:\\Windows\\System32\\oleaut32.dll",
51    "C:\\Windows\\System32\\comctl32.dll",
52    "C:\\Windows\\System32\\comdlg32.dll",
53    "C:\\Windows\\System32\\winmm.dll",
54    "C:\\Windows\\System32\\shlwapi.dll",
55    "C:\\Windows\\System32\\urlmon.dll",
56    "C:\\Windows\\System32\\wininet.dll",
57    "C:\\Windows\\System32\\msi.dll",
58    "C:\\Windows\\System32\\crypt32.dll",
59    "C:\\Windows\\System32\\cryptbase.dll",
60    "C:\\Windows\\System32\\cryptnet.dll",
61    "C:\\Windows\\System32\\ncrypt.dll",
62    "C:\\Windows\\System32\\bcryptprimitives.dll",
63    "C:\\Windows\\System32\\secur32.dll",
64    "C:\\Windows\\System32\\sspicli.dll",
65    "C:\\Windows\\System32\\ntsecapi.dll",
66    "C:\\Windows\\System32\\wlanapi.dll",
67    "C:\\Windows\\System32\\netapi32.dll",
68    "C:\\Windows\\System32\\iphlpapi.dll",
69    "C:\\Windows\\System32\\dnsapi.dll",
70    "C:\\Windows\\System32\\nsi.dll",
71    "C:\\Windows\\System32\\setupapi.dll",
72    "C:\\Windows\\System32\\cfgmgr32.dll",
73    "C:\\Windows\\System32\\regapi.dll",
74    "C:\\Windows\\System32\\opengl32.dll",
75    // Common EXEs
76    "C:\\Windows\\System32\\services.exe",
77    "C:\\Windows\\System32\\lsass.exe",
78    "C:\\Windows\\System32\\csrss.exe",
79    "C:\\Windows\\System32\\svchost.exe",
80    "C:\\Windows\\System32\\rundll32.exe",
81    "C:\\Windows\\System32\\cmd.exe",
82    "C:\\Windows\\System32\\notepad.exe",
83    "C:\\Windows\\System32\\regedit.exe",
84    "C:\\Windows\\System32\\conhost.exe",
85    // Core kernel DLLs (SysWOW64 - 32-bit)
86    "C:\\Windows\\SysWOW64\\kernel32.dll",
87    "C:\\Windows\\SysWOW64\\ntdll.dll",
88    "C:\\Windows\\SysWOW64\\msvcrt.dll",
89    "C:\\Windows\\SysWOW64\\advapi32.dll",
90    "C:\\Windows\\SysWOW64\\user32.dll",
91    "C:\\Windows\\SysWOW64\\gdi32.dll",
92    "C:\\Windows\\SysWOW64\\ws2_32.dll",
93    "C:\\Windows\\SysWOW64\\shell32.dll",
94    "C:\\Windows\\SysWOW64\\ole32.dll",
95    "C:\\Windows\\SysWOW64\\oleaut32.dll",
96    "C:\\Windows\\SysWOW64\\comctl32.dll",
97    "C:\\Windows\\SysWOW64\\comdlg32.dll",
98    "C:\\Windows\\SysWOW64\\crypt32.dll",
99    "C:\\Windows\\SysWOW64\\cryptbase.dll",
100    "C:\\Windows\\SysWOW64\\secur32.dll",
101    "C:\\Windows\\SysWOW64\\setupapi.dll",
102    // Directory references
103    "C:\\Program Files\\",
104    "C:\\Program Files (x86)\\",
105    "C:\\Windows\\",
106    "C:\\Windows\\System32\\",
107    "C:\\Windows\\SysWOW64\\",
108];
109
110/// Initialize the path cache HashMap.
111fn init_path_cache() -> HashMap<CaseInsensitiveKey<'static>, &'static str> {
112    COMMON_PATHS
113        .iter()
114        .map(|&path| (CaseInsensitiveKey(path), path))
115        .collect()
116}
117
118/// Efficient image path representation for EXEs and DLLs.
119///
120/// Provides:
121/// - Path caching to reduce memory usage
122/// - Case-insensitive comparison
123/// - Ownership tracking (owned vs cached)
124#[derive(Debug, Clone)]
125pub enum ImagePath {
126    /// Reference to a static cached path
127    Cached(&'static str),
128    /// Dynamically allocated path
129    Owned(String),
130}
131
132impl ImagePath {
133    /// Create an ImagePath from a string, using cache when possible.
134    pub fn new(path: impl Into<String>) -> Self {
135        let path_str = path.into();
136
137        // Try to find in cache
138        if let Some(cached) = Self::find_cached(&path_str) {
139            return ImagePath::Cached(cached);
140        }
141
142        ImagePath::Owned(path_str)
143    }
144
145    /// Create an ImagePath from a &str, checking cache without allocating if found.
146    ///
147    /// This is more efficient than `new()` when working with string slices,
148    /// as it avoids allocation if the path is in the cache.
149    ///
150    /// Automatically strips null terminators from the input to enable cache hits
151    /// when receiving null-terminated strings from Windows APIs.
152    pub fn from_str(path: &str) -> Self {
153        // Strip null terminator if present (common in Windows API results)
154        let path = path.trim_end_matches('\0');
155
156        // Try to find in cache first (no allocation needed)
157        if let Some(cached) = Self::find_cached_str(path) {
158            return ImagePath::Cached(cached);
159        }
160
161        // Not in cache, allocate
162        ImagePath::Owned(path.to_string())
163    }
164
165    /// Create an ImagePath from UTF-16 data (e.g., from GetProcessImageFileNameW).
166    ///
167    /// This is the most efficient constructor for Windows API results that return UTF-16.
168    /// It checks the cache before allocating, using the lossy UTF-16 decoding.
169    /// Automatically strips null terminators for cache hits with null-terminated strings.
170    pub fn from_utf16(utf16_data: &[u16]) -> Self {
171        // Decode UTF-16 (lossy conversion for invalid sequences)
172        let path_string = String::from_utf16_lossy(utf16_data);
173
174        // Strip null terminator if present (common in Windows API results)
175        let path_str = path_string.trim_end_matches('\0');
176        let path_lower = path_str.to_lowercase();
177
178        // Try cache lookup with the lowercase version
179        if let Some(cached) = Self::find_cached_str(&path_lower) {
180            return ImagePath::Cached(cached);
181        }
182
183        // Not in cache, use the allocated string (without null terminator)
184        ImagePath::Owned(path_str.to_string())
185    }
186
187    /// Create an ImagePath from UTF-8 data (e.g., from Windows API results).
188    ///
189    /// Returns None if the data is not valid UTF-8.
190    /// Automatically strips null terminators for cache hits with null-terminated strings.
191    pub fn from_utf8(utf8_data: &[u8]) -> Option<Self> {
192        let path_str = std::str::from_utf8(utf8_data).ok()?;
193
194        // Strip null terminator if present
195        let path_str = path_str.trim_end_matches('\0');
196
197        // Try to find in cache first
198        if let Some(cached) = Self::find_cached_str(path_str) {
199            return Some(ImagePath::Cached(cached));
200        }
201
202        // Not in cache, allocate
203        Some(ImagePath::Owned(path_str.to_string()))
204    }
205
206    /// Get the path as a string slice.
207    pub fn as_str(&self) -> &str {
208        match self {
209            ImagePath::Cached(s) => s,
210            ImagePath::Owned(s) => s.as_str(),
211        }
212    }
213
214    /// Check if path matches another (case-insensitive on Windows).
215    pub fn eq_case_insensitive(&self, other: &str) -> bool {
216        self.as_str().eq_ignore_ascii_case(other)
217    }
218
219    /// Check if path contains a substring (case-insensitive).
220    pub fn contains_case_insensitive(&self, needle: &str) -> bool {
221        self.as_str()
222            .to_lowercase()
223            .contains(&needle.to_lowercase())
224    }
225
226    /// Check if path ends with a suffix (case-insensitive).
227    pub fn ends_with_case_insensitive(&self, suffix: &str) -> bool {
228        let path_lower = self.as_str().to_lowercase();
229        let suffix_lower = suffix.to_lowercase();
230        path_lower.ends_with(&suffix_lower)
231    }
232
233    /// Check if this is a system path.
234    pub fn is_system_path(&self) -> bool {
235        let path_lower = self.as_str().to_lowercase();
236        path_lower.contains("\\windows\\") || path_lower.contains("\\winnt\\")
237    }
238
239    /// Check if this is a 32-bit SysWOW64 module.
240    pub fn is_wow64(&self) -> bool {
241        self.contains_case_insensitive("\\SysWOW64\\")
242    }
243
244    /// Get the file name only (without path).
245    pub fn file_name(&self) -> &str {
246        self.as_str().rsplit('\\').next().unwrap_or(self.as_str())
247    }
248
249    /// Find a matching cached path for the given string (does lowercase comparison).
250    fn find_cached(path: &str) -> Option<&'static str> {
251        Self::find_cached_str(path)
252    }
253
254    /// Find a matching cached path using case-insensitive HashMap lookup (O(1)).
255    /// No temporary string allocations - hashing is done on-the-fly during lookup.
256    fn find_cached_str(path: &str) -> Option<&'static str> {
257        let cache = PATH_CACHE.get_or_init(init_path_cache);
258        cache.get(&CaseInsensitiveKey(path)).copied()
259    }
260
261    /// Add a new path to the cache (for runtime-discovered common paths).
262    /// Note: This is a no-op since we use static strings. Consider using this
263    /// if implementing a mutable cache in the future.
264    pub fn cache_path(_path: &'static str) {
265        // Future: could maintain a separate runtime cache
266    }
267
268    /// Check if using cached storage.
269    pub fn is_cached(&self) -> bool {
270        matches!(self, ImagePath::Cached(_))
271    }
272}
273
274impl fmt::Display for ImagePath {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        write!(f, "{}", self.as_str())
277    }
278}
279
280impl PartialEq for ImagePath {
281    fn eq(&self, other: &Self) -> bool {
282        self.eq_case_insensitive(other.as_str())
283    }
284}
285
286impl PartialEq<str> for ImagePath {
287    fn eq(&self, other: &str) -> bool {
288        self.eq_case_insensitive(other)
289    }
290}
291
292impl PartialEq<ImagePath> for str {
293    fn eq(&self, other: &ImagePath) -> bool {
294        other.eq_case_insensitive(self)
295    }
296}
297
298impl PartialEq<&str> for ImagePath {
299    fn eq(&self, other: &&str) -> bool {
300        self.eq_case_insensitive(other)
301    }
302}
303
304impl PartialEq<ImagePath> for &str {
305    fn eq(&self, other: &ImagePath) -> bool {
306        other.eq_case_insensitive(self)
307    }
308}
309
310/// Process access rights.
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum ProcessAccess {
313    /// Query basic information.
314    QueryInformation,
315    /// Query limited information.
316    QueryLimitedInformation,
317    /// Read process memory.
318    VmRead,
319    /// Write process memory.
320    VmWrite,
321    /// Terminate the process.
322    Terminate,
323    /// Create threads.
324    CreateThread,
325    /// All access rights.
326    AllAccess,
327    /// Custom access rights.
328    Custom(PROCESS_ACCESS_RIGHTS),
329}
330
331impl ProcessAccess {
332    pub(crate) fn to_windows(self) -> PROCESS_ACCESS_RIGHTS {
333        use windows::Win32::System::Threading::*;
334
335        const PROCESS_SYNCHRONIZE: PROCESS_ACCESS_RIGHTS = PROCESS_ACCESS_RIGHTS(0x0010_0000);
336
337        match self {
338            ProcessAccess::QueryInformation => PROCESS_QUERY_INFORMATION | PROCESS_SYNCHRONIZE,
339            ProcessAccess::QueryLimitedInformation => {
340                PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE
341            }
342            ProcessAccess::VmRead => PROCESS_VM_READ | PROCESS_SYNCHRONIZE,
343            ProcessAccess::VmWrite => PROCESS_VM_WRITE | PROCESS_SYNCHRONIZE,
344            ProcessAccess::Terminate => PROCESS_TERMINATE | PROCESS_SYNCHRONIZE,
345            ProcessAccess::CreateThread => PROCESS_CREATE_THREAD | PROCESS_SYNCHRONIZE,
346            ProcessAccess::AllAccess => PROCESS_ALL_ACCESS,
347            ProcessAccess::Custom(rights) => rights,
348        }
349    }
350}
351
352/// Basic process information.
353#[derive(Debug, Clone)]
354pub struct ProcessInfo {
355    /// Process ID.
356    pub pid: ProcessId,
357    /// Parent process ID.
358    pub parent_pid: Option<ProcessId>,
359    /// Process name (executable file name).
360    pub name: String,
361    /// Number of threads.
362    pub thread_count: u32,
363}
364
365/// Thread information.
366#[derive(Debug, Clone)]
367pub struct ThreadInfo {
368    /// Thread ID.
369    pub tid: ThreadId,
370    /// Owning process ID.
371    pub pid: ProcessId,
372    /// Base priority.
373    pub base_priority: i32,
374}
375
376/// Module (DLL) information.
377#[derive(Debug, Clone)]
378pub struct ModuleInfo {
379    /// Module name (e.g., "kernel32.dll").
380    pub name: String,
381    /// Full path to the module.
382    pub path: ImagePath,
383    /// Base address in process memory.
384    pub base_address: usize,
385    /// Module size in bytes.
386    pub size: u32,
387}
388
389/// Process parameters from PEB.
390#[derive(Debug, Clone)]
391pub struct ProcessParameters {
392    /// Command line.
393    pub command_line: String,
394    /// Current directory.
395    pub current_directory: String,
396    /// Image path.
397    pub image_path: ImagePath,
398}
399
400/// Memory usage information.
401#[derive(Debug, Clone, Copy)]
402pub struct MemoryInfo {
403    /// Working set size in bytes.
404    pub working_set: usize,
405    /// Peak working set size in bytes.
406    pub peak_working_set: usize,
407    /// Page fault count.
408    pub page_fault_count: u32,
409}
410
411/// Process CPU time counters.
412///
413/// Values are cumulative from process start, in 100-nanosecond units.
414#[derive(Debug, Clone, Copy, Default)]
415pub struct ProcessCpuTimes {
416    /// Cumulative time spent in user mode (100ns units).
417    pub user_time_100ns: u64,
418    /// Cumulative time spent in kernel mode (100ns units).
419    pub kernel_time_100ns: u64,
420    /// Sum of user and kernel times (100ns units).
421    pub total_time_100ns: u64,
422}
423
424/// Extended process memory metrics.
425#[derive(Debug, Clone, Copy, Default)]
426pub struct ProcessMemoryMetrics {
427    /// Working set size in bytes.
428    pub working_set_bytes: usize,
429    /// Peak working set size in bytes.
430    pub peak_working_set_bytes: usize,
431    /// Page fault count.
432    pub page_fault_count: u32,
433    /// Private memory usage in bytes.
434    pub private_usage_bytes: usize,
435    /// Commit charge (pagefile usage) in bytes.
436    pub commit_usage_bytes: usize,
437    /// Peak commit charge in bytes.
438    pub peak_commit_usage_bytes: usize,
439}
440
441/// Point-in-time metrics for a process.
442#[derive(Debug, Clone, Copy, Default)]
443pub struct ProcessMetrics {
444    /// Memory metrics.
445    pub memory: ProcessMemoryMetrics,
446    /// CPU time counters.
447    pub cpu: ProcessCpuTimes,
448}
449
450/// Point-in-time memory metrics for the host.
451#[derive(Debug, Clone, Copy, Default)]
452pub struct HostMemoryMetrics {
453    /// Total visible physical memory in bytes.
454    pub total_physical_bytes: u64,
455    /// Available physical memory in bytes.
456    pub available_physical_bytes: u64,
457    /// Total virtual memory available to the process in bytes.
458    pub total_virtual_bytes: u64,
459    /// Available virtual memory in bytes.
460    pub available_virtual_bytes: u64,
461    /// Percentage of physical memory in use.
462    pub memory_load_percent: u32,
463}
464
465/// Point-in-time metrics for the host.
466#[derive(Debug, Clone, Copy, Default)]
467pub struct HostMetrics {
468    /// Number of logical processors visible to the current process.
469    pub logical_cpu_count: u32,
470    /// Host memory metrics.
471    pub memory: HostMemoryMetrics,
472}
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_image_path_cache_hit() {
479        // Test that kernel32.dll is cached
480        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
481        assert!(path.is_cached(), "kernel32.dll should be cached");
482        match path {
483            ImagePath::Cached(s) => {
484                assert_eq!(s, "C:\\Windows\\System32\\kernel32.dll");
485            }
486            _ => panic!("Expected Cached variant"),
487        }
488    }
489
490    #[test]
491    fn test_image_path_cache_miss() {
492        // Test that unknown path allocates
493        let path = ImagePath::from_str("C:\\Unknown\\Path\\custom.dll");
494        assert!(!path.is_cached(), "Unknown path should not be cached");
495        match path {
496            ImagePath::Owned(s) => {
497                assert_eq!(s, "C:\\Unknown\\Path\\custom.dll");
498            }
499            _ => panic!("Expected Owned variant"),
500        }
501    }
502
503    #[test]
504    fn test_image_path_case_insensitive_cache_hit() {
505        // Test that cache lookup is case-insensitive
506        let path_upper = ImagePath::from_str("C:\\WINDOWS\\SYSTEM32\\KERNEL32.DLL");
507        let path_mixed = ImagePath::from_str("c:\\windows\\system32\\kernel32.dll");
508
509        assert!(path_upper.is_cached(), "Uppercase path should be cached");
510        assert!(path_mixed.is_cached(), "Lowercase path should be cached");
511    }
512
513    #[test]
514    fn test_image_path_new_allocates_then_caches() {
515        // Test new() method
516        let path = ImagePath::new("C:\\Windows\\System32\\ntdll.dll");
517        assert!(path.is_cached(), "Common path should be cached with new()");
518    }
519
520    #[test]
521    fn test_image_path_from_utf16() {
522        // Test UTF-16 construction
523        let utf16: Vec<u16> = "C:\\Windows\\System32\\advapi32.dll"
524            .encode_utf16()
525            .collect();
526        let path = ImagePath::from_utf16(&utf16);
527
528        assert!(path.is_cached(), "UTF-16 common path should be cached");
529        assert_eq!(path.as_str(), "C:\\Windows\\System32\\advapi32.dll");
530    }
531
532    #[test]
533    fn test_image_path_from_utf16_case_insensitive() {
534        // Test UTF-16 with different case
535        let utf16: Vec<u16> = "C:\\WINDOWS\\SYSTEM32\\USER32.DLL".encode_utf16().collect();
536        let path = ImagePath::from_utf16(&utf16);
537
538        assert!(path.is_cached(), "UTF-16 uppercase path should be cached");
539    }
540
541    #[test]
542    fn test_image_path_from_utf8() {
543        // Test UTF-8 construction
544        let path = ImagePath::from_utf8(b"C:\\Windows\\System32\\shell32.dll");
545        assert!(path.is_some(), "Valid UTF-8 should succeed");
546        let path = path.unwrap();
547        assert!(path.is_cached(), "UTF-8 common path should be cached");
548    }
549
550    #[test]
551    fn test_image_path_from_utf8_invalid() {
552        // Test invalid UTF-8
553        let invalid_utf8 = [0xFF, 0xFE];
554        let path = ImagePath::from_utf8(&invalid_utf8);
555        assert!(path.is_none(), "Invalid UTF-8 should return None");
556    }
557
558    #[test]
559    fn test_image_path_display() {
560        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
561        assert_eq!(format!("{}", path), "C:\\Windows\\System32\\kernel32.dll");
562    }
563
564    #[test]
565    fn test_image_path_partial_eq_self() {
566        let path1 = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
567        let path2 = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
568        assert_eq!(path1, path2, "Same paths should be equal");
569    }
570
571    #[test]
572    fn test_image_path_partial_eq_different_case() {
573        let path1 = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
574        let path2 = ImagePath::from_str("c:\\windows\\system32\\kernel32.dll");
575        assert_eq!(
576            path1, path2,
577            "Different case should be equal (case-insensitive)"
578        );
579    }
580
581    #[test]
582    fn test_image_path_eq_str() {
583        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
584        assert_eq!(&path, "C:\\Windows\\System32\\kernel32.dll");
585        assert_eq!(&path, "c:\\windows\\system32\\kernel32.dll");
586    }
587
588    #[test]
589    fn test_image_path_eq_owned_vs_cached() {
590        let cached = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
591        let owned = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
592
593        // Both should be cached and equal
594        assert!(cached.is_cached());
595        assert!(owned.is_cached());
596        assert_eq!(cached, owned);
597    }
598
599    #[test]
600    fn test_image_path_file_name() {
601        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
602        assert_eq!(path.file_name(), "kernel32.dll");
603    }
604
605    #[test]
606    fn test_image_path_file_name_no_path() {
607        let path = ImagePath::from_str("kernel32.dll");
608        assert_eq!(path.file_name(), "kernel32.dll");
609    }
610
611    #[test]
612    fn test_image_path_is_system_path() {
613        let sys_path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
614        assert!(sys_path.is_system_path(), "Should detect Windows path");
615
616        let other_path = ImagePath::from_str("C:\\Program Files\\app.exe");
617        assert!(
618            !other_path.is_system_path(),
619            "Should not detect non-Windows path"
620        );
621    }
622
623    #[test]
624    fn test_image_path_is_system_path_case_insensitive() {
625        let sys_path = ImagePath::from_str("C:\\WINDOWS\\SYSTEM32\\kernel32.dll");
626        assert!(
627            sys_path.is_system_path(),
628            "Should detect Windows path (uppercase)"
629        );
630    }
631
632    #[test]
633    fn test_image_path_is_wow64() {
634        let wow64_path = ImagePath::from_str("C:\\Windows\\SysWOW64\\kernel32.dll");
635        assert!(wow64_path.is_wow64(), "Should detect SysWOW64 path");
636
637        let normal_path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
638        assert!(
639            !normal_path.is_wow64(),
640            "Should not detect System32 as WOW64"
641        );
642    }
643
644    #[test]
645    fn test_image_path_is_wow64_case_insensitive() {
646        let wow64_path = ImagePath::from_str("C:\\WINDOWS\\SYSWOW64\\kernel32.dll");
647        assert!(
648            wow64_path.is_wow64(),
649            "Should detect SysWOW64 path (uppercase)"
650        );
651    }
652
653    #[test]
654    fn test_image_path_ends_with_case_insensitive() {
655        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
656        assert!(
657            path.ends_with_case_insensitive(".dll"),
658            "Should end with .dll"
659        );
660        assert!(
661            path.ends_with_case_insensitive(".DLL"),
662            "Should end with .DLL (case-insensitive)"
663        );
664        assert!(
665            !path.ends_with_case_insensitive(".exe"),
666            "Should not end with .exe"
667        );
668    }
669
670    #[test]
671    fn test_image_path_contains_case_insensitive() {
672        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
673        assert!(
674            path.contains_case_insensitive("system32"),
675            "Should contain system32"
676        );
677        assert!(
678            path.contains_case_insensitive("SYSTEM32"),
679            "Should contain SYSTEM32 (case-insensitive)"
680        );
681        assert!(
682            path.contains_case_insensitive("kernel32"),
683            "Should contain kernel32"
684        );
685        assert!(
686            !path.contains_case_insensitive("syswow64"),
687            "Should not contain syswow64"
688        );
689    }
690
691    #[test]
692    fn test_image_path_eq_case_insensitive() {
693        let path = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
694        assert!(path.eq_case_insensitive("C:\\Windows\\System32\\kernel32.dll"));
695        assert!(path.eq_case_insensitive("c:\\windows\\system32\\kernel32.dll"));
696        assert!(path.eq_case_insensitive("C:\\WINDOWS\\SYSTEM32\\KERNEL32.DLL"));
697    }
698
699    #[test]
700    fn test_image_path_clone() {
701        let path1 = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
702        let path2 = path1.clone();
703
704        assert_eq!(path1, path2);
705        assert_eq!(path1.as_str(), path2.as_str());
706    }
707
708    #[test]
709    fn test_cache_initialization() {
710        // First access should initialize the cache
711        let path1 = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
712        assert!(path1.is_cached());
713
714        // Subsequent accesses should use the same cache
715        let path2 = ImagePath::from_str("C:\\Windows\\System32\\ntdll.dll");
716        assert!(path2.is_cached());
717    }
718
719    #[test]
720    fn test_multiple_cache_hits() {
721        // Test that multiple different cached paths work
722        let paths = vec![
723            "C:\\Windows\\System32\\kernel32.dll",
724            "C:\\Windows\\System32\\ntdll.dll",
725            "C:\\Windows\\System32\\msvcrt.dll",
726            "C:\\Windows\\SysWOW64\\kernel32.dll",
727        ];
728
729        for p in paths {
730            let image_path = ImagePath::from_str(p);
731            assert!(image_path.is_cached(), "Path {} should be cached", p);
732            assert_eq!(image_path.as_str(), p);
733        }
734    }
735
736    #[test]
737    fn test_cache_with_owned_allocation() {
738        // Mix of cached and owned
739        let cached = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
740        let owned = ImagePath::from_str("C:\\Custom\\unknown.dll");
741
742        assert!(cached.is_cached());
743        assert!(!owned.is_cached());
744
745        assert_eq!(cached.as_str(), "C:\\Windows\\System32\\kernel32.dll");
746        assert_eq!(owned.as_str(), "C:\\Custom\\unknown.dll");
747    }
748
749    #[test]
750    fn test_case_insensitive_key_hashing() {
751        // Test that case-insensitive keys hash consistently
752        let key1 = CaseInsensitiveKey("C:\\Windows\\System32\\kernel32.dll");
753        let key2 = CaseInsensitiveKey("c:\\windows\\system32\\kernel32.dll");
754
755        use std::collections::hash_map::DefaultHasher;
756        use std::hash::{Hash, Hasher};
757
758        let mut hasher1 = DefaultHasher::new();
759        key1.hash(&mut hasher1);
760        let hash1 = hasher1.finish();
761
762        let mut hasher2 = DefaultHasher::new();
763        key2.hash(&mut hasher2);
764        let hash2 = hasher2.finish();
765
766        assert_eq!(
767            hash1, hash2,
768            "Case-insensitive keys should hash to the same value"
769        );
770    }
771
772    #[test]
773    fn test_process_id_from_u32() {
774        let id = ProcessId::from(1234u32);
775        assert_eq!(id.as_u32(), 1234);
776    }
777
778    #[test]
779    fn test_thread_id_from_u32() {
780        let id = ThreadId::from(5678u32);
781        assert_eq!(id.as_u32(), 5678);
782    }
783
784    #[test]
785    fn test_image_path_with_null_terminator() {
786        // Test behavior when a path string contains null terminator at the end
787        // This simulates paths coming from Windows APIs that might include null termination
788        let path_with_null = "C:\\Windows\\System32\\kernel32.dll\0";
789        let image_path = ImagePath::from_str(path_with_null);
790
791        // The null terminator should be stripped so we can match against cached entries
792        // Cached entries don't have null terminators, so we must trim them for cache hits
793        assert_eq!(image_path.as_str(), "C:\\Windows\\System32\\kernel32.dll");
794        assert!(
795            !image_path.as_str().ends_with('\0'),
796            "Null terminator should be stripped"
797        );
798
799        // Test that it's cached (null terminator was stripped, allowing cache lookup)
800        assert!(
801            image_path.is_cached(),
802            "Should be cached after null terminator is stripped"
803        );
804
805        // Test that the result matches the cached version
806        let path_without_null = ImagePath::from_str("C:\\Windows\\System32\\kernel32.dll");
807        assert_eq!(image_path, path_without_null);
808    }
809}