Skip to content
Closed
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
101 changes: 43 additions & 58 deletions src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ const EXPECTED_VERSION: u32 = 25;
/// `is_stale`), so shrinking this does not weaken staleness detection.
const MINIMUM_CACHE_SIZE: usize = 4 * 1024;

const CACHE_FILE_SUFFIX: &[u8] = if bun_core::env::IS_DEBUG {
b".debug.pile"
} else {
b".pile"
};
/// Hex `input_hash` + suffix, as written by `write_cache_filename`.
const CACHE_FILE_NAME_LEN: usize = size_of::<u64>() * 2 + CACHE_FILE_SUFFIX.len();
/// Separator + file name + NUL, appended to the directory by `get_cache_file_path`.
const CACHE_FILE_NAME_RESERVE: usize = 1 + CACHE_FILE_NAME_LEN + 1;

// When making parser changes, it gets extremely confusing.
#[cfg(bun_debug)]
static BUN_DEBUG_RESTORE_FROM_CACHE: AtomicBool = AtomicBool::new(false);
Expand Down Expand Up @@ -641,20 +651,14 @@ impl RuntimeTranspilerCache {
buf: &mut [u8],
input_hash: u64,
) -> crate::CrateResult<usize> {
// Hex-encode the 8 native-endian bytes of `input_hash`.
let bytes = input_hash.to_ne_bytes();
let suffix: &[u8] = if bun_core::env::IS_DEBUG {
b".debug.pile"
} else {
b".pile"
};
let needed = bytes.len() * 2 + suffix.len();
if buf.len() < needed {
if buf.len() < CACHE_FILE_NAME_LEN {
return Err(crate::CrateError::Sys(bun_errno::SystemErrno::ENOSPC));
}
// Hex-encode the 8 native-endian bytes of `input_hash`.
let bytes = input_hash.to_ne_bytes();
let i = bun_core::fmt::bytes_to_hex_lower(&bytes, &mut buf[..bytes.len() * 2]);
buf[i..i + suffix.len()].copy_from_slice(suffix);
Ok(needed)
buf[i..i + CACHE_FILE_SUFFIX.len()].copy_from_slice(CACHE_FILE_SUFFIX);
Ok(CACHE_FILE_NAME_LEN)
}

pub(crate) fn get_cache_file_path(
Expand All @@ -672,8 +676,8 @@ impl RuntimeTranspilerCache {
Ok(ZStr::from_buf(&buf[..], total))
}

/// Writes the resolved cache directory into `buf` (NUL-terminated) and
/// returns its byte length. Returns 0 to mean "cache disabled".
/// Writes the resolved cache directory into `buf` (NUL-terminated) and returns
/// its byte length, or 0 when the cache is disabled or the directory does not fit.
Comment thread
robobun marked this conversation as resolved.
fn really_get_cache_dir(buf: &mut PathBuffer) -> usize {
#[cfg(bun_debug)]
{
Expand All @@ -685,59 +689,40 @@ impl RuntimeTranspilerCache {
);
}

let dir_buf = &mut buf[..MAX_PATH_BYTES - CACHE_FILE_NAME_RESERVE];

if let Some(dir) = env_var::BUN_RUNTIME_TRANSPILER_CACHE_PATH.get() {
if dir.is_empty() || (dir.len() == 1 && dir[0] == b'0') {
if dir.is_empty() || (dir.len() == 1 && dir[0] == b'0') || dir.len() > dir_buf.len() {
return 0;
}

let len = dir.len().min(MAX_PATH_BYTES - 1);
buf[0..len].copy_from_slice(&dir[0..len]);
buf[len] = 0;
return len;
dir_buf[..dir.len()].copy_from_slice(dir);
buf[dir.len()] = 0;
return dir.len();
}

// The inline `bun_resolver::fs::FileSystem` surface only exposes
// `abs_buf` (no NUL-terminating `_z` variant), so go straight to the
// underlying joiner with the same `top_level_dir` + `Loose` platform
// that `absBufZ` used.
let top = FileSystem::instance().top_level_dir;

if let Some(dir) = env_var::XDG_CACHE_HOME.get() {
let parts: &[&[u8]] = &[dir, b"bun", b"@t@"];
return path_handler::join_abs_string_buf_z::<platform::Loose>(
top,
&mut buf[..],
parts,
)
.len();
}

#[cfg(target_os = "macos")]
{
// On a mac, default to ~/Library/Caches/bun/*
// This is different than ~/.bun/install/cache, and not configurable by the user.
if let Some(home) = env_var::HOME.get() {
let parts: &[&[u8]] = &[home, b"Library/", b"Caches/", b"bun", b"@t@"];
return path_handler::join_abs_string_buf_z::<platform::Loose>(
top,
&mut buf[..],
parts,
)
.len();
let parts: &[&[u8]] = if let Some(dir) = env_var::XDG_CACHE_HOME.get() {
&[dir, b"bun", b"@t@"]
} else if let Some(home) = env_var::HOME.get() {
if cfg!(target_os = "macos") {
&[home, b"Library/", b"Caches/", b"bun", b"@t@"]
} else {
&[home, b".bun", b"install", b"cache", b"@t@"]
}
}

if let Some(dir) = env_var::HOME.get() {
let parts: &[&[u8]] = &[dir, b".bun", b"install", b"cache", b"@t@"];
return path_handler::join_abs_string_buf_z::<platform::Loose>(
top,
&mut buf[..],
parts,
)
.len();
}
} else {
return 0;
};

0
// `FileSystem::abs_buf` has no size-checked variant, so call the joiner it wraps.
let top = FileSystem::instance().top_level_dir;
let Some(dir) =
path_handler::join_abs_string_buf_checked::<platform::Loose>(top, dir_buf, parts)
else {
return 0;
};
let len = dir.len();
buf[len] = 0;
len
}

// Only do this at most once per-thread.
Expand Down
28 changes: 28 additions & 0 deletions test/cli/run/transpiler-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,34 @@ describe("transpiler cache", () => {
expect(await bunRun(join(temp_dir, "a.js"), env)).toSpawn("no-tmpdir-cache");
expect(newCacheCount()).toBe(1);
});
test("disables the cache when the cache directory does not fit in a path buffer", async () => {
writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "long-cache-dir"));

// The cache directory is derived from environment variables, which can be
// longer than PATH_MAX (4096 on Linux, 1024 on macOS). Joining such a value
// into the fixed-size path buffer used to panic while loading the first
// file large enough for the cache. 4095 is the longest value that still
// fits on its own; 4200 does not fit at all. (On Windows, where the buffer
// is 98 KB, they fit and creating the directory fails instead.) Windows
// reads HOME from USERPROFILE, so both are set.
const variants: Record<string, string | undefined>[] = [];
for (const length of [4095, 4200]) {
const dir = "/" + Buffer.alloc(length - 1, "h").toString();
variants.push(
{ BUN_RUNTIME_TRANSPILER_CACHE_PATH: undefined, XDG_CACHE_HOME: undefined, HOME: dir, USERPROFILE: dir },
{ BUN_RUNTIME_TRANSPILER_CACHE_PATH: undefined, XDG_CACHE_HOME: dir },
{ BUN_RUNTIME_TRANSPILER_CACHE_PATH: dir },
);
}

const results = await Promise.all(variants.map(vars => bunRun(join(temp_dir, "a.js"), { ...env, ...vars })));
for (const result of results) {
expect(result).toSpawn("long-cache-dir");
}
// Every variant overrode the cache location from `env`, so none of the runs
// exercised that one instead of its over-long value.
expect(existsSync(cache_dir)).toBeFalse();
});
test("works if the cache is not user-readable", async () => {
mkdirSync(cache_dir, { recursive: true });
writeFileSync(join(temp_dir, "a.js"), dummyFile((50 * 1024 * 1.5) | 0, "1", "b"));
Expand Down
Loading