1mod install;
14
15pub(crate) use install::declare_types;
16pub use install::{declare, install};
17
18use base64::Engine as _;
19use base64::alphabet;
20use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, NO_PAD, PAD};
21use wasmtime::{ArrayRef, Caller, Rooted, StructRef, StructRefPre, Val};
22
23use crate::runtime::StoreData;
24use crate::runtime::fuel;
25use crate::runtime::host::{
26 fatal_host_error, host_boxed_number_vtable, read_uint8, read_uint8_array_arg,
27 read_uint8_array_range, uint8_array_backing, write_submilli_uint8array_struct,
28};
29use crate::runtime::intrinsic_types::intrinsic_types;
30use crate::runtime::number::{format_number_js, to_uint8};
31use crate::runtime::prelude::array::{ElementCallback, merge_sort};
32use crate::runtime::prelude::closure::Closure;
33use crate::runtime::prelude::iterator::as_struct;
34use crate::runtime::prelude::keep::KeptValue;
35use crate::runtime::prelude::vtable::{read_object_entries, read_string_units};
36
37pub(crate) fn read_bytes(
46 caller: &mut Caller<'_, StoreData>,
47 val: &Val,
48 name: &str,
49) -> wasmtime::Result<Vec<u8>> {
50 read_uint8_array_arg(caller, val, name)
51}
52
53fn build(caller: &mut Caller<'_, StoreData>, bytes: &[u8]) -> wasmtime::Result<Val> {
55 let st = write_submilli_uint8array_struct(caller, bytes)?;
56 Ok(Val::AnyRef(Some(st.to_anyref())))
57}
58
59fn store_bytes(
63 caller: &mut Caller<'_, StoreData>,
64 receiver: &Val,
65 bytes: &[u8],
66) -> wasmtime::Result<()> {
67 let raw = backing(caller, receiver)?;
68 fuel::charge(&mut *caller, fuel::COPY, bytes.len() as u64)?;
69 raw.write_i8(&mut *caller, 0, bytes)
70}
71
72fn backing(
74 caller: &mut Caller<'_, StoreData>,
75 receiver: &Val,
76) -> wasmtime::Result<Rooted<ArrayRef>> {
77 let st = as_struct(caller, receiver, "Uint8Array mutate receiver")?;
78 match st.field(&mut *caller, 1)? {
79 Val::AnyRef(Some(arr)) => arr.unwrap_array(&mut *caller),
80 other => Err(wasmtime::Error::msg(format!(
81 "Uint8Array: malformed $rawUint8Array backing {other:?}"
82 ))),
83 }
84}
85
86fn box_byte(caller: &mut Caller<'_, StoreData>, b: u8) -> wasmtime::Result<Val> {
88 let boxed = intrinsic_types(&mut *caller)?.boxed_number.clone();
89 let vtable = host_boxed_number_vtable(caller)?;
90 let pre = StructRefPre::new(&mut *caller, boxed);
91 let st = StructRef::new(
92 &mut *caller,
93 &pre,
94 &[vtable, Val::F64(f64::from(b).to_bits())],
95 )?;
96 Ok(Val::AnyRef(Some(st.to_anyref())))
97}
98
99fn unbox_byte(caller: &mut Caller<'_, StoreData>, v: &Val, name: &str) -> wasmtime::Result<u8> {
101 let Val::AnyRef(Some(any)) = v else {
102 return Err(wasmtime::Error::msg(format!(
103 "{name} callback returned {v:?}, expected a number"
104 )));
105 };
106 let st = any
107 .as_struct(&mut *caller)?
108 .ok_or_else(|| wasmtime::Error::msg(format!("{name} callback result is not a number")))?;
109 match st.field(&mut *caller, 1)? {
110 Val::F64(bits) => Ok(to_uint8(f64::from_bits(bits))),
111 other => Err(wasmtime::Error::msg(format!(
112 "{name} callback result field was {other:?}, not f64"
113 ))),
114 }
115}
116
117fn trunc_sat(x: f64) -> i32 {
122 x as i32
123}
124
125fn norm_clamp(x: f64, len: i32) -> i32 {
126 let mut i = trunc_sat(x);
127 if i < 0 {
128 i += len;
129 }
130 i.clamp(0, len)
131}
132
133fn at_index(x: f64, len: i32) -> Option<usize> {
134 let mut i = trunc_sat(x);
135 if i < 0 {
136 i += len;
137 }
138 if i < 0 || i >= len {
139 None
140 } else {
141 Some(i as usize)
142 }
143}
144
145fn fwd_from(x: f64, len: i32) -> i32 {
146 let mut fi = trunc_sat(x);
147 if fi < 0 {
148 fi += len;
149 }
150 fi.max(0)
151}
152
153fn last_from(x: f64, len: i32) -> Option<i32> {
154 let mut fi = trunc_sat(x);
155 if fi < 0 {
156 fi += len;
157 }
158 if fi < 0 {
159 return None;
160 }
161 Some(fi.min(len - 1))
162}
163
164fn length(caller: &mut Caller<'_, StoreData>, receiver: &Val, name: &str) -> wasmtime::Result<f64> {
170 let arr = uint8_array_backing(caller, receiver, name)?;
171 Ok(f64::from(arr.len(&mut *caller)?))
172}
173
174fn at(
176 caller: &mut Caller<'_, StoreData>,
177 receiver: &Val,
178 index: f64,
179 name: &str,
180) -> wasmtime::Result<Val> {
181 let arr = uint8_array_backing(caller, receiver, name)?;
182 let len = arr.len(&mut *caller)?;
183 match at_index(index, len as i32) {
184 Some(i) => {
185 let byte = read_uint8(caller, arr, i)?;
186 box_byte(caller, byte)
187 }
188 None => super::undefined::value(caller),
189 }
190}
191
192fn slice(
194 caller: &mut Caller<'_, StoreData>,
195 receiver: &Val,
196 start: f64,
197 end: f64,
198 name: &str,
199) -> wasmtime::Result<Vec<u8>> {
200 let arr = uint8_array_backing(caller, receiver, name)?;
201 let len = arr.len(&mut *caller)? as i32;
202 let si = norm_clamp(start, len);
203 let count = (norm_clamp(end, len) - si).max(0) as usize;
204 read_uint8_array_range(caller, arr, si as usize, count, name)
205}
206
207fn with(bytes: &[u8], index: f64, value: f64) -> Option<Vec<u8>> {
210 let i = at_index(index, bytes.len() as i32)?;
211 let mut out = bytes.to_vec();
212 out[i] = to_uint8(value);
213 Some(out)
214}
215
216fn searched_byte(target: f64) -> Option<u8> {
219 (target.fract() == 0.0 && (0.0..=255.0).contains(&target)).then_some(target as u8)
220}
221
222fn index_of(bytes: &[u8], target: f64, from: f64) -> f64 {
223 let len = bytes.len() as i32;
224 let Some(target) = searched_byte(target) else {
225 return -1.0;
226 };
227 let mut i = fwd_from(from, len);
228 while i < len {
229 if bytes[i as usize] == target {
230 return f64::from(i);
231 }
232 i += 1;
233 }
234 -1.0
235}
236
237fn last_index_of(bytes: &[u8], target: f64, from: f64) -> f64 {
238 let len = bytes.len() as i32;
239 let Some(target) = searched_byte(target) else {
240 return -1.0;
241 };
242 let Some(mut i) = last_from(from, len) else {
243 return -1.0;
244 };
245 while i >= 0 {
246 if bytes[i as usize] == target {
247 return f64::from(i);
248 }
249 i -= 1;
250 }
251 -1.0
252}
253
254fn includes(bytes: &[u8], target: f64, from: f64) -> bool {
255 index_of(bytes, target, from) >= 0.0
256}
257
258pub(crate) fn join(bytes: &[u8], sep: &[u16]) -> Vec<u16> {
261 let mut out: Vec<u16> = Vec::new();
262 for (i, &b) in bytes.iter().enumerate() {
263 if i > 0 {
264 out.extend_from_slice(sep);
265 }
266 out.extend(format_number_js(f64::from(b)).encode_utf16());
267 }
268 out
269}
270
271fn equals(a: &[u8], b: &[u8]) -> bool {
272 a == b
273}
274
275fn reverse(
280 caller: &mut Caller<'_, StoreData>,
281 receiver: &Val,
282 mut bytes: Vec<u8>,
283) -> wasmtime::Result<Val> {
284 bytes.reverse();
285 store_bytes(caller, receiver, &bytes)?;
286 Ok(*receiver)
287}
288
289fn fill(
290 caller: &mut Caller<'_, StoreData>,
291 receiver: &Val,
292 value: f64,
293 start: f64,
294 end: f64,
295) -> wasmtime::Result<Val> {
296 let raw = backing(caller, receiver)?;
297 let len = i32::try_from(raw.len(&mut *caller)?).map_err(fatal_host_error)?;
298 let start = norm_clamp(start, len) as u32;
299 let end = norm_clamp(end, len) as u32;
300 let count = end.saturating_sub(start);
301 fuel::charge(&mut *caller, fuel::COPY, u64::from(count))?;
302 let chunk = [to_uint8(value); 4096];
303 let mut offset = start;
304 while offset < end {
305 let count = (end - offset).min(chunk.len() as u32);
306 let bytes = chunk
307 .get(..count as usize)
308 .ok_or_else(|| fatal_host_error("invalid fill chunk"))?;
309 raw.write_i8(&mut *caller, offset, bytes)?;
310 offset += count;
311 }
312 Ok(*receiver)
313}
314
315fn copy_within(
316 caller: &mut Caller<'_, StoreData>,
317 receiver: &Val,
318 target: f64,
319 start: f64,
320 end: f64,
321) -> wasmtime::Result<Val> {
322 let raw = backing(caller, receiver)?;
323 let len = i32::try_from(raw.len(&mut *caller)?).map_err(fatal_host_error)?;
324 let target = norm_clamp(target, len) as usize;
325 let start = norm_clamp(start, len) as usize;
326 let end = norm_clamp(end, len) as usize;
327 let count = end.saturating_sub(start).min(len as usize - target);
328 let bytes = read_uint8_array_range(caller, raw, start, count, "Uint8Array#copyWithin")?;
330 fuel::charge(&mut *caller, fuel::COPY, count as u64)?;
331 raw.write_i8(&mut *caller, target as u32, &bytes)?;
332 Ok(*receiver)
333}
334
335fn set(
337 caller: &mut Caller<'_, StoreData>,
338 receiver: &Val,
339 source: &Val,
340 offset: f64,
341) -> wasmtime::Result<()> {
342 let target = backing(caller, receiver)?;
343 let source = backing(caller, source)?;
344 let target_len = target.len(&mut *caller)?;
345 let source_len = source.len(&mut *caller)?;
346 let offset = trunc_sat(offset);
347 if offset < 0
348 || (offset as u32)
349 .checked_add(source_len)
350 .is_none_or(|end| end > target_len)
351 {
352 return Err(crate::runtime::host::range_error("offset is out of bounds"));
353 }
354 let bytes = read_uint8_array_range(
355 caller,
356 source,
357 0,
358 source_len as usize,
359 "Uint8Array#set source",
360 )?;
361 fuel::charge(&mut *caller, fuel::COPY, u64::from(source_len))?;
362 target.write_i8(&mut *caller, offset as u32, &bytes)
363}
364
365fn allocate(caller: &mut Caller<'_, StoreData>, len: usize) -> wasmtime::Result<Val> {
366 use wasmtime::ArrayRefPre;
367 let intr = intrinsic_types(&mut *caller)?;
368 fuel::charge(&mut *caller, fuel::COPY, len as u64)?;
369 let len = u32::try_from(len).map_err(fatal_host_error)?;
370 let pre = ArrayRefPre::new(&mut *caller, intr.raw_uint8_array.clone());
371 let raw = ArrayRef::new_zeroed_i8(&mut *caller, &pre, len)?;
372 let vtable = crate::runtime::host::host_uint8_array_vtable(caller)?;
373 let pre = StructRefPre::new(&mut *caller, intr.uint8_array.clone());
374 let value = StructRef::new(
375 &mut *caller,
376 &pre,
377 &[vtable, Val::AnyRef(Some(raw.to_anyref()))],
378 )?;
379 Ok(Val::AnyRef(Some(value.to_anyref())))
380}
381
382async fn sort_bytes(
383 caller: &mut Caller<'_, StoreData>,
384 bytes: &mut Vec<u8>,
385 cmp: Option<&Closure>,
386) -> wasmtime::Result<()> {
387 let Some(cmp) = cmp else {
388 bytes.sort_unstable();
389 return Ok(());
390 };
391 if bytes.len() < 2 {
392 return Ok(());
393 }
394 let mut boxes = Vec::with_capacity(256);
397 for byte in 0..=u8::MAX {
398 boxes.push(box_byte(caller, byte)?);
399 }
400 merge_sort(caller, bytes, cmp, |_, byte| {
401 boxes
402 .get(usize::from(byte))
403 .copied()
404 .ok_or_else(|| fatal_host_error("Uint8Array#sort: byte has no box"))
405 })
406 .await
407}
408
409async fn sort(
410 caller: &mut Caller<'_, StoreData>,
411 receiver: &Val,
412 mut bytes: Vec<u8>,
413 cmp: Option<Closure>,
414) -> wasmtime::Result<Val> {
415 sort_bytes(caller, &mut bytes, cmp.as_ref()).await?;
416 store_bytes(caller, receiver, &bytes)?;
417 Ok(*receiver)
418}
419
420fn to_reversed(mut bytes: Vec<u8>) -> Vec<u8> {
425 bytes.reverse();
426 bytes
427}
428
429async fn to_sorted(
430 caller: &mut Caller<'_, StoreData>,
431 mut bytes: Vec<u8>,
432 cmp: Option<Closure>,
433) -> wasmtime::Result<Vec<u8>> {
434 sort_bytes(caller, &mut bytes, cmp.as_ref()).await?;
435 Ok(bytes)
436}
437
438async fn for_each(
443 caller: &mut Caller<'_, StoreData>,
444 array: Val,
445 bytes: Vec<u8>,
446 f: &Closure,
447) -> wasmtime::Result<()> {
448 let f = ElementCallback::new(caller, f, Some(array))?;
449 for (index, b) in bytes.into_iter().enumerate() {
450 let boxed = box_byte(caller, b)?;
451 f.call(caller, None, boxed, index).await?;
452 }
453 Ok(())
454}
455
456async fn map(
457 caller: &mut Caller<'_, StoreData>,
458 array: Val,
459 bytes: Vec<u8>,
460 f: &Closure,
461) -> wasmtime::Result<Vec<u8>> {
462 let f = ElementCallback::new(caller, f, Some(array))?;
463 let mut out = Vec::with_capacity(bytes.len());
464 for (index, b) in bytes.into_iter().enumerate() {
465 let boxed = box_byte(caller, b)?;
466 let r = f.call(caller, None, boxed, index).await?;
467 out.push(unbox_byte(caller, &r, "Uint8Array#map")?);
468 }
469 Ok(out)
470}
471
472async fn filter(
473 caller: &mut Caller<'_, StoreData>,
474 array: Val,
475 bytes: Vec<u8>,
476 pred: &Closure,
477) -> wasmtime::Result<Vec<u8>> {
478 let pred = ElementCallback::new(caller, pred, Some(array))?;
479 let mut out = Vec::new();
480 for (index, b) in bytes.into_iter().enumerate() {
481 let boxed = box_byte(caller, b)?;
482 if pred.test(caller, boxed, index).await? {
483 out.push(b);
484 }
485 }
486 Ok(out)
487}
488
489async fn reduce(
490 caller: &mut Caller<'_, StoreData>,
491 array: Val,
492 bytes: Vec<u8>,
493 f: &Closure,
494 mut acc: Val,
495 reverse: bool,
496) -> wasmtime::Result<Val> {
497 let f = ElementCallback::new(caller, f, Some(array))?;
498 let order: Vec<usize> = if reverse {
499 (0..bytes.len()).rev().collect()
500 } else {
501 (0..bytes.len()).collect()
502 };
503 let kept_acc = KeptValue::new(caller)?;
505 for i in order {
506 let boxed = box_byte(caller, bytes[i])?;
507 acc = f.call(caller, Some(acc), boxed, i).await?;
508 kept_acc.set(caller, acc)?;
509 }
510 Ok(acc)
511}
512
513async fn some(
514 caller: &mut Caller<'_, StoreData>,
515 array: Val,
516 bytes: Vec<u8>,
517 pred: &Closure,
518) -> wasmtime::Result<bool> {
519 let pred = ElementCallback::new(caller, pred, Some(array))?;
520 for (index, b) in bytes.into_iter().enumerate() {
521 let boxed = box_byte(caller, b)?;
522 if pred.test(caller, boxed, index).await? {
523 return Ok(true);
524 }
525 }
526 Ok(false)
527}
528
529async fn every(
530 caller: &mut Caller<'_, StoreData>,
531 array: Val,
532 bytes: Vec<u8>,
533 pred: &Closure,
534) -> wasmtime::Result<bool> {
535 let pred = ElementCallback::new(caller, pred, Some(array))?;
536 for (index, b) in bytes.into_iter().enumerate() {
537 let boxed = box_byte(caller, b)?;
538 if !pred.test(caller, boxed, index).await? {
539 return Ok(false);
540 }
541 }
542 Ok(true)
543}
544
545async fn find_match(
547 caller: &mut Caller<'_, StoreData>,
548 array: Val,
549 bytes: &[u8],
550 pred: &Closure,
551 reverse: bool,
552) -> wasmtime::Result<Option<usize>> {
553 let pred = ElementCallback::new(caller, pred, Some(array))?;
554 let order: Vec<usize> = if reverse {
555 (0..bytes.len()).rev().collect()
556 } else {
557 (0..bytes.len()).collect()
558 };
559 for i in order {
560 let boxed = box_byte(caller, bytes[i])?;
561 if pred.test(caller, boxed, i).await? {
562 return Ok(Some(i));
563 }
564 }
565 Ok(None)
566}
567
568pub(crate) fn to_base64_standard(bytes: &[u8]) -> String {
574 encode_base64(bytes, false, false)
575}
576
577fn encode_base64(bytes: &[u8], url_safe: bool, omit_padding: bool) -> String {
578 let alpha = if url_safe {
579 &alphabet::URL_SAFE
580 } else {
581 &alphabet::STANDARD
582 };
583 let config: GeneralPurposeConfig = if omit_padding { NO_PAD } else { PAD };
584 GeneralPurpose::new(alpha, config).encode(bytes)
585}
586
587fn decode_base64(s: &str, url_safe: bool) -> wasmtime::Result<Vec<u8>> {
589 let alpha = if url_safe {
590 &alphabet::URL_SAFE
591 } else {
592 &alphabet::STANDARD
593 };
594 let config = if s.as_bytes().last() == Some(&b'=') {
595 PAD
596 } else {
597 NO_PAD
598 };
599 GeneralPurpose::new(alpha, config)
600 .decode(s.as_bytes())
601 .map_err(|e| crate::runtime::host::syntax_error(format!("Uint8Array.fromBase64: {e}")))
602}
603
604fn to_hex(bytes: &[u8]) -> String {
605 let mut out = String::with_capacity(bytes.len() * 2);
606 for &b in bytes {
607 out.push(hex_digit(b >> 4));
608 out.push(hex_digit(b & 0xf));
609 }
610 out
611}
612
613fn hex_digit(nibble: u8) -> char {
614 char::from(if nibble < 10 {
615 b'0' + nibble
616 } else {
617 b'a' + (nibble - 10)
618 })
619}
620
621fn from_hex(units: &[u16]) -> wasmtime::Result<Vec<u8>> {
624 if !units.len().is_multiple_of(2) {
625 return Err(crate::runtime::host::syntax_error(
626 "Uint8Array.fromHex: string must have an even number of characters",
627 ));
628 }
629 let mut out = Vec::with_capacity(units.len() / 2);
630 for pair in units.as_chunks::<2>().0 {
631 let hi = hex_nibble(pair[0])?;
632 let lo = hex_nibble(pair[1])?;
633 out.push((hi << 4) | lo);
634 }
635 Ok(out)
636}
637
638fn hex_nibble(c: u16) -> wasmtime::Result<u8> {
639 match c {
640 0x30..=0x39 => Ok((c - 0x30) as u8),
641 0x61..=0x66 => Ok((c - 0x61 + 10) as u8),
642 0x41..=0x46 => Ok((c - 0x41 + 10) as u8),
643 _ => Err(crate::runtime::host::syntax_error(
644 "Uint8Array.fromHex: invalid hex digit",
645 )),
646 }
647}
648
649fn read_base64_options(
652 caller: &mut Caller<'_, StoreData>,
653 opt: &Val,
654) -> wasmtime::Result<(bool, bool)> {
655 if super::undefined::is_undefined(caller, opt)? {
656 return Ok((false, false));
657 }
658 let (mut url_safe, mut omit_padding) = (false, false);
659 for (name, value) in read_object_entries(caller, opt, "Base64Options")? {
660 if super::undefined::is_undefined(caller, &value)? {
663 continue;
664 }
665 match String::from_utf16_lossy(&name).as_str() {
666 "alphabet" => {
667 let alphabet = read_string_units(caller, &value, "Base64Options.alphabet")?;
668 url_safe = String::from_utf16_lossy(&alphabet) == "base64url";
669 }
670 "omitPadding" => omit_padding = read_boolean(caller, &value)?,
671 _ => {}
672 }
673 }
674 Ok((url_safe, omit_padding))
675}
676
677fn read_boolean(caller: &mut Caller<'_, StoreData>, val: &Val) -> wasmtime::Result<bool> {
678 let st = as_struct(caller, val, "Base64Options.omitPadding")?;
679 match st.field(&mut *caller, 1)? {
680 Val::I32(b) => Ok(b != 0),
681 other => wasmtime::bail!("Base64Options.omitPadding: field 1 is {other:?}, not i32"),
682 }
683}
684
685pub(crate) fn alloc_len(n: f64) -> wasmtime::Result<usize> {
698 if n.trunc() < 0.0 {
701 return Err(crate::runtime::host::range_error(format!(
702 "invalid Uint8Array length {} — a length cannot be negative",
703 format_number_js(n)
704 )));
705 }
706 if n > MAX_ALLOC_LEN as f64 {
707 return Err(crate::runtime::host::range_error(format!(
708 "invalid Uint8Array length {} — the maximum is {MAX_ALLOC_LEN}",
709 format_number_js(n)
710 )));
711 }
712 Ok(n as u32 as usize)
713}
714
715pub(crate) const MAX_ALLOC_LEN: usize = 1 << 30;
719
720fn read_number_array(
722 caller: &mut Caller<'_, StoreData>,
723 val: &Val,
724 name: &str,
725) -> wasmtime::Result<Vec<u8>> {
726 let elements = crate::runtime::prelude::array::read_array(caller, val, name)?;
727 let mut bytes = Vec::with_capacity(elements.len());
728 for e in &elements {
729 bytes.push(unbox_byte(caller, e, name)?);
730 }
731 Ok(bytes)
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 fn units(s: &str) -> Vec<u16> {
739 s.encode_utf16().collect()
740 }
741
742 #[test]
743 fn base64_round_trips_both_alphabets() {
744 let bytes = [0xfb_u8, 0xff, 0x00, 0x10, 0x41];
745 for url_safe in [false, true] {
746 let padded = encode_base64(&bytes, url_safe, false);
747 assert_eq!(decode_base64(&padded, url_safe).unwrap(), bytes);
748 let unpadded = encode_base64(&bytes, url_safe, true);
750 assert_eq!(decode_base64(&unpadded, url_safe).unwrap(), bytes);
751 }
752 assert!(encode_base64(&[0xfb, 0xff], true, false).contains('-'));
754 assert!(encode_base64(&[0xfb, 0xff], false, false).contains('+'));
755 }
756
757 #[test]
758 fn base64_padding_selection_preserves_previous_acceptance() {
759 let alphabet = b"AQf=_-";
762 for length in 0..=5_u32 {
763 for mut code in 0..alphabet.len().pow(length) {
764 let mut input = Vec::new();
765 for _ in 0..length {
766 input.push(alphabet[code % alphabet.len()]);
767 code /= alphabet.len();
768 }
769 let text = std::str::from_utf8(&input).unwrap();
770 for url_safe in [false, true] {
771 let alpha = if url_safe {
772 &alphabet::URL_SAFE
773 } else {
774 &alphabet::STANDARD
775 };
776 let old = GeneralPurpose::new(alpha, PAD)
777 .decode(&input)
778 .or_else(|_| GeneralPurpose::new(alpha, NO_PAD).decode(&input));
779 let new = decode_base64(text, url_safe);
780 assert_eq!(new.ok(), old.ok(), "{text:?}, url_safe={url_safe}");
781 }
782 }
783 }
784 }
785
786 #[test]
787 fn hex_round_trips_and_rejects_bad_input() {
788 let bytes = [0x00_u8, 0x0f, 0xa0, 0xff];
789 assert_eq!(to_hex(&bytes), "000fa0ff");
790 assert_eq!(from_hex(&units("000fA0Ff")).unwrap(), bytes);
791 assert!(
792 from_hex(&units("0"))
793 .unwrap_err()
794 .to_string()
795 .contains("even number of characters")
796 );
797 assert!(
798 from_hex(&units("0g"))
799 .unwrap_err()
800 .to_string()
801 .contains("invalid hex digit")
802 );
803 }
804}