diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 1fe0e5ca1..276dff080 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -387,7 +387,6 @@ class CmdLineTest(unittest.TestCase): "be directly executed") self._check_import_error(["-m", "test_pkg"], msg, cwd=script_dir) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_issue8202(self): # Make sure package __init__ modules see "-m" in sys.argv0 while # searching for the module to execute @@ -656,7 +655,6 @@ class CmdLineTest(unittest.TestCase): ], ) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_syntaxerror_invalid_escape_sequence_multi_line(self): script = 'foo = """\\q"""\n' with os_helper.temp_dir() as script_dir: @@ -673,7 +671,6 @@ class CmdLineTest(unittest.TestCase): ], ) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_syntaxerror_null_bytes(self): script = "x = '\0' nothing to see here\n';import os;os.system('echo pwnd')\n" with os_helper.temp_dir() as script_dir: @@ -686,7 +683,6 @@ class CmdLineTest(unittest.TestCase): ], ) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_syntaxerror_null_bytes_in_multiline_string(self): scripts = ["\n'''\nmultilinestring\0\n'''", "\nf'''\nmultilinestring\0\n'''"] # Both normal and f-strings with os_helper.temp_dir() as script_dir: diff --git a/Lib/test/test_string_literals.py b/Lib/test/test_string_literals.py index 14327d288..df25bfd7e 100644 --- a/Lib/test/test_string_literals.py +++ b/Lib/test/test_string_literals.py @@ -225,7 +225,6 @@ class TestLiterals(unittest.TestCase): self.assertRaises(SyntaxError, eval, r""" b'\x' """) self.assertRaises(SyntaxError, eval, r""" b'\x0' """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_eval_bytes_invalid_escape(self): for b in range(1, 128): if b in b"""\n\r"'01234567\\abfnrtvx""": diff --git a/crates/vm/src/vm/compile.rs b/crates/vm/src/vm/compile.rs index 1cc46adf4..787b1e763 100644 --- a/crates/vm/src/vm/compile.rs +++ b/crates/vm/src/vm/compile.rs @@ -26,6 +26,13 @@ pub struct CompileWarningError { filename: String, lineno: usize, offset: usize, + replacement: Option, +} + +#[derive(Debug)] +struct SyntaxErrorReplacement { + message: String, + end_offset: usize, } impl From for VmCompileError { @@ -71,13 +78,16 @@ impl CompileWarningError { { return self.exception; } - let Ok(message) = self.exception.as_object().str(vm) else { - return self.exception; + let (message, end_offset) = if let Some(replacement) = self.replacement { + (replacement.message, Some(replacement.end_offset)) + } else { + let Ok(message) = self.exception.as_object().str(vm) else { + return self.exception; + }; + (message.to_string_lossy().into_owned(), None) }; - let syntax_error = vm.new_exception_msg( - vm.ctx.exceptions.syntax_error.to_owned(), - message.as_wtf8().to_owned(), - ); + let syntax_error = + vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), message.into()); syntax_error .as_object() .set_attr("lineno", vm.ctx.new_int(self.lineno), vm) @@ -86,6 +96,16 @@ impl CompileWarningError { .as_object() .set_attr("offset", vm.ctx.new_int(self.offset), vm) .unwrap(); + if let Some(end_offset) = end_offset { + syntax_error + .as_object() + .set_attr("end_lineno", vm.ctx.new_int(self.lineno), vm) + .unwrap(); + syntax_error + .as_object() + .set_attr("end_offset", vm.ctx.new_int(end_offset), vm) + .unwrap(); + } syntax_error .as_object() .set_attr("filename", vm.ctx.new_str(self.filename), vm) @@ -484,6 +504,7 @@ mod escape_warnings { filename: filename.to_owned(), lineno, offset, + replacement: None, } } @@ -609,16 +630,20 @@ mod escape_warnings { filename: &str, vm: &VirtualMachine, ) -> Result<(), CompileWarningError> { - let lineno = line_number_at(source, offset); - let message = vm.ctx.new_str(format!( + let (lineno, column) = line_offset_at(source, offset); + let warning = format!( "\"\\{ch}\" is an invalid escape sequence. \ Such sequences will not work in the future. \ Did you mean \"\\\\{ch}\"? A raw string is also an option." - )); + ); + let syntax_error = format!( + "\"\\{ch}\" is an invalid escape sequence. \ + Did you mean \"\\\\{ch}\"? A raw string is also an option." + ); let fname = vm.ctx.new_str(filename); warn::warn_explicit( Some(vm.ctx.exceptions.syntax_warning.to_owned()), - message.into(), + vm.ctx.new_str(warning).into(), fname, lineno, None, @@ -627,7 +652,16 @@ mod escape_warnings { None, vm, ) - .map_err(|err| compile_warning_error(err, source, filename, offset)) + .map_err(|exception| CompileWarningError { + exception, + filename: filename.to_owned(), + lineno, + offset: column, + replacement: Some(SyntaxErrorReplacement { + message: syntax_error, + end_offset: column + 2, + }), + }) } fn warn_syntax_at_offset( @@ -678,6 +712,7 @@ mod escape_warnings { filename: filename.to_owned(), lineno: location.line.get(), offset: location.character_offset.get(), + replacement: None, }) } diff --git a/crates/vm/src/vm/python_run.rs b/crates/vm/src/vm/python_run.rs index 11db4b78b..f2e1ce0c1 100644 --- a/crates/vm/src/vm/python_run.rs +++ b/crates/vm/src/vm/python_run.rs @@ -55,8 +55,8 @@ impl VirtualMachine { #[cfg(feature = "host_env")] mod file_run { use crate::{ - Py, PyResult, VirtualMachine, - builtins::{PyCode, PyDict}, + AsObject, Py, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyCode, PyDict}, compiler::{self}, scope::Scope, }; @@ -107,11 +107,8 @@ mod file_run { } match crate::host_env::fs::read(path) { Ok(source_bytes) => { - if source_bytes.contains(&0) { - return Err(self.new_exception_msg( - self.ctx.exceptions.syntax_error.to_owned(), - "source code cannot contain null bytes".into(), - )); + if let Some(null_at) = source_bytes.iter().position(|&b| b == 0) { + return Err(null_byte_syntax_error(self, path, &source_bytes, null_at)); } #[cfg(feature = "parser")] // Match compile() by honoring BOMs and encoding cookies in files. @@ -138,6 +135,31 @@ mod file_run { } } + fn null_byte_syntax_error( + vm: &VirtualMachine, + path: &str, + source_bytes: &[u8], + null_at: usize, + ) -> PyBaseExceptionRef { + let before = &source_bytes[..null_at]; + let lineno = before.iter().filter(|&&b| b == b'\n').count() + 1; + let line_start = before + .iter() + .rposition(|&b| b == b'\n') + .map_or(0, |i| i + 1); + let line = String::from_utf8_lossy(&source_bytes[line_start..null_at]); + let syntax_error = vm.new_exception_msg( + vm.ctx.exceptions.syntax_error.to_owned(), + "source code cannot contain null bytes".into(), + ); + let obj = syntax_error.as_object(); + obj.set_attr("filename", vm.ctx.new_str(path), vm).unwrap(); + obj.set_attr("lineno", vm.ctx.new_int(lineno), vm).unwrap(); + obj.set_attr("text", vm.ctx.new_str(format!("{line}\n")), vm) + .unwrap(); + syntax_error + } + fn set_main_loader( module_dict: &Py, filename: &str, diff --git a/src/settings.rs b/src/settings.rs index 254386da3..da36d9888 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -136,7 +136,7 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { Short('I') => args.isolate = true, Short('m') => { let module = parser.value()?.string()?; - let argv = argv("PLACEHOLDER".to_owned(), parser)?; + let argv = argv("-m".to_owned(), parser)?; return Ok((args, RunMode::Module(module), argv)); } Short('O') => args.optimize += 1,