bumparena 0.0.1

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.
@@ -0,0 +1,34 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm run build
20
+ - run: npm test
21
+
22
+ publish-npm:
23
+ needs: build
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-node@v4
28
+ with:
29
+ node-version: 20
30
+ registry-url: https://registry.npmjs.org/
31
+ - run: npm ci
32
+ - run: npm publish
33
+ env:
34
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eugen252009
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ ## High-Speed: Linear Bulk Import
2
+
3
+ Use `reserve()` for high-volume, linear data ingestion. To guarantee maximum throughput and deterministic behavior, `reserve()` **always appends** memory at the end of the arena buffer. No compaction, no relocation, no surprises.
4
+
5
+ This makes it ideal for tight loops, streaming parsers, and bulk loaders where predictable memory behavior is critical.
6
+
7
+ ---
8
+
9
+ ### Allocation Strategies
10
+
11
+ #### Performance & GC Efficiency
12
+
13
+ This Arena is explicitly designed to bypass the typical performance pitfalls of the V8 runtime:
14
+
15
+ **GC Invisibility**
16
+ The entire arena is backed by a single `ArrayBuffer`. Since the Garbage Collector does not traverse raw buffer contents, this completely avoids costly mark-and-sweep scans over arena memory.
17
+
18
+ **Zero Object Churn**
19
+ Data insertion does not allocate JavaScript objects. All writes are performed directly against the buffer. The engine reuses the same execution paths without producing heap garbage.
20
+
21
+ **Deferred Management Overhead**
22
+ Logical pointers (`ArenaLocation`, implemented as `BigInt`) are created *only* when `label()` is invoked. This keeps pointer bookkeeping out of performance-critical hot paths.
23
+
24
+ ---
25
+
26
+ ### Technical Architecture
27
+
28
+ #### Memory Layout (16-Byte Header)
29
+
30
+ Each allocation block is prefixed with a fixed-size metadata header:
31
+
32
+ - Size
33
+ - Generation ID
34
+ - Flags / reserved space
35
+
36
+ `reserve()` eagerly initializes these headers to ensure full compatibility with `read()` and `label()` without requiring additional bookkeeping passes.
37
+
38
+ This guarantees **O(1)** access and validation at read time.
39
+
40
+ ---
41
+
42
+ #### Pointer Security & Validation
43
+
44
+ An `ArenaLocation` is encoded as a **64-bit BigInt**:
45
+
46
+ - **High 32 bits** → Physical byte offset inside the arena
47
+ - **Low 32 bits** → Generation ID
48
+
49
+ When `read()` is called, the arena validates the pointer by comparing its generation ID with the one stored in the block header:
50
+
51
+ - ✅ Match → valid, data is returned
52
+ - ❌ Mismatch → block was freed or recycled → `null` is returned
53
+
54
+ This provides **use-after-free protection** without reference tracking, weak maps, or GC involvement.
55
+
56
+ ---
57
+
58
+ ### 📖 Documentation & Tests
59
+
60
+ The integrated test suite doubles as a **reference implementation**.
61
+ See the `// --- SHOWCASE & TESTS ---` section at the end of the source file for:
62
+
63
+ - Stress tests for dynamic resizing
64
+ - Validation of pointer expiration semantics
65
+ - Advanced bucket reuse and fragmentation behavior
66
+ - Edge cases around generation rollover
67
+
68
+ To run the showcase directly, execute the file with Node.js. No external dependencies required.
69
+
70
+ ---
71
+
72
+ ### ⚖️ License
73
+
74
+ MIT License — use freely in commercial and non-commercial projects.
package/arena.ts ADDED
@@ -0,0 +1,214 @@
1
+ export type ArenaLocation = bigint & { readonly __data_pointer: unique symbol };
2
+ export interface ArenaOptions {
3
+ initalSize?: number
4
+ littleEndian?: boolean
5
+ allignment?: 8 | 16 | 32 | 64
6
+ bucketOffsets?: number[];
7
+ bucketCapacities?: number[];
8
+ }
9
+ export interface ArenaCustomHeaders {
10
+ header0: number,
11
+ header1: number,
12
+ header2: number
13
+ }
14
+ export interface ArenaHeaders {
15
+ totalLength: number
16
+ payloadlength: number
17
+ deleted: boolean
18
+ header0: number
19
+ header1: number
20
+ header2: number
21
+ }
22
+ const enum SHIFTOFFSETS {
23
+ BYTE_8 = 0, // No SHIFT needed
24
+ BYTE_16 = 1,
25
+ BYTE_32 = 2,
26
+ BYTE_64 = 3,
27
+ }
28
+ const enum HEADERS {
29
+ TOTAL_LENGTH_0_32 = 0,
30
+ PAYLOAD_LENGTH_0_32 = 1,
31
+ GENERATION_BYTE_0_32 = 2,
32
+ DELETED_8 = 12,
33
+ USER_STATUS_0_8 = 13,
34
+ USER_STATUS_1_8 = 14,
35
+ USER_STATUS_2_8 = 15,
36
+ }
37
+
38
+ export class Arena {
39
+ private HEADER_SIZE_BYTES = 16
40
+ private _buffer: ArrayBuffer
41
+ private _view8: Uint8Array
42
+ private _view32: Uint32Array
43
+ private _offset: number
44
+ private _emptySpots: Uint32Array
45
+ private _allignMask: 7 | 15 | 31 | 63
46
+ private _allignShift: number
47
+ private _bucketOffsets: number[];
48
+ private _bucketCapacities: number[];
49
+ private _bucketcount: number;
50
+
51
+ constructor(options?: ArenaOptions) {
52
+ this._buffer = new ArrayBuffer(options?.initalSize || 64 * 1024)
53
+ this._view8 = new Uint8Array(this._buffer);
54
+ this._view32 = new Uint32Array(this._buffer)
55
+ this._offset = 0;
56
+ //@ts-ignore
57
+ this._allignMask = ((options?.allignment || 8) - 1!) as number
58
+ this._allignShift = Math.log2(this._allignMask + 1);
59
+ this._bucketCapacities = options?.bucketCapacities || [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024]
60
+ this._bucketOffsets = [];
61
+ this._bucketcount = this._bucketCapacities.length;
62
+ let currentOffset = 0;
63
+ for (const cap of this._bucketCapacities) {
64
+ this._bucketOffsets.push(currentOffset)
65
+ currentOffset += (cap + 1)
66
+ }
67
+ this._emptySpots = new Uint32Array(currentOffset)
68
+ }
69
+ private _getBucketCount(bucketIdx: number): number {
70
+ return this._emptySpots[this._bucketOffsets[bucketIdx]!]!;
71
+ }
72
+
73
+ private _setBucketCount(bucketIdx: number, count: number): void {
74
+ this._emptySpots[this._bucketOffsets[bucketIdx]!] = count;
75
+ }
76
+
77
+ private _getBucketOffset(bucketIdx: number, slotIdx: number): number {
78
+ return this._emptySpots[this._bucketOffsets[bucketIdx]! + slotIdx + 1]!;
79
+ }
80
+
81
+ private _setBucketOffset(bucketIdx: number, slotIdx: number, offset: number): void {
82
+ this._emptySpots[this._bucketOffsets[bucketIdx]! + slotIdx + 1] = offset;
83
+ }
84
+
85
+ private _initBlock(start: number, dataLength: number, { header0, header1, header2 }: { header0?: number, header1?: number, header2?: number }) {
86
+ const h0 = (header0 || 0) & 0xFF;
87
+ const h1 = (header1 || 0) & 0xFF;
88
+ const h2 = (header2 || 0) & 0xFF;
89
+ const shift32 = start >> SHIFTOFFSETS.BYTE_32
90
+ this._view32[shift32 + HEADERS.TOTAL_LENGTH_0_32] = (dataLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask
91
+ this._view32[shift32 + HEADERS.PAYLOAD_LENGTH_0_32] = dataLength
92
+ this._view32[shift32 + (HEADERS.DELETED_8 >> SHIFTOFFSETS.BYTE_32)] = (h2 << 24) | (h1 << 16) | (h0 << 8) | 0
93
+ }
94
+
95
+ public alloc(data: Uint8Array, headers?: ArenaCustomHeaders): ArenaLocation {
96
+ if (headers == undefined) headers = { header0: 0, header1: 0, header2: 0 }
97
+ const totalBlockSize = (data.byteLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
98
+ const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
99
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
100
+ const count = this._getBucketCount(bucketIdx);
101
+ if (count > 0) {
102
+ const recycledOffset = this._getBucketOffset(bucketIdx, count - 1);
103
+ this._setBucketCount(bucketIdx, count - 1);
104
+ this._initBlock(recycledOffset, data.length, headers);
105
+ return (BigInt(recycledOffset) << 32n | BigInt(this._view32[(recycledOffset >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)) as ArenaLocation
106
+ }
107
+ }
108
+ const start = this._offset
109
+ this._checkForSpace(data.byteLength + this.HEADER_SIZE_BYTES) && this._resize()
110
+ this._initBlock(start, data.byteLength, headers || {})
111
+ this._view8.set(data, start + this.HEADER_SIZE_BYTES)
112
+ this._offset = (data.byteLength + this.HEADER_SIZE_BYTES + this._offset + this._allignMask) & ~this._allignMask;
113
+ return (BigInt(start) << 32n | BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)) as ArenaLocation
114
+ }
115
+ public read(location: ArenaLocation): Uint8Array | null {
116
+ let start = Number(BigInt(location as bigint) >> 32n)
117
+ const generation = BigInt(location as bigint) & 0xFFFFFFFFn
118
+ let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32]!
119
+ const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]!)
120
+ if (diff !== 0n) return null
121
+ return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + Number(length))
122
+ }
123
+ public free(location: ArenaLocation): ArenaLocation {
124
+ let start = Number(BigInt(location as bigint) >> 32n)
125
+ const currgen = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!
126
+ this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32] = currgen + 1 // this part can wrap around
127
+ this._view8[start + HEADERS.DELETED_8] = 1
128
+ const totalBlockSize = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32]!
129
+ const bucketIdx = (totalBlockSize >> this._allignShift) - 1
130
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
131
+ const count = this._getBucketCount(bucketIdx)
132
+ const capacity = this._bucketCapacities[bucketIdx]!
133
+ if (count < capacity) {
134
+ this._setBucketOffset(bucketIdx, count, start);
135
+ this._setBucketCount(bucketIdx, count + 1);
136
+ }
137
+ }
138
+ return 0n as ArenaLocation
139
+ }
140
+
141
+ private _checkForSpace(size: number): boolean {
142
+ if (this._buffer.byteLength >= this._offset + size) return false
143
+ return true
144
+ }
145
+
146
+ private _resize() {
147
+ //@ts-ignore
148
+ this._buffer = this._buffer.transfer(this._buffer.byteLength * 2)
149
+ this._view8 = new Uint8Array(this._buffer)
150
+ this._view32 = new Uint32Array(this._buffer)
151
+ }
152
+ public size(): number {
153
+ return this._buffer.byteLength
154
+ }
155
+ public getBuffer(): Uint8Array {
156
+ return this._view8.subarray(0, this._offset)
157
+ }
158
+ public reserve(size: number): Uint8Array {
159
+ const start = this._offset
160
+ this._checkForSpace(start + this.HEADER_SIZE_BYTES + size + this._allignMask & ~this._allignMask) && this._resize()
161
+ this._initBlock(start, size, {})
162
+ this._offset = this._offset + size + this.HEADER_SIZE_BYTES + this._allignMask & ~this._allignMask
163
+ return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + size)
164
+ }
165
+ public translate(ptr: ArenaLocation) {
166
+ let start = Number(BigInt(ptr as bigint) >> 32n)
167
+ const generation = BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)
168
+ return { start, generation }
169
+ }
170
+ public readWithHeaders(ptr: ArenaLocation): Uint8Array | null {
171
+ let start = Number(BigInt(ptr as bigint) >> 32n)
172
+ const generation = BigInt(ptr as bigint) & 0xFFFFFFFFn
173
+ let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32]!
174
+ const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]!)
175
+ if (diff !== 0n) return null
176
+ return this._view8.subarray(start + 12, start + this.HEADER_SIZE_BYTES + Number(length))
177
+ }
178
+ public label(): Array<ArenaLocation> {
179
+ const ptrArray = new Array<ArenaLocation>()
180
+ const limit = this._offset;
181
+ let pos = 0
182
+ while (pos < limit) {
183
+ const totalLength = this._view32[pos >> SHIFTOFFSETS.BYTE_32]!;
184
+ if (totalLength === 0) {
185
+ pos = (pos + 16) & ~15
186
+ continue;
187
+ }
188
+ const isDeleted = this._view8[pos + HEADERS.DELETED_8] === 1
189
+ if (!isDeleted) {
190
+ ptrArray.push(((BigInt(pos) << 32n) | BigInt(this._view32[(pos >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]!)) as ArenaLocation);
191
+ }
192
+ pos += totalLength
193
+ if (totalLength === 0) break;
194
+ }
195
+ return ptrArray
196
+ }
197
+ public getHeaders(ptr: ArenaLocation): ArenaHeaders {
198
+ let start = Number(BigInt(ptr as bigint) >> 32n)
199
+ return {
200
+ totalLength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32]!.toString()),
201
+ payloadlength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32]!.toString()),
202
+ deleted: this._view8[start + HEADERS.DELETED_8] === 1,
203
+ header0: Number(this._view8[start + HEADERS.USER_STATUS_0_8]!.toString()),
204
+ header1: Number(this._view8[start + HEADERS.USER_STATUS_1_8]!.toString()),
205
+ header2: Number(this._view8[start + HEADERS.USER_STATUS_2_8]!.toString()),
206
+ }
207
+ }
208
+ public estimate(size: number, amnt: number): number {
209
+ return (((size + this._allignMask) & ~this._allignMask) + this.HEADER_SIZE_BYTES) * amnt
210
+ }
211
+ }
212
+
213
+
214
+
@@ -0,0 +1,58 @@
1
+ export type ArenaLocation = bigint & {
2
+ readonly __data_pointer: unique symbol;
3
+ };
4
+ export interface ArenaOptions {
5
+ initalSize?: number;
6
+ littleEndian?: boolean;
7
+ allignment?: 8 | 16 | 32 | 64;
8
+ bucketOffsets?: number[];
9
+ bucketCapacities?: number[];
10
+ }
11
+ export interface ArenaCustomHeaders {
12
+ header0: number;
13
+ header1: number;
14
+ header2: number;
15
+ }
16
+ export interface ArenaHeaders {
17
+ totalLength: number;
18
+ payloadlength: number;
19
+ deleted: boolean;
20
+ header0: number;
21
+ header1: number;
22
+ header2: number;
23
+ }
24
+ export declare class Arena {
25
+ private HEADER_SIZE_BYTES;
26
+ private _buffer;
27
+ private _view8;
28
+ private _view32;
29
+ private _offset;
30
+ private _emptySpots;
31
+ private _allignMask;
32
+ private _allignShift;
33
+ private _bucketOffsets;
34
+ private _bucketCapacities;
35
+ private _bucketcount;
36
+ constructor(options?: ArenaOptions);
37
+ private _getBucketCount;
38
+ private _setBucketCount;
39
+ private _getBucketOffset;
40
+ private _setBucketOffset;
41
+ private _initBlock;
42
+ alloc(data: Uint8Array, headers?: ArenaCustomHeaders): ArenaLocation;
43
+ read(location: ArenaLocation): Uint8Array | null;
44
+ free(location: ArenaLocation): ArenaLocation;
45
+ private _checkForSpace;
46
+ private _resize;
47
+ size(): number;
48
+ getBuffer(): Uint8Array;
49
+ reserve(size: number): Uint8Array;
50
+ translate(ptr: ArenaLocation): {
51
+ start: number;
52
+ generation: bigint;
53
+ };
54
+ readWithHeaders(ptr: ArenaLocation): Uint8Array | null;
55
+ label(): Array<ArenaLocation>;
56
+ getHeaders(ptr: ArenaLocation): ArenaHeaders;
57
+ estimate(size: number, amnt: number): number;
58
+ }
package/dist/arena.js ADDED
@@ -0,0 +1,178 @@
1
+ var SHIFTOFFSETS;
2
+ (function (SHIFTOFFSETS) {
3
+ SHIFTOFFSETS[SHIFTOFFSETS["BYTE_8"] = 0] = "BYTE_8";
4
+ SHIFTOFFSETS[SHIFTOFFSETS["BYTE_16"] = 1] = "BYTE_16";
5
+ SHIFTOFFSETS[SHIFTOFFSETS["BYTE_32"] = 2] = "BYTE_32";
6
+ SHIFTOFFSETS[SHIFTOFFSETS["BYTE_64"] = 3] = "BYTE_64";
7
+ })(SHIFTOFFSETS || (SHIFTOFFSETS = {}));
8
+ var HEADERS;
9
+ (function (HEADERS) {
10
+ HEADERS[HEADERS["TOTAL_LENGTH_0_32"] = 0] = "TOTAL_LENGTH_0_32";
11
+ HEADERS[HEADERS["PAYLOAD_LENGTH_0_32"] = 1] = "PAYLOAD_LENGTH_0_32";
12
+ HEADERS[HEADERS["GENERATION_BYTE_0_32"] = 2] = "GENERATION_BYTE_0_32";
13
+ HEADERS[HEADERS["DELETED_8"] = 12] = "DELETED_8";
14
+ HEADERS[HEADERS["USER_STATUS_0_8"] = 13] = "USER_STATUS_0_8";
15
+ HEADERS[HEADERS["USER_STATUS_1_8"] = 14] = "USER_STATUS_1_8";
16
+ HEADERS[HEADERS["USER_STATUS_2_8"] = 15] = "USER_STATUS_2_8";
17
+ })(HEADERS || (HEADERS = {}));
18
+ export class Arena {
19
+ constructor(options) {
20
+ this.HEADER_SIZE_BYTES = 16;
21
+ this._buffer = new ArrayBuffer(options?.initalSize || 64 * 1024);
22
+ this._view8 = new Uint8Array(this._buffer);
23
+ this._view32 = new Uint32Array(this._buffer);
24
+ this._offset = 0;
25
+ //@ts-ignore
26
+ this._allignMask = ((options?.allignment || 8) - 1);
27
+ this._allignShift = Math.log2(this._allignMask + 1);
28
+ this._bucketCapacities = options?.bucketCapacities || [1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024];
29
+ this._bucketOffsets = [];
30
+ this._bucketcount = this._bucketCapacities.length;
31
+ let currentOffset = 0;
32
+ for (const cap of this._bucketCapacities) {
33
+ this._bucketOffsets.push(currentOffset);
34
+ currentOffset += (cap + 1);
35
+ }
36
+ this._emptySpots = new Uint32Array(currentOffset);
37
+ }
38
+ _getBucketCount(bucketIdx) {
39
+ return this._emptySpots[this._bucketOffsets[bucketIdx]];
40
+ }
41
+ _setBucketCount(bucketIdx, count) {
42
+ this._emptySpots[this._bucketOffsets[bucketIdx]] = count;
43
+ }
44
+ _getBucketOffset(bucketIdx, slotIdx) {
45
+ return this._emptySpots[this._bucketOffsets[bucketIdx] + slotIdx + 1];
46
+ }
47
+ _setBucketOffset(bucketIdx, slotIdx, offset) {
48
+ this._emptySpots[this._bucketOffsets[bucketIdx] + slotIdx + 1] = offset;
49
+ }
50
+ _initBlock(start, dataLength, { header0, header1, header2 }) {
51
+ const h0 = (header0 || 0) & 0xFF;
52
+ const h1 = (header1 || 0) & 0xFF;
53
+ const h2 = (header2 || 0) & 0xFF;
54
+ const shift32 = start >> SHIFTOFFSETS.BYTE_32;
55
+ this._view32[shift32 + HEADERS.TOTAL_LENGTH_0_32] = (dataLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
56
+ this._view32[shift32 + HEADERS.PAYLOAD_LENGTH_0_32] = dataLength;
57
+ this._view32[shift32 + (HEADERS.DELETED_8 >> SHIFTOFFSETS.BYTE_32)] = (h2 << 24) | (h1 << 16) | (h0 << 8) | 0;
58
+ }
59
+ alloc(data, headers) {
60
+ if (headers == undefined)
61
+ headers = { header0: 0, header1: 0, header2: 0 };
62
+ const totalBlockSize = (data.byteLength + this.HEADER_SIZE_BYTES + this._allignMask) & ~this._allignMask;
63
+ const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
64
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
65
+ const count = this._getBucketCount(bucketIdx);
66
+ if (count > 0) {
67
+ const recycledOffset = this._getBucketOffset(bucketIdx, count - 1);
68
+ this._setBucketCount(bucketIdx, count - 1);
69
+ this._initBlock(recycledOffset, data.length, headers);
70
+ return (BigInt(recycledOffset) << 32n | BigInt(this._view32[(recycledOffset >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]));
71
+ }
72
+ }
73
+ const start = this._offset;
74
+ this._checkForSpace(data.byteLength + this.HEADER_SIZE_BYTES) && this._resize();
75
+ this._initBlock(start, data.byteLength, headers || {});
76
+ this._view8.set(data, start + this.HEADER_SIZE_BYTES);
77
+ this._offset = (data.byteLength + this.HEADER_SIZE_BYTES + this._offset + this._allignMask) & ~this._allignMask;
78
+ return (BigInt(start) << 32n | BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]));
79
+ }
80
+ read(location) {
81
+ let start = Number(BigInt(location) >> 32n);
82
+ const generation = BigInt(location) & 0xffffffffn;
83
+ let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32];
84
+ const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]);
85
+ if (diff !== 0n)
86
+ return null;
87
+ return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + Number(length));
88
+ }
89
+ free(location) {
90
+ let start = Number(BigInt(location) >> 32n);
91
+ const currgen = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32];
92
+ this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32] = currgen + 1; // this part can wrap around
93
+ this._view8[start + HEADERS.DELETED_8] = 1;
94
+ const totalBlockSize = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32];
95
+ const bucketIdx = (totalBlockSize >> this._allignShift) - 1;
96
+ if (bucketIdx >= 0 && bucketIdx < this._bucketcount) {
97
+ const count = this._getBucketCount(bucketIdx);
98
+ const capacity = this._bucketCapacities[bucketIdx];
99
+ if (count < capacity) {
100
+ this._setBucketOffset(bucketIdx, count, start);
101
+ this._setBucketCount(bucketIdx, count + 1);
102
+ }
103
+ }
104
+ return 0n;
105
+ }
106
+ _checkForSpace(size) {
107
+ if (this._buffer.byteLength >= this._offset + size)
108
+ return false;
109
+ return true;
110
+ }
111
+ _resize() {
112
+ //@ts-ignore
113
+ this._buffer = this._buffer.transfer(this._buffer.byteLength * 2);
114
+ this._view8 = new Uint8Array(this._buffer);
115
+ this._view32 = new Uint32Array(this._buffer);
116
+ }
117
+ size() {
118
+ return this._buffer.byteLength;
119
+ }
120
+ getBuffer() {
121
+ return this._view8.subarray(0, this._offset);
122
+ }
123
+ reserve(size) {
124
+ const start = this._offset;
125
+ this._checkForSpace(start + this.HEADER_SIZE_BYTES + size + this._allignMask & ~this._allignMask) && this._resize();
126
+ this._initBlock(start, size, {});
127
+ this._offset = this._offset + size + this.HEADER_SIZE_BYTES + this._allignMask & ~this._allignMask;
128
+ return this._view8.subarray(start + this.HEADER_SIZE_BYTES, start + this.HEADER_SIZE_BYTES + size);
129
+ }
130
+ translate(ptr) {
131
+ let start = Number(BigInt(ptr) >> 32n);
132
+ const generation = BigInt(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32]);
133
+ return { start, generation };
134
+ }
135
+ readWithHeaders(ptr) {
136
+ let start = Number(BigInt(ptr) >> 32n);
137
+ const generation = BigInt(ptr) & 0xffffffffn;
138
+ let length = this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32];
139
+ const diff = generation ^ BigInt(this._view32[((start >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32)]);
140
+ if (diff !== 0n)
141
+ return null;
142
+ return this._view8.subarray(start + 12, start + this.HEADER_SIZE_BYTES + Number(length));
143
+ }
144
+ label() {
145
+ const ptrArray = new Array();
146
+ const limit = this._offset;
147
+ let pos = 0;
148
+ while (pos < limit) {
149
+ const totalLength = this._view32[pos >> SHIFTOFFSETS.BYTE_32];
150
+ if (totalLength === 0) {
151
+ pos = (pos + 16) & ~15;
152
+ continue;
153
+ }
154
+ const isDeleted = this._view8[pos + HEADERS.DELETED_8] === 1;
155
+ if (!isDeleted) {
156
+ ptrArray.push(((BigInt(pos) << 32n) | BigInt(this._view32[(pos >> SHIFTOFFSETS.BYTE_32) + HEADERS.GENERATION_BYTE_0_32])));
157
+ }
158
+ pos += totalLength;
159
+ if (totalLength === 0)
160
+ break;
161
+ }
162
+ return ptrArray;
163
+ }
164
+ getHeaders(ptr) {
165
+ let start = Number(BigInt(ptr) >> 32n);
166
+ return {
167
+ totalLength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.TOTAL_LENGTH_0_32].toString()),
168
+ payloadlength: Number(this._view32[(start >> SHIFTOFFSETS.BYTE_32) + HEADERS.PAYLOAD_LENGTH_0_32].toString()),
169
+ deleted: this._view8[start + HEADERS.DELETED_8] === 1,
170
+ header0: Number(this._view8[start + HEADERS.USER_STATUS_0_8].toString()),
171
+ header1: Number(this._view8[start + HEADERS.USER_STATUS_1_8].toString()),
172
+ header2: Number(this._view8[start + HEADERS.USER_STATUS_2_8].toString()),
173
+ };
174
+ }
175
+ estimate(size, amnt) {
176
+ return (((size + this._allignMask) & ~this._allignMask) + this.HEADER_SIZE_BYTES) * amnt;
177
+ }
178
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "bumparena",
3
+ "version":"0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "dist/arena.js",
7
+ "module": "dist/arena.js",
8
+ "types": "dist/arena.d.ts",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "node ./dist/arena.js"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^25.2.3"
15
+ }
16
+ }
package/test/test.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { Arena, ArenaCustomHeaders } from "../arena.ts"
2
+
3
+ //Tests
4
+ function Test() {
5
+ console.log("running Tests")
6
+ const a = new Arena()
7
+ const testdata = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7])
8
+ const headers = { header0: 42, header1: 24, header2: 240 };
9
+ _testCheckPtrAccess(a, testdata, headers)
10
+ }
11
+
12
+ function _testCheckPtrAccess(a: Arena, testdata: Uint8Array, headers: ArenaCustomHeaders) {
13
+ // USE-AFTER-FREE
14
+ const useAfterFreePtr = a.alloc(testdata, headers)
15
+ a.free(useAfterFreePtr)
16
+ // free Slot reuse
17
+ const ptr = a.alloc(testdata, headers)
18
+ const res = a.read(ptr)
19
+ if (res == null) throw new Error(`${testdata} Could not read data`)
20
+ const resultheaders = a.getHeaders(ptr)
21
+
22
+ if (resultheaders.totalLength !== testdata.byteLength + 16) throw new Error(`totalLength not right: got: ${resultheaders.totalLength} needed: ${testdata.byteLength}`)
23
+ if (resultheaders.payloadlength !== testdata.byteLength) throw new Error(`payloadLength not right: got: ${resultheaders.payloadlength} needed: ${testdata.byteLength}`)
24
+ if (resultheaders.header0 !== headers.header0) throw new Error(`header0: got: ${resultheaders.header0} needed: ${headers.header0}`)
25
+ if (resultheaders.header1 !== headers.header1) throw new Error(`header1: got: ${resultheaders.header1} needed: ${headers.header1}`)
26
+ if (resultheaders.header2 !== headers.header2) throw new Error(`header2: got: ${resultheaders.header2} needed: ${headers.header2}`)
27
+
28
+ if (res.toString() !== testdata.toString()) {
29
+ throw new Error(`${testdata} had a problem with data integrety`)
30
+ }
31
+ //final use after free with Generation this should never be possible
32
+ if (a.read(useAfterFreePtr) !== null) {
33
+ throw new Error(`Use after Free is Possible, this is a big Problem`)
34
+ }
35
+ console.log("CheckPtrAccess Done!")
36
+ }
37
+
38
+ Test()
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ES2020"],
4
+ "target": "ES2020",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowJs": true,
8
+ "verbatimModuleSyntax": true,
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "noImplicitOverride": true,
14
+
15
+ // für Build / .d.ts:
16
+ "declaration": true,
17
+ "emitDeclarationOnly": false,
18
+ "outDir": "dist"
19
+ },
20
+ "include": ["arena.ts"]
21
+ }