Skip to content

Bun.Archive: support per-entry file modes when creating archives - #33213

Open
robobun wants to merge 6 commits into
mainfrom
farm/d881004b/archive-entry-mode
Open

robobun wants to merge 6 commits into
mainfrom
farm/d881004b/archive-entry-mode

Conversation

@robobun

@robobun robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What

Bun.Archive hardcoded 0o644 for every entry when creating a tarball (entry_ref.set_perm(0o644) in build_tarball_from_object), so there was no way to mark a file executable. That made it unusable for building npm tarballs that ship precompiled binaries (esbuild-style platform packages), where the binary must carry 0o755 inside the tarball.

Closes #33212.

API

Entry values may now be a { data, mode } descriptor in addition to the existing string / Blob / TypedArray / ArrayBuffer forms:

new Bun.Archive({
  "bin/mytool": { data: bytes, mode: 0o755 },
  "README.md": "hello",
});
  • mode is optional; missing/undefined falls back to 0o644.
  • mode is masked to 0o777, matching the bits the extraction path already preserves (so a full fs.stat() mode like 0o100755 resolves to 0o755).
  • Non-integer, negative, NaN, Infinity, and non-number modes throw a clear TypeError; a descriptor missing data throws.
  • Both new Bun.Archive({...}) and Bun.Archive.write(path, {...}) go through the same build path, so both gain the feature.

Blob / ArrayBuffer / TypedArray are matched before the descriptor branch because they are objects too. Plain-object entry values were never part of the typed input (Record<string, BlobPart>), so treating them as descriptors is not a change to any typed usage; extraction already preserved modes, so this only closes the creation-side gap.

Verification

New tests in test/js/bun/archive.test.ts (a per-entry file mode block) verify the stored mode by parsing the tar header bytes (umask-independent, cross-platform) and by extracting on POSIX and checking the owner-exec bit:

  • { data, mode: 0o755 } round-trips to 0o755; plain values stay 0o644.
  • descriptor data accepts string / Blob / Uint8Array / ArrayBuffer and the content round-trips.
  • mode defaults to 0o644 when omitted; high bits mask to 0o777.
  • Bun.Archive.write(...) preserves modes on disk.
  • invalid mode values and a descriptor without data are rejected.
 12 pass  0 fail   (per-entry file mode block, bun bd test)
117 pass  1 skip  0 fail   (full archive.test.ts, bun bd test)

11 of the 12 new tests fail on the released binary (the 12th asserts the 0o644 default, which matches the old hardcoded value); all pass with the change. The bun-types type test passes with the updated ArchiveInput / new ArchiveEntry types.

Archive creation hardcoded 0o644 for every entry, so there was no way to
mark a file executable inside a created tarball. Entry values may now be a
{ data, mode } descriptor in addition to the existing string / Blob /
TypedArray / ArrayBuffer forms:

    new Bun.Archive({
      "bin/mytool": { data: bytes, mode: 0o755 },
      "README.md": "hello",
    });

The mode is masked to 0o777 (matching what the extraction path preserves)
and defaults to 0o644 when the descriptor omits it. Blob/ArrayBuffer/
TypedArray are matched before the descriptor branch since they are objects
too. Both new Bun.Archive({...}) and Bun.Archive.write(path, {...}) share
the same build path, so both gain the feature.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7d756979-9207-4919-8231-ca5dd3a25fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6ab1c and 783a296.

📒 Files selected for processing (4)
  • docs/runtime/archive.mdx
  • packages/bun-types/bun.d.ts
  • src/runtime/api/Archive.rs
  • test/js/bun/archive.test.ts

Walkthrough

This PR adds per-entry Unix file mode support to Bun.Archive when creating archives. Object entries can now use a { data, mode } descriptor to override the default 0o644 permission, validated and masked with 0o777. Rust implementation, TypeScript typings, docs, and tests are updated accordingly.

Changes

Per-entry Archive Mode Support

Layer / File(s) Summary
Rust permission resolution
src/runtime/api/Archive.rs
Adds DEFAULT_ENTRY_PERM (0o644), get_entry_data_and_perm, and permission wiring so { data, mode? } entries resolve a per-file mode and write it into tarball headers.
TypeScript typings
packages/bun-types/bun.d.ts
Adds ArchiveEntry, updates ArchiveInput to Record<string, ArchiveEntry>, and adds a constructor doc example using mode: 0o755.
Runtime documentation
docs/runtime/archive.mdx
Documents the { data, mode } input form, adds a "Setting file modes" section with masking behavior, and updates the ArchiveEntry/ArchiveInput reference types.
Tests
test/js/bun/archive.test.ts
Adds a tar-header mode parser helper and tests for stored modes, defaults, masking, write() preservation, extraction behavior, and invalid inputs.

Related Issues: #33212 – Bun.Archive: support per-entry file modes when creating archives

Suggested labels: runtime, bun.archive, documentation, types

Suggested reviewers: none

🐰

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change to Bun.Archive file modes.
Description check ✅ Passed The description covers the feature, validation behavior, and verification, but it does not use the template's exact section headings.
Linked Issues check ✅ Passed The changes implement per-entry archive modes, defaulting, masking, and validation as requested in #33212.
Out of Scope Changes check ✅ Passed The docs, type updates, runtime changes, and tests all support the archive mode feature and appear in scope.

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

@robobun

robobun commented Jul 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:40 AM PT - Jul 2nd, 2026

❌ @robobun, your commit 783a296 has 4 failures in Build #67822 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33213

That installs a local version of the PR into your bun-33213 executable, so you can run:

bun-33213 --bun

Comment thread packages/bun-types/bun.d.ts Outdated
@mintlify

mintlify Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bun 🟢 Ready View Preview Jul 1, 2026, 11:09 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/runtime/archive.mdx (1)

79-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider documenting invalid-input rejection.

The Rust implementation throws TypeError for a non-numeric/negative/non-integer mode or a descriptor missing data, and this is covered by tests. The docs only cover the happy path (default, override, masking) — a brief note on validation would round out the section.

🤖 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 `@docs/runtime/archive.mdx` around lines 79 - 89, The Bun.Archive docs section
only shows valid `{ data, mode }` usage; add a brief validation note to document
that entry descriptors must include data and that mode must be a non-negative
integer. Mention the behavior in the Bun.Archive constructor example area so
readers know invalid descriptors are rejected with TypeError, including
non-numeric, negative, or non-integer mode values. Keep the note close to the
existing mode masking explanation to cover both accepted and rejected inputs.
🤖 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.

Outside diff comments:
In `@docs/runtime/archive.mdx`:
- Around line 79-89: The Bun.Archive docs section only shows valid `{ data, mode
}` usage; add a brief validation note to document that entry descriptors must
include data and that mode must be a non-negative integer. Mention the behavior
in the Bun.Archive constructor example area so readers know invalid descriptors
are rejected with TypeError, including non-numeric, negative, or non-integer
mode values. Keep the note close to the existing mode masking explanation to
cover both accepted and rejected inputs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f68efeb-25e3-40a3-af6e-fa22d168ce11

📥 Commits

Reviewing files that changed from the base of the PR and between 617883a and a7f6315.

📒 Files selected for processing (1)
  • docs/runtime/archive.mdx

@robobun

robobun commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the docs suggestion in 8e6ab1c: the "Setting file modes" section now notes that a descriptor must include data and that mode must be a non-negative integer, otherwise a TypeError is thrown.

@robobun

robobun commented Jul 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status for a maintainer: the diff is green and ready. The change now uses node::types::mode_from_js (the node:fs mode validator) per the review, and the full test/js/bun/archive.test.ts run passes on the local debug ASAN build (13 per-entry tests + 105 existing, fail-before verified on the released binary).

Every red CI lane so far has been an unrelated flaky test, different ones each run and none touching the changed files:

  • test/bake/dev/production.test.ts (Windows, Bake SSR; recurring flake)
  • test/cli/hot/hot.test.ts (macOS aarch64, hot-reload)
  • test/js/bun/http/serve.test.ts (Windows; Buildkite-flagged flaky)
  • earlier: test/bake/dev-and-prod.test.ts (Windows, SIGKILL)

No archive-related failures appear in any build, and the changed files are only src/runtime/api/Archive.rs, packages/bun-types/bun.d.ts, docs/runtime/archive.mdx, and test/js/bun/archive.test.ts. I've used my one CI re-roll; ready for a maintainer to merge.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Implementation and tests look solid and my earlier docs note was addressed — deferring only because this adds new public Bun.Archive API surface ({ data, mode } descriptor shape, 0o777 masking, strict integer validation), which is a design call a maintainer should sign off on.

Extended reasoning...

Overview

Adds per-entry Unix file mode support to Bun.Archive creation. Entry values may now be a { data, mode? } descriptor in addition to string/Blob/TypedArray/ArrayBuffer. Touches four files: src/runtime/api/Archive.rs (~50 new lines: DEFAULT_ENTRY_PERM, get_entry_data_and_perm, resolve_entry_perm, and one changed line in build_tarball_from_object), packages/bun-types/bun.d.ts (new ArchiveEntry type + updated ArchiveInput), docs/runtime/archive.mdx (bullet, subsection, reference type), and test/js/bun/archive.test.ts (12 new tests + a parseTarModes helper).

The Rust change is careful: Blob/ArrayBuffer are matched before the descriptor branch (they're objects too), mode is validated as a finite non-negative integer via is_number() → as_number() → range/fract checks before the as u32 cast, and the perm is fully resolved before get_entry_data returns a borrowed byte view so no slice is held across the .get(global, "mode") property read. The one behavioral change to previously-untyped input — a plain-object entry value now requires data instead of stringifying to "[object Object]" — is a strict improvement.

Security risks

None identified. The mode is masked to 0o777 (matching what the extraction path already preserves), so setuid/setgid/sticky bits and file-type bits cannot be injected into created archives. Input validation rejects NaN/Infinity/negative/non-integer before the integer cast. No new filesystem or path handling.

Level of scrutiny

Moderate. The native-code change is small and localized to one function, and test coverage is thorough (mode set/default/mask, all four data types round-trip, Archive.write path, extracted exec bit on POSIX, and rejection of five invalid mode shapes plus missing data). But this is new public API surface on Bun.Archive: the descriptor shape, the choice to mask to 0o777 rather than accept setuid/sticky bits, and throwing (vs. coercing) on non-integer modes are design decisions that lock in once shipped. That's the kind of call the repo's review guidance flags for maintainer sign-off rather than bot approval.

Other factors

My earlier inline comment (missing archive.mdx docs) was fully addressed in 617883a/8e6ab1cd and the thread is resolved. No other reviewer comments are outstanding. CI failures (zlib.test.js, test-net-connect-memleak.js, bake/dev/production.test.ts) are in unrelated files with no plausible connection to this diff. The bug-hunting pass found nothing.

@robobun

robobun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Rationale on the three API decisions a maintainer would want to weigh, each kept consistent with existing Bun.Archive behavior:

  • Descriptor shape { data, mode }: matches the shape proposed in Bun.Archive: support per-entry file modes when creating archives #33212 and coexists with the existing string/Blob/TypedArray/ArrayBuffer entry values. Plain objects were never a valid entry value (the old Record<string, BlobPart> type excluded them, and at runtime they stringified to "[object Object]"), so requiring data on an object entry is not a breaking change to any typed usage.
  • Masking mode to 0o777: the extraction path already masks entry perms to 0o777, so storing setuid/setgid/sticky or file-type bits on creation would be dropped on the round trip anyway. Masking keeps create and extract symmetric, and makes a full fs.stat() mode such as 0o100755 resolve to 0o755 instead of erroring.
  • Throwing on an invalid mode (non-number, negative, NaN/Infinity, non-integer) rather than coercing: mirrors how the constructor already validates the gzip level option (rejects non-numbers and out-of-range values). Silently writing a coerced mode is harder to debug than a clear error.

Happy to adjust any of these if different semantics are preferred.

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use validateIntegerRange or even better the existing node:fs mode validator

Replace the hand-rolled mode checks in resolve_entry_perm with
node::types::mode_from_js, the same validator node:fs uses for chmod/mkdir.
This gains node-parity error codes (ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE /
ERR_INVALID_ARG_VALUE) and octal-string support (e.g. mode: "755"), and keeps
the 0o777 masking. The ArchiveEntry mode type widens to number | string to
match.
@robobun

robobun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Done in 783a296: resolve_entry_perm now calls node::types::mode_from_js (the validator node:fs uses for chmod/mkdir) instead of the hand-rolled checks. That gives node-parity error codes (ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE / ERR_INVALID_ARG_VALUE) and octal-string support (mode: "755"), keeping the 0o777 mask. Widened the ArchiveEntry mode type to number | string and updated the tests/docs to match.

@robobun

robobun commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Stale PR review: keep open, rework.

The feature is still wanted. Main still hard-codes entry_ref.set_perm(0o644) (src/runtime/api/Archive.rs:352), #33212 is open, and I found no other open PR that adds per-entry modes. The one review request (use the node:fs mode validator) was met in 783a296. The { data, mode } shape is the one #33212 proposes, so keep the public API.

A plain rebase is not safe:

  • This PR relies on mode_from_js to mask the mode to 0o777. Since a8d0692 (Hardening round 11: input validation, bounds checks, lifetimes #33072), mode_from_js returns the mode without a mask (src/runtime/node/types.rs:1419). libarchive writes mode & 07777 into the ustar header, so the setuid, setgid and sticky bits would reach the archive. Add an explicit & 0o777 in resolve_entry_perm.
  • The mask test does not catch this, because its parseTarModes helper applies & 0o777 to the header value. Compare the raw header mode.
  • The branch conflicts with main in src/runtime/api/Archive.rs. Port the change onto main's get_entry_data.

After that, the new public API needs a maintainer's decision.

This branch was successfully deployed

1 active deployment
staging - docs — 783a2962 Deployed Jul 2, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun.Archive: support per-entry file modes when creating archives

2 participants