ladybird/Libraries/LibTextCodec/Rust/src/lib.rs
Shannon Booth 7b0dd6ab30 LibTextCodec: Use encoding_rs for legacy codecs
Replace the generated C++ legacy codec implementations with a
small Rust wrapper around encoding_rs.

This keeps the existing LibTextCodec API while moving label lookup,
legacy decode/encode, validation, and streaming decoder state to Rust.
The generated index data and generator are no longer needed.

It also fixes several TextDecoder EOF cases due to a more correct
implementation. encoding_rs finalizes decoders according to the
Encoding Standard, so incomplete UTF-8/Big5 tails and malformed
UTF-16 surrogate tails produce the required single replacement at
end-of-queue instead of being dropped, buffered, or double-counted
by our old hand-written decoders.
2026-06-20 21:56:43 +02:00

298 lines
9.6 KiB
Rust

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
use encoding_rs::CoderResult;
use encoding_rs::EncoderResult;
use encoding_rs::Encoding;
use std::ffi::c_void;
use std::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
type FfiBytesFn = unsafe extern "C" fn(ctx: *mut c_void, data: *const u8, len: usize);
type FfiCodePointFn = unsafe extern "C" fn(ctx: *mut c_void, code_point: u32);
pub struct TextCodecRustStreamingDecoder {
decoder: encoding_rs::Decoder,
}
fn abort_on_panic<F: FnOnce() -> R, R>(f: F) -> R {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(result) => result,
Err(payload) => {
let message = if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"unknown panic".to_string()
};
eprintln!("Rust panic at FFI boundary: {message}");
std::process::abort();
}
}
}
unsafe fn bytes_from_raw<'a>(bytes: *const u8, len: usize) -> Option<&'a [u8]> {
unsafe {
if len == 0 {
return Some(&[]);
}
if bytes.is_null() {
eprintln!("bytes_from_raw: null pointer with non-zero length {len}");
return None;
}
Some(std::slice::from_raw_parts(bytes, len))
}
}
unsafe fn set_static_string(out: *mut *const u8, out_len: *mut usize, value: &'static str) -> bool {
unsafe {
if out.is_null() || out_len.is_null() {
eprintln!("set_static_string: null output pointer");
return false;
}
*out = value.as_ptr();
*out_len = value.len();
true
}
}
/// # Safety
/// - `label`/`label_len` must be a valid byte slice.
/// - `out_name` and `out_name_len` must be valid writable pointers.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_get_standardized_encoding(
label: *const u8,
label_len: usize,
out_name: *mut *const u8,
out_name_len: *mut usize,
) -> bool {
unsafe {
abort_on_panic(|| {
let Some(label) = bytes_from_raw(label, label_len) else {
return false;
};
let Some(encoding) = Encoding::for_label(label) else {
return false;
};
set_static_string(out_name, out_name_len, encoding.name())
})
}
}
/// # Safety
/// - `encoding_label`/`encoding_label_len` and `input`/`input_len` must be valid byte slices.
/// - `on_bytes` must not retain `data` beyond the duration of the callback.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_decode_to_utf8(
encoding_label: *const u8,
encoding_label_len: usize,
input: *const u8,
input_len: usize,
remove_bom: bool,
ctx: *mut c_void,
on_bytes: FfiBytesFn,
) -> bool {
unsafe {
abort_on_panic(|| {
let Some(label) = bytes_from_raw(encoding_label, encoding_label_len) else {
return false;
};
let Some(input) = bytes_from_raw(input, input_len) else {
return false;
};
let Some(encoding) = Encoding::for_label(label) else {
return false;
};
let (output, _) = if remove_bom {
encoding.decode_with_bom_removal(input)
} else {
encoding.decode_without_bom_handling(input)
};
on_bytes(ctx, output.as_bytes().as_ptr(), output.len());
true
})
}
}
/// # Safety
/// - `encoding_label`/`encoding_label_len` and `input`/`input_len` must be valid byte slices.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_validate(
encoding_label: *const u8,
encoding_label_len: usize,
input: *const u8,
input_len: usize,
remove_bom: bool,
) -> bool {
unsafe {
abort_on_panic(|| {
let Some(label) = bytes_from_raw(encoding_label, encoding_label_len) else {
return false;
};
let Some(input) = bytes_from_raw(input, input_len) else {
return false;
};
let Some(encoding) = Encoding::for_label(label) else {
return false;
};
let input = if remove_bom {
if encoding == encoding_rs::UTF_8 && input.starts_with(b"\xEF\xBB\xBF") {
&input[3..]
} else if (encoding == encoding_rs::UTF_16LE && input.starts_with(b"\xFF\xFE"))
|| (encoding == encoding_rs::UTF_16BE && input.starts_with(b"\xFE\xFF"))
{
&input[2..]
} else {
input
}
} else {
input
};
encoding
.decode_without_bom_handling_and_without_replacement(input)
.is_some()
})
}
}
/// # Safety
/// - `encoding_label`/`encoding_label_len` must be a valid byte slice.
/// - The returned pointer must be freed with `textcodec_rust_streaming_decoder_free`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_streaming_decoder_new(
encoding_label: *const u8,
encoding_label_len: usize,
remove_bom: bool,
) -> *mut TextCodecRustStreamingDecoder {
unsafe {
abort_on_panic(|| {
let Some(label) = bytes_from_raw(encoding_label, encoding_label_len) else {
return std::ptr::null_mut();
};
let Some(encoding) = Encoding::for_label(label) else {
return std::ptr::null_mut();
};
let decoder = if remove_bom {
encoding.new_decoder_with_bom_removal()
} else {
encoding.new_decoder_without_bom_handling()
};
Box::into_raw(Box::new(TextCodecRustStreamingDecoder { decoder }))
})
}
}
/// # Safety
/// - `decoder` must be null or a pointer returned by `textcodec_rust_streaming_decoder_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_streaming_decoder_free(decoder: *mut TextCodecRustStreamingDecoder) {
unsafe {
abort_on_panic(|| {
if !decoder.is_null() {
drop(Box::from_raw(decoder));
}
});
}
}
/// # Safety
/// - `decoder` must be a valid pointer returned by `textcodec_rust_streaming_decoder_new`.
/// - `input`/`input_len` must be a valid byte slice.
/// - `on_bytes` must not retain `data` beyond the duration of the callback.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_streaming_decoder_decode_to_utf8(
decoder: *mut TextCodecRustStreamingDecoder,
input: *const u8,
input_len: usize,
last: bool,
ctx: *mut c_void,
on_bytes: FfiBytesFn,
) -> bool {
unsafe {
abort_on_panic(|| {
if decoder.is_null() {
eprintln!("textcodec_rust_streaming_decoder_decode_to_utf8: null decoder pointer");
return false;
}
let Some(input) = bytes_from_raw(input, input_len) else {
return false;
};
let decoder = &mut *decoder;
let Some(output_capacity) = decoder.decoder.max_utf8_buffer_length(input.len()) else {
return false;
};
let mut output = String::with_capacity(output_capacity);
let (result, _, _) = decoder.decoder.decode_to_string(input, &mut output, last);
if !output.is_empty() {
on_bytes(ctx, output.as_ptr(), output.len());
}
matches!(result, CoderResult::InputEmpty)
})
}
}
/// # Safety
/// - `encoding_label`/`encoding_label_len` and `input`/`input_len` must be valid byte slices.
/// - `on_bytes` and `on_error` must not retain data beyond their callback duration.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn textcodec_rust_encode_from_utf8(
encoding_label: *const u8,
encoding_label_len: usize,
input: *const u8,
input_len: usize,
ctx: *mut c_void,
on_bytes: FfiBytesFn,
on_error: FfiCodePointFn,
) -> bool {
unsafe {
abort_on_panic(|| {
let Some(label) = bytes_from_raw(encoding_label, encoding_label_len) else {
return false;
};
let Some(input) = bytes_from_raw(input, input_len) else {
return false;
};
let Ok(input) = std::str::from_utf8(input) else {
return false;
};
let Some(encoding) = Encoding::for_label(label) else {
return false;
};
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;
if !output.is_empty() {
on_bytes(ctx, output.as_ptr(), output.len());
output.clear();
}
match result {
EncoderResult::InputEmpty => return true,
EncoderResult::OutputFull => return false,
EncoderResult::Unmappable(unmappable) => {
on_error(ctx, unmappable as u32);
}
}
}
})
}
}