Skip to content

Commit 5600d0f

Browse files
haasonsaasclaude
andcommitted
TDD: fix URL percent-encoding of multi-byte UTF-8 characters in symbol_index
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb56f56 commit 5600d0f

File tree

1 file changed

+69
-4
lines changed

1 file changed

+69
-4
lines changed

src/core/symbol_index.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,12 +1181,77 @@ fn path_to_uri(path: &Path) -> Result<String> {
11811181

11821182
fn url_encode(segment: &str) -> String {
11831183
let mut out = String::new();
1184-
for ch in segment.chars() {
1185-
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' || ch == '~' {
1186-
out.push(ch);
1184+
for byte in segment.bytes() {
1185+
if byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_' || byte == b'.' || byte == b'~' {
1186+
out.push(byte as char);
11871187
} else {
1188-
out.push_str(&format!("%{:02X}", ch as u32));
1188+
out.push_str(&format!("%{:02X}", byte));
11891189
}
11901190
}
11911191
out
11921192
}
1193+
1194+
#[cfg(test)]
1195+
mod tests {
1196+
use super::*;
1197+
1198+
#[test]
1199+
fn test_url_encode_ascii() {
1200+
assert_eq!(url_encode("hello"), "hello");
1201+
assert_eq!(url_encode("file.rs"), "file.rs");
1202+
assert_eq!(url_encode("a b"), "a%20b");
1203+
}
1204+
1205+
#[test]
1206+
fn test_url_encode_multibyte_utf8() {
1207+
// '€' is U+20AC, UTF-8 bytes: 0xE2 0x82 0xAC
1208+
// Correct percent-encoding: %E2%82%AC
1209+
let encoded = url_encode("€");
1210+
assert_eq!(
1211+
encoded, "%E2%82%AC",
1212+
"Multi-byte UTF-8 chars must be percent-encoded per byte, got: {}",
1213+
encoded
1214+
);
1215+
}
1216+
1217+
#[test]
1218+
fn test_url_encode_two_byte_utf8() {
1219+
// 'é' is U+00E9, UTF-8 bytes: 0xC3 0xA9
1220+
// Correct percent-encoding: %C3%A9
1221+
let encoded = url_encode("café");
1222+
assert_eq!(
1223+
encoded, "caf%C3%A9",
1224+
"Two-byte UTF-8 chars must be percent-encoded per byte, got: {}",
1225+
encoded
1226+
);
1227+
}
1228+
1229+
#[test]
1230+
fn test_normalize_relative_path() {
1231+
let result = normalize_relative_path(PathBuf::from("src/../lib/./utils.rs"));
1232+
assert_eq!(result, PathBuf::from("lib/utils.rs"));
1233+
}
1234+
1235+
#[test]
1236+
fn test_normalize_relative_path_no_dots() {
1237+
let result = normalize_relative_path(PathBuf::from("src/lib/utils.rs"));
1238+
assert_eq!(result, PathBuf::from("src/lib/utils.rs"));
1239+
}
1240+
1241+
#[test]
1242+
fn test_candidate_paths_with_extension() {
1243+
let candidates = candidate_paths(Path::new("src/lib.rs"));
1244+
assert_eq!(candidates.len(), 1);
1245+
assert_eq!(candidates[0], PathBuf::from("src/lib.rs"));
1246+
}
1247+
1248+
#[test]
1249+
fn test_candidate_paths_without_extension() {
1250+
let candidates = candidate_paths(Path::new("src/lib"));
1251+
// Should include the original + extensions + directory index files
1252+
assert!(candidates.len() > 1);
1253+
assert!(candidates.contains(&PathBuf::from("src/lib")));
1254+
assert!(candidates.contains(&PathBuf::from("src/lib.rs")));
1255+
assert!(candidates.contains(&PathBuf::from("src/lib.py")));
1256+
}
1257+
}

0 commit comments

Comments
 (0)