Skip to content

Fix several ICU usage bugs - #998

Merged
Leonard Hecker (lhecker) merged 2 commits into
mainfrom
dev/lhecker/994-icu-bugs
Oct 27, 2026
Merged

Leonard Hecker (lhecker) merged 2 commits into
mainfrom
dev/lhecker/994-icu-bugs

Conversation

@lhecker

Copy link
Copy Markdown
Member

};

self.find_select_next(search, next_search_offset, true);
self.find_select_next(search, next_search_offset, true)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains just Option<> -> Result<Option<>> plumbing.


impl iter::FusedIterator for Utf8Chars<'_> {}

/// Returns the last character using the same invalid-UTF-8 recovery as `Utf8Chars`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed utf8_decode_last to not only split off the last char but also return the remaining unconsumed text. Used by ICU.

Comment thread crates/edit/src/icu.rs
}

extern "C" fn utext_access(ut: &mut icu_ffi::UText, native_index: i64, forward: bool) -> bool {
if let Some(cache) = utext_access_impl(ut, native_index, forward) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug with the "freezes when replacing at end of file" was that ICU would request indices that are out of bounds and I would reject them with an early return false WITHOUT updating the chunk contents (bad!). So, here you see: Early return removed, clamp the requested index, and always update the chunk contents.

Comment thread crates/edit/src/icu.rs
Comment on lines -429 to -431
if index_contained < 0 || index_contained as usize >= tb.text_length() {
return None;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is the bad part.)

Comment thread crates/edit/src/icu.rs
// The worst case ratio for UTF-8 to UTF-16 is 1:1, when the text is ASCII.
// This allows us to safely subtract the UTF-16 buffer size
// and assume that whatever we read as UTF-8 will fit.
// TODO: Test what happens if you have lots of invalid UTF-8 text blow up to U+FFFD.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right I also fixed this "blow up" by limiting the number of chars to probe for UTF8 continuation bytes.

Comment thread crates/edit/src/icu.rs

if forward {
utf8_len_limit = (tb.text_length() - native_index).min(UTF16_LEN_LIMIT);
native_start = native_index;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the issue here is that while reverse iteration below aligned the UTF8 chunk start, we actually need to do it unconditionally.

Comment thread crates/edit/src/icu.rs

// Translate the given range from UTF-8 to UTF-16.
// NOTE: This code makes the assumption that the `native_index` is always
// at UTF-8 codepoint boundaries which technically isn't guaranteed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See, I even noted it as a TODO lol.

Comment thread crates/edit/src/icu.rs
if status == icu_ffi::U_BUFFER_OVERFLOW_ERROR && output_len > 0 {
output.reserve_exact(arena, output_len as usize);
let output = output.spare_capacity_mut();
status = icu_ffi::U_ZERO_ERROR;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes fold_case, because without this the ucasemap_utf8FoldCase call below gets skipped (ICU early returns if the given status is not okay).

This was not apparent, because I preallocate a guesstimated amount of memory. This fallback path is only executed when that size was not enough (rare).

Comment thread crates/edit/src/icu.rs
//
// TODO: It may be worth avoiding this if requested index
// neighbors the previous block during linear scans by ICU.
if native_start < tb.text_length() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

native_start being len-1 would not be a problem here, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean because it enters the if condition? No, that should be fine. The if condition only exists so we can safely do native_start + 1. Otherwise, everyone's welcome.

@lhecker
Leonard Hecker (lhecker) enabled auto-merge (squash) October 27, 2026 22:44
@lhecker
Leonard Hecker (lhecker) merged commit 996d2ee into main Oct 27, 2026
6 checks passed
@lhecker
Leonard Hecker (lhecker) deleted the dev/lhecker/994-icu-bugs branch October 27, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace All hangs when the last match ends at the end of the file and the replacement matches again

2 participants