From e1413d2ded99782a49fc2e273e003bbc73818750 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Sat, 11 Apr 2026 06:01:22 -0400 Subject: [PATCH 1/3] fmt --- index.d.ts | 6 ++++++ package.json | 4 ++++ scripts/codegen-index-mjs.mts | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/index.d.ts b/index.d.ts index 1cf52ab..cf754e5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -71,6 +71,7 @@ export function unsafeArrayBufferAt( export function unsafeCountNonNullBytes(ptr: T, maxBytes: number): number /** +<<<<<<< HEAD * A copy of a C UTF-8 string who's properties indicate the original pointer, * offset, and byte length. * @@ -99,3 +100,8 @@ export function unsafeCStringAt +======= + * Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary. + */ +export function unsafeCStringAt(ptr: T, offset?: number, byteLength?: number): string +>>>>>>> fe3e7f3 (fmt) diff --git a/package.json b/package.json index b38431f..5cb91b0 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,12 @@ "build:js": "./scripts/codegen-index-mjs.mts", "build:c": "./scripts/zig-build-prebuilds.mts", "test": "node --test ./index.test.js", +<<<<<<< HEAD "typecheck": "tsc --pretty -p jsconfig.json", "fmt": "oxfmt" +======= + "typecheck": "tsc --pretty -p jsconfig.json" +>>>>>>> fe3e7f3 (fmt) }, "dependencies": { "node-gyp-build": "^4.8.4" diff --git a/scripts/codegen-index-mjs.mts b/scripts/codegen-index-mjs.mts index b5ecddf..922e70f 100755 --- a/scripts/codegen-index-mjs.mts +++ b/scripts/codegen-index-mjs.mts @@ -22,10 +22,17 @@ export function generateIndexMjs(): void { const output = [ "// Generated from index.d.ts by scripts/codegen-index-mjs.mts. Do not edit.", +<<<<<<< HEAD 'import { createRequire } from "node:module"', "", "const require = createRequire(import.meta.url)", 'const binding = require("./index.js")', +======= + "import { createRequire } from 'node:module'", + "", + "const require = createRequire(import.meta.url)", + "const binding = require('./index.js')", +>>>>>>> fe3e7f3 (fmt) "", ...exportNames.map((name) => `export const ${name} = binding.${name}`), "", From 05a7fac7610ee5baa7c92315cfa2e4c4cb07df78 Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Sat, 11 Apr 2026 06:04:57 -0400 Subject: [PATCH 2/3] fmt in ci --- index.d.ts | 6 ------ package.json | 4 ---- scripts/codegen-index-mjs.mts | 7 ------- 3 files changed, 17 deletions(-) diff --git a/index.d.ts b/index.d.ts index cf754e5..1cf52ab 100644 --- a/index.d.ts +++ b/index.d.ts @@ -71,7 +71,6 @@ export function unsafeArrayBufferAt( export function unsafeCountNonNullBytes(ptr: T, maxBytes: number): number /** -<<<<<<< HEAD * A copy of a C UTF-8 string who's properties indicate the original pointer, * offset, and byte length. * @@ -100,8 +99,3 @@ export function unsafeCStringAt -======= - * Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary. - */ -export function unsafeCStringAt(ptr: T, offset?: number, byteLength?: number): string ->>>>>>> fe3e7f3 (fmt) diff --git a/package.json b/package.json index 5cb91b0..b38431f 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,8 @@ "build:js": "./scripts/codegen-index-mjs.mts", "build:c": "./scripts/zig-build-prebuilds.mts", "test": "node --test ./index.test.js", -<<<<<<< HEAD "typecheck": "tsc --pretty -p jsconfig.json", "fmt": "oxfmt" -======= - "typecheck": "tsc --pretty -p jsconfig.json" ->>>>>>> fe3e7f3 (fmt) }, "dependencies": { "node-gyp-build": "^4.8.4" diff --git a/scripts/codegen-index-mjs.mts b/scripts/codegen-index-mjs.mts index 922e70f..b5ecddf 100755 --- a/scripts/codegen-index-mjs.mts +++ b/scripts/codegen-index-mjs.mts @@ -22,17 +22,10 @@ export function generateIndexMjs(): void { const output = [ "// Generated from index.d.ts by scripts/codegen-index-mjs.mts. Do not edit.", -<<<<<<< HEAD 'import { createRequire } from "node:module"', "", "const require = createRequire(import.meta.url)", 'const binding = require("./index.js")', -======= - "import { createRequire } from 'node:module'", - "", - "const require = createRequire(import.meta.url)", - "const binding = require('./index.js')", ->>>>>>> fe3e7f3 (fmt) "", ...exportNames.map((name) => `export const ${name} = binding.${name}`), "", From d7e367132decf9aa737bda29abd73da309d67d5d Mon Sep 17 00:00:00 2001 From: Jake Teton-Landis Date: Sat, 11 Apr 2026 15:14:39 -0400 Subject: [PATCH 3/3] add agents.md, simplify c --- AGENTS.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++ src/binding.c | 115 +++++++++++++++++++++++++++++--------------------- 2 files changed, 172 insertions(+), 49 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5a803cb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,106 @@ +# AGENTS + +## Overview + +- Package name: `unsafe-pointer` +- Purpose: expose a tiny unsafe Node-API addon for turning `ArrayBuffer` memory into raw pointers and raw pointers back into `ArrayBuffer` aliases. +- Runtime loading uses `node-gyp-build`. +- Prebuild generation uses `zig-build`. +- Source-build fallback uses `node-gyp`. + +## Tooling + +- Install tools with `mise install`. +- Tool versions are pinned in [mise.toml](mise.toml). +- Use `bun` for local repo tasks. +- Use `npm` in packed-tarball verification flows and publish flows. + +## Important Files + +- [index.d.ts](index.d.ts): The spec for this package. When the user updates this spec, the agent should update the native implementation to match +- [src/binding.c](src/binding.c): native implementation +- [index.mjs](index.mjs): generated ESM wrapper +- [index.js](index.js): CommonJS loader via `node-gyp-build` +- [scripts/codegen-index-mjs.mts](scripts/codegen-index-mjs.mts): generates `index.mjs` from `index.d.ts` +- [scripts/zig-build-prebuilds.mts](scripts/zig-build-prebuilds.mts): builds and publishes prebuilds +- [test/register-unsafe-pointer-tests.js](test/register-unsafe-pointer-tests.js): shared runtime tests +- [dist-test/index.test.js](dist-test/index.test.js): packed-tarball verification test +- [.github/workflows/ci.yml](.github/workflows/ci.yml): CI and publish workflow + +## Commands + +- Install deps: `bun install` +- Format check: `bun run fmt --check` +- Typecheck: `bun run typecheck` +- Rebuild local addon from source: `bun run rebuild` +- Run tests: `bun run test` +- Generate JS wrapper: `bun run build:js` +- Build native prebuilds: `bun run build:c` +- Full build, all platforms: `bun run build` + +## Generated Files + +- `index.d.ts` is the source of truth for the JS-facing API. +- `index.mjs` is generated from `index.d.ts`. Do not hand-edit it. +- `bun run build:js` must be stable. Running it should not introduce formatter drift. + +## Native/API Sync Rules + +- If the public API changes, update all of: + - [src/binding.c](src/binding.c) + - [test/register-unsafe-pointer-tests.js](test/register-unsafe-pointer-tests.js) + - [README.md](README.md) + - generated [index.mjs](index.mjs), via `bun run build:js` +- Keep the native implementation simple and direct. Avoid duplicating logic, share with helper functions. +- The addon is plain C with Node-API. Keep it small and dependency-free. +- `binding.gyp` exists for source builds. Do not remove the `node-gyp` fallback. + +## Prebuilds + +- Supported prebuild targets are: + - `darwin-x64` + - `darwin-arm64` + - `linux-x64-glibc` + - `linux-arm64-glibc` + - `linux-x64-musl` + - `linux-arm64-musl` + - `win32-x64` + - `win32-arm64` +- Linux filenames include the libc tag. +- Darwin and Windows filenames do not. +- Windows targets must be built in separate `zig-build` batches to avoid `node.lib` races. + +## CI + +- CI has two main jobs: + - `build`: format check, typecheck, build, tracked-file cleanliness check, tests, `npm pack` + - `dist-test`: install the packed tarball and test both prebuilt and source-build paths across OS/arch variants +- Pushes to `main` may publish to npm via GitHub OIDC trusted publishing after `dist-test` passes. +- Musl verification runs inside `node:22-alpine` via `docker run` from the Linux runners. +- CI fails if `bun run build` changes tracked files. +- Before pushing, the safe local gate is: + - `bun run fmt --check` + - `bun run typecheck` + - `bun run build` + - `git diff --exit-code` + - `bun run test` + +## PR Flow + +- Use `gh` for PR work. +- Push implementation work to a branch, then open or update a PR with `gh pr create` and `gh pr view`. +- After each push, monitor checks with `gh pr checks --watch`. +- If CI fails, inspect the failing run with `gh pr checks ` and `gh run view --job --log-failed`. +- Keep iterating until the PR is green. +- Once implementation is complete and the remaining work is just burning through CI or test failures, ask the user whether to enable auto-merge. +- Do not enable auto-merge without asking. +- If approved, prefer `gh pr merge --auto --squash`. + +## Docs And Style + +- Keep docs dry and concise. +- Use sentence case. Sentences start with a capital letter and end with punctuation. +- Prefer TypeScript in README examples. +- Author the README API section from [index.d.ts](index.d.ts). +- Do not soften the safety language. This package can crash the process, corrupt memory, or enable attacker-controlled execution. +- Preserve existing naming and packaging conventions unless there is a concrete reason to change them. diff --git a/src/binding.c b/src/binding.c index 668d94b..8835278 100644 --- a/src/binding.c +++ b/src/binding.c @@ -6,6 +6,8 @@ #define MAX_SAFE_INTEGER_AS_DOUBLE 9007199254740991.0 #define MAX_SAFE_INTEGER_AS_UINT64 9007199254740991ULL +#define MAX_SAFE_BYTE_LENGTH \ + (SIZE_MAX > MAX_SAFE_INTEGER_AS_UINT64 ? MAX_SAFE_INTEGER_AS_UINT64 : SIZE_MAX) static void nop_finalize(napi_env env, void* finalize_data, void* finalize_hint) { (void)env; @@ -214,43 +216,73 @@ static bool create_size_t_number(napi_env env, size_t value, napi_value* result) return true; } -static bool get_c_string_byte_length( +static bool count_non_null_bytes( napi_env env, const uint8_t* data, - bool has_byte_length, - int64_t requested_byte_length, - size_t* result + bool has_limit, + uint64_t limit, + bool* found_null, + uint64_t* result ) { - if (!has_byte_length) { - size_t count = 0; + uint64_t count = 0; - while (data[count] != 0) { - if (count == SIZE_MAX) { - throw_range_error(env, "ERR_OUT_OF_RANGE", "string length exceeds supported range"); - return false; - } + while (true) { + if (has_limit && count == limit) { + *found_null = false; + *result = count; + return true; + } - count++; + if (data[count] == 0) { + *found_null = true; + *result = count; + return true; } - *result = count; - return true; + if (count == MAX_SAFE_BYTE_LENGTH) { + throw_range_error(env, "ERR_OUT_OF_RANGE", "byte count exceeds safe integer range"); + return false; + } + + count++; } +} + +static bool get_c_string_byte_length( + napi_env env, + const uint8_t* data, + bool has_byte_length, + int64_t requested_byte_length, + size_t* result +) { + bool has_limit = false; + uint64_t limit = 0; + bool found_null = false; + uint64_t count = 0; - if (requested_byte_length >= 0) { + if (has_byte_length && requested_byte_length >= 0) { *result = (size_t)requested_byte_length; return true; } - size_t limit = (size_t)(-requested_byte_length); - for (size_t count = 0; count < limit; count++) { - if (data[count] == 0) { - *result = count; - return true; - } + if (has_byte_length) { + has_limit = true; + limit = (uint64_t)(-requested_byte_length); + } + + if (!count_non_null_bytes( + env, + data, + has_limit, + limit, + &found_null, + &count + )) { + return false; } - *result = limit; + (void)found_null; + *result = (size_t)count; return true; } @@ -467,6 +499,8 @@ static napi_value unsafeCountNonNullBytes(napi_env env, napi_callback_info info) int64_t max_bytes = 0; napi_value result; const uint8_t* data = NULL; + bool found_null = false; + uint64_t count = 0; if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok) { napi_throw_error(env, NULL, "Failed to read arguments"); @@ -495,35 +529,18 @@ static napi_value unsafeCountNonNullBytes(napi_env env, napi_callback_info info) data = (const uint8_t*)(uintptr_t)pointer; - if (max_bytes < 0) { - uint64_t count = 0; - - while (data[count] != 0) { - if (count == MAX_SAFE_INTEGER_AS_UINT64) { - return throw_range_error(env, "ERR_OUT_OF_RANGE", "count exceeds number range"); - } - - count++; - } - - if (!create_int64_result(env, (int64_t)count, &result)) { - return NULL; - } - - return result; - } - - for (int64_t count = 0; count < max_bytes; count++) { - if (data[count] == 0) { - if (!create_int64_result(env, count, &result)) { - return NULL; - } - - return result; - } + if (!count_non_null_bytes( + env, + data, + max_bytes >= 0, + max_bytes >= 0 ? (uint64_t)max_bytes : 0, + &found_null, + &count + )) { + return NULL; } - if (!create_int64_result(env, -1, &result)) { + if (!create_int64_result(env, found_null ? (int64_t)count : -1, &result)) { return NULL; }