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.
This commit is contained in:
Shannon Booth 2026-06-20 00:39:56 +02:00 committed by Andreas Kling
parent 3a0f857cc1
commit 250842d36a
7 changed files with 35 additions and 79 deletions

1
Cargo.lock generated
View file

@ -741,6 +741,7 @@ name = "liburl_rust"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cbindgen", "cbindgen",
"encoding_rs",
"libregex_rust", "libregex_rust",
"libunicode_rust", "libunicode_rust",
] ]

View file

@ -12,5 +12,5 @@ ladybird_lib(LibURL url)
target_link_libraries(LibURL PRIVATE liburl_rust LibUnicode LibTextCodec LibRegex PkgConfig::LIBPSL) 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) 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) target_compile_definitions(LibURL PRIVATE ENABLE_RUST)

View file

@ -7,6 +7,7 @@ edition = "2024"
crate-type = ["staticlib"] crate-type = ["staticlib"]
[dependencies] [dependencies]
encoding_rs = "0.8.35"
libregex_rust = { path = "../../LibRegex/Rust" } libregex_rust = { path = "../../LibRegex/Rust" }
libunicode_rust = { path = "../../LibUnicode/Rust" } libunicode_rust = { path = "../../LibUnicode/Rust" }

View file

@ -4,7 +4,8 @@
* SPDX-License-Identifier: BSD-2-Clause * 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)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum EncodeItem { pub(crate) enum EncodeItem {
@ -12,37 +13,6 @@ pub(crate) enum EncodeItem {
Error(u32), 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 // https://encoding.spec.whatwg.org/#get-an-output-encoding
pub(crate) fn get_output_encoding(encoding: &str) -> &str { pub(crate) fn get_output_encoding(encoding: &str) -> &str {
// 1. If encoding is replacement or UTF-16BE/LE, then return UTF-8. // 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 { 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. let mut encoder = encoding.new_encoder();
unsafe { let mut total_read = 0usize;
textcodec_rust_encode( let Some(output_capacity) = encoder.max_buffer_length_from_utf8_without_replacement(input.len()) else {
encoding.as_ptr(), return false;
encoding.len(), };
input.as_ptr(), let mut output = Vec::with_capacity(output_capacity);
input.len(),
std::ptr::addr_of_mut!(callbacks) as *mut c_void, loop {
on_encode_byte_with_callbacks, let (result, read) =
on_encode_error_with_callbacks, 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)),
}
} }
} }

View file

@ -177,10 +177,7 @@ pub(super) fn percent_encode_after_encoding(
result.push_str("%3B"); result.push_str("%3B");
} }
}); });
assert!( assert!(did_succeed, "encoding_rs should encode any valid output encoding");
did_succeed,
"TextCodec::encode should succeed for a valid output encoding"
);
// 6. Return output. // 6. Return output.
result result

View file

@ -11,7 +11,6 @@
#include <AK/StringUtils.h> #include <AK/StringUtils.h>
#include <AK/Utf8View.h> #include <AK/Utf8View.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibTextCodec/Encoder.h>
#include <LibURL/Parser.h> #include <LibURL/Parser.h>
#include <LibURL/RustFFI.h> #include <LibURL/RustFFI.h>
#include <LibURL/RustIntegration.h> #include <LibURL/RustIntegration.h>
@ -480,26 +479,3 @@ Optional<URL> parse_basic_url(StringView input, Optional<URL const&> 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<void> {
on_byte(ctx, byte);
return {};
},
[&](u32 error) -> ErrorOr<void> {
on_error(ctx, error);
return {};
});
return !result.is_error();
}
}

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 12 tests Found 12 tests
8 Pass 12 Pass
4 Fail
Pass iso-2022-jp encoder: very basic Pass iso-2022-jp encoder: very basic
Pass iso-2022-jp encoder: basics Pass iso-2022-jp encoder: basics
Pass iso-2022-jp encoder: Katakana 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: Roman SO/SI ESC
Pass iso-2022-jp encoder: Katakana SO/SI ESC Pass iso-2022-jp encoder: Katakana SO/SI ESC
Pass iso-2022-jp encoder: jis0208 SO/SI ESC Pass iso-2022-jp encoder: jis0208 SO/SI ESC
Fail iso-2022-jp encoder: U+FFFD Pass iso-2022-jp encoder: U+FFFD
Fail iso-2022-jp encoder: Roman U+FFFD Pass iso-2022-jp encoder: Roman U+FFFD
Fail iso-2022-jp encoder: Katakana U+FFFD Pass iso-2022-jp encoder: Katakana U+FFFD
Fail iso-2022-jp encoder: jis0208 U+FFFD Pass iso-2022-jp encoder: jis0208 U+FFFD