use std::{fmt, io};
use bstr::{BStr, BString};
use byteorder::{BigEndian, WriteBytesExt};
use chrono::{DateTime, SubsecRound, TimeZone, Utc};
use nom::combinator::{map, map_opt, rest};
use nom::multi::length_data;
use nom::number::streaming::{be_u32, be_u8};
use nom::sequence::tuple;
use nom::IResult;
use num_traits::FromPrimitive;
use crate::errors::Result;
use crate::line_writer::LineBreak;
use crate::normalize_lines::Normalized;
use crate::packet::PacketTrait;
use crate::ser::Serialize;
use crate::types::{Tag, Version};
#[derive(Clone, PartialEq, Eq)]
pub struct LiteralData {
packet_version: Version,
mode: DataMode,
file_name: BString,
created: DateTime<Utc>,
data: Vec<u8>,
}
#[derive(Debug, Copy, Clone, FromPrimitive, PartialEq, Eq)]
#[repr(u8)]
pub enum DataMode {
Binary = b'b',
Text = b't',
Utf8 = b'u',
Mime = b'm',
}
impl LiteralData {
pub fn from_str(file_name: impl Into<BString>, raw_data: &str) -> Self {
let data = Normalized::new(raw_data.bytes(), LineBreak::Crlf).collect();
LiteralData {
packet_version: Version::New,
mode: DataMode::Utf8,
file_name: file_name.into(),
created: Utc::now().trunc_subsecs(0),
data,
}
}
pub fn from_bytes(file_name: &BStr, data: &[u8]) -> Self {
LiteralData {
packet_version: Version::New,
mode: DataMode::Binary,
file_name: file_name.to_owned(),
created: Utc::now().trunc_subsecs(0),
data: data.to_owned(),
}
}
pub fn from_slice(packet_version: Version, input: &[u8]) -> Result<Self> {
let (_, pk) = parse(packet_version)(input)?;
Ok(pk)
}
pub fn is_binary(&self) -> bool {
matches!(self.mode, DataMode::Binary)
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn to_string(&self) -> Option<String> {
match self.mode {
DataMode::Binary => None,
_ => std::str::from_utf8(&self.data).map(str::to_owned).ok(),
}
}
}
impl Serialize for LiteralData {
fn to_writer<W: io::Write>(&self, writer: &mut W) -> Result<()> {
let name = &self.file_name;
writer.write_all(&[self.mode as u8, name.len() as u8])?;
writer.write_all(name)?;
writer.write_u32::<BigEndian>(self.created.timestamp() as u32)?;
writer.write_all(&self.data)?;
Ok(())
}
}
fn parse(packet_version: Version) -> impl Fn(&[u8]) -> IResult<&[u8], LiteralData> {
move |i: &[u8]| {
map(
tuple((
map_opt(be_u8, DataMode::from_u8),
map(length_data(be_u8), BStr::new::<[u8]>),
map_opt(be_u32, |v| Utc.timestamp_opt(i64::from(v), 0).single()),
rest,
)),
|(mode, name, created, data)| LiteralData {
packet_version,
mode,
created,
file_name: name.to_owned(),
data: data.to_vec(),
},
)(i)
}
}
impl PacketTrait for LiteralData {
fn packet_version(&self) -> Version {
self.packet_version
}
fn tag(&self) -> Tag {
Tag::LiteralData
}
}
impl fmt::Debug for LiteralData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LiteralData")
.field("packet_version", &self.packet_version)
.field("mode", &self.mode)
.field("created", &self.created)
.field("file_name", &self.file_name)
.field("data", &hex::encode(&self.data))
.finish()
}
}
#[test]
fn test_utf8_literal() {
#![allow(clippy::unwrap_used)]
let slogan = "一门赋予每个人构建可靠且高效软件能力的语言。";
let literal = LiteralData::from_str("", slogan);
assert!(String::from_utf8(literal.data).unwrap() == slogan);
}