use crate::codec::{Mapping, MappingId};
use crate::{checksum_bytes, Error, Mode, Result, PROTOCOL_NAME, PROTOCOL_VERSION};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionManifest {
pub protocol: String,
pub version: u8,
pub mode: String,
pub session_id: String,
pub mapping_id: String,
pub alphabet: String,
pub checksum: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed_hex: Option<String>,
}
impl SessionManifest {
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| Error::InvalidManifest(e.to_string()))
}
pub fn to_json_pretty(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(|e| Error::InvalidManifest(e.to_string()))
}
pub fn from_json(s: &str) -> Result<Self> {
let m: Self = serde_json::from_str(s).map_err(|e| Error::InvalidManifest(e.to_string()))?;
m.validate()?;
Ok(m)
}
pub fn validate(&self) -> Result<()> {
if self.protocol != PROTOCOL_NAME {
return Err(Error::InvalidManifest(format!(
"unexpected protocol '{}'",
self.protocol
)));
}
if self.version != PROTOCOL_VERSION {
return Err(Error::UnsupportedVersion(self.version));
}
if self.mode != Mode::Session.as_str() {
return Err(Error::InvalidManifest(format!(
"expected mode 'session', got '{}'",
self.mode
)));
}
if self.session_id.is_empty() || self.mapping_id.is_empty() {
return Err(Error::InvalidManifest(
"session_id and mapping_id must be non-empty".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Session {
seed: Vec<u8>,
session_id: String,
mapping: Mapping,
manifest: SessionManifest,
}
impl Session {
pub fn builder() -> SessionBuilder {
SessionBuilder::default()
}
pub fn from_seed(seed: impl AsRef<[u8]>, session_id: impl Into<String>) -> Result<Self> {
SessionBuilder::default()
.seed(seed.as_ref())
.session_id(session_id)
.build()
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn mapping_id(&self) -> &MappingId {
self.mapping.id()
}
pub fn mapping(&self) -> &Mapping {
&self.mapping
}
pub fn manifest(&self) -> &SessionManifest {
&self.manifest
}
pub fn encode(&self, text: &str) -> Result<String> {
Ok(self.mapping.encode_str(text))
}
pub fn decode(&self, encoded: &str) -> Result<String> {
self.mapping.decode_str(encoded)
}
pub fn seed(&self) -> &[u8] {
&self.seed
}
}
#[derive(Debug, Default)]
pub struct SessionBuilder {
seed: Option<Vec<u8>>,
session_id: Option<String>,
}
impl SessionBuilder {
pub fn seed(mut self, seed: impl AsRef<[u8]>) -> Self {
self.seed = Some(seed.as_ref().to_vec());
self
}
pub fn session_id(mut self, id: impl Into<String>) -> Self {
self.session_id = Some(id.into());
self
}
pub fn build(self) -> Result<Session> {
let seed = self.seed.unwrap_or_default();
let mapping = Mapping::from_seed(&seed)?;
let session_id = self
.session_id
.unwrap_or_else(|| format!("sess-{:08x}", checksum_bytes(&seed)));
let table_checksum = checksum_bytes(mapping.encode_table());
let manifest = SessionManifest {
protocol: PROTOCOL_NAME.into(),
version: PROTOCOL_VERSION,
mode: Mode::Session.as_str().into(),
session_id: session_id.clone(),
mapping_id: mapping.id().to_string(),
alphabet: format!("pua-byte-{}", crate::ALPHABET_SIZE),
checksum: format!("{table_checksum:08x}"),
seed_hex: Some(hex::encode(&seed)),
};
manifest.validate()?;
Ok(Session {
seed,
session_id,
mapping,
manifest,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_api() {
let session = Session::builder().seed(b"demo").build().unwrap();
let payload = session.encode("hello").unwrap();
assert_eq!(session.decode(&payload).unwrap(), "hello");
let json = session.manifest().to_json().unwrap();
let parsed = SessionManifest::from_json(&json).unwrap();
assert_eq!(parsed.session_id, session.session_id());
}
}