Skip to content

Commit 47b6766

Browse files
committed
Report the full invalid UTF-8 span in the converted decode errors
The conversions hard-coded the decode-error span as valid_up_to() + 1, which under-reports multi-byte invalid sequences. Derive the span from the Utf8Error instead: valid_up_to() + error_len(), or to the end of the input for a truncated sequence (error_len() == None), matching CPython. Centralize this in a new_unicode_decode_error_utf8 helper so the offset logic lives in one place. Assisted-by: Claude Code:claude-fable-5
1 parent 02c70c4 commit 47b6766

6 files changed

Lines changed: 28 additions & 46 deletions

File tree

crates/stdlib/src/csv.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,7 @@ mod _csv {
6565
bytes: &[u8],
6666
err: core::str::Utf8Error,
6767
) -> PyBaseExceptionRef {
68-
vm.new_unicode_decode_error_real(
69-
vm.ctx.new_str("utf-8"),
70-
vm.ctx.new_bytes(bytes.to_vec()),
71-
err.valid_up_to(),
72-
err.valid_up_to() + 1,
73-
vm.ctx.new_str("csv not utf8"),
74-
)
68+
vm.new_unicode_decode_error_utf8(bytes, err, "csv not utf8")
7569
}
7670

7771
#[pyattr]

crates/stdlib/src/socket.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,13 +2613,7 @@ mod _socket {
26132613
Some(ArgStrOrBytesLike::Buf(b)) => {
26142614
let bytes = b.borrow_buf();
26152615
let host_str = core::str::from_utf8(&bytes).map_err(|e| {
2616-
vm.new_unicode_decode_error_real(
2617-
vm.ctx.new_str("utf-8"),
2618-
vm.ctx.new_bytes(bytes.to_vec()),
2619-
e.valid_up_to(),
2620-
e.valid_up_to() + 1,
2621-
vm.ctx.new_str("host bytes is not utf8"),
2622-
)
2616+
vm.new_unicode_decode_error_utf8(&bytes, e, "host bytes is not utf8")
26232617
})?;
26242618
Some(host_str.to_owned())
26252619
}
@@ -2655,13 +2649,7 @@ mod _socket {
26552649
let bytes = b.borrow_buf();
26562650
core::str::from_utf8(&bytes)
26572651
.map_err(|e| {
2658-
vm.new_unicode_decode_error_real(
2659-
vm.ctx.new_str("utf-8"),
2660-
vm.ctx.new_bytes(bytes.to_vec()),
2661-
e.valid_up_to(),
2662-
e.valid_up_to() + 1,
2663-
vm.ctx.new_str("port is not utf8"),
2664-
)
2652+
vm.new_unicode_decode_error_utf8(&bytes, e, "port is not utf8")
26652653
})?
26662654
.to_owned()
26672655
}

crates/vm/src/function/fspath.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,8 @@ impl FsPath {
125125
}
126126

127127
pub fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ffi::OsStr> {
128-
rustpython_host_env::os::bytes_as_os_str(b).map_err(|e| {
129-
vm.new_unicode_decode_error_real(
130-
vm.ctx.new_str("utf-8"),
131-
vm.ctx.new_bytes(b.to_vec()),
132-
e.valid_up_to(),
133-
e.valid_up_to() + 1,
134-
vm.ctx.new_str("can't decode path for utf-8"),
135-
)
136-
})
128+
rustpython_host_env::os::bytes_as_os_str(b)
129+
.map_err(|e| vm.new_unicode_decode_error_utf8(b, e, "can't decode path for utf-8"))
137130
}
138131
}
139132

crates/vm/src/stdlib/os.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,8 @@ pub(super) struct FollowSymlinks(
130130

131131
#[cfg(not(windows))]
132132
fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ffi::OsStr> {
133-
rustpython_host_env::os::bytes_as_os_str(b).map_err(|e| {
134-
vm.new_unicode_decode_error_real(
135-
vm.ctx.new_str("utf-8"),
136-
vm.ctx.new_bytes(b.to_vec()),
137-
e.valid_up_to(),
138-
e.valid_up_to() + 1,
139-
vm.ctx.new_str("can't decode path for utf-8"),
140-
)
141-
})
133+
rustpython_host_env::os::bytes_as_os_str(b)
134+
.map_err(|e| vm.new_unicode_decode_error_utf8(b, e, "can't decode path for utf-8"))
142135
}
143136

144137
pub(crate) fn warn_if_bool_fd(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {

crates/vm/src/stdlib/posix.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,13 +1732,7 @@ pub mod module {
17321732
return Err(vm.new_os_error("unable to determine login name"));
17331733
};
17341734
login.to_str().map(|s| s.to_owned()).map_err(|e| {
1735-
vm.new_unicode_decode_error_real(
1736-
vm.ctx.new_str("utf-8"),
1737-
vm.ctx.new_bytes(login.as_bytes().to_vec()),
1738-
e.valid_up_to(),
1739-
e.valid_up_to() + 1,
1740-
vm.ctx.new_str("unable to decode login name"),
1741-
)
1735+
vm.new_unicode_decode_error_utf8(login.as_bytes(), e, "unable to decode login name")
17421736
})
17431737
}
17441738

crates/vm/src/vm/vm_new.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,26 @@ impl VirtualMachine {
529529
exc
530530
}
531531

532+
/// Build a `UnicodeDecodeError` for a failed UTF-8 decode of `bytes`, taking
533+
/// the invalid span from `err`: the whole invalid sequence, or up to the end
534+
/// of the input for a truncated one (`error_len() == None`).
535+
pub fn new_unicode_decode_error_utf8(
536+
&self,
537+
bytes: &[u8],
538+
err: core::str::Utf8Error,
539+
reason: &str,
540+
) -> PyBaseExceptionRef {
541+
let start = err.valid_up_to();
542+
let end = err.error_len().map_or(bytes.len(), |len| start + len);
543+
self.new_unicode_decode_error_real(
544+
self.ctx.new_str("utf-8"),
545+
self.ctx.new_bytes(bytes.to_vec()),
546+
start,
547+
end,
548+
self.ctx.new_str(reason),
549+
)
550+
}
551+
532552
pub fn new_unicode_encode_error_real(
533553
&self,
534554
encoding: PyStrRef,

0 commit comments

Comments
 (0)