1use crate::parse::implementation_level::ImplementationLevel;
5
6#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[error("{message} at string byte {offset}")]
9pub(crate) struct StringError {
10 offset: usize,
12 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
150pub(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;