wdext 0.1.0

A DbgEng wrapper framework
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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
// SPDX-FileCopyrightText: 2026 takubokudori
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::{data::DebugExecutionFlags, util::parse_masm_value, *};
use bitflags::bitflags;

bitflags! {
    #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
    pub struct MemoryType:u32 {
        const Private =   0x20000;
        const Mapped  =   0x40000;
        const Image   = 0x1000000;
    }
}

impl MemoryType {
    fn from_varname(s: impl AsRef<str>) -> Self {
        let s = s.as_ref();
        let mut ret = Self::empty();
        for x in s.split_whitespace() {
            match x {
                "MEM_PRIVATE" => ret |= Self::Private,
                "MEM_MAPPED" => ret |= Self::Mapped,
                "MEM_IMAGE" => ret |= Self::Image,
                _ => {}
            }
        }
        ret
    }
}

bitflags! {
    #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
    pub struct MemoryState: u32 {
        const Commit  =  0x1000;
        const Reserve =  0x2000;
        const Free    = 0x10000;
    }
}

impl MemoryState {
    fn from_varname(s: impl AsRef<str>) -> Self {
        let s = s.as_ref();
        let mut ret = Self::empty();
        for x in s.split_whitespace() {
            match x {
                "MEM_COMMIT" => ret |= Self::Commit,
                "MEM_RESERVE" => ret |= Self::Reserve,
                "MEM_FREE" => ret |= Self::Free,
                _ => {}
            }
        }
        ret
    }
}

bitflags! {
    #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
    pub struct MemoryProtection: u32 {
        const NoAccess = 0x01;
        const ReadOnly = 0x02;
        const ReadWrite = 0x04;
        const Guard = 0x100;
        const NoCache = 0x200;
        const WriteCombine = 0x400;
        const Execute = 0x10;
        const ExecuteRead = 0x20;
        const ExecuteReadWrite = 0x40;
        const ExecuteWriteCopy = 0x80;
        const WriteCopy = 0x08;
        const TargetsInvalid = 0x40000000;
    }
}

impl MemoryProtection {
    fn from_varname(s: impl AsRef<str>) -> Self {
        let s = s.as_ref();
        let mut ret = Self::empty();
        for x in s.split_whitespace() {
            match x {
                "PAGE_NOACCESS" => ret |= Self::NoAccess,
                "PAGE_READONLY" => ret |= Self::ReadOnly,
                "PAGE_READWRITE" => ret |= Self::ReadWrite,
                "PAGE_GUARD" => ret |= Self::Guard,
                "PAGE_NOCACHE" => ret |= Self::NoCache,
                "PAGE_WRITECOMBINE" => ret |= Self::WriteCombine,
                "PAGE_EXECUTE" => ret |= Self::Execute,
                "PAGE_EXECUTE_READ" => ret |= Self::ExecuteRead,
                "PAGE_EXECUTE_READWRITE" => ret |= Self::ExecuteReadWrite,
                "PAGE_EXECUTE_WRITECOPY" => ret |= Self::ExecuteWriteCopy,
                "PAGE_WRITECOPY" => ret |= Self::WriteCopy,
                "PAGE_TARGETS_INVALID" | "PAGE_TARGETS_NO_UPDATE" => {
                    ret |= Self::TargetsInvalid
                }
                _ => {}
            }
        }
        ret
    }

    pub fn is_readable(&self) -> bool {
        self.contains(Self::ReadOnly)
            | self.contains(Self::ReadWrite)
            | self.contains(Self::ExecuteRead)
            | self.contains(Self::ExecuteReadWrite)
            | self.contains(Self::ExecuteWriteCopy)
            | self.contains(Self::WriteCopy)
    }

    pub fn is_writable(&self) -> bool {
        self.contains(Self::ReadWrite)
            | self.contains(Self::WriteCombine)
            | self.contains(Self::ExecuteReadWrite)
            | self.contains(Self::ExecuteWriteCopy)
            | self.contains(Self::WriteCopy)
    }

    pub fn is_executable(&self) -> bool {
        self.contains(Self::Execute)
            | self.contains(Self::ExecuteRead)
            | self.contains(Self::ExecuteReadWrite)
            | self.contains(Self::ExecuteWriteCopy)
    }

    pub fn to_rwx_str(&self) -> &'static str {
        match (self.is_readable(), self.is_writable(), self.is_executable()) {
            (true, true, true) => "RWX",
            (true, true, false) => "RW-",
            (true, false, true) => "R-X",
            (true, false, false) => "R--",
            (false, true, true) => "-WX",
            (false, true, false) => "-W-",
            (false, false, true) => "--X",
            (false, false, false) => "---",
        }
    }
}

#[derive(Default, Debug, Clone)]
pub enum RegionSummary {
    Free,
    Heap(HeapRegion),
    Heap32(HeapRegion),
    Heap64(HeapRegion),
    Stack(StackRegion),
    Stack32(StackRegion),
    Stack64(StackRegion),
    Peb(PebRegion),
    Peb32(PebRegion),
    Peb64(PebRegion),
    Teb(TebRegion),
    Teb32(TebRegion),
    Teb64(TebRegion),
    MappedFile(MappedFileRegion),
    Image(ImageRegion),
    #[default]
    Unknown,
    Other(OtherRegion),
    Other32(OtherRegion),
    Other64(OtherRegion),
    /// Any usage other than those listed above. Stores `(Usage, Summary)`.
    Unknown2(String, Option<String>),
}

impl RegionSummary {
    pub fn usage(&self) -> &str {
        match self {
            RegionSummary::Free => "Free",
            RegionSummary::Heap(_) => "Heap",
            RegionSummary::Heap32(_) => "Heap32",
            RegionSummary::Heap64(_) => "Heap64",
            RegionSummary::Stack(_) => "Stack",
            RegionSummary::Stack32(_) => "Stack32",
            RegionSummary::Stack64(_) => "Stack64",
            RegionSummary::Peb(_) => "PEB",
            RegionSummary::Peb32(_) => "PEB32",
            RegionSummary::Peb64(_) => "PEB64",
            RegionSummary::Teb(_) => "TEB",
            RegionSummary::Teb32(_) => "TEB32",
            RegionSummary::Teb64(_) => "TEB64",
            RegionSummary::MappedFile(_) => "MappedFile",
            RegionSummary::Image(_) => "Image",
            RegionSummary::Unknown => "<unknown>",
            RegionSummary::Other(_) => "Other",
            RegionSummary::Other32(_) => "Other32",
            RegionSummary::Other64(_) => "Other64",
            RegionSummary::Unknown2(s, _) => s,
        }
    }
    fn parse(usage: &str, summary: &str) -> Option<Self> {
        match usage {
            "Free" => Some(Self::Free),
            "Heap" => Some(Self::Heap(HeapRegion::from_summary_str(summary)?)),
            "Heap32" => {
                Some(Self::Heap32(HeapRegion::from_summary_str(summary)?))
            }
            "Heap64" => {
                Some(Self::Heap64(HeapRegion::from_summary_str(summary)?))
            }
            "Stack" => {
                Some(Self::Stack(StackRegion::from_summary_str(summary)?))
            }
            "Stack32" => {
                Some(Self::Stack32(StackRegion::from_summary_str(summary)?))
            }
            "Stack64" => {
                Some(Self::Stack64(StackRegion::from_summary_str(summary)?))
            }
            "PEB" => Some(Self::Peb(PebRegion::from_summary_str(summary)?)),
            "PEB32" => Some(Self::Peb32(PebRegion::from_summary_str(summary)?)),
            "PEB64" => Some(Self::Peb64(PebRegion::from_summary_str(summary)?)),
            "TEB" => Some(Self::Teb(TebRegion::from_summary_str(summary)?)),
            "TEB32" => Some(Self::Teb32(TebRegion::from_summary_str(summary)?)),
            "TEB64" => Some(Self::Teb64(TebRegion::from_summary_str(summary)?)),
            "MappedFile" => Some(Self::MappedFile(
                MappedFileRegion::from_summary_str(summary)?,
            )),
            "Image" => {
                Some(Self::Image(ImageRegion::from_summary_str(summary)?))
            }
            "<unknown>" => Some(Self::Unknown),
            "Other" => {
                Some(Self::Other(OtherRegion::from_summary_str(summary)?))
            }
            "Other32" => {
                Some(Self::Other32(OtherRegion::from_summary_str(summary)?))
            }
            "Other64" => {
                Some(Self::Other64(OtherRegion::from_summary_str(summary)?))
            }
            x => Some(Self::Unknown2(x.to_string(), Some(summary.to_string()))),
        }
    }

    fn parse2(usage: &str) -> Option<Self> {
        match usage {
            "Free" => Some(Self::Free),
            "<unknown>" => Some(Self::Unknown),
            x => Some(Self::Unknown2(x.to_string(), None)),
        }
    }
}

#[derive(Debug, Clone)]
pub struct HeapRegion {
    pub id: u32,
    pub handle: DebuggeeOffset,
    pub r#type: String,
}

impl HeapRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        // [ID: 0; Handle: 00000173ad510000; Type: Segment]
        let summary = summary.trim().strip_prefix('[')?.strip_suffix(']')?;

        let mut sp = summary.split(';');

        let mut sp2 = sp.next()?.split(':').map(|x| x.trim());
        let x = sp2.next()?;
        if x != "ID" {
            return None;
        }
        let x = sp2.next()?;
        let id = u32::from_str(x).ok()?;

        let mut sp2 = sp.next()?.split(':').map(|x| x.trim());
        let x = sp2.next()?;
        if x != "Handle" {
            return None;
        }
        let x = sp2.next()?;
        let handle = u64::from_str_radix(x, 16).ok()?;

        let mut sp2 = sp.next()?.split(':').map(|x| x.trim());
        let x = sp2.next()?;
        if x != "Type" {
            return None;
        }
        let r#type = sp2.next()?;

        Some(Self {
            id,
            handle,
            r#type: r#type.to_string(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct ImageRegion {
    pub image_path: String,
    pub module_name: String,
}

impl ImageRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        // [Module Name; "Image Path"]
        let summary = summary.trim().strip_prefix('[')?.strip_suffix(']')?;
        let mut us = summary.split(';');
        let module_name = us.next()?.trim();

        let s2 = us.next()?.trim();

        // remove double quotes
        let image_path = &s2[1..s2.len() - 1];

        Some(Self {
            image_path: image_path.to_string(),
            module_name: module_name.to_string(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct StackRegion {
    pub engine_thread_id: EngineThreadId,
    pub thread_id: ThreadId,
    pub process_id: ProcessId,
}

impl StackRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        // [~0; 12ab.34cd]
        let summary = summary.trim().strip_prefix('[')?.strip_suffix(']')?;
        let mut sp = summary.split(';');
        let l1 = sp.next()?;
        let l1 = l1.strip_prefix('~')?;
        let etid = u32::from_str(l1).ok()?;
        let l2 = sp.next()?;
        let mut sp = l2.split('.');
        let l3 = sp.next()?;
        let pid = u32::from_str_radix(l3.trim(), 16).ok()?;
        let l4 = sp.next()?;
        let tid = u32::from_str_radix(l4.trim(), 16).ok()?;
        Some(Self {
            engine_thread_id: etid.into(),
            thread_id: tid.into(),
            process_id: pid.into(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct PebRegion {
    pub pid: ProcessId,
}

impl PebRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        let summary = summary.trim().strip_prefix('[')?.strip_suffix(']')?;
        let pid = u32::from_str_radix(summary, 16).ok()?;

        Some(Self {
            pid: ProcessId(pid),
        })
    }
}

#[derive(Debug, Clone)]
pub struct TebRegion {
    pub engine_thread_id: EngineThreadId,
    pub thread_id: ThreadId,
    pub process_id: ProcessId,
}

impl TebRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        let r = StackRegion::from_summary_str(summary)?;
        Some(Self {
            engine_thread_id: r.engine_thread_id,
            thread_id: r.thread_id,
            process_id: r.process_id,
        })
    }
}

#[derive(Debug, Clone)]
pub struct MappedFileRegion {
    pub mapped_file: String,
}

impl MappedFileRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        let summary = summary.trim().strip_prefix('"')?.strip_suffix('"')?;

        Some(Self {
            mapped_file: summary.to_string(),
        })
    }
}

#[derive(Debug, Clone)]
pub struct OtherRegion {
    pub info: String,
}

impl OtherRegion {
    fn from_summary_str(summary: &str) -> Option<Self> {
        let summary = summary.trim().strip_prefix('[')?.strip_suffix(']')?;

        Some(Self {
            info: summary.to_string(),
        })
    }
}

#[derive(Default, Debug, Clone)]
pub struct MemoryRegion {
    pub base_address: DebuggeeOffset,
    pub end_address: DebuggeeOffset,
    pub region_size: DebuggeeOffset,
    pub state: MemoryState,
    pub protect: Option<MemoryProtection>,
    pub r#type: Option<MemoryType>,
    pub allocation_base: Option<DebuggeeOffset>,
    pub allocation_protect: Option<u32>,
    pub summary: RegionSummary,
}

#[derive(Debug, Clone)]
/// Address-map structure.
pub struct AddressMap {
    regions: Vec<MemoryRegion>,
}

impl AddressMap {
    /// Parser for output in the `!address -o:tsv` format.
    fn from_command_output(s: impl AsRef<str>) -> WdResult<Self> {
        let s = s.as_ref();
        let mut regions = Vec::with_capacity(512);
        let mut before_base = 0;
        for l in s.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
            let mut region = MemoryRegion::default();
            let mut st = l.split('\t');
            let base_address = st.next().expect("Failed to get Base Address");
            region.base_address = match parse_masm_value(base_address) {
                Ok(x) => x,
                Err(_) => {
                    // The first `!address` output includes text such as `Mapping...`, so it needs to be ignored.
                    continue;
                }
            };
            // Assume base addresses are always in ascending order.
            assert!(before_base <= region.base_address);
            before_base = region.base_address;
            let end_address = st.next().expect("Failed to get End Address");
            region.end_address = parse_masm_value(end_address)?;
            let region_size = st.next().expect("Failed to get Region Size");
            // This one is decimal only.
            region.region_size = u64::from_str(region_size)?;
            let ty = st.next().expect("Failed to get Type");
            region.r#type =
                (!ty.is_empty()).then_some(MemoryType::from_varname(ty));
            let state = st.next().expect("Failed to get State");
            region.state = MemoryState::from_varname(state);
            let protect = st.next().expect("Failed to get Protect");
            if !protect.is_empty() {
                region.protect = Some(MemoryProtection::from_varname(protect));
            }
            let usage = st.next().expect("Failed to get Usage");
            match st.next() {
                None => {
                    region.summary = RegionSummary::parse2(usage)
                        .expect("Failed to parse summary");
                }
                Some(summary) => {
                    region.summary = RegionSummary::parse(usage, summary)
                        .expect("Failed to parse summary");
                }
            }
            regions.push(region);
        }
        Ok(Self { regions })
    }

    pub fn get(ctrl: &WdControl) -> WdResult<Self> {
        let out = ctrl.execute_and_capture_text(
            DebugOutctlFlags::ThisClient,
            "!address -o:tsv",
            DebugExecutionFlags::NotLogged | DebugExecutionFlags::NoRepeat,
        )?;
        Self::from_command_output(out)
    }

    pub fn regions(&self) -> &[MemoryRegion] { &self.regions }

    /// Returns the region containing the specified offset.
    pub fn find_region(
        &self,
        address: DebuggeeOffset,
    ) -> Option<&MemoryRegion> {
        let i = self.regions.partition_point(|r| r.base_address <= address);

        if i == 0 {
            return None;
        }

        let region = &self.regions[i - 1];
        (address < region.end_address).then_some(region)
    }
}

#[test]
fn test_address_map() {
    let s = r#"

Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...
0x0000000000000000	0x000000007ffe0000	2147352576		MEM_FREE	PAGE_NOACCESS	Free
0x000000007ffe0000	0x000000007ffe1000	4096	MEM_PRIVATE	MEM_COMMIT	PAGE_READONLY	Other	[User Shared Data]
0x0000006f17334000	0x0000006f17335000	4096	MEM_PRIVATE	MEM_COMMIT	PAGE_READWRITE	PEB	[17e0]
0x0000006f17335000	0x0000006f17337000	8192	MEM_PRIVATE	MEM_COMMIT	PAGE_READWRITE	TEB	[~0; 17e0.77c0]
0x0000006f17337000	0x0000006f17339000	8192	MEM_PRIVATE	MEM_COMMIT	PAGE_READWRITE	TEB	[~1; 17e0.3380]
0x0000006f17339000	0x0000006f1733d000	16384	MEM_PRIVATE	MEM_COMMIT	PAGE_READWRITE	TEB	[~2; 17e0.5924]
0x0000006f1733d000	0x0000006f17400000	798720	MEM_PRIVATE	MEM_RESERVE		<unknown>
0x0000006f1747d000	0x0000006f1747e000	4096	MEM_PRIVATE	MEM_COMMIT	PAGE_READONLY | PAGE_GUARD	<unknown>
0x0000006f1747e000	0x0000006f1747f000	4096	MEM_PRIVATE	MEM_COMMIT	PAGE_READONLY	<unknown>	[................]
0x000000720d660000	0x000000720d6cc000	442368	MEM_PRIVATE	MEM_RESERVE		Stack	[~0; 17e0.77c0]
0x00000173ad390000	0x00000173ad3b8000	163840	MEM_MAPPED	MEM_COMMIT	PAGE_READONLY	MappedFile	"\Device\HarddiskVolume3\Windows\System32\C_932.NLS"
0x00000173ad3c0000	0x00000173ad3c3000	12288	MEM_MAPPED	MEM_COMMIT	PAGE_READONLY	MappedFile	"\Device\HarddiskVolume3\Windows\System32\l_intl.nls"
0x00000173ad410000	0x00000173ad420000	65536	MEM_MAPPED	MEM_COMMIT	PAGE_READWRITE	MappedFile	"PageFile"
0x00000173ad510000	0x00000173ad51c000	49152	MEM_PRIVATE	MEM_COMMIT	PAGE_READWRITE	Heap	[ID: 0; Handle: 00000173ad510000; Type: Segment]
0x00007ff9d2c60000	0x00007ff9d2c61000	4096	MEM_IMAGE	MEM_COMMIT	PAGE_READONLY	Image	[ntdll; "ntdll.dll"]
0x00007ff9d2c61000	0x00007ff9d2dd3000	1515520	MEM_IMAGE	MEM_COMMIT	PAGE_EXECUTE_READ	Image	[ntdll; "ntdll.dll"]
0x00007ff9d2dd3000	0x00007ff9d2e2c000	364544	MEM_IMAGE	MEM_COMMIT	PAGE_READONLY	Image	[ntdll; "ntdll.dll"]
0x00007ff9d2e2c000	0x00007ff9d2e35000	36864	MEM_IMAGE	MEM_COMMIT	PAGE_READWRITE	Image	[ntdll; "ntdll.dll"]
0x00007ff9d2e35000	0x00007ff9d2ec7000	598016	MEM_IMAGE	MEM_COMMIT	PAGE_READONLY	Image	[ntdll; "ntdll.dll"]
"#;
    let map = AddressMap::from_command_output(s).unwrap();
    let regions = map.regions();
    assert_eq!(regions.len(), 19);

    {
        let r = &regions[0];
        assert_eq!(r.base_address, 0);
        assert_eq!(r.end_address, 0x7ffe0000);
        assert_eq!(r.region_size, 2147352576);
        assert_eq!(r.r#type, None);
        assert_eq!(r.state, MemoryState::Free);
        assert_eq!(r.protect, Some(MemoryProtection::NoAccess));
        let RegionSummary::Free = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
    }

    {
        let r = &regions[1];
        assert_eq!(r.base_address, 0x7ffe0000);
        assert_eq!(r.end_address, 0x7ffe1000);
        assert_eq!(r.region_size, 4096);
        assert_eq!(r.r#type, Some(MemoryType::Private));
        assert_eq!(r.state, MemoryState::Commit);
        assert_eq!(r.protect, Some(MemoryProtection::ReadOnly));
        let RegionSummary::Other(s) = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
        assert_eq!(s.info, "User Shared Data");
    }

    {
        let r = &regions[2];
        assert_eq!(r.base_address, 0x6f17334000);
        assert_eq!(r.end_address, 0x6f17335000);
        assert_eq!(r.region_size, 4096);
        assert_eq!(r.r#type, Some(MemoryType::Private));
        assert_eq!(r.state, MemoryState::Commit);
        assert_eq!(r.protect, Some(MemoryProtection::ReadWrite));
        let RegionSummary::Peb(s) = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
        assert_eq!(s.pid, 0x17e0);
    }

    {
        let r = &regions[3];
        assert_eq!(r.base_address, 0x6f17335000);
        assert_eq!(r.end_address, 0x6f17337000);
        assert_eq!(r.region_size, 8192);
        assert_eq!(r.r#type, Some(MemoryType::Private));
        assert_eq!(r.state, MemoryState::Commit);
        assert_eq!(r.protect, Some(MemoryProtection::ReadWrite));
        let RegionSummary::Teb(s) = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
        assert_eq!(s.engine_thread_id, 0);
        assert_eq!(s.process_id, 0x17e0);
        assert_eq!(s.thread_id, 0x77c0);
    }

    {
        let r = &regions[6];
        assert_eq!(r.base_address, 0x6f1733d000);
        assert_eq!(r.end_address, 0x6f17400000);
        assert_eq!(r.region_size, 798720);
        assert_eq!(r.r#type, Some(MemoryType::Private));
        assert_eq!(r.state, MemoryState::Reserve);
        assert_eq!(r.protect, None);
        let RegionSummary::Unknown = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
    }

    {
        let r = &regions[10];
        assert_eq!(r.base_address, 0x173ad390000);
        assert_eq!(r.end_address, 0x173ad3b8000);
        assert_eq!(r.region_size, 163840);
        assert_eq!(r.r#type, Some(MemoryType::Mapped));
        assert_eq!(r.state, MemoryState::Commit);
        assert_eq!(r.protect, Some(MemoryProtection::ReadOnly));
        let RegionSummary::MappedFile(s) = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
        assert_eq!(
            s.mapped_file,
            r#"\Device\HarddiskVolume3\Windows\System32\C_932.NLS"#
        );
    }

    {
        let r = &regions[13];
        assert_eq!(r.base_address, 0x173ad510000);
        assert_eq!(r.end_address, 0x173ad51c000);
        assert_eq!(r.region_size, 49152);
        assert_eq!(r.r#type, Some(MemoryType::Private));
        assert_eq!(r.state, MemoryState::Commit);
        assert_eq!(r.protect, Some(MemoryProtection::ReadWrite));
        let RegionSummary::Heap(s) = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
        assert_eq!(s.id, 0);
        assert_eq!(s.handle, 0x173ad510000);
        assert_eq!(s.r#type, "Segment");
    }

    {
        let r = &regions[14];
        assert_eq!(r.base_address, 0x7ff9d2c60000);
        assert_eq!(r.end_address, 0x7ff9d2c61000);
        assert_eq!(r.region_size, 4096);
        assert_eq!(r.r#type, Some(MemoryType::Image));
        assert_eq!(r.state, MemoryState::Commit);
        assert_eq!(r.protect, Some(MemoryProtection::ReadOnly));
        let RegionSummary::Image(s) = &r.summary else {
            panic!("unexpected region summary: {:?}", r.summary);
        };
        assert_eq!(s.module_name, "ntdll");
        assert_eq!(s.image_path, "ntdll.dll");
    }

    assert_eq!(
        map.find_region(0x00007ff9d2c60000).unwrap().base_address,
        0x00007ff9d2c60000
    );
    assert_eq!(
        map.find_region(0x00007ff9d2c60001).unwrap().base_address,
        0x00007ff9d2c60000
    );
    assert_eq!(
        map.find_region(0x00007ff9d2c60fff).unwrap().base_address,
        0x00007ff9d2c60000
    );
    assert_eq!(
        map.find_region(0x00007ff9d2c61000).unwrap().base_address,
        0x00007ff9d2c61000
    );
    assert!(map.find_region(0x00007ff9d2ec7000).is_none());
}