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