@@ -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