From 4c9b879ecb5eca5b8f0830c40ea24012f7e1ee31 Mon Sep 17 00:00:00 2001 From: Andrei Homescu Date: Fri, 24 Jul 2026 13:10:27 -0700 Subject: [PATCH 1/2] refactor: add test for non-ASCII identifiers --- c2rust-refactor/tests/snapshots.rs | 13 +++++++++ .../snapshots/reorganize_non_ascii_ident.rs | 28 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs diff --git a/c2rust-refactor/tests/snapshots.rs b/c2rust-refactor/tests/snapshots.rs index c2301d72bc..e3fb8b8efc 100644 --- a/c2rust-refactor/tests/snapshots.rs +++ b/c2rust-refactor/tests/snapshots.rs @@ -518,6 +518,19 @@ fn test_reorganize_multi_namespace() { .test(); } +/// TODO Broken. +/// `find_destination_id` compares a header module's name against a candidate +/// destination's by slicing the header name at the destination name's length +/// in *bytes*, so a destination whose name length lands inside a multi-byte +/// character of the header name panics in `str::split_at`. +#[should_panic(expected = "byte index 1 is not a char boundary")] +#[test] +fn test_reorganize_non_ascii_ident() { + refactor("reorganize_definitions") + .named("reorganize_non_ascii_ident.rs") + .test(); +} + #[test] fn test_reorganize_self_import_destination() { refactor("reorganize_definitions") diff --git a/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs b/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs new file mode 100644 index 0000000000..09302a4752 --- /dev/null +++ b/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs @@ -0,0 +1,28 @@ +#![feature(register_tool)] +#![register_tool(c2rust)] +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +// The destination module's name is one byte long, and the header module it +// contains starts with a two-byte character. `find_destination_id` tests +// whether the header name starts with the module name by slicing the header +// name at `module_ident.len()` *bytes*, which lands in the middle of the `é` +// and panics. +pub mod a { + #[c2rust::header_src = "/home/user/some/workspace/é.h:1"] + pub mod é_h { + #[c2rust::src_loc = "2:0"] + #[derive(Copy, Clone)] + #[repr(C)] + pub struct thing { + pub x: i32, + } + } + + pub fn go() -> i32 { + let t = é_h::thing { x: 1 }; + t.x + } +} + +fn main() {} From 65eed9ce42b981600fd484da0c158a1fcc7c8c72 Mon Sep 17 00:00:00 2001 From: Andrei Homescu Date: Fri, 24 Jul 2026 13:26:14 -0700 Subject: [PATCH 2/2] refactor: reorganize_definitions: fix for non-ASCII identifiers --- .../src/transform/reorganize_definitions.rs | 12 +++--- c2rust-refactor/tests/snapshots.rs | 16 +++++--- .../snapshots/reorganize_non_ascii_ident.rs | 28 +++++++++---- ...nitions-reorganize_non_ascii_ident.rs.snap | 40 +++++++++++++++++++ 4 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 c2rust-refactor/tests/snapshots/snapshots__refactor-reorganize_definitions-reorganize_non_ascii_ident.rs.snap diff --git a/c2rust-refactor/src/transform/reorganize_definitions.rs b/c2rust-refactor/src/transform/reorganize_definitions.rs index f652329e7b..d77b30678a 100644 --- a/c2rust-refactor/src/transform/reorganize_definitions.rs +++ b/c2rust-refactor/src/transform/reorganize_definitions.rs @@ -267,13 +267,15 @@ impl<'a, 'tcx> Reorganizer<'a, 'tcx> { return false; } + // The header belongs to this module if it is named after it, + // either exactly or with the usual `_h` suffix. Splitting the + // header name at the module name's length in bytes would panic + // when that offset falls inside a multi-byte character. let header_ident = declaration.parent_header.ident.as_str(); let module_ident = dest_module_info.orig_ident.as_str(); - if header_ident.len() >= module_ident.len() { - let (base, ext) = header_ident.split_at(module_ident.len()); - base == &*module_ident && (ext.is_empty() || ext == "_h") - } else { - false + match header_ident.strip_prefix(&*module_ident) { + Some(ext) => ext.is_empty() || ext == "_h", + None => false, } }); let dest_module = match dest_module { diff --git a/c2rust-refactor/tests/snapshots.rs b/c2rust-refactor/tests/snapshots.rs index e3fb8b8efc..031bbe56f5 100644 --- a/c2rust-refactor/tests/snapshots.rs +++ b/c2rust-refactor/tests/snapshots.rs @@ -518,12 +518,16 @@ fn test_reorganize_multi_namespace() { .test(); } -/// TODO Broken. -/// `find_destination_id` compares a header module's name against a candidate -/// destination's by slicing the header name at the destination name's length -/// in *bytes*, so a destination whose name length lands inside a multi-byte -/// character of the header name panics in `str::split_at`. -#[should_panic(expected = "byte index 1 is not a char boundary")] +/// `find_destination_id` decides whether a header belongs to a candidate +/// destination by comparing their names, which must not assume either is +/// ASCII: slicing the header name at the destination name's length in *bytes* +/// panics when that offset falls inside a multi-byte character. +/// +/// Module `a` is one byte long, so comparing it against `ü_h` used to split +/// the `ü` in half; the names don't match, so `thing` moves to a new module. +/// Module `é` is the matching case, pinning down that non-ASCII names are +/// still compared correctly rather than merely never matching: `é_h` is named +/// after its parent, so `other` moves into it. #[test] fn test_reorganize_non_ascii_ident() { refactor("reorganize_definitions") diff --git a/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs b/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs index 09302a4752..ed436addd1 100644 --- a/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs +++ b/c2rust-refactor/tests/snapshots/reorganize_non_ascii_ident.rs @@ -3,14 +3,9 @@ #![allow(non_camel_case_types)] #![allow(dead_code)] -// The destination module's name is one byte long, and the header module it -// contains starts with a two-byte character. `find_destination_id` tests -// whether the header name starts with the module name by slicing the header -// name at `module_ident.len()` *bytes*, which lands in the middle of the `é` -// and panics. pub mod a { - #[c2rust::header_src = "/home/user/some/workspace/é.h:1"] - pub mod é_h { + #[c2rust::header_src = "/home/user/some/workspace/ü.h:1"] + pub mod ü_h { #[c2rust::src_loc = "2:0"] #[derive(Copy, Clone)] #[repr(C)] @@ -20,9 +15,26 @@ pub mod a { } pub fn go() -> i32 { - let t = é_h::thing { x: 1 }; + let t = ü_h::thing { x: 1 }; t.x } } +pub mod é { + #[c2rust::header_src = "/home/user/some/workspace/é.h:3"] + pub mod é_h { + #[c2rust::src_loc = "4:0"] + #[derive(Copy, Clone)] + #[repr(C)] + pub struct other { + pub y: i32, + } + } + + pub fn go() -> i32 { + let o = é_h::other { y: 2 }; + o.y + } +} + fn main() {} diff --git a/c2rust-refactor/tests/snapshots/snapshots__refactor-reorganize_definitions-reorganize_non_ascii_ident.rs.snap b/c2rust-refactor/tests/snapshots/snapshots__refactor-reorganize_definitions-reorganize_non_ascii_ident.rs.snap new file mode 100644 index 0000000000..85866c767d --- /dev/null +++ b/c2rust-refactor/tests/snapshots/snapshots__refactor-reorganize_definitions-reorganize_non_ascii_ident.rs.snap @@ -0,0 +1,40 @@ +--- +source: c2rust-refactor/tests/snapshots.rs +expression: c2rust-refactor reorganize_definitions --rewrite-mode alongside -- tests/snapshots/reorganize_non_ascii_ident.rs --edition 2021 +--- +#![feature(register_tool)] +#![register_tool(c2rust)] +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub mod ü_h { + #[derive(Copy, Clone)] + #[repr(C)] + pub struct thing { + pub x: i32, + } +} +pub mod a { + + pub fn go() -> i32 { + let t = crate::ü_h::thing { x: 1 }; + t.x + } +} + +pub mod é { + + // =============== BEGIN é_h ================ + #[derive(Copy, Clone)] + #[repr(C)] + pub struct other { + pub y: i32, + } + + pub fn go() -> i32 { + let o = crate::é::other { y: 2 }; + o.y + } +} + +fn main() {}