mini_racer_rs 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,68 +1,130 @@
1
- // Returned HTML is mostly ASCII, which is copied in spans; only blocks holding
2
- // another byte are converted a byte at a time.
1
+ // Returned HTML is mostly ASCII, which is copied in blocks; only blocks
2
+ // holding another byte are converted a byte at a time.
3
3
  const LATIN1_BLOCK: usize = 32;
4
- // Each block's count of non-ASCII bytes is summed in a u8.
5
- const _: () = assert!(LATIN1_BLOCK <= u8::MAX as usize);
4
+ // Lengths are counted a block at a time in a u8, which vectorizes; a UTF-16
5
+ // unit adds at most 3.
6
+ const COUNT_BLOCK: usize = 64;
7
+ const _: () = assert!(COUNT_BLOCK * 3 <= u8::MAX as usize);
6
8
 
7
- #[expect(clippy::expect_used, reason = "callers pass only spans of ASCII bytes")]
8
- fn ascii(bytes: &[u8]) -> &str {
9
- std::str::from_utf8(bytes).expect("ASCII is UTF-8")
9
+ /// The length in UTF-8 of Latin-1 `bytes`.
10
+ #[must_use]
11
+ pub fn latin1_utf8_len(bytes: &[u8]) -> usize {
12
+ let wide = bytes.chunks(COUNT_BLOCK).fold(0_usize, |wide, block| {
13
+ let count = block.iter().fold(0_u8, |count, &byte| {
14
+ count.wrapping_add(u8::from(!byte.is_ascii()))
15
+ });
16
+ wide.saturating_add(usize::from(count))
17
+ });
18
+ bytes.len().saturating_add(wide)
10
19
  }
11
20
 
12
- #[must_use]
13
- pub fn latin1_to_utf8(bytes: &[u8]) -> String {
14
- if bytes.len() < LATIN1_BLOCK {
15
- return bytes.iter().copied().map(char::from).collect();
16
- }
17
- let (wide, mixed) =
18
- bytes
19
- .chunks(LATIN1_BLOCK)
20
- .fold((0_usize, 0_usize), |(wide, mixed), block| {
21
- let count = block
22
- .iter()
23
- .map(|&byte| u8::from(!byte.is_ascii()))
24
- .sum::<u8>();
25
- (
26
- wide.saturating_add(usize::from(count)),
27
- mixed.saturating_add(usize::from(count > 0)),
28
- )
29
- });
30
- let mut out = String::with_capacity(bytes.len().saturating_add(wide));
31
- // Text where most blocks need converting anyway, like accented prose, is
32
- // faster a byte at a time throughout.
33
- if mixed.saturating_mul(LATIN1_BLOCK).saturating_mul(2) > bytes.len() {
34
- out.extend(bytes.iter().copied().map(char::from));
35
- return out;
36
- }
37
- let mut span = 0;
21
+ // Writes `c` at `at` and returns where it ends, or `None` when it does not fit.
22
+ fn put(out: &mut [u8], at: usize, c: char) -> Option<usize> {
23
+ let end = at.checked_add(c.len_utf8())?;
24
+ c.encode_utf8(out.get_mut(at..end)?);
25
+ Some(end)
26
+ }
27
+
28
+ /// Writes the UTF-8 of Latin-1 `bytes` at the start of `out`, up to the last
29
+ /// character that fits, and returns how many bytes it wrote: all
30
+ /// `latin1_utf8_len(bytes)` of them when `out` is that long.
31
+ pub fn latin1_to_utf8_into(bytes: &[u8], out: &mut [u8]) -> usize {
38
32
  let mut at: usize = 0;
39
33
  for block in bytes.chunks(LATIN1_BLOCK) {
40
- let next = at.saturating_add(block.len());
41
- if !block.is_ascii() {
42
- if span < at {
43
- out.push_str(ascii(bytes.get(span..at).unwrap_or_default()));
44
- }
45
- for &byte in block {
46
- out.push(char::from(byte));
34
+ let end = at.saturating_add(block.len());
35
+ if block.is_ascii()
36
+ && let Some(slots) = out.get_mut(at..end)
37
+ {
38
+ slots.copy_from_slice(block);
39
+ at = end;
40
+ continue;
41
+ }
42
+ for &byte in block {
43
+ match put(out, at, char::from(byte)) {
44
+ Some(next) => at = next,
45
+ None => return at,
47
46
  }
48
- span = next;
49
47
  }
50
- at = next;
51
48
  }
52
- out.push_str(ascii(bytes.get(span..).unwrap_or_default()));
53
- out
49
+ at
54
50
  }
55
51
 
56
52
  #[must_use]
57
- pub fn utf16le_to_utf8(bytes: &[u8]) -> Option<String> {
53
+ #[expect(clippy::expect_used, reason = "only whole characters are written")]
54
+ pub fn latin1_to_utf8(bytes: &[u8]) -> String {
55
+ let mut out = vec![0; latin1_utf8_len(bytes)];
56
+ let written = latin1_to_utf8_into(bytes, &mut out);
57
+ out.truncate(written);
58
+ String::from_utf8(out).expect("Latin-1 transcodes to UTF-8")
59
+ }
60
+
61
+ fn utf16le_chars(bytes: &[u8]) -> Option<impl Iterator<Item = Result<char, ()>>> {
62
+ let (pairs, rest) = bytes.as_chunks::<2>();
63
+ let units = pairs.iter().copied().map(u16::from_le_bytes);
64
+ rest.is_empty()
65
+ .then(|| char::decode_utf16(units).map(|c| c.map_err(|_| ())))
66
+ }
67
+
68
+ /// The length in UTF-8 of UTF-16LE `bytes`, or `None` when they hold an odd
69
+ /// byte or a lone surrogate.
70
+ #[must_use]
71
+ pub fn utf16le_utf8_len(bytes: &[u8]) -> Option<usize> {
58
72
  let (pairs, rest) = bytes.as_chunks::<2>();
59
73
  if !rest.is_empty() {
60
74
  return None;
61
75
  }
62
- let units = pairs.iter().copied().map(u16::from_le_bytes);
63
- char::decode_utf16(units)
64
- .collect::<Result<String, _>>()
65
- .ok()
76
+ // A unit takes one byte, one more from U+0080 and one more from U+0800,
77
+ // which counts the two units of a surrogate pair as six bytes for its four.
78
+ let mut len: usize = 0;
79
+ let mut surrogates = false;
80
+ for block in pairs.chunks(COUNT_BLOCK) {
81
+ let mut count: u8 = 0;
82
+ for &pair in block {
83
+ let unit = u16::from_le_bytes(pair);
84
+ count = count
85
+ .wrapping_add(1)
86
+ .wrapping_add(u8::from(unit >= 0x80))
87
+ .wrapping_add(u8::from(unit >= 0x800));
88
+ surrogates |= (0xD800..0xE000).contains(&unit);
89
+ }
90
+ len = len.saturating_add(usize::from(count));
91
+ }
92
+ if !surrogates {
93
+ return Some(len);
94
+ }
95
+ let mut surrogate_pairs: usize = 0;
96
+ let mut units = pairs.iter().copied().map(u16::from_le_bytes);
97
+ while let Some(unit) = units.next() {
98
+ if (0xD800..0xDC00).contains(&unit) {
99
+ units.next().filter(|low| (0xDC00..0xE000).contains(low))?;
100
+ surrogate_pairs = surrogate_pairs.saturating_add(1);
101
+ } else if (0xDC00..0xE000).contains(&unit) {
102
+ return None;
103
+ }
104
+ }
105
+ Some(len.saturating_sub(surrogate_pairs.saturating_mul(2)))
106
+ }
107
+
108
+ /// Writes the UTF-8 of UTF-16LE `bytes` at the start of `out` and returns how
109
+ /// many bytes it wrote.
110
+ ///
111
+ /// `None` when they hold an odd byte or a lone surrogate, or when `out` lacks
112
+ /// room, which three bytes per unit never does.
113
+ pub fn utf16le_to_utf8_into(bytes: &[u8], out: &mut [u8]) -> Option<usize> {
114
+ let mut at = 0;
115
+ for c in utf16le_chars(bytes)? {
116
+ at = put(out, at, c.ok()?)?;
117
+ }
118
+ Some(at)
119
+ }
120
+
121
+ #[must_use]
122
+ #[expect(clippy::expect_used, reason = "only whole characters are written")]
123
+ pub fn utf16le_to_utf8(bytes: &[u8]) -> Option<String> {
124
+ let mut out = vec![0; utf16le_utf8_len(bytes)?];
125
+ let written = utf16le_to_utf8_into(bytes, &mut out)?;
126
+ out.truncate(written);
127
+ Some(String::from_utf8(out).expect("UTF-16 transcodes to UTF-8"))
66
128
  }
67
129
 
68
130
  #[cfg(test)]
@@ -83,8 +145,38 @@ mod tests {
83
145
  utf16le_to_utf8(&[0x3D, 0xD8, 0x00, 0xDE]).as_deref(),
84
146
  Some("😀")
85
147
  );
148
+ assert_eq!(
149
+ utf16le_utf8_len(&[0x3D, 0xD8, 0x00, 0xDE, 0x41, 0x00]),
150
+ Some(5)
151
+ );
86
152
  assert_eq!(utf16le_to_utf8(&[0x41]), None);
153
+ assert_eq!(utf16le_utf8_len(&[0x41]), None);
87
154
  assert_eq!(utf16le_to_utf8(&[0x3D, 0xD8]), None);
155
+ assert_eq!(utf16le_utf8_len(&[0x41, 0x00, 0x00, 0xDC]), None);
156
+ }
157
+
158
+ #[test]
159
+ fn writers_stop_where_text_does_not_decode_or_fit() {
160
+ let mut out = [0; 8];
161
+ assert_eq!(
162
+ utf16le_to_utf8_into(&[0x41, 0x00, 0x00, 0xDC], &mut out),
163
+ None
164
+ );
165
+ assert_eq!(utf16le_to_utf8_into(&[0x41, 0x00, 0x42], &mut out), None);
166
+ assert_eq!(
167
+ utf16le_to_utf8_into(&[0x3D, 0xD8, 0x00, 0xDE], out.get_mut(..3).unwrap()),
168
+ None
169
+ );
170
+ assert_eq!(
171
+ utf16le_to_utf8_into(&[0x3D, 0xD8, 0x00, 0xDE], &mut out),
172
+ Some(4)
173
+ );
174
+ let written = latin1_to_utf8_into(b"ab\xE9c", out.get_mut(..3).unwrap());
175
+ assert_eq!(out.get(..written), Some(b"ab".as_slice()));
176
+ }
177
+
178
+ fn utf16le(units: &[u16]) -> Vec<u8> {
179
+ units.iter().flat_map(|unit| unit.to_le_bytes()).collect()
88
180
  }
89
181
 
90
182
  proptest! {
@@ -100,7 +192,26 @@ mod tests {
100
192
  )) {
101
193
  let bytes: Vec<u8> = segments.into_iter().flat_map(|(run, wide)| run.into_iter().chain(wide)).collect();
102
194
  let by_byte: String = bytes.iter().copied().map(char::from).collect();
195
+ prop_assert_eq!(latin1_utf8_len(&bytes), by_byte.len());
196
+ let mut out = vec![0; bytes.len().saturating_mul(2)];
197
+ let written = latin1_to_utf8_into(&bytes, &mut out);
198
+ prop_assert_eq!(out.get(..written), Some(by_byte.as_bytes()));
103
199
  prop_assert_eq!(latin1_to_utf8(&bytes), by_byte);
104
200
  }
201
+
202
+ // Units from each range UTF-8 sizes differently, surrogates included,
203
+ // so pairs, lone halves and pairs in the wrong order all come up.
204
+ #[test]
205
+ fn utf16_matches_std(units in proptest::collection::vec(
206
+ prop_oneof![0u16..0x80, 0x80u16..0x800, 0x800u16..0xD800, 0xD800u16..0xE000, 0xE000u16..=0xFFFF],
207
+ 0..64,
208
+ )) {
209
+ let expected = String::from_utf16(&units).ok();
210
+ prop_assert_eq!(utf16le_utf8_len(&utf16le(&units)), expected.as_ref().map(String::len));
211
+ let mut out = vec![0; units.len().saturating_mul(3)];
212
+ let written = utf16le_to_utf8_into(&utf16le(&units), &mut out);
213
+ prop_assert_eq!(written.and_then(|n| out.get(..n)), expected.as_ref().map(String::as_bytes));
214
+ prop_assert_eq!(utf16le_to_utf8(&utf16le(&units)), expected);
215
+ }
105
216
  }
106
217
  }