Skip to content

Commit f2e78ef

Browse files
committed
feat: Implement proposal-arraybuffer-base64
1 parent 90571e3 commit f2e78ef

9 files changed

Lines changed: 1085 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ strum = { version = "0.28", features = ["derive"] }
147147
unsend = { version = "0.2.1", default-features = false }
148148
husky-rs = "0.3.2"
149149
async-channel = "2.5.0"
150+
data-encoding = "2.11.0"
151+
data-encoding-macro = "0.1.20"
150152

151153
# ICU4X core
152154

core/engine/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ dynify = { workspace = true, features = ["macros"] }
142142
futures-concurrency.workspace = true
143143
oneshot = { workspace = true, features = ["async"] }
144144
async-channel.workspace = true
145+
data-encoding.workspace = true
146+
data-encoding-macro.workspace = true
145147

146148
# intl deps
147149
boa_icu_provider = { workspace = true, features = ["std"], optional = true }
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
//! Base64 helpers for `Uint8Array` proposal methods.
2+
//!
3+
//! This is adapted from the `ecma262` helper in the `data-encoding` repository so that
4+
//! `Uint8Array.{fromBase64,setFromBase64}` follow the proposal's partial-decoding rules.
5+
6+
use data_encoding::{Character, DecodeError, DecodeKind, DecodePartial, Encoding};
7+
use data_encoding_macro::new_encoding;
8+
9+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10+
pub(crate) enum Alphabet {
11+
Base64,
12+
Base64Url,
13+
}
14+
15+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16+
pub(crate) enum LastChunkHandling {
17+
Loose,
18+
Strict,
19+
StopBeforePartial,
20+
}
21+
use LastChunkHandling::{Loose, StopBeforePartial, Strict};
22+
23+
#[derive(Debug, PartialEq, Eq)]
24+
pub(crate) struct DecodeMutResult {
25+
pub(crate) read: usize,
26+
pub(crate) written: usize,
27+
pub(crate) error: Option<DecodeError>,
28+
}
29+
30+
/// Decodes `input` in `output` according to the given parameters.
31+
///
32+
/// # Panics
33+
///
34+
/// Panics if `output.len() < 6 * input.len() / 8`. It is not an error if `max_length` is smaller
35+
/// than `output.len()`. This function will however not optimize those cases.
36+
pub(crate) fn decode_mut(
37+
input: &[u8],
38+
output: &mut [u8],
39+
alphabet: Alphabet,
40+
last_chunk_handling: LastChunkHandling,
41+
max_length: Option<usize>,
42+
) -> DecodeMutResult {
43+
// Select the appropriate encoding.
44+
let base = match alphabet {
45+
Alphabet::Base64 => &BASE64,
46+
Alphabet::Base64Url => &BASE64URL,
47+
};
48+
let max_length = max_length.unwrap_or(usize::MAX);
49+
50+
// Decode as much as possible.
51+
let (mut read, mut written) = match base.decode_mut(input, &mut output[..6 * input.len() / 8]) {
52+
Ok(olen) => (input.len(), olen),
53+
Err(DecodePartial { read, written, .. }) => (read, written),
54+
};
55+
56+
// Backtrack to the last complete chunk that fits below the maximum output length.
57+
let extra_output = written - core::cmp::min(written, max_length) / 3 * 3;
58+
let mut extra_input = (8 * extra_output).div_ceil(6);
59+
written -= extra_output;
60+
loop {
61+
// Backtrack white-spaces.
62+
while 0 < read && base.interpret_byte(input[read - 1]).is_ignored() {
63+
read -= 1;
64+
}
65+
if extra_input == 0 {
66+
break;
67+
}
68+
// Backtrack one symbol.
69+
read -= 1;
70+
extra_input -= 1;
71+
debug_assert!(base.interpret_byte(input[read]).is_symbol().is_some());
72+
}
73+
74+
// Parse the next chunk manually.
75+
let mut index = [0; 4]; // maps to index in input
76+
let mut index_len = 0;
77+
let mut index_pad = 4;
78+
let mut ipos = read;
79+
let remaining = max_length - written;
80+
if remaining == 0 {
81+
return DecodeMutResult {
82+
read,
83+
written,
84+
error: None,
85+
};
86+
}
87+
while ipos < input.len() {
88+
let byte = input[ipos];
89+
let position = ipos;
90+
ipos += 1;
91+
let kind = match base.interpret_byte(byte) {
92+
Character::Padding => unreachable!(),
93+
Character::Ignored => continue,
94+
Character::Symbol { .. } if index_pad < 4 => Some(DecodeKind::Padding),
95+
Character::Symbol { .. } => None,
96+
Character::Invalid if byte != b'=' => Some(DecodeKind::Symbol),
97+
Character::Invalid if index_len < 2 => Some(DecodeKind::Padding),
98+
Character::Invalid => {
99+
index_pad = core::cmp::min(index_pad, index_len);
100+
None
101+
}
102+
};
103+
if let Some(kind) = kind {
104+
return DecodeMutResult {
105+
read,
106+
written,
107+
error: Some(DecodeError { position, kind }),
108+
};
109+
}
110+
if index_len == 4 {
111+
debug_assert!(index_pad < 4);
112+
let error = Some(DecodeError {
113+
position,
114+
kind: DecodeKind::Padding,
115+
});
116+
return DecodeMutResult {
117+
read,
118+
written,
119+
error,
120+
};
121+
}
122+
index[index_len] = position;
123+
index_len += 1;
124+
if matches!(
125+
(core::cmp::min(index_len, index_pad), remaining),
126+
(3, 1) | (4, 2)
127+
) {
128+
return DecodeMutResult {
129+
read,
130+
written,
131+
error: None,
132+
};
133+
}
134+
}
135+
debug_assert!(index_len <= 4 && index_pad <= 4);
136+
debug_assert!(index_len < 4 || index_pad < 4);
137+
138+
// Process the last chunk.
139+
if index_len == 0 {
140+
return DecodeMutResult {
141+
read: input.len(),
142+
written,
143+
error: None,
144+
};
145+
}
146+
let check = match (last_chunk_handling, index_len, index_pad) {
147+
(Loose, 1, _) | (Loose, 0..4, 0..4) | (Strict, 0..4, _) => {
148+
let error = Some(DecodeError {
149+
position: ipos,
150+
kind: DecodeKind::Length,
151+
});
152+
return DecodeMutResult {
153+
read,
154+
written,
155+
error,
156+
};
157+
}
158+
(Strict, _, _) => true,
159+
(StopBeforePartial, 0..4, _) => {
160+
return DecodeMutResult {
161+
read,
162+
written,
163+
error: None,
164+
};
165+
}
166+
(Loose | StopBeforePartial, _, _) => false,
167+
};
168+
let iend = core::cmp::min(index_len, index_pad);
169+
let oend = iend - 1;
170+
let mut ichunk = [b'A'; 4];
171+
for i in 0..iend {
172+
ichunk[i] = input[index[i]];
173+
}
174+
let mut ochunk = [0; 3];
175+
let rchunk = base.decode_mut(&ichunk, &mut ochunk);
176+
debug_assert_eq!(rchunk, Ok(3));
177+
if check && iend < 4 && ochunk[oend] != 0 {
178+
let error = Some(DecodeError {
179+
position: index[iend],
180+
kind: DecodeKind::Trailing,
181+
});
182+
return DecodeMutResult {
183+
read,
184+
written,
185+
error,
186+
};
187+
}
188+
output[written..][..oend].copy_from_slice(&ochunk[..oend]);
189+
read = input.len();
190+
written += oend;
191+
DecodeMutResult {
192+
read,
193+
written,
194+
error: None,
195+
}
196+
}
197+
198+
#[derive(Debug, PartialEq, Eq)]
199+
pub(crate) struct DecodeResult {
200+
pub(crate) read: usize,
201+
pub(crate) output: Vec<u8>,
202+
pub(crate) error: Option<DecodeError>,
203+
}
204+
205+
/// Decodes `input` in `output` according to the given parameters.
206+
pub(crate) fn decode(
207+
input: &[u8],
208+
alphabet: Alphabet,
209+
last_chunk_handling: LastChunkHandling,
210+
max_length: Option<usize>,
211+
) -> DecodeResult {
212+
let mut output = vec![0; 6 * input.len() / 8];
213+
let DecodeMutResult {
214+
read,
215+
written,
216+
error,
217+
} = decode_mut(
218+
input,
219+
&mut output,
220+
alphabet,
221+
last_chunk_handling,
222+
max_length,
223+
);
224+
debug_assert!(written <= output.len());
225+
output.truncate(written);
226+
DecodeResult {
227+
read,
228+
output,
229+
error,
230+
}
231+
}
232+
233+
pub(crate) fn encode(input: &[u8], alphabet: Alphabet, omit_padding: bool) -> String {
234+
let base = match (alphabet, omit_padding) {
235+
(Alphabet::Base64, false) => &data_encoding::BASE64,
236+
(Alphabet::Base64, true) => &data_encoding::BASE64_NOPAD,
237+
(Alphabet::Base64Url, false) => &data_encoding::BASE64URL,
238+
(Alphabet::Base64Url, true) => &data_encoding::BASE64URL_NOPAD,
239+
};
240+
241+
let mut output = String::with_capacity(base.encode_len(input.len()));
242+
base.encode_append(input, &mut output);
243+
output
244+
}
245+
246+
const BASE64: Encoding = new_encoding! {
247+
symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
248+
ignore: " \t\n\x0C\r",
249+
check_trailing_bits: false,
250+
};
251+
252+
const BASE64URL: Encoding = new_encoding! {
253+
symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
254+
ignore: " \t\n\x0C\r",
255+
check_trailing_bits: false,
256+
};

0 commit comments

Comments
 (0)