Skip to content

Commit 128e3d8

Browse files
committed
fix(rst): a section's text no longer reads an unwritten byte
rst_body cut a reST section's text at 500 bytes by setting a stop value past the end, then backing off from out[RST_BODY_MAX] while that byte looked like a UTF-8 continuation byte. That byte was never written, so where the text ended (499 or 500 bytes) depended on what the arena memory held, and when a multibyte character did not fit, bytes never written could end up in the text. Only whole characters are written now, and the text ends at the last one written. The same django tree indexed twice gave doc_link_candidates rows that differed in about 250 of 11,300 (through the section text's TF-IDF); with this change two runs are identical, and so are the 6,594 sections. The new test pins the cut at the boundary: a word that fills the 500 bytes, a two-byte character that would cross them, a word cut at a character. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent c1cb70e commit 128e3d8

2 files changed

Lines changed: 44 additions & 10 deletions

File tree

‎internal/cbm/doclink_rst.c‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,18 +1606,19 @@ static const char *rst_body(rst_doc_t *d, int from, int to) {
16061606
return NULL;
16071607
}
16081608
int w = 0;
1609-
for (int k = from; k < to && w < RST_BODY_MAX; k++) {
1609+
bool full = false; /* the next character did not fit: the text ends before it */
1610+
for (int k = from; k < to && !full; k++) {
16101611
const char *s = d->L[k].s;
16111612
int len = d->L[k].len;
16121613
int i = 0;
1613-
while (i < len && w < RST_BODY_MAX) {
1614+
while (i < len && !full) {
16141615
int n;
16151616
uint32_t cp = rst_cp(s, len, i, &n);
16161617
if (rst_space_cp(cp)) {
16171618
i += n;
16181619
continue;
16191620
}
1620-
if (w > 0) {
1621+
if (w > 0 && w < RST_BODY_MAX) {
16211622
out[w++] = ' ';
16221623
}
16231624
while (i < len) {
@@ -1626,7 +1627,7 @@ static const char *rst_body(rst_doc_t *d, int from, int to) {
16261627
break;
16271628
}
16281629
if (w + n > RST_BODY_MAX) {
1629-
w = RST_BODY_MAX + 1; /* stop: the next character does not fit */
1630+
full = true; /* only whole characters are written: none is cut */
16301631
break;
16311632
}
16321633
memcpy(out + w, s + i, (size_t)n);
@@ -1635,12 +1636,6 @@ static const char *rst_body(rst_doc_t *d, int from, int to) {
16351636
}
16361637
}
16371638
}
1638-
if (w > RST_BODY_MAX) {
1639-
w = RST_BODY_MAX;
1640-
while (w > 0 && ((unsigned char)out[w] & 0xC0) == 0x80) {
1641-
w--;
1642-
}
1643-
}
16441639
while (w > 0 && out[w - 1] == ' ') {
16451640
w--;
16461641
}

‎tests/test_doc_links_rst.c‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,9 +558,48 @@ TEST(rst_ship_gate) {
558558
PASS();
559559
}
560560

561+
/* A section's text is cut before the first character that does not fit its
562+
* 500 bytes: only whole characters are written, and the text ends at the last
563+
* one written (it once tested an unwritten byte past the end, so where the
564+
* text ended depended on what that memory held). */
565+
TEST(rst_section_text_cut) {
566+
static const struct {
567+
const char *tail;
568+
const char *want_tail;
569+
} cases[] = {
570+
{" xxxxx tail", " xxxxx"}, /* a word that ends at byte 500 */
571+
{" xxxx\xc3\xa9 more", " xxxx"}, /* a two-byte character that would cross it */
572+
{" yyyyyyyy", " yyyyy"}, /* a word cut at a character */
573+
};
574+
for (size_t c = 0; c < sizeof(cases) / sizeof(cases[0]); c++) {
575+
char doc[1024];
576+
char want[600];
577+
int n = snprintf(doc, sizeof(doc), "Title\n=====\n\n");
578+
int w = 0;
579+
for (int k = 0; k < 99; k++) { /* 99 words: 494 bytes, nine per paragraph */
580+
n += snprintf(doc + n, sizeof(doc) - (size_t)n, "%sabcd",
581+
k == 0 ? ""
582+
: k % 9 == 0 ? "\n\n"
583+
: " ");
584+
w += snprintf(want + w, sizeof(want) - (size_t)w, "%sabcd", k ? " " : "");
585+
}
586+
snprintf(doc + n, sizeof(doc) - (size_t)n, "%s\n", cases[c].tail);
587+
snprintf(want + w, sizeof(want) - (size_t)w, "%s", cases[c].want_tail);
588+
CBMFileResult *r = dm_extract(doc, CBM_LANG_RST, "docs/cut.rst");
589+
ASSERT_NOT_NULL(r);
590+
const CBMDefinition *t = rst_def(r, "Section", "Title");
591+
ASSERT_NOT_NULL(t);
592+
ASSERT_NOT_NULL(t->docstring);
593+
ASSERT_STR_EQ(t->docstring, want);
594+
cbm_free_result(r);
595+
}
596+
PASS();
597+
}
598+
561599
SUITE(doc_links_rst) {
562600
dm_ship_held_families();
563601
RUN_TEST(rst_scan_structure);
602+
RUN_TEST(rst_section_text_cut);
564603
RUN_TEST(rst_python_scope_blob);
565604
RUN_TEST(rst_links_pipeline);
566605
RUN_TEST(rst_c_member_owner);

0 commit comments

Comments
 (0)