Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Lib/test/test_codeccallbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,8 +828,6 @@ def handle(exc):
self.assertEqual(exc.end, 2)
self.assertEqual(exc.object, input)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_encode_unencodable_replacement(self):
def unencrepl(exc):
if isinstance(exc, UnicodeEncodeError):
Expand Down Expand Up @@ -1063,7 +1061,6 @@ def test_decodehelper_bug36819(self):
decoded = input.decode(enc, "test.bug36819")
self.assertEqual(decoded, 'abcdx' * 51)

@unittest.expectedFailureIf(sys.platform != "win32", "TODO: RUSTPYTHON")
def test_encodehelper_bug36819(self):
handler = RepeatedPosReturn()
codecs.register_error("test.bug36819", handler.handle)
Expand Down Expand Up @@ -1133,8 +1130,6 @@ def test_bug828737(self):
text = 'abc<def>ghi'*n
text.translate(charmap)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_mutatingdecodehandler(self):
baddata = [
("ascii", b"\xff"),
Expand Down Expand Up @@ -1171,8 +1166,6 @@ def mutating(exc):
self.assertEqual(data.decode(encoding, "test.mutating"), "\u4242")

# issue32583
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_crashing_decode_handler(self):
# better generating one more character to fill the extra space slot
# so in debug build it can steadily fail
Expand Down
3 changes: 0 additions & 3 deletions Lib/test/test_codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,6 @@ def test_escape(self):
check(br"[\x41]", b"[A]")
check(br"[\x410]", b"[A0]")

@unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered
def test_warnings(self):
decode = codecs.escape_decode
check = coding_checker(self, decode)
Expand Down Expand Up @@ -2582,7 +2581,6 @@ def test_streamreaderwriter(self):


class TypesTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'codecs' has no attribute 'utf_32_ex_decode'. Did you mean: 'utf_16_ex_decode'?
def test_decode_unicode(self):
# Most decoders don't accept unicode input
decoders = [
Expand Down Expand Up @@ -2684,7 +2682,6 @@ def test_escape_decode(self):
check(br"\u20ac", "\u20ac")
check(br"\U0001d120", "\U0001d120")

@unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered
def test_decode_warnings(self):
decode = codecs.unicode_escape_decode
check = coding_checker(self, decode)
Expand Down
8 changes: 8 additions & 0 deletions crates/common/src/encodings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};

#[cfg(feature = "cjk-codecs")]
pub mod cjk;
mod wide;
pub use wide::ByteOrder;
pub mod escape;
pub mod raw_unicode_escape;
pub mod unicode_escape;
pub mod utf16;
pub mod utf32;
pub mod utf7;

pub trait StrBuffer: AsRef<Wtf8> {
fn is_compatible_with(&self, kind: StrKind) -> bool {
Expand Down
153 changes: 153 additions & 0 deletions crates/common/src/encodings/escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Bytes-to-bytes Python string-literal escape transform (`escape_encode` /
//! `escape_decode`).

use alloc::string::String;
use alloc::vec::Vec;

/// Encode `data` the way `escape_encode` / `string_escape_encode` does.
#[must_use]
pub fn encode(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len());
for &byte in data {
match byte {
b'\t' => out.extend_from_slice(b"\\t"),
b'\n' => out.extend_from_slice(b"\\n"),
b'\r' => out.extend_from_slice(b"\\r"),
b'\\' => out.extend_from_slice(b"\\\\"),
b'\'' => out.extend_from_slice(b"\\'"),
0x20..=0x7e => out.push(byte),
value => out.extend_from_slice(format!("\\x{value:02x}").as_bytes()),
}
}
out
}

/// How a malformed `\x` escape is reported.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EscapeErrorMode {
Strict,
Replace,
Ignore,
}

impl EscapeErrorMode {
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"strict" => Some(Self::Strict),
"replace" => Some(Self::Replace),
"ignore" => Some(Self::Ignore),
_ => None,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EscapeDecodeError {
TrailingBackslash,
InvalidHex { position: usize },
UnknownHandler { name: String },
}

/// Decode a Python bytes-literal escape sequence.
///
/// `warning` is the first invalid-escape deprecation text, if any.
pub fn decode(
data: &[u8],
errors: EscapeErrorMode,
) -> Result<(Vec<u8>, Option<String>), EscapeDecodeError> {
let mut out = Vec::with_capacity(data.len());
let mut pos = 0usize;
let mut warning = None;
while pos < data.len() {
if data[pos] != b'\\' {
out.push(data[pos]);
pos += 1;
continue;
}
let escape_start = pos;
pos += 1;
if pos == data.len() {
return Err(EscapeDecodeError::TrailingBackslash);
}
let ch = data[pos];
pos += 1;
match ch {
b'\n' => {}
b'\\' => out.push(b'\\'),
b'\'' => out.push(b'\''),
b'"' => out.push(b'"'),
b'b' => out.push(0x08),
b'f' => out.push(0x0c),
b't' => out.push(b'\t'),
b'n' => out.push(b'\n'),
b'r' => out.push(b'\r'),
b'v' => out.push(0x0b),
b'a' => out.push(0x07),
b'0'..=b'7' => {
let octal_start = pos - 1;
while pos < data.len()
&& pos < octal_start + 3
&& (b'0'..=b'7').contains(&data[pos])
{
pos += 1;
}
let raw = data[octal_start..pos]
.iter()
.fold(0u16, |value, digit| value * 8 + u16::from(digit - b'0'));
if raw >= 256 && warning.is_none() {
warning = Some(invalid_escape_warning(
"b",
&String::from_utf8_lossy(&data[octal_start..pos]),
true,
));
}
out.push(raw as u8);
}
b'x' => {
let hi = data.get(pos).and_then(|byte| (*byte as char).to_digit(16));
let lo = data
.get(pos + 1)
.and_then(|byte| (*byte as char).to_digit(16));
if let (Some(hi), Some(lo)) = (hi, lo) {
out.push((hi * 16 + lo) as u8);
pos += 2;
} else {
match errors {
EscapeErrorMode::Strict => {
return Err(EscapeDecodeError::InvalidHex {
position: escape_start,
});
}
EscapeErrorMode::Replace => out.push(b'?'),
EscapeErrorMode::Ignore => {}
}
if data.get(pos).is_some_and(u8::is_ascii_hexdigit) {
pos += 1;
}
}
}
other => {
out.push(b'\\');
pos -= 1;
if warning.is_none() {
warning = Some(invalid_escape_warning(
"b",
&char::from(other).to_string(),
false,
));
}
}
}
}
Ok((out, warning))
}

fn invalid_escape_warning(prefix: &str, sequence: &str, octal: bool) -> String {
let kind = if octal {
"an invalid octal escape sequence"
} else {
"an invalid escape sequence"
};
format!("{prefix}\"\\{sequence}\" is {kind}. Such sequences will not work in the future. ")
}
112 changes: 112 additions & 0 deletions crates/common/src/encodings/raw_unicode_escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! `raw-unicode-escape` encode / incremental decode.

use super::*;
use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};

pub const ENCODING_NAME: &str = "rawunicodeescape";

/// Encode `s`. Code points below 0x100 stay as a Latin-1 byte; the rest
/// become `\uXXXX` / `\UXXXXXXXX`.
#[must_use]
pub fn encode_bytes(s: &Wtf8) -> Vec<u8> {
let mut out = Vec::new();
for cp in s.code_points() {
let v = cp.to_u32();
if v < 0x100 {
out.push(v as u8);
} else if v < 0x10000 {
out.extend_from_slice(format!("\\u{v:04x}").as_bytes());
} else {
out.extend_from_slice(format!("\\U{v:08x}").as_bytes());
}
}
out
}

pub fn encode<Ctx, E>(ctx: Ctx, _errors: &E) -> Result<Vec<u8>, Ctx::Error>
where
Ctx: EncodeContext,
E: EncodeErrorHandler<Ctx>,
{
Ok(encode_bytes(ctx.remaining_data()))
}

pub fn decode<Ctx, E>(
mut ctx: Ctx,
errors: &E,
final_decode: bool,
) -> Result<(Wtf8Buf, usize), Ctx::Error>
where
Ctx: DecodeContext,
E: DecodeErrorHandler<Ctx>,
{
let mut out = Wtf8Buf::new();
loop {
let rest = ctx.remaining_data();
if rest.is_empty() {
break;
}
let b = rest[0];
if b != b'\\' {
out.push_char(b as char);
ctx.advance(1);
continue;
}
let kind = rest.get(1).copied();
let want = match kind {
Some(b'u') => 4usize,
Some(b'U') => 8usize,
_ => 0,
};
if want != 0 {
let escape_start = ctx.position();
let digits_start = 2;
if !final_decode && rest.len() < digits_start + want {
break;
}
let available = (digits_start + want).min(rest.len());
let mut hex_end = digits_start;
while hex_end < available && rest[hex_end].is_ascii_hexdigit() {
hex_end += 1;
}
let numeric = if available == digits_start + want && hex_end == available {
core::str::from_utf8(&rest[digits_start..available])
.ok()
.and_then(|s| u32::from_str_radix(s, 16).ok())
} else {
None
};
if let Some(c) = numeric.and_then(CodePoint::from_u32) {
out.push(c);
ctx.advance(available);
continue;
}
let error_end = ctx.position()
+ if numeric.is_some() {
available
} else {
hex_end
};
let reason = if numeric.is_some() {
"illegal Unicode character"
} else if want == 4 {
"truncated \\uXXXX escape"
} else {
"truncated \\UXXXXXXXX escape"
};
let replace = ctx.handle_error(errors, escape_start..error_end, Some(reason))?;
out.push_wtf8(replace.as_ref());
continue;
}
if kind.is_none() && !final_decode {
break;
}
out.push_char(b as char);
ctx.advance(1);
if let Some(next) = kind {
out.push_char(next as char);
ctx.advance(1);
}
}
Ok((out, ctx.position()))
}
Loading
Loading