1use std::borrow::Cow;
28use std::fmt;
29use std::io;
30
31use rig_core::conversation::{
32 Aside, AsideId, Author, Change, ChangeRecord, Conversation, Entry, Halt, Lineage, MessageId,
33};
34use rig_core::redact;
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37
38pub const FORMAT: &str = "rig-session/4";
40
41pub const SESSION: &str = "session";
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
50pub struct Stamp {
51 pub n: u64,
55 pub seq: u64,
57 pub t: u64,
59}
60
61#[derive(Debug, thiserror::Error)]
64pub enum SessionError {
65 #[error(transparent)]
67 Io(#[from] io::Error),
68 #[error("this session was made by another version of rig; start a new session")]
71 OldLayout,
72 #[error(
75 "this session was made by another version of rig; start a new session (the log `{log}` \
76 is in format {}, not {FORMAT})",
77 .found.as_deref().unwrap_or("(none)")
78 )]
79 Format {
80 log: String,
82 found: Option<String>,
84 },
85 #[error("the log `{log}` cannot be read at line {line}: {why}")]
87 Record {
88 log: String,
90 line: usize,
92 why: String,
94 },
95 #[error("the log `{log}` changed since it was read")]
98 Changed {
99 log: String,
101 },
102}
103
104impl From<SessionError> for io::Error {
105 fn from(error: SessionError) -> Self {
109 match error {
110 SessionError::Io(error) => error,
111 error => io::Error::new(io::ErrorKind::InvalidData, error),
112 }
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
118pub enum Repair {
119 TornTail {
124 log: String,
126 kept: u64,
128 cut: u64,
130 set_aside: Option<String>,
132 },
133 LostImage {
135 log: String,
137 why: String,
139 },
140}
141
142impl fmt::Display for Repair {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::TornTail {
146 log,
147 cut,
148 set_aside,
149 ..
150 } => {
151 write!(f, "the log `{log}` ended in a line cut short ({cut} bytes)")?;
152 match set_aside {
153 Some(file) => write!(f, ", set aside in {file}"),
154 None => Ok(()),
155 }
156 }
157 Self::LostImage { log, why } => write!(f, "an image of the log `{log}` is lost: {why}"),
158 }
159 }
160}
161
162#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
164pub(crate) struct Header {
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub format: Option<String>,
167 #[serde(default)]
168 pub log: String,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub parent: Option<String>,
172 #[serde(default)]
175 pub lineage: Lineage,
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub rewrites: Vec<LogRewrite>,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub struct LogRewrite {
185 pub t: u64,
187 pub by: Author,
189}
190
191impl Header {
192 pub fn new(log: &str, parent: Option<&str>) -> Self {
194 Self {
195 format: Some(FORMAT.to_owned()),
196 log: log.to_owned(),
197 parent: parent.map(str::to_owned),
198 lineage: Lineage::fresh(),
199 rewrites: Vec::new(),
200 }
201 }
202}
203
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
206#[serde(tag = "type", rename_all = "snake_case")]
207pub(crate) enum Meta {
208 Header(Header),
209 State {
210 key: String,
211 #[serde(default)]
212 value: Option<Value>,
213 },
214 Retired,
215}
216
217pub(crate) enum Body {
219 Meta(Meta),
220 Change(ChangeRecord),
222}
223
224#[derive(Deserialize)]
226struct Head<'a> {
227 seq: u64,
228 n: u64,
229 t: u64,
230 #[serde(rename = "type", borrow)]
231 kind: std::borrow::Cow<'a, str>,
232}
233
234const CHANGES: [&str; 5] = ["push", "splice", "halt", "aside", "reset"];
236
237pub(crate) fn stamp_of(line: &[u8]) -> Result<(Stamp, bool), String> {
240 let head: Head<'_> = serde_json::from_slice(line).map_err(|error| error.to_string())?;
241 Ok((head.stamp(), CHANGES.contains(&&*head.kind)))
242}
243
244impl Head<'_> {
245 fn stamp(&self) -> Stamp {
246 Stamp {
247 n: self.n,
248 seq: self.seq,
249 t: self.t,
250 }
251 }
252}
253
254#[derive(Deserialize)]
259struct Read<'a> {
260 seq: u64,
261 n: u64,
262 t: u64,
263 #[serde(rename = "type", borrow)]
264 kind: std::borrow::Cow<'a, str>,
265 #[serde(default)]
266 by: Option<Author>,
267 entries: Option<Vec<Entry>>,
268 from: Option<usize>,
269 to: Option<usize>,
270 removed: Option<Vec<MessageId>>,
271 with: Option<Vec<Entry>>,
272 #[serde(default)]
273 halt: Option<Halt>,
274 id: Option<AsideId>,
275 #[serde(default)]
276 aside: Option<Aside>,
277 state: Option<Box<Conversation>>,
278}
279
280impl Read<'_> {
281 fn change(self) -> Result<Change, String> {
283 fn given<T>(field: Option<T>, name: &str) -> Result<T, String> {
284 field.ok_or_else(|| format!("missing field `{name}`"))
285 }
286 Ok(match &*self.kind {
287 "push" => Change::Push {
288 entries: given(self.entries, "entries")?,
289 },
290 "splice" => Change::Splice {
291 from: given(self.from, "from")?,
292 to: given(self.to, "to")?,
293 removed: given(self.removed, "removed")?,
294 with: given(self.with, "with")?,
295 },
296 "halt" => Change::Halt { halt: self.halt },
297 "aside" => Change::Aside {
298 id: given(self.id, "id")?,
299 aside: self.aside,
300 },
301 "reset" => Change::Reset {
302 state: given(self.state, "state")?,
303 },
304 other => return Err(format!("unknown record type {other:?}")),
305 })
306 }
307}
308
309pub(crate) fn parse(line: &[u8]) -> Result<(Stamp, Body), String> {
311 let mut read: Read<'_> = serde_json::from_slice(line).map_err(|error| error.to_string())?;
312 let stamp = Stamp {
313 n: read.n,
314 seq: read.seq,
315 t: read.t,
316 };
317 let body = match &*read.kind {
318 "header" | "state" | "retired" => {
319 Body::Meta(serde_json::from_slice(line).map_err(|error| error.to_string())?)
320 }
321 _ => {
322 let by = read.by.take();
323 Body::Change(ChangeRecord {
324 change: read.change()?,
325 by,
326 at: Some(stamp.t),
327 })
328 }
329 };
330 Ok((stamp, body))
331}
332
333#[derive(Serialize)]
335struct Line<'a> {
336 seq: u64,
337 n: u64,
338 t: u64,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 by: Option<&'a Author>,
341 #[serde(flatten)]
342 body: Out<'a>,
343}
344
345#[derive(Serialize)]
346#[serde(untagged)]
347enum Out<'a> {
348 Meta(&'a Meta),
349 Change(&'a Change),
350}
351
352pub(crate) fn write_meta(out: &mut Vec<u8>, stamp: Stamp, meta: &Meta) -> serde_json::Result<()> {
354 write(out, stamp, None, Out::Meta(meta))
355}
356
357pub(crate) fn write_change(
359 out: &mut Vec<u8>,
360 stamp: Stamp,
361 by: Option<&Author>,
362 change: &Change,
363) -> serde_json::Result<()> {
364 write(out, stamp, by, Out::Change(change))
365}
366
367fn write(
371 out: &mut Vec<u8>,
372 stamp: Stamp,
373 by: Option<&Author>,
374 body: Out<'_>,
375) -> serde_json::Result<()> {
376 let line = Line {
377 seq: stamp.seq,
378 n: stamp.n,
379 t: stamp.t,
380 by,
381 body,
382 };
383 let start = out.len();
384 serde_json::to_writer(&mut *out, &line)?;
385 let written = out
388 .get(start..)
389 .and_then(|line| std::str::from_utf8(line).ok());
390 if let Some(Cow::Owned(redacted)) = written.map(redact::json_text) {
391 out.truncate(start);
392 out.extend_from_slice(redacted.as_bytes());
393 }
394 out.push(b'\n');
395 Ok(())
396}
397
398#[derive(Clone, Debug, Default)]
401pub(crate) struct LogLines {
402 bytes: Vec<u8>,
403 ends: Vec<usize>,
405}
406
407impl LogLines {
408 pub fn parse(bytes: Vec<u8>) -> Self {
409 let mut ends: Vec<usize> = memchr::memchr_iter(b'\n', &bytes)
410 .map(|at| at + 1)
411 .collect();
412 let last_start = ends
413 .len()
414 .checked_sub(2)
415 .and_then(|at| ends.get(at))
416 .copied();
417 let last = ends
418 .last()
419 .and_then(|end| bytes.get(last_start.unwrap_or(0)..end.saturating_sub(1)));
420 if last.is_some_and(|line| {
421 !line.is_empty() && serde_json::from_slice::<serde::de::IgnoredAny>(line).is_err()
422 }) {
423 ends.pop();
424 }
425 Self { bytes, ends }
426 }
427
428 pub fn iter(&self) -> impl Iterator<Item = (usize, u64, &[u8])> {
431 let starts = std::iter::once(0).chain(self.ends.iter().copied());
432 starts
433 .zip(self.ends.iter().copied())
434 .enumerate()
435 .filter_map(|(at, (start, end))| {
436 let line = self.bytes.get(start..end.saturating_sub(1))?;
437 (!line.is_empty()).then_some((at + 1, end as u64, line))
438 })
439 }
440
441 pub fn len(&self) -> u64 {
443 self.ends.last().map_or(0, |end| *end as u64)
444 }
445
446 pub fn tail(&self) -> &[u8] {
448 let len = usize::try_from(self.len()).unwrap_or(usize::MAX);
449 self.bytes.get(len..).unwrap_or_default()
450 }
451
452 pub fn prefix(&self, len: u64) -> &[u8] {
454 let len = usize::try_from(len.min(self.len())).unwrap_or(0);
455 self.bytes.get(..len).unwrap_or_default()
456 }
457}
458
459#[cfg(test)]
460mod tests;