Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/EpdFont/EpdFontFamily.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ class EpdFontFamily {
/// constraint instead of leaving it to documentation.
void setFallback(const EpdFont* f) {
if (f) {
assert(f->data->glyphMissHandler == nullptr &&
// resolveGlyph runs its pointer-identity miss detection on BOTH the selected primary
// (any of the four, per style) AND the fallback, so EVERY non-null primary must satisfy
// the same static-glyph-storage precondition as the fallback — not just the fallback.
assert((regular == nullptr || regular->data->glyphMissHandler == nullptr) &&
(bold == nullptr || bold->data->glyphMissHandler == nullptr) &&
(italic == nullptr || italic->data->glyphMissHandler == nullptr) &&
(boldItalic == nullptr || boldItalic->data->glyphMissHandler == nullptr) &&
f->data->glyphMissHandler == nullptr &&
"setFallback: ring-buffer-backed fonts (glyphMissHandler != nullptr) "
"are unsafe with the resolver's pointer-identity miss detection. "
"If you need SD-backed fallback, use an SD-safe lookup that does not "
Expand Down
5 changes: 4 additions & 1 deletion lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ void ChapterHtmlSlimParser::dispatchNonCjk(Utf8ClusterAssembler::NonCjkKind kind
nextJoin = WordJoin::Glue;
break;
case Utf8ClusterAssembler::NonCjkKind::Feff:
break; // discard BOM / ZWNBSP
// Discard the BOM / ZWNBSP codepoint. If nextJoin was CjkBreak the assembler has already
// upgraded it to Glue (intrinsic no-break); a real Space or existing Glue is left untouched.
// Either way, do NOT touch nextJoin here.
break;
default:
assert(false);
std::abort();
Expand Down
8 changes: 7 additions & 1 deletion lib/Epub/Epub/parsers/Utf8ClusterAssembler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,16 @@ Utf8ClusterAssembler::ConsumeResult Utf8ClusterAssembler::tryConsumeCodepoint(
return hadBase ? ConsumeResult::EmittedFlushable : ConsumeResult::StagedOnly;
}

// Non-CJK: Latin / whitespace / NBSP / FEFF. Do NOT touch nextJoin — the caller sets it.
// Non-CJK: Latin / whitespace / NBSP / FEFF. The caller sets nextJoin for Latin/whitespace/
// NBSP (those couple the join to a parser-side flush/emit action). FEFF (ZWNBSP) is the lone
// exception: it produces no token, so its only effect is an intrinsic no-break — glue across
// it here so the next CJK base stages with Glue instead of the run's default CjkBreak. Only
// upgrade a breakable CJK adjacency (CjkBreak): a preceding Space is a real space carried as
// the join, and an existing Glue is already no-break, so leave both untouched.
outNonCjkKind = classifyNonCjk(cp);
outNonCjkCp = cp;
outNonCjkLen = cpLen;
if (outNonCjkKind == NonCjkKind::Feff && nextJoin == WordJoin::CjkBreak) nextJoin = WordJoin::Glue;
if (state.pendingCjkBaseLen > 0) {
fillFlushableFromState(state, outFlushable);
state.pendingCjkBaseLen = 0; // clear staged base
Expand Down
4 changes: 3 additions & 1 deletion lib/Epub/Epub/parsers/Utf8ClusterAssembler.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class Utf8ClusterAssembler {
Latin, // ordinary printable codepoint — caller appends raw UTF-8 bytes to Latin buffer
Whitespace, // U+0020 / U+0009 / U+000A / U+000D — flush Latin, set nextJoin = Space
Nbsp, // U+00A0 / U+202F — flush, emit synthesized " " token with Glue, then nextJoin = Glue
Feff, // U+FEFF (BOM) — discard
Feff, // U+FEFF (BOM / ZWNBSP) — caller discards the codepoint; the assembler upgrades a
// breakable adjacency (nextJoin CjkBreak→Glue) so the no-break intent survives, but
// leaves a real Space (or an existing Glue) untouched
};

enum class ConsumeResult : uint8_t {
Expand Down
46 changes: 46 additions & 0 deletions test/parser_cluster/ParserClusterTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,4 +461,50 @@ TEST(ParserCluster, StyleSnapshotAtStageTimeNotFlushTime) {
}
}

// 13. FEFF (ZWNBSP) is a no-break: 日<U+FEFF>本 must NOT break before 本, while plain
// 日本 DOES. The ZWNBSP carries an intrinsic no-break intent, so the assembler must set
// nextJoin = Glue across it — the trailing base then stages with Glue, not CjkBreak.
TEST(ParserCluster, FeffIsNoBreakBetweenCjk) {
// 日 = E6 97 A5, U+FEFF = EF BB BF, 本 = E6 9C AC
const std::vector<uint8_t> withFeff = {0xE6, 0x97, 0xA5, 0xEF, 0xBB, 0xBF, 0xE6, 0x9C, 0xAC};
State state;
WordJoin nextJoin = WordJoin::Space;
feed(withFeff, state, nextJoin);

// The trailing base 本 is still staged; drain it and inspect its join.
Flushable drained;
ASSERT_TRUE(Utf8ClusterAssembler::flushPendingBase(state, drained));
EXPECT_EQ(decodeOnly(drained.bytes, drained.len), 0x672Cu); // 本
EXPECT_EQ(drained.join, WordJoin::Glue); // no break across the ZWNBSP

// Control: plain 日本 (no FEFF) keeps the ordinary CJK run join — breakable before 本.
const std::vector<uint8_t> plain = {0xE6, 0x97, 0xA5, 0xE6, 0x9C, 0xAC};
State st2;
WordJoin nj2 = WordJoin::Space;
feed(plain, st2, nj2);
Flushable d2;
ASSERT_TRUE(Utf8ClusterAssembler::flushPendingBase(st2, d2));
EXPECT_EQ(decodeOnly(d2.bytes, d2.len), 0x672Cu); // 本
EXPECT_EQ(d2.join, WordJoin::CjkBreak); // ordinary CJK run: may break here
}

// 14. FEFF's no-break only upgrades a breakable CJK adjacency (CjkBreak); it must NOT clobber a
// preceding space. In 日<space><FEFF>本 the parser's dispatchNonCjk(Whitespace) sets
// nextJoin = Space before the FEFF codepoint is consumed (the space exists only as that join,
// never as a token), so we enter FEFF with nextJoin = Space — feed <FEFF>本 from that state.
// Glueing across the FEFF here would silently swallow the space, so the trailing base must
// keep Space (and stay breakable), NOT become Glue.
TEST(ParserCluster, FeffDoesNotSwallowPrecedingSpace) {
// U+FEFF = EF BB BF, 本 = E6 9C AC. nextJoin = Space models the just-processed space.
const std::vector<uint8_t> buf = {0xEF, 0xBB, 0xBF, 0xE6, 0x9C, 0xAC};
State state;
WordJoin nextJoin = WordJoin::Space;
feed(buf, state, nextJoin);

Flushable drained;
ASSERT_TRUE(Utf8ClusterAssembler::flushPendingBase(state, drained));
EXPECT_EQ(decodeOnly(drained.bytes, drained.len), 0x672Cu); // 本
EXPECT_EQ(drained.join, WordJoin::Space); // space survives the ZWNBSP
}

} // namespace
Loading