Skip to main content

cadmpeg_step/
strings.rs

1// SPDX-License-Identifier: Apache-2.0
2//! ISO 10303-21 string escape decoding and canonical encoding.
3
4use std::fmt::Write;
5
6/// A malformed or unsupported string escape.
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[error("{message} at string byte {offset}")]
9pub struct StringError {
10    /// Byte position within the unquoted string token.
11    pub offset: usize,
12    /// Description of the violated escape invariant.
13    pub message: String,
14}
15
16/// Decode the bytes between a Part 21 string token's apostrophe delimiters.
17pub fn decode(input: &[u8]) -> Result<String, StringError> {
18    let mut output = String::new();
19    let mut at = 0;
20    let mut page = b'A';
21    while at < input.len() {
22        match input[at] {
23            b'\'' if input.get(at + 1) == Some(&b'\'') => {
24                output.push('\'');
25                at += 2;
26            }
27            b'\\' if input.get(at + 1) == Some(&b'\\') => {
28                output.push('\\');
29                at += 2;
30            }
31            b'\\' if input.get(at + 1) == Some(&b'P') => {
32                if !matches!(input.get(at + 2), Some(b'A'..=b'I'))
33                    || input.get(at + 3) != Some(&b'\\')
34                {
35                    return error(at, "invalid page-selection escape");
36                }
37                page = input[at + 2];
38                at += 4;
39            }
40            b'\\' if input.get(at + 1) == Some(&b'S') => {
41                if input.get(at + 2) != Some(&b'\\') {
42                    return error(at, "invalid S escape");
43                }
44                let Some(&code) = input.get(at + 3) else {
45                    return error(at, "truncated S escape");
46                };
47                output.push(decode_page_byte(page, code | 0x80, at)?);
48                at += 4;
49            }
50            b'\\' if input.get(at + 1) == Some(&b'X') => match input.get(at + 2) {
51                Some(b'\\') => {
52                    let byte = hex_byte(input, at + 3)?;
53                    output.push(char::from(byte));
54                    at += 5;
55                }
56                Some(b'2') if input.get(at + 3) == Some(&b'\\') => {
57                    let (decoded, end) = decode_wide(input, at + 4, 4)?;
58                    output.push_str(&decoded);
59                    at = end;
60                }
61                Some(b'4') if input.get(at + 3) == Some(&b'\\') => {
62                    let (decoded, end) = decode_wide(input, at + 4, 8)?;
63                    output.push_str(&decoded);
64                    at = end;
65                }
66                _ => return error(at, "invalid X escape"),
67            },
68            b'\'' => return error(at, "unpaired apostrophe"),
69            b'\\' => return error(at, "unknown reverse-solidus escape"),
70            byte => {
71                output.push(char::from(byte));
72                at += 1;
73            }
74        }
75    }
76    Ok(output)
77}
78
79fn decode_page_byte(page: u8, byte: u8, offset: usize) -> Result<char, StringError> {
80    let part = page - b'A' + 1;
81    if byte < 0xa0 || part == 1 {
82        return char::from_u32(u32::from(byte)).ok_or_else(|| StringError {
83            offset,
84            message: "invalid ISO 8859 scalar".into(),
85        });
86    }
87    if part == 9 {
88        return Ok(match byte {
89            0xd0 => '\u{011e}',
90            0xdd => '\u{0130}',
91            0xde => '\u{015e}',
92            0xf0 => '\u{011f}',
93            0xfd => '\u{0131}',
94            0xfe => '\u{015f}',
95            _ => char::from(byte),
96        });
97    }
98    let label = format!("iso-8859-{part}");
99    let encoding =
100        encoding_rs::Encoding::for_label(label.as_bytes()).ok_or_else(|| StringError {
101            offset,
102            message: format!("ISO 8859 part {part} is unavailable"),
103        })?;
104    let bytes = [byte];
105    let (decoded, had_errors) = encoding.decode_without_bom_handling(&bytes);
106    if had_errors {
107        return error(
108            offset,
109            "S escape is undefined in the selected ISO 8859 part",
110        );
111    }
112    decoded.chars().next().ok_or_else(|| StringError {
113        offset,
114        message: "S escape decoded to no character".into(),
115    })
116}
117
118/// Encode text as bytes suitable between Part 21 apostrophe delimiters.
119pub fn encode(input: &str) -> String {
120    let mut output = String::new();
121    for character in input.chars() {
122        match character {
123            '\'' => output.push_str("''"),
124            '\\' => output.push_str("\\\\"),
125            '\u{20}'..='\u{7e}' => output.push(character),
126            character if u32::from(character) <= 0xffff => {
127                write!(output, "\\X2\\{:04X}\\X0\\", u32::from(character))
128                    .expect("writing to a String cannot fail");
129            }
130            character => {
131                write!(output, "\\X4\\{:08X}\\X0\\", u32::from(character))
132                    .expect("writing to a String cannot fail");
133            }
134        }
135    }
136    output
137}
138
139fn decode_wide(input: &[u8], start: usize, width: usize) -> Result<(String, usize), StringError> {
140    let Some(relative_end) = input[start..]
141        .windows(4)
142        .position(|bytes| bytes == b"\\X0\\")
143    else {
144        return error(start, "unterminated wide escape");
145    };
146    let end = start + relative_end;
147    if !(end - start).is_multiple_of(width) {
148        return error(start, "wide escape has incomplete code unit");
149    }
150    let mut scalars = Vec::new();
151    for offset in (start..end).step_by(width) {
152        let raw = std::str::from_utf8(&input[offset..offset + width])
153            .ok()
154            .and_then(|hex| u32::from_str_radix(hex, 16).ok())
155            .ok_or_else(|| StringError {
156                offset,
157                message: "wide escape contains non-hexadecimal digits".into(),
158            })?;
159        scalars.push(raw);
160    }
161    let decoded = if width == 4 {
162        let units = scalars.into_iter().map(|value| value as u16);
163        char::decode_utf16(units)
164            .collect::<Result<String, _>>()
165            .map_err(|_| StringError {
166                offset: start,
167                message: "wide escape contains an isolated surrogate".into(),
168            })?
169    } else {
170        scalars
171            .into_iter()
172            .map(|value| {
173                char::from_u32(value).ok_or_else(|| StringError {
174                    offset: start,
175                    message: "wide escape contains an invalid Unicode scalar".into(),
176                })
177            })
178            .collect::<Result<String, _>>()?
179    };
180    Ok((decoded, end + 4))
181}
182
183fn hex_byte(input: &[u8], offset: usize) -> Result<u8, StringError> {
184    let Some(bytes) = input.get(offset..offset + 2) else {
185        return error(offset, "truncated byte escape");
186    };
187    std::str::from_utf8(bytes)
188        .ok()
189        .and_then(|hex| u8::from_str_radix(hex, 16).ok())
190        .ok_or_else(|| StringError {
191            offset,
192            message: "byte escape contains non-hexadecimal digits".into(),
193        })
194}
195
196fn error<T>(offset: usize, message: &str) -> Result<T, StringError> {
197    Err(StringError {
198        offset,
199        message: message.into(),
200    })
201}