use std::fmt;
use std::io::Write;
use std::str::FromStr;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use flate2::write::GzEncoder;
use serde_json::Value;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use uuid::Uuid;
mod emit;
mod jose;
use emit::{Node, Obj};
pub use jose::{
Chain, Compression, ContentEncrypter, CryptoError, KeyEncrypter, Sealed, Signer, Zeroizing,
encrypt, sign, sign_again,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Date(String);
impl Date {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Date {
type Err = WriteError;
fn from_str(text: &str) -> Result<Date, WriteError> {
OffsetDateTime::parse(text, &Rfc3339).map_err(|_| WriteError::BadDate)?;
Ok(Date(text.to_owned()))
}
}
impl TryFrom<OffsetDateTime> for Date {
type Error = WriteError;
fn try_from(time: OffsetDateTime) -> Result<Date, WriteError> {
time.format(&Rfc3339)
.map(Date)
.map_err(|_| WriteError::BadDate)
}
}
impl fmt::Display for Date {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
const MAX_UNSIGNED_INT: u64 = (1 << 53) - 1;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Duration(Seconds);
#[derive(Debug, Clone, Copy, PartialEq)]
enum Seconds {
Whole(u64),
Fraction(f64),
}
impl Duration {
pub fn try_from_secs(seconds: u64) -> Result<Duration, WriteError> {
if seconds > MAX_UNSIGNED_INT {
return Err(WriteError::BadDuration);
}
Ok(Duration(Seconds::Whole(seconds)))
}
pub fn try_from_secs_f64(seconds: f64) -> Result<Duration, WriteError> {
let written = serde_json::Number::from_f64(seconds).map(|n| n.to_string());
match written {
Some(text) if !seconds.is_sign_negative() && text.contains('.') => {
Ok(Duration(Seconds::Fraction(seconds)))
}
_ => Err(WriteError::BadDuration),
}
}
}
macro_rules! index {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[doc = concat!("let i = vcon::write::", stringify!($name), "::new(2);")]
#[doc = "assert_eq!(i.get(), 2, \"the index given\");"]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(usize);
impl $name {
#[doc = concat!("let i = vcon::write::", stringify!($name), "::new(0);")]
#[doc = "assert_eq!(i.get(), 0, \"the first entry\");"]
#[must_use]
pub const fn new(i: usize) -> $name {
$name(i)
}
#[doc = concat!("assert_eq!(vcon::write::", stringify!($name), "::new(7).get(), 7, \"the number given\");")]
#[must_use]
pub const fn get(self) -> usize {
self.0
}
}
};
}
index!(
PartyIndex
);
index!(
DialogIndex
);
index!(
AttachmentIndex
);
index!(
AnalysisIndex
);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContentHash(String);
impl ContentHash {
pub fn try_new(alg: &str, digest: &[u8]) -> Result<ContentHash, WriteError> {
if !is_hash_algorithm(alg) || digest.is_empty() {
return Err(WriteError::BadHash);
}
Ok(ContentHash(format!(
"{alg}-{}",
URL_SAFE_NO_PAD.encode(digest)
)))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn algorithm(&self) -> &str {
self.0.split_once('-').map_or("", |(alg, _)| alg)
}
#[must_use]
pub fn digest(&self) -> Vec<u8> {
let digest = self.0.split_once('-').map_or("", |(_, d)| d);
URL_SAFE_NO_PAD.decode(digest).unwrap_or_default()
}
}
impl FromStr for ContentHash {
type Err = WriteError;
fn from_str(token: &str) -> Result<ContentHash, WriteError> {
let (alg, digest) = token.split_once('-').ok_or(WriteError::BadHash)?;
let decoded = URL_SAFE_NO_PAD
.decode(digest)
.map_err(|_| WriteError::BadHash)?;
if !is_hash_algorithm(alg) || decoded.is_empty() {
return Err(WriteError::BadHash);
}
Ok(ContentHash(token.to_owned()))
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn is_hash_algorithm(alg: &str) -> bool {
!alg.is_empty()
&& alg
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Body {
Bytes(Vec<u8>),
Text(String),
Json(Value),
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Content {
Inline(Body),
External {
url: String,
hashes: Vec<ContentHash>,
},
HashOnly(Vec<ContentHash>),
Absent,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Reference {
uuid: Option<Uuid>,
url: Option<String>,
hashes: Vec<ContentHash>,
}
impl Reference {
#[must_use]
pub fn by_uuid(uuid: Uuid) -> Reference {
Reference {
uuid: Some(uuid),
url: None,
hashes: Vec::new(),
}
}
#[must_use]
pub fn by_url(url: impl Into<String>, hashes: Vec<ContentHash>) -> Reference {
Reference {
uuid: None,
url: Some(url.into()),
hashes,
}
}
#[must_use]
pub fn with_url(mut self, url: impl Into<String>, hashes: Vec<ContentHash>) -> Reference {
self.url = Some(url.into());
self.hashes = hashes;
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct CivicAddress {
members: Vec<(&'static str, String)>,
}
macro_rules! civic {
($($name:ident),*) => {
impl CivicAddress {
#[must_use]
pub fn new() -> CivicAddress {
CivicAddress::default()
}
$(
#[doc = concat!("Sets `", stringify!($name), "`.")]
#[doc = concat!("let address = vcon::write::CivicAddress::new().", stringify!($name), "(\"x\");")]
#[doc = "assert_ne!(address, vcon::write::CivicAddress::new(), \"one member set\");"]
#[must_use]
pub fn $name(mut self, value: impl Into<String>) -> CivicAddress {
self.set(stringify!($name), value.into());
self
}
)*
}
pub(crate) const CIVIC_ORDER: &[&str] = &[$(stringify!($name)),*];
};
}
civic!(
country, a1, a2, a3, a4, a5, a6, prd, pod, sts, hno, hns, lmk, loc, flr, nam, pc
);
impl CivicAddress {
fn set(&mut self, name: &'static str, value: String) {
match self.members.iter_mut().find(|(n, _)| *n == name) {
Some(slot) => slot.1 = value,
None => self.members.push((name, value)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
struct Members(Vec<(String, Value)>);
const RESERVED: [&str; 3] = ["vcon", "must_support", "appended"];
const TOP_MEMBERS: &[&str] = &[
"uuid",
"extensions",
"critical",
"created_at",
"updated_at",
"subject",
"redacted",
"amended",
"parties",
"dialog",
"analysis",
"attachments",
"payload",
"signatures",
"ciphertext",
"recipients",
];
const PARTY_MEMBERS: &[&str] = &[
"tel",
"sip",
"stir",
"mailto",
"name",
"did",
"validation",
"gmlpos",
"civicaddress",
"uuid",
"type",
"org",
"dept",
];
const DIALOG_MEMBERS: &[&str] = &[
"type",
"start",
"duration",
"parties",
"originator",
"recordings",
"recording_set",
"mediatype",
"filename",
"body",
"encoding",
"url",
"content_hash",
"disposition",
"session_id",
"party_history",
"transferee",
"transferor",
"transfer_target",
"original",
"consultation",
"target_dialog",
"application",
"message_id",
];
const ATTACHMENT_MEMBERS: &[&str] = &[
"purpose",
"start",
"party",
"dialog",
"mediatype",
"filename",
"body",
"encoding",
"url",
"content_hash",
];
const ANALYSIS_MEMBERS: &[&str] = &[
"type",
"dialog",
"attachment",
"mediatype",
"filename",
"vendor",
"product",
"schema",
"body",
"encoding",
"url",
"content_hash",
];
impl Members {
fn set(&mut self, core: &[&str], name: &str, value: Value) -> Result<(), WriteError> {
if core.contains(&name) || RESERVED.contains(&name) {
return Err(WriteError::CoreMember(name.to_owned()));
}
match self.0.iter_mut().find(|(n, _)| n == name) {
Some(slot) => slot.1 = value,
None => self.0.push((name.to_owned(), value)),
}
Ok(())
}
fn put_into(&self, obj: &mut Obj) {
for (name, value) in &self.0 {
obj.put(name, Node::Val(value.clone()));
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Party {
tel: Option<String>,
sip: Option<String>,
stir: Option<String>,
mailto: Option<String>,
name: Option<String>,
did: Option<String>,
validation: Option<String>,
gmlpos: Option<String>,
civicaddress: Option<CivicAddress>,
uuid: Option<String>,
kind: Option<String>,
org: Option<String>,
dept: Option<String>,
members: Members,
}
macro_rules! text_setters {
($ty:ident, $base:literal { $($(#[$doc:meta])* $method:ident => $field:ident),* $(,)? }) => {
impl $ty {
$(
$(#[$doc])*
#[doc = concat!("let set = ", $base, ".", stringify!($method), "(\"x\");")]
#[doc = concat!("assert_ne!(set, ", $base, ", \"the member is set\");")]
#[must_use]
pub fn $method(mut self, value: impl Into<String>) -> $ty {
self.$field = Some(value.into());
self
}
)*
}
};
}
text_setters!(Party, "vcon::write::Party::new()" {
tel => tel,
sip => sip,
stir => stir,
mailto => mailto,
name => name,
did => did,
validation => validation,
gmlpos => gmlpos,
uuid => uuid,
kind => kind,
org => org,
dept => dept,
});
impl Party {
#[must_use]
pub fn new() -> Party {
Party::default()
}
#[must_use]
pub fn civicaddress(mut self, address: CivicAddress) -> Party {
self.civicaddress = Some(address);
self
}
pub fn set_member(mut self, name: &str, value: Value) -> Result<Party, WriteError> {
self.members.set(PARTY_MEMBERS, name, value)?;
Ok(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DialogType {
Recording,
RecordingSet,
Text,
Transfer,
Incomplete,
}
impl DialogType {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
DialogType::Recording => "recording",
DialogType::RecordingSet => "recording-set",
DialogType::Text => "text",
DialogType::Transfer => "transfer",
DialogType::Incomplete => "incomplete",
}
}
}
impl fmt::Display for DialogType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Disposition {
NoAnswer,
Congestion,
Failed,
Busy,
HungUp,
VoicemailNoMessage,
}
impl Disposition {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Disposition::NoAnswer => "no-answer",
Disposition::Congestion => "congestion",
Disposition::Failed => "failed",
Disposition::Busy => "busy",
Disposition::HungUp => "hung-up",
Disposition::VoicemailNoMessage => "voicemail-no-message",
}
}
}
impl fmt::Display for Disposition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Event {
Join,
Drop,
Hold,
Unhold,
Mute,
Unmute,
Keydown(String),
Keyup(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PartyHistory {
party: PartyIndex,
time: Date,
event: Event,
}
impl PartyHistory {
#[must_use]
pub fn new(party: PartyIndex, time: Date, event: Event) -> PartyHistory {
PartyHistory { party, time, event }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Channel {
One(PartyIndex),
Mix(Vec<PartyIndex>),
Unrecorded,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Parties {
One(PartyIndex),
Channels(Vec<Channel>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SessionId(Option<(String, String)>);
impl SessionId {
#[must_use]
pub fn new(local: impl Into<String>, remote: impl Into<String>) -> SessionId {
SessionId(Some((local.into(), remote.into())))
}
#[must_use]
pub fn none() -> SessionId {
SessionId(None)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SessionChannel {
One(SessionId),
Mix(Vec<SessionId>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SessionIds {
One(SessionId),
Channels(Vec<SessionChannel>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Dialog {
kind: DialogType,
start: Option<Date>,
duration: Option<Duration>,
parties: Option<Parties>,
originator: Option<PartyIndex>,
recordings: Option<Vec<DialogIndex>>,
recording_set: Option<DialogIndex>,
mediatype: Option<String>,
filename: Option<String>,
content: Content,
disposition: Option<Disposition>,
session_id: Option<SessionIds>,
party_history: Option<Vec<PartyHistory>>,
transferee: Option<PartyIndex>,
transferor: Option<PartyIndex>,
transfer_target: Option<PartyIndex>,
original: Option<DialogIndex>,
consultation: Option<DialogIndex>,
target_dialog: Option<DialogIndex>,
application: Option<String>,
message_id: Option<String>,
members: Members,
removed: bool,
}
text_setters!(Dialog, "vcon::write::Dialog::transfer()" {
mediatype => mediatype,
filename => filename,
application => application,
message_id => message_id,
});
macro_rules! setters {
($ty:ident { $($(#[$doc:meta])* $method:ident($arg:ty) => $field:ident, $base:literal, $value:literal);* $(;)? }) => {
impl $ty {
$(
$(#[$doc])*
#[doc = concat!("let set = ", $base, ".", stringify!($method), "(", $value, ");")]
#[doc = concat!("assert_ne!(set, ", $base, ", \"the member is set\");")]
#[must_use]
pub fn $method(mut self, value: $arg) -> $ty {
self.$field = Some(value);
self
}
)*
}
};
}
setters!(Dialog {
start(Date) => start, "vcon::write::Dialog::transfer()",
"\"2022-06-21T17:53:26Z\".parse().expect(\"RFC 3339\")";
duration(Duration) => duration, "vcon::write::Dialog::transfer()",
"vcon::write::Duration::try_from_secs(60).expect(\"seconds\")";
parties(Parties) => parties, "vcon::write::Dialog::transfer()",
"vcon::write::Parties::One(vcon::write::PartyIndex::new(0))";
originator(PartyIndex) => originator, "vcon::write::Dialog::transfer()",
"vcon::write::PartyIndex::new(1)";
recordings(Vec<DialogIndex>) => recordings, "vcon::write::Dialog::transfer()",
"vec![vcon::write::DialogIndex::new(0)]";
in_recording_set(DialogIndex) => recording_set, "vcon::write::Dialog::transfer()",
"vcon::write::DialogIndex::new(2)";
disposition(Disposition) => disposition, "vcon::write::Dialog::transfer()",
"vcon::write::Disposition::Busy";
session_id(SessionIds) => session_id, "vcon::write::Dialog::transfer()",
"vcon::write::SessionIds::One(vcon::write::SessionId::none())";
party_history(Vec<PartyHistory>) => party_history, "vcon::write::Dialog::transfer()",
"Vec::new()";
transferee(PartyIndex) => transferee, "vcon::write::Dialog::transfer()",
"vcon::write::PartyIndex::new(0)";
transferor(PartyIndex) => transferor, "vcon::write::Dialog::transfer()",
"vcon::write::PartyIndex::new(1)";
transfer_target(PartyIndex) => transfer_target, "vcon::write::Dialog::transfer()",
"vcon::write::PartyIndex::new(2)";
original(DialogIndex) => original, "vcon::write::Dialog::transfer()",
"vcon::write::DialogIndex::new(0)";
consultation(DialogIndex) => consultation, "vcon::write::Dialog::transfer()",
"vcon::write::DialogIndex::new(1)";
target_dialog(DialogIndex) => target_dialog, "vcon::write::Dialog::transfer()",
"vcon::write::DialogIndex::new(2)";
});
impl Dialog {
#[must_use]
pub fn new(kind: DialogType) -> Dialog {
Dialog {
kind,
start: None,
duration: None,
parties: None,
originator: None,
recordings: None,
recording_set: None,
mediatype: None,
filename: None,
content: Content::Absent,
disposition: None,
session_id: None,
party_history: None,
transferee: None,
transferor: None,
transfer_target: None,
original: None,
consultation: None,
target_dialog: None,
application: None,
message_id: None,
members: Members::default(),
removed: false,
}
}
#[must_use]
pub fn recording(content: Content) -> Dialog {
Dialog::new(DialogType::Recording).content(content)
}
#[must_use]
pub fn text(content: Content) -> Dialog {
Dialog::new(DialogType::Text).content(content)
}
#[must_use]
pub fn recording_set(recordings: Vec<DialogIndex>) -> Dialog {
Dialog::new(DialogType::RecordingSet).recordings(recordings)
}
#[must_use]
pub fn transfer() -> Dialog {
Dialog::new(DialogType::Transfer)
}
#[must_use]
pub fn incomplete(disposition: Disposition) -> Dialog {
Dialog::new(DialogType::Incomplete).disposition(disposition)
}
pub fn try_placeholder(kind: DialogType) -> Result<Dialog, WriteError> {
match kind {
DialogType::Recording => Ok(Dialog::new(DialogType::Recording)),
DialogType::Incomplete => Ok(Dialog::incomplete(Disposition::Failed)),
other => Err(WriteError::NoPlaceholder(other)),
}
}
#[must_use]
pub fn content(mut self, content: Content) -> Dialog {
self.content = content;
self
}
pub fn set_member(mut self, name: &str, value: Value) -> Result<Dialog, WriteError> {
self.members.set(DIALOG_MEMBERS, name, value)?;
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Attachment {
purpose: Option<String>,
start: Date,
party: PartyIndex,
dialog: DialogIndex,
mediatype: Option<String>,
filename: Option<String>,
content: Content,
members: Members,
}
text_setters!(Attachment, "vcon::write::Attachment::new(\"2022-06-21T17:55:00Z\".parse().expect(\"RFC 3339\"), vcon::write::PartyIndex::new(0), vcon::write::DialogIndex::new(0), vcon::write::Content::Absent)" {
purpose => purpose,
mediatype => mediatype,
filename => filename,
});
impl Attachment {
#[must_use]
pub fn new(
start: Date,
party: PartyIndex,
dialog: DialogIndex,
content: Content,
) -> Attachment {
Attachment {
purpose: None,
start,
party,
dialog,
mediatype: None,
filename: None,
content,
members: Members::default(),
}
}
pub fn set_member(mut self, name: &str, value: Value) -> Result<Attachment, WriteError> {
self.members.set(ATTACHMENT_MEMBERS, name, value)?;
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Analysis {
kind: String,
dialog: Option<OneOrMany<DialogIndex>>,
attachment: Option<OneOrMany<AttachmentIndex>>,
mediatype: Option<String>,
filename: Option<String>,
vendor: String,
product: Option<String>,
schema: Option<String>,
content: Content,
members: Members,
}
text_setters!(Analysis, "vcon::write::Analysis::new(\"summary\", \"Example\", vcon::write::Content::Absent)" {
mediatype => mediatype,
filename => filename,
product => product,
schema => schema,
});
impl Analysis {
#[must_use]
pub fn new(kind: impl Into<String>, vendor: impl Into<String>, content: Content) -> Analysis {
Analysis {
kind: kind.into(),
dialog: None,
attachment: None,
mediatype: None,
filename: None,
vendor: vendor.into(),
product: None,
schema: None,
content,
members: Members::default(),
}
}
#[must_use]
pub fn dialog(mut self, dialog: DialogIndex) -> Analysis {
self.dialog = Some(OneOrMany::One(dialog));
self
}
#[must_use]
pub fn dialogs(mut self, dialogs: Vec<DialogIndex>) -> Analysis {
self.dialog = Some(OneOrMany::Many(dialogs));
self
}
#[must_use]
pub fn attachment(mut self, attachment: AttachmentIndex) -> Analysis {
self.attachment = Some(OneOrMany::One(attachment));
self
}
#[must_use]
pub fn attachments(mut self, attachments: Vec<AttachmentIndex>) -> Analysis {
self.attachment = Some(OneOrMany::Many(attachments));
self
}
pub fn set_member(mut self, name: &str, value: Value) -> Result<Analysis, WriteError> {
self.members.set(ANALYSIS_MEMBERS, name, value)?;
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Redacted {
kind: String,
prior: Option<Reference>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Vcon {
uuid: Uuid,
extensions: Vec<String>,
critical: Vec<String>,
created_at: Date,
updated_at: Option<Date>,
subject: Option<String>,
redacted: Option<Redacted>,
amended: Option<Reference>,
parties: Vec<Option<Party>>,
dialog: Vec<Dialog>,
analysis: Vec<Option<Analysis>>,
attachments: Vec<Option<Attachment>>,
members: Members,
}
impl Vcon {
#[must_use]
pub fn new(uuid: Uuid, created_at: Date) -> Vcon {
Vcon {
uuid,
extensions: Vec::new(),
critical: Vec::new(),
created_at,
updated_at: None,
subject: None,
redacted: None,
amended: None,
parties: Vec::new(),
dialog: Vec::new(),
analysis: Vec::new(),
attachments: Vec::new(),
members: Members::default(),
}
}
#[must_use]
pub fn uuid(&self) -> Uuid {
self.uuid
}
pub fn set_subject(&mut self, subject: impl Into<String>) {
self.subject = Some(subject.into());
}
pub fn set_updated_at(&mut self, updated_at: Date) {
self.updated_at = Some(updated_at);
}
pub fn use_extension(&mut self, name: &str) {
if !self.extensions.iter().any(|e| e == name) {
self.extensions.push(name.to_owned());
}
}
pub fn use_critical_extension(&mut self, name: &str) {
self.use_extension(name);
if !self.critical.iter().any(|e| e == name) {
self.critical.push(name.to_owned());
}
}
pub fn set_member(&mut self, name: &str, value: Value) -> Result<(), WriteError> {
self.members.set(TOP_MEMBERS, name, value)
}
pub fn set_amended(&mut self, prior: Reference) {
self.amended = Some(prior);
}
pub fn set_redacted(&mut self, kind: impl Into<String>, prior: Reference) {
self.redacted = Some(Redacted {
kind: kind.into(),
prior: Some(prior),
});
}
pub fn set_redacted_without_prior(&mut self, kind: impl Into<String>) {
self.redacted = Some(Redacted {
kind: kind.into(),
prior: None,
});
}
pub fn add_party(&mut self, party: Party) -> PartyIndex {
self.parties.push(Some(party));
PartyIndex(self.parties.len() - 1)
}
pub fn add_dialog(&mut self, dialog: Dialog) -> DialogIndex {
self.dialog.push(dialog);
DialogIndex(self.dialog.len() - 1)
}
pub fn add_attachment(&mut self, attachment: Attachment) -> AttachmentIndex {
self.attachments.push(Some(attachment));
AttachmentIndex(self.attachments.len() - 1)
}
pub fn add_analysis(&mut self, analysis: Analysis) -> AnalysisIndex {
self.analysis.push(Some(analysis));
AnalysisIndex(self.analysis.len() - 1)
}
pub fn replace_party(&mut self, i: PartyIndex, party: Party) -> Result<(), WriteError> {
*slot(&mut self.parties, i.0, "parties")? = Some(party);
Ok(())
}
pub fn replace_dialog(&mut self, i: DialogIndex, dialog: Dialog) -> Result<(), WriteError> {
*slot(&mut self.dialog, i.0, "dialog")? = dialog;
Ok(())
}
pub fn replace_attachment(
&mut self,
i: AttachmentIndex,
attachment: Attachment,
) -> Result<(), WriteError> {
*slot(&mut self.attachments, i.0, "attachments")? = Some(attachment);
Ok(())
}
pub fn replace_analysis(
&mut self,
i: AnalysisIndex,
analysis: Analysis,
) -> Result<(), WriteError> {
*slot(&mut self.analysis, i.0, "analysis")? = Some(analysis);
Ok(())
}
pub fn remove_party(&mut self, i: PartyIndex) -> Result<(), WriteError> {
*slot(&mut self.parties, i.0, "parties")? = None;
Ok(())
}
pub fn remove_dialog(&mut self, i: DialogIndex) -> Result<(), WriteError> {
let entry = slot(&mut self.dialog, i.0, "dialog")?;
let mut placeholder = Dialog::new(entry.kind);
if entry.kind == DialogType::Incomplete {
placeholder.disposition = entry.disposition;
}
placeholder.removed = true;
*entry = placeholder;
Ok(())
}
pub fn remove_attachment(&mut self, i: AttachmentIndex) -> Result<(), WriteError> {
*slot(&mut self.attachments, i.0, "attachments")? = None;
Ok(())
}
pub fn remove_analysis(&mut self, i: AnalysisIndex) -> Result<(), WriteError> {
*slot(&mut self.analysis, i.0, "analysis")? = None;
Ok(())
}
pub fn amend(prior: &Vcon, uuid: Uuid, prior_ref: Reference) -> Result<Vcon, WriteError> {
let mut new = prior.copy_as(uuid)?;
new.amended = Some(prior_ref);
Ok(new)
}
pub fn redact(
prior: &Vcon,
uuid: Uuid,
kind: &str,
prior_ref: Reference,
) -> Result<Vcon, WriteError> {
let mut new = prior.copy_as(uuid)?;
new.redacted = Some(Redacted {
kind: kind.to_owned(),
prior: Some(prior_ref),
});
Ok(new)
}
pub fn to_json(&self) -> Result<Vec<u8>, WriteError> {
emit::render(&self.node()?)
}
pub fn to_gzip(&self) -> Result<Vec<u8>, WriteError> {
Ok(gzip(&self.to_json()?))
}
fn copy_as(&self, uuid: Uuid) -> Result<Vcon, WriteError> {
if uuid == self.uuid {
return Err(WriteError::SameUuid);
}
let mut new = self.clone();
new.uuid = uuid;
new.amended = None;
new.redacted = None;
Ok(new)
}
fn node(&self) -> Result<Node, WriteError> {
let counts = Counts {
parties: self.parties.len(),
dialog: self.dialog.len(),
attachments: self.attachments.len(),
};
let redacted = self.redacted.is_some();
let mut top = Obj::default();
top.put("uuid", Node::str(&self.uuid.hyphenated().to_string()));
if !self.extensions.is_empty() {
top.put("extensions", strings(&self.extensions));
}
if !self.critical.is_empty() {
top.put("critical", strings(&self.critical));
}
top.put("created_at", Node::str(self.created_at.as_str()));
if let Some(date) = &self.updated_at {
top.put("updated_at", Node::str(date.as_str()));
}
top.put_str("subject", &self.subject);
if self.redacted.is_some() && self.amended.is_some() {
return Err(WriteError::BothLinks);
}
if let Some(r) = &self.redacted {
let mut obj = Obj::default();
if let Some(prior) = &r.prior {
self.check_reference(prior)?;
if let Some(uuid) = prior.uuid {
obj.put("uuid", Node::str(&uuid.hyphenated().to_string()));
}
obj.put("type", Node::str(&r.kind));
reference_url(prior, &mut obj);
} else {
obj.put("type", Node::str(&r.kind));
}
top.put("redacted", obj.done());
}
if let Some(prior) = &self.amended {
self.check_reference(prior)?;
let mut obj = Obj::default();
if let Some(uuid) = prior.uuid {
obj.put("uuid", Node::str(&uuid.hyphenated().to_string()));
}
reference_url(prior, &mut obj);
top.put("amended", obj.done());
}
let parties = self
.parties
.iter()
.map(|p| p.as_ref().map_or(Node::Obj(Vec::new()), Party::node))
.collect();
top.put("parties", Node::Arr(parties));
let mut dialog = Vec::new();
for (i, d) in self.dialog.iter().enumerate() {
dialog.push(d.node(&format!("/dialog/{i}"), redacted, &counts)?);
}
top.put("dialog", Node::Arr(dialog));
let mut analysis = Vec::new();
for (i, a) in self.analysis.iter().enumerate() {
analysis.push(match a {
Some(a) => a.node(&format!("/analysis/{i}"), redacted, &counts)?,
None => Node::Obj(Vec::new()),
});
}
top.put("analysis", Node::Arr(analysis));
let mut attachments = Vec::new();
for (i, a) in self.attachments.iter().enumerate() {
attachments.push(match a {
Some(a) => a.node(&format!("/attachments/{i}"), redacted, &counts)?,
None => Node::Obj(Vec::new()),
});
}
top.put("attachments", Node::Arr(attachments));
self.members.put_into(&mut top);
Ok(top.done())
}
fn check_reference(&self, r: &Reference) -> Result<(), WriteError> {
if let Some(url) = &r.url
&& (!is_https(url) || r.hashes.is_empty())
{
return Err(WriteError::BadReference);
}
if r.uuid == Some(self.uuid) {
return Err(WriteError::SameUuid);
}
Ok(())
}
}
fn gzip(bytes: &[u8]) -> Vec<u8> {
let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder
.write_all(bytes)
.expect("writing to a Vec does not fail");
encoder.finish().expect("writing to a Vec does not fail")
}
fn slot<'a, T>(list: &'a mut [T], i: usize, name: &str) -> Result<&'a mut T, WriteError> {
list.get_mut(i).ok_or_else(|| WriteError::UnknownIndex {
path: format!("/{name}/{i}"),
})
}
struct Counts {
parties: usize,
dialog: usize,
attachments: usize,
}
fn index(i: usize, count: usize, path: String) -> Result<Node, WriteError> {
if i >= count {
return Err(WriteError::UnknownIndex { path });
}
Ok(Node::Raw(i.to_string()))
}
fn strings(list: &[String]) -> Node {
Node::Arr(list.iter().map(|s| Node::str(s)).collect())
}
fn hashes(list: &[ContentHash]) -> Node {
match list {
[one] => Node::str(one.as_str()),
many => Node::Arr(many.iter().map(|h| Node::str(h.as_str())).collect()),
}
}
fn reference_url(r: &Reference, obj: &mut Obj) {
if let Some(url) = &r.url {
obj.put("url", Node::str(url));
obj.put("content_hash", hashes(&r.hashes));
}
}
fn is_https(url: &str) -> bool {
let Some(scheme) = url.get(..8) else {
return false;
};
let host = url[8..].split(['/', '?', '#']).next().unwrap_or("");
scheme.eq_ignore_ascii_case("https://") && !host.is_empty()
}
impl Content {
fn member(&self) -> Option<&'static str> {
match self {
Content::Inline(_) => Some("body"),
Content::External { .. } => Some("url"),
Content::HashOnly(_) => Some("content_hash"),
Content::Absent => None,
}
}
fn put_into(
&self,
path: &str,
redacted: bool,
absent_ok: bool,
obj: &mut Obj,
) -> Result<(), WriteError> {
let bad = || WriteError::BadContent {
path: path.to_owned(),
};
match self {
Content::Inline(body) => {
let (node, encoding) = match body {
Body::Bytes(bytes) => (Node::Str(URL_SAFE_NO_PAD.encode(bytes)), "base64url"),
Body::Text(text) => (Node::str(text), "none"),
Body::Json(value) => (Node::Val(value.clone()), "json"),
};
obj.put("body", node);
obj.put("encoding", Node::str(encoding));
}
Content::External { url, hashes: list } => {
if !is_https(url) || list.is_empty() {
return Err(bad());
}
obj.put("url", Node::str(url));
obj.put("content_hash", hashes(list));
}
Content::HashOnly(list) => {
if !redacted || list.is_empty() {
return Err(bad());
}
obj.put("content_hash", hashes(list));
}
Content::Absent => {
if !(redacted || absent_ok) {
return Err(bad());
}
}
}
Ok(())
}
}
impl Party {
fn node(&self) -> Node {
let mut obj = Obj::default();
obj.put_str("tel", &self.tel);
obj.put_str("sip", &self.sip);
obj.put_str("stir", &self.stir);
obj.put_str("mailto", &self.mailto);
obj.put_str("name", &self.name);
obj.put_str("did", &self.did);
obj.put_str("validation", &self.validation);
obj.put_str("gmlpos", &self.gmlpos);
if let Some(address) = &self.civicaddress {
let mut civic = Obj::default();
for name in CIVIC_ORDER {
if let Some((_, value)) = address.members.iter().find(|(n, _)| n == name) {
civic.put(name, Node::str(value));
}
}
obj.put("civicaddress", civic.done());
}
obj.put_str("uuid", &self.uuid);
obj.put_str("type", &self.kind);
obj.put_str("org", &self.org);
obj.put_str("dept", &self.dept);
self.members.put_into(&mut obj);
obj.done()
}
}
impl Duration {
fn node(&self) -> Node {
match self.0 {
Seconds::Whole(n) => Node::Raw(n.to_string()),
Seconds::Fraction(f) => Node::Raw(
serde_json::Number::from_f64(f).map_or_else(String::new, |n| n.to_string()),
),
}
}
}
impl SessionId {
fn node(&self) -> Node {
let mut obj = Obj::default();
if let Some((local, remote)) = &self.0 {
obj.put("local", Node::str(local));
obj.put("remote", Node::str(remote));
}
obj.done()
}
}
impl Event {
fn put_into(&self, obj: &mut Obj) {
let (event, button) = match self {
Event::Join => ("join", None),
Event::Drop => ("drop", None),
Event::Hold => ("hold", None),
Event::Unhold => ("unhold", None),
Event::Mute => ("mute", None),
Event::Unmute => ("unmute", None),
Event::Keydown(b) => ("keydown", Some(b)),
Event::Keyup(b) => ("keyup", Some(b)),
};
obj.put("event", Node::str(event));
if let Some(b) = button {
obj.put("button", Node::str(b));
}
}
}
fn forbidden(kind: DialogType, name: &str) -> bool {
use DialogType::{Incomplete, Recording, RecordingSet, Text, Transfer};
let x: &[DialogType] = match name {
"parties" | "originator" | "session_id" | "party_history" => &[Transfer],
"recordings" => &[Recording, Text, Transfer, Incomplete],
"recording_set" => &[RecordingSet, Text, Transfer, Incomplete],
"mediatype" | "filename" | "body" | "url" | "content_hash" | "message_id" => {
&[RecordingSet, Transfer, Incomplete]
}
"transferee" | "transferor" | "transfer_target" | "original" | "consultation"
| "target_dialog" => &[Recording, RecordingSet, Text, Incomplete],
_ => &[],
};
x.contains(&kind)
}
impl Dialog {
fn is_bare(&self) -> bool {
*self == Dialog::new(self.kind)
}
fn members_set(&self) -> Vec<&'static str> {
let mut set = Vec::new();
let mut add = |name, present: bool| {
if present {
set.push(name);
}
};
add("parties", self.parties.is_some());
add("originator", self.originator.is_some());
add("recordings", self.recordings.is_some());
add("recording_set", self.recording_set.is_some());
add("mediatype", self.mediatype.is_some());
add("filename", self.filename.is_some());
if let Some(name) = self.content.member() {
add(name, true);
}
add("session_id", self.session_id.is_some());
add("party_history", self.party_history.is_some());
add("transferee", self.transferee.is_some());
add("transferor", self.transferor.is_some());
add("transfer_target", self.transfer_target.is_some());
add("original", self.original.is_some());
add("consultation", self.consultation.is_some());
add("target_dialog", self.target_dialog.is_some());
add("message_id", self.message_id.is_some());
set
}
fn node(&self, path: &str, redacted: bool, counts: &Counts) -> Result<Node, WriteError> {
let missing = |name| WriteError::MissingParameter {
path: path.to_owned(),
name,
};
for name in self.members_set() {
if forbidden(self.kind, name) {
return Err(WriteError::ForbiddenParameter {
path: path.to_owned(),
name,
});
}
}
if !self.removed {
if self.kind == DialogType::RecordingSet && self.recordings.is_none() {
return Err(missing("recordings"));
}
if self.kind == DialogType::Incomplete && self.disposition.is_none() {
return Err(missing("disposition"));
}
if matches!(self.kind, DialogType::Recording | DialogType::Text)
&& matches!(self.content, Content::Inline(_))
&& self.mediatype.is_none()
{
return Err(missing("mediatype"));
}
}
let parties = |i: PartyIndex, at: String| index(i.0, counts.parties, at);
let dialogs = |i: DialogIndex, at: String| index(i.0, counts.dialog, at);
let mut obj = Obj::default();
obj.put("type", Node::str(self.kind.as_str()));
if let Some(date) = &self.start {
obj.put("start", Node::str(date.as_str()));
}
if let Some(d) = &self.duration {
obj.put("duration", d.node());
}
if let Some(p) = &self.parties {
let at = format!("{path}/parties");
let node = match p {
Parties::One(i) => parties(*i, at)?,
Parties::Channels(channels) => {
let mut list = Vec::new();
for (j, c) in channels.iter().enumerate() {
list.push(match c {
Channel::One(i) => parties(*i, format!("{at}/{j}"))?,
Channel::Mix(mix) => {
let mut inner = Vec::new();
for (k, i) in mix.iter().enumerate() {
inner.push(parties(*i, format!("{at}/{j}/{k}"))?);
}
Node::Arr(inner)
}
Channel::Unrecorded => Node::Raw("null".into()),
});
}
Node::Arr(list)
}
};
obj.put("parties", node);
}
if let Some(i) = self.originator {
obj.put("originator", parties(i, format!("{path}/originator"))?);
}
if let Some(list) = &self.recordings {
let mut out = Vec::new();
for (j, i) in list.iter().enumerate() {
out.push(dialogs(*i, format!("{path}/recordings/{j}"))?);
}
obj.put("recordings", Node::Arr(out));
}
if let Some(i) = self.recording_set {
obj.put(
"recording_set",
dialogs(i, format!("{path}/recording_set"))?,
);
}
obj.put_str("mediatype", &self.mediatype);
obj.put_str("filename", &self.filename);
let absent_ok = self.removed
|| (self.kind == DialogType::Recording && self.is_bare())
|| matches!(
self.kind,
DialogType::RecordingSet | DialogType::Transfer | DialogType::Incomplete
);
self.content.put_into(path, redacted, absent_ok, &mut obj)?;
if let Some(d) = self.disposition {
obj.put("disposition", Node::str(d.as_str()));
}
if let Some(s) = &self.session_id {
let node = match s {
SessionIds::One(id) => id.node(),
SessionIds::Channels(list) => Node::Arr(
list.iter()
.map(|c| match c {
SessionChannel::One(id) => id.node(),
SessionChannel::Mix(ids) => {
Node::Arr(ids.iter().map(SessionId::node).collect())
}
})
.collect(),
),
};
obj.put("session_id", node);
}
if let Some(history) = &self.party_history {
let mut out = Vec::new();
for (j, h) in history.iter().enumerate() {
let mut entry = Obj::default();
entry.put(
"party",
parties(h.party, format!("{path}/party_history/{j}/party"))?,
);
entry.put("time", Node::str(h.time.as_str()));
h.event.put_into(&mut entry);
out.push(entry.done());
}
obj.put("party_history", Node::Arr(out));
}
for (name, value) in [
("transferee", self.transferee),
("transferor", self.transferor),
("transfer_target", self.transfer_target),
] {
if let Some(i) = value {
obj.put(name, parties(i, format!("{path}/{name}"))?);
}
}
for (name, value) in [
("original", self.original),
("consultation", self.consultation),
("target_dialog", self.target_dialog),
] {
if let Some(i) = value {
obj.put(name, dialogs(i, format!("{path}/{name}"))?);
}
}
obj.put_str("application", &self.application);
obj.put_str("message_id", &self.message_id);
self.members.put_into(&mut obj);
Ok(obj.done())
}
}
impl Attachment {
fn node(&self, path: &str, redacted: bool, counts: &Counts) -> Result<Node, WriteError> {
if matches!(self.content, Content::Inline(_)) && self.mediatype.is_none() {
return Err(WriteError::MissingParameter {
path: path.to_owned(),
name: "mediatype",
});
}
let mut obj = Obj::default();
obj.put_str("purpose", &self.purpose);
obj.put("start", Node::str(self.start.as_str()));
obj.put(
"party",
index(self.party.0, counts.parties, format!("{path}/party"))?,
);
obj.put(
"dialog",
index(self.dialog.0, counts.dialog, format!("{path}/dialog"))?,
);
obj.put_str("mediatype", &self.mediatype);
obj.put_str("filename", &self.filename);
self.content.put_into(path, redacted, false, &mut obj)?;
self.members.put_into(&mut obj);
Ok(obj.done())
}
}
fn one_or_many<T: Copy>(
value: &OneOrMany<T>,
get: fn(T) -> usize,
count: usize,
at: &str,
) -> Result<Node, WriteError> {
match value {
OneOrMany::One(i) => index(get(*i), count, at.to_owned()),
OneOrMany::Many(list) => {
let mut out = Vec::new();
for (j, i) in list.iter().enumerate() {
out.push(index(get(*i), count, format!("{at}/{j}"))?);
}
Ok(Node::Arr(out))
}
}
}
impl Analysis {
fn node(&self, path: &str, redacted: bool, counts: &Counts) -> Result<Node, WriteError> {
let mut obj = Obj::default();
obj.put("type", Node::str(&self.kind));
if let Some(d) = &self.dialog {
obj.put(
"dialog",
one_or_many(
d,
DialogIndex::get,
counts.dialog,
&format!("{path}/dialog"),
)?,
);
}
if let Some(a) = &self.attachment {
obj.put(
"attachment",
one_or_many(
a,
AttachmentIndex::get,
counts.attachments,
&format!("{path}/attachment"),
)?,
);
}
obj.put_str("mediatype", &self.mediatype);
obj.put_str("filename", &self.filename);
obj.put("vendor", Node::str(&self.vendor));
obj.put_str("product", &self.product);
obj.put_str("schema", &self.schema);
self.content.put_into(path, redacted, false, &mut obj)?;
self.members.put_into(&mut obj);
Ok(obj.done())
}
}
#[must_use]
pub fn uuid_v8(unix_ms: u64, rand_a: u16, fqhn: &str) -> Uuid {
let digest = sha1_smol::Sha1::from(fqhn).digest().bytes();
let mut high = [0u8; 8];
high.copy_from_slice(&digest[..8]);
let rand_b = u64::from_be_bytes(high) >> 2;
let value = (u128::from(unix_ms & 0xffff_ffff_ffff) << 80)
| (0x8 << 76)
| (u128::from(rand_a & 0xfff) << 64)
| (0b10 << 62)
| u128::from(rand_b);
Uuid::from_u128(value)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[must_use]
pub enum WriteError {
UnknownIndex {
path: String,
},
MissingParameter {
path: String,
name: &'static str,
},
ForbiddenParameter {
path: String,
name: &'static str,
},
BadContent {
path: String,
},
BadHash,
BadDate,
BadDuration,
BadReference,
BothLinks,
SameUuid,
CoreMember(String),
NoPlaceholder(DialogType),
TooDeep,
NotSigned,
TooManyLayers,
NoSigner,
NoRecipient,
BadChain,
Crypto(CryptoError),
}
impl fmt::Display for WriteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WriteError::UnknownIndex { path } => {
write!(f, "the index at {path} refers to no entry")
}
WriteError::MissingParameter { path, name } => {
write!(f, "{path} lacks {name}, which its type requires")
}
WriteError::ForbiddenParameter { path, name } => {
write!(f, "{path} has {name}, which its type does not allow")
}
WriteError::BadContent { path } => {
write!(f, "the content of {path} cannot be written there")
}
WriteError::BadHash => f.write_str("a content hash is malformed"),
WriteError::BadDate => f.write_str("a date is not RFC 3339 with an offset"),
WriteError::BadDuration => {
f.write_str("a duration is not an unsigned number of seconds")
}
WriteError::BadReference => {
f.write_str("a reference to a prior vCon is incomplete or its url is not https")
}
WriteError::BothLinks => f.write_str("both amended and redacted are set"),
WriteError::SameUuid => f.write_str("a vCon refers to a vCon with its own uuid"),
WriteError::CoreMember(name) => write!(f, "the member name {name} is reserved"),
WriteError::NoPlaceholder(kind) => write!(f, "no placeholder is made of type {kind}"),
WriteError::TooDeep => write!(
f,
"the output would nest more than {} arrays and objects",
crate::read::MAX_JSON_DEPTH
),
WriteError::NotSigned => f.write_str("the input is not a signed vCon"),
WriteError::TooManyLayers => write!(
f,
"signing again would pass {} signed layers",
crate::read::MAX_SIGNED_LAYERS
),
WriteError::NoSigner => f.write_str("no signer was given"),
WriteError::NoRecipient => f.write_str("no recipient was given"),
WriteError::BadChain => {
f.write_str("a signer's chain is an empty x5c or an x5u that is not https")
}
WriteError::Crypto(e) => write!(f, "a signer or encrypter failed: {e}"),
}
}
}
impl std::error::Error for WriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
WriteError::Crypto(e) => Some(e),
_ => None,
}
}
}