use crate::error::{Error, Result};
use time::{
format_description::BorrowedFormatItem, macros::format_description, OffsetDateTime,
PrimitiveDateTime,
};
const HIVE_TIME_FORMAT: &[BorrowedFormatItem<'_>] =
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
pub trait GrapheneSerialize {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()>;
fn to_wire(&self) -> Result<Vec<u8>> {
let mut out = Vec::new();
self.append_to(&mut out)?;
Ok(out)
}
}
pub fn write_varint32(out: &mut Vec<u8>, mut n: u32) {
while n >= 0x80 {
out.push(((n & 0x7f) as u8) | 0x80);
n >>= 7;
}
out.push(n as u8);
}
pub fn read_varint32(data: &[u8]) -> Result<(u32, usize)> {
let mut result: u64 = 0;
let mut shift = 0u32;
for (i, &b) in data.iter().enumerate() {
if shift > 28 {
return Err(Error::ser("varint32 is longer than 5 bytes"));
}
result |= ((b & 0x7f) as u64) << shift;
if b & 0x80 == 0 {
if result > u32::MAX as u64 {
return Err(Error::ser("varint32 overflows 32 bits"));
}
return Ok((result as u32, i + 1));
}
shift += 7;
}
Err(Error::ser("truncated varint32"))
}
fn write_len(out: &mut Vec<u8>, len: usize, what: &str) -> Result<()> {
let len = u32::try_from(len)
.map_err(|_| Error::ser(format!("{what} is longer than u32::MAX bytes")))?;
write_varint32(out, len);
Ok(())
}
pub fn write_u8(out: &mut Vec<u8>, v: u8) {
out.push(v);
}
pub fn write_bool(out: &mut Vec<u8>, v: bool) {
out.push(u8::from(v));
}
pub fn write_i16(out: &mut Vec<u8>, v: i16) {
out.extend_from_slice(&v.to_le_bytes());
}
pub fn write_u16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_le_bytes());
}
pub fn write_u32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_le_bytes());
}
pub fn write_i64(out: &mut Vec<u8>, v: i64) {
out.extend_from_slice(&v.to_le_bytes());
}
pub fn write_u64(out: &mut Vec<u8>, v: u64) {
out.extend_from_slice(&v.to_le_bytes());
}
pub(crate) fn hived_transport_form(s: &str) -> std::borrow::Cow<'_, str> {
fn affected(c: char) -> bool {
matches!(c, '\u{00}'..='\u{08}' | '\u{0b}' | '\u{0c}' | '\u{0e}'..='\u{1f}')
}
if !s.contains(affected) {
return std::borrow::Cow::Borrowed(s);
}
let mut rewritten = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\u{08}' => rewritten.push('b'),
'\u{0c}' => rewritten.push('f'),
_ if affected(c) => {
use std::fmt::Write as _;
let _ = write!(rewritten, "u{:04x}", c as u32);
}
_ => rewritten.push(c),
}
}
std::borrow::Cow::Owned(rewritten)
}
pub fn write_string(out: &mut Vec<u8>, s: &str) -> Result<()> {
let transported = hived_transport_form(s);
let bytes = transported.as_bytes();
write_len(out, bytes.len(), "string")?;
out.extend_from_slice(bytes);
Ok(())
}
pub fn write_bytes(out: &mut Vec<u8>, b: &[u8]) -> Result<()> {
write_len(out, b.len(), "buffer")?;
out.extend_from_slice(b);
Ok(())
}
pub fn write_raw(out: &mut Vec<u8>, b: &[u8]) {
out.extend_from_slice(b);
}
pub fn write_array<T: GrapheneSerialize>(out: &mut Vec<u8>, items: &[T]) -> Result<()> {
write_len(out, items.len(), "array")?;
for item in items {
item.append_to(out)?;
}
Ok(())
}
pub fn write_optional<T: GrapheneSerialize>(out: &mut Vec<u8>, v: Option<&T>) -> Result<()> {
match v {
None => {
out.push(0);
Ok(())
}
Some(inner) => {
out.push(1);
inner.append_to(out)
}
}
}
pub fn write_static_variant<T: GrapheneSerialize>(
out: &mut Vec<u8>,
tag: u32,
value: &T,
) -> Result<()> {
write_varint32(out, tag);
value.append_to(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PointInTime(u32);
impl PointInTime {
pub const MAXIMUM: PointInTime = PointInTime(u32::MAX);
pub fn from_unix(secs: i64) -> Result<Self> {
if let Ok(v) = u32::try_from(secs) {
return Ok(PointInTime(v));
}
if let Ok(v) = i32::try_from(secs) {
return Ok(PointInTime(v as u32));
}
Err(Error::Time(format!(
"{secs} is outside the uint32 epoch range"
)))
}
pub fn is_maximum(&self) -> bool {
self.0 == u32::MAX
}
pub fn unix(&self) -> u32 {
self.0
}
pub fn parse(s: &str) -> Result<Self> {
let trimmed = s.trim();
let core = trimmed.strip_suffix('Z').unwrap_or(trimmed);
let dt = PrimitiveDateTime::parse(core, HIVE_TIME_FORMAT).map_err(|e| {
Error::Time(format!("could not parse {core:?} as a Hive timestamp: {e}"))
})?;
Self::from_unix(dt.assume_utc().unix_timestamp())
}
pub fn now_plus(seconds: u32) -> Result<Self> {
let now = OffsetDateTime::now_utc().unix_timestamp();
Self::from_unix(now + i64::from(seconds))
}
pub fn to_iso(&self) -> Result<String> {
let seconds = if self.0 >= 0x8000_0000 {
i64::from(self.0 as i32)
} else {
i64::from(self.0)
};
let dt =
OffsetDateTime::from_unix_timestamp(seconds).map_err(|e| Error::Time(e.to_string()))?;
dt.format(HIVE_TIME_FORMAT)
.map_err(|e| Error::Time(e.to_string()))
}
}
impl GrapheneSerialize for PointInTime {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_u32(out, self.0);
Ok(())
}
}
impl<'de> serde::Deserialize<'de> for PointInTime {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error as _;
let s = String::deserialize(d)?;
PointInTime::parse(&s).map_err(D::Error::custom)
}
}
impl serde::Serialize for PointInTime {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::Error as _;
s.serialize_str(&self.to_iso().map_err(S::Error::custom)?)
}
}
impl GrapheneSerialize for String {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_string(out, self)
}
}
impl GrapheneSerialize for u8 {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_u8(out, *self);
Ok(())
}
}
impl GrapheneSerialize for u16 {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_u16(out, *self);
Ok(())
}
}
impl GrapheneSerialize for u32 {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_u32(out, *self);
Ok(())
}
}
impl GrapheneSerialize for u64 {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
write_u64(out, *self);
Ok(())
}
}
impl<T: GrapheneSerialize> GrapheneSerialize for &T {
fn append_to(&self, out: &mut Vec<u8>) -> Result<()> {
(*self).append_to(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn varint_matches_graphene_vectors() {
let cases: &[(u32, &[u8])] = &[
(0, &[0x00]),
(1, &[0x01]),
(127, &[0x7f]),
(128, &[0x80, 0x01]),
(300, &[0xac, 0x02]),
(16383, &[0xff, 0x7f]),
(16384, &[0x80, 0x80, 0x01]),
(u32::MAX, &[0xff, 0xff, 0xff, 0xff, 0x0f]),
];
for (n, expected) in cases {
let mut out = Vec::new();
write_varint32(&mut out, *n);
assert_eq!(&out[..], *expected, "varint({n})");
assert_eq!(read_varint32(&out).unwrap(), (*n, expected.len()));
}
}
#[test]
fn varint_rejects_overlong_and_truncated() {
assert!(read_varint32(&[0x80, 0x80, 0x80, 0x80, 0x80, 0x01]).is_err());
assert!(read_varint32(&[0x80]).is_err());
assert!(read_varint32(&[]).is_err());
}
#[test]
fn control_characters_take_the_form_hived_will_receive() {
let encode = |s: &str| {
let mut out = Vec::new();
write_string(&mut out, s).unwrap();
out
};
assert_eq!(encode("\u{1}"), b"\x05u0001".to_vec());
assert_eq!(encode("\u{8}"), b"\x01b".to_vec());
assert_eq!(encode("\u{c}"), b"\x01f".to_vec());
assert_eq!(encode("\t"), b"\x01\t".to_vec());
assert_eq!(encode("\n"), b"\x01\n".to_vec());
assert_eq!(encode("\r"), b"\x01\r".to_vec());
assert_eq!(encode("\""), b"\x01\"".to_vec());
assert_eq!(encode("\\"), b"\x01\\".to_vec());
assert_eq!(encode("\u{7f}"), b"\x01\x7f".to_vec());
assert_eq!(encode("é"), b"\x02\xc3\xa9".to_vec());
assert_eq!(encode("\u{1}\u{8}\u{c}"), b"\x07u0001bf".to_vec());
assert_eq!(encode("x\u{1}y"), b"\x07xu0001y".to_vec());
assert_eq!(encode("\u{1}")[0], 5);
}
#[test]
fn strings_without_control_characters_are_untouched() {
for sample in ["", "alice", "{\"a\":1}", "a\nb\tc\r", "unicode é 中文 🐝"] {
assert!(
matches!(hived_transport_form(sample), std::borrow::Cow::Borrowed(_)),
"{sample:?} should pass through borrowed"
);
let mut out = Vec::new();
write_string(&mut out, sample).unwrap();
assert_eq!(&out[out.len() - sample.len()..], sample.as_bytes());
}
}
#[test]
fn string_length_counts_utf8_bytes_not_chars() {
let mut out = Vec::new();
write_string(&mut out, "é🐝").unwrap();
assert_eq!(out[0], 6);
assert_eq!(out.len(), 7);
}
#[test]
fn empty_string_is_a_single_zero() {
let mut out = Vec::new();
write_string(&mut out, "").unwrap();
assert_eq!(out, vec![0x00]);
}
#[test]
fn optional_distinguishes_empty_from_absent() {
let mut absent = Vec::new();
write_optional::<String>(&mut absent, None).unwrap();
assert_eq!(absent, vec![0x00]);
let mut empty = Vec::new();
write_optional(&mut empty, Some(&String::new())).unwrap();
assert_eq!(empty, vec![0x01, 0x00]);
}
#[test]
fn timestamps_parse_as_utc() {
let t = PointInTime::parse("2026-08-22T14:30:00").unwrap();
assert_eq!(t.unix(), 1787409000);
assert_eq!(t.to_iso().unwrap(), "2026-08-22T14:30:00");
assert_eq!(PointInTime::parse("2026-08-22T14:30:00Z").unwrap(), t);
}
#[test]
fn timestamps_serialize_little_endian_u32() {
let t = PointInTime::from_unix(1).unwrap();
assert_eq!(t.to_wire().unwrap(), vec![0x01, 0x00, 0x00, 0x00]);
}
#[test]
fn timestamps_reject_junk_and_out_of_range() {
assert!(PointInTime::parse("not a time").is_err());
assert!(PointInTime::parse("2026-08-22 14:30:00").is_err());
assert!(PointInTime::parse("2026-08-22T14:30:00+02:00").is_err());
assert!(PointInTime::from_unix(i64::from(u32::MAX) + 1).is_err());
assert!(PointInTime::from_unix(i64::from(i32::MIN) - 1).is_err());
}
#[test]
fn the_never_sentinel_round_trips() {
let sentinel = PointInTime::parse("1969-12-31T23:59:59").unwrap();
assert_eq!(sentinel, PointInTime::MAXIMUM);
assert!(sentinel.is_maximum());
assert_eq!(sentinel.unix(), u32::MAX);
assert_eq!(sentinel.to_iso().unwrap(), "1969-12-31T23:59:59");
assert_eq!(sentinel.to_wire().unwrap(), vec![0xff, 0xff, 0xff, 0xff]);
}
#[test]
fn the_epoch_itself_is_not_a_sentinel() {
let epoch = PointInTime::parse("1970-01-01T00:00:00").unwrap();
assert_eq!(epoch.unix(), 0);
assert!(!epoch.is_maximum());
assert_eq!(epoch.to_iso().unwrap(), "1970-01-01T00:00:00");
}
#[test]
fn arrays_are_length_prefixed() {
let mut out = Vec::new();
write_array(&mut out, &["a".to_string(), "bb".to_string()]).unwrap();
assert_eq!(out, vec![0x02, 0x01, b'a', 0x02, b'b', b'b']);
}
}