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
30 changes: 29 additions & 1 deletion src/base64/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,14 @@ pub mod vlq {
let mut len: u8 = 0;
let mut bytes: [u8; VLQ_MAX_IN_BYTES] = [0; VLQ_MAX_IN_BYTES];

// Sign-magnitude: i32::MIN has no representation (its magnitude
// overflows the u32 VLQ), so it wraps to "-0" instead of panicking.
// The crash handler encodes bitcast u32 address halves through here
// and must not panic while already reporting a crash.
let mut vlq: u32 = if value >= 0 {
(value << 1) as u32
} else {
((-value << 1) | 1) as u32
(value.unsigned_abs() << 1) | 1
};

// source mappings are limited to i32
Expand Down Expand Up @@ -404,6 +408,30 @@ pub mod vlq {
pub fn decode_assume_valid(encoded: &[u8], start: usize) -> VLQResult {
decode_impl::<true>(encoded, start)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn encode_decode_roundtrip() {
for value in [0, 1, -1, 255, 256, -255, -256, i32::MAX, i32::MIN + 1] {
let encoded = VLQ::encode(value);
let result = decode(encoded.slice(), 0);
assert_eq!(result.value, value);
assert_eq!(result.start, encoded.len as usize);
}
assert_eq!(VLQ::encode(i32::MAX).slice(), b"+/////D");
assert_eq!(VLQ::encode(i32::MIN + 1).slice(), b"//////D");
}

#[test]
fn encode_i32_min_does_not_panic() {
// i32::MIN is outside the sign-magnitude domain; it wraps to "-0".
let encoded = VLQ::encode(i32::MIN);
assert_eq!(decode(encoded.slice(), 0).value, 0);
}
}
}

pub mod zig_base64 {
Expand Down
42 changes: 42 additions & 0 deletions test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,48 @@ describe("InternalSourceMap.fromVLQ validation", () => {
});
});

describe("InternalSourceMap.toVLQ", () => {
test.concurrent("window state of i32::MIN must not crash the VLQ encoder", async () => {
// SyncEntry state is raw i32, so a blob whose first window starts at
// generated column i32::MIN makes appendVLQTo pass a delta of exactly
// i32::MIN to VLQ.encode. i32::MIN has no sign-magnitude representation;
// the encoder's magnitude negation overflowed on it (debug builds abort
// with "attempt to negate with overflow"). The crash handler reaches the
// same encoder edge with bitcast u32 address halves while reporting a
// crash. fromVLQ rejects negative absolutes, so craft the blob by hand.
// Run in a child process so the abort is recorded as a failure here
// instead of taking down the test runner.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const { internalSourceMap } = require("bun:internal-for-testing");
// Blob layout (see src/sourcemap/InternalSourceMap.rs):
// [0..8] total_len u64, [8..16] mapping_count u64,
// [16..24] input_line_count u64, [24..28] sync_count u32,
// [28..32] stream_offset u32, [32..56] SyncEntry, [56..88] window
// header (count=1, no deltas), [88] stream tail pad.
const blob = new Uint8Array(89);
const dv = new DataView(blob.buffer);
dv.setBigUint64(0, 89n, true); // total_len
dv.setBigUint64(8, 1n, true); // mapping_count
dv.setUint32(24, 1, true); // sync_count
dv.setUint32(28, 56, true); // stream_offset
dv.setInt32(36, -2147483648, true); // SyncEntry.generated_column
blob[56] = 1; // window mapping count
console.log("TOVLQ: " + internalSourceMap.toVLQ(blob));`,
],
env: bunEnv,
stdout: "pipe",
stderr: "ignore",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
// i32::MIN wraps to the "-0" encoding ("B"); the other three fields are 0.
expect(stdout).toBe("TOVLQ: BAAA\n");
expect(exitCode).toBe(0);
});
});

describe("InternalSourceMap round-trip", () => {
test("synthetic: fromVLQ → toVLQ preserves all 4-field positions; names dropped, 1-field skipped", () => {
const vlqIn = buildSyntheticVLQ();
Expand Down
Loading