Skip to content
Open
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
32 changes: 21 additions & 11 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use bun_event_loop::{self, AnyEventLoop, EventLoopHandle};
use bun_http as http;
use bun_ini as ini;
use bun_paths::resolve_path::{self, PosixToWinNormalizer, platform};
use bun_paths::{DELIMITER, PathBuffer, SEP, SEP_STR};
use bun_paths::{DELIMITER, MAX_PATH_BYTES, PathBuffer, SEP, SEP_STR};
use bun_semver as Semver;
use bun_sys::{self, Fd};
use bun_threading::{ThreadPool, UnboundedQueue, thread_pool};
Expand Down Expand Up @@ -1434,6 +1434,22 @@ pub(crate) fn get() -> *mut PackageManager {
// init
// ──────────────────────────────────────────────────────────────────────────

/// `<dir>/.npmrc` for the user-level `.npmrc` candidates in [`init`]. `dir` is
/// `$XDG_CONFIG_HOME` or `$HOME`, so it can be longer than `buf`; a path that
/// does not fit could not be opened either, and `None` makes the caller treat
/// it like a missing file.
fn user_npmrc_path<'a>(dir: &[u8], buf: &'a mut PathBuffer) -> Option<&'a ZStr> {
// The last byte is reserved for the NUL terminator.
let len = resolve_path::join_abs_string_buf_checked::<platform::Auto>(
dir,
&mut buf[..MAX_PATH_BYTES - 1],
&[b".npmrc"],
)?
.len();
buf[len] = 0;
Some(ZStr::from_buf(&buf[..], len))
}

/// Returns `&'static mut PackageManager` — the process-singleton (held in
/// `holder::RAW_PTR`) is leaked for the process lifetime and `init()` is called
/// exactly once on the single CLI dispatch thread. Every
Expand Down Expand Up @@ -1877,24 +1893,18 @@ pub fn init(
let npmrc_local = ZBox::from_bytes(b".npmrc");

let mut buf = PathBuffer::uninit();
let parts = [b"./.npmrc" as &[u8]];

// npm reads `$HOME/.npmrc` and ignores XDG_CONFIG_HOME; keep
// `$XDG_CONFIG_HOME/.npmrc` only when that file actually exists.
let mut global_len: usize = 0;
if let Some(xdg_dir) = bun_core::env_var::XDG_CONFIG_HOME.get_not_empty() {
let p =
resolve_path::join_abs_string_buf_z::<platform::Auto>(xdg_dir, &mut buf, &parts);
if bun_sys::exists_z(p) {
global_len = p.len();
}
global_len = user_npmrc_path(xdg_dir, &mut buf)
.filter(|p| bun_sys::exists_z(p))
.map_or(0, ZStr::len);
}
if global_len == 0 {
if let Some(home_dir) = bun_core::env_var::HOME.get_not_empty() {
global_len = resolve_path::join_abs_string_buf_z::<platform::Auto>(
home_dir, &mut buf, &parts,
)
.len();
global_len = user_npmrc_path(home_dir, &mut buf).map_or(0, ZStr::len);
}
}

Expand Down
50 changes: 49 additions & 1 deletion test/cli/install/npmrc.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { write } from "bun";
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
import { mkdirSync, writeFileSync } from "fs";
import { rm } from "fs/promises";
import { VerdaccioRegistry, bunExe, bunEnv as env, tempDir } from "harness";
import { VerdaccioRegistry, bunExe, bunEnv as env, isLinux, isWindows, tempDir } from "harness";
import { join } from "path";
const { iniInternals } = require("bun:internal-for-testing");
const { loadNpmrc } = iniInternals;
Expand Down Expand Up @@ -222,6 +223,53 @@ registry = http://localhost:${registry.port}/
const result = await publishDryRun(String(dir), { XDG_CONFIG_HOME: "" });
expect(result).toEqual(usesRegistry(1));
});

// The candidate paths are built in a buffer of MAX_PATH_BYTES (4096 on Linux, 1024 on the
// other POSIX platforms; on Windows it is larger than any environment variable can be), so
// the longest directory whose "/.npmrc" and NUL terminator still fit is MAX_PATH_BYTES - 8
// bytes. A longer directory cannot contain an openable .npmrc and counts as having none.
describe.skipIf(isWindows)("$HOME longer than the path buffer", () => {
const MAX_PATH_BYTES = isLinux ? 4096 : 1024;
const LONGEST_HOME = MAX_PATH_BYTES - 8;

// xdg/ has no .npmrc, so the lookup falls through to $HOME. The global bunfig.toml lookup
// starts at $XDG_CONFIG_HOME too; the bunfig there keeps it away from the oversized $HOME,
// which is not what is under test here.
const xdg = { "xdg/.bunfig.toml": "" };
const envWith = (dir: string, home: string) => ({ XDG_CONFIG_HOME: join(dir, "xdg"), HOME: home });

// An absolute path of exactly `length` bytes. Nothing exists there.
const missingHome = (length: number) => "/" + Buffer.alloc(length - 1, "a").toString();

it.concurrent("reads $HOME/.npmrc from the longest $HOME that fits", async () => {
using dir = tempDir("npmrc-home-longest", { ...pkg, ...xdg });
// Pad `<dir>/home/` out to exactly LONGEST_HOME bytes with nested directories of 200
// bytes each (NAME_MAX is 255); the last byte is never a separator.
const prefix = join(String(dir), "home") + "/";
const padding = Buffer.alloc(LONGEST_HOME - Buffer.byteLength(prefix), "a");
for (let i = 200; i < padding.length - 1; i += 201) padding[i] = "/".charCodeAt(0);
const home = prefix + padding.toString();
expect(Buffer.byteLength(home)).toBe(LONGEST_HOME);
mkdirSync(home, { recursive: true });
writeFileSync(join(home, ".npmrc"), npmrc(1));

const result = await publishDryRun(String(dir), envWith(String(dir), home));
expect(result).toEqual(usesRegistry(1));
});

// The project .npmrc is the only one left, and shows the command still ran normally.
it.concurrent("skips a $HOME one byte longer than fits", async () => {
using dir = tempDir("npmrc-home-one-too-long", { ...pkg, ...xdg, "pkg/.npmrc": npmrc(3) });
const result = await publishDryRun(String(dir), envWith(String(dir), missingHome(LONGEST_HOME + 1)));
expect(result).toEqual(usesRegistry(3));
});

it.concurrent("skips a $HOME longer than the whole buffer", async () => {
using dir = tempDir("npmrc-home-too-long", { ...pkg, ...xdg, "pkg/.npmrc": npmrc(3) });
const result = await publishDryRun(String(dir), envWith(String(dir), missingHome(MAX_PATH_BYTES + 1000)));
expect(result).toEqual(usesRegistry(3));
});
});
});

it("package config overrides home config", async () => {
Expand Down
Loading