bun-memory 1.1.28 → 1.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -5
- package/example/benchmark.ts +52 -33
- package/package.json +1 -1
- package/runtime/extensions.ts +1 -1
- package/structs/Memory.ts +86 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Blazing fast, high-performance Windows process memory manipulation for Bun.
|
|
|
11
11
|
- Attach to processes by name or PID
|
|
12
12
|
- Efficient, allocation-free operations using user-provided buffers (scratches)
|
|
13
13
|
- Module enumeration and pointer chain resolution
|
|
14
|
+
- Pattern search with wildcards (`**` and `??`)
|
|
14
15
|
- Read and write all primitive types, arrays, buffers, and common structures
|
|
15
16
|
- Typed helpers for vectors, matrices, colors, and more
|
|
16
17
|
|
|
@@ -27,7 +28,7 @@ bun add bun-memory
|
|
|
27
28
|
|
|
28
29
|
## Quick Start
|
|
29
30
|
|
|
30
|
-
❗ **Important**: [Example: Using Scratches (Recommended)](#example-using-scratches-recommended)
|
|
31
|
+
❗ **Important**: [Example: Using Scratches (Recommended)](#example-using-scratches-recommended)
|
|
31
32
|
|
|
32
33
|
```ts
|
|
33
34
|
import Memory from 'bun-memory';
|
|
@@ -61,26 +62,43 @@ See the code and type definitions for full details. All methods are documented w
|
|
|
61
62
|
## Example: Efficient Scratch Reuse
|
|
62
63
|
|
|
63
64
|
```ts
|
|
65
|
+
// Reuse buffers and arrays for fast, allocation-free memory operations
|
|
64
66
|
const buffer = Buffer.allocUnsafe(256);
|
|
65
|
-
cs2.read(0x12345678n, buffer); // Fills buffer in-place
|
|
67
|
+
void cs2.read(0x12345678n, buffer); // Fills buffer in-place
|
|
66
68
|
// …use buffer…
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
```ts
|
|
72
|
+
// Typed arrays work the same way
|
|
70
73
|
const array = new Float32Array(32);
|
|
71
|
-
cs2.read(0x12345678n, array); // Fills array in-place
|
|
74
|
+
void cs2.read(0x12345678n, array); // Fills array in-place
|
|
72
75
|
// …use buffer…
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
## Example: Pattern Search
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Find a byte pattern in memory (supports wildcards: ** and ??)
|
|
82
|
+
const needle = 'deadbeef';
|
|
83
|
+
// const needle = 'de**beef';
|
|
84
|
+
// const needle = 'de????ef';
|
|
85
|
+
const address = cs2.pattern(needle, 0x10000000n, 0x1000);
|
|
86
|
+
if (address !== -1n) {
|
|
87
|
+
console.log(`Found at 0x${address.toString(16)}`);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
75
91
|
## Example: Pointer Chains
|
|
76
92
|
|
|
77
93
|
```ts
|
|
94
|
+
// Follow a pointer chain to resolve nested addresses
|
|
78
95
|
const address = cs2.follow(0x10000000n, [0x10n, 0x20n]);
|
|
79
96
|
```
|
|
80
97
|
|
|
81
98
|
## Example: Searching Memory
|
|
82
99
|
|
|
83
100
|
```ts
|
|
101
|
+
// Search for a buffer or array in memory
|
|
84
102
|
const needle = Buffer.from([0x01, 0x02, 0x03]);
|
|
85
103
|
// const needle = new Uint8Array([0x01, 0x02, 0x03]);
|
|
86
104
|
// const needle = new Uint32Array([0x012345, 0x123456, 0x234567]);
|
|
@@ -94,6 +112,7 @@ if (address !== -1n) {
|
|
|
94
112
|
## Example: Typed Arrays
|
|
95
113
|
|
|
96
114
|
```ts
|
|
115
|
+
// Read or write arrays of numbers and structures
|
|
97
116
|
const array = cs2.f32Array(0x12345678n, 4); // Float32Array of length 4
|
|
98
117
|
// const array = cs2.u64Array(0x12345678n, 4);
|
|
99
118
|
// const array = cs2.vector3Array(0x12345678n, 4);
|
|
@@ -105,9 +124,9 @@ cs2.vector3Array(0x12345678n, [{ x: 1, y: 2, z: 3 }]);
|
|
|
105
124
|
|
|
106
125
|
## Example: Using Scratches (Recommended)
|
|
107
126
|
|
|
108
|
-
Scratches let you reuse buffers and typed arrays for repeated memory operations, avoiding unnecessary allocations and maximizing performance. This is the most efficient way to read or write large or frequent data.
|
|
109
|
-
|
|
110
127
|
```ts
|
|
128
|
+
// Scratches let you reuse buffers and arrays for repeated memory operations
|
|
129
|
+
// This avoids allocations and maximizes performance
|
|
111
130
|
const array = new BigUint64Array(0xf000 / 0x08);
|
|
112
131
|
|
|
113
132
|
while (true) {
|
package/example/benchmark.ts
CHANGED
|
@@ -35,14 +35,22 @@ console.log('Warming up…');
|
|
|
35
35
|
for (let i = 0; i < 1e6; i++) {
|
|
36
36
|
const GlobalVarsPtr = cs2.u64(ClientPtr + Client.Other.dwGlobalVars);
|
|
37
37
|
/* */ const CurTime = cs2.f32(GlobalVarsPtr + 0x30n);
|
|
38
|
+
|
|
39
|
+
const lPlayerControllerPtr = cs2.u64(ClientPtr + Client.Other.dwLocalPlayerController);
|
|
40
|
+
|
|
41
|
+
const lPlayerPawnPtr = cs2.u64(ClientPtr + Client.Other.dwLocalPlayerPawn);
|
|
42
|
+
/* */ const lHealth = cs2.u32(lPlayerPawnPtr + Client.C_BaseEntity.m_iHealth);
|
|
43
|
+
/* */ const lTeamNum = cs2.u8(lPlayerPawnPtr + Client.C_BaseEntity.m_iTeamNum);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
// Create caches and scratches to optimize performance…
|
|
41
|
-
const
|
|
47
|
+
const BaseEntityPtrs = new Map<string, bigint[]>();
|
|
42
48
|
|
|
43
49
|
const EntityChunkScratch = new BigUint64Array(0xf000 / 0x08);
|
|
44
50
|
const EntityListScratch = new BigUint64Array(0x200 / 0x08);
|
|
45
51
|
|
|
52
|
+
const EntityClassInfoNames = new Map<bigint, string>();
|
|
53
|
+
|
|
46
54
|
// Start the test…
|
|
47
55
|
console.log('Starting the test…');
|
|
48
56
|
|
|
@@ -51,53 +59,64 @@ const performance1 = performance.now();
|
|
|
51
59
|
const EntityListPtr = cs2.u64(ClientPtr + Client.Other.dwEntityList);
|
|
52
60
|
|
|
53
61
|
for (let i = 0; i < 1e6; i++) {
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
try {
|
|
63
|
+
// Traverse the entity list and store it in `BaseEntityPtrs`…
|
|
64
|
+
cs2.read(EntityListPtr + 0x10n, EntityListScratch);
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
// Traverse each of the potential 64 entity chunks…
|
|
67
|
+
for (let i = 0; i < 0x40; i++) {
|
|
68
|
+
const EntityChunkPtr = EntityListScratch[i];
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (EntityChunkPtr === 0n) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
70
|
+
if (EntityChunkPtr === 0n) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
66
73
|
|
|
67
|
-
|
|
74
|
+
cs2.read(EntityChunkPtr, EntityChunkScratch);
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
// Traverse each of the potential 512 entities within this chunk…
|
|
77
|
+
// for (let j = 0, l = 0; j < 0x200; j++, l += 0x0f) {
|
|
78
|
+
for (let l = 0; l < 0x1e00; l += 0x0f) {
|
|
79
|
+
const BaseEntityPtr = EntityChunkScratch[l];
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
if (BaseEntityPtr === 0n) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
const EntityClassInfoPtr = EntityChunkScratch[l + 0x01];
|
|
77
86
|
|
|
78
|
-
|
|
87
|
+
let Name = EntityClassInfoNames.get(EntityClassInfoPtr);
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
if (Name === undefined) {
|
|
90
|
+
const SchemaClassInfoDataPtr = cs2.u64(EntityClassInfoPtr + 0x30n);
|
|
91
|
+
/* */ const NamePtr = cs2.u64(SchemaClassInfoDataPtr + 0x08n);
|
|
92
|
+
/* */ Name = cs2.buffer(NamePtr, 0x20).toString();
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
EntityClassInfoNames.set(EntityClassInfoPtr, Name);
|
|
95
|
+
}
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
}
|
|
97
|
+
let BaseEntityPtrs_ = BaseEntityPtrs.get(Name);
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
if (BaseEntityPtrs_ === undefined) {
|
|
100
|
+
BaseEntityPtrs_ = [];
|
|
91
101
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/* */ const NamePtr = cs2.u64(SchemaClassInfoDataPtr + 0x08n);
|
|
95
|
-
/* */ Name = cs2.cString(NamePtr, 0x2a).toString();
|
|
102
|
+
BaseEntityPtrs.set(Name, BaseEntityPtrs_);
|
|
103
|
+
}
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
BaseEntityPtrs_.push(BaseEntityPtr);
|
|
98
106
|
}
|
|
107
|
+
}
|
|
99
108
|
|
|
100
|
-
|
|
109
|
+
// ! —————————————————————————————————————————————————————————————————————————————————————————————
|
|
110
|
+
// ! YOUR CODE GOES HERE…
|
|
111
|
+
// ! —————————————————————————————————————————————————————————————————————————————————————————————
|
|
112
|
+
|
|
113
|
+
// ! —————————————————————————————————————————————————————————————————————————————————————————————
|
|
114
|
+
// ! YOUR CODE ENDS HERE…
|
|
115
|
+
// ! —————————————————————————————————————————————————————————————————————————————————————————————
|
|
116
|
+
} finally {
|
|
117
|
+
// Clear the entity list…
|
|
118
|
+
for (const BaseEntityPtrs_ of BaseEntityPtrs.values()) {
|
|
119
|
+
BaseEntityPtrs_.length = 0;
|
|
101
120
|
}
|
|
102
121
|
}
|
|
103
122
|
}
|
package/package.json
CHANGED
package/runtime/extensions.ts
CHANGED
|
@@ -182,7 +182,7 @@ declare global {
|
|
|
182
182
|
/**
|
|
183
183
|
* Installs the `ptr` property on all supported binary view prototypes.
|
|
184
184
|
*
|
|
185
|
-
* The property is non-enumerable and configurable. The getter calls `ptr(this)`.
|
|
185
|
+
* The property is non-enumerable and non-configurable. The getter calls `ptr(this)`.
|
|
186
186
|
*/
|
|
187
187
|
const constructors = [ArrayBuffer, BigInt64Array, BigUint64Array, Buffer, DataView, Float32Array, Float64Array, Int16Array, Int32Array, Int8Array, SharedArrayBuffer, Uint16Array, Uint32Array, Uint8Array, Uint8ClampedArray] as const;
|
|
188
188
|
|
package/structs/Memory.ts
CHANGED
|
@@ -109,11 +109,24 @@ class Memory {
|
|
|
109
109
|
throw new Error(`Process not found: ${identifier}…`);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Memory protection flags for safe and unsafe regions.
|
|
114
|
+
* Used to filter readable/writable memory areas.
|
|
115
|
+
*/
|
|
112
116
|
private static readonly MemoryProtections = {
|
|
113
117
|
Safe: 0x10 /* PAGE_EXECUTE */ | 0x20 /* PAGE_EXECUTE_READ */ | 0x40 /* PAGE_EXECUTE_READWRITE */ | 0x80 /* PAGE_EXECUTE_WRITECOPY */ | 0x02 /* PAGE_READONLY */ | 0x04 /* PAGE_READWRITE */ | 0x08 /* PAGE_WRITECOPY */,
|
|
114
118
|
Unsafe: 0x100 /* PAGE_GUARD */ | 0x01 /* PAGE_NOACCESS */,
|
|
115
119
|
};
|
|
116
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Regex patterns for matching hex strings and wildcards in memory scans.
|
|
123
|
+
* Used by the pattern method.
|
|
124
|
+
*/
|
|
125
|
+
private static readonly Patterns = {
|
|
126
|
+
MatchAll: /(?:[0-9A-Fa-f]{2})+/g,
|
|
127
|
+
Test: /^(?:\*{2}|[0-9A-Fa-f]{2}|\?{2})+$/,
|
|
128
|
+
};
|
|
129
|
+
|
|
117
130
|
/**
|
|
118
131
|
* Map of loaded modules in the process, keyed by module name.
|
|
119
132
|
* @example
|
|
@@ -124,6 +137,10 @@ class Memory {
|
|
|
124
137
|
*/
|
|
125
138
|
private _modules: { [key: string]: Module };
|
|
126
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Scratch buffers and typed views for temporary FFI reads/writes.
|
|
142
|
+
* Used internally for efficient memory access and conversions.
|
|
143
|
+
*/
|
|
127
144
|
private readonly Scratch1 = new Uint8Array(0x01);
|
|
128
145
|
private readonly Scratch1Int8Array = new Int8Array(this.Scratch1.buffer, this.Scratch1.byteOffset, 0x01);
|
|
129
146
|
|
|
@@ -324,6 +341,75 @@ class Memory {
|
|
|
324
341
|
return address + (offsets[last] ?? 0n);
|
|
325
342
|
}
|
|
326
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Finds the address of a byte pattern in memory. `**` and `??` match any byte.
|
|
346
|
+
* @param needle Hex string pattern to search for (e.g., 'deadbeed', 'dead**ef', 'dead??ef').
|
|
347
|
+
* @param address Start address to search.
|
|
348
|
+
* @param length Number of bytes to search.
|
|
349
|
+
* @returns Address of the pattern if found, or -1n.
|
|
350
|
+
* @example
|
|
351
|
+
* ```ts
|
|
352
|
+
* const cs2 = new Memory('cs2.exe');
|
|
353
|
+
* const addr = cs2.pattern('dead**ef', 0x10000000n, 0x1000);
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
public pattern(needle: string, address: bigint, length: number): bigint {
|
|
357
|
+
const test = Memory.Patterns.Test.test(needle);
|
|
358
|
+
|
|
359
|
+
if (!test) {
|
|
360
|
+
return -1n;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const tokens = [...needle.matchAll(Memory.Patterns.MatchAll)]
|
|
364
|
+
.map((match) => ({ buffer: Buffer.from(match[0], 'hex'), index: match.index >>> 1, length: match[0].length >>> 1 })) //
|
|
365
|
+
.sort(({ length: a }, { length: b }) => b - a);
|
|
366
|
+
|
|
367
|
+
if (tokens.length === 0) {
|
|
368
|
+
return needle.length >>> 1 <= length ? address : -1n;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const anchor = tokens.shift()!;
|
|
372
|
+
|
|
373
|
+
const haystack = this.buffer(address, length);
|
|
374
|
+
|
|
375
|
+
const end = length - (needle.length >>> 1);
|
|
376
|
+
let start = haystack.indexOf(anchor.buffer); // prettier-ignore
|
|
377
|
+
|
|
378
|
+
if (start === -1) {
|
|
379
|
+
return -1n;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
outer: do {
|
|
383
|
+
const base = start - anchor.index;
|
|
384
|
+
|
|
385
|
+
if (base < 0) {
|
|
386
|
+
continue;
|
|
387
|
+
} else if (base > end) {
|
|
388
|
+
return -1n;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const { buffer, index, length } of tokens) {
|
|
392
|
+
const sourceEnd = base + index + length,
|
|
393
|
+
sourceStart = base + index,
|
|
394
|
+
target = buffer,
|
|
395
|
+
targetEnd = length,
|
|
396
|
+
targetStart = 0;
|
|
397
|
+
|
|
398
|
+
const compare = haystack.compare(target, targetStart, targetEnd, sourceStart, sourceEnd);
|
|
399
|
+
|
|
400
|
+
if (compare !== 0) {
|
|
401
|
+
continue outer;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return address + BigInt(base);
|
|
406
|
+
|
|
407
|
+
// Finish…
|
|
408
|
+
} while ((start = haystack.indexOf(anchor.buffer, start + 0x01)) !== -1);
|
|
409
|
+
|
|
410
|
+
return -1n;
|
|
411
|
+
}
|
|
412
|
+
|
|
327
413
|
/**
|
|
328
414
|
* Reads memory into a buffer.
|
|
329
415
|
* @param address Address to read from.
|