use crate::base::charstr::CharStr;
#[cfg(feature = "serde")]
use crate::base::charstr::DeserializeCharStrSeed;
use crate::base::cmp::CanonicalOrd;
use crate::base::iana::Rtype;
use crate::base::rdata::{
ComposeRecordData, LongRecordData, ParseRecordData, RecordData,
};
use crate::base::scan::Scanner;
#[cfg(feature = "serde")]
use crate::base::scan::Symbol;
use crate::base::wire::{Composer, FormError, ParseError};
use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
#[cfg(feature = "bytes")]
use bytes::BytesMut;
use core::cmp::Ordering;
use core::convert::{Infallible, TryFrom};
use core::{fmt, hash, mem, str};
use octseq::builder::{
infallible, EmptyBuilder, FreezeBuilder, FromBuilder, OctetsBuilder,
ShortBuf,
};
use octseq::octets::{Octets, OctetsFrom, OctetsInto};
use octseq::parse::Parser;
#[cfg(feature = "serde")]
use octseq::serde::{DeserializeOctets, SerializeOctets};
#[derive(Clone)]
#[repr(transparent)]
pub struct Txt<Octs: ?Sized>(Octs);
impl Txt<()> {
pub(crate) const RTYPE: Rtype = Rtype::TXT;
}
impl<Octs: FromBuilder> Txt<Octs> {
pub fn build_from_slice(text: &[u8]) -> Result<Self, TxtAppendError>
where
<Octs as FromBuilder>::Builder:
EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
let mut builder = TxtBuilder::<Octs::Builder>::new();
builder.append_slice(text)?;
builder.finish()
}
}
impl<Octs> Txt<Octs> {
pub fn from_octets(octets: Octs) -> Result<Self, TxtError>
where
Octs: AsRef<[u8]>,
{
Txt::check_slice(octets.as_ref())?;
Ok(unsafe { Txt::from_octets_unchecked(octets) })
}
unsafe fn from_octets_unchecked(octets: Octs) -> Self {
Txt(octets)
}
}
impl Txt<[u8]> {
pub fn from_slice(slice: &[u8]) -> Result<&Self, TxtError> {
Txt::check_slice(slice)?;
Ok(unsafe { Txt::from_slice_unchecked(slice) })
}
unsafe fn from_slice_unchecked(slice: &[u8]) -> &Self {
mem::transmute(slice)
}
fn check_slice(mut slice: &[u8]) -> Result<(), TxtError> {
if slice.is_empty() {
return Err(TxtError(TxtErrorInner::Empty));
}
LongRecordData::check_len(slice.len())?;
while let Some(&len) = slice.first() {
let len = usize::from(len);
if slice.len() <= len {
return Err(TxtError(TxtErrorInner::ShortInput));
}
slice = &slice[len + 1..];
}
Ok(())
}
}
impl<Octs> Txt<Octs> {
pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
parser: &mut Parser<'a, Src>,
) -> Result<Self, ParseError>
where
Octs: AsRef<[u8]>,
{
let len = parser.remaining();
let text = parser.parse_octets(len)?;
let mut tmp = Parser::from_ref(text.as_ref());
while tmp.remaining() != 0 {
CharStr::skip(&mut tmp)?
}
Ok(Txt(text))
}
pub fn scan<S: Scanner<Octets = Octs>>(
scanner: &mut S,
) -> Result<Self, S::Error> {
scanner.scan_charstr_entry().map(Txt)
}
}
impl<Octs: AsRef<[u8]> + ?Sized> Txt<Octs> {
pub fn iter(&self) -> TxtIter<'_> {
TxtIter(self.iter_charstrs())
}
pub fn iter_charstrs(&self) -> TxtCharStrIter<'_> {
TxtCharStrIter(Parser::from_ref(self.0.as_ref()))
}
pub fn as_flat_slice(&self) -> Option<&[u8]> {
if usize::from(self.0.as_ref()[0]) == self.0.as_ref().len() - 1 {
Some(&self.0.as_ref()[1..])
} else {
None
}
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.0.as_ref().len()
}
pub fn try_text<T: FromBuilder>(
&self,
) -> Result<T, <<T as FromBuilder>::Builder as OctetsBuilder>::AppendError>
where
<T as FromBuilder>::Builder: EmptyBuilder,
{
let mut res = T::Builder::with_capacity(self.len());
for item in self.iter() {
res.append_slice(item)?;
}
Ok(res.freeze())
}
pub fn text<T: FromBuilder>(&self) -> T
where
<T as FromBuilder>::Builder: EmptyBuilder,
<<T as FromBuilder>::Builder as OctetsBuilder>::AppendError:
Into<Infallible>,
{
infallible(self.try_text())
}
}
impl<SrcOcts> Txt<SrcOcts> {
pub(in crate::rdata) fn convert_octets<Target: OctetsFrom<SrcOcts>>(
self,
) -> Result<Txt<Target>, Target::Error> {
Ok(Txt(self.0.try_octets_into()?))
}
pub(in crate::rdata) fn flatten<Octs: OctetsFrom<SrcOcts>>(
self,
) -> Result<Txt<Octs>, Octs::Error> {
self.convert_octets()
}
}
impl<Octs, SrcOcts> OctetsFrom<Txt<SrcOcts>> for Txt<Octs>
where
Octs: OctetsFrom<SrcOcts>,
{
type Error = Octs::Error;
fn try_octets_from(source: Txt<SrcOcts>) -> Result<Self, Self::Error> {
Octs::try_octets_from(source.0).map(Self)
}
}
impl<'a, Octs: AsRef<[u8]>> IntoIterator for &'a Txt<Octs> {
type Item = &'a [u8];
type IntoIter = TxtIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<Octs, Other> PartialEq<Txt<Other>> for Txt<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn eq(&self, other: &Txt<Other>) -> bool {
self.0.as_ref().eq(other.0.as_ref())
}
}
impl<Octs: AsRef<[u8]>> Eq for Txt<Octs> {}
impl<Octs, Other> PartialOrd<Txt<Other>> for Txt<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn partial_cmp(&self, other: &Txt<Other>) -> Option<Ordering> {
self.0.as_ref().partial_cmp(other.0.as_ref())
}
}
impl<Octs, Other> CanonicalOrd<Txt<Other>> for Txt<Octs>
where
Octs: AsRef<[u8]>,
Other: AsRef<[u8]>,
{
fn canonical_cmp(&self, other: &Txt<Other>) -> Ordering {
self.0.as_ref().cmp(other.0.as_ref())
}
}
impl<Octs: AsRef<[u8]>> Ord for Txt<Octs> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.as_ref().cmp(other.0.as_ref())
}
}
impl<Octs: AsRef<[u8]>> hash::Hash for Txt<Octs> {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.0.as_ref().hash(state)
}
}
impl<Octs> RecordData for Txt<Octs> {
fn rtype(&self) -> Rtype {
Txt::RTYPE
}
}
impl<'a, Octs> ParseRecordData<'a, Octs> for Txt<Octs::Range<'a>>
where
Octs: Octets + ?Sized,
{
fn parse_rdata(
rtype: Rtype,
parser: &mut Parser<'a, Octs>,
) -> Result<Option<Self>, ParseError> {
if rtype == Txt::RTYPE {
Self::parse(parser).map(Some)
} else {
Ok(None)
}
}
}
impl<Octs: AsRef<[u8]>> ComposeRecordData for Txt<Octs> {
fn rdlen(&self, _compress: bool) -> Option<u16> {
Some(u16::try_from(self.0.as_ref().len()).expect("long TXT rdata"))
}
fn compose_rdata<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
target.append_slice(self.0.as_ref())
}
fn compose_canonical_rdata<Target: Composer + ?Sized>(
&self,
target: &mut Target,
) -> Result<(), Target::AppendError> {
self.compose_rdata(target)
}
}
impl<Octs: AsRef<[u8]>> fmt::Display for Txt<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for slice in self.iter_charstrs() {
if !first {
f.write_str(" ")?;
} else {
first = false;
}
write!(f, "{}", slice.display_quoted())?;
}
Ok(())
}
}
impl<Octs: AsRef<[u8]>> fmt::Debug for Txt<Octs> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Txt(")?;
fmt::Display::fmt(self, f)?;
f.write_str(")")
}
}
impl<Octs> ZonefileFmt for Txt<Octs>
where
Octs: AsRef<[u8]>,
{
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
p.block(|p| {
for slice in self.iter_charstrs() {
p.write_token(slice.display_quoted())?;
}
Ok(())
})
}
}
#[cfg(feature = "serde")]
impl<Octs> serde::Serialize for Txt<Octs>
where
Octs: AsRef<[u8]> + SerializeOctets,
{
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeSeq;
struct TxtSeq<'a, Octs>(&'a Txt<Octs>);
impl<Octs> serde::Serialize for TxtSeq<'_, Octs>
where
Octs: AsRef<[u8]> + SerializeOctets,
{
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut serializer = serializer.serialize_seq(None)?;
for item in self.0.iter_charstrs() {
serializer.serialize_element(item)?;
}
serializer.end()
}
}
if serializer.is_human_readable() {
serializer.serialize_newtype_struct("Txt", &TxtSeq(self))
} else {
serializer.serialize_newtype_struct(
"Txt",
&self.0.as_serialized_octets(),
)
}
}
}
#[cfg(feature = "serde")]
impl<'de, Octs> serde::Deserialize<'de> for Txt<Octs>
where
Octs: FromBuilder + DeserializeOctets<'de>,
<Octs as FromBuilder>::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
use core::marker::PhantomData;
struct NewtypeVisitor<T>(PhantomData<T>);
impl<'de, Octs> serde::de::Visitor<'de> for NewtypeVisitor<Octs>
where
Octs: FromBuilder + DeserializeOctets<'de>,
<Octs as FromBuilder>::Builder:
OctetsBuilder + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
type Value = Txt<Octs>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("TXT record data")
}
fn visit_newtype_struct<D: serde::Deserializer<'de>>(
self,
deserializer: D,
) -> Result<Self::Value, D::Error> {
if deserializer.is_human_readable() {
deserializer.deserialize_seq(ReadableVisitor(PhantomData))
} else {
Octs::deserialize_with_visitor(
deserializer,
CompactVisitor(Octs::visitor()),
)
}
}
}
struct ReadableVisitor<Octs>(PhantomData<Octs>);
impl<'de, Octs> serde::de::Visitor<'de> for ReadableVisitor<Octs>
where
Octs: FromBuilder,
<Octs as FromBuilder>::Builder:
OctetsBuilder + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
type Value = Txt<Octs>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("TXT record data")
}
fn visit_str<E: serde::de::Error>(
self,
v: &str,
) -> Result<Self::Value, E> {
let mut builder =
TxtBuilder::<<Octs as FromBuilder>::Builder>::new();
let mut chars = v.chars();
while let Some(ch) =
Symbol::from_chars(&mut chars).map_err(E::custom)?
{
builder
.append_u8(ch.into_octet().map_err(E::custom)?)
.map_err(E::custom)?;
}
builder.finish().map_err(E::custom)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Self::Value, A::Error> {
let mut builder = <Octs as FromBuilder>::Builder::empty();
while seq
.next_element_seed(DeserializeCharStrSeed::new(
&mut builder,
))?
.is_some()
{
LongRecordData::check_len(builder.as_ref().len())
.map_err(serde::de::Error::custom)?;
}
if builder.as_ref().is_empty() {
builder
.append_slice(b"\0")
.map_err(|_| serde::de::Error::custom(ShortBuf))?;
}
Ok(Txt(builder.freeze()))
}
}
struct CompactVisitor<'de, T: DeserializeOctets<'de>>(T::Visitor);
impl<'de, Octs> serde::de::Visitor<'de> for CompactVisitor<'de, Octs>
where
Octs: FromBuilder + DeserializeOctets<'de>,
<Octs as FromBuilder>::Builder:
OctetsBuilder + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>,
{
type Value = Txt<Octs>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("TXT record data")
}
fn visit_borrowed_bytes<E: serde::de::Error>(
self,
value: &'de [u8],
) -> Result<Self::Value, E> {
self.0.visit_borrowed_bytes(value).and_then(|octets| {
Txt::from_octets(octets).map_err(E::custom)
})
}
#[cfg(feature = "std")]
fn visit_byte_buf<E: serde::de::Error>(
self,
value: std::vec::Vec<u8>,
) -> Result<Self::Value, E> {
self.0.visit_byte_buf(value).and_then(|octets| {
Txt::from_octets(octets).map_err(E::custom)
})
}
}
deserializer
.deserialize_newtype_struct("Txt", NewtypeVisitor(PhantomData))
}
}
#[derive(Clone)]
pub struct TxtCharStrIter<'a>(Parser<'a, [u8]>);
impl<'a> Iterator for TxtCharStrIter<'a> {
type Item = &'a CharStr<[u8]>;
fn next(&mut self) -> Option<Self::Item> {
if self.0.remaining() == 0 {
None
} else {
Some(CharStr::parse_slice(&mut self.0).unwrap())
}
}
}
#[derive(Clone)]
pub struct TxtIter<'a>(TxtCharStrIter<'a>);
impl<'a> Iterator for TxtIter<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(CharStr::as_slice)
}
}
#[derive(Clone, Debug)]
pub struct TxtBuilder<Builder> {
builder: Builder,
start: Option<usize>,
}
impl<Builder: OctetsBuilder + EmptyBuilder> TxtBuilder<Builder> {
#[must_use]
pub fn new() -> Self {
TxtBuilder {
builder: Builder::empty(),
start: None,
}
}
}
#[cfg(feature = "bytes")]
impl TxtBuilder<BytesMut> {
pub fn new_bytes() -> Self {
Self::new()
}
}
impl<Builder: OctetsBuilder + AsRef<[u8]> + AsMut<[u8]>> TxtBuilder<Builder> {
fn builder_append_slice(
&mut self,
slice: &[u8],
) -> Result<(), TxtAppendError> {
LongRecordData::check_append_len(
self.builder.as_ref().len(),
slice.len(),
)?;
self.builder.append_slice(slice)?;
Ok(())
}
pub fn append_slice(
&mut self,
mut slice: &[u8],
) -> Result<(), TxtAppendError> {
if let Some(start) = self.start {
let left = 255 - (self.builder.as_ref().len() - (start + 1));
if slice.len() < left {
self.builder_append_slice(slice)?;
return Ok(());
}
let (append, left) = slice.split_at(left);
self.builder_append_slice(append)?;
self.builder.as_mut()[start] = 255;
slice = left;
}
for chunk in slice.chunks(255) {
self.start = if chunk.len() == 255 {
None
} else {
Some(self.builder.as_ref().len())
};
self.builder_append_slice(&[chunk.len() as u8])?;
self.builder_append_slice(chunk)?;
}
Ok(())
}
pub fn append_u8(&mut self, ch: u8) -> Result<(), TxtAppendError> {
self.append_slice(&[ch])
}
pub fn append_charstr<Octs: AsRef<[u8]> + ?Sized>(
&mut self,
s: &CharStr<Octs>,
) -> Result<(), TxtAppendError> {
self.close_charstr();
LongRecordData::check_append_len(
self.builder.as_ref().len(),
usize::from(s.compose_len()),
)?;
s.compose(&mut self.builder)?;
Ok(())
}
pub fn close_charstr(&mut self) {
if let Some(start) = self.start {
let last_slice_len = self.builder.as_ref().len() - (start + 1);
self.builder.as_mut()[start] = last_slice_len as u8;
self.start = None;
}
}
pub fn finish(mut self) -> Result<Txt<Builder::Octets>, TxtAppendError>
where
Builder: FreezeBuilder,
{
self.close_charstr();
if self.builder.as_ref().is_empty() {
self.builder.append_slice(b"\0")?;
}
Ok(Txt(self.builder.freeze()))
}
}
impl<Builder: OctetsBuilder + EmptyBuilder> Default for TxtBuilder<Builder> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug)]
pub struct TxtError(TxtErrorInner);
#[derive(Clone, Copy, Debug)]
enum TxtErrorInner {
Empty,
Long(LongRecordData),
ShortInput,
}
impl TxtError {
#[must_use]
pub fn as_str(self) -> &'static str {
match self.0 {
TxtErrorInner::Empty => "empty TXT record",
TxtErrorInner::Long(err) => err.as_str(),
TxtErrorInner::ShortInput => "short input",
}
}
}
impl From<LongRecordData> for TxtError {
fn from(err: LongRecordData) -> TxtError {
TxtError(TxtErrorInner::Long(err))
}
}
impl From<TxtError> for FormError {
fn from(err: TxtError) -> FormError {
FormError::new(err.as_str())
}
}
impl fmt::Display for TxtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug)]
pub enum TxtAppendError {
LongRecordData,
ShortBuf,
}
impl TxtAppendError {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
TxtAppendError::LongRecordData => "record data too long",
TxtAppendError::ShortBuf => "buffer size exceeded",
}
}
}
impl From<LongRecordData> for TxtAppendError {
fn from(_: LongRecordData) -> TxtAppendError {
TxtAppendError::LongRecordData
}
}
impl<T: Into<ShortBuf>> From<T> for TxtAppendError {
fn from(_: T) -> TxtAppendError {
TxtAppendError::ShortBuf
}
}
impl fmt::Display for TxtAppendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
#[cfg(all(feature = "std", feature = "bytes"))]
mod test {
use super::*;
use crate::base::rdata::test::{
test_compose_parse, test_rdlen, test_scan,
};
use std::vec::Vec;
#[test]
#[allow(clippy::redundant_closure)] fn txt_compose_parse_scan() {
let rdata = Txt::from_octets(b"\x03foo\x03bar".as_ref()).unwrap();
test_rdlen(&rdata);
test_compose_parse(&rdata, |parser| Txt::parse(parser));
test_scan(&["foo", "bar"], Txt::scan, &rdata);
}
#[test]
fn txt_from_slice() {
assert!(Txt::from_octets(b"").is_err());
let short = b"01234";
let txt: Txt<Vec<u8>> = Txt::build_from_slice(short).unwrap();
assert_eq!(Some(&short[..]), txt.as_flat_slice());
assert_eq!(short.to_vec(), txt.text::<Vec<u8>>());
let full = short.repeat(51);
let txt: Txt<Vec<u8>> = Txt::build_from_slice(&full).unwrap();
assert_eq!(Some(&full[..]), txt.as_flat_slice());
assert_eq!(full.to_vec(), txt.text::<Vec<u8>>());
let long = short.repeat(52);
let txt: Txt<Vec<u8>> = Txt::build_from_slice(&long).unwrap();
assert_eq!(None, txt.as_flat_slice());
assert_eq!(long.to_vec(), txt.text::<Vec<u8>>());
let mut builder: TxtBuilder<Vec<u8>> = TxtBuilder::new();
for chunk in long.chunks(9) {
builder.append_slice(chunk).unwrap();
}
let txt = builder.finish().unwrap();
assert_eq!(None, txt.as_flat_slice());
assert_eq!(long.to_vec(), txt.text::<Vec<u8>>());
let builder: TxtBuilder<Vec<u8>> = TxtBuilder::new();
let txt = builder.finish().unwrap();
assert_eq!(Some(b"".as_ref()), txt.as_flat_slice());
let mut builder: TxtBuilder<Vec<u8>> = TxtBuilder::new();
builder.append_slice(b"").unwrap();
let txt = builder.finish().unwrap();
assert_eq!(Some(b"".as_ref()), txt.as_flat_slice());
let mut parser = Parser::from_static(b"\x01");
assert!(Txt::parse(&mut parser).is_err());
let mut builder: TxtBuilder<Vec<u8>> = TxtBuilder::new();
assert!(builder
.append_slice(&b"\x00".repeat(u16::MAX as usize))
.is_err());
let mut builder: TxtBuilder<Vec<u8>> = TxtBuilder::new();
assert!(builder
.append_slice(&b"\x00".repeat(u16::MAX as usize - 512))
.is_ok());
assert!(builder.append_slice(&b"\x00".repeat(512)).is_err());
}
#[test]
fn txt_canonical_compare() {
let data = [
"mailru-verification: 14505c6eb222c847",
"yandex-verification: 6059b187e78de544",
"v=spf1 include:_spf.protonmail.ch ~all",
"swisssign-check=CF0JHMTlTDNoES3rrknIRggocffSwqmzMb9X8YbjzK",
"google-site-\
verification=aq9zJnp3H3bNE0Y4D4rH5I5Dhj8VMaLYx0uQ7Rozfgg",
"ahrefs-site-verification_\
4bdac6bbaa81e0d591d7c0f3ef238905c0521b69bf3d74e64d3775bc\
b2743afd",
"brave-ledger-verification=\
66a7f27fb99949cc0c564ab98efcc58ea1bac3e97eb557c782ab2d44b\
49aefd7",
];
let records = data
.iter()
.map(|e| {
let mut builder = TxtBuilder::<Vec<u8>>::new();
builder.append_slice(e.as_bytes()).unwrap();
builder.finish().unwrap()
})
.collect::<Vec<_>>();
let mut sorted = records.clone();
sorted.sort_by(|a, b| a.canonical_cmp(b));
for (a, b) in records.iter().zip(sorted.iter()) {
assert_eq!(a, b);
}
}
#[test]
fn txt_strings_eq() {
let records = [["foo", "bar"], ["foob", "ar"], ["foo", "bar"]];
let records = records
.iter()
.map(|strings| {
let mut builder = TxtBuilder::<Vec<u8>>::new();
for string in strings {
builder
.append_charstr(
CharStr::from_slice(string.as_bytes()).unwrap(),
)
.unwrap();
}
builder.finish().unwrap()
})
.collect::<Vec<_>>();
assert_ne!(records[0], records[1]);
assert_eq!(records[0], records[2]);
}
#[cfg(all(feature = "serde", feature = "std"))]
#[test]
fn txt_ser_de() {
use serde_test::{assert_tokens, Configure, Token};
let txt = Txt::from_octets(Vec::from(b"\x03foo".as_ref())).unwrap();
assert_tokens(
&txt.clone().compact(),
&[
Token::NewtypeStruct { name: "Txt" },
Token::ByteBuf(b"\x03foo"),
],
);
assert_tokens(
&txt.readable(),
&[
Token::NewtypeStruct { name: "Txt" },
Token::Seq { len: None },
Token::NewtypeStruct { name: "CharStr" },
Token::BorrowedStr("foo"),
Token::SeqEnd,
],
);
let txt = Txt::from_octets(Vec::from(b"\x03foo\x04\\bar".as_ref()))
.unwrap();
assert_tokens(
&txt.clone().compact(),
&[
Token::NewtypeStruct { name: "Txt" },
Token::ByteBuf(b"\x03foo\x04\\bar"),
],
);
assert_tokens(
&txt.readable(),
&[
Token::NewtypeStruct { name: "Txt" },
Token::Seq { len: None },
Token::NewtypeStruct { name: "CharStr" },
Token::BorrowedStr("foo"),
Token::NewtypeStruct { name: "CharStr" },
Token::BorrowedStr("\\\\bar"),
Token::SeqEnd,
],
);
}
#[cfg(all(feature = "serde", feature = "std"))]
#[test]
fn txt_de_str() {
use serde_test::{assert_de_tokens, Configure, Token};
assert_de_tokens(
&Txt::from_octets(Vec::from(b"\x03foo".as_ref()))
.unwrap()
.readable(),
&[
Token::NewtypeStruct { name: "Txt" },
Token::BorrowedStr("foo"),
],
);
}
#[test]
fn txt_display() {
fn cmp(input: &[u8], output: &str) {
assert_eq!(
format!("{}", Txt::from_octets(input).unwrap()),
output
);
}
cmp(b"\x03foo", "\"foo\"");
cmp(b"\x03foo\x03bar", "\"foo\" \"bar\"");
cmp(b"\x03fo\"\x04bar ", "\"fo\\\"\" \"bar \"");
}
}