use std::borrow::Cow;
use std::fmt;
use std::io;
use rig_core::conversation::{
Aside, AsideId, Author, Change, ChangeRecord, Conversation, Entry, Halt, Lineage, MessageId,
};
use rig_core::redact;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const FORMAT: &str = "rig-session/4";
pub const SESSION: &str = "session";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Stamp {
pub n: u64,
pub seq: u64,
pub t: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("this session was made by another version of rig; start a new session")]
OldLayout,
#[error(
"this session was made by another version of rig; start a new session (the log `{log}` \
is in format {}, not {FORMAT})",
.found.as_deref().unwrap_or("(none)")
)]
Format {
log: String,
found: Option<String>,
},
#[error("the log `{log}` cannot be read at line {line}: {why}")]
Record {
log: String,
line: usize,
why: String,
},
#[error("the log `{log}` changed since it was read")]
Changed {
log: String,
},
}
impl From<SessionError> for io::Error {
fn from(error: SessionError) -> Self {
match error {
SessionError::Io(error) => error,
error => io::Error::new(io::ErrorKind::InvalidData, error),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Repair {
TornTail {
log: String,
kept: u64,
cut: u64,
set_aside: Option<String>,
},
LostImage {
log: String,
why: String,
},
}
impl fmt::Display for Repair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TornTail {
log,
cut,
set_aside,
..
} => {
write!(f, "the log `{log}` ended in a line cut short ({cut} bytes)")?;
match set_aside {
Some(file) => write!(f, ", set aside in {file}"),
None => Ok(()),
}
}
Self::LostImage { log, why } => write!(f, "an image of the log `{log}` is lost: {why}"),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub(crate) struct Header {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default)]
pub log: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(default)]
pub lineage: Lineage,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rewrites: Vec<LogRewrite>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogRewrite {
pub t: u64,
pub by: Author,
}
impl Header {
pub fn new(log: &str, parent: Option<&str>) -> Self {
Self {
format: Some(FORMAT.to_owned()),
log: log.to_owned(),
parent: parent.map(str::to_owned),
lineage: Lineage::fresh(),
rewrites: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum Meta {
Header(Header),
State {
key: String,
#[serde(default)]
value: Option<Value>,
},
Retired,
}
pub(crate) enum Body {
Meta(Meta),
Change(ChangeRecord),
}
#[derive(Deserialize)]
struct Head<'a> {
seq: u64,
n: u64,
t: u64,
#[serde(rename = "type", borrow)]
kind: std::borrow::Cow<'a, str>,
}
const CHANGES: [&str; 5] = ["push", "splice", "halt", "aside", "reset"];
pub(crate) fn stamp_of(line: &[u8]) -> Result<(Stamp, bool), String> {
let head: Head<'_> = serde_json::from_slice(line).map_err(|error| error.to_string())?;
Ok((head.stamp(), CHANGES.contains(&&*head.kind)))
}
impl Head<'_> {
fn stamp(&self) -> Stamp {
Stamp {
n: self.n,
seq: self.seq,
t: self.t,
}
}
}
#[derive(Deserialize)]
struct Read<'a> {
seq: u64,
n: u64,
t: u64,
#[serde(rename = "type", borrow)]
kind: std::borrow::Cow<'a, str>,
#[serde(default)]
by: Option<Author>,
entries: Option<Vec<Entry>>,
from: Option<usize>,
to: Option<usize>,
removed: Option<Vec<MessageId>>,
with: Option<Vec<Entry>>,
#[serde(default)]
halt: Option<Halt>,
id: Option<AsideId>,
#[serde(default)]
aside: Option<Aside>,
state: Option<Box<Conversation>>,
}
impl Read<'_> {
fn change(self) -> Result<Change, String> {
fn given<T>(field: Option<T>, name: &str) -> Result<T, String> {
field.ok_or_else(|| format!("missing field `{name}`"))
}
Ok(match &*self.kind {
"push" => Change::Push {
entries: given(self.entries, "entries")?,
},
"splice" => Change::Splice {
from: given(self.from, "from")?,
to: given(self.to, "to")?,
removed: given(self.removed, "removed")?,
with: given(self.with, "with")?,
},
"halt" => Change::Halt { halt: self.halt },
"aside" => Change::Aside {
id: given(self.id, "id")?,
aside: self.aside,
},
"reset" => Change::Reset {
state: given(self.state, "state")?,
},
other => return Err(format!("unknown record type {other:?}")),
})
}
}
pub(crate) fn parse(line: &[u8]) -> Result<(Stamp, Body), String> {
let mut read: Read<'_> = serde_json::from_slice(line).map_err(|error| error.to_string())?;
let stamp = Stamp {
n: read.n,
seq: read.seq,
t: read.t,
};
let body = match &*read.kind {
"header" | "state" | "retired" => {
Body::Meta(serde_json::from_slice(line).map_err(|error| error.to_string())?)
}
_ => {
let by = read.by.take();
Body::Change(ChangeRecord {
change: read.change()?,
by,
at: Some(stamp.t),
})
}
};
Ok((stamp, body))
}
#[derive(Serialize)]
struct Line<'a> {
seq: u64,
n: u64,
t: u64,
#[serde(skip_serializing_if = "Option::is_none")]
by: Option<&'a Author>,
#[serde(flatten)]
body: Out<'a>,
}
#[derive(Serialize)]
#[serde(untagged)]
enum Out<'a> {
Meta(&'a Meta),
Change(&'a Change),
}
pub(crate) fn write_meta(out: &mut Vec<u8>, stamp: Stamp, meta: &Meta) -> serde_json::Result<()> {
write(out, stamp, None, Out::Meta(meta))
}
pub(crate) fn write_change(
out: &mut Vec<u8>,
stamp: Stamp,
by: Option<&Author>,
change: &Change,
) -> serde_json::Result<()> {
write(out, stamp, by, Out::Change(change))
}
fn write(
out: &mut Vec<u8>,
stamp: Stamp,
by: Option<&Author>,
body: Out<'_>,
) -> serde_json::Result<()> {
let line = Line {
seq: stamp.seq,
n: stamp.n,
t: stamp.t,
by,
body,
};
let start = out.len();
serde_json::to_writer(&mut *out, &line)?;
let written = out
.get(start..)
.and_then(|line| std::str::from_utf8(line).ok());
if let Some(Cow::Owned(redacted)) = written.map(redact::json_text) {
out.truncate(start);
out.extend_from_slice(redacted.as_bytes());
}
out.push(b'\n');
Ok(())
}
#[derive(Clone, Debug, Default)]
pub(crate) struct LogLines {
bytes: Vec<u8>,
ends: Vec<usize>,
}
impl LogLines {
pub fn parse(bytes: Vec<u8>) -> Self {
let mut ends: Vec<usize> = memchr::memchr_iter(b'\n', &bytes)
.map(|at| at + 1)
.collect();
let last_start = ends
.len()
.checked_sub(2)
.and_then(|at| ends.get(at))
.copied();
let last = ends
.last()
.and_then(|end| bytes.get(last_start.unwrap_or(0)..end.saturating_sub(1)));
if last.is_some_and(|line| {
!line.is_empty() && serde_json::from_slice::<serde::de::IgnoredAny>(line).is_err()
}) {
ends.pop();
}
Self { bytes, ends }
}
pub fn iter(&self) -> impl Iterator<Item = (usize, u64, &[u8])> {
let starts = std::iter::once(0).chain(self.ends.iter().copied());
starts
.zip(self.ends.iter().copied())
.enumerate()
.filter_map(|(at, (start, end))| {
let line = self.bytes.get(start..end.saturating_sub(1))?;
(!line.is_empty()).then_some((at + 1, end as u64, line))
})
}
pub fn len(&self) -> u64 {
self.ends.last().map_or(0, |end| *end as u64)
}
pub fn tail(&self) -> &[u8] {
let len = usize::try_from(self.len()).unwrap_or(usize::MAX);
self.bytes.get(len..).unwrap_or_default()
}
pub fn prefix(&self, len: u64) -> &[u8] {
let len = usize::try_from(len.min(self.len())).unwrap_or(0);
self.bytes.get(..len).unwrap_or_default()
}
}
#[cfg(test)]
mod tests;