Skip to main content

ag_psd/
engine_data.rs

1/*
2File: crates/ag-psd/src/engine_data.rs
3
4Purpose:
5парсер и сериализатор Engine Data (структура текстовых движков Photoshop).
6
7Source compatibility:
8- порт upstream-файла `test/ag-psd/src/engineData.ts` (разбиение 1:1).
9
10Main responsibilities:
11- зеркалировать соответствующий upstream-модуль при портировании;
12- держать публичный контракт этого участка в одном месте.
13
14Mapping TS -> Rust:
15- `parseEngineData(data)`            -> `parse_engine_data(data: &[u8]) -> Result<EngineValue, EngineDataError>`
16- `serializeEngineData(data, cond?)` -> `serialize_engine_data(data: &EngineValue, condensed: bool) -> Vec<u8>`
17
18Модель значения EngineData (`EngineValue`):
19Upstream хранит распарсенные данные как нетипизированный JS-объект. Значениями могут быть:
20`null`, число (всегда `number`), булево, строка (как обычный текст в `( )`, так и
21«имя» вида `/Name` — в JS это просто строка, начинающаяся с `/`), массив и словарь.
22Словари ДОЛЖНЫ сохранять порядок ключей вставки, потому что сериализатор обходит
23`Object.keys()` в порядке вставки (с одной поправкой `getKeys`, переносящей `99`/`98`
24в начало) и побайтовый вывод сверяется Photoshop'ом. Поэтому `Dict` хранится как
25`Vec<(String, EngineValue)>` (insertion-ordered), а не как `HashMap`.
26
27Строки моделируются единым вариантом `EngineValue::Str(String)`: «имена» (`/Name`)
28в upstream'е — это обычные строки, начинающиеся с `/`, и сериализатор отличает их
29только по первому символу `/` (для ключей `98`/`99`). Отдельный вариант `Name` создал
30бы расхождение с TS-семантикой, поэтому мы держим строку «как есть», включая ведущий `/`.
31
32Замечание о точности строк:
33Upstream работает на UTF-16 code units (`charCodeAt`/`String.fromCharCode`). Здесь текст
34хранится как Rust `String` и конвертируется через `encode_utf16`/`from_utf16`, что
35побайтово эквивалентно для любого валидного Unicode (типичный случай PSD-текста).
36Одиночные суррогаты в `&str` невозможны.
37*/
38
39use std::fmt;
40
41/// Ошибка парсинга EngineData (зеркало `throw new Error(...)` в upstream'е).
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct EngineDataError(pub String);
44
45impl fmt::Display for EngineDataError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "{}", self.0)
48    }
49}
50
51impl std::error::Error for EngineDataError {}
52
53/// Распарсенное значение EngineData. См. обоснование модели в шапке файла.
54#[derive(Debug, Clone, PartialEq)]
55pub enum EngineValue {
56    /// `null` (в upstream'е — JS `null`).
57    Null,
58    /// Число. Upstream всегда хранит `number` (f64).
59    Number(f64),
60    /// Булево значение.
61    Bool(bool),
62    /// Строка. «Имена» хранятся как строка с ведущим `/` (например `/Name`).
63    Str(String),
64    /// Массив значений.
65    Array(Vec<EngineValue>),
66    /// Словарь `<< /Key value >>` с сохранением порядка ключей.
67    Dict(Vec<(String, EngineValue)>),
68}
69
70impl EngineValue {
71    pub fn is_object(&self) -> bool {
72        // Зеркало `typeof value === 'object'` в JS для null/array/dict
73        // (используется только там, где это влияет на ветвление).
74        matches!(self, EngineValue::Dict(_) | EngineValue::Array(_) | EngineValue::Null)
75    }
76}
77
78// ' ', '\n', '\r', '\t'
79fn is_whitespace(char: u8) -> bool {
80    char == 32 || char == 10 || char == 13 || char == 9
81}
82
83// 0123456789.-
84fn is_number(char: u8) -> bool {
85    (48..=57).contains(&char) || char == 46 || char == 45
86}
87
88// Внутренний узел стека парсера: либо контейнер (значение), либо имя ключа (строка).
89enum StackItem {
90    Value(EngineValue),
91    Key(String),
92}
93
94/// Порт `parseEngineData`. Принимает байты блоба, возвращает корневое значение.
95pub fn parse_engine_data(data: &[u8]) -> Result<EngineValue, EngineDataError> {
96    let mut index: usize = 0;
97
98    // --- helpers, оперирующие над `data`/`index` ---
99
100    fn skip_whitespace(data: &[u8], index: &mut usize) {
101        while *index < data.len() && is_whitespace(data[*index]) {
102            *index += 1;
103        }
104    }
105
106    fn get_text_byte(data: &[u8], index: &mut usize) -> u8 {
107        let mut byte = data[*index];
108        *index += 1;
109
110        if byte == 92 {
111            // \
112            byte = data[*index];
113            *index += 1;
114        }
115
116        byte
117    }
118
119    fn get_text(data: &[u8], index: &mut usize) -> Result<String, EngineDataError> {
120        let mut units: Vec<u16> = Vec::new();
121
122        if data[*index] == 41 {
123            // )
124            *index += 1;
125            return Ok(String::new());
126        }
127
128        // Strings start with utf-16 BOM
129        if data.get(*index) != Some(&0xFE) || data.get(*index + 1) != Some(&0xFF) {
130            return Err(EngineDataError("Invalid utf-16 BOM".to_string()));
131        }
132
133        *index += 2;
134
135        // ), ( and \ characters are escaped in ascii manner, remove the escapes before
136        // interpreting the bytes as utf-16
137        while *index < data.len() && data[*index] != 41 {
138            // )
139            let high = get_text_byte(data, index) as u16;
140            let low = get_text_byte(data, index) as u16;
141            let char = (high << 8) | low;
142            units.push(char);
143        }
144
145        *index += 1;
146        Ok(String::from_utf16_lossy(&units))
147    }
148
149    // Корень и стек. В отличие от TS (где объекты — ссылки, и контейнер одновременно
150    // лежит в родителе и на стеке), здесь контейнеры ЖИВУТ на стеке до закрытия, а при
151    // `pop` присоединяются к родителю. Семантически это эквивалентно: TS присоединяет
152    // контейнер в родителя в момент открытия и затем мутирует через алиас, мы — в момент
153    // закрытия. Итоговая структура та же.
154    let mut root: Option<EngineValue> = None;
155    let mut stack: Vec<StackItem> = Vec::new();
156
157    fn set_dict(map: &mut Vec<(String, EngineValue)>, key: &str, value: EngineValue) {
158        // Семантика `obj[key] = value`: перезапись по существующему ключу с
159        // сохранением позиции, иначе вставка в конец.
160        if let Some(slot) = map.iter_mut().find(|(k, _)| k == key) {
161            slot.1 = value;
162        } else {
163            map.push((key.to_string(), value));
164        }
165    }
166
167    // pushValue: помещает значение в текущий контекст (объект-ключ или массив).
168    fn push_value(
169        stack: &mut Vec<StackItem>,
170        _root: &mut Option<EngineValue>,
171        value: EngineValue,
172    ) -> Result<(), EngineDataError> {
173        let top = stack.last().ok_or_else(|| EngineDataError("Invalid data".to_string()))?;
174
175        match top {
176            StackItem::Key(_) => {
177                // top — строка-ключ; снимаем её и кладём значение в объект под ней.
178                let key = match stack.pop().unwrap() {
179                    StackItem::Key(k) => k,
180                    _ => unreachable!(),
181                };
182                match stack.last_mut() {
183                    Some(StackItem::Value(EngineValue::Dict(map))) => {
184                        set_dict(map, &key, value);
185                        Ok(())
186                    }
187                    _ => Err(EngineDataError("Invalid data".to_string())),
188                }
189            }
190            StackItem::Value(EngineValue::Array(_)) => {
191                if let Some(StackItem::Value(EngineValue::Array(arr))) = stack.last_mut() {
192                    arr.push(value);
193                }
194                Ok(())
195            }
196            _ => Err(EngineDataError("Invalid data".to_string())),
197        }
198    }
199
200    // pushContainer: открывает новый контейнер. Кладём его кадром на стек; присоединение
201    // к родителю произойдёт в `pop`.
202    fn push_container(
203        stack: &mut Vec<StackItem>,
204        _root: &mut Option<EngineValue>,
205        value: EngineValue,
206    ) {
207        stack.push(StackItem::Value(value));
208    }
209
210    // pushProperty
211    fn push_property(
212        stack: &mut Vec<StackItem>,
213        root: &mut Option<EngineValue>,
214        name: &str,
215    ) -> Result<(), EngineDataError> {
216        if stack.is_empty() {
217            push_container(stack, root, EngineValue::Dict(Vec::new()));
218        }
219
220        match stack.last() {
221            Some(StackItem::Key(_)) => {
222                // top — строка: трактуем имя как значение.
223                if name == "nil" {
224                    push_value(stack, root, EngineValue::Null)
225                } else {
226                    push_value(stack, root, EngineValue::Str(format!("/{}", name)))
227                }
228            }
229            Some(StackItem::Value(_)) => {
230                // top — объект/массив: имя становится ключом.
231                stack.push(StackItem::Key(name.to_string()));
232                Ok(())
233            }
234            None => Err(EngineDataError("Invalid data".to_string())),
235        }
236    }
237
238    // pop: закрывает текущий контейнер. Снимает кадр со стека и присоединяет его значение
239    // к родителю (либо делает его корнем, если стек опустел).
240    fn pop(
241        stack: &mut Vec<StackItem>,
242        root: &mut Option<EngineValue>,
243    ) -> Result<(), EngineDataError> {
244        let item = stack.pop().ok_or_else(|| EngineDataError("Invalid data".to_string()))?;
245        let value = match item {
246            StackItem::Value(v) => v,
247            StackItem::Key(_) => return Err(EngineDataError("Invalid data".to_string())),
248        };
249
250        if stack.is_empty() {
251            *root = Some(value);
252            Ok(())
253        } else {
254            push_value(stack, root, value)
255        }
256    }
257
258    skip_whitespace(data, &mut index);
259
260    let mut data_length = data.len();
261
262    while data_length > 0 && data[data_length - 1] == 0 {
263        data_length -= 1; // trim 0 bytes from end
264    }
265
266    while index < data_length {
267        let i = index;
268        let char = data[i];
269
270        if char == 60 && data.get(i + 1) == Some(&60) {
271            // <<
272            index += 2;
273            push_container(&mut stack, &mut root, EngineValue::Dict(Vec::new()));
274        } else if char == 62 && data.get(i + 1) == Some(&62) {
275            // >>
276            index += 2;
277            pop(&mut stack, &mut root)?;
278        } else if char == 47 {
279            // /
280            index += 1;
281            let start = index;
282
283            while index < data.len() && !is_whitespace(data[index]) {
284                index += 1;
285            }
286
287            let mut name = String::new();
288            for j in start..index {
289                name.push(data[j] as char);
290            }
291
292            push_property(&mut stack, &mut root, &name)?;
293        } else if char == 40 {
294            // (
295            index += 1;
296            let text = get_text(data, &mut index)?;
297            push_value(&mut stack, &mut root, EngineValue::Str(text))?;
298        } else if char == 91 {
299            // [
300            index += 1;
301            push_container(&mut stack, &mut root, EngineValue::Array(Vec::new()));
302        } else if char == 93 {
303            // ]
304            index += 1;
305            pop(&mut stack, &mut root)?;
306        } else if char == 110
307            && data.get(i + 1) == Some(&117)
308            && data.get(i + 2) == Some(&108)
309            && data.get(i + 3) == Some(&108)
310        {
311            // null
312            index += 4;
313            push_value(&mut stack, &mut root, EngineValue::Null)?;
314        } else if char == 116
315            && data.get(i + 1) == Some(&114)
316            && data.get(i + 2) == Some(&117)
317            && data.get(i + 3) == Some(&101)
318        {
319            // true
320            index += 4;
321            push_value(&mut stack, &mut root, EngineValue::Bool(true))?;
322        } else if char == 102
323            && data.get(i + 1) == Some(&97)
324            && data.get(i + 2) == Some(&108)
325            && data.get(i + 3) == Some(&115)
326            && data.get(i + 4) == Some(&101)
327        {
328            // false
329            index += 5;
330            push_value(&mut stack, &mut root, EngineValue::Bool(false))?;
331        } else if is_number(char) {
332            let mut value = String::new();
333
334            while index < data.len() && is_number(data[index]) {
335                value.push(data[index] as char);
336                index += 1;
337            }
338
339            let parsed = parse_float(&value);
340            push_value(&mut stack, &mut root, EngineValue::Number(parsed))?;
341        } else {
342            index += 1;
343            // Зеркало `console.log` (невалидный токен пропускается, не бросается).
344            eprintln!(
345                "Invalid token '{}' ({}) at {}",
346                char as char, char, index
347            );
348        }
349
350        skip_whitespace(data, &mut index);
351    }
352
353    // В TS корень фиксируется при первом pushContainer. У нас контейнеры присоединяются
354    // при `pop`, поэтому верхнеуровневый словарь condensed-формата (без обёртки `<< >>`)
355    // остаётся на дне стека незакрытым — берём его как корень.
356    if root.is_none() {
357        if let Some(StackItem::Value(v)) = stack.into_iter().next() {
358            root = Some(v);
359        }
360    }
361
362    Ok(root.unwrap_or(EngineValue::Null))
363}
364
365// Зеркало JS `parseFloat`: парсит ведущий числовой префикс, иначе NaN.
366fn parse_float(value: &str) -> f64 {
367    let s = value.trim_start();
368    // Найти максимальный валидный префикс f64.
369    let mut end = 0;
370    let bytes = s.as_bytes();
371    let mut seen_dot = false;
372    let mut i = 0;
373    if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
374        i += 1;
375    }
376    while i < bytes.len() {
377        let c = bytes[i];
378        if c.is_ascii_digit() {
379            end = i + 1;
380            i += 1;
381        } else if c == b'.' && !seen_dot {
382            seen_dot = true;
383            i += 1;
384        } else {
385            break;
386        }
387    }
388    if end == 0 {
389        f64::NAN
390    } else {
391        s[..end].parse::<f64>().unwrap_or(f64::NAN)
392    }
393}
394
395const FLOAT_KEYS: &[&str] = &[
396    "Axis", "XY", "Zone", "WordSpacing", "FirstLineIndent", "GlyphSpacing", "StartIndent",
397    "EndIndent", "SpaceBefore", "SpaceAfter", "LetterSpacing", "Values", "GridSize",
398    "GridLeading", "PointBase", "BoxBounds", "TransformPoint0", "TransformPoint1",
399    "TransformPoint2", "FontSize", "Leading", "HorizontalScale", "VerticalScale",
400    "BaselineShift", "Tsume", "OutlineWidth", "AutoLeading",
401];
402
403const INT_ARRAYS: &[&str] = &["RunLengthArray"];
404
405// serializeInt
406fn serialize_int(value: f64) -> String {
407    // JS `value.toString()` для целочисленных значений.
408    js_number_to_string(value)
409}
410
411// serializeFloat: toFixed(5) + три regex-замены.
412fn serialize_float(value: f64) -> String {
413    let mut s = format!("{:.5}", value);
414
415    // .replace(/(\d)0+$/g, '$1') — убрать хвостовые нули после ненулевой цифры.
416    s = replace_trailing_zeros(&s);
417    // .replace(/^0+\.([1-9])/g, '.$1')
418    s = replace_leading_zero_dot(&s);
419    // .replace(/^-0+\.0(\d)/g, '-.0$1')
420    s = replace_neg_zero_dot(&s);
421
422    s
423}
424
425// /(\d)0+$/ -> $1 : находит САМУЮ ЛЕВУЮ цифру `p`, после которой до конца строки идут
426// только нули (>=1), и оставляет строку по `p` включительно (отбрасывает хвост нулей).
427// Это в точности семантика глобальной замены leftmost-greedy у движка JS-regex.
428fn replace_trailing_zeros(s: &str) -> String {
429    let bytes = s.as_bytes();
430    let n = bytes.len();
431    for p in 0..n {
432        if bytes[p].is_ascii_digit() && p + 1 < n && bytes[p + 1..].iter().all(|&c| c == b'0') {
433            return s[..=p].to_string();
434        }
435    }
436    s.to_string()
437}
438
439// /^0+\.([1-9])/ -> .$1 : ведущие нули + точка + цифра 1-9 -> точка + цифра.
440fn replace_leading_zero_dot(s: &str) -> String {
441    let bytes = s.as_bytes();
442    let mut i = 0;
443    while i < bytes.len() && bytes[i] == b'0' {
444        i += 1;
445    }
446    // Нужен хотя бы один ведущий ноль, далее '.', далее [1-9].
447    if i >= 1
448        && i + 1 < bytes.len()
449        && bytes[i] == b'.'
450        && (b'1'..=b'9').contains(&bytes[i + 1])
451    {
452        format!(".{}", &s[i + 1..])
453    } else {
454        s.to_string()
455    }
456}
457
458// /^-0+\.0(\d)/ -> -.0$1 : '-' + нули + ".0" + цифра -> "-.0" + цифра.
459fn replace_neg_zero_dot(s: &str) -> String {
460    let bytes = s.as_bytes();
461    if bytes.first() != Some(&b'-') {
462        return s.to_string();
463    }
464    let mut i = 1;
465    while i < bytes.len() && bytes[i] == b'0' {
466        i += 1;
467    }
468    // Нужен хотя бы один ноль после '-', затем ".0", затем цифра.
469    if i >= 2
470        && i + 2 < bytes.len()
471        && bytes[i] == b'.'
472        && bytes[i + 1] == b'0'
473        && bytes[i + 2].is_ascii_digit()
474    {
475        format!("-.0{}", &s[i + 2..])
476    } else {
477        s.to_string()
478    }
479}
480
481// serializeNumber
482fn serialize_number(value: f64, key: Option<&str>) -> String {
483    // (key && floatKeys.indexOf(key) !== -1) || (value | 0) !== value
484    let is_float = key.map(|k| FLOAT_KEYS.contains(&k)).unwrap_or(false) || (to_int32(value) as f64) != value;
485    if is_float {
486        serialize_float(value)
487    } else {
488        serialize_int(value)
489    }
490}
491
492// JS `value | 0` — приведение к int32 через ToInt32.
493fn to_int32(value: f64) -> i32 {
494    if !value.is_finite() {
495        return 0;
496    }
497    let n = value.trunc();
498    let m = n.rem_euclid(4294967296.0); // 2^32
499    let u = m as u64 as u32;
500    u as i32
501}
502
503// JS Number.toString() для значений, которые мы выводим как целые.
504fn js_number_to_string(value: f64) -> String {
505    if value == value.trunc() && value.is_finite() && value.abs() < 1e21 {
506        // целое
507        format!("{}", value as i64)
508    } else {
509        // не должно достигаться для serialize_int (вызывается только для целых),
510        // но оставим разумный fallback.
511        let s = format!("{}", value);
512        s
513    }
514}
515
516// getKeys: воспроизводит порядок ключей upstream'а, включая баг с двойным переносом '99'.
517fn get_keys(map: &[(String, EngineValue)]) -> Vec<String> {
518    let mut keys: Vec<String> = map.iter().map(|(k, _)| k.clone()).collect();
519
520    // if (keys.indexOf('98') !== -1) keys.unshift(...keys.splice(keys.indexOf('99'), 1));
521    if keys.iter().any(|k| k == "98") {
522        if let Some(pos) = keys.iter().position(|k| k == "99") {
523            let removed = keys.remove(pos);
524            keys.insert(0, removed);
525        }
526        // Если '99' нет — splice(-1, 1) удалил бы последний элемент и перенёс его в начало.
527        else if !keys.is_empty() {
528            let removed = keys.remove(keys.len() - 1);
529            keys.insert(0, removed);
530        }
531    }
532
533    // if (keys.indexOf('99') !== -1) keys.unshift(...keys.splice(keys.indexOf('99'), 1));
534    if let Some(pos) = keys.iter().position(|k| k == "99") {
535        let removed = keys.remove(pos);
536        keys.insert(0, removed);
537    }
538
539    keys
540}
541
542/// Порт `serializeEngineData`. Возвращает байты блоба.
543pub fn serialize_engine_data(data: &EngineValue, condensed: bool) -> Vec<u8> {
544    let mut buffer: Vec<u8> = Vec::with_capacity(1024);
545    let mut indent: usize = 0;
546
547    serialize_engine_data_inner(data, condensed, &mut buffer, &mut indent);
548
549    buffer
550}
551
552fn write_str(buffer: &mut Vec<u8>, value: &str) {
553    // writeString: пишет младший байт каждой UTF-16 code unit (charCodeAt & 0xff
554    // фактически — но upstream пишет charCodeAt целиком в Uint8Array, что усекает
555    // до байта). Для ASCII-литералов это эквивалентно прямой записи байтов.
556    for unit in value.encode_utf16() {
557        buffer.push((unit & 0xff) as u8);
558    }
559}
560
561fn write_indent(buffer: &mut Vec<u8>, condensed: bool, indent: usize) {
562    if condensed {
563        write_str(buffer, " ");
564    } else {
565        for _ in 0..indent {
566            write_str(buffer, "\t");
567        }
568    }
569}
570
571fn write_string_byte(buffer: &mut Vec<u8>, value: u8) {
572    if value == 40 || value == 41 || value == 92 {
573        // ( ) \
574        buffer.push(92); // \
575    }
576    buffer.push(value);
577}
578
579fn serialize_engine_data_inner(
580    data: &EngineValue,
581    condensed: bool,
582    buffer: &mut Vec<u8>,
583    indent: &mut usize,
584) {
585    if condensed {
586        if let EngineValue::Dict(map) = data {
587            for key in get_keys(map) {
588                let value = map.iter().find(|(k, _)| *k == key).map(|(_, v)| v).unwrap();
589                write_property(&key, value, condensed, buffer, indent);
590            }
591        }
592        // (если data — массив/число/строка в condensed-режиме, upstream ничего не пишет,
593        //  т.к. `typeof data === 'object'` для массива true — но тогда for...in по
594        //  числовым индексам; на практике корень EngineData всегда объект.)
595        // Для точности: массив в condensed обрабатывается как объект с числовыми ключами.
596        else if let EngineValue::Array(arr) = data {
597            // Object.keys массива — это строковые индексы "0", "1", ...
598            for (i, value) in arr.iter().enumerate() {
599                let key = i.to_string();
600                write_property(&key, value, condensed, buffer, indent);
601            }
602        }
603    } else {
604        write_str(buffer, "\n\n");
605        write_value(data, None, false, condensed, buffer, indent);
606    }
607}
608
609fn write_property(
610    key: &str,
611    value: &EngineValue,
612    condensed: bool,
613    buffer: &mut Vec<u8>,
614    indent: &mut usize,
615) {
616    write_indent(buffer, condensed, *indent);
617    write_str(buffer, &format!("/{}", key));
618    write_value(value, Some(key), true, condensed, buffer, indent);
619    if !condensed {
620        write_str(buffer, "\n");
621    }
622}
623
624fn write_value(
625    value: &EngineValue,
626    key: Option<&str>,
627    in_property: bool,
628    condensed: bool,
629    buffer: &mut Vec<u8>,
630    indent: &mut usize,
631) {
632    // writePrefix
633    fn write_prefix(in_property: bool, condensed: bool, buffer: &mut Vec<u8>, indent: usize) {
634        if in_property {
635            write_str(buffer, " ");
636        } else {
637            write_indent(buffer, condensed, indent);
638        }
639    }
640
641    match value {
642        EngineValue::Null => {
643            write_prefix(in_property, condensed, buffer, *indent);
644            write_str(buffer, if condensed { "/nil" } else { "null" });
645        }
646        EngineValue::Number(n) => {
647            write_prefix(in_property, condensed, buffer, *indent);
648            write_str(buffer, &serialize_number(*n, key));
649        }
650        EngineValue::Bool(b) => {
651            write_prefix(in_property, condensed, buffer, *indent);
652            write_str(buffer, if *b { "true" } else { "false" });
653        }
654        EngineValue::Str(s) => {
655            write_prefix(in_property, condensed, buffer, *indent);
656
657            let is_name = (key == Some("99") || key == Some("98")) && s.starts_with('/');
658            if is_name {
659                write_str(buffer, s);
660            } else {
661                write_str(buffer, "(");
662                buffer.push(0xfe);
663                buffer.push(0xff);
664
665                for code in s.encode_utf16() {
666                    write_string_byte(buffer, ((code >> 8) & 0xff) as u8);
667                    write_string_byte(buffer, (code & 0xff) as u8);
668                }
669
670                write_str(buffer, ")");
671            }
672        }
673        EngineValue::Array(arr) => {
674            write_prefix(in_property, condensed, buffer, *indent);
675
676            let all_numbers = arr.iter().all(|x| matches!(x, EngineValue::Number(_)));
677            if all_numbers {
678                write_str(buffer, "[");
679
680                let int_array = key.map(|k| INT_ARRAYS.contains(&k)).unwrap_or(false);
681
682                for x in arr {
683                    if let EngineValue::Number(n) = x {
684                        write_str(buffer, " ");
685                        let s = if int_array {
686                            serialize_number(*n, None)
687                        } else {
688                            serialize_float(*n)
689                        };
690                        write_str(buffer, &s);
691                    }
692                }
693
694                write_str(buffer, " ]");
695            } else {
696                write_str(buffer, "[");
697                if !condensed {
698                    write_str(buffer, "\n");
699                }
700
701                for x in arr {
702                    write_value(x, key, false, condensed, buffer, indent);
703                    if !condensed {
704                        write_str(buffer, "\n");
705                    }
706                }
707
708                write_indent(buffer, condensed, *indent);
709                write_str(buffer, "]");
710            }
711        }
712        EngineValue::Dict(map) => {
713            if in_property && !condensed {
714                write_str(buffer, "\n");
715            }
716
717            write_indent(buffer, condensed, *indent);
718            write_str(buffer, "<<");
719
720            if !condensed {
721                write_str(buffer, "\n");
722            }
723
724            *indent += 1;
725
726            for k in get_keys(map) {
727                let v = map.iter().find(|(kk, _)| *kk == k).map(|(_, v)| v).unwrap();
728                write_property(&k, v, condensed, buffer, indent);
729            }
730
731            *indent -= 1;
732            write_indent(buffer, condensed, *indent);
733            write_str(buffer, ">>");
734        }
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    fn dict(pairs: Vec<(&str, EngineValue)>) -> EngineValue {
743        EngineValue::Dict(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
744    }
745
746    fn sample() -> EngineValue {
747        dict(vec![
748            (
749                "EngineDict",
750                dict(vec![
751                    (
752                        "Editor",
753                        dict(vec![("Text", EngineValue::Str("Привет ❤".to_string()))]),
754                    ),
755                    (
756                        "RunLengthArray",
757                        EngineValue::Array(vec![
758                            EngineValue::Number(3.0),
759                            EngineValue::Number(5.0),
760                        ]),
761                    ),
762                    (
763                        "Values",
764                        EngineValue::Array(vec![
765                            EngineValue::Number(0.0),
766                            EngineValue::Number(0.5),
767                            EngineValue::Number(1.0),
768                        ]),
769                    ),
770                    ("FontSize", EngineValue::Number(12.0)),
771                    ("AntiAlias", EngineValue::Number(4.0)),
772                    ("UseFractionalGlyphWidths", EngineValue::Bool(true)),
773                    ("Nothing", EngineValue::Null),
774                ]),
775            ),
776            (
777                "ResourceDict",
778                dict(vec![(
779                    "Nested",
780                    dict(vec![("Flag", EngineValue::Bool(false))]),
781                )]),
782            ),
783        ])
784    }
785
786    #[test]
787    fn round_trip_full() {
788        let value = sample();
789        let bytes = serialize_engine_data(&value, false);
790        let parsed = parse_engine_data(&bytes).unwrap();
791        assert_eq!(parsed, value);
792    }
793
794    #[test]
795    fn round_trip_condensed() {
796        let value = sample();
797        let bytes = serialize_engine_data(&value, true);
798        let parsed = parse_engine_data(&bytes).unwrap();
799        assert_eq!(parsed, value);
800    }
801
802    #[test]
803    fn condensed_byte_exact() {
804        // Маленький проверяемый вручную случай.
805        let value = dict(vec![
806            ("A", EngineValue::Number(1.0)),
807            ("B", EngineValue::Bool(true)),
808            ("C", EngineValue::Number(12.0)),
809            ("D", EngineValue::Null),
810        ]);
811        let bytes = serialize_engine_data(&value, true);
812        // condensed: " /A 1 /B true /C 12 /D /nil"
813        let expected = b" /A 1 /B true /C 12 /D /nil".to_vec();
814        assert_eq!(bytes, expected);
815    }
816
817    #[test]
818    fn condensed_string_byte_exact() {
819        // Строка "AB" в condensed: " /T (<FE><FF>\0A\0B)"
820        let value = dict(vec![("T", EngineValue::Str("AB".to_string()))]);
821        let bytes = serialize_engine_data(&value, true);
822        let mut expected: Vec<u8> = Vec::new();
823        expected.extend_from_slice(b" /T (");
824        expected.push(0xfe);
825        expected.push(0xff);
826        expected.extend_from_slice(&[0x00, b'A', 0x00, b'B']);
827        expected.push(b')');
828        assert_eq!(bytes, expected);
829    }
830
831    #[test]
832    fn condensed_string_escapes() {
833        // Скобки и обратный слэш в строке должны экранироваться.
834        let value = dict(vec![("T", EngineValue::Str("()\\".to_string()))]);
835        let bytes = serialize_engine_data(&value, true);
836        let mut expected: Vec<u8> = Vec::new();
837        expected.extend_from_slice(b" /T (");
838        expected.push(0xfe);
839        expected.push(0xff);
840        // '(' = 0x28 high 0x00 low 0x28 -> 0x00, '\(' (escaped 0x28)
841        expected.extend_from_slice(&[0x00, 0x5c, 0x28]); // 0x00 then \(
842        expected.extend_from_slice(&[0x00, 0x5c, 0x29]); // 0x00 then \)
843        expected.extend_from_slice(&[0x00, 0x5c, 0x5c]); // 0x00 then \\
844        expected.push(b')');
845        assert_eq!(bytes, expected);
846    }
847
848    #[test]
849    fn non_condensed_byte_exact() {
850        // { A: 1, B: { C: 2 } } в не-condensed форме.
851        let value = dict(vec![
852            ("A", EngineValue::Number(1.0)),
853            ("B", dict(vec![("C", EngineValue::Number(2.0))])),
854        ]);
855        let bytes = serialize_engine_data(&value, false);
856        // \n\n<<\n\t/A 1\n\t/B\n\t<<\n\t\t/C 2\n\t>>\n>>
857        let expected = "\n\n<<\n\t/A 1\n\t/B\n\t<<\n\t\t/C 2\n\t>>\n>>";
858        assert_eq!(String::from_utf8(bytes).unwrap(), expected);
859    }
860
861    #[test]
862    fn float_formatting() {
863        assert_eq!(serialize_float(0.5), ".5");
864        assert_eq!(serialize_float(1.0), "1.0");
865        assert_eq!(serialize_float(12.25), "12.25");
866        assert_eq!(serialize_float(-0.05), "-.05");
867        assert_eq!(serialize_float(100.0), "100.0");
868    }
869
870    #[test]
871    fn number_int_vs_float() {
872        // FontSize в floatKeys -> float-формат ("12.00000" -> "12.0").
873        assert_eq!(serialize_number(12.0, Some("FontSize")), "12.0");
874        // Обычный ключ, целое значение -> int.
875        assert_eq!(serialize_number(12.0, Some("AntiAlias")), "12");
876        // Нецелое без ключа -> float.
877        assert_eq!(serialize_number(0.5, None), ".5");
878    }
879
880    #[test]
881    fn get_keys_99_first() {
882        let map = vec![
883            ("0".to_string(), EngineValue::Number(1.0)),
884            ("99".to_string(), EngineValue::Str("/Type".to_string())),
885            ("1".to_string(), EngineValue::Number(2.0)),
886        ];
887        assert_eq!(get_keys(&map), vec!["99", "0", "1"]);
888    }
889}