Skip to main content

rustpython_common/encodings/
unicode_escape.rs

1//! `unicode-escape` encode / incremental decode.
2
3use super::*;
4use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};
5
6pub const ENCODING_NAME: &str = "unicodeescape";
7
8fn push_hex(out: &mut Vec<u8>, prefix: u8, cp: u32, digits: usize) {
9    out.push(b'\\');
10    out.push(prefix);
11    for shift in (0..digits).rev() {
12        out.push(b"0123456789abcdef"[((cp >> (shift * 4)) & 0xf) as usize]);
13    }
14}
15
16/// Encode `s` as a Python unicode-escape byte string. Every code point is
17/// representable, so this cannot fail.
18#[must_use]
19pub fn encode_bytes(s: &Wtf8) -> Vec<u8> {
20    let mut out = Vec::new();
21    for cp in s.code_points() {
22        match cp.to_u32() {
23            0x5c => out.extend_from_slice(br"\\"),
24            0x09 => out.extend_from_slice(br"\t"),
25            0x0a => out.extend_from_slice(br"\n"),
26            0x0d => out.extend_from_slice(br"\r"),
27            0x20..=0x7e => out.push(cp.to_u32() as u8),
28            c @ 0x00..=0xff => push_hex(&mut out, b'x', c, 2),
29            c @ 0x100..=0xffff => push_hex(&mut out, b'u', c, 4),
30            c => push_hex(&mut out, b'U', c, 8),
31        }
32    }
33    out
34}
35
36pub fn encode<Ctx, E>(ctx: Ctx, _errors: &E) -> Result<Vec<u8>, Ctx::Error>
37where
38    Ctx: EncodeContext,
39    E: EncodeErrorHandler<Ctx>,
40{
41    Ok(encode_bytes(ctx.remaining_data()))
42}
43
44/// Optional first invalid-escape deprecation text. The caller issues it.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct EscapeNote {
47    pub message: String,
48}
49
50fn invalid_escape_warning(prefix: &str, sequence: &str, octal: bool) -> String {
51    let kind = if octal {
52        "an invalid octal escape sequence"
53    } else {
54        "an invalid escape sequence"
55    };
56    format!("{prefix}\"\\{sequence}\" is {kind}. Such sequences will not work in the future. ")
57}
58
59/// Incremental unicode-escape decode.
60pub fn decode<Ctx, E>(
61    mut ctx: Ctx,
62    errors: &E,
63    final_decode: bool,
64) -> Result<(Wtf8Buf, usize, Option<EscapeNote>), Ctx::Error>
65where
66    Ctx: DecodeContext,
67    E: DecodeErrorHandler<Ctx>,
68{
69    let mut out = Wtf8Buf::new();
70    let mut warning = None;
71    loop {
72        let rest = ctx.remaining_data();
73        if rest.is_empty() {
74            break;
75        }
76        let ch = rest[0];
77        if ch != b'\\' {
78            if let Some(cp) = CodePoint::from_u32(ch as u32) {
79                out.push(cp);
80            }
81            ctx.advance(1);
82            continue;
83        }
84        let escape_start = ctx.position();
85        if rest.len() == 1 {
86            if !final_decode {
87                break;
88            }
89            let end = ctx.full_data().len();
90            let replace =
91                ctx.handle_error(errors, escape_start..end, Some("\\ at end of string"))?;
92            out.push_wtf8(replace.as_ref());
93            continue;
94        }
95        let intro = rest[1];
96        match intro {
97            b'\n' => ctx.advance(2),
98            b'\\' => {
99                out.push_char('\\');
100                ctx.advance(2);
101            }
102            b'\'' => {
103                out.push_char('\'');
104                ctx.advance(2);
105            }
106            b'"' => {
107                out.push_char('"');
108                ctx.advance(2);
109            }
110            b'b' => {
111                out.push_char('\x08');
112                ctx.advance(2);
113            }
114            b'f' => {
115                out.push_char('\x0c');
116                ctx.advance(2);
117            }
118            b't' => {
119                out.push_char('\t');
120                ctx.advance(2);
121            }
122            b'n' => {
123                out.push_char('\n');
124                ctx.advance(2);
125            }
126            b'r' => {
127                out.push_char('\r');
128                ctx.advance(2);
129            }
130            b'v' => {
131                out.push_char('\x0b');
132                ctx.advance(2);
133            }
134            b'a' => {
135                out.push_char('\x07');
136                ctx.advance(2);
137            }
138            b'0'..=b'7' => {
139                ctx.advance(2);
140                let mut value = (intro - b'0') as u32;
141                let octal_start = escape_start + 1;
142                for _ in 0..2 {
143                    let rest = ctx.remaining_data();
144                    if rest.first().is_some_and(|b| matches!(b, b'0'..=b'7')) {
145                        value = (value << 3) + (rest[0] - b'0') as u32;
146                        ctx.advance(1);
147                    }
148                }
149                if value > 0o377 && warning.is_none() {
150                    let seq = &ctx.full_data()[octal_start..ctx.position()];
151                    warning = Some(EscapeNote {
152                        message: invalid_escape_warning("", &String::from_utf8_lossy(seq), true),
153                    });
154                }
155                if let Some(cp) = CodePoint::from_u32(value) {
156                    out.push(cp);
157                }
158            }
159            b'x' | b'u' | b'U' => {
160                let (digits, message) = match intro {
161                    b'x' => (2usize, "truncated \\xXX escape"),
162                    b'u' => (4usize, "truncated \\uXXXX escape"),
163                    _ => (8usize, "truncated \\UXXXXXXXX escape"),
164                };
165                if !final_decode && rest.len() < 2 + digits {
166                    break;
167                }
168                ctx.advance(2);
169                let rest = ctx.remaining_data();
170                if rest.len() >= digits && rest[..digits].iter().all(u8::is_ascii_hexdigit) {
171                    let value =
172                        u32::from_str_radix(core::str::from_utf8(&rest[..digits]).unwrap(), 16)
173                            .unwrap();
174                    if let Some(cp) = CodePoint::from_u32(value) {
175                        out.push(cp);
176                        ctx.advance(digits);
177                        continue;
178                    }
179                    let start = escape_start;
180                    let end = ctx.position() + digits;
181                    let replace =
182                        ctx.handle_error(errors, start..end, Some("illegal Unicode character"))?;
183                    out.push_wtf8(replace.as_ref());
184                    continue;
185                }
186                let mut hex_end = 0;
187                while hex_end < rest.len() && rest[hex_end].is_ascii_hexdigit() {
188                    hex_end += 1;
189                }
190                let start = escape_start;
191                let end = ctx.position() + hex_end;
192                let replace = ctx.handle_error(errors, start..end, Some(message))?;
193                out.push_wtf8(replace.as_ref());
194            }
195            b'N' => {
196                if rest.len() == 2 && !final_decode {
197                    break;
198                }
199                if rest.len() >= 3 && rest[2] == b'{' {
200                    let name_start = 3;
201                    let mut look = name_start;
202                    while look < rest.len() && rest[look] != b'}' {
203                        look += 1;
204                    }
205                    if look >= rest.len() {
206                        if !final_decode {
207                            break;
208                        }
209                        let start = escape_start;
210                        let end = ctx.full_data().len();
211                        let replace = ctx.handle_error(
212                            errors,
213                            start..end,
214                            Some("malformed \\N character escape"),
215                        )?;
216                        out.push_wtf8(replace.as_ref());
217                        continue;
218                    }
219                    if look == name_start {
220                        let start = escape_start;
221                        let end = ctx.position() + name_start;
222                        let replace = ctx.handle_error(
223                            errors,
224                            start..end,
225                            Some("malformed \\N character escape"),
226                        )?;
227                        out.push_wtf8(replace.as_ref());
228                        continue;
229                    }
230                    let name = core::str::from_utf8(&rest[name_start..look]).ok();
231                    match name.and_then(rustpython_unicode::lookup_character) {
232                        Some(ch) => {
233                            out.push_char(ch);
234                            ctx.advance(look + 1);
235                            continue;
236                        }
237                        None => {
238                            let start = escape_start;
239                            let end = ctx.position() + look + 1;
240                            let replace = ctx.handle_error(
241                                errors,
242                                start..end,
243                                Some("unknown Unicode character name"),
244                            )?;
245                            out.push_wtf8(replace.as_ref());
246                            continue;
247                        }
248                    }
249                }
250                ctx.advance(2);
251                let start = escape_start;
252                let end = ctx.position();
253                let replace =
254                    ctx.handle_error(errors, start..end, Some("malformed \\N character escape"))?;
255                out.push_wtf8(replace.as_ref());
256            }
257            other => {
258                if warning.is_none() {
259                    warning = Some(EscapeNote {
260                        message: invalid_escape_warning("", &char::from(other).to_string(), false),
261                    });
262                }
263                out.push_char('\\');
264                if let Some(cp) = CodePoint::from_u32(other as u32) {
265                    out.push(cp);
266                }
267                ctx.advance(2);
268            }
269        }
270    }
271    Ok((out, ctx.position(), warning))
272}