1use std::fmt::Write;
5
6#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8#[error("{message} at string byte {offset}")]
9pub struct StringError {
10 pub offset: usize,
12 pub message: String,
14}
15
16pub 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
118pub 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}