Coverage Report

Created: 2026-09-04 06:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.4/src/replace.rs
Line
Count
Source
1
// This file is part of ICU4X. For terms of use, please see the file
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5
use crate::{impl_display_with_writeable, LengthHint, Writeable};
6
use core::fmt;
7
8
/// A [`Writeable`] adapter that replaces occurrences of a needle with a replacement.
9
///
10
/// This adapter performs the replacement in a streaming fashion during `write_to`,
11
/// requiring zero allocations.
12
///
13
/// # Examples
14
///
15
/// ```
16
/// use writeable::adapters::Replace;
17
/// use writeable::assert_writeable_eq;
18
/// use writeable::concat_writeable;
19
///
20
/// let source = concat_writeable!("I 💖 🦀", " and 🦀 loves me!");
21
/// let replace = Replace {
22
///     source,
23
///     needle: "🦀",
24
///     replacement: "Rust",
25
/// };
26
///
27
/// assert_writeable_eq!(replace, "I 💖 Rust and Rust loves me!");
28
/// ```
29
#[derive(Debug)]
30
#[allow(clippy::exhaustive_structs)] // designed for nesting
31
pub struct Replace<A, B, C> {
32
    /// The source writeable.
33
    pub source: A,
34
    /// The needle to search for.
35
    pub needle: B,
36
    /// The replacement writeable.
37
    pub replacement: C,
38
}
39
40
// Computes the Knuth-Morris-Pratt (KMP) prefix function (failure function) value
41
// for the character prefix ending at byte index `matched_bytes` in `needle`.
42
//
43
// Returns the byte length of the longest proper prefix of `needle[0..matched_bytes]`
44
// that is also a suffix of `needle[0..matched_bytes]`.
45
//
46
// This is computed on the fly without allocation by iterating over char boundaries.
47
0
fn get_pi_bytes(needle: &str, matched_bytes: usize) -> usize {
48
0
    let s = match needle.get(0..matched_bytes) {
49
0
        Some(s) => s,
50
0
        None => return 0,
51
    };
52
    // char_indices() gives us the byte offsets of character starts.
53
    // These offsets correspond to the byte lengths of all possible prefixes.
54
    // We want to iterate them in reverse order, excluding the first one (0)
55
    // because we want proper prefixes.
56
0
    for k in s
57
0
        .char_indices()
58
0
        .map(|(idx, _)| idx)
59
0
        .rev()
60
0
        .filter(|&idx| idx > 0)
61
    {
62
        // Compare the prefix of length `k` with the suffix of length `k`.
63
0
        if let Some(suffix) = s.as_bytes().get(s.len() - k..) {
64
0
            if s.as_bytes().starts_with(suffix) {
65
0
                return k;
66
0
            }
67
0
        }
68
    }
69
0
    0
70
0
}
71
72
// A writer wrapper that performs streaming replacement.
73
// It intercepts characters written to it, matches them against `needle` using KMP
74
// (tracking progress by storing the remaining unmatched suffix of the needle),
75
// and writes `replacement` when a full match is found, or the original characters otherwise.
76
struct ReplaceWriter<'a, W: ?Sized, C> {
77
    // The underlying sink to write to.
78
    sink: &'a mut W,
79
    // The needle we are searching for.
80
    needle: &'a str,
81
    // The replacement to write when the needle is matched.
82
    replacement: &'a C,
83
    // The remaining unmatched suffix of the needle.
84
    // This is always a suffix of `needle` starting at a character boundary.
85
    remaining_needle: &'a str,
86
}
87
88
impl<'a, W, C> ReplaceWriter<'a, W, C>
89
where
90
    W: fmt::Write + ?Sized,
91
    C: Writeable,
92
{
93
0
    fn new(sink: &'a mut W, needle: &'a str, replacement: &'a C) -> Self {
94
0
        Self {
95
0
            sink,
96
0
            needle,
97
0
            replacement,
98
0
            remaining_needle: needle,
99
0
        }
100
0
    }
101
102
    // Helper to get the length of the prefix matched so far.
103
0
    fn matched_len(&self) -> usize {
104
0
        self.needle.len() - self.remaining_needle.len()
105
0
    }
106
107
    // Finalizes the writer, flushing any partially matched prefix to the sink.
108
0
    fn finalize(&mut self) -> fmt::Result {
109
0
        let matched = self.matched_len();
110
0
        if matched > 0 {
111
0
            let slice = self.needle.get(0..matched).ok_or(fmt::Error)?;
112
0
            self.sink.write_str(slice)?;
113
0
            self.remaining_needle = self.needle;
114
0
        }
115
0
        Ok(())
116
0
    }
117
}
118
119
impl<'a, W, C> fmt::Write for ReplaceWriter<'a, W, C>
120
where
121
    W: fmt::Write + ?Sized,
122
    C: Writeable,
123
{
124
0
    fn write_str(&mut self, s: &str) -> fmt::Result {
125
0
        for c in s.chars() {
126
0
            self.write_char(c)?;
127
        }
128
0
        Ok(())
129
0
    }
130
131
0
    fn write_char(&mut self, c: char) -> fmt::Result {
132
        // If the needle is empty, we just pass through the characters.
133
0
        if self.needle.is_empty() {
134
0
            return self.sink.write_char(c);
135
0
        }
136
137
0
        let mut matched = self.matched_len();
138
        // KMP State Transition:
139
        // While we have a mismatch and we are not at the start of the needle,
140
        // backtrack using the prefix function.
141
0
        while matched > 0 && !self.remaining_needle.starts_with(c) {
142
0
            let old_j = matched;
143
0
            matched = get_pi_bytes(self.needle, old_j);
144
            // Since we backtracked, the prefix of length `old_j - j` is no longer
145
            // part of the potential match. We write it to the sink as a single slice.
146
0
            let slice = self.needle.get(0..(old_j - matched)).ok_or(fmt::Error)?;
147
0
            self.sink.write_str(slice)?;
148
            // Update remaining_needle to reflect the new matched length.
149
0
            self.remaining_needle = self.needle.get(matched..).ok_or(fmt::Error)?;
150
        }
151
152
        // If the character matches the next character in the needle, advance the match state.
153
0
        if self.remaining_needle.starts_with(c) {
154
            // Advance remaining_needle by the matched character.
155
0
            self.remaining_needle = self
156
0
                .remaining_needle
157
0
                .get(c.len_utf8()..)
158
0
                .ok_or(fmt::Error)?;
159
0
            if self.remaining_needle.is_empty() {
160
                // Full match found! Write the replacement instead of the needle.
161
0
                self.replacement.write_to(self.sink)?;
162
                // Reset match state.
163
0
                self.remaining_needle = self.needle;
164
0
            }
165
        } else {
166
            // Mismatch at the very beginning of the needle. Write the character as is.
167
0
            self.sink.write_char(c)?;
168
        }
169
0
        Ok(())
170
0
    }
171
}
172
173
impl<A, C> Writeable for Replace<A, &str, C>
174
where
175
    A: Writeable,
176
    C: Writeable,
177
{
178
    // We do not implement writeable_borrow because it is meant to be a constant-time O(1)
179
    // operation, but determining if a replacement occurred would require O(N) scanning.
180
0
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
181
0
        let mut writer = ReplaceWriter::new(sink, self.needle, &self.replacement);
182
0
        self.source.write_to(&mut writer)?;
183
0
        writer.finalize()
184
0
    }
185
186
0
    fn writeable_length_hint(&self) -> LengthHint {
187
0
        let source_hint = self.source.writeable_length_hint();
188
0
        let needle_len = self.needle.len();
189
0
        let replacement_hint = self.replacement.writeable_length_hint();
190
191
        // If needle and replacement have same exact length, length is unchanged.
192
0
        if let Some(r_upper) = replacement_hint.1 {
193
0
            if replacement_hint.0 == r_upper && needle_len == r_upper {
194
0
                return source_hint;
195
0
            }
196
0
        }
197
198
0
        let mut lower = 0;
199
0
        let mut upper = None;
200
201
        // If replacement is always larger than or equal to needle:
202
        // New length is at least the source length.
203
0
        if replacement_hint.0 >= needle_len {
204
0
            lower = source_hint.0;
205
0
        }
206
207
        // If replacement is always smaller than or equal to needle:
208
        // New length is at most the source length.
209
0
        if let Some(r_upper) = replacement_hint.1 {
210
0
            if r_upper <= needle_len {
211
0
                upper = source_hint.1;
212
0
            }
213
0
        }
214
215
0
        LengthHint(lower, upper)
216
0
    }
217
}
218
219
impl_display_with_writeable!(Replace<A, &'a str, C>, #[cfg(feature = "alloc")], where 'a, A: Writeable, C: Writeable);
220
221
#[test]
222
fn test_replace() {
223
    use crate::assert_writeable_eq;
224
    use crate::concat::Concat;
225
226
    // Basic replacement
227
    let replace1 = Replace {
228
        source: Concat("Hello", " 10 22 1101 33"),
229
        needle: "10",
230
        replacement: Concat("4", "4"),
231
    };
232
    assert_writeable_eq!(replace1, "Hello 44 22 1441 33");
233
234
    // Empty needle (should just write source)
235
    let replace2 = Replace {
236
        source: "Hello World",
237
        needle: "",
238
        replacement: "X",
239
    };
240
    assert_writeable_eq!(replace2, "Hello World");
241
242
    // Empty replacement
243
    let replace3 = Replace {
244
        source: "Hello 10 World 10",
245
        needle: "10",
246
        replacement: "",
247
    };
248
    assert_writeable_eq!(replace3, "Hello  World ");
249
250
    // Needle not found
251
    let replace4 = Replace {
252
        source: "Hello World",
253
        needle: "10",
254
        replacement: "X",
255
    };
256
    assert_writeable_eq!(replace4, "Hello World");
257
258
    // Needle at the beginning
259
    let replace5 = Replace {
260
        source: "10 Hello World",
261
        needle: "10",
262
        replacement: "X",
263
    };
264
    assert_writeable_eq!(replace5, "X Hello World");
265
266
    // Needle at the end
267
    let replace6 = Replace {
268
        source: "Hello World 10",
269
        needle: "10",
270
        replacement: "X",
271
    };
272
    assert_writeable_eq!(replace6, "Hello World X");
273
274
    // Overlapping needles (should consume and not match again)
275
    let replace7 = Replace {
276
        source: "ababa",
277
        needle: "aba",
278
        replacement: "X",
279
    };
280
    assert_writeable_eq!(replace7, "Xba");
281
282
    // Self-overlap but no match
283
    let replace8 = Replace {
284
        source: "aab",
285
        needle: "aac",
286
        replacement: "X",
287
    };
288
    assert_writeable_eq!(replace8, "aab");
289
290
    // Multi-byte UTF-8
291
    let replace9 = Replace {
292
        source: "🚀 🛸 🚀🚀 🚁",
293
        needle: "🚀",
294
        replacement: "星",
295
    };
296
    assert_writeable_eq!(replace9, "星 🛸 星星 🚁");
297
298
    // Multi-byte UTF-8 with partial match
299
    let replace10 = Replace {
300
        source: "🚀🚁",
301
        needle: "🚀🛸",
302
        replacement: "星",
303
    };
304
    assert_writeable_eq!(replace10, "🚀🚁");
305
306
    // Multi-byte UTF-8 with backtracking (no match)
307
    let replace11 = Replace {
308
        source: "🚀🚀🚁",
309
        needle: "🚀🚀🛸",
310
        replacement: "星",
311
    };
312
    assert_writeable_eq!(replace11, "🚀🚀🚁");
313
314
    // Multi-byte UTF-8 with backtracking (match)
315
    let replace12 = Replace {
316
        source: "🚀🚀🚀🛸",
317
        needle: "🚀🚀🛸",
318
        replacement: "星",
319
    };
320
    assert_writeable_eq!(replace12, "🚀星");
321
}