Skip to main content

ag_psd/
utf8.rs

1/*
2File: crates/ag-psd/src/utf8.rs
3
4Purpose:
5кодирование и декодирование UTF-8 строк.
6
7Source compatibility:
8- порт upstream-файла `test/ag-psd/src/utf8.ts` (разбиение 1:1).
9
10Main responsibilities:
11- зеркалировать соответствующий upstream-модуль при портировании;
12- держать публичный контракт этого участка в одном месте.
13
14Mapping TS -> Rust:
15- `charLengthInBytes(code)`        -> `char_length_in_bytes(code: u32) -> usize`
16- `stringLengthInBytes(value)`     -> `string_length_in_bytes(value: &str) -> usize`
17- `writeCharacter(buf, off, code)` -> `write_character(buffer: &mut [u8], offset: usize, code: u32) -> usize`
18- `encodeStringTo(buf, off, val)`  -> `encode_string_to(buffer: &mut [u8], offset: usize, value: &str) -> usize`
19- `encodeString(value)`            -> `encode_string(value: &str) -> Vec<u8>`
20- `decodeString(value)`            -> `decode_string(value: &[u8]) -> Result<String, Utf8DecodeError>`
21
22Notes on faithfulness:
23- Upstream работает на UTF-16 code units (`charCodeAt`) и вручную собирает суррогатные
24  пары перед кодированием в UTF-8. В Rust `&str` уже хранит валидный UTF-8, поэтому
25  ручная посимвольная сборка по `char`-ам (scalar values) даёт ровно те же байты, что и
26  ветвь `charLengthInBytes`/`writeCharacter` для любой валидной строки. Суррогаты в `&str`
27  невозможны, так что ветка склейки high/low surrogate автоматически выполняется самим Rust.
28- `decodeString` повторяет ручной UTF-8-декодер upstream'а, включая отклонение overlong-
29  последовательностей, одиночных суррогатов и кодов вне диапазона. Вместо `String::from_utf8`
30  оставлен ручной разбор, чтобы воспроизвести точную семантику ошибок upstream'а
31  (одиночные суррогаты, проверка границ continuation-байтов и т.п.).
32*/
33
34// PORT STATUS: ported
35
36/// Ошибка ручного UTF-8-декодера (зеркало `throw Error(...)` из upstream).
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum Utf8DecodeError {
39    InvalidByteIndex,
40    InvalidContinuationByte,
41    LoneSurrogate(u32),
42    InvalidUtf8Detected,
43}
44
45impl std::fmt::Display for Utf8DecodeError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Utf8DecodeError::InvalidByteIndex => write!(f, "Invalid byte index"),
49            Utf8DecodeError::InvalidContinuationByte => write!(f, "Invalid continuation byte"),
50            Utf8DecodeError::LoneSurrogate(code) => write!(
51                f,
52                "Lone surrogate U+{} is not a scalar value",
53                format!("{:X}", code)
54            ),
55            Utf8DecodeError::InvalidUtf8Detected => write!(f, "Invalid UTF-8 detected"),
56        }
57    }
58}
59
60impl std::error::Error for Utf8DecodeError {}
61
62/// Длина в байтах для UTF-8-кодирования code point'а (зеркало `charLengthInBytes`).
63fn char_length_in_bytes(code: u32) -> usize {
64    if (code & 0xffff_ff80) == 0 {
65        1
66    } else if (code & 0xffff_f800) == 0 {
67        2
68    } else if (code & 0xffff_0000) == 0 {
69        3
70    } else {
71        4
72    }
73}
74
75/// Сколько байт займёт строка в UTF-8 (зеркало `stringLengthInBytes`).
76///
77/// В TS итерация идёт по UTF-16 code unit'ам со сборкой суррогатов; в Rust `chars()`
78/// уже отдаёт scalar values, поэтому `char_length_in_bytes(c as u32)` эквивалентно.
79pub fn string_length_in_bytes(value: &str) -> usize {
80    let mut result = 0;
81    for c in value.chars() {
82        result += char_length_in_bytes(c as u32);
83    }
84    result
85}
86
87/// Записать один code point в `buffer` начиная с `offset`, вернуть число записанных байт
88/// (зеркало `writeCharacter`).
89fn write_character(buffer: &mut [u8], offset: usize, code: u32) -> usize {
90    let length = char_length_in_bytes(code);
91
92    match length {
93        1 => {
94            buffer[offset] = code as u8;
95        }
96        2 => {
97            buffer[offset] = (((code >> 6) & 0x1f) | 0xc0) as u8;
98            buffer[offset + 1] = ((code & 0x3f) | 0x80) as u8;
99        }
100        3 => {
101            buffer[offset] = (((code >> 12) & 0x0f) | 0xe0) as u8;
102            buffer[offset + 1] = (((code >> 6) & 0x3f) | 0x80) as u8;
103            buffer[offset + 2] = ((code & 0x3f) | 0x80) as u8;
104        }
105        _ => {
106            buffer[offset] = (((code >> 18) & 0x07) | 0xf0) as u8;
107            buffer[offset + 1] = (((code >> 12) & 0x3f) | 0x80) as u8;
108            buffer[offset + 2] = (((code >> 6) & 0x3f) | 0x80) as u8;
109            buffer[offset + 3] = ((code & 0x3f) | 0x80) as u8;
110        }
111    }
112
113    length
114}
115
116/// Закодировать строку в `buffer` начиная с `offset`, вернуть новый offset
117/// (зеркало `encodeStringTo`).
118pub fn encode_string_to(buffer: &mut [u8], offset: usize, value: &str) -> usize {
119    let mut offset = offset;
120    for c in value.chars() {
121        offset += write_character(buffer, offset, c as u32);
122    }
123    offset
124}
125
126/// Закодировать строку в новый `Vec<u8>` (зеркало `encodeString`).
127///
128/// Upstream для длинных строк (>1000) делегирует в `TextEncoder` (стандартный UTF-8);
129/// ручная ветка для валидных строк даёт ровно те же байты, так что результат идентичен
130/// вне зависимости от длины.
131pub fn encode_string(value: &str) -> Vec<u8> {
132    let mut buffer = vec![0u8; string_length_in_bytes(value)];
133    encode_string_to(&mut buffer, 0, value);
134    buffer
135}
136
137/// Прочитать continuation-байт по индексу `index` (зеркало `continuationByte`).
138fn continuation_byte(buffer: &[u8], index: usize) -> Result<u32, Utf8DecodeError> {
139    if index >= buffer.len() {
140        return Err(Utf8DecodeError::InvalidByteIndex);
141    }
142
143    let continuation_byte = buffer[index];
144
145    if (continuation_byte & 0xC0) == 0x80 {
146        Ok((continuation_byte & 0x3F) as u32)
147    } else {
148        Err(Utf8DecodeError::InvalidContinuationByte)
149    }
150}
151
152/// Декодировать UTF-8 байты в строку (зеркало `decodeString`).
153///
154/// Ручной разбор сохранён намеренно (вместо `String::from_utf8`), чтобы повторить точную
155/// семантику ошибок upstream'а.
156pub fn decode_string(value: &[u8]) -> Result<String, Utf8DecodeError> {
157    let mut result = String::new();
158    let mut i = 0usize;
159
160    while i < value.len() {
161        let byte1 = value[i] as u32;
162        i += 1;
163        let code: u32;
164
165        if (byte1 & 0x80) == 0 {
166            code = byte1;
167        } else if (byte1 & 0xe0) == 0xc0 {
168            let byte2 = continuation_byte(value, i)?;
169            i += 1;
170            code = ((byte1 & 0x1f) << 6) | byte2;
171
172            if code < 0x80 {
173                return Err(Utf8DecodeError::InvalidContinuationByte);
174            }
175        } else if (byte1 & 0xf0) == 0xe0 {
176            let byte2 = continuation_byte(value, i)?;
177            i += 1;
178            let byte3 = continuation_byte(value, i)?;
179            i += 1;
180            code = ((byte1 & 0x0f) << 12) | (byte2 << 6) | byte3;
181
182            if code < 0x0800 {
183                return Err(Utf8DecodeError::InvalidContinuationByte);
184            }
185
186            if (0xd800..=0xdfff).contains(&code) {
187                return Err(Utf8DecodeError::LoneSurrogate(code));
188            }
189        } else if (byte1 & 0xf8) == 0xf0 {
190            let byte2 = continuation_byte(value, i)?;
191            i += 1;
192            let byte3 = continuation_byte(value, i)?;
193            i += 1;
194            let byte4 = continuation_byte(value, i)?;
195            i += 1;
196            code = ((byte1 & 0x0f) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
197
198            if code < 0x01_0000 || code > 0x10_ffff {
199                return Err(Utf8DecodeError::InvalidContinuationByte);
200            }
201        } else {
202            return Err(Utf8DecodeError::InvalidUtf8Detected);
203        }
204
205        // Upstream собирает UTF-16-строку (включая суррогатную пару для code > 0xFFFF).
206        // В Rust строка хранит scalar values, поэтому достаточно `char::from_u32`.
207        // Код к этому моменту уже провалидирован как валидный scalar value.
208        if let Some(ch) = char::from_u32(code) {
209            result.push(ch);
210        } else {
211            // Недостижимо для прошедших проверки кодов, но на всякий случай.
212            return Err(Utf8DecodeError::InvalidUtf8Detected);
213        }
214    }
215
216    Ok(result)
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn ascii_round_trip() {
225        let s = "Hello, World!";
226        let bytes = encode_string(s);
227        assert_eq!(bytes, s.as_bytes());
228        assert_eq!(decode_string(&bytes).unwrap(), s);
229    }
230
231    #[test]
232    fn empty_round_trip() {
233        let s = "";
234        let bytes = encode_string(s);
235        assert!(bytes.is_empty());
236        assert_eq!(string_length_in_bytes(s), 0);
237        assert_eq!(decode_string(&bytes).unwrap(), s);
238    }
239
240    #[test]
241    fn cyrillic_round_trip() {
242        let s = "Привет";
243        let bytes = encode_string(s);
244        // Each Cyrillic letter is 2 bytes in UTF-8 -> 6 chars * 2 = 12 bytes.
245        assert_eq!(bytes.len(), 12);
246        assert_eq!(string_length_in_bytes(s), 12);
247        // "Пр" = D0 9F D1 80
248        assert_eq!(&bytes[0..4], &[0xD0, 0x9F, 0xD1, 0x80]);
249        assert_eq!(bytes, s.as_bytes());
250        assert_eq!(decode_string(&bytes).unwrap(), s);
251    }
252
253    #[test]
254    fn three_byte_round_trip() {
255        let s = "あ"; // U+3042, 3 bytes
256        let bytes = encode_string(s);
257        assert_eq!(bytes, vec![0xE3, 0x81, 0x82]);
258        assert_eq!(string_length_in_bytes(s), 3);
259        assert_eq!(decode_string(&bytes).unwrap(), s);
260    }
261
262    #[test]
263    fn emoji_surrogate_pair_round_trip() {
264        let s = "😀"; // U+1F600, 4 bytes, surrogate pair in UTF-16
265        let bytes = encode_string(s);
266        assert_eq!(bytes, vec![0xF0, 0x9F, 0x98, 0x80]);
267        assert_eq!(string_length_in_bytes(s), 4);
268        assert_eq!(decode_string(&bytes).unwrap(), s);
269    }
270
271    #[test]
272    fn mixed_round_trip() {
273        let s = "aЯ あ😀z";
274        let bytes = encode_string(s);
275        assert_eq!(bytes, s.as_bytes());
276        assert_eq!(string_length_in_bytes(s), s.len());
277        assert_eq!(decode_string(&bytes).unwrap(), s);
278    }
279
280    #[test]
281    fn decode_rejects_lone_surrogate() {
282        // ED A0 80 -> U+D800, a lone surrogate (upstream throws).
283        let err = decode_string(&[0xED, 0xA0, 0x80]).unwrap_err();
284        assert_eq!(err, Utf8DecodeError::LoneSurrogate(0xD800));
285    }
286
287    #[test]
288    fn decode_rejects_overlong_two_byte() {
289        // C0 80 would decode to U+0000 (overlong) -> code < 0x80.
290        let err = decode_string(&[0xC0, 0x80]).unwrap_err();
291        assert_eq!(err, Utf8DecodeError::InvalidContinuationByte);
292    }
293
294    #[test]
295    fn decode_rejects_bad_continuation() {
296        // C2 followed by a non-continuation byte.
297        let err = decode_string(&[0xC2, 0x20]).unwrap_err();
298        assert_eq!(err, Utf8DecodeError::InvalidContinuationByte);
299    }
300
301    #[test]
302    fn decode_rejects_truncated() {
303        // E3 81 missing third byte -> index out of range.
304        let err = decode_string(&[0xE3, 0x81]).unwrap_err();
305        assert_eq!(err, Utf8DecodeError::InvalidByteIndex);
306    }
307
308    #[test]
309    fn decode_rejects_invalid_lead() {
310        let err = decode_string(&[0xFF]).unwrap_err();
311        assert_eq!(err, Utf8DecodeError::InvalidUtf8Detected);
312    }
313}