Skip to main content

redact_core/
utf8.rs

1// Copyright 2026 Censgate LLC. // pragma: allowlist secret
2// Licensed under the Apache License, Version 2.0. See the LICENSE file
3// in the project root for license information.
4
5//! Char-boundary helpers.
6//!
7//! `str::floor_char_boundary` and `str::ceil_char_boundary` stabilized in Rust
8//! 1.91. This workspace's MSRV is 1.88, so the same behavior lives here.
9
10/// Largest char boundary at or before `index`.
11///
12/// Indexes past `s.len()` clamp to the end. `0` and `s.len()` are always
13/// boundaries.
14pub fn floor_char_boundary(s: &str, mut index: usize) -> usize {
15    if index > s.len() {
16        index = s.len();
17    }
18    while index > 0 && !s.is_char_boundary(index) {
19        index -= 1;
20    }
21    index
22}
23
24/// Smallest char boundary at or after `index`.
25///
26/// Indexes past `s.len()` clamp to the end.
27pub fn ceil_char_boundary(s: &str, mut index: usize) -> usize {
28    if index > s.len() {
29        index = s.len();
30    }
31    while index < s.len() && !s.is_char_boundary(index) {
32        index += 1;
33    }
34    index
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn snaps_multibyte_scalars_without_passing_the_ends() {
43        let text = "ó📄你";
44        assert_eq!(floor_char_boundary(text, 0), 0);
45        assert_eq!(floor_char_boundary(text, 1), 0);
46        assert_eq!(ceil_char_boundary(text, 1), 2);
47        assert_eq!(floor_char_boundary(text, text.len() + 5), text.len());
48        assert_eq!(ceil_char_boundary(text, text.len()), text.len());
49        assert!(text.is_char_boundary(floor_char_boundary(text, 3)));
50        assert!(text.is_char_boundary(ceil_char_boundary(text, 3)));
51    }
52}