use std::fmt;
use crate::crc32c::crc32c;
use crate::ticket_addr::TicketAddr;
pub(crate) const VERSION_V0: u8 = 0x00;
const BACKEND_OPENAI_COMPATIBLE: u8 = 0x00;
const VERSION_LEN: usize = 1;
const ENDPOINT_ID_LEN: usize = 32;
const ADDR_COUNT_LEN: usize = 1;
const BACKEND_LEN: usize = 1;
pub(crate) const CRC_LEN: usize = 4;
pub(crate) const MIN_V0: usize =
VERSION_LEN + ENDPOINT_ID_LEN + ADDR_COUNT_LEN + BACKEND_LEN + CRC_LEN;
pub(crate) const MAX_TICKET_BYTES: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum BackendHint {
OpenAiCompatible,
Unknown(u8),
}
impl BackendHint {
const fn as_byte(self) -> u8 {
match self {
Self::OpenAiCompatible => BACKEND_OPENAI_COMPATIBLE,
Self::Unknown(b) => b,
}
}
const fn from_byte(b: u8) -> Self {
if b == BACKEND_OPENAI_COMPATIBLE {
Self::OpenAiCompatible
} else {
Self::Unknown(b)
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Ticket {
endpoint_id: [u8; ENDPOINT_ID_LEN],
addrs: Vec<TicketAddr>,
backend: BackendHint,
}
impl Ticket {
pub(crate) fn new(
endpoint_id: [u8; ENDPOINT_ID_LEN],
addrs: Vec<TicketAddr>,
backend: BackendHint,
) -> Self {
let mut encoded: Vec<(Vec<u8>, TicketAddr)> = addrs
.into_iter()
.filter_map(|a| a.encoded().map(|bytes| (bytes, a)))
.collect();
encoded.sort_by(|a, b| a.0.cmp(&b.0));
encoded.dedup_by(|a, b| a.0 == b.0);
encoded.truncate(u8::MAX as usize);
let mut total = MIN_V0 + encoded.iter().map(|(b, _)| b.len()).sum::<usize>();
while total > MAX_TICKET_BYTES {
let (bytes, _) = encoded.pop().expect("MIN_V0 alone is inside the cap");
total -= bytes.len();
}
Self {
endpoint_id,
addrs: encoded.into_iter().map(|(_, a)| a).collect(),
backend,
}
}
pub(crate) const fn endpoint_id(&self) -> &[u8; ENDPOINT_ID_LEN] {
&self.endpoint_id
}
pub(crate) fn addrs(&self) -> &[TicketAddr] {
&self.addrs
}
pub fn fingerprint(&self) -> String {
crate::fingerprint::of(&self.endpoint_id)
}
pub(crate) fn encode(&self) -> Vec<u8> {
let mut body = Vec::with_capacity(MIN_V0);
body.push(VERSION_V0);
body.extend_from_slice(&self.endpoint_id);
body.push(u8::try_from(self.addrs.len()).expect("new() bounds the count"));
for addr in &self.addrs {
body.extend_from_slice(&addr.encoded().expect("new() dropped unframeable addresses"));
}
body.push(self.backend.as_byte());
let crc = crc32c(&body);
body.extend_from_slice(&crc.to_be_bytes());
debug_assert!(body.len() <= MAX_TICKET_BYTES, "new() bounds the total");
body
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self, TicketParseError> {
let body = &bytes[..bytes.len() - CRC_LEN];
let mut pos = VERSION_LEN;
let endpoint_id: [u8; ENDPOINT_ID_LEN] = body
.get(pos..pos + ENDPOINT_ID_LEN)
.ok_or(TicketParseError::Malformed)?
.try_into()
.expect("slice length checked");
pos += ENDPOINT_ID_LEN;
let count = *body.get(pos).ok_or(TicketParseError::Malformed)?;
pos += ADDR_COUNT_LEN;
let mut addrs = Vec::with_capacity(count as usize);
for _ in 0..count {
if let Some(addr) = TicketAddr::read(body, &mut pos)? {
addrs.push(addr);
}
}
let backend = BackendHint::from_byte(*body.get(pos).ok_or(TicketParseError::Malformed)?);
pos += BACKEND_LEN;
if pos != body.len() {
return Err(TicketParseError::Malformed);
}
Ok(Self::new(endpoint_id, addrs, backend))
}
}
impl fmt::Debug for Ticket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Ticket").field(&self.fingerprint()).finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TicketParseError {
Malformed,
UnsupportedVersion(u8),
}
impl fmt::Display for TicketParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Malformed => write!(f, "ticket is malformed — re-copy it from the serve side"),
Self::UnsupportedVersion(v) => write!(f, "ticket format v{v} is newer than this build"),
}
}
}
impl std::error::Error for TicketParseError {}
#[cfg(test)]
#[path = "ticket_tests.rs"]
mod ticket_tests;