Skip to main content

cadmpeg_codec_step/
strings.rs

1// SPDX-License-Identifier: Apache-2.0
2//! ISO 10303-21 string escape decoding and canonical encoding.
3
4use crate::parse::implementation_level::ImplementationLevel;
5
6/// A malformed or unsupported string escape.
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[error("{message} at string byte {offset}")]
9pub(crate) struct StringError {
10    /// Byte position within the unquoted string token.
11    offset: usize,
12    /// Description of the violated escape invariant.
13    message: String,
14}
15
16const MAX_STORED_STRING_OCTETS: usize = 32_767;
17
18pub(crate) fn decode(input: &[u8], level: ImplementationLevel) -> Result<String, StringError> {
19    if input.len() > MAX_STORED_STRING_OCTETS {
20        return Err(StringError {
21            offset: 0,
22            message: "string exceeds maximum stored length".to_owned(),
23        });
24    }
25    let mut output = String::new();
26    decode_chars(input, level, |character| output.push(character))?;
27    Ok(output)
28}
29
30fn decode_chars(
31    input: &[u8],
32    level: ImplementationLevel,
33    mut emit: impl FnMut(char),
34) -> Result<(), StringError> {
35    let mut at = 0;
36    let mut page = b'A';
37    while at < input.len() {
38        match input[at] {
39            b'\'' if input.get(at + 1) == Some(&b'\'') => {
40                emit('\'');
41                at += 2;
42            }
43            b'\\'
44                if matches!(input.get(at + 1), Some(b'N' | b'F'))
45                    && input.get(at + 2) == Some(&b'\\') =>
46            {
47                at += 3;
48            }
49            b'\\' if input.get(at + 1) == Some(&b'\\') => {
50                emit('\\');
51                at += 2;
52            }
53            b'\\' if input.get(at + 1) == Some(&b'P') => {
54                if !matches!(input.get(at + 2), Some(b'A'..=b'I'))
55                    || input.get(at + 3) != Some(&b'\\')
56                {
57                    return error(at, "invalid page-selection escape");
58                }
59                page = input[at + 2];
60                at += 4;
61            }
62            b'\\' if input.get(at + 1) == Some(&b'S') => {
63                if input.get(at + 2) != Some(&b'\\') {
64                    return error(at, "invalid S escape");
65                }
66                let Some(&code) = input.get(at + 3) else {
67                    return error(at, "truncated S escape");
68                };
69                emit(decode_page_byte(page, code | 0x80, at)?);
70                at += 4;
71            }
72            b'\\' if input.get(at + 1) == Some(&b'X') => match input.get(at + 2) {
73                Some(b'\\') => {
74                    let byte = hex_byte(input, at + 3)?;
75                    emit(char::from(byte));
76                    at += 5;
77                }
78                Some(b'2') if input.get(at + 3) == Some(&b'\\') => {
79                    at = decode_wide(input, at + 4, 4, &mut emit)?;
80                }
81                Some(b'4') if input.get(at + 3) == Some(&b'\\') => {
82                    at = decode_wide(input, at + 4, 8, &mut emit)?;
83                }
84                _ => return error(at, "invalid X escape"),
85            },
86            b'\'' => return error(at, "unpaired apostrophe"),
87            byte if byte.is_ascii_control() => at += 1,
88            b'\\' => return error(at, "unknown reverse-solidus escape"),
89            _ => {
90                let start = at;
91                while at < input.len() && !matches!(input[at], b'\'' | b'\\') {
92                    at += 1;
93                }
94                let direct = &input[start..at];
95                if level.is_edition3() {
96                    let text = std::str::from_utf8(direct).map_err(|error| StringError {
97                        offset: start + error.valid_up_to(),
98                        message: "invalid UTF-8 direct string bytes".into(),
99                    })?;
100                    for character in text.chars() {
101                        emit(character);
102                    }
103                } else {
104                    for byte in direct {
105                        emit(char::from(*byte));
106                    }
107                }
108            }
109        }
110    }
111    Ok(())
112}
113
114fn decode_page_byte(page: u8, byte: u8, offset: usize) -> Result<char, StringError> {
115    let part = page - b'A' + 1;
116    if byte < 0xa0 || part == 1 {
117        return Ok(char::from(byte));
118    }
119    if part == 9 {
120        return Ok(match byte {
121            0xd0 => '\u{011e}',
122            0xdd => '\u{0130}',
123            0xde => '\u{015e}',
124            0xf0 => '\u{011f}',
125            0xfd => '\u{0131}',
126            0xfe => '\u{015f}',
127            _ => char::from(byte),
128        });
129    }
130    let label = format!("iso-8859-{part}");
131    let encoding =
132        encoding_rs::Encoding::for_label(label.as_bytes()).ok_or_else(|| StringError {
133            offset,
134            message: format!("ISO 8859 part {part} is unavailable"),
135        })?;
136    let bytes = [byte];
137    let (decoded, had_errors) = encoding.decode_without_bom_handling(&bytes);
138    if had_errors {
139        return error(
140            offset,
141            "S escape is undefined in the selected ISO 8859 part",
142        );
143    }
144    decoded.chars().next().ok_or_else(|| StringError {
145        offset,
146        message: "S escape decoded to no character".into(),
147    })
148}
149
150/// Encode text as bytes suitable between Part 21 apostrophe delimiters.
151pub(crate) fn encode(input: &str) -> String {
152    let mut output = String::new();
153    for character in input.chars() {
154        match character {
155            '\'' => output.push_str("''"),
156            '\\' => output.push_str("\\\\"),
157            '\u{20}'..='\u{7e}' => output.push(character),
158            character if u32::from(character) <= 0xffff => {
159                output.push_str("\\X2\\");
160                push_hex_digits(&mut output, &u32::from(character).to_be_bytes()[2..]);
161                output.push_str("\\X0\\");
162            }
163            character => {
164                output.push_str("\\X4\\");
165                push_hex_digits(&mut output, &u32::from(character).to_be_bytes());
166                output.push_str("\\X0\\");
167            }
168        }
169    }
170    output
171}
172
173fn push_hex_digits(output: &mut String, bytes: &[u8]) {
174    const HEX: &[u8; 16] = b"0123456789ABCDEF";
175    for byte in bytes {
176        output.push(char::from(HEX[usize::from(byte >> 4)]));
177        output.push(char::from(HEX[usize::from(byte & 0xF)]));
178    }
179}
180
181fn decode_wide(
182    input: &[u8],
183    start: usize,
184    width: usize,
185    emit: &mut impl FnMut(char),
186) -> Result<usize, StringError> {
187    let Some(relative_end) = input[start..]
188        .windows(4)
189        .position(|bytes| bytes == b"\\X0\\")
190    else {
191        return error(start, "unterminated wide escape");
192    };
193    let end = start + relative_end;
194    if !(end - start).is_multiple_of(width) {
195        return error(start, "wide escape has incomplete code unit");
196    }
197    let mut high_surrogate = None;
198    for offset in (start..end).step_by(width) {
199        let raw = std::str::from_utf8(&input[offset..offset + width])
200            .ok()
201            .and_then(|hex| u32::from_str_radix(hex, 16).ok())
202            .ok_or_else(|| StringError {
203                offset,
204                message: "wide escape contains non-hexadecimal digits".into(),
205            })?;
206        if width == 4 {
207            let Ok(unit) = u16::try_from(raw) else {
208                return error(offset, "wide escape contains non-hexadecimal digits");
209            };
210            match unit {
211                0xd800..=0xdbff => {
212                    if high_surrogate.replace(unit).is_some() {
213                        return error(start, "wide escape contains an isolated surrogate");
214                    }
215                }
216                0xdc00..=0xdfff => {
217                    let Some(high) = high_surrogate.take() else {
218                        return error(start, "wide escape contains an isolated surrogate");
219                    };
220                    let scalar =
221                        0x10000 + ((u32::from(high) - 0xd800) << 10) + (u32::from(unit) - 0xdc00);
222                    let Some(character) = char::from_u32(scalar) else {
223                        return error(start, "wide escape contains an isolated surrogate");
224                    };
225                    emit(character);
226                }
227                _ => {
228                    if high_surrogate.is_some() {
229                        return error(start, "wide escape contains an isolated surrogate");
230                    }
231                    let Some(character) = char::from_u32(u32::from(unit)) else {
232                        return error(start, "wide escape contains an isolated surrogate");
233                    };
234                    emit(character);
235                }
236            }
237        } else {
238            let character = char::from_u32(raw).ok_or_else(|| StringError {
239                offset: start,
240                message: "wide escape contains an invalid Unicode scalar".into(),
241            })?;
242            emit(character);
243        }
244    }
245    if high_surrogate.is_some() {
246        return error(start, "wide escape contains an isolated surrogate");
247    }
248    Ok(end + 4)
249}
250
251fn hex_byte(input: &[u8], offset: usize) -> Result<u8, StringError> {
252    let Some(bytes) = input.get(offset..offset + 2) else {
253        return error(offset, "truncated byte escape");
254    };
255    std::str::from_utf8(bytes)
256        .ok()
257        .and_then(|hex| u8::from_str_radix(hex, 16).ok())
258        .ok_or_else(|| StringError {
259            offset,
260            message: "byte escape contains non-hexadecimal digits".into(),
261        })
262}
263
264fn error<T>(offset: usize, message: &str) -> Result<T, StringError> {
265    Err(StringError {
266        offset,
267        message: message.into(),
268    })
269}
270
271#[cfg(test)]
272pub(crate) mod tests;