Skip to main content

procmod_layout/
lib.rs

1//! Struct mapping with pointer chain traversal via derive macros.
2//!
3//! Map remote process memory into Rust structs using `#[derive(GameStruct)]`.
4//! Each field declares its byte offset from a base address. Fields can follow
5//! pointer chains through multiple indirections before reading the final value.
6//!
7//! Built on top of [procmod-core](https://crates.io/crates/procmod-core) for
8//! cross-platform memory access.
9//!
10//! # Example
11//!
12//! ```ignore
13//! use procmod_layout::{GameStruct, Process};
14//!
15//! #[derive(GameStruct)]
16//! struct Player {
17//!     #[offset(0x100)]
18//!     health: f32,
19//!     #[offset(0x104)]
20//!     max_health: f32,
21//!     #[offset(0x200)]
22//!     #[pointer_chain(0x10, 0x8)]
23//!     damage_mult: f32,
24//! }
25//!
26//! let process = Process::attach(pid)?;
27//! let player = Player::read(&process, base_address)?;
28//! println!("hp: {}/{}", player.health, player.max_health);
29//! ```
30
31// allows the derive macro's generated `::procmod_layout::...` paths to resolve
32// when this crate is being compiled (including in tests)
33extern crate self as procmod_layout;
34
35pub use procmod_core::{Error, Process, Result};
36pub use procmod_layout_derive::GameStruct;
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[derive(GameStruct, Debug)]
43    struct SimpleLayout {
44        #[offset(0)]
45        a: u32,
46        #[offset(4)]
47        b: u32,
48    }
49
50    #[derive(GameStruct, Debug)]
51    struct WithGap {
52        #[offset(0)]
53        first: u64,
54        #[offset(16)]
55        second: u64,
56    }
57
58    #[derive(GameStruct, Debug)]
59    struct SingleField {
60        #[offset(0)]
61        value: f32,
62    }
63
64    #[derive(GameStruct, Debug)]
65    struct ArrayField {
66        #[offset(0)]
67        values: [u32; 4],
68    }
69
70    #[derive(GameStruct, Debug)]
71    struct MixedTypes {
72        #[offset(0)]
73        byte_val: u8,
74        #[offset(4)]
75        int_val: u32,
76        #[offset(8)]
77        float_val: f64,
78    }
79
80    #[derive(GameStruct, Debug)]
81    struct WithPointerChain {
82        #[offset(0)]
83        direct: u32,
84        #[offset(8)]
85        #[pointer_chain(0)]
86        through_ptr: u32,
87    }
88
89    fn self_process() -> Process {
90        let pid = std::process::id();
91        Process::attach(pid).expect("failed to attach to self")
92    }
93
94    #[test]
95    fn read_simple_layout() {
96        let data: [u32; 2] = [42, 99];
97        let process = self_process();
98        let base = data.as_ptr() as usize;
99        let result = SimpleLayout::read(&process, base).unwrap();
100        assert_eq!(result.a, 42);
101        assert_eq!(result.b, 99);
102    }
103
104    #[test]
105    fn read_with_gap() {
106        let mut buf = [0u8; 24];
107        let first: u64 = 0xDEAD_BEEF;
108        let second: u64 = 0xCAFE_BABE;
109        buf[0..8].copy_from_slice(&first.to_ne_bytes());
110        buf[16..24].copy_from_slice(&second.to_ne_bytes());
111
112        let process = self_process();
113        let base = buf.as_ptr() as usize;
114        let result = WithGap::read(&process, base).unwrap();
115        assert_eq!(result.first, 0xDEAD_BEEF);
116        assert_eq!(result.second, 0xCAFE_BABE);
117    }
118
119    #[test]
120    fn read_single_field() {
121        let value: f32 = 3.14;
122        let process = self_process();
123        let base = &value as *const f32 as usize;
124        let result = SingleField::read(&process, base).unwrap();
125        assert!((result.value - 3.14).abs() < f32::EPSILON);
126    }
127
128    #[test]
129    fn read_array_field() {
130        let data: [u32; 4] = [10, 20, 30, 40];
131        let process = self_process();
132        let base = data.as_ptr() as usize;
133        let result = ArrayField::read(&process, base).unwrap();
134        assert_eq!(result.values, [10, 20, 30, 40]);
135    }
136
137    #[test]
138    fn read_mixed_types() {
139        let mut buf = [0u8; 16];
140        buf[0] = 0xFF;
141        buf[4..8].copy_from_slice(&42u32.to_ne_bytes());
142        buf[8..16].copy_from_slice(&2.718f64.to_ne_bytes());
143
144        let process = self_process();
145        let base = buf.as_ptr() as usize;
146        let result = MixedTypes::read(&process, base).unwrap();
147        assert_eq!(result.byte_val, 0xFF);
148        assert_eq!(result.int_val, 42);
149        assert!((result.float_val - 2.718).abs() < f64::EPSILON);
150    }
151
152    #[test]
153    fn read_pointer_chain() {
154        let target: u32 = 12345;
155        let target_ptr: usize = &target as *const u32 as usize;
156
157        // buf layout:
158        //   offset 0: direct u32 (999)
159        //   offset 4: padding
160        //   offset 8: pointer to target_ptr (which points to target)
161        let mut buf = [0u8; 16];
162        buf[0..4].copy_from_slice(&999u32.to_ne_bytes());
163        buf[8..8 + std::mem::size_of::<usize>()].copy_from_slice(&target_ptr.to_ne_bytes());
164
165        let process = self_process();
166        let base = buf.as_ptr() as usize;
167        let result = WithPointerChain::read(&process, base).unwrap();
168        assert_eq!(result.direct, 999);
169        assert_eq!(result.through_ptr, 12345);
170    }
171
172    #[test]
173    fn read_multi_hop_pointer_chain() {
174        #[derive(GameStruct, Debug)]
175        struct MultiHop {
176            #[offset(0)]
177            #[pointer_chain(0, 0)]
178            value: u64,
179        }
180
181        let target: u64 = 0xBEEF;
182        let target_addr: usize = &target as *const u64 as usize;
183        let mid_ptr: usize = &target_addr as *const usize as usize;
184
185        // base holds a pointer to mid_ptr, which holds a pointer to target
186        let base_data: usize = mid_ptr;
187        let process = self_process();
188        let base = &base_data as *const usize as usize;
189        let result = MultiHop::read(&process, base).unwrap();
190        assert_eq!(result.value, 0xBEEF);
191    }
192
193    #[test]
194    fn read_pointer_chain_with_offsets() {
195        #[derive(GameStruct, Debug)]
196        struct OffsetChain {
197            #[offset(0)]
198            #[pointer_chain(8)]
199            value: u32,
200        }
201
202        // second level: [padding 8 bytes] [target u32]
203        let mut level2 = [0u8; 12];
204        level2[8..12].copy_from_slice(&7777u32.to_ne_bytes());
205        let level2_addr: usize = level2.as_ptr() as usize;
206
207        // base level: pointer to level2
208        let process = self_process();
209        let base = &level2_addr as *const usize as usize;
210        let result = OffsetChain::read(&process, base).unwrap();
211        assert_eq!(result.value, 7777);
212    }
213
214    #[test]
215    fn read_u8_as_flag() {
216        #[derive(GameStruct, Debug)]
217        struct WithFlag {
218            #[offset(0)]
219            alive: u8,
220            #[offset(4)]
221            score: u32,
222        }
223
224        let mut buf = [0u8; 8];
225        buf[0] = 1;
226        buf[4..8].copy_from_slice(&100u32.to_ne_bytes());
227
228        let process = self_process();
229        let base = buf.as_ptr() as usize;
230        let result = WithFlag::read(&process, base).unwrap();
231        assert!(result.alive != 0);
232        assert_eq!(result.score, 100);
233    }
234
235    #[test]
236    fn pointer_chain_null_pointer() {
237        #[derive(GameStruct, Debug)]
238        #[allow(dead_code)]
239        struct NullChain {
240            #[offset(0)]
241            #[pointer_chain(0)]
242            value: u32,
243        }
244
245        // base holds a null pointer
246        let null_ptr: usize = 0;
247        let process = self_process();
248        let base = &null_ptr as *const usize as usize;
249        let result = NullChain::read(&process, base);
250        assert!(result.is_err());
251    }
252
253    #[test]
254    fn read_negative_values() {
255        #[derive(GameStruct, Debug)]
256        struct Signed {
257            #[offset(0)]
258            x: i32,
259            #[offset(4)]
260            y: i32,
261        }
262
263        let data: [i32; 2] = [-50, -100];
264        let process = self_process();
265        let base = data.as_ptr() as usize;
266        let result = Signed::read(&process, base).unwrap();
267        assert_eq!(result.x, -50);
268        assert_eq!(result.y, -100);
269    }
270}