From 250842d36af4312688f49a0c00f812fb585dc3d4 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 20 Jun 2026 00:39:56 +0200 Subject: [PATCH] LibURL: Use encoding_rs for form URL encoding Replace the Rust URL form-encoding callback bridge into C++ TextCodec with a direct encoding_rs encoder. This keeps percent-encode-after-encoding entirely in Rust and removes liburl_rust's dependency on LibTextCodec. It also happens to fix ISO-2022-JP URL encoding of literal U+FFFD. The LibTextCodec reverse lookup treats generated 0xFFFD table holes as real JIS0208 mappings, so literal U+FFFD skipped the encoder-error path. encoding_rs treats U+FFFD as unmappable, so URL encoding emits the required numeric character reference. --- Cargo.lock | 1 + Libraries/LibURL/CMakeLists.txt | 2 +- Libraries/LibURL/Rust/Cargo.toml | 1 + Libraries/LibURL/Rust/src/textcodec.rs | 70 +++++++------------ .../LibURL/Rust/src/url/percent_encoding.rs | 5 +- Libraries/LibURL/RustIntegration.cpp | 24 ------- .../encoding/iso-2022-jp-encoder.txt | 11 ++- 7 files changed, 35 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d603594cca..58a4180425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,6 +741,7 @@ name = "liburl_rust" version = "0.1.0" dependencies = [ "cbindgen", + "encoding_rs", "libregex_rust", "libunicode_rust", ] diff --git a/Libraries/LibURL/CMakeLists.txt b/Libraries/LibURL/CMakeLists.txt index b80226baff..ee04d1f5bf 100644 --- a/Libraries/LibURL/CMakeLists.txt +++ b/Libraries/LibURL/CMakeLists.txt @@ -12,5 +12,5 @@ ladybird_lib(LibURL url) target_link_libraries(LibURL PRIVATE liburl_rust LibUnicode LibTextCodec LibRegex PkgConfig::LIBPSL) import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME liburl_rust FEATURES allocator FFI_HEADER RustFFI.h) -target_link_libraries(liburl_rust INTERFACE LibUnicode LibTextCodec LibRegex) +target_link_libraries(liburl_rust INTERFACE LibUnicode LibRegex) target_compile_definitions(LibURL PRIVATE ENABLE_RUST) diff --git a/Libraries/LibURL/Rust/Cargo.toml b/Libraries/LibURL/Rust/Cargo.toml index 7934af24a8..3f2d2d44d5 100644 --- a/Libraries/LibURL/Rust/Cargo.toml +++ b/Libraries/LibURL/Rust/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" crate-type = ["staticlib"] [dependencies] +encoding_rs = "0.8.35" libregex_rust = { path = "../../LibRegex/Rust" } libunicode_rust = { path = "../../LibUnicode/Rust" } diff --git a/Libraries/LibURL/Rust/src/textcodec.rs b/Libraries/LibURL/Rust/src/textcodec.rs index b0bb39deaf..9ce077c5ee 100644 --- a/Libraries/LibURL/Rust/src/textcodec.rs +++ b/Libraries/LibURL/Rust/src/textcodec.rs @@ -4,7 +4,8 @@ * SPDX-License-Identifier: BSD-2-Clause */ -use std::ffi::c_void; +use encoding_rs::EncoderResult; +use encoding_rs::Encoding; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum EncodeItem { @@ -12,37 +13,6 @@ pub(crate) enum EncodeItem { Error(u32), } -type FfiByteFn = unsafe extern "C" fn(*mut c_void, u8); -type FfiCodePointFn = unsafe extern "C" fn(*mut c_void, u32); - -unsafe extern "C" { - fn textcodec_rust_encode( - encoding: *const u8, - encoding_length: usize, - input: *const u8, - input_length: usize, - ctx: *mut c_void, - on_byte: FfiByteFn, - on_error: FfiCodePointFn, - ) -> bool; -} - -struct EncodeCallbacks<'a> { - on_item: &'a mut dyn FnMut(EncodeItem), -} - -unsafe extern "C" fn on_encode_byte_with_callbacks(ctx: *mut c_void, byte: u8) { - // SAFETY: `ctx` was set to `addr_of_mut!(callbacks)` in `encode_into`. - let callbacks = unsafe { &mut *(ctx as *mut EncodeCallbacks<'_>) }; - (callbacks.on_item)(EncodeItem::Byte(byte)); -} - -unsafe extern "C" fn on_encode_error_with_callbacks(ctx: *mut c_void, error: u32) { - // SAFETY: `ctx` was set to `addr_of_mut!(callbacks)` in `encode_into`. - let callbacks = unsafe { &mut *(ctx as *mut EncodeCallbacks<'_>) }; - (callbacks.on_item)(EncodeItem::Error(error)); -} - // https://encoding.spec.whatwg.org/#get-an-output-encoding pub(crate) fn get_output_encoding(encoding: &str) -> &str { // 1. If encoding is replacement or UTF-16BE/LE, then return UTF-8. @@ -58,18 +28,30 @@ pub(crate) fn get_output_encoding(encoding: &str) -> &str { } pub(crate) fn encode_into(encoding: &str, input: &str, mut on_item: impl FnMut(EncodeItem)) -> bool { - let mut callbacks = EncodeCallbacks { on_item: &mut on_item }; + let Some(encoding) = Encoding::for_label(encoding.as_bytes()) else { + return false; + }; - // SAFETY: `encoding`, `input`, and `callbacks` are valid for the duration of the call. - unsafe { - textcodec_rust_encode( - encoding.as_ptr(), - encoding.len(), - input.as_ptr(), - input.len(), - std::ptr::addr_of_mut!(callbacks) as *mut c_void, - on_encode_byte_with_callbacks, - on_encode_error_with_callbacks, - ) + let mut encoder = encoding.new_encoder(); + let mut total_read = 0usize; + let Some(output_capacity) = encoder.max_buffer_length_from_utf8_without_replacement(input.len()) else { + return false; + }; + let mut output = Vec::with_capacity(output_capacity); + + loop { + let (result, read) = + encoder.encode_from_utf8_to_vec_without_replacement(&input[total_read..], &mut output, true); + total_read += read; + + for byte in output.drain(..) { + on_item(EncodeItem::Byte(byte)); + } + + match result { + EncoderResult::InputEmpty => return true, + EncoderResult::OutputFull => return false, + EncoderResult::Unmappable(unmappable) => on_item(EncodeItem::Error(unmappable as u32)), + } } } diff --git a/Libraries/LibURL/Rust/src/url/percent_encoding.rs b/Libraries/LibURL/Rust/src/url/percent_encoding.rs index 875e191793..b6f33816d1 100644 --- a/Libraries/LibURL/Rust/src/url/percent_encoding.rs +++ b/Libraries/LibURL/Rust/src/url/percent_encoding.rs @@ -177,10 +177,7 @@ pub(super) fn percent_encode_after_encoding( result.push_str("%3B"); } }); - assert!( - did_succeed, - "TextCodec::encode should succeed for a valid output encoding" - ); + assert!(did_succeed, "encoding_rs should encode any valid output encoding"); // 6. Return output. result diff --git a/Libraries/LibURL/RustIntegration.cpp b/Libraries/LibURL/RustIntegration.cpp index e1bc1c252d..a4d213a754 100644 --- a/Libraries/LibURL/RustIntegration.cpp +++ b/Libraries/LibURL/RustIntegration.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -480,26 +479,3 @@ Optional parse_basic_url(StringView input, Optional base_url, U } } - -namespace URL::FFI { - -extern "C" bool textcodec_rust_encode(u8 const* encoding, size_t encoding_length, u8 const* input, size_t input_length, void* ctx, FfiByteFn on_byte, FfiCodePointFn on_error) -{ - auto encoder = TextCodec::encoder_for(StringView { encoding, encoding_length }); - if (!encoder.has_value()) - return false; - - auto result = encoder->process( - Utf8View { StringView { input, input_length } }, - [&](u8 byte) -> ErrorOr { - on_byte(ctx, byte); - return {}; - }, - [&](u32 error) -> ErrorOr { - on_error(ctx, error); - return {}; - }); - return !result.is_error(); -} - -} diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/iso-2022-jp-encoder.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/iso-2022-jp-encoder.txt index 4e9b07c487..3079a4713b 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/encoding/iso-2022-jp-encoder.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/iso-2022-jp-encoder.txt @@ -2,8 +2,7 @@ Harness status: OK Found 12 tests -8 Pass -4 Fail +12 Pass Pass iso-2022-jp encoder: very basic Pass iso-2022-jp encoder: basics Pass iso-2022-jp encoder: Katakana @@ -12,7 +11,7 @@ Pass iso-2022-jp encoder: SO/SI ESC Pass iso-2022-jp encoder: Roman SO/SI ESC Pass iso-2022-jp encoder: Katakana SO/SI ESC Pass iso-2022-jp encoder: jis0208 SO/SI ESC -Fail iso-2022-jp encoder: U+FFFD -Fail iso-2022-jp encoder: Roman U+FFFD -Fail iso-2022-jp encoder: Katakana U+FFFD -Fail iso-2022-jp encoder: jis0208 U+FFFD \ No newline at end of file +Pass iso-2022-jp encoder: U+FFFD +Pass iso-2022-jp encoder: Roman U+FFFD +Pass iso-2022-jp encoder: Katakana U+FFFD +Pass iso-2022-jp encoder: jis0208 U+FFFD \ No newline at end of file