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
12 changes: 7 additions & 5 deletions src/sourcemap/LineOffsetTable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,17 +192,19 @@ impl LineOffsetTable {
let len_ = strings::wtf8_byte_sequence_length_with_invalid(b0);
// After the SIMD skip below lands, the loop head
// is overwhelmingly an ASCII '\r'/'\n' or a non-ASCII lead byte, so keep the
// 1-byte path branch-only and confine the zero+min+copy pad to the cold
// 1-byte path branch-only and confine the zero+copy pad to the cold
// multibyte arm.
// `len_` is the lead byte's *declared* width; a source whose final bytes are
// a truncated multibyte sequence declares more bytes than remain, so every
// slice below (decode, SIMD-skip offset, advance) must use the clamped width.
let cp_len = (len_ as usize).min(remaining.len());
let c: i32 = if len_ == 1 {
b0 as i32
} else {
let mut cp_bytes = [0u8; 4];
let take = (len_ as usize).min(remaining.len());
cp_bytes[..take].copy_from_slice(&remaining[..take]);
cp_bytes[..cp_len].copy_from_slice(&remaining[..cp_len]);
strings::decode_wtf8_rune_t::<i32>(cp_bytes, len_, 0)
};
let cp_len = len_ as usize;

let offset = (remaining.as_ptr() as usize - base) as u32;

Expand Down Expand Up @@ -233,7 +235,7 @@ impl LineOffsetTable {
// skip ahead to the next newline or non-ascii character
if let Some(j) = strings::index_of_newline_or_non_ascii_check_start::<false>(
remaining,
len_ as u32,
cp_len as u32,
) {
column += i32::try_from(j).expect("int cast");
remaining = &remaining[j as usize..];
Expand Down
38 changes: 38 additions & 0 deletions test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,41 @@ console.log(go());
}
});
});

// LineOffsetTable::generate builds the per-line column table directly from the
// raw source bytes, so it must tolerate a truncated trailing multi-byte UTF-8
// sequence: a lead byte that declares more bytes than the file has left.
describe.concurrent("sourcemap of a source with a truncated trailing UTF-8 sequence", () => {
test.each([
["lone 4-byte lead 0xF0", [0xf0]],
["lone 3-byte lead 0xE2", [0xe2]],
["lone 2-byte lead 0xC3", [0xc3]],
["overlong 2-byte lead 0xC1", [0xc1]],
["2 of 3 bytes 0xE0 0x81", [0xe0, 0x81]],
["3 of 4 bytes 0xF0 0x9F 0x92", [0xf0, 0x9f, 0x92]],
] as [string, number[]][])("%s", async (_name, tail) => {
using dir = tempDir("sourcemap-truncated-utf8", {
"in.js": Buffer.concat([Buffer.from("console.log(1);//"), Buffer.from(tail)]),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--sourcemap=external", "--outdir=out", "in.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ exitCode, stdout, stderr }).toEqual({
exitCode: 0,
stdout: expect.any(String),
stderr: expect.any(String),
});
// `console.log(1);` is identical in every variant and the truncated bytes
// live in a stripped comment, so the encoded mappings must be identical too.
const map = await Bun.file(path.join(String(dir), "out", "in.js.map")).json();
expect(map).toMatchObject({
sources: ["../in.js"],
mappings: ";AAAA,QAAQ,IAAI,CAAC;",
});
});
});
Loading