Skip to main content

xim_ctext/
lib.rs

1//! A parser for the compound text encoding used by the X Input Method protocol.
2//!
3//! This is intended to be used as a building block for higher level libraries. See the [`xim`] crate for an example.
4//!
5//! [xim]: https://crates.io/crates/xim
6
7#![no_std]
8#![allow(clippy::uninlined_format_args)]
9#![forbid(unsafe_code, future_incompatible)]
10
11extern crate alloc;
12
13#[cfg(feature = "std")]
14extern crate std;
15
16use alloc::string::String;
17use alloc::vec::Vec;
18use core::fmt;
19
20#[cfg(feature = "std")]
21use std::io::{self, Write};
22
23const UTF8_START: &[u8] = &[0x1B, 0x25, 0x47];
24const UTF8_END: &[u8] = &[0x1B, 0x25, 0x40];
25
26/// Wrapper for reduce allocation
27#[derive(Clone, Copy)]
28#[repr(transparent)]
29pub struct CText<'s> {
30    utf8: &'s str,
31}
32
33impl<'s> fmt::Debug for CText<'s> {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(self.utf8)
36    }
37}
38
39impl<'s> fmt::Display for CText<'s> {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.write_str(self.utf8)
42    }
43}
44
45impl<'s> CText<'s> {
46    pub const fn new(utf8: &'s str) -> Self {
47        Self { utf8 }
48    }
49
50    pub const fn len(self) -> usize {
51        self.utf8.len() + UTF8_START.len() + UTF8_END.len()
52    }
53
54    pub const fn is_empty(self) -> bool {
55        self.utf8.is_empty()
56    }
57
58    #[cfg(feature = "std")]
59    pub fn write(self, mut out: impl Write) -> io::Result<usize> {
60        let mut writed = 0;
61        writed += out.write(UTF8_START)?;
62        writed += out.write(self.utf8.as_bytes())?;
63        writed += out.write(UTF8_END)?;
64        Ok(writed)
65    }
66}
67
68/// Encoding utf8 to COMPOUND_TEXT with utf8 escape
69pub fn utf8_to_compound_text(text: &str) -> Vec<u8> {
70    let mut ret = Vec::with_capacity(text.len() + 6);
71    ret.extend_from_slice(UTF8_START);
72    ret.extend_from_slice(text.as_bytes());
73    ret.extend_from_slice(UTF8_END);
74    ret
75}
76
77#[derive(Debug, Clone)]
78pub enum DecodeError {
79    InvalidEncoding,
80    UnsupportedEncoding,
81    Utf8Error(alloc::string::FromUtf8Error),
82}
83
84impl From<alloc::string::FromUtf8Error> for DecodeError {
85    fn from(err: alloc::string::FromUtf8Error) -> Self {
86        DecodeError::Utf8Error(err)
87    }
88}
89
90impl fmt::Display for DecodeError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::InvalidEncoding => write!(f, "Invalid compound text"),
94            Self::UnsupportedEncoding => write!(f, "This encoding is not supported yet"),
95            Self::Utf8Error(e) => write!(f, "Not a valid utf8 {}", e),
96        }
97    }
98}
99
100macro_rules! decode {
101    ($decoder:expr, $out:expr, $bytes:expr, $last:expr) => {
102        let mut _current_bytes: &[u8] = $bytes;
103        loop {
104            let (ret, nread, _) = $decoder.decode_to_string(_current_bytes, $out, $last);
105
106            match ret {
107                encoding_rs::CoderResult::InputEmpty => break,
108                encoding_rs::CoderResult::OutputFull => {
109                    $out.reserve(
110                        $decoder
111                            .max_utf8_buffer_length($bytes.len())
112                            .unwrap_or_default(),
113                    );
114                    _current_bytes = &_current_bytes[nread..];
115                }
116            }
117        }
118    };
119}
120
121pub fn compound_text_to_utf8(bytes: &[u8]) -> Result<String, DecodeError> {
122    let split = bytes.split(|&b| b == 0x1b);
123
124    let mut result = String::new();
125
126    for chunk in split {
127        let mut iter = chunk.iter();
128        match (iter.next(), iter.next()) {
129            // UTF-8
130            (Some(0x25), Some(0x47)) => {
131                let left = iter.as_slice().to_vec();
132                match String::from_utf8(left) {
133                    Ok(out) => result.push_str(&out),
134                    Err(e) => return Err(DecodeError::from(e)),
135                };
136            }
137            // UTF-8 End
138            (Some(0x25), Some(0x40)) => {}
139            // ISO-2022 return to ASCII (ESC ( B) or JIS X 0201 Roman (ESC ( J).
140            // Mixed CJK+ASCII commits (e.g. Sogou date phrases) insert these between
141            // charset segments; without handling them, "(B" / "(J" leaks into UTF-8.
142            (Some(0x28), Some(0x42)) | (Some(0x28), Some(0x4a)) => {
143                let left = iter.as_slice();
144                let out = encoding_rs::mem::decode_latin1(left);
145                result.push_str(&out);
146            }
147            // 94N
148            (Some(0x24), Some(0x28)) => match iter.next() {
149                // JP
150                Some(0x42) => {
151                    let left = iter.as_slice();
152                    let mut decoder = encoding_rs::ISO_2022_JP.new_decoder_without_bom_handling();
153                    let mut out = String::new();
154                    decode!(decoder, &mut out, &[0x1B, 0x24, 0x42], false);
155                    decode!(decoder, &mut out, &left, true);
156
157                    result.push_str(&out);
158                }
159
160                // CN (GB2312)
161                Some(0x41) => {
162                    let left: Vec<u8> = iter.map(|&b| b + 0x80).collect();
163                    let (out, _) = encoding_rs::GBK.decode_without_bom_handling(&left);
164                    result.push_str(&out);
165                }
166
167                // KR (KS C 5601)
168                Some(0x43) => {
169                    let left: Vec<u8> = iter.map(|&b| b + 0x80).collect();
170                    let (out, _) = encoding_rs::EUC_KR.decode_with_bom_removal(&left);
171                    result.push_str(&out);
172                }
173                // Invalid encode
174                _ => return Err(DecodeError::InvalidEncoding),
175            },
176            // ISO-8859-1
177            (Some(0x2d), Some(0x41)) => {
178                let left = iter.as_slice();
179                let out = encoding_rs::mem::decode_latin1(left);
180                result.push_str(&out);
181            }
182            // ISO-8859-2
183            (Some(0x2d), Some(0x42)) => {
184                let left = iter.as_slice();
185                let (out, _) = encoding_rs::ISO_8859_2.decode_without_bom_handling(left);
186                result.push_str(&out);
187            }
188            // ISO-8859-3
189            (Some(0x2d), Some(0x43)) => {
190                let left = iter.as_slice();
191                let (out, _) = encoding_rs::ISO_8859_3.decode_without_bom_handling(left);
192                result.push_str(&out);
193            }
194            // ISO-8859-4
195            (Some(0x2d), Some(0x44)) => {
196                let left = iter.as_slice();
197                let (out, _) = encoding_rs::ISO_8859_4.decode_without_bom_handling(left);
198                result.push_str(&out);
199            }
200            // ISO-8859-7
201            (Some(0x2d), Some(0x46)) => {
202                let left = iter.as_slice();
203                let (out, _) = encoding_rs::ISO_8859_7.decode_without_bom_handling(left);
204                result.push_str(&out);
205            }
206            // ISO-8859-6
207            (Some(0x2d), Some(0x47)) => {
208                let left = iter.as_slice();
209                let (out, _) = encoding_rs::ISO_8859_6.decode_without_bom_handling(left);
210                result.push_str(&out);
211            }
212            // ISO-8859-8
213            (Some(0x2d), Some(0x48)) => {
214                let left = iter.as_slice();
215                let (out, _) = encoding_rs::ISO_8859_8.decode_without_bom_handling(left);
216                result.push_str(&out);
217            }
218            // ISO-8859-5
219            (Some(0x2d), Some(0x4c)) => {
220                let left = iter.as_slice();
221                let (out, _) = encoding_rs::ISO_8859_5.decode_without_bom_handling(left);
222                result.push_str(&out);
223            }
224            // ISO-8859-9
225            (Some(0x2d), Some(0x4d)) => {
226                let left = iter.as_slice();
227                let (out, _) = encoding_rs::WINDOWS_1254.decode_without_bom_handling(left);
228                result.push_str(&out);
229            }
230            // ISO-8859-10
231            (Some(0x2d), Some(0x56)) => {
232                let left = iter.as_slice();
233                let (out, _) = encoding_rs::ISO_8859_10.decode_without_bom_handling(left);
234                result.push_str(&out);
235            }
236            // ISO-8859-13
237            (Some(0x2d), Some(0x59)) => {
238                let left = iter.as_slice();
239                let (out, _) = encoding_rs::ISO_8859_13.decode_without_bom_handling(left);
240                result.push_str(&out);
241            }
242            // ISO-8859-14
243            (Some(0x2d), Some(0x5f)) => {
244                let left = iter.as_slice();
245                let (out, _) = encoding_rs::ISO_8859_14.decode_without_bom_handling(left);
246                result.push_str(&out);
247            }
248            // ISO-8859-15
249            (Some(0x2d), Some(0x62)) => {
250                let left = iter.as_slice();
251                let (out, _) = encoding_rs::ISO_8859_15.decode_without_bom_handling(left);
252                result.push_str(&out);
253            }
254            // ISO-8859-16
255            (Some(0x2d), Some(0x66)) => {
256                let left = iter.as_slice();
257                let (out, _) = encoding_rs::ISO_8859_16.decode_without_bom_handling(left);
258                result.push_str(&out);
259            }
260            // defaults to ISO-8859-1
261            _ => {
262                let out = encoding_rs::mem::decode_latin1(chunk);
263                result.push_str(&out);
264            }
265        };
266    }
267    Ok(result)
268}
269
270#[cfg(test)]
271mod tests {
272    #[test]
273    fn korean() {
274        const UTF8: &str = "가나다";
275        const COMP: &[u8] = &[
276            27, 37, 71, 234, 176, 128, 235, 130, 152, 235, 139, 164, 27, 37, 64,
277        ];
278        assert_eq!(crate::utf8_to_compound_text(UTF8), COMP);
279        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
280    }
281
282    #[test]
283    fn iso_2022_jp() {
284        const UTF8: &str = "東京";
285        const COMP: &[u8] = &[27, 36, 40, 66, 69, 108, 53, 126];
286        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
287    }
288
289    #[test]
290    fn iso_2022_jp_long() {
291        const UTF8: &str = "知ってるつもり";
292        const COMP: &[u8] = &[
293            27, 36, 40, 66, 67, 78, 36, 67, 36, 70, 36, 107, 36, 68, 36, 98, 36, 106, 27, 40, 66,
294        ];
295        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
296    }
297
298    #[test]
299    fn gb2312_cn_mixed_ascii_digits() {
300        // "2026年07月16日" as COMPOUND_TEXT with ESC ( B between GB2312 and ASCII.
301        const UTF8: &str = "2026年07月16日";
302        #[rustfmt::skip]
303        const COMP: &[u8] = &[
304            b'2', b'0', b'2', b'6',
305            0x1b, 0x24, 0x28, 0x41, 0x44, 0x6a, // 年
306            0x1b, 0x28, 0x42, b'0', b'7',
307            0x1b, 0x24, 0x28, 0x41, 0x54, 0x42, // 月
308            0x1b, 0x28, 0x42, b'1', b'6',
309            0x1b, 0x24, 0x28, 0x41, 0x48, 0x55, // 日
310        ];
311        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
312    }
313
314    #[test]
315    fn gb2312_cn() {
316        const UTF8: &str = "很高兴认识你";
317        const COMP: &[u8] = &[
318            0x1b, 0x24, 0x28, 0x41, 0x3a, 0x5c, 0x38, 0x5f, 0x50, 0x4b, 0x48, 0x4f, 0x4a, 0x36,
319            0x44, 0x63,
320        ];
321        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
322    }
323
324    #[test]
325    fn gb2312_cn_mixed() {
326        const UTF8: &str = "炸哦你";
327        const COMP: &[u8] = &[
328            0x1b, 0x24, 0x28, 0x42, 0x5f, 0x5a, 0x53, 0x28, 0x1b, 0x24, 0x28, 0x41, 0x44, 0x63,
329        ];
330        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
331    }
332
333    #[test]
334    fn ks_c_5601() {
335        const UTF8: &str = "넌최고야";
336        const COMP: &[u8] = &[
337            0x1b, 0x24, 0x28, 0x43, 0x33, 0x4d, 0x43, 0x56, 0x30, 0x6d, 0x3e, 0x5f,
338        ];
339        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
340    }
341
342    #[test]
343    fn iso_8859_1() {
344        const UTF8: &str = "¡¸ÀÑâó";
345        const COMP: &[u8] = &[0x1b, 0x2d, 0x41, 0xa1, 0xb8, 0xc0, 0xd1, 0xe2, 0xf3];
346        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
347    }
348
349    #[test]
350    fn iso_8859_2() {
351        const UTF8: &str = "ĄŁĽŚŠŤ";
352        const COMP: &[u8] = &[0x1b, 0x2d, 0x42, 0xa1, 0xa3, 0xa5, 0xa6, 0xa9, 0xab];
353        assert_eq!(crate::compound_text_to_utf8(COMP).unwrap(), UTF8);
354    }
355}