mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 18:51:13 +08:00
feat: Uint8Array base64/hex 六方法与共享原型 toString
This commit is contained in:
@@ -60,6 +60,8 @@ pub mod string;
|
||||
pub mod stubs;
|
||||
/// Symbol 内置对象实现(constructor/for/keyFor/toString)。
|
||||
pub mod symbol;
|
||||
/// TypedArray base64/hex 编解码纯函数(FromBase64/FromHex 状态机与编码输出)。
|
||||
pub mod ta_codec;
|
||||
/// Temporal 内置对象实现(目录模块树:Now / Instant / PlainDate / PlainTime / PlainDateTime / PlainMonthDay / PlainYearMonth / ZonedDateTime / Duration)。
|
||||
pub mod temporal;
|
||||
/// TypedArray 内置对象实现(元素访问、fill/slice/subarray/set 等)。
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
//! Uint8Array base64/hex 编解码纯函数层(无 VM 依赖)。
|
||||
//!
|
||||
//! 承载 FromBase64/FromHex 状态机与编码输出,供 typed_array.rs 的
|
||||
//! setFromBase64/setFromHex/toBase64/toHex/fromBase64/fromHex 六个内置共用。
|
||||
//! 解码逐块产出 `(read, written)`:`read` 计全部已消费字符(含跳过的 ASCII
|
||||
//! 空白),`written` 为已写入字节数;`Err(DecodeError)` 统一表示 SyntaxError。已解码
|
||||
//! 块在出错前经 `write` 回调逐个写出(setFrom 族"前块已写入后抛错"语义)。
|
||||
//! 解码失败统一以 `Err(DecodeError)` 表示,绑定层映射为 SyntaxError。
|
||||
|
||||
/// 解码失败标记(绑定层映射为 SyntaxError)。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DecodeError;
|
||||
|
||||
/// base64 字母表:标准表(`+/`)与 URL 安全表(`-_`)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Base64Alphabet {
|
||||
Standard,
|
||||
Url,
|
||||
}
|
||||
|
||||
/// 尾块处理模式(lastChunkHandling 三取值)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LastChunkHandling {
|
||||
Loose,
|
||||
Strict,
|
||||
StopBeforePartial,
|
||||
}
|
||||
|
||||
const B64_ALPHABET_STANDARD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const B64_ALPHABET_URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
|
||||
fn b64_table(alphabet: Base64Alphabet) -> &'static [u8; 64] {
|
||||
match alphabet {
|
||||
Base64Alphabet::Standard => B64_ALPHABET_STANDARD,
|
||||
Base64Alphabet::Url => B64_ALPHABET_URL,
|
||||
}
|
||||
}
|
||||
|
||||
/// 判 ASCII 空白(0x09/0x0A/0x0C/0x0D/0x20 五码;VT 与所有非 ASCII 空格均不算)。
|
||||
fn is_base64_whitespace(unit: u16) -> bool {
|
||||
matches!(unit, 0x09 | 0x0A | 0x0C | 0x0D | 0x20)
|
||||
}
|
||||
|
||||
/// 取 base64 数据字符的值(0-63);`=` 与任何非字母表字符返回 `None`。
|
||||
fn base64_value(unit: u16, alphabet: Base64Alphabet) -> Option<u32> {
|
||||
match unit {
|
||||
0x41..=0x5a => Some(unit as u32 - 0x41),
|
||||
0x61..=0x7a => Some(unit as u32 - 0x61 + 26),
|
||||
0x30..=0x39 => Some(unit as u32 - 0x30 + 52),
|
||||
0x2b if alphabet == Base64Alphabet::Standard => Some(62),
|
||||
0x2f if alphabet == Base64Alphabet::Standard => Some(63),
|
||||
0x2d if alphabet == Base64Alphabet::Url => Some(62),
|
||||
0x5f if alphabet == Base64Alphabet::Url => Some(63),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 解码一个满 4 字符块:校验 padding 位置(`=` 只可居 3/4 位)与数据字符,
|
||||
/// strict 模式另查 padding 位必须为 0。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - `chunk` 恰 4 字符;调用方保证只在此处解码满块。
|
||||
/// - 返回 `(24 位值, 解码字节数 1-3)`:24 位值按 4 槽位左对齐(padding 槽补 0),
|
||||
/// 字节数 = 3 - padding 数。
|
||||
fn decode_full_chunk(
|
||||
chunk: [u16; 4], alphabet: Base64Alphabet, handling: LastChunkHandling,
|
||||
) -> Result<(u32, usize), DecodeError> {
|
||||
let pad = match (chunk[2] == 0x3d, chunk[3] == 0x3d) {
|
||||
(false, false) => 0,
|
||||
(false, true) => 1,
|
||||
(true, true) => 2,
|
||||
// `XX=X`:padding 居第 3 位,四种模式全非法。
|
||||
(true, false) => return Err(DecodeError),
|
||||
};
|
||||
let data = &chunk[..4 - pad];
|
||||
let mut bits: u32 = 0;
|
||||
for &c in data {
|
||||
let v = base64_value(c, alphabet).ok_or(DecodeError)?;
|
||||
bits = (bits << 6) | v;
|
||||
}
|
||||
bits <<= 6 * pad as u32;
|
||||
if handling == LastChunkHandling::Strict && pad > 0 {
|
||||
// padding 位即末数据字符的低 2/4 位(pad 1 查 2 位、pad 2 查 4 位)。
|
||||
let last = base64_value(*data.last().unwrap(), alphabet).unwrap();
|
||||
let mask = if pad == 1 { 0b11 } else { 0b1111 };
|
||||
if last & mask != 0 {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
}
|
||||
Ok((bits, 3 - pad))
|
||||
}
|
||||
|
||||
/// 把满块解码出的 24 位值按字节数经回调写出,返回新的已写字节数。
|
||||
fn write_chunk_bytes(write: &mut dyn FnMut(usize, u8), written: usize, bits: u32, count: usize) -> usize {
|
||||
for i in 0..count {
|
||||
write(written + i, (bits >> (16 - 8 * i)) as u8);
|
||||
}
|
||||
written + count
|
||||
}
|
||||
|
||||
/// 尾块(1-3 个非空白字符,已越过全部满块)的终态裁决。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 1 字符尾块:`=` 三种模式全抛;数据字符仅 stop-before-partial 停(成功);
|
||||
/// - 含 `=` 尾块:唯一放行形态是 3 字符 `XX=` 且 stop-before-partial(停在不写
|
||||
/// 出的块前);`X==`/`X=`/`=` 居首等其余 padding 形态三种模式全抛;
|
||||
/// - 全数据 2-3 字符尾块:strict 抛、stop-before-partial 停在块前、loose 解
|
||||
/// `clen - 1` 字节(2 字符 1 字节、3 字符 2 字节)并写出。
|
||||
/// - 成功 `read`:解出字节的 loose 路径取块后消费位置;各"停在块前"路径
|
||||
/// (sbp、余量不足)取块前位置 `chunk_start`。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn finish_partial(
|
||||
chunk: [u16; 4], clen: usize, alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option<usize>,
|
||||
write: &mut dyn FnMut(usize, u8), mut written: usize, chunk_start: usize, read: usize,
|
||||
) -> Result<(usize, usize), DecodeError> {
|
||||
if (0..clen).any(|i| chunk[i] == 0x3d) {
|
||||
let trailing = (0..clen).rev().take_while(|&i| chunk[i] == 0x3d).count();
|
||||
if clen == 3 && trailing == 1 && handling == LastChunkHandling::StopBeforePartial {
|
||||
for &c in &chunk[..2] {
|
||||
base64_value(c, alphabet).ok_or(DecodeError)?;
|
||||
}
|
||||
return Ok((chunk_start, written));
|
||||
}
|
||||
return Err(DecodeError);
|
||||
}
|
||||
for &c in &chunk[..clen] {
|
||||
base64_value(c, alphabet).ok_or(DecodeError)?;
|
||||
}
|
||||
match handling {
|
||||
LastChunkHandling::StopBeforePartial => Ok((chunk_start, written)),
|
||||
LastChunkHandling::Strict => Err(DecodeError),
|
||||
LastChunkHandling::Loose => {
|
||||
if clen == 1 {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
// maxLength 余量不足时不消费该块直接停(不写出)。
|
||||
if let Some(m) = max_len {
|
||||
if written + clen - 1 > m {
|
||||
return Ok((chunk_start, written));
|
||||
}
|
||||
}
|
||||
let v = |i: usize| base64_value(chunk[i], alphabet).unwrap();
|
||||
write(written, (v(0) << 2 | v(1) >> 4) as u8);
|
||||
written += 1;
|
||||
if clen == 3 {
|
||||
write(written, ((v(1) << 4 | v(2) >> 2) & 0xFF) as u8);
|
||||
written += 1;
|
||||
}
|
||||
Ok((read, written))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FromBase64 状态机:按块消费输入串,已解码字节经 `write` 回调逐块写出。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. `max_len == 0` 短路:输入整体忽略,直接成功空结果。
|
||||
/// 2. 循环收集至多 4 个非空白字符(跳过五码 ASCII 空白)组成块。
|
||||
/// 3. 满块先过 maxLength 余量判定(不足则块前停、不消费、不写出),再解码写出;
|
||||
/// 写出后恰好补齐 maxLength 即成功返回。
|
||||
/// 4. 块不足 4 字符时走终态裁决([`finish_partial`])。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - `units` 为输入串的 UTF-16 单元序列;非 ASCII 码元一律是非法字符。
|
||||
/// - 出错时 `write` 已产生的写入保留(调用方承载"前块已写入"语义)。
|
||||
/// - `Ok((read, written))` 中 `read` 为成功语义下的消费位置(含跳过空白,
|
||||
/// 停止路径为块前位置)。
|
||||
pub fn decode_base64(
|
||||
units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option<usize>,
|
||||
write: &mut dyn FnMut(usize, u8),
|
||||
) -> Result<(usize, usize), DecodeError> {
|
||||
if max_len == Some(0) {
|
||||
return Ok((0, 0));
|
||||
}
|
||||
let n = units.len();
|
||||
let mut read = 0usize;
|
||||
let mut written = 0usize;
|
||||
loop {
|
||||
let chunk_start = read;
|
||||
let mut chunk = [0u16; 4];
|
||||
let mut clen = 0usize;
|
||||
while clen < 4 && read < n {
|
||||
let c = units[read];
|
||||
read += 1;
|
||||
if is_base64_whitespace(c) {
|
||||
continue;
|
||||
}
|
||||
chunk[clen] = c;
|
||||
clen += 1;
|
||||
}
|
||||
match clen {
|
||||
4 => {
|
||||
if chunk[2] == 0x3d || chunk[3] == 0x3d {
|
||||
// 带 padding 的满块只可作末块:其后还有非空白字符时
|
||||
// ("extra padding")在写出该块字节前抛。
|
||||
if units[read..].iter().any(|&c| !is_base64_whitespace(c)) {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
}
|
||||
let (bits, count) = decode_full_chunk(chunk, alphabet, handling)?;
|
||||
if let Some(m) = max_len {
|
||||
if written + count > m {
|
||||
return Ok((chunk_start, written));
|
||||
}
|
||||
}
|
||||
written = write_chunk_bytes(write, written, bits, count);
|
||||
if let Some(m) = max_len {
|
||||
if written == m {
|
||||
return Ok((read, written));
|
||||
}
|
||||
}
|
||||
}
|
||||
0 => return Ok((read, written)),
|
||||
_ => return finish_partial(chunk, clen, alphabet, handling, max_len, write, written, chunk_start, read),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 取单个 hex 字符值(大小写宽容);非 hex 字符返回 `None`。
|
||||
fn hex_value(unit: u16) -> Option<u8> {
|
||||
match unit {
|
||||
0x30..=0x39 => Some(unit as u8 - 0x30),
|
||||
0x61..=0x66 => Some(unit as u8 - 0x61 + 10),
|
||||
0x41..=0x46 => Some(unit as u8 - 0x41 + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// FromHex 状态机:无空白跳过、无 padding;长度奇数在最前判定
|
||||
/// (`read` 0、零写入);坏字符抛错时保留已写出的前字节。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - `units` 为输入串的 UTF-16 单元序列;任何非 hex 字符(含所有空白)非法。
|
||||
/// - 每对解码前判 `written + 1 > max_len` 则对该对前停(不消费、不写出)。
|
||||
pub fn decode_hex(
|
||||
units: &[u16], max_len: Option<usize>, write: &mut dyn FnMut(usize, u8),
|
||||
) -> Result<(usize, usize), DecodeError> {
|
||||
if units.len() % 2 == 1 {
|
||||
return Err(DecodeError);
|
||||
}
|
||||
let mut written = 0usize;
|
||||
let mut read = 0usize;
|
||||
for (i, pair) in units.chunks_exact(2).enumerate() {
|
||||
let hi = hex_value(pair[0]).ok_or(DecodeError)?;
|
||||
let lo = hex_value(pair[1]).ok_or(DecodeError)?;
|
||||
if let Some(m) = max_len {
|
||||
if written + 1 > m {
|
||||
return Ok((i * 2, written));
|
||||
}
|
||||
}
|
||||
write(written, hi * 16 + lo);
|
||||
written += 1;
|
||||
read = i * 2 + 2;
|
||||
}
|
||||
Ok((read, written))
|
||||
}
|
||||
|
||||
/// 编码 base64:标准表或 URL 表;`omit_padding` 时剥尾部 `=`。
|
||||
pub fn encode_base64(bytes: &[u8], alphabet: Base64Alphabet, omit_padding: bool) -> String {
|
||||
let table = b64_table(alphabet);
|
||||
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
|
||||
for chunk in bytes.chunks(3) {
|
||||
let bits = (chunk[0] as u32) << 16
|
||||
| (chunk.get(1).copied().unwrap_or(0) as u32) << 8
|
||||
| chunk.get(2).copied().unwrap_or(0) as u32;
|
||||
out.push(table[(bits >> 18) as usize] as char);
|
||||
out.push(table[((bits >> 12) & 0x3f) as usize] as char);
|
||||
if chunk.len() > 1 {
|
||||
out.push(table[((bits >> 6) & 0x3f) as usize] as char);
|
||||
} else if !omit_padding {
|
||||
out.push('=');
|
||||
}
|
||||
if chunk.len() > 2 {
|
||||
out.push(table[(bits & 0x3f) as usize] as char);
|
||||
} else if !omit_padding {
|
||||
out.push('=');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 编码小写两位 hex。
|
||||
pub fn encode_hex(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut out = String::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
out.push(HEX[(b >> 4) as usize] as char);
|
||||
out.push(HEX[(b & 0x0f) as usize] as char);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn units(s: &str) -> Vec<u16> {
|
||||
s.chars().map(|c| c as u16).collect()
|
||||
}
|
||||
|
||||
fn decode(
|
||||
units: &[u16], alphabet: Base64Alphabet, handling: LastChunkHandling, max_len: Option<usize>,
|
||||
) -> Result<(usize, Vec<u8>), DecodeError> {
|
||||
let mut bytes = Vec::new();
|
||||
decode_base64(units, alphabet, handling, max_len, &mut |_, b| bytes.push(b))
|
||||
.map(|(r, w)| (r, bytes.into_iter().take(w).collect()))
|
||||
}
|
||||
|
||||
fn decode_hex_full(units: &[u16], max_len: Option<usize>) -> Result<(usize, Vec<u8>), DecodeError> {
|
||||
let mut bytes = Vec::new();
|
||||
decode_hex(units, max_len, &mut |_, b| bytes.push(b)).map(|(r, w)| (r, bytes.into_iter().take(w).collect()))
|
||||
}
|
||||
|
||||
/// RFC 4648 标准向量:编码/解码双向。
|
||||
#[test]
|
||||
fn rfc4648_vectors_roundtrip() {
|
||||
let vectors: [(&str, &[u8]); 7] = [
|
||||
("", &[]),
|
||||
("Zg==", &[102]),
|
||||
("Zm8=", &[102, 111]),
|
||||
("Zm9v", &[102, 111, 111]),
|
||||
("Zm9vYg==", &[102, 111, 111, 98]),
|
||||
("Zm9vYmE=", &[102, 111, 111, 98, 97]),
|
||||
("Zm9vYmFy", &[102, 111, 111, 98, 97, 114]),
|
||||
];
|
||||
for (s, bytes) in vectors {
|
||||
assert_eq!(encode_base64(bytes, Base64Alphabet::Standard, false), s);
|
||||
let (read, decoded) = decode(&units(s), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap();
|
||||
assert_eq!(decoded, *bytes);
|
||||
assert_eq!(read, s.chars().count());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_base64_url_alphabet_and_omit_padding() {
|
||||
assert_eq!(encode_base64(&[199, 239, 242], Base64Alphabet::Standard, false), "x+/y");
|
||||
assert_eq!(encode_base64(&[199, 239, 242], Base64Alphabet::Url, false), "x-_y");
|
||||
assert_eq!(encode_base64(&[199, 239], Base64Alphabet::Standard, false), "x+8=");
|
||||
assert_eq!(encode_base64(&[199, 239], Base64Alphabet::Standard, true), "x+8");
|
||||
assert_eq!(encode_base64(&[255], Base64Alphabet::Standard, true), "/w");
|
||||
assert_eq!(encode_base64(&[255], Base64Alphabet::Url, true), "_w");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_whitespace_and_read_count() {
|
||||
for ws in [' ', '\t', '\n', '\u{0C}', '\r'] {
|
||||
let s = format!("Z{ws}g==");
|
||||
let (read, bytes) = decode(&units(&s), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap();
|
||||
assert_eq!(bytes, vec![102]);
|
||||
assert_eq!(read, 5);
|
||||
}
|
||||
// VT 不是 ASCII 空白:非法字符。
|
||||
assert!(decode(&units("Z\u{0B}g=="), Base64Alphabet::Standard, LastChunkHandling::Loose, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_strict_padding_bits() {
|
||||
// 末数据字符低 2 位非 0(`h` = 33 = 0b100001):strict 抛,其余过。
|
||||
for (h, ok) in [
|
||||
(LastChunkHandling::Loose, true),
|
||||
(LastChunkHandling::Strict, false),
|
||||
(LastChunkHandling::StopBeforePartial, true),
|
||||
] {
|
||||
let r = decode(&units("ZXhhZh=="), Base64Alphabet::Standard, h, None);
|
||||
assert_eq!(r.is_ok(), ok, "ZXhhZh== {h:?}");
|
||||
if ok {
|
||||
assert_eq!(r.unwrap().1, vec![101, 120, 97, 102]);
|
||||
}
|
||||
}
|
||||
// 低 4 位非 0(`h` 居 pad 2 末位):strict 抛;`g` 低 4 位全 0 则过。
|
||||
assert!(decode(&units("ZXhhZg=="), Base64Alphabet::Standard, LastChunkHandling::Strict, None).is_ok());
|
||||
assert!(decode(&units("Zg1="), Base64Alphabet::Standard, LastChunkHandling::Strict, None).is_err());
|
||||
// pad 2 块字节值:`Zg==` → 102。
|
||||
assert_eq!(
|
||||
decode(&units("Zg=="), Base64Alphabet::Standard, LastChunkHandling::Loose, None)
|
||||
.unwrap()
|
||||
.1,
|
||||
vec![102]
|
||||
);
|
||||
// pad 1 块字节值:`Zm8=` → [102, 111]。
|
||||
assert_eq!(
|
||||
decode(&units("Zm8="), Base64Alphabet::Standard, LastChunkHandling::Loose, None)
|
||||
.unwrap()
|
||||
.1,
|
||||
vec![102, 111]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_last_chunk_matrix() {
|
||||
let cases: [(&str, [bool; 3]); 13] = [
|
||||
// (输入, [loose, strict, sbp] 是否成功)
|
||||
("A", [false, false, true]),
|
||||
("ABCDA", [false, false, true]),
|
||||
("AA=", [false, false, true]),
|
||||
("aQ=", [false, false, true]),
|
||||
("ABCDAA=", [false, false, true]),
|
||||
("AAA", [true, false, true]),
|
||||
("ZXhhZg", [true, false, true]),
|
||||
("ZXhhZg=", [false, false, true]),
|
||||
("ZXhhZg===", [false, false, false]),
|
||||
("=", [false, false, false]),
|
||||
("A==", [false, false, false]),
|
||||
("AAAA=", [false, false, false]),
|
||||
("ZXhhZgg", [true, false, true]),
|
||||
];
|
||||
for (s, expect) in cases {
|
||||
for (h, ok) in [
|
||||
(LastChunkHandling::Loose, expect[0]),
|
||||
(LastChunkHandling::Strict, expect[1]),
|
||||
(LastChunkHandling::StopBeforePartial, expect[2]),
|
||||
] {
|
||||
let r = decode(&units(s), Base64Alphabet::Standard, h, None);
|
||||
assert_eq!(r.is_ok(), ok, "{s:?} {h:?}");
|
||||
}
|
||||
}
|
||||
// sbp 停止路径:前块保留、尾块不消费。
|
||||
let (read, bytes) =
|
||||
decode(&units("ABCDAA="), Base64Alphabet::Standard, LastChunkHandling::StopBeforePartial, None).unwrap();
|
||||
assert_eq!((read, bytes), (4, vec![0, 16, 131]));
|
||||
let (read, bytes) =
|
||||
decode(&units("ZXhhZg"), Base64Alphabet::Standard, LastChunkHandling::StopBeforePartial, None).unwrap();
|
||||
assert_eq!((read, bytes), (4, vec![101, 120, 97]));
|
||||
// loose 对 2 字符尾块解 1 字节。
|
||||
let (read, bytes) = decode(&units("ZXhhZg"), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap();
|
||||
assert_eq!((read, bytes), (6, vec![101, 120, 97, 102]));
|
||||
// loose 对 3 字符尾块解 2 字节。
|
||||
let (read, bytes) =
|
||||
decode(&units("ZXhhZgg"), Base64Alphabet::Standard, LastChunkHandling::Loose, None).unwrap();
|
||||
assert_eq!((read, bytes), (7, vec![101, 120, 97, 102, 8]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_max_length_stop_timing() {
|
||||
// 块前停:第 2 块 3 字节会超限,不消费(read 停在前值 4)。
|
||||
let (read, bytes) =
|
||||
decode(&units("Zm9vYmFy"), Base64Alphabet::Standard, LastChunkHandling::Loose, Some(5)).unwrap();
|
||||
assert_eq!((read, bytes), (4, vec![102, 111, 111]));
|
||||
// 恰好补齐:`YmE=` 出 2 字节补到 5。
|
||||
let (read, bytes) =
|
||||
decode(&units("Zm9vYmE="), Base64Alphabet::Standard, LastChunkHandling::Loose, Some(5)).unwrap();
|
||||
assert_eq!((read, bytes), (8, vec![102, 111, 111, 98, 97]));
|
||||
// 零长短路:垃圾输入整体忽略。
|
||||
let (read, bytes) =
|
||||
decode(&units("aaaa#"), Base64Alphabet::Standard, LastChunkHandling::Strict, Some(0)).unwrap();
|
||||
assert_eq!((read, bytes), (0, Vec::new()));
|
||||
// 尾 3 字符块余量不足:不消费。
|
||||
let (read, bytes) =
|
||||
decode(&units("Zm9vYmE"), Base64Alphabet::Standard, LastChunkHandling::Loose, Some(3)).unwrap();
|
||||
assert_eq!((read, bytes), (4, vec![102, 111, 111]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_illegal_characters() {
|
||||
for s in ["Zm.9v", "Zm9v^", "Zg==&", "Z\u{2212}==", "Zg\u{00A0}==", "Zg\u{2009}==", "Zg\u{2028}=="] {
|
||||
assert!(
|
||||
decode(&units(s), Base64Alphabet::Standard, LastChunkHandling::Loose, None).is_err(),
|
||||
"{s:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_url_alphabet_decode() {
|
||||
let (read, bytes) = decode(&units("x-_y"), Base64Alphabet::Url, LastChunkHandling::Loose, None).unwrap();
|
||||
assert_eq!((read, bytes), (4, vec![199, 239, 242]));
|
||||
// URL 表中标准表符号非法。
|
||||
assert!(decode(&units("x+/y"), Base64Alphabet::Url, LastChunkHandling::Loose, None).is_err());
|
||||
assert!(decode(&units("x-_y"), Base64Alphabet::Standard, LastChunkHandling::Loose, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_base64_writes_up_to_error() {
|
||||
// 坏字符前已写出的字节保留(setFrom 语义同)。
|
||||
let mut out = Vec::new();
|
||||
let r = decode_base64(
|
||||
&units("MjYyZm.9v"),
|
||||
Base64Alphabet::Standard,
|
||||
LastChunkHandling::Loose,
|
||||
None,
|
||||
&mut |i, b| out.insert(i, b),
|
||||
);
|
||||
assert!(r.is_err());
|
||||
assert_eq!(out, vec![50, 54, 50]);
|
||||
// 带 padding 满块后随非空白字符:该块字节不写出(extra padding)。
|
||||
let mut out = Vec::new();
|
||||
let r = decode_base64(
|
||||
&units("MjYyZg==="),
|
||||
Base64Alphabet::Standard,
|
||||
LastChunkHandling::Loose,
|
||||
None,
|
||||
&mut |i, b| out.insert(i, b),
|
||||
);
|
||||
assert!(r.is_err());
|
||||
assert_eq!(out, vec![50, 54, 50]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_hex_vectors_and_case_insensitive() {
|
||||
let cases: [(&str, &[u8]); 5] = [
|
||||
("", &[]),
|
||||
("66", &[102]),
|
||||
("666f", &[102, 111]),
|
||||
("666F", &[102, 111]),
|
||||
("666f6f626172", &[102, 111, 111, 98, 97, 114]),
|
||||
];
|
||||
for (s, bytes) in cases {
|
||||
let (read, decoded) = decode_hex_full(&units(s), None).unwrap();
|
||||
assert_eq!(decoded, *bytes);
|
||||
assert_eq!(read, s.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_hex_odd_and_max_length() {
|
||||
assert!(decode_hex_full(&units("a"), None).is_err());
|
||||
assert!(decode_hex_full(&units("aaa"), None).is_err());
|
||||
// 奇数在最前判定:零长目标也抛(零写入)。
|
||||
assert!(decode_hex_full(&units("1"), Some(0)).is_err());
|
||||
// 对前停:2 字节目标下 `aabbcc` 停在第 3 对前(read 4)。
|
||||
let (read, bytes) = decode_hex_full(&units("aabbcc"), Some(2)).unwrap();
|
||||
assert_eq!((read, bytes), (4, vec![0xaa, 0xbb]));
|
||||
// 恰好补齐。
|
||||
let (read, bytes) = decode_hex_full(&units("aabbcc"), Some(3)).unwrap();
|
||||
assert_eq!((read, bytes), (6, vec![0xaa, 0xbb, 0xcc]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_hex_illegal_characters() {
|
||||
for s in ["a.a", "aa^", "a a", "a\ta", "a\u{00A0}a", "a\u{2009}a", "a\u{2028}a"] {
|
||||
assert!(decode_hex_full(&units(s), None).is_err(), "{s:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_hex_writes_up_to_error() {
|
||||
// 前对写出、坏对抛错。
|
||||
let mut out = Vec::new();
|
||||
let r = decode_hex(&units("aaag"), None, &mut |i, b| out.insert(i, b));
|
||||
assert!(r.is_err());
|
||||
assert_eq!(out, vec![0xaa]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_hex_lowercase() {
|
||||
assert_eq!(encode_hex(&[]), "");
|
||||
assert_eq!(encode_hex(&[102]), "66");
|
||||
assert_eq!(encode_hex(&[102, 111, 111, 98, 97, 114]), "666f6f626172");
|
||||
assert_eq!(encode_hex(&[170, 187, 204]), "aabbcc");
|
||||
}
|
||||
}
|
||||
@@ -1671,3 +1671,233 @@ pub fn typed_array_from<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
NativeResult::Ok(new_obj)
|
||||
}
|
||||
|
||||
// ── Uint8Array base64/hex 六方法族 ─────────────────────────────────────────────
|
||||
|
||||
/// ValidateUint8Array:接收者须为 kind 恰为 Uint8 的 TypedArray 对象;
|
||||
/// Uint8Clamped 与其他种类一律抛 TypeError。
|
||||
fn validate_uint8_array<H: VmHost>(vm: &mut H, this_val: JsValue) -> Result<TypedArrayData, JsValue> {
|
||||
let view = get_typed_array_data(vm, this_val)?;
|
||||
if view.kind != TypedArrayKind::Uint8 {
|
||||
return Err(type_error(vm, "Uint8Array method called on incompatible receiver"));
|
||||
}
|
||||
Ok(view)
|
||||
}
|
||||
|
||||
/// GetOptionsObject:undefined 直通;其余值过 ToObject,失败抛 TypeError。
|
||||
fn ta_options_object<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<JsValue>, JsValue> {
|
||||
if value.is_undefined() {
|
||||
return Ok(None);
|
||||
}
|
||||
match oxide_runtime_api::to_object(value, vm) {
|
||||
Ok(obj) => Ok(Some(obj)),
|
||||
Err(e) => Err(type_error(vm, &e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 读只收字符串原始值的选项字段(alphabet/lastChunkHandling):undefined 取
|
||||
/// 默认值;装箱串/抛错 toString 对象等一律抛 TypeError,不做 ToPrimitive。
|
||||
fn ta_string_option<H: VmHost>(
|
||||
vm: &mut H, opts: &JsObject, opts_val: JsValue, key: &str, default: &'static str,
|
||||
) -> Result<String, JsValue> {
|
||||
let key_si = vm.string_key_si(key);
|
||||
let v = vm
|
||||
.ordinary_get(opts, key_si, opts_val)
|
||||
.map_err(|e| crate::iterator::engine_error(vm, &e))?;
|
||||
if v.is_undefined() {
|
||||
return Ok(default.to_string());
|
||||
}
|
||||
if !v.is_string() {
|
||||
return Err(type_error(vm, &format!("{key} must be a string")));
|
||||
}
|
||||
Ok(vm.lookup_str(v).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// ToBoolean:仅 null/undefined/false/NaN/±0 为假。
|
||||
fn ta_to_boolean(v: JsValue) -> bool {
|
||||
if v.is_null() || v.is_undefined() {
|
||||
return false;
|
||||
}
|
||||
if v.is_bool() {
|
||||
return v.as_bool();
|
||||
}
|
||||
if v.is_int() {
|
||||
return v.as_int() != 0;
|
||||
}
|
||||
if v.is_double() {
|
||||
let d = v.as_double();
|
||||
return !d.is_nan() && d != 0.0;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// 取选项中的 alphabet 值并归一到字母表枚举;非 "base64"/"base64url" 抛 TypeError。
|
||||
fn ta_alphabet_option<H: VmHost>(
|
||||
vm: &mut H, opts: &JsObject, opts_val: JsValue,
|
||||
) -> Result<crate::ta_codec::Base64Alphabet, JsValue> {
|
||||
let s = ta_string_option(vm, opts, opts_val, "alphabet", "base64")?;
|
||||
match s.as_str() {
|
||||
"base64url" => Ok(crate::ta_codec::Base64Alphabet::Url),
|
||||
"base64" => Ok(crate::ta_codec::Base64Alphabet::Standard),
|
||||
_ => Err(type_error(vm, "alphabet must be 'base64' or 'base64url'")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 取选项中的 lastChunkHandling 值并归一到模式枚举;非三取值抛 TypeError。
|
||||
fn ta_handling_option<H: VmHost>(
|
||||
vm: &mut H, opts: &JsObject, opts_val: JsValue,
|
||||
) -> Result<crate::ta_codec::LastChunkHandling, JsValue> {
|
||||
let s = ta_string_option(vm, opts, opts_val, "lastChunkHandling", "loose")?;
|
||||
match s.as_str() {
|
||||
"strict" => Ok(crate::ta_codec::LastChunkHandling::Strict),
|
||||
"stop-before-partial" => Ok(crate::ta_codec::LastChunkHandling::StopBeforePartial),
|
||||
"loose" => Ok(crate::ta_codec::LastChunkHandling::Loose),
|
||||
_ => Err(type_error(vm, "lastChunkHandling must be 'loose', 'strict', or 'stop-before-partial'")),
|
||||
}
|
||||
}
|
||||
|
||||
/// setFromBase64/fromBase64 共用入口:string 类型检查先于选项读取,
|
||||
/// 选项读完跑 FromBase64 状态机,已解码字节经 `write` 逐块写出;
|
||||
/// `max_len` 为 None 时静态方法无界。
|
||||
fn ta_decode_base64<H: VmHost>(
|
||||
vm: &mut H, string: JsValue, options: JsValue, max_len: Option<usize>, write: &mut dyn FnMut(usize, u8),
|
||||
) -> Result<(usize, usize), JsValue> {
|
||||
if !string.is_string() {
|
||||
return Err(type_error(vm, "string argument required"));
|
||||
}
|
||||
let opts = ta_options_object(vm, options)?;
|
||||
let mut alphabet = crate::ta_codec::Base64Alphabet::Standard;
|
||||
let mut handling = crate::ta_codec::LastChunkHandling::Loose;
|
||||
if let Some(opts_val) = opts {
|
||||
let obj = unsafe { &*opts_val.as_js_object_ptr() };
|
||||
// SAFETY: opts_val 来自 ToObject 成功路径,必为活动对象指针。
|
||||
alphabet = ta_alphabet_option(vm, obj, opts_val)?;
|
||||
handling = ta_handling_option(vm, obj, opts_val)?;
|
||||
}
|
||||
let units = vm.string_units(string).into_owned();
|
||||
match crate::ta_codec::decode_base64(&units, alphabet, handling, max_len, write) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_) => Err(crate::error::create_syntax_error(vm, "invalid base64 input")),
|
||||
}
|
||||
}
|
||||
|
||||
/// setFrom* 结果对象 `{ read, written }`:普通对象 + 两数据属性。
|
||||
fn ta_read_written_result<H: VmHost>(vm: &mut H, read: usize, written: usize) -> JsValue {
|
||||
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
|
||||
let read_si = vm.kernel_core().perm_interner().intern("read").0;
|
||||
let written_si = vm.kernel_core().perm_interner().intern("written").0;
|
||||
let obj_ref = unsafe { &mut *obj };
|
||||
vm.set_or_create_prop_value(obj_ref, read_si, JsValue::int(read as i32));
|
||||
vm.set_or_create_prop_value(obj_ref, written_si, JsValue::int(written as i32));
|
||||
JsValue::from_js_object(obj)
|
||||
}
|
||||
|
||||
/// `%Uint8Array%.prototype.toBase64(options)`:ValidateUint8Array 先于选项
|
||||
/// 副作用,按字母表与 omitPadding 编码视图字节。
|
||||
pub fn uint8array_to_base64<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||
let view = native_try!(validate_uint8_array(vm, this_val));
|
||||
let opts = native_try!(ta_options_object(vm, if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }));
|
||||
let mut alphabet = crate::ta_codec::Base64Alphabet::Standard;
|
||||
let mut omit_padding = false;
|
||||
if let Some(opts_val) = opts {
|
||||
let obj = unsafe { &*opts_val.as_js_object_ptr() };
|
||||
// SAFETY: opts_val 来自 ToObject 成功路径,必为活动对象指针。
|
||||
alphabet = native_try!(ta_alphabet_option(vm, obj, opts_val));
|
||||
let key_si = vm.string_key_si("omitPadding");
|
||||
let v = native_try!(vm
|
||||
.ordinary_get(obj, key_si, opts_val)
|
||||
.map_err(|e| crate::iterator::engine_error(vm, &e)));
|
||||
omit_padding = ta_to_boolean(v);
|
||||
}
|
||||
let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer));
|
||||
let buffer = unsafe { &*buffer_ptr };
|
||||
// SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。
|
||||
NativeResult::Ok(vm.new_string_owned(crate::ta_codec::encode_base64(
|
||||
&buffer[view.byte_offset..view.byte_offset + view.length],
|
||||
alphabet,
|
||||
omit_padding,
|
||||
)))
|
||||
}
|
||||
|
||||
/// `%Uint8Array%.prototype.toHex()`:ValidateUint8Array 后编码小写两位 hex。
|
||||
pub fn uint8array_to_hex<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||
let view = native_try!(validate_uint8_array(vm, this_val));
|
||||
let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer));
|
||||
let buffer = unsafe { &*buffer_ptr };
|
||||
// SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。
|
||||
NativeResult::Ok(
|
||||
vm.new_string_owned(crate::ta_codec::encode_hex(&buffer[view.byte_offset..view.byte_offset + view.length])),
|
||||
)
|
||||
}
|
||||
|
||||
/// `%Uint8Array%.prototype.setFromBase64(string, options)`:解码写入视图,
|
||||
/// 返回 `{ read, written }`;前块已写入后遇错抛 SyntaxError(前字节保留)。
|
||||
pub fn uint8array_set_from_base64<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||
let view = native_try!(validate_uint8_array(vm, this_val));
|
||||
let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||
let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer));
|
||||
let buffer = unsafe { &mut *buffer_ptr };
|
||||
// SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。
|
||||
let start = view.byte_offset;
|
||||
let (read, written) = native_try!(ta_decode_base64(vm, string, options, Some(view.length), &mut |idx, b| {
|
||||
buffer[start + idx] = b;
|
||||
}));
|
||||
NativeResult::Ok(ta_read_written_result(vm, read, written))
|
||||
}
|
||||
|
||||
/// `%Uint8Array%.prototype.setFromHex(string)`:无选项;奇数长度最前判定
|
||||
/// (零写入)抛 SyntaxError;坏字符保留前字节写入后抛。
|
||||
pub fn uint8array_set_from_hex<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||
let view = native_try!(validate_uint8_array(vm, this_val));
|
||||
let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
if !string.is_string() {
|
||||
return NativeResult::Err(type_error(vm, "string argument required"));
|
||||
}
|
||||
let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer));
|
||||
let buffer = unsafe { &mut *buffer_ptr };
|
||||
// SAFETY: buffer_ptr 来自活动 ArrayBuffer 对象,视图范围由构造保证界内。
|
||||
let start = view.byte_offset;
|
||||
let units = vm.string_units(string).into_owned();
|
||||
let (read, written) = match crate::ta_codec::decode_hex(&units, Some(view.length), &mut |idx, b| {
|
||||
buffer[start + idx] = b;
|
||||
}) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return NativeResult::Err(crate::error::create_syntax_error(vm, "invalid hex input")),
|
||||
};
|
||||
NativeResult::Ok(ta_read_written_result(vm, read, written))
|
||||
}
|
||||
|
||||
/// `Uint8Array.fromBase64(string, options)`:不读 this,结果 proto 恒为
|
||||
/// %Uint8Array%.prototype(不经 species/构造器)。
|
||||
pub fn uint8array_from_base64<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
let options = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() };
|
||||
let mut bytes = Vec::new();
|
||||
native_try!(ta_decode_base64(vm, string, options, None, &mut |_, b| bytes.push(b)));
|
||||
let len = bytes.len();
|
||||
let buffer = JsValue::from_js_object(new_array_buffer(vm, bytes));
|
||||
NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, TypedArrayKind::Uint8, buffer, 0, len)))
|
||||
}
|
||||
|
||||
/// `Uint8Array.fromHex(string)`:不读 this;奇数长度最前判定抛 SyntaxError。
|
||||
pub fn uint8array_from_hex<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let string = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
|
||||
if !string.is_string() {
|
||||
return NativeResult::Err(type_error(vm, "string argument required"));
|
||||
}
|
||||
let units = vm.string_units(string).into_owned();
|
||||
let mut bytes = Vec::new();
|
||||
match crate::ta_codec::decode_hex(&units, None, &mut |_, b| bytes.push(b)) {
|
||||
Ok(_) => {}
|
||||
Err(_) => return NativeResult::Err(crate::error::create_syntax_error(vm, "invalid hex input")),
|
||||
}
|
||||
let len = bytes.len();
|
||||
let buffer = JsValue::from_js_object(new_array_buffer(vm, bytes));
|
||||
NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, TypedArrayKind::Uint8, buffer, 0, len)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user