Describe the bug
Searching a Chinese-language EPUB via publication.search(query:) reliably crashes with a Swift precondition failure inside ContentSearchService:
libswiftCore.dylib: _assertionFailure — String index is out of bounds
String.index(_:offsetBy:)
Iterator.makeLocator(matchStart:matchEnd:) (ContentSearchService.swift:510)
Iterator.search(dangerZoneCapacity:)
Iterator.next()
How to reproduce?
- Open any reflowable Chinese EPUB (multi-byte CJK text; ours also mixes Latin fragments).
let iterator = try await publication.search(query: "测试").get()
_ = await iterator.next() → crash on books/queries where matches land after mixed-width text.
Readium version
3.11.0
OS version
iOS 26.5
Testing device
iPhone 15 Pro (device) + iPhone 17 (simulator)
Environment
macOS: 26.5.2
platform: arm64
Xcode 26.6
Build version 17F113
Additional context
Analysis
In ContentSearchService.search(dangerZoneCapacity:), match ranges returned by the search algorithm are converted to window offsets via searchSlice.distance(from:to:) (Character distances), and later makeLocator(matchStart:matchEnd:) re-derives indices with windowText.index(windowText.startIndex, offsetBy: matchStart). When the window has been trimmed (trimAmount bookkeeping) and/or when findRanges operates on NSString/UTF-16 semantics (e.g. .diacriticInsensitive via ICU), the character-offset arithmetic drifts on multi-byte CJK content, producing offsets past windowText.endIndex — String.index(_:offsetBy:) then traps.
A defensive fix would be index(_:offsetBy:limitedBy:) with a nil-guard in makeLocator/search; a root fix is keeping all offset bookkeeping in a single unit (either String.Index end-to-end, or UTF-16 offsets converted at the boundary).
Note that ContentSearchService has been the default EPUB search backend since 3.9.0 (PR #787), and the 3.9.0–3.11.0 changelogs show no CJK/offset-related fixes in Shared — so the same window/offset arithmetic likely affects all three releases, not just 3.11.0.
Workaround
We replaced the service with a custom SearchService (per-segment String.range(of:options:) matching, no window arithmetic) via setSearchServiceFactory, which is stable on CJK.
Describe the bug
Searching a Chinese-language EPUB via
publication.search(query:)reliably crashes with a Swift precondition failure insideContentSearchService:How to reproduce?
let iterator = try await publication.search(query: "测试").get()_ = await iterator.next()→ crash on books/queries where matches land after mixed-width text.Readium version
3.11.0
OS version
iOS 26.5
Testing device
iPhone 15 Pro (device) + iPhone 17 (simulator)
Environment
Additional context
Analysis
In
ContentSearchService.search(dangerZoneCapacity:), match ranges returned by the search algorithm are converted to window offsets viasearchSlice.distance(from:to:)(Character distances), and latermakeLocator(matchStart:matchEnd:)re-derives indices withwindowText.index(windowText.startIndex, offsetBy: matchStart). When the window has been trimmed (trimAmountbookkeeping) and/or whenfindRangesoperates on NSString/UTF-16 semantics (e.g..diacriticInsensitivevia ICU), the character-offset arithmetic drifts on multi-byte CJK content, producing offsets pastwindowText.endIndex—String.index(_:offsetBy:)then traps.A defensive fix would be
index(_:offsetBy:limitedBy:)with a nil-guard inmakeLocator/search; a root fix is keeping all offset bookkeeping in a single unit (eitherString.Indexend-to-end, or UTF-16 offsets converted at the boundary).Note that
ContentSearchServicehas been the default EPUB search backend since 3.9.0 (PR #787), and the 3.9.0–3.11.0 changelogs show no CJK/offset-related fixes in Shared — so the same window/offset arithmetic likely affects all three releases, not just 3.11.0.Workaround
We replaced the service with a custom
SearchService(per-segmentString.range(of:options:)matching, no window arithmetic) viasetSearchServiceFactory, which is stable on CJK.