Skip to content

Commit 3ebcf48

Browse files
fix(extract): snap docstring truncation to UTF-8 codepoint boundary (DeusData#1017)
Signed-off-by: Harshita Joshi <j.harshitaa06@gmail.com>
1 parent 37ef5b5 commit 3ebcf48

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

‎internal/cbm/extract_defs.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,10 +1168,14 @@ static bool is_comment_node(const char *kind) {
11681168
}
11691169

11701170
// Extract comment text, truncating to MAX_COMMENT_LEN.
1171+
// #1017: snap the cut point back to a complete UTF-8 codepoint boundary.
11711172
static char *extract_comment_text(CBMArena *a, TSNode node, const char *source) {
11721173
char *text = cbm_node_text(a, node, source);
11731174
if (text && strlen(text) > MAX_COMMENT_LEN) {
1174-
text[MAX_COMMENT_LEN] = '\0';
1175+
size_t cut = MAX_COMMENT_LEN;
1176+
while (cut > 0 && ((unsigned char)text[cut] & 0xC0) == 0x80)
1177+
cut--;
1178+
text[cut] = '\0';
11751179
}
11761180
return text;
11771181
}

‎tests/test_extraction.c‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3517,6 +3517,81 @@ TEST(extract_rust_test_attr_marks_is_test_issue855) {
35173517
PASS();
35183518
}
35193519

3520+
/* #1017: docstring truncation at MAX_COMMENT_LEN (500 bytes) can split a
3521+
* multi-byte UTF-8 character, leaving an incomplete byte sequence.
3522+
* Craft a Go comment whose 498th-500th bytes are a 3-byte CJK character
3523+
* (U+6210 = 成 = e6 88 90). The raw byte truncation at offset 500 lands
3524+
* one byte past the character start, splitting it. After the fix the
3525+
* truncated string must end on a complete codepoint boundary. */
3526+
TEST(docstring_utf8_truncation_boundary_issue1017) {
3527+
/* Build a comment: "// " (3 bytes) + 495 ASCII 'A' + "成成成" (9 bytes)
3528+
* Total comment text = 3 + 495 + 9 = 507 bytes.
3529+
* MAX_COMMENT_LEN = 500. The first kanji (成 = e6 88 90) occupies
3530+
* offsets 498-500, so text[500] = '\0' keeps bytes 0-499: the lead
3531+
* byte 0xe6 plus one continuation 0x88 — an incomplete 2-of-3 sequence.
3532+
* Before fix: the truncated string ended with that broken pair. */
3533+
char comment[600];
3534+
int off = 0;
3535+
comment[off++] = '/';
3536+
comment[off++] = '/';
3537+
comment[off++] = ' ';
3538+
for (int i = 0; i < 495; i++)
3539+
comment[off++] = 'A';
3540+
/* U+6210 (成) = 0xe6 0x88 0x90 — 3-byte UTF-8 */
3541+
const char *kanji = "\xe6\x88\x90";
3542+
for (int k = 0; k < 3; k++) {
3543+
memcpy(comment + off, kanji, 3);
3544+
off += 3;
3545+
}
3546+
comment[off] = '\0';
3547+
3548+
/* Wrap in a Go function so the comment becomes the docstring. */
3549+
char src[800];
3550+
snprintf(src, sizeof(src), "package main\n\n%s\nfunc Compute() {}\n", comment);
3551+
3552+
CBMFileResult *r = extract(src, CBM_LANG_GO, "test", "main.go");
3553+
ASSERT_NOT_NULL(r);
3554+
ASSERT_FALSE(r->has_error);
3555+
ASSERT(has_def(r, "Function", "Compute"));
3556+
3557+
const char *doc = NULL;
3558+
for (int i = 0; i < r->defs.count; i++) {
3559+
if (strcmp(r->defs.items[i].name, "Compute") == 0) {
3560+
doc = r->defs.items[i].docstring;
3561+
break;
3562+
}
3563+
}
3564+
ASSERT_NOT_NULL(doc);
3565+
3566+
/* Verify every byte in the truncated docstring is valid UTF-8:
3567+
* no trailing incomplete multi-byte sequence. */
3568+
size_t len = strlen(doc);
3569+
ASSERT_TRUE(len <= 500);
3570+
const unsigned char *u = (const unsigned char *)doc;
3571+
size_t i = 0;
3572+
while (i < len) {
3573+
unsigned char c = u[i];
3574+
int seq_len;
3575+
if (c < 0x80)
3576+
seq_len = 1;
3577+
else if ((c & 0xE0) == 0xC0)
3578+
seq_len = 2;
3579+
else if ((c & 0xF0) == 0xE0)
3580+
seq_len = 3;
3581+
else if ((c & 0xF8) == 0xF0)
3582+
seq_len = 4;
3583+
else
3584+
FAIL("invalid UTF-8 lead byte");
3585+
ASSERT_TRUE(i + (size_t)seq_len <= len);
3586+
for (int j = 1; j < seq_len; j++)
3587+
ASSERT_TRUE((u[i + (size_t)j] & 0xC0) == 0x80);
3588+
i += (size_t)seq_len;
3589+
}
3590+
3591+
cbm_free_result(r);
3592+
PASS();
3593+
}
3594+
35203595
/* Reproduce-first (ms-typescript reallyLargeFile.ts, 2026-07-07): a file
35213596
* whose root node has hundreds of thousands of FLAT SIBLINGS (580k ////
35223597
* comment lines in the 3.5 MB fourslash fixture) hung extraction for over
@@ -3868,6 +3943,7 @@ SUITE(extraction) {
38683943
RUN_TEST(extract_c_clean_file_no_recovery_duplicates_issue961);
38693944
RUN_TEST(walk_defs_no_truncation_over_4096_issue668);
38703945
RUN_TEST(extract_rust_test_attr_marks_is_test_issue855);
3946+
RUN_TEST(docstring_utf8_truncation_boundary_issue1017);
38713947

38723948
cbm_shutdown();
38733949
}

0 commit comments

Comments
 (0)