diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 995fbe53..294e3da6 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -49,6 +49,7 @@ hashbrown = { version = "0.8", optional = true } [dev-dependencies] serde_json = "1.0.0" serde_test = "1.0.19" +serde_derive = "1.0.103" secp256k1 = { version = "0.24.0", features = [ "recovery", "rand-std" ] } bincode = "1.3.1" diff --git a/bitcoin/build.rs b/bitcoin/build.rs new file mode 100644 index 00000000..0879f651 --- /dev/null +++ b/bitcoin/build.rs @@ -0,0 +1,31 @@ +fn main() { + let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let output = std::process::Command::new(rustc) + .arg("--version") + .output() + .expect("Failed to run rustc --version"); + assert!(output.status.success(), "Failed to get rust version"); + let stdout = String::from_utf8(output.stdout).expect("rustc produced non-UTF-8 output"); + let version_prefix = "rustc "; + if !stdout.starts_with(version_prefix) { + panic!("unexpected rustc output: {}", stdout); + } + + let version = &stdout[version_prefix.len()..]; + let end = version.find(&[' ', '-'] as &[_]).unwrap_or(version.len()); + let version = &version[..end]; + let mut version_components = version.split('.'); + let major = version_components.next().unwrap(); + assert_eq!(major, "1", "Unexpected Rust version"); + let minor = version_components + .next() + .unwrap_or("0") + .parse::() + .expect("invalid Rust minor version"); + + for activate_version in &[46] { + if minor >= *activate_version { + println!("cargo:rustc-cfg=rust_v_1_{}", activate_version); + } + } +} diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index a43d6b12..df3e2493 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -1192,6 +1192,24 @@ mod tests { assert_eq!(tx_without_witness.strippedsize(), expected_strippedsize); } + // We temporarily abuse `Transaction` for testing consensus serde adapter. + #[cfg(feature = "serde")] + #[test] + fn test_consensus_serde() { + use crate::consensus::serde as con_serde; + let json = "\"010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3603da1b0e00045503bd5704c7dd8a0d0ced13bb5785010800000000000a636b706f6f6c122f4e696e6a61506f6f6c2f5345475749542fffffffff02b4e5a212000000001976a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac0000000000000000266a24aa21a9edf91c46b49eb8a29089980f02ee6b57e7d63d33b18b4fddac2bcd7db2a39837040120000000000000000000000000000000000000000000000000000000000000000000000000\""; + let mut deserializer = serde_json::Deserializer::from_str(json); + let tx = con_serde::With::::deserialize::<'_, Transaction, _>(&mut deserializer) + .unwrap(); + let tx_bytes = Vec::from_hex(&json[1..(json.len() - 1)]).unwrap(); + let expected = deserialize::(&tx_bytes).unwrap(); + assert_eq!(tx, expected); + let mut bytes = Vec::new(); + let mut serializer = serde_json::Serializer::new(&mut bytes); + con_serde::With::::serialize(&tx, &mut serializer).unwrap(); + assert_eq!(bytes, json.as_bytes()) + } + #[test] fn test_transaction_version() { let tx_bytes = Vec::from_hex("ffffff7f0100000000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap(); diff --git a/bitcoin/src/consensus/mod.rs b/bitcoin/src/consensus/mod.rs index 40514e9f..9ccae25f 100644 --- a/bitcoin/src/consensus/mod.rs +++ b/bitcoin/src/consensus/mod.rs @@ -12,3 +12,7 @@ pub mod params; pub use self::encode::{Encodable, Decodable, WriteExt, ReadExt}; pub use self::encode::{serialize, deserialize, deserialize_partial}; pub use self::params::Params; + +#[cfg(feature = "serde")] +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +pub mod serde; diff --git a/bitcoin/src/consensus/serde.rs b/bitcoin/src/consensus/serde.rs new file mode 100644 index 00000000..6de7ddfa --- /dev/null +++ b/bitcoin/src/consensus/serde.rs @@ -0,0 +1,542 @@ +//! Serde serialization via consensus encoding +//! +//! This provides functions for (de)serializing any type as consensus-encoded bytes. +//! For human-readable formats it serializes as a string with a consumer-supplied encoding, for +//! binary formats it serializes as a sequence of bytes (not `serialize_bytes` to avoid allocations). +//! +//! The string encoding has to be specified using a marker type implementing the encoding strategy. +//! This crate provides hex encoding via `Hex` and `Hex` + +use core::fmt; +use core::marker::PhantomData; +use crate::io; +use serde::{Serializer, Deserializer}; +use serde::de::{Visitor, SeqAccess, Unexpected}; +use serde::ser::SerializeSeq; +use super::{Encodable, Decodable}; +use super::encode::Error as ConsensusError; + +/// Hex-encoding strategy +pub struct Hex(PhantomData) where Case: hex::Case; + +impl Default for Hex { + fn default() -> Self { + Hex(Default::default()) + } +} + +impl ByteEncoder for Hex { + type Encoder = hex::Encoder; +} + +/// Implements hex encoding. +pub mod hex { + use core::fmt; + use core::marker::PhantomData; + use bitcoin_internals as internals; + use internals::hex::BufEncoder; + + /// Marker for upper/lower case type-level flags ("type-level enum"). + /// + /// You may use this trait in bounds only. + pub trait Case: sealed::Case {} + impl Case for T {} + + /// Marker for using lower-case hex encoding. + pub enum Lower {} + /// Marker for using upper-case hex encoding. + pub enum Upper {} + + mod sealed { + use bitcoin_internals as internals; + + pub trait Case { + /// Internal detail, don't depend on it!!! + const INTERNAL_CASE: internals::hex::Case; + } + + impl Case for super::Lower { + const INTERNAL_CASE: internals::hex::Case = internals::hex::Case::Lower; + } + + impl Case for super::Upper { + const INTERNAL_CASE: internals::hex::Case = internals::hex::Case::Upper; + } + } + + // TODO measure various sizes and determine the best value + const HEX_BUF_SIZE: usize = 512; + + /// Hex byte encoder. + // We wrap `BufEncoder` to not leak internal representation. + pub struct Encoder(BufEncoder<[u8; HEX_BUF_SIZE]>, PhantomData); + + impl From> for Encoder { + fn from(_: super::Hex) -> Self { + Encoder(BufEncoder::new([0; HEX_BUF_SIZE]), Default::default()) + } + } + + impl super::EncodeBytes for Encoder { + fn encode_chunk(&mut self, writer: &mut W, mut bytes: &[u8]) -> fmt::Result { + while !bytes.is_empty() { + if self.0.is_full() { + self.flush(writer)?; + } + bytes = self.0.put_bytes_min(bytes, C::INTERNAL_CASE); + } + Ok(()) + } + + fn flush(&mut self, writer: &mut W) -> fmt::Result { + writer.write_str(self.0.as_str())?; + self.0.clear(); + Ok(()) + } + } + + // Newtypes to hide internal details. + // TODO: statically prove impossible cases + + /// Error returned when a hex string decoder can't be created. + #[derive(Debug)] + pub struct DecodeInitError(bitcoin_hashes::hex::Error); + + /// Error returned when a hex string contains invalid characters. + #[derive(Debug)] + pub struct DecodeError(bitcoin_hashes::hex::Error); + + /// Hex decoder state. + pub struct Decoder<'a>(bitcoin_hashes::hex::HexIterator<'a>); + + impl<'a> Decoder<'a> { + fn new(s: &'a str) -> Result { + match bitcoin_hashes::hex::HexIterator::new(s) { + Ok(iter) => Ok(Decoder(iter)), + Err(error) => Err(DecodeInitError(error)), + } + } + } + + impl<'a> Iterator for Decoder<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + self.0.next().map(|result| result.map_err(DecodeError)) + } + } + + impl<'a, C: Case> super::ByteDecoder<'a> for super::Hex { + type InitError = DecodeInitError; + type DecodeError = DecodeError; + type Decoder = Decoder<'a>; + + fn from_str(s: &'a str) -> Result { + Decoder::new(s) + } + } + + impl super::IntoDeError for DecodeInitError { + fn into_de_error(self) -> E { + use bitcoin_hashes::hex::Error; + + match self.0 { + Error::OddLengthString(len) => E::invalid_length(len, &"an even number of ASCII-encoded hex digits"), + error => panic!("unexpected error: {:?}", error), + } + } + } + + impl super::IntoDeError for DecodeError { + fn into_de_error(self) -> E { + use bitcoin_hashes::hex::Error; + use serde::de::Unexpected; + + const EXPECTED_CHAR: &str = "an ASCII-encoded hex digit"; + + match self.0 { + Error::InvalidChar(c) if c.is_ascii() => E::invalid_value(Unexpected::Char(c as _), &EXPECTED_CHAR), + Error::InvalidChar(c) => E::invalid_value(Unexpected::Unsigned(c.into()), &EXPECTED_CHAR), + error => panic!("unexpected error: {:?}", error), + } + } + } +} + +struct DisplayWrapper<'a, T: 'a + Encodable, E>(&'a T, PhantomData); + +impl<'a, T: 'a + Encodable, E: ByteEncoder> fmt::Display for DisplayWrapper<'a, T, E> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut writer = IoWrapper::<'_, _, E::Encoder>::new(f, E::default().into()); + self.0.consensus_encode(&mut writer).map_err(|error| { + #[cfg(debug_assertions)] + { + use crate::StdError; + + if error.kind() != io::ErrorKind::Other || error.source().is_some() || !writer.writer.was_error { + panic!("{} returned an unexpected error: {:?}", core::any::type_name::(), error); + } + } + fmt::Error + })?; + let result = writer.actually_flush(); + if result.is_err() { + writer.writer.assert_was_error::(); + } + result + } +} + +struct ErrorTrackingWriter { + writer: W, + #[cfg(debug_assertions)] + was_error: bool, +} + +impl ErrorTrackingWriter { + fn new(writer: W) -> Self { + ErrorTrackingWriter { + writer, + #[cfg(debug_assertions)] + was_error: false, + } + } + + #[cfg_attr(rust_v_1_46, track_caller)] + fn assert_no_error(&self, fun: &str) { + #[cfg(debug_assertions)] + { + if self.was_error { + panic!("`{}` called on errored writer", fun); + } + } + } + + fn assert_was_error(&self) { + #[cfg(debug_assertions)] + { + if !self.was_error { + panic!("{} returned an error unexpectedly", core::any::type_name::()); + } + } + } + + fn set_error(&mut self, was: bool) { + #[cfg(debug_assertions)] + { + self.was_error |= was; + } + } + + fn check_err(&mut self, result: Result) -> Result { + self.set_error(result.is_err()); + result + } +} + +impl fmt::Write for ErrorTrackingWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.assert_no_error("write_str"); + let result = self.writer.write_str(s); + self.check_err(result) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + self.assert_no_error("write_char"); + let result = self.writer.write_char(c); + self.check_err(result) + } +} + +struct IoWrapper<'a, W: fmt::Write, E: EncodeBytes> { + writer: ErrorTrackingWriter<&'a mut W>, + encoder: E, +} + +impl<'a, W: fmt::Write, E: EncodeBytes> IoWrapper<'a, W, E> { + fn new(writer: &'a mut W, encoder: E) -> Self { + IoWrapper { + writer: ErrorTrackingWriter::new(writer), + encoder, + } + } + + fn actually_flush(&mut self) -> fmt::Result { + self.encoder.flush(&mut self.writer) + } +} + +impl<'a, W: fmt::Write, E: EncodeBytes> io::Write for IoWrapper<'a, W, E> { + fn write(&mut self, bytes: &[u8]) -> io::Result { + match self.encoder.encode_chunk(&mut self.writer, bytes) { + Ok(()) => Ok(bytes.len()), + Err(fmt::Error) => { + self.writer.assert_was_error::(); + Err(io::Error::from(io::ErrorKind::Other)) + } + } + } + // we intentionally ignore flushes because we will do a single flush at the end. + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} + +/// Provides an instance of byte-to-string encoder. +/// +/// This is basically a type constructor used in places where value arguments are not accepted. +/// Such as the generic `serialize`. +pub trait ByteEncoder: Default { + /// The encoder state. + type Encoder: EncodeBytes + From; +} + +/// Transforms given bytes and writes to the writer. +/// +/// The encoder is allowed to be buffered (and probably should be). +/// The design passing writer each time bypasses the need for GAT. +pub trait EncodeBytes { + /// Transform the provided slice and write to the writer. + /// + /// This is similar to the `write_all` method on `io::Write`. + fn encode_chunk(&mut self, writer: &mut W, bytes: &[u8]) -> fmt::Result; + + /// Write data in buffer (if any) to the writer. + fn flush(&mut self, writer: &mut W) -> fmt::Result; +} + +/// Provides an instance of string-to-byte decoder. +/// +/// This is basically a type constructor used in places where value arguments are not accepted. +/// Such as the generic `serialize`. +pub trait ByteDecoder<'a> { + /// Error returned when decoder can't be created. + /// + /// This is typically returned when string length is invalid. + type InitError: IntoDeError + fmt::Debug; + + /// Error returned when decoding fails. + /// + /// This is typically returned when the input string contains malformed chars. + type DecodeError: IntoDeError + fmt::Debug; + + /// The decoder state. + type Decoder: Iterator>; + + /// Constructs the decoder from string. + fn from_str(s: &'a str) -> Result; +} + +/// Converts error into a type implementing `serde::de::Error` +pub trait IntoDeError { + /// Performs the conversion. + fn into_de_error(self) -> E; +} + +struct BinWriter{ + serializer: S, + error: Option, +} + +impl io::Write for BinWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.write_all(buf).map(|_| buf.len()) + } + + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + for byte in buf { + if let Err(error) = self.serializer.serialize_element(byte) { + self.error = Some(error); + return Err(io::ErrorKind::Other.into()); + } + } + Ok(()) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} + +struct DisplayExpected(D); + +impl serde::de::Expected for DisplayExpected { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, formatter) + } +} + +enum DecodeError { + TooManyBytes, + Consensus(ConsensusError), + Other(E), +} + +// not a trait impl because we panic on some variants +fn consensus_error_into_serde(error: ConsensusError) -> E { + match error { + ConsensusError::Io(error) => panic!("unexpected IO error {:?}", error), + ConsensusError::Psbt(_) => panic!("PSBT shouldn't implement consensus encoding"), + ConsensusError::OversizedVectorAllocation { requested, max } => E::custom(format_args!("the requested allocation of {} items exceeds maximum of {}", requested, max)), + ConsensusError::InvalidChecksum { expected, actual } => E::invalid_value(Unexpected::Bytes(&actual), &DisplayExpected(format_args!("checksum {:02x}{:02x}{:02x}{:02x}", expected[0], expected[1], expected[2], expected[3]))), + ConsensusError::NonMinimalVarInt => E::custom(format_args!("compact size was not encoded minimally")), + ConsensusError::ParseFailed(msg) => E::custom(msg), + ConsensusError::UnsupportedSegwitFlag(flag) => E::invalid_value(Unexpected::Unsigned(flag.into()), &"segwit version 1 flag"), + } +} + +impl DecodeError where E: serde::de::Error { + fn unify(self) -> E { + match self { + DecodeError::Other(error) => error, + DecodeError::TooManyBytes => E::custom(format_args!("got more bytes than expected")), + DecodeError::Consensus(error) => consensus_error_into_serde(error), + } + } +} + +impl IntoDeError for DecodeError where E: IntoDeError { + fn into_de_error(self) -> DE { + match self { + DecodeError::Other(error) => error.into_de_error(), + DecodeError::TooManyBytes => DE::custom(format_args!("got more bytes than expected")), + DecodeError::Consensus(error) => consensus_error_into_serde(error), + } + } +} + +struct IterReader>> { + iterator: core::iter::Fuse, + error: Option, +} + +impl>> IterReader { + fn new(iterator: I) -> Self { + IterReader { + iterator: iterator.fuse(), + error: None, + } + } + + fn decode(mut self) -> Result> { + use crate::StdError; + + let result = T::consensus_decode(&mut self); + match (result, self.error) { + (Ok(_), None) if self.iterator.next().is_some() => { + Err(DecodeError::TooManyBytes) + }, + (Ok(value), None) => Ok(value), + (Ok(_), Some(error)) => panic!("{} silently ate the error: {:?}", core::any::type_name::(), error), + (Err(ConsensusError::Io(io_error)), Some(de_error)) if io_error.kind() == io::ErrorKind::Other && io_error.source().is_none() => Err(DecodeError::Other(de_error)), + (Err(consensus_error), None) => Err(DecodeError::Consensus(consensus_error)), + (Err(ConsensusError::Io(io_error)), de_error) => panic!("Unexpected IO error {:?} returned from {}::consensus_decode(), deserialization error: {:?}", io_error, core::any::type_name::(), de_error), + (Err(consensus_error), Some(de_error)) => panic!("{} should've returned `Other` IO error because of deserialization error {:?} but it returned consensus error {:?} instead", core::any::type_name::(), de_error, consensus_error), + } + } +} + +impl>> io::Read for IterReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let mut count = 0; + for (dst, src) in buf.iter_mut().zip(&mut self.iterator) { + match src { + Ok(byte) => *dst = byte, + Err(error) => { + self.error = Some(error); + return Err(io::ErrorKind::Other.into()); + } + } + // bounded by the length of buf + count += 1; + } + Ok(count) + } +} + +/// Helper for `#[serde(with = "")]`. +/// +/// To (de)serialize a field using consensus encoding you can write e.g.: +/// +/// ``` +/// use bitcoin::Transaction; +/// use bitcoin::consensus; +/// use serde_derive::{Serialize, Deserialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// # #[serde(crate = "actual_serde")] +/// pub struct MyStruct { +/// #[serde(with = "consensus::serde::With::")] +/// tx: Transaction, +/// } +/// ``` +pub struct With(PhantomData); + +impl With { + /// Serializes the value as consensus-encoded + pub fn serialize(value: &T, serializer: S) -> Result where E: ByteEncoder { + if serializer.is_human_readable() { + serializer.collect_str(&DisplayWrapper::<'_, _, E>(value, Default::default())) + } else { + use crate::StdError; + + let serializer = serializer.serialize_seq(None)?; + let mut writer = BinWriter { + serializer, + error: None, + }; + + let result = value.consensus_encode(&mut writer); + match (result, writer.error) { + (Ok(_), None) => writer.serializer.end(), + (Ok(_), Some(error)) => panic!("{} silently ate an IO error: {:?}", core::any::type_name::(), error), + (Err(io_error), Some(ser_error)) if io_error.kind() == io::ErrorKind::Other && io_error.source().is_none() => Err(ser_error), + (Err(io_error), ser_error) => panic!("{} returned an unexpected IO error: {:?} serialization error: {:?}", core::any::type_name::(), io_error, ser_error), + } + } + } + + /// Deserializes the value as consensus-encoded + pub fn deserialize<'d, T: Decodable, D: Deserializer<'d>>(deserializer: D) -> Result where for<'a> E: ByteDecoder<'a> { + if deserializer.is_human_readable() { + deserializer.deserialize_str(HRVisitor::<_, E>(Default::default())) + } else { + deserializer.deserialize_seq(BinVisitor(Default::default())) + } + } +} + +struct HRVisitor ByteDecoder<'a>>(PhantomData (T, D)>); + +impl<'de, T: Decodable, D: for<'a> ByteDecoder<'a>> Visitor<'de> for HRVisitor { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("bytes encoded as a hex string") + } + + fn visit_str(self, s: &str) -> Result { + let decoder = D::from_str(s).map_err(IntoDeError::into_de_error)?; + IterReader::new(decoder).decode().map_err(IntoDeError::into_de_error) + } +} + +struct BinVisitor(PhantomData T>); + +impl<'de, T: Decodable> Visitor<'de> for BinVisitor { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a sequence of bytes") + } + + fn visit_seq>(self, s: S) -> Result { + IterReader::new(SeqIterator(s, Default::default())).decode().map_err(DecodeError::unify) + } +} + +struct SeqIterator<'a, S: serde::de::SeqAccess<'a>>(S, PhantomData<&'a ()>); + +impl<'a, S: serde::de::SeqAccess<'a>> Iterator for SeqIterator<'a, S> { + type Item = Result; + + fn next(&mut self) -> Option { + self.0.next_element::().transpose() + } +} diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index f6337af5..c7910da3 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -116,6 +116,15 @@ use std::io; #[cfg(not(feature = "std"))] use core2::io; +// May depend on crate features and we don't want to bother with it +#[allow(unused)] +#[cfg(feature = "std")] +use std::error::Error as StdError; + +#[allow(unused)] +#[cfg(not(feature = "std"))] +use core2::error::Error as StdError; + pub use crate::address::{Address, AddressType}; pub use crate::amount::{Amount, Denomination, SignedAmount}; pub use crate::blockdata::block::{self, Block}; diff --git a/internals/src/hex/buf_encoder.rs b/internals/src/hex/buf_encoder.rs index 48174bec..7a09d2e3 100644 --- a/internals/src/hex/buf_encoder.rs +++ b/internals/src/hex/buf_encoder.rs @@ -177,15 +177,25 @@ impl BufEncoder { #[inline] #[cfg_attr(rust_v_1_46, track_caller)] pub fn put_bytes(&mut self, bytes: &[u8], case: Case) { - // Panic if the result wouldn't fit address space to not waste time and give the optimizer - // more opportunities. - let double_len = bytes.len().checked_mul(2).expect("overflow"); - assert!(double_len <= self.buf.as_out_bytes().len() - self.pos); + assert!(bytes.len() <= self.space_remaining()); for byte in bytes { self.put_byte(*byte, case); } } + /// Encodes as many `bytes` as fit into the buffer as hex and return the remainder. + /// + /// This method works just like `put_bytes` but instead of panicking it returns the unwritten + /// bytes. The method returns an empty slice if all bytes were written + #[must_use = "this may write only part of the input buffer"] + #[inline] + #[cfg_attr(rust_v_1_46, track_caller)] + pub fn put_bytes_min<'a>(&mut self, bytes: &'a [u8], case: Case) -> &'a [u8] { + let to_write = self.space_remaining().min(bytes.len()); + self.put_bytes(&bytes[..to_write], case); + &bytes[to_write..] + } + /// Returns true if no more bytes can be written into the buffer. #[inline] pub fn is_full(&self) -> bool { self.pos == self.buf.as_out_bytes().len() } @@ -200,6 +210,14 @@ impl BufEncoder { /// Resets the buffer to become empty. #[inline] pub fn clear(&mut self) { self.pos = 0; } + + /// How many bytes can be written to this buffer. + /// + /// Note that this returns the number of bytes before encoding, not number of hex digits. + #[inline] + pub fn space_remaining(&self) -> usize { + (self.buf.as_out_bytes().len() - self.pos) / 2 + } } #[cfg(test)] @@ -218,13 +236,17 @@ mod tests { fn single_byte_exact_buf() { let mut buf = [0u8; 2]; let mut encoder = BufEncoder::new(&mut buf); + assert_eq!(encoder.space_remaining(), 1); encoder.put_byte(42, Case::Lower); assert_eq!(encoder.as_str(), "2a"); + assert_eq!(encoder.space_remaining(), 0); assert!(encoder.is_full()); encoder.clear(); + assert_eq!(encoder.space_remaining(), 1); assert!(!encoder.is_full()); encoder.put_byte(42, Case::Upper); assert_eq!(encoder.as_str(), "2A"); + assert_eq!(encoder.space_remaining(), 0); assert!(encoder.is_full()); } @@ -232,12 +254,16 @@ mod tests { fn single_byte_oversized_buf() { let mut buf = [0u8; 4]; let mut encoder = BufEncoder::new(&mut buf); + assert_eq!(encoder.space_remaining(), 2); encoder.put_byte(42, Case::Lower); + assert_eq!(encoder.space_remaining(), 1); assert_eq!(encoder.as_str(), "2a"); assert!(!encoder.is_full()); encoder.clear(); + assert_eq!(encoder.space_remaining(), 2); encoder.put_byte(42, Case::Upper); assert_eq!(encoder.as_str(), "2A"); + assert_eq!(encoder.space_remaining(), 1); assert!(!encoder.is_full()); } @@ -246,7 +272,9 @@ mod tests { let mut buf = [0u8; 4]; let mut encoder = BufEncoder::new(&mut buf); encoder.put_byte(42, Case::Lower); + assert_eq!(encoder.space_remaining(), 1); encoder.put_byte(255, Case::Lower); + assert_eq!(encoder.space_remaining(), 0); assert_eq!(encoder.as_str(), "2aff"); assert!(encoder.is_full()); encoder.clear(); @@ -257,6 +285,22 @@ mod tests { assert!(encoder.is_full()); } + #[test] + fn put_bytes_min() { + let mut buf = [0u8; 2]; + let mut encoder = BufEncoder::new(&mut buf); + let remainder = encoder.put_bytes_min(b"", Case::Lower); + assert_eq!(remainder, b""); + assert_eq!(encoder.as_str(), ""); + let remainder = encoder.put_bytes_min(b"*", Case::Lower); + assert_eq!(remainder, b""); + assert_eq!(encoder.as_str(), "2a"); + encoder.clear(); + let remainder = encoder.put_bytes_min(&[42, 255], Case::Lower); + assert_eq!(remainder, &[255]); + assert_eq!(encoder.as_str(), "2a"); + } + #[test] fn same_as_fmt() { use core::fmt::{self, Write};