diff --git a/neqo-common/src/codec.rs b/neqo-common/src/codec.rs index d5e92a2e48..6ddb717a64 100644 --- a/neqo-common/src/codec.rs +++ b/neqo-common/src/codec.rs @@ -4,7 +4,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use std::fmt::Debug; +use std::{fmt::Debug, mem::size_of}; use crate::hex_with_len; @@ -54,7 +54,7 @@ impl<'a> Decoder<'a> { /// Only use this for tests because we panic rather than reporting a result. #[cfg(any(test, feature = "test-fixture"))] pub fn skip_vec(&mut self, n: usize) { - let len = self.decode_uint(n); + let len = self.decode_n(n); self.skip_inner(len); } @@ -66,16 +66,6 @@ impl<'a> Decoder<'a> { self.skip_inner(len); } - /// Decodes (reads) a single byte. - pub fn decode_byte(&mut self) -> Option { - if self.remaining() < 1 { - return None; - } - let b = self.buf[self.offset]; - self.offset += 1; - Some(b) - } - /// Provides the next byte without moving the read position. #[must_use] pub const fn peek_byte(&self) -> Option { @@ -96,33 +86,43 @@ impl<'a> Decoder<'a> { Some(res) } - /// Decodes an unsigned integer of length 1..=8. - /// - /// # Panics - /// - /// This panics if `n` is not in the range `1..=8`. - pub fn decode_uint(&mut self, n: usize) -> Option { - assert!(n > 0 && n <= 8); + #[inline] + pub(crate) fn decode_n(&mut self, n: usize) -> Option { + debug_assert!(n > 0 && n <= 8); if self.remaining() < n { return None; } - let mut v = 0_u64; - for i in 0..n { - let b = self.buf[self.offset + i]; - v = v << 8 | u64::from(b); - } - self.offset += n; - Some(v) + Some(if n == 1 { + let v = u64::from(self.buf[self.offset]); + self.offset += 1; + v + } else { + let mut buf = [0; 8]; + buf[8 - n..].copy_from_slice(&self.buf[self.offset..self.offset + n]); + self.offset += n; + u64::from_be_bytes(buf) + }) + } + + /// Decodes a big-endian, unsigned integer value into the target type. + /// This returns `None` if there is not enough data remaining + /// or if the conversion to the identified type fails. + /// Conversion is via `u64`, so failures are impossible for + /// unsigned integer types: `u8`, `u16`, `u32`, or `u64`. + /// Signed types will fail if the high bit is set. + pub fn decode_uint>(&mut self) -> Option { + let v = self.decode_n(size_of::()); + v.and_then(|v| T::try_from(v).ok()) } /// Decodes a QUIC varint. pub fn decode_varint(&mut self) -> Option { - let b1 = self.decode_byte()?; + let b1 = self.decode_n(1)?; match b1 >> 6 { - 0 => Some(u64::from(b1 & 0x3f)), - 1 => Some((u64::from(b1 & 0x3f) << 8) | self.decode_uint(1)?), - 2 => Some((u64::from(b1 & 0x3f) << 24) | self.decode_uint(3)?), - 3 => Some((u64::from(b1 & 0x3f) << 56) | self.decode_uint(7)?), + 0 => Some(b1), + 1 => Some((b1 & 0x3f) << 8 | self.decode_n(1)?), + 2 => Some((b1 & 0x3f) << 24 | self.decode_n(3)?), + 3 => Some((b1 & 0x3f) << 56 | self.decode_n(7)?), _ => unreachable!(), } } @@ -147,7 +147,7 @@ impl<'a> Decoder<'a> { /// Decodes a TLS-style length-prefixed buffer. pub fn decode_vec(&mut self, n: usize) -> Option<&'a [u8]> { - let len = self.decode_uint(n); + let len = self.decode_n(n); self.decode_checked(len) } @@ -486,16 +486,28 @@ mod tests { let enc = Encoder::from_hex("0123"); let mut dec = enc.as_decoder(); - assert_eq!(dec.decode_byte().unwrap(), 0x01); - assert_eq!(dec.decode_byte().unwrap(), 0x23); - assert!(dec.decode_byte().is_none()); + assert_eq!(dec.decode_uint::().unwrap(), 0x01); + assert_eq!(dec.decode_uint::().unwrap(), 0x23); + assert!(dec.decode_uint::().is_none()); + } + + #[test] + fn peek_byte() { + let enc = Encoder::from_hex("01"); + let mut dec = enc.as_decoder(); + + assert_eq!(dec.offset(), 0); + assert_eq!(dec.peek_byte().unwrap(), 0x01); + dec.skip(1); + assert_eq!(dec.offset(), 1); + assert!(dec.peek_byte().is_none()); } #[test] fn decode_byte_short() { let enc = Encoder::from_hex(""); let mut dec = enc.as_decoder(); - assert!(dec.decode_byte().is_none()); + assert!(dec.decode_uint::().is_none()); } #[test] @@ -506,7 +518,7 @@ mod tests { assert!(dec.decode(2).is_none()); let mut dec = Decoder::from(&[]); - assert_eq!(dec.decode_remainder().len(), 0); + assert!(dec.decode_remainder().is_empty()); } #[test] diff --git a/neqo-common/src/incrdecoder.rs b/neqo-common/src/incrdecoder.rs index 43a135f497..340e0ec5e4 100644 --- a/neqo-common/src/incrdecoder.rs +++ b/neqo-common/src/incrdecoder.rs @@ -31,7 +31,7 @@ impl IncrementalDecoderUint { if amount < 8 { self.v <<= amount * 8; } - self.v |= dv.decode_uint(amount).unwrap(); + self.v |= dv.decode_n(amount).unwrap(); *r -= amount; if *r == 0 { Some(self.v) @@ -39,7 +39,7 @@ impl IncrementalDecoderUint { None } } else { - let (v, remaining) = dv.decode_byte().map_or_else( + let (v, remaining) = dv.decode_uint::().map_or_else( || unreachable!(), |b| { ( diff --git a/neqo-http3/src/frames/hframe.rs b/neqo-http3/src/frames/hframe.rs index a60b6e9481..707242ae48 100644 --- a/neqo-http3/src/frames/hframe.rs +++ b/neqo-http3/src/frames/hframe.rs @@ -87,7 +87,9 @@ impl HFrame { Self::PriorityUpdateRequest { .. } => H3_FRAME_TYPE_PRIORITY_UPDATE_REQUEST, Self::PriorityUpdatePush { .. } => H3_FRAME_TYPE_PRIORITY_UPDATE_PUSH, Self::Grease => { - HFrameType(Decoder::from(&random::<7>()).decode_uint(7).unwrap() * 0x1f + 0x21) + let r = Decoder::from(&random::<8>()).decode_uint::().unwrap(); + // Zero out the top 7 bits: 2 for being a varint; 5 to account for the *0x1f. + HFrameType((r >> 7) * 0x1f + 0x21) } } } diff --git a/neqo-http3/src/frames/wtframe.rs b/neqo-http3/src/frames/wtframe.rs index 519d318d5d..4a962b5ba5 100644 --- a/neqo-http3/src/frames/wtframe.rs +++ b/neqo-http3/src/frames/wtframe.rs @@ -37,8 +37,7 @@ impl FrameDecoder for WebTransportFrame { if frame_len > WT_FRAME_CLOSE_MAX_MESSAGE_SIZE + 4 { return Err(Error::HttpMessageError); } - let error = - u32::try_from(dec.decode_uint(4).ok_or(Error::HttpMessageError)?).unwrap(); + let error = dec.decode_uint().ok_or(Error::HttpMessageError)?; let Ok(message) = String::from_utf8(dec.decode_remainder().to_vec()) else { return Err(Error::HttpMessageError); }; diff --git a/neqo-transport/src/addr_valid.rs b/neqo-transport/src/addr_valid.rs index 2570134dc3..5cb9341e0b 100644 --- a/neqo-transport/src/addr_valid.rs +++ b/neqo-transport/src/addr_valid.rs @@ -166,9 +166,9 @@ impl AddressValidation { let peer_addr = Self::encode_aad(peer_address, retry); let data = self.self_encrypt.open(peer_addr.as_ref(), token).ok()?; let mut dec = Decoder::new(&data); - match dec.decode_uint(4) { + match dec.decode_uint::() { Some(d) => { - let end = self.start_time + Duration::from_millis(d); + let end = self.start_time + Duration::from_millis(u64::from(d)); if end < now { qtrace!("Expired token: {:?} vs. {:?}", end, now); return None; diff --git a/neqo-transport/src/connection/mod.rs b/neqo-transport/src/connection/mod.rs index bb8098abaa..1d19f1abe7 100644 --- a/neqo-transport/src/connection/mod.rs +++ b/neqo-transport/src/connection/mod.rs @@ -731,9 +731,10 @@ impl Connection { ); let mut dec = Decoder::from(token.as_ref()); - let version = Version::try_from(u32::try_from( - dec.decode_uint(4).ok_or(Error::InvalidResumptionToken)?, - )?)?; + let version = Version::try_from( + dec.decode_uint::() + .ok_or(Error::InvalidResumptionToken)?, + )?; qtrace!([self], " version {:?}", version); if !self.conn_params.get_versions().all().contains(&version) { return Err(Error::DisabledVersion); diff --git a/neqo-transport/src/frame.rs b/neqo-transport/src/frame.rs index af933a3455..d0d191f9e3 100644 --- a/neqo-transport/src/frame.rs +++ b/neqo-transport/src/frame.rs @@ -629,7 +629,7 @@ impl<'a> Frame<'a> { return Err(Error::FrameEncodingError); } let delay = dv(dec)?; - let ignore_order = match d(dec.decode_uint(1))? { + let ignore_order = match d(dec.decode_uint::())? { 0 => false, 1 => true, _ => return Err(Error::FrameEncodingError), diff --git a/neqo-transport/src/packet/mod.rs b/neqo-transport/src/packet/mod.rs index 8f7ace63f1..7512a181e6 100644 --- a/neqo-transport/src/packet/mod.rs +++ b/neqo-transport/src/packet/mod.rs @@ -610,7 +610,7 @@ impl<'a> PublicPacket<'a> { #[allow(clippy::similar_names)] pub fn decode(data: &'a [u8], dcid_decoder: &dyn ConnectionIdDecoder) -> Res<(Self, &'a [u8])> { let mut decoder = Decoder::new(data); - let first = Self::opt(decoder.decode_byte())?; + let first = Self::opt(decoder.decode_uint::())?; if first & 0x80 == PACKET_BIT_SHORT { // Conveniently, this also guarantees that there is enough space @@ -638,7 +638,7 @@ impl<'a> PublicPacket<'a> { } // Generic long header. - let version = WireVersion::try_from(Self::opt(decoder.decode_uint(4))?)?; + let version = Self::opt(decoder.decode_uint())?; let dcid = ConnectionIdRef::from(Self::opt(decoder.decode_vec(1))?); let scid = ConnectionIdRef::from(Self::opt(decoder.decode_vec(1))?); @@ -893,7 +893,7 @@ impl<'a> PublicPacket<'a> { let mut decoder = Decoder::new(&self.data[self.header_len..]); let mut res = Vec::new(); while decoder.remaining() > 0 { - let version = WireVersion::try_from(Self::opt(decoder.decode_uint(4))?)?; + let version = Self::opt(decoder.decode_uint::())?; res.push(version); } Ok(res) diff --git a/neqo-transport/src/shuffle.rs b/neqo-transport/src/shuffle.rs index 0c2f0ae04e..fd81190c27 100644 --- a/neqo-transport/src/shuffle.rs +++ b/neqo-transport/src/shuffle.rs @@ -21,29 +21,33 @@ pub fn find_sni(buf: &[u8]) -> Option> { } #[must_use] - fn skip_vec(dec: &mut Decoder) -> Option<()> { - let len = dec.decode_uint(N)?.try_into().ok()?; - skip(dec, len) + fn skip_vec(dec: &mut Decoder) -> Option<()> + where + T: TryFrom, + usize: TryFrom, + { + let len = dec.decode_uint::()?; + skip(dec, usize::try_from(len).ok()?) } let mut dec = Decoder::from(buf); // Return if buf is empty or does not contain a ClientHello (first byte == 1) - if buf.is_empty() || dec.decode_byte()? != 1 { + if buf.is_empty() || dec.decode_uint::()? != 1 { return None; } skip(&mut dec, 3 + 2 + 32)?; // Skip length, version, random - skip_vec::<1>(&mut dec)?; // Skip session_id - skip_vec::<2>(&mut dec)?; // Skip cipher_suites - skip_vec::<1>(&mut dec)?; // Skip compression_methods + skip_vec::(&mut dec)?; // Skip session_id + skip_vec::(&mut dec)?; // Skip cipher_suites + skip_vec::(&mut dec)?; // Skip compression_methods skip(&mut dec, 2)?; while dec.remaining() >= 4 { - let ext_type: u16 = dec.decode_uint(2)?.try_into().ok()?; - let ext_len: u16 = dec.decode_uint(2)?.try_into().ok()?; + let ext_type: u16 = dec.decode_uint()?; + let ext_len: u16 = dec.decode_uint()?; if ext_type == 0 { // SNI! - let sni_len: u16 = dec.decode_uint(2)?.try_into().ok()?; + let sni_len: u16 = dec.decode_uint()?; skip(&mut dec, 3)?; // Skip name_type and host_name length let start = dec.offset(); let end = start + usize::from(sni_len) - 3; diff --git a/neqo-transport/src/tparams.rs b/neqo-transport/src/tparams.rs index 7c8139f7b6..baf11ff7a4 100644 --- a/neqo-transport/src/tparams.rs +++ b/neqo-transport/src/tparams.rs @@ -185,7 +185,7 @@ impl TransportParameter { fn decode_preferred_address(d: &mut Decoder) -> Res { // IPv4 address (maybe) let v4ip = Ipv4Addr::from(<[u8; 4]>::try_from(d.decode(4).ok_or(Error::NoMoreData)?)?); - let v4port = u16::try_from(d.decode_uint(2).ok_or(Error::NoMoreData)?)?; + let v4port = d.decode_uint::().ok_or(Error::NoMoreData)?; // Can't have non-zero IP and zero port, or vice versa. if v4ip.is_unspecified() ^ (v4port == 0) { return Err(Error::TransportParameterError); @@ -200,7 +200,7 @@ impl TransportParameter { let v6ip = Ipv6Addr::from(<[u8; 16]>::try_from( d.decode(16).ok_or(Error::NoMoreData)?, )?); - let v6port = u16::try_from(d.decode_uint(2).ok_or(Error::NoMoreData)?)?; + let v6port = d.decode_uint().ok_or(Error::NoMoreData)?; if v6ip.is_unspecified() ^ (v6port == 0) { return Err(Error::TransportParameterError); } @@ -229,11 +229,11 @@ impl TransportParameter { fn decode_versions(dec: &mut Decoder) -> Res { fn dv(dec: &mut Decoder) -> Res { - let v = dec.decode_uint(4).ok_or(Error::NoMoreData)?; + let v = dec.decode_uint::().ok_or(Error::NoMoreData)?; if v == 0 { Err(Error::TransportParameterError) } else { - Ok(WireVersion::try_from(v)?) + Ok(v) } } @@ -457,8 +457,7 @@ impl TransportParameters { let rbuf = random::<4>(); let mut other = Vec::with_capacity(versions.all().len() + 1); let mut dec = Decoder::new(&rbuf); - let grease = - (u32::try_from(dec.decode_uint(4).unwrap()).unwrap()) & 0xf0f0_f0f0 | 0x0a0a_0a0a; + let grease = dec.decode_uint::().unwrap() & 0xf0f0_f0f0 | 0x0a0a_0a0a; other.push(grease); for &v in versions.all() { if role == Role::Client && !versions.initial().is_compatible(v) { diff --git a/neqo-transport/src/tracking.rs b/neqo-transport/src/tracking.rs index 565b9bfd4d..20baea811d 100644 --- a/neqo-transport/src/tracking.rs +++ b/neqo-transport/src/tracking.rs @@ -1062,7 +1062,7 @@ mod tests { assert_eq!(stats.ack, 1); let mut dec = builder.as_decoder(); - _ = dec.decode_byte().unwrap(); // Skip the short header. + dec.skip(1); // Skip the short header. let frame = Frame::decode(&mut dec).unwrap(); if let Frame::Ack { ack_ranges, .. } = frame { assert_eq!(ack_ranges.len(), 0); diff --git a/neqo-transport/tests/server.rs b/neqo-transport/tests/server.rs index 12078a8960..8ff6511e0b 100644 --- a/neqo-transport/tests/server.rs +++ b/neqo-transport/tests/server.rs @@ -15,6 +15,7 @@ use neqo_crypto::{ }; use neqo_transport::{ server::{ConnectionRef, Server, ValidateAddress}, + version::WireVersion, CloseReason, Connection, ConnectionParameters, Error, Output, State, StreamType, Version, MIN_INITIAL_PACKET_SIZE, }; @@ -584,13 +585,13 @@ fn version_negotiation_ignored() { let vn = vn.expect("a vn packet"); let mut dec = Decoder::from(&vn[1..]); // Skip first byte. - assert_eq!(dec.decode_uint(4).expect("VN"), 0); + assert_eq!(dec.decode_uint::().expect("VN"), 0); assert_eq!(dec.decode_vec(1).expect("VN DCID"), &s_cid[..]); assert_eq!(dec.decode_vec(1).expect("VN SCID"), &d_cid[..]); let mut found = false; while dec.remaining() > 0 { - let v = dec.decode_uint(4).expect("supported version"); - found |= v == u64::from(Version::default().wire_version()); + let v = dec.decode_uint::().expect("supported version"); + found |= v == Version::default().wire_version(); } assert!(found, "valid version not found"); diff --git a/test-fixture/src/assertions.rs b/test-fixture/src/assertions.rs index 52d0194cbb..4cb3732258 100644 --- a/test-fixture/src/assertions.rs +++ b/test-fixture/src/assertions.rs @@ -14,8 +14,7 @@ use crate::{DEFAULT_ADDR, DEFAULT_ADDR_V4}; const PACKET_TYPE_MASK: u8 = 0b1011_0000; fn assert_default_version(dec: &mut Decoder) -> Version { - let version = - Version::try_from(WireVersion::try_from(dec.decode_uint(4).unwrap()).unwrap()).unwrap(); + let version = Version::try_from(dec.decode_uint::().unwrap()).unwrap(); assert!(version == Version::Version1 || version == Version::Version2); version } @@ -38,8 +37,12 @@ fn assert_long_packet_type(b: u8, v1_expected: u8, version: Version) { /// If this is not a long header packet with the given version. pub fn assert_version(payload: &[u8], v: u32) { let mut dec = Decoder::from(payload); - assert_eq!(dec.decode_byte().unwrap() & 0x80, 0x80, "is long header"); - assert_eq!(dec.decode_uint(4).unwrap(), u64::from(v)); + assert_eq!( + dec.decode_uint::().unwrap() & 0x80, + 0x80, + "is long header" + ); + assert_eq!(dec.decode_uint::().unwrap(), v); } /// Simple checks for a Version Negotiation packet. @@ -49,8 +52,12 @@ pub fn assert_version(payload: &[u8], v: u32) { /// If this is clearly not a Version Negotiation packet. pub fn assert_vn(payload: &[u8]) { let mut dec = Decoder::from(payload); - assert_eq!(dec.decode_byte().unwrap() & 0x80, 0x80, "is long header"); - assert_eq!(dec.decode_uint(4).unwrap(), 0); + assert_eq!( + dec.decode_uint::().unwrap() & 0x80, + 0x80, + "is long header" + ); + assert_eq!(dec.decode_uint::().unwrap(), 0); dec.skip_vec(1); // DCID dec.skip_vec(1); // SCID assert_eq!(dec.remaining() % 4, 0); @@ -64,7 +71,7 @@ pub fn assert_vn(payload: &[u8]) { pub fn assert_coalesced_0rtt(payload: &[u8]) { assert!(payload.len() >= MIN_INITIAL_PACKET_SIZE); let mut dec = Decoder::from(payload); - let initial_type = dec.decode_byte().unwrap(); // Initial + let initial_type = dec.decode_uint::().unwrap(); // Initial let version = assert_default_version(&mut dec); assert_long_packet_type(initial_type, 0b1000_0000, version); dec.skip_vec(1); // DCID @@ -72,7 +79,7 @@ pub fn assert_coalesced_0rtt(payload: &[u8]) { dec.skip_vvec(); let initial_len = dec.decode_varint().unwrap(); dec.skip(initial_len.try_into().unwrap()); - let zrtt_type = dec.decode_byte().unwrap(); + let zrtt_type = dec.decode_uint::().unwrap(); assert_long_packet_type(zrtt_type, 0b1001_0000, version); } @@ -81,7 +88,7 @@ pub fn assert_coalesced_0rtt(payload: &[u8]) { /// If the tests fail. pub fn assert_retry(payload: &[u8]) { let mut dec = Decoder::from(payload); - let t = dec.decode_byte().unwrap(); + let t = dec.decode_uint::().unwrap(); let version = assert_default_version(&mut dec); assert_long_packet_type(t, 0b1011_0000, version); } @@ -93,7 +100,7 @@ pub fn assert_retry(payload: &[u8]) { /// If the tests fail. pub fn assert_initial(payload: &[u8], expect_token: bool) { let mut dec = Decoder::from(payload); - let t = dec.decode_byte().unwrap(); + let t = dec.decode_uint::().unwrap(); let version = assert_default_version(&mut dec); assert_long_packet_type(t, 0b1000_0000, version); dec.skip_vec(1); // Destination Connection ID. @@ -109,7 +116,7 @@ pub fn assert_initial(payload: &[u8], expect_token: bool) { /// If the tests fail. pub fn assert_handshake(payload: &[u8]) { let mut dec = Decoder::from(payload); - let t = dec.decode_byte().unwrap(); + let t = dec.decode_uint::().unwrap(); let version = assert_default_version(&mut dec); assert_long_packet_type(t, 0b1010_0000, version); } @@ -119,7 +126,7 @@ pub fn assert_handshake(payload: &[u8]) { /// If the tests fail. pub fn assert_no_1rtt(payload: &[u8]) { let mut dec = Decoder::from(payload); - while let Some(b1) = dec.decode_byte() { + while let Some(b1) = dec.decode_uint::() { // If this is just padding, that's OK. Check. if payload.iter().skip(dec.offset()).all(|b| *b == 0) { return; diff --git a/test-fixture/src/header_protection.rs b/test-fixture/src/header_protection.rs index f4e0338421..3b7b8b0dc2 100644 --- a/test-fixture/src/header_protection.rs +++ b/test-fixture/src/header_protection.rs @@ -120,11 +120,10 @@ pub fn remove_header_protection(hp: &HpKey, header: &[u8], payload: &[u8]) -> (V // Trim down to size. fixed_header.truncate(pn_offset + pn_len); // The packet number should be 1. - let pn = Decoder::new(&fixed_header[pn_offset..]) - .decode_uint(pn_len) - .unwrap(); - - (fixed_header, pn) + // This doesn't use a `Decoder` because the public API can't handle a three byte packet number. + let mut pn = [0; 8]; + pn[8 - pn_len..].copy_from_slice(&fixed_header[pn_offset..pn_offset + pn_len]); + (fixed_header, u64::from_be_bytes(pn)) } #[allow(clippy::missing_panics_doc)] diff --git a/test-fixture/src/lib.rs b/test-fixture/src/lib.rs index 97b68e50c6..f2a86f6793 100644 --- a/test-fixture/src/lib.rs +++ b/test-fixture/src/lib.rs @@ -329,8 +329,8 @@ fn split_packet(buf: &[u8]) -> (&[u8], Option<&[u8]>) { return (buf, None); } let mut dec = Decoder::from(buf); - let first = dec.decode_byte().unwrap(); - let v = Version::try_from(WireVersion::try_from(dec.decode_uint(4).unwrap()).unwrap()).unwrap(); // Version. + let first: u8 = dec.decode_uint().unwrap(); + let v = Version::try_from(dec.decode_uint::().unwrap()).unwrap(); // Version. let (initial_type, retry_type) = if v == Version::Version2 { (0b1001_0000, 0b1000_0000) } else { diff --git a/test-fixture/src/sim/rng.rs b/test-fixture/src/sim/rng.rs index dd94798252..4bbb6b5aca 100644 --- a/test-fixture/src/sim/rng.rs +++ b/test-fixture/src/sim/rng.rs @@ -21,10 +21,10 @@ impl Random { let mut dec = Decoder::from(&seed); Self { state: [ - dec.decode_uint(8).unwrap(), - dec.decode_uint(8).unwrap(), - dec.decode_uint(8).unwrap(), - dec.decode_uint(8).unwrap(), + dec.decode_uint().unwrap(), + dec.decode_uint().unwrap(), + dec.decode_uint().unwrap(), + dec.decode_uint().unwrap(), ], } }