Skip to main content

openbim_step/
escape.rs

1//! STEP string-literal escape decoding and encoding.
2//!
3//! Supported forms are doubled apostrophes, doubled backslashes, `\N\`, `\F\`,
4//! `\S\`, `\X\`, UTF-16BE `\X2\...\X0\`, and UTF-32BE `\X4\...\X0\`.
5
6/// Decodes a STEP string literal body to Unicode.
7///
8/// Unknown or malformed escapes are preserved verbatim instead of discarded.
9#[must_use]
10pub fn decode(raw: &[u8]) -> String {
11    let mut output = String::with_capacity(raw.len());
12    let mut position = 0;
13    let mut alphabet = b'A';
14    while position < raw.len() {
15        match raw[position] {
16            b'\'' if raw.get(position + 1) == Some(&b'\'') => {
17                output.push('\'');
18                position += 2;
19            }
20            b'\'' => {
21                output.push('\'');
22                position += 1;
23            }
24            b'\\' => {
25                if let Some(consumed) = decode_escape(&raw[position..], &mut output, &mut alphabet)
26                {
27                    position += consumed;
28                } else {
29                    output.push('\\');
30                    position += 1;
31                }
32            }
33            _ => {
34                let start = position;
35                while !matches!(raw.get(position), None | Some(b'\'' | b'\\')) {
36                    position += 1;
37                }
38                output.push_str(&String::from_utf8_lossy(&raw[start..position]));
39            }
40        }
41    }
42    output
43}
44
45fn decode_escape(input: &[u8], output: &mut String, alphabet: &mut u8) -> Option<usize> {
46    match input.get(1)? {
47        b'\\' => {
48            output.push('\\');
49            Some(2)
50        }
51        b'N' | b'n' | b'F' | b'f' if input.get(2) == Some(&b'\\') => Some(3),
52        b'S' | b's' if input.get(2) == Some(&b'\\') => {
53            let byte = input.get(3)?.wrapping_add(128);
54            output.push(decode_alphabet_byte(*alphabet, byte));
55            Some(4)
56        }
57        b'P' | b'p'
58            if matches!(input.get(2), Some(b'A'..=b'I' | b'a'..=b'i'))
59                && input.get(3) == Some(&b'\\') =>
60        {
61            *alphabet = input[2].to_ascii_uppercase();
62            Some(4)
63        }
64        b'X' | b'x' => match input.get(2)? {
65            b'\\' => {
66                let byte = u8::try_from(parse_hex(input.get(3..5)?)?).ok()?;
67                output.push(char::from(byte));
68                Some(5)
69            }
70            b'2' => {
71                decode_wide(input, output, 4).or_else(|| preserve_malformed_wide(input, output))
72            }
73            b'4' => {
74                decode_wide(input, output, 8).or_else(|| preserve_malformed_wide(input, output))
75            }
76            _ => None,
77        },
78        _ => None,
79    }
80}
81
82fn decode_alphabet_byte(alphabet: u8, byte: u8) -> char {
83    if alphabet == b'A' {
84        return char::from(byte);
85    }
86    let encoding = match alphabet {
87        b'B' => encoding_rs::ISO_8859_2,
88        b'C' => encoding_rs::ISO_8859_3,
89        b'D' => encoding_rs::ISO_8859_4,
90        b'E' => encoding_rs::ISO_8859_5,
91        b'F' => encoding_rs::ISO_8859_6,
92        b'G' => encoding_rs::ISO_8859_7,
93        b'H' => encoding_rs::ISO_8859_8,
94        b'I' => encoding_rs::WINDOWS_1254,
95        _ => encoding_rs::WINDOWS_1252,
96    };
97    let bytes = [byte];
98    let (decoded, _, _) = encoding.decode(&bytes);
99    decoded.chars().next().unwrap_or('\u{fffd}')
100}
101
102fn parse_hex(input: &[u8]) -> Option<u32> {
103    u32::from_str_radix(std::str::from_utf8(input).ok()?, 16).ok()
104}
105
106fn preserve_malformed_wide(input: &[u8], output: &mut String) -> Option<usize> {
107    let end = input
108        .get(4..)?
109        .windows(4)
110        .position(|window| window.eq_ignore_ascii_case(b"\\X0\\"))?;
111    let consumed = 4 + end + 4;
112    output.push_str(&String::from_utf8_lossy(&input[..consumed]));
113    Some(consumed)
114}
115
116fn decode_wide(input: &[u8], output: &mut String, digits: usize) -> Option<usize> {
117    if input.get(3) != Some(&b'\\') {
118        return None;
119    }
120    let mut position = 4;
121    let mut utf16 = Vec::new();
122    let mut utf32 = Vec::new();
123    loop {
124        if input
125            .get(position..position + 4)?
126            .eq_ignore_ascii_case(b"\\X0\\")
127        {
128            position += 4;
129            break;
130        }
131        let unit = parse_hex(input.get(position..position + digits)?)?;
132        if digits == 4 {
133            utf16.push(u16::try_from(unit).ok()?);
134        } else {
135            utf32.push(unit);
136        }
137        position += digits;
138    }
139    let mut decoded = String::new();
140    if digits == 4 {
141        if utf16.is_empty() {
142            return None;
143        }
144        for value in std::char::decode_utf16(utf16) {
145            decoded.push(value.ok()?);
146        }
147    } else {
148        if utf32.is_empty() {
149            return None;
150        }
151        for value in utf32 {
152            decoded.push(char::from_u32(value)?);
153        }
154    }
155    output.push_str(&decoded);
156    Some(position)
157}
158
159fn flush_utf16(pending: &mut Vec<u16>, output: &mut String) {
160    if pending.is_empty() {
161        return;
162    }
163    output.push_str("\\X2\\");
164    for unit in pending.drain(..) {
165        use std::fmt::Write as _;
166        let _ = write!(output, "{unit:04X}");
167    }
168    output.push_str("\\X0\\");
169}
170
171/// Encodes Unicode as a STEP string literal body.
172///
173/// Printable ASCII is retained, apostrophes and backslashes are doubled, and
174/// BMP runs use UTF-16BE `\X2\` notation and supplementary characters use
175/// UTF-32BE `\X4\` notation.
176#[must_use]
177pub fn encode(text: &str) -> String {
178    let mut output = String::with_capacity(text.len());
179    let mut pending = Vec::new();
180
181    for character in text.chars() {
182        match character {
183            '\'' => {
184                flush_utf16(&mut pending, &mut output);
185                output.push_str("''");
186            }
187            '\\' => {
188                flush_utf16(&mut pending, &mut output);
189                output.push_str("\\\\");
190            }
191            value if value.is_ascii_control() => {
192                use std::fmt::Write as _;
193
194                flush_utf16(&mut pending, &mut output);
195                let _ = write!(output, "\\X\\{:02X}", value as u32);
196            }
197            value if value.is_ascii() => {
198                flush_utf16(&mut pending, &mut output);
199                output.push(value);
200            }
201            value if u32::from(value) <= 0xffff => {
202                let mut units = [0_u16; 2];
203                pending.extend_from_slice(value.encode_utf16(&mut units));
204            }
205            value => {
206                use std::fmt::Write as _;
207
208                flush_utf16(&mut pending, &mut output);
209                let _ = write!(output, "\\X4\\{:08X}\\X0\\", u32::from(value));
210            }
211        }
212    }
213    flush_utf16(&mut pending, &mut output);
214    output
215}