Skip to content

fix(js-exec): honor encoding arg in Buffer.from / toString / write / byteLength#1

Open
Hazzng wants to merge 6 commits into
mainfrom
fix/js-encoding
Open

fix(js-exec): honor encoding arg in Buffer.from / toString / write / byteLength#1
Hazzng wants to merge 6 commits into
mainfrom
fix/js-encoding

Conversation

@Hazzng

@Hazzng Hazzng commented May 16, 2026

Copy link
Copy Markdown
Owner

Problem

The Buffer shim inside the QuickJS-backed js-exec runtime ignored the encoding argument on every encoding-aware method. All calls silently fell through to UTF-8, so base64, hex, latin1, ascii, and utf16le were broken. Encode + decode tests masked this because both sides were no-ops.

Smoking gun: Buffer.from([0xff,0xfe,0x00]).toString('base64') returned "" instead of "//4A".

Failing cases (before fix)

Run inside js-exec (QuickJS), compared against real Node:

Expression Before (broken) After (Node-correct)
Buffer.from([0xff,0xfe,0x00]).toString('base64') "" "//4A"
Buffer.from([0xff,0xfe,0x00]).toString('hex') "" "fffe00"
Buffer.from('//4A', 'base64') <Buffer 2f 2f 34 41> (raw utf8 of the string) <Buffer ff fe 00>
Buffer.from('68656c6c6f', 'hex').toString() "68656c6c6f" "hello"
Buffer.byteLength('aGVsbG8=', 'base64') 8 (utf8 length of input string) 5
Buffer.from('hello', 'latin1').toString('latin1') worked only because both sides no-op'd matches Node, also works for non-ASCII (0x800xff)

Root cause: every entry point built the byte array via _utf8Encode(str) regardless of the encoding argument; toString symmetrically decoded as UTF-8.

Solution

Added pure-JS encode/decode helpers inside BUFFER_MODULE_SOURCE (the IIFE that runs in QuickJS where TextEncoder/atob/btoa are unavailable), and wired them into the four entry points:

Method What changed
Buffer.from(str, encoding) dispatches to the right encoder
buf.toString(encoding) dispatches to the right decoder
Buffer.byteLength(str, encoding) returns decoded byte count, not UTF-8 length
buf.write(str, offset, length, encoding) full Node overload signature

Encodings supported: utf8, base64, base64url, hex, latin1/binary, ascii, utf16le/ucs2. Node's lenient parsing is matched (base64 ignores whitespace + missing padding; hex stops at first invalid char). Unknown encodings throw TypeError.

After fix — verified against node -e

$ node ./dist/cli/just-bash.js -c 'js-exec <<<"
  const b = Buffer.from([0xff,0xfe,0x00]);
  console.log(b.toString(\"base64\"));      // //4A
  console.log(b.toString(\"hex\"));         // fffe00
  console.log(Buffer.from(\"//4A\",\"base64\")); // <Buffer ff fe 00>
  console.log(Buffer.from(\"68656c6c6f\",\"hex\").toString()); // hello
  console.log(Buffer.byteLength(\"aGVsbG8=\",\"base64\"));     // 5
"' --root .
//4A
fffe00
<Buffer ff fe 00>
hello
5

Byte-for-byte match with node -e running the same snippet.

Tests

New file js-exec.buffer-encoding.test.ts — 29 cases:

  • Encode-only (assert exact output constant, not round-trip)
  • Decode-only
  • Binary bytes (0xff, 0xfe, 0x00) — these were the broken path
  • Alias names (binary, ucs2, utf-8)
  • Forgiving inputs (missing base64 padding, odd-length hex, invalid hex char)
  • Unknown encoding throws

Existing js-exec.node-compat.test.ts (120 tests) stays green — no regressions.

Verification

pnpm exec vitest run --config vitest.wasm.config.ts src/commands/js-exec/js-exec.buffer-encoding.test.ts
# 29/29 passed

pnpm exec vitest run --config vitest.wasm.config.ts src/commands/js-exec/js-exec.node-compat.test.ts
# 120/120 passed

pnpm typecheck && pnpm lint:fix && pnpm knip
# all clean

Manual spot-checks against node -e confirmed byte-for-byte match for aGVsbG8=, hello, //4A, 68656c6c6f, ff00ab, and byteLength = 5.

Summary by CodeRabbit

  • New Features

    • Expanded Buffer support for base64/base64url, hex, latin1, ascii, utf8 and utf16le, plus encoding aliases and encoding-aware byte-length, read and write operations.
  • Bug Fixes

    • Improved encoding validation (unknown encodings now throw), corrected ASCII/latin1 handling, fixed slicing/clamping and write bounds, and other edge-case behaviors.
  • Tests

    • Added comprehensive tests covering encode/decode, byte-length, write results, aliasing, error cases and regression constants.

Review Change Stack

…byteLength

Add pure-JS encoders/decoders for base64, base64url, hex, latin1/binary,
ascii, and utf16le inside BUFFER_MODULE_SOURCE. Dispatch on a normalized
encoding string in Buffer.from, Buffer.prototype.toString,
Buffer.prototype.write, and Buffer.byteLength so the QuickJS shim matches
real Node.js Buffer semantics for every encoding Node supports.

- _normEnc: normalizes encoding aliases (binary→latin1, ucs2→utf16le, etc.)
- _hexEncode/_hexDecode: lenient hex (stops at first invalid char)
- _latin1Encode/_latin1Decode, _asciiEncode/_asciiDecode
- _utf16leEncode/_utf16leDecode
- _b64Encode/_b64UrlEncode/_b64Decode: forgiving base64 (ignores whitespace,
  tolerates missing padding, supports base64url alphabet)

Adds js-exec.buffer-encoding.test.ts with 29 cases covering encode-only,
decode-only, round-trips, binary bytes (0xff/0xfe/0x00), alias names, and
forgiving inputs. Adds a guardrail comment above _normEnc explaining why
round-trip tests alone would mask a broken encoding implementation.
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Hazzng, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 48 minutes. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a2f3c525-7bdb-49f0-8c0d-ae52d97be6a6

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce7a69 and 52bc964.

📒 Files selected for processing (2)
  • packages/just-bash/src/commands/js-exec/js-exec.buffer-encoding.test.ts
  • packages/just-bash/src/commands/js-exec/module-shims.ts
📝 Walkthrough

Walkthrough

Adds multi-encoding support to the QuickJS Buffer shim: encoding normalization and helpers, encoding-aware Buffer.from, Buffer.byteLength, Buffer.prototype.toString, and Buffer.prototype.write, plus a comprehensive Vitest suite and a changeset note.

Changes

Multi-encoding Buffer support

Layer / File(s) Summary
Encoding infrastructure and helpers
packages/just-bash/src/commands/js-exec/module-shims.ts
Adds encoding name normalization, validation, and encode/decode helpers for utf16le, latin1, ascii, hex, and base64/base64url.
Buffer.from string decoding
packages/just-bash/src/commands/js-exec/module-shims.ts
Updates Buffer.from(data, encoding) so string inputs decode using normalized encodings and unknown encodings throw TypeError; preserves non-string construction paths.
Buffer.byteLength and toString
packages/just-bash/src/commands/js-exec/module-shims.ts
Adds Buffer.byteLength(value, encoding) with encoding-aware sizing and expands Buffer.prototype.toString(encoding, start, end) for multiple encodings with slicing.
Buffer.prototype.write implementation
packages/just-bash/src/commands/js-exec/module-shims.ts
Replaces Buffer.prototype.write(...) with an encoding-aware implementation that validates/clamps bounds, converts strings to bytes for supported encodings, writes into the backing store, and returns the byte count.
Comprehensive encoding test suite
packages/just-bash/src/commands/js-exec/js-exec.buffer-encoding.test.ts
Adds Vitest cases exercising Buffer.toString (encode), Buffer.from(..., encoding) (decode with edge cases), Buffer.byteLength & write cross-API behavior, regression constants, unknown-encoding errors, encoding aliases, and Node-compat edge cases.
Changeset metadata
.changeset/clever-pumas-drum.md
Adds a patch changeset note describing Buffer shim fixes and behavioral corrections.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Bytes and bunnies, hopping through code,
Base64 and hex on a carrot-shaped road,
Aliases snug, errors caught tight,
Buffers now sing in many encodings tonight,
A tiny hop for tests, a giant nibble of delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and precisely summarizes the main change: adding encoding argument support to four core Buffer methods (from, toString, write, byteLength) that were previously ignoring this parameter.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/js-encoding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Hazzng

Hazzng commented May 16, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

@greptile-apps

greptile-apps Bot commented May 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the Buffer shim inside the QuickJS-backed js-exec runtime, which previously silently fell through to UTF-8 for every encoding-aware method. Pure-JS encode/decode helpers (base64, base64url, hex, latin1, ascii, utf16le) are wired into Buffer.from, toString, byteLength, and write, with Node-compatible lenient parsing.

  • module-shims.ts: ~140 lines of new encoding helpers added to BUFFER_MODULE_SOURCE; all four entry points updated to dispatch on a normalised encoding name.
  • js-exec.buffer-encoding.test.ts: 29 unit tests asserting exact output constants (not round-trips) across encode-only, decode-only, binary-byte, alias, forgiving-input, and unknown-encoding paths; existing 120-test suite stays green.

Confidence Score: 4/5

The encoding logic is correct across all seven encodings; the only gap is the absence of formal comparison tests required by team guidelines.

The implementation correctly handles all encoding types, including edge cases like base64 whitespace stripping, odd-length hex truncation, and surrogate-pair utf16le encoding. The _b64UrlEncode template-literal double-backslash escaping is intentional and correct. The one gap is that AGENTS.md requires comparison tests for major command functionality, and none are present.

No files have correctness issues; js-exec.buffer-encoding.test.ts is the only file that could benefit from a companion comparison test.

Important Files Changed

Filename Overview
packages/just-bash/src/commands/js-exec/module-shims.ts Adds ~140 lines of pure-JS encoding helpers and wires them into Buffer.from, toString, byteLength, and write; the implementation is logically correct for all covered encodings.
packages/just-bash/src/commands/js-exec/js-exec.buffer-encoding.test.ts 29 unit tests with asserted constants rather than round-trips; covers all new encodings, aliases, forgiving inputs, and unknown-encoding errors; no comparison tests present.

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(js-exec): honor encoding arg in Buff..." | Re-trigger Greptile

Comment on lines +1 to +10
import { describe, expect, it } from "vitest";
import { Bash } from "../../Bash.js";

describe("buffer encoding", () => {
// ─── Encode side ─────────────────────────────────────────────────

it("Buffer.from(str).toString('base64')", async () => {
const env = new Bash({ javascript: true });
const result = await env.exec(
`js-exec -c "console.log(Buffer.from('hello').toString('base64'))"`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing comparison tests for major feature

AGENTS.md requires comparison tests for major command functionality alongside unit tests. Fixing all Buffer encoding methods (from, toString, byteLength, write) is a significant behavioral change that warrants formal comparison tests against Node.js rather than ad-hoc node -e spot-checks. Without a comparison-tests file here, future regressions won't be caught by the comparison suite, and the manual spot-checks (documented only in the PR description) won't persist as executable evidence of correctness.

Context Used: packages/just-bash/AGENTS.md (source)

Fix in Claude Code

@greptile-apps

greptile-apps Bot commented May 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the Buffer shim inside the QuickJS-backed js-exec runtime, which previously ignored the encoding argument on all encoding-aware methods and silently fell through to UTF-8. Pure-JS codec helpers (_hexEncode/_hexDecode, _latin1*, _ascii*, _utf16le*, _b64*) are added inside the IIFE and wired into Buffer.from, buf.toString, Buffer.byteLength, and buf.write.

  • All eight canonical Node.js encodings and their aliases (binary, ucs2, ucs-2, utf-8, utf-16le) are handled; unknown encodings throw TypeError matching Node.js behavior.
  • The new test file adds 29 integration cases that assert exact encoded constants rather than round-trips, which correctly catches the class of bug being fixed.
  • _normEnc(null) currently returns 'utf8' rather than undefined, and Buffer.byteLength for base64/base64url calls _b64Decode to count bytes where an O(1) formula suffices.

Confidence Score: 4/5

Safe to merge; encoding logic is correct and well-tested with only minor deviations from Node.js edge-case behaviour.

The codec implementations are correct end-to-end — binary-byte round-trips, alias resolution, lenient base64 padding/whitespace, and hex stop-on-invalid all match Node.js. The two noted issues are narrow: null passed as encoding silently defaults to UTF-8 instead of throwing, and Buffer.byteLength for base64 allocates an unnecessary array. Neither affects the fixed functionality or existing tests.

The codec helpers in module-shims.ts — specifically _normEnc around the null branch and the Buffer.byteLength base64 path.

Important Files Changed

Filename Overview
packages/just-bash/src/commands/js-exec/module-shims.ts Adds ~140 lines of pure-JS encode/decode helpers and wires them into all four encoding-aware Buffer methods; logic is correct with two minor deviations: null encoding silently uses UTF-8, and Buffer.byteLength for base64 allocates unnecessarily.
packages/just-bash/src/commands/js-exec/js-exec.buffer-encoding.test.ts 29 integration cases asserting exact stdout constants; covers encode, decode, binary bytes, aliases, lenient inputs, and unknown-encoding error paths.

Fix All in Claude Code

Reviews (2): Last reviewed commit: "fix(js-exec): honor encoding arg in Buff..." | Re-trigger Greptile

Comment thread packages/just-bash/src/commands/js-exec/module-shims.ts
Comment thread packages/just-bash/src/commands/js-exec/module-shims.ts Outdated
Replace _b64Decode(value).length with a length-based formula
(strip non-alphabet + padding, then floor(len * 3 / 4)). Matches
Node's Buffer.byteLength semantics and avoids allocating an array
just to count bytes.
@Hazzng

Hazzng commented May 16, 2026

Copy link
Copy Markdown
Owner Author

Triaged the bot reviews:

Greptile Issue 1 — _normEnc(null) should throw: Invalid. Verified against real Node 22:

> Buffer.from('hi', null).toString()
'hi'
> Buffer.byteLength('hi', null)
2

Node accepts null and treats it as utf8. Our shim matches Node — leaving as-is.

Greptile Issue 2 — Buffer.byteLength should be O(1) for base64: Valid. Applied in 0e9c51b. New formula strips non-alphabet/padding then floor(len * 3 / 4). Matches Node for all five spot-checked inputs (aGVsbG8=, aGVsbG8, YQ==, YWI=, __4A). All 29 buffer-encoding tests stay green.

Greptile review #1 — missing comparison tests: Not applicable. Comparison tests in this repo diff just-bash command output against recorded real-bash fixtures. This change is internal JS Buffer shim behavior inside QuickJS — there is no bash counterpart to compare against. The 29 unit tests assert exact constants (not round-trips) which is the right defense against the bug class being fixed.

CodeRabbit: No actionable comments.

Hazzng added 2 commits May 25, 2026 19:54
…m correctness

- Replace _latin1Encode/_asciiEncode with shared _rawEncode (both truncate to low byte; encode semantics are identical)
- Fix Buffer.from(ArrayBuffer, byteOffset, length) to respect offset and length args
- Fix Buffer.byteLength to throw TypeError for non-string/non-Buffer input instead of returning 0
- Fix Buffer.byteLength base64 regex to correctly handle mid-string '=' as terminator
- Fix Buffer.prototype.toString to clamp negative start to 0, matching Node behavior
- Fix Buffer.prototype.write to throw RangeError for negative/out-of-range offset
@Hazzng
Hazzng force-pushed the fix/js-encoding branch from 624237f to 66cde6d Compare May 25, 2026 10:43
…n, toString negative end

- Buffer.from(ArrayBuffer, offset, length) now shares the backing store as Node does
  (previously the inner Uint8Array was copied through the Buffer constructor).
- Buffer.prototype.write throws RangeError for negative length or length > remaining,
  matching Node's ERR_OUT_OF_RANGE rather than silently clamping.
- Buffer.prototype.toString clamps negative end to 0 (Node behavior), so
  Buffer.from("abc").toString("utf8", 0, -1) returns "" instead of "ab".

Adds edge-case tests covering all three behaviors.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/clever-pumas-drum.md:
- Line 5: Add a top-level H1 heading to the changeset file to satisfy
markdownlint MD041: open .changeset/clever-pumas-drum.md and insert a
single-line H1 (e.g., "# js-exec: fix Buffer shim correctness") at the very top
before the existing body so the file begins with an H1 heading rather than plain
text; ensure the heading accurately reflects the existing diff summary and
preserves the rest of the file content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 329150a9-55ce-469d-9456-7588f7dc9cfd

📥 Commits

Reviewing files that changed from the base of the PR and between 624237f and 9ce7a69.

📒 Files selected for processing (3)
  • .changeset/clever-pumas-drum.md
  • packages/just-bash/src/commands/js-exec/js-exec.buffer-encoding.test.ts
  • packages/just-bash/src/commands/js-exec/module-shims.ts

"just-bash": patch
---

js-exec: fix Buffer shim correctness — ascii encode now uses & 0xff (not & 0x7f), consolidate latin1/ascii into shared \_rawEncode, fix Buffer.from(ArrayBuffer, offset, length), throw on invalid byteLength input, clamp negative toString start, throw RangeError for out-of-range write offset

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a top-level heading to satisfy markdownlint MD041.

This file currently starts body content without an H1, which triggers the reported lint warning.

Suggested patch
 ---
 "just-bash": patch
 ---
 
+# js-exec Buffer shim correctness fixes
+
 js-exec: fix Buffer shim correctness — ascii encode now uses & 0xff (not & 0x7f), consolidate latin1/ascii into shared \_rawEncode, fix Buffer.from(ArrayBuffer, offset, length), throw on invalid byteLength input, clamp negative toString start, throw RangeError for out-of-range write offset
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
js-exec: fix Buffer shim correctness — ascii encode now uses & 0xff (not & 0x7f), consolidate latin1/ascii into shared \_rawEncode, fix Buffer.from(ArrayBuffer, offset, length), throw on invalid byteLength input, clamp negative toString start, throw RangeError for out-of-range write offset
---
"just-bash": patch
---
# js-exec Buffer shim correctness fixes
js-exec: fix Buffer shim correctness — ascii encode now uses & 0xff (not & 0x7f), consolidate latin1/ascii into shared \_rawEncode, fix Buffer.from(ArrayBuffer, offset, length), throw on invalid byteLength input, clamp negative toString start, throw RangeError for out-of-range write offset
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/clever-pumas-drum.md at line 5, Add a top-level H1 heading to the
changeset file to satisfy markdownlint MD041: open
.changeset/clever-pumas-drum.md and insert a single-line H1 (e.g., "# js-exec:
fix Buffer shim correctness") at the very top before the existing body so the
file begins with an H1 heading rather than plain text; ensure the heading
accurately reflects the existing diff summary and preserves the rest of the file
content.

…writes to remaining

Per vercel bot review: Node validates the `length` arg against the buffer's
total size, not the remaining space after `offset`, and clamps actual writes
to the remaining space. The previous fix validated against remaining, which
incorrectly threw for cases like `Buffer.alloc(5).write('abcde', 3, 5)` that
Node accepts (writing 2 bytes).

Adds a regression test for the offset+length clamping case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant