Skip to content

Commit e2d41d9

Browse files
authored
Merge pull request DeusData#1094 from harshitaajoshi/fix/utf8-docstring-truncation-1017
fix(extract): snap docstring truncation to UTF-8 codepoint boundary (DeusData#1017)
2 parents 2308ae0 + 4ce04f2 commit e2d41d9

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
@@ -1194,10 +1194,14 @@ static bool is_comment_node(const char *kind) {
11941194
}
11951195

11961196
// Extract comment text, truncating to MAX_COMMENT_LEN.
1197+
// #1017: snap the cut point back to a complete UTF-8 codepoint boundary.
11971198
static char *extract_comment_text(CBMArena *a, TSNode node, const char *source) {
11981199
char *text = cbm_node_text(a, node, source);
11991200
if (text && strlen(text) > MAX_COMMENT_LEN) {
1200-
text[MAX_COMMENT_LEN] = '\0';
1201+
size_t cut = MAX_COMMENT_LEN;
1202+
while (cut > 0 && ((unsigned char)text[cut] & 0xC0) == 0x80)
1203+
cut--;
1204+
text[cut] = '\0';
12011205
}
12021206
return text;
12031207
}

‎tests/test_extraction.c‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3711,6 +3711,81 @@ TEST(extract_rust_test_attr_marks_is_test_issue855) {
37113711
PASS();
37123712
}
37133713

3714+
/* #1017: docstring truncation at MAX_COMMENT_LEN (500 bytes) can split a
3715+
* multi-byte UTF-8 character, leaving an incomplete byte sequence.
3716+
* Craft a Go comment whose 498th-500th bytes are a 3-byte CJK character
3717+
* (U+6210 = 成 = e6 88 90). The raw byte truncation at offset 500 lands
3718+
* one byte past the character start, splitting it. After the fix the
3719+
* truncated string must end on a complete codepoint boundary. */
3720+
TEST(docstring_utf8_truncation_boundary_issue1017) {
3721+
/* Build a comment: "// " (3 bytes) + 495 ASCII 'A' + "成成成" (9 bytes)
3722+
* Total comment text = 3 + 495 + 9 = 507 bytes.
3723+
* MAX_COMMENT_LEN = 500. The first kanji (成 = e6 88 90) occupies
3724+
* offsets 498-500, so text[500] = '\0' keeps bytes 0-499: the lead
3725+
* byte 0xe6 plus one continuation 0x88 — an incomplete 2-of-3 sequence.
3726+
* Before fix: the truncated string ended with that broken pair. */
3727+
char comment[600];
3728+
int off = 0;
3729+
comment[off++] = '/';
3730+
comment[off++] = '/';
3731+
comment[off++] = ' ';
3732+
for (int i = 0; i < 495; i++)
3733+
comment[off++] = 'A';
3734+
/* U+6210 (成) = 0xe6 0x88 0x90 — 3-byte UTF-8 */
3735+
const char *kanji = "\xe6\x88\x90";
3736+
for (int k = 0; k < 3; k++) {
3737+
memcpy(comment + off, kanji, 3);
3738+
off += 3;
3739+
}
3740+
comment[off] = '\0';
3741+
3742+
/* Wrap in a Go function so the comment becomes the docstring. */
3743+
char src[800];
3744+
snprintf(src, sizeof(src), "package main\n\n%s\nfunc Compute() {}\n", comment);
3745+
3746+
CBMFileResult *r = extract(src, CBM_LANG_GO, "test", "main.go");
3747+
ASSERT_NOT_NULL(r);
3748+
ASSERT_FALSE(r->has_error);
3749+
ASSERT(has_def(r, "Function", "Compute"));
3750+
3751+
const char *doc = NULL;
3752+
for (int i = 0; i < r->defs.count; i++) {
3753+
if (strcmp(r->defs.items[i].name, "Compute") == 0) {
3754+
doc = r->defs.items[i].docstring;
3755+
break;
3756+
}
3757+
}
3758+
ASSERT_NOT_NULL(doc);
3759+
3760+
/* Verify every byte in the truncated docstring is valid UTF-8:
3761+
* no trailing incomplete multi-byte sequence. */
3762+
size_t len = strlen(doc);
3763+
ASSERT_TRUE(len <= 500);
3764+
const unsigned char *u = (const unsigned char *)doc;
3765+
size_t i = 0;
3766+
while (i < len) {
3767+
unsigned char c = u[i];
3768+
int seq_len;
3769+
if (c < 0x80)
3770+
seq_len = 1;
3771+
else if ((c & 0xE0) == 0xC0)
3772+
seq_len = 2;
3773+
else if ((c & 0xF0) == 0xE0)
3774+
seq_len = 3;
3775+
else if ((c & 0xF8) == 0xF0)
3776+
seq_len = 4;
3777+
else
3778+
FAIL("invalid UTF-8 lead byte");
3779+
ASSERT_TRUE(i + (size_t)seq_len <= len);
3780+
for (int j = 1; j < seq_len; j++)
3781+
ASSERT_TRUE((u[i + (size_t)j] & 0xC0) == 0x80);
3782+
i += (size_t)seq_len;
3783+
}
3784+
3785+
cbm_free_result(r);
3786+
PASS();
3787+
}
3788+
37143789
/* Reproduce-first (ms-typescript reallyLargeFile.ts, 2026-07-07): a file
37153790
* whose root node has hundreds of thousands of FLAT SIBLINGS (580k ////
37163791
* comment lines in the 3.5 MB fourslash fixture) hung extraction for over
@@ -4836,6 +4911,7 @@ SUITE(extraction) {
48364911
RUN_TEST(extract_c_clean_file_no_recovery_duplicates_issue961);
48374912
RUN_TEST(walk_defs_no_truncation_over_4096_issue668);
48384913
RUN_TEST(extract_rust_test_attr_marks_is_test_issue855);
4914+
RUN_TEST(docstring_utf8_truncation_boundary_issue1017);
48394915

48404916
cbm_shutdown();
48414917
}

0 commit comments

Comments
 (0)