Skip to content

ContentSearchService crashes with "String index is out of bounds" on CJK content #876

Description

@qqfly

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?

  1. Open any reflowable Chinese EPUB (multi-byte CJK text; ours also mixes Latin fragments).
  2. let iterator = try await publication.search(query: "测试").get()
  3. _ = 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.endIndexString.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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions