Skip to main content

potential_utf/
writeable.rs

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
5use crate::{PotentialUtf16, PotentialUtf8};
6use core::fmt::Write;
7use writeable::{LengthHint, Part, PartsWrite, TryWriteable};
8
9use core::{char::DecodeUtf16Error, fmt, str::Utf8Error};
10
11/// This impl requires enabling the optional `writeable` Cargo feature
12impl TryWriteable for &'_ PotentialUtf8 {
13    type Error = Utf8Error;
14
15    fn try_write_to_parts<S: PartsWrite + ?Sized>(
16        &self,
17        sink: &mut S,
18    ) -> Result<Result<(), Self::Error>, fmt::Error> {
19        let mut remaining = &self.0;
20        let mut r = Ok(());
21        loop {
22            match core::str::from_utf8(remaining) {
23                Ok(valid) => {
24                    sink.write_str(valid)?;
25                    return Ok(r);
26                }
27                Err(e) => {
28                    // SAFETY: By Utf8Error invariants
29                    let valid = unsafe {
30                        core::str::from_utf8_unchecked(remaining.get_unchecked(..e.valid_up_to()))
31                    };
32                    sink.write_str(valid)?;
33                    sink.with_part(Part::ERROR, |s| s.write_char(char::REPLACEMENT_CHARACTER))?;
34                    if r.is_ok() {
35                        r = Err(e);
36                    }
37                    let Some(error_len) = e.error_len() else {
38                        return Ok(r); // end of string
39                    };
40                    // SAFETY: By Utf8Error invariants
41                    remaining = unsafe { remaining.get_unchecked(e.valid_up_to() + error_len..) }
42                }
43            }
44        }
45    }
46
47    fn writeable_length_hint(&self) -> LengthHint {
48        // Lower bound is all valid UTF-8, upper bound is all bytes with the high bit, which become replacement characters.
49        LengthHint::between(self.0.len(), self.0.len() * 3)
50    }
51}
52
53/// This impl requires enabling the optional `writeable` Cargo feature
54impl TryWriteable for &'_ PotentialUtf16 {
55    type Error = DecodeUtf16Error;
56
57    fn try_write_to_parts<S: PartsWrite + ?Sized>(
58        &self,
59        sink: &mut S,
60    ) -> Result<Result<(), Self::Error>, fmt::Error> {
61        let mut r = Ok(());
62        for c in char::decode_utf16(self.0.iter().copied()) {
63            match c {
64                Ok(c) => sink.write_char(c)?,
65                Err(e) => {
66                    if r.is_ok() {
67                        r = Err(e);
68                    }
69                    sink.with_part(Part::ERROR, |s| s.write_char(char::REPLACEMENT_CHARACTER))?;
70                }
71            }
72        }
73        Ok(r)
74    }
75
76    fn writeable_length_hint(&self) -> LengthHint {
77        // Lower bound is all ASCII, upper bound is all 3-byte code points (including replacement character)
78        LengthHint::between(self.0.len(), self.0.len() * 3)
79    }
80}
81
82#[cfg(test)]
83mod test {
84    #![allow(invalid_from_utf8)] // only way to construct the error
85    use super::*;
86    use writeable::assert_try_writeable_parts_eq;
87
88    #[test]
89    fn test_utf8() {
90        assert_try_writeable_parts_eq!(
91            PotentialUtf8::from_bytes(b"Foo Bar"),
92            "Foo Bar",
93            Ok(()),
94            []
95        );
96        assert_try_writeable_parts_eq!(
97            PotentialUtf8::from_bytes(b"Foo\xFDBar"),
98            "Foo�Bar",
99            Err(core::str::from_utf8(b"Foo\xFDBar").unwrap_err()),
100            [(3, 6, Part::ERROR)]
101        );
102        assert_try_writeable_parts_eq!(
103            PotentialUtf8::from_bytes(b"Foo\xFDBar\xff"),
104            "Foo�Bar�",
105            Err(core::str::from_utf8(b"Foo\xFDBar\xff").unwrap_err()),
106            [(3, 6, Part::ERROR), (9, 12, Part::ERROR)],
107        );
108    }
109
110    #[test]
111    fn test_utf16() {
112        assert_try_writeable_parts_eq!(
113            PotentialUtf16::from_slice(&[0xD83E, 0xDD73]),
114            "🥳",
115            Ok(()),
116            []
117        );
118        assert_try_writeable_parts_eq!(
119            PotentialUtf16::from_slice(&[0xD83E, 0x20, 0xDD73]),
120            "� �",
121            Err(char::decode_utf16([0xD83E].into_iter())
122                .next()
123                .unwrap()
124                .unwrap_err()),
125            [(0, 3, Part::ERROR), (4, 7, Part::ERROR)]
126        );
127    }
128}