Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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 <pr> --watch`.
- If CI fails, inspect the failing run with `gh pr checks <pr>` and `gh run view <run-id> --job <job-id> --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.
115 changes: 66 additions & 49 deletions src/binding.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}

Expand Down