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.
298 lines
9.6 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|