use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::path::Path;
use anyhow::Result;
use crate::models::{FileEdit, Message, ParsedSession, SessionRecord};
pub(crate) const FINGERPRINT_LEN: usize = 4096;
pub(crate) const OVERLAP_BYTES: i64 = 1 << 20;
fn fnv1a_hex(bytes: &[u8]) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{h:016x}")
}
fn read_up_to<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
let mut filled = 0;
while filled < buf.len() {
let n = reader.read(&mut buf[filled..])?;
if n == 0 {
break;
}
filled += n;
}
Ok(filled)
}
pub fn prefix_fingerprint(path: &Path) -> Result<String> {
let mut file = File::open(path)?;
let mut buf = vec![0u8; FINGERPRINT_LEN];
let n = read_up_to(&mut file, &mut buf)?;
Ok(format!("{n}:{}", fnv1a_hex(&buf[..n])))
}
pub fn fingerprint_matches(path: &Path, stored: &str) -> Result<bool> {
let Some((len_str, hash)) = stored.split_once(':') else {
return Ok(false);
};
let Ok(len) = len_str.parse::<usize>() else {
return Ok(false);
};
let mut file = File::open(path)?;
let mut buf = vec![0u8; len];
let n = read_up_to(&mut file, &mut buf)?;
if n < len {
return Ok(false);
}
Ok(fnv1a_hex(&buf[..len]) == hash)
}
pub fn complete_prefix_offset(path: &Path) -> Result<i64> {
let mut file = File::open(path)?;
let size = file.seek(SeekFrom::End(0))?;
complete_prefix_offset_inner(&mut file, size)
}
fn complete_prefix_offset_inner(file: &mut File, size: u64) -> Result<i64> {
complete_prefix_offset_inner_until(file, size, &|| false)?.ok_or_else(|| {
anyhow::anyhow!("complete-prefix scan cancelled with a non-cancelling token")
})
}
fn complete_prefix_offset_inner_until(
file: &mut File,
size: u64,
should_cancel: &dyn Fn() -> bool,
) -> Result<Option<i64>> {
if size == 0 {
return Ok(Some(0));
}
let mut pos = size;
let mut buf = [0u8; 8192];
while pos > 0 {
if should_cancel() {
return Ok(None);
}
let chunk = pos.min(buf.len() as u64);
pos -= chunk;
file.seek(SeekFrom::Start(pos))?;
let slice = &mut buf[..chunk as usize];
file.read_exact(slice)?;
if let Some(rel) = slice.iter().rposition(|&b| b == b'\n') {
return Ok(Some((pos + rel as u64 + 1) as i64));
}
}
Ok(Some(0))
}
pub struct TailParse {
pub new_messages: Vec<Message>,
pub new_file_edits: Vec<FileEdit>,
pub session: SessionRecord,
pub new_transcript: String,
pub new_tail_offset: i64,
pub new_fingerprint: String,
}
pub(crate) enum TailParseRun {
Complete(Option<Box<TailParse>>),
Cancelled,
}
pub fn tail_parse<F>(
path: &Path,
checkpoint_offset: i64,
parse_slice: F,
) -> Result<Option<TailParse>>
where
F: Fn(Cursor<Vec<u8>>, &Path) -> Result<ParsedSession>,
{
match tail_parse_until(path, checkpoint_offset, &|| false, parse_slice)? {
TailParseRun::Complete(parsed) => Ok(parsed.map(|parsed| *parsed)),
TailParseRun::Cancelled => unreachable!("the default tail-parse token never cancels"),
}
}
pub(crate) fn tail_parse_until<F>(
path: &Path,
checkpoint_offset: i64,
should_cancel: &dyn Fn() -> bool,
parse_slice: F,
) -> Result<TailParseRun>
where
F: Fn(Cursor<Vec<u8>>, &Path) -> Result<ParsedSession>,
{
if should_cancel() {
return Ok(TailParseRun::Cancelled);
}
let mut file = File::open(path)?;
let size = file.seek(SeekFrom::End(0))?;
let Some(new_offset) = complete_prefix_offset_inner_until(&mut file, size, should_cancel)?
else {
return Ok(TailParseRun::Cancelled);
};
let new_fingerprint = prefix_fingerprint(path)?;
if new_offset <= checkpoint_offset {
return Ok(TailParseRun::Complete(None));
}
let overlap_start = (checkpoint_offset - OVERLAP_BYTES).max(0);
let prefix_len = (checkpoint_offset - overlap_start) as usize;
let all_len = (new_offset - overlap_start) as usize;
file.seek(SeekFrom::Start(overlap_start as u64))?;
let mut buf = vec![0u8; all_len];
let mut filled = 0usize;
while filled < buf.len() {
if should_cancel() {
return Ok(TailParseRun::Cancelled);
}
let end = (filled + 32 * 1024).min(buf.len());
file.read_exact(&mut buf[filled..end])?;
filled = end;
}
if should_cancel() {
return Ok(TailParseRun::Cancelled);
}
let parsed_all = parse_slice(Cursor::new(buf.clone()), path)?;
if should_cancel() {
return Ok(TailParseRun::Cancelled);
}
let parsed_overlap = parse_slice(Cursor::new(buf[..prefix_len].to_vec()), path)?;
let m0 = parsed_overlap.messages.len();
let e0 = parsed_overlap.file_edits.len();
let new_messages = parsed_all.messages.get(m0..).unwrap_or(&[]).to_vec();
let new_file_edits = parsed_all.file_edits.get(e0..).unwrap_or(&[]).to_vec();
let new_transcript = parsed_all
.transcript_text
.strip_prefix(&parsed_overlap.transcript_text)
.unwrap_or(&parsed_all.transcript_text)
.trim_start_matches('\n')
.to_string();
Ok(TailParseRun::Complete(Some(Box::new(TailParse {
new_messages,
new_file_edits,
session: parsed_all.session,
new_transcript,
new_tail_offset: new_offset,
new_fingerprint,
}))))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
fn write(path: &Path, bytes: &[u8]) {
let mut f = File::create(path).unwrap();
f.write_all(bytes).unwrap();
}
#[test]
fn complete_prefix_offset_stops_at_last_newline() {
let dir = tempdir().unwrap();
let p = dir.path().join("f");
write(&p, b"line one\nline two\n");
assert_eq!(complete_prefix_offset(&p).unwrap(), 18);
write(&p, b"line one\nline two\npar");
assert_eq!(complete_prefix_offset(&p).unwrap(), 18);
write(&p, b"partial only");
assert_eq!(complete_prefix_offset(&p).unwrap(), 0);
write(&p, b"");
assert_eq!(complete_prefix_offset(&p).unwrap(), 0);
}
#[test]
fn tail_read_stops_between_chunks_when_cancelled() {
let dir = tempdir().unwrap();
let path = dir.path().join("session.jsonl");
let prefix = b"prefix\n";
let mut bytes = prefix.to_vec();
bytes.extend(std::iter::repeat_n(b'x', 256 * 1024));
bytes.push(b'\n');
write(&path, &bytes);
let checks = std::cell::Cell::new(0usize);
let run = tail_parse_until(
&path,
prefix.len() as i64,
&|| {
let next = checks.get() + 1;
checks.set(next);
next >= 4
},
|_, _| unreachable!("the parser must not receive a cancelled tail"),
)
.unwrap();
assert!(matches!(run, TailParseRun::Cancelled));
assert!(checks.get() >= 4);
}
#[test]
fn fingerprint_matches_across_appends_but_not_head_rewrites() {
let dir = tempdir().unwrap();
let p = dir.path().join("f");
write(&p, b"HEADER line\nbody\n");
let fp1 = prefix_fingerprint(&p).unwrap();
write(&p, b"HEADER line\nbody\nmore\n");
assert!(
fingerprint_matches(&p, &fp1).unwrap(),
"append must keep the head fingerprint matching"
);
write(&p, b"DIFFERENT!!!\nbody\nmore\n");
assert!(
!fingerprint_matches(&p, &fp1).unwrap(),
"a head rewrite must break the match"
);
write(&p, b"HEAD");
assert!(
!fingerprint_matches(&p, &fp1).unwrap(),
"truncation below the fingerprint must not match"
);
}
}