Skip to main content

dearxan_test_utils/
lib.rs

1use std::{ffi::OsStr, path::PathBuf, sync::LazyLock};
2
3use pelite::pe64::{Pe, PeFile, PeObject, PeView};
4
5// Re-implementation of `PeFile::to_view` as the original is broken
6pub fn pe_file_to_view(pe: pelite::pe64::PeFile) -> Vec<u8> {
7    let (sizeof_headers, sizeof_image) = {
8        let optional_header = pe.optional_header();
9        (optional_header.SizeOfHeaders, optional_header.SizeOfImage)
10    };
11
12    // Zero fill the underlying image
13    let mut vec = vec![0u8; sizeof_image as usize];
14
15    // Start by copying the headers
16    let image = pe.image();
17    unsafe {
18        // Validated by constructor
19        let dest_headers = vec.get_unchecked_mut(..sizeof_headers as usize);
20        let src_headers = image.get_unchecked(..sizeof_headers as usize);
21        dest_headers.copy_from_slice(src_headers);
22    }
23
24    // Copy the section file data
25    for section in pe.section_headers() {
26        let dest = vec.get_mut(
27            section.VirtualAddress as usize
28                ..u32::wrapping_add(section.VirtualAddress, section.VirtualSize) as usize,
29        );
30        let src = image.get(
31            section.PointerToRawData as usize
32                ..u32::wrapping_add(section.PointerToRawData, section.SizeOfRawData) as usize,
33        );
34        // Skip invalid sections...
35        if let (Some(dest), Some(src)) = (dest, src) {
36            let write_sz = src.len().min(dest.len());
37            dest[..write_sz].copy_from_slice(&src[..write_sz]);
38        }
39    }
40
41    vec
42}
43
44#[derive(Debug, Clone)]
45pub struct FsExe {
46    pub game: String,
47    pub ver: String,
48    pub path: PathBuf,
49}
50
51impl FsExe {
52    pub fn load_64(&self) -> pelite::Result<MappedFsExe> {
53        let disk_view = std::fs::read(&self.path).unwrap();
54        let pe_file = PeFile::from_bytes(&disk_view)?;
55        let mem_view = pe_file_to_view(pe_file);
56
57        Ok(MappedFsExe {
58            exe: self.clone(),
59            disk_view,
60            mem_view,
61        })
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct MappedFsExe {
67    exe: FsExe,
68    disk_view: Vec<u8>,
69    mem_view: Vec<u8>,
70}
71
72impl MappedFsExe {
73    pub fn exe_info(&self) -> &FsExe {
74        &self.exe
75    }
76
77    pub fn game(&self) -> &str {
78        &self.exe.game
79    }
80
81    pub fn ver(&self) -> &str {
82        &self.exe.ver
83    }
84
85    pub fn pe_file(&self) -> PeFile<'_> {
86        PeFile::from_bytes(&self.disk_view).unwrap()
87    }
88
89    pub fn pe_view(&self) -> PeView<'_> {
90        PeView::from_bytes(&self.mem_view).unwrap()
91    }
92}
93
94pub struct FsGame {
95    pub name: String,
96    pub versions: Vec<FsExe>,
97}
98
99pub fn fsbins() -> &'static [FsGame] {
100    static FS_BINS: LazyLock<Vec<FsGame>> = LazyLock::new(|| {
101        let fsbins_root = std::env::var("FSBINS_PATH").expect(
102            "This test requires fsbins to be installed and the FSBINS_PATH environment 
103            variable to point to its root.",
104        );
105
106        std::fs::read_dir(fsbins_root)
107            .unwrap()
108            .filter_map(|game_folder| {
109                let game_folder = game_folder.ok()?;
110                let game_name = game_folder.file_name().to_string_lossy().to_string();
111                if !game_folder.file_type().ok()?.is_dir() {
112                    return None;
113                }
114
115                let mut versions: Vec<_> = std::fs::read_dir(game_folder.path())
116                    .ok()?
117                    .filter_map(|ver_folder| {
118                        let ver_folder = ver_folder.ok()?;
119                        let ver_name = ver_folder.file_name().to_string_lossy().to_string();
120                        if !ver_folder.file_type().ok()?.is_dir() {
121                            return None;
122                        }
123
124                        let exe = std::fs::read_dir(ver_folder.path())
125                            .ok()?
126                            .filter_map(|f| f.ok())
127                            .find(|f| f.path().extension() == Some(OsStr::new("exe")))?;
128
129                        Some(FsExe {
130                            game: game_name.clone(),
131                            ver: ver_name.clone(),
132                            path: exe.path(),
133                        })
134                    })
135                    .collect();
136
137                if versions.is_empty() {
138                    return None;
139                }
140
141                // permissive semver sorting: if dot-delimited versions are numbers, compare them as
142                // such otherwise compare them as strings
143                versions.sort_by(|a, b| {
144                    let mut a_split = a.ver.split('.');
145                    let mut b_split = b.ver.split('.');
146                    for (a_seg, b_seg) in (&mut a_split).zip(&mut b_split) {
147                        let ordering = match (a_seg.parse::<usize>(), b_seg.parse::<usize>()) {
148                            (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
149                            _ => a_seg.cmp(b_seg),
150                        };
151                        if ordering != std::cmp::Ordering::Equal {
152                            return ordering;
153                        }
154                    }
155                    a_split.cmp(b_split)
156                });
157                Some(FsGame {
158                    name: game_name.to_string(),
159                    versions,
160                })
161            })
162            .collect()
163    });
164    &FS_BINS
165}
166
167pub fn latest_fsbins() -> Vec<&'static FsExe> {
168    fsbins().iter().map(|g| g.versions.last().unwrap()).collect()
169}
170
171pub fn init_log(level: log::LevelFilter) {
172    simplelog::SimpleLogger::init(level, simplelog::Config::default()).unwrap();
173}