use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use super::types::DIMS;
const MAGIC: &[u8; 8] = b"WCIVEC01";
pub const DEFAULT_MAX_LOG_BYTES: u64 = 64 * 1024 * 1024;
const MAX_NAME: usize = 256;
#[derive(Debug, Clone, PartialEq)]
pub struct LogRecord {
pub instance: String,
pub node: String,
pub id: u64,
pub values: [f32; DIMS],
}
impl LogRecord {
pub fn encoded_len(&self) -> u64 {
(2 + self.instance.len() + 2 + self.node.len() + 8 + DIMS * 4) as u64
}
pub fn is_encodable(&self) -> bool {
self.instance.len() <= MAX_NAME && self.node.len() <= MAX_NAME
}
fn encode(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&(self.instance.len() as u16).to_le_bytes());
out.extend_from_slice(self.instance.as_bytes());
out.extend_from_slice(&(self.node.len() as u16).to_le_bytes());
out.extend_from_slice(self.node.as_bytes());
out.extend_from_slice(&self.id.to_le_bytes());
for f in &self.values {
out.extend_from_slice(&f.to_le_bytes());
}
}
}
pub fn frame_len(records: &[LogRecord]) -> u64 {
12 + records.iter().map(LogRecord::encoded_len).sum::<u64>()
}
struct Cursor<'a>(&'a [u8]);
impl<'a> Cursor<'a> {
fn take(&mut self, n: usize) -> Option<&'a [u8]> {
(self.0.len() >= n).then(|| {
let (a, b) = self.0.split_at(n);
self.0 = b;
a
})
}
fn u16(&mut self) -> Option<u16> {
Some(u16::from_le_bytes(self.take(2)?.try_into().ok()?))
}
fn u32(&mut self) -> Option<u32> {
Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
}
fn name(&mut self) -> Option<String> {
let n = self.u16()? as usize;
if n > MAX_NAME {
return None;
}
String::from_utf8(self.take(n)?.to_vec()).ok()
}
fn record(&mut self) -> Option<LogRecord> {
let instance = self.name()?;
let node = self.name()?;
let id = u64::from_le_bytes(self.take(8)?.try_into().ok()?);
let mut values = [0f32; DIMS];
for v in &mut values {
*v = f32::from_le_bytes(self.take(4)?.try_into().ok()?);
}
Some(LogRecord { instance, node, id, values })
}
}
fn crc(payload: &[u8]) -> [u8; 4] {
let h = blake3::hash(payload);
[h.as_bytes()[0], h.as_bytes()[1], h.as_bytes()[2], h.as_bytes()[3]]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Tail {
Clean,
Torn,
Corrupt,
}
fn decode_frames(data: &[u8]) -> (Vec<LogRecord>, usize, Tail) {
let mut out = Vec::new();
let mut good = 0usize;
let mut cur = Cursor(data);
let tail = loop {
if cur.0.is_empty() {
break Tail::Clean;
}
let Some(len) = cur.u32() else { break Tail::Torn };
let Some(payload) = cur.take(len as usize) else { break Tail::Torn };
let Some(sum) = cur.take(4) else { break Tail::Torn };
if sum != crc(payload) {
break Tail::Corrupt;
}
let mut p = Cursor(payload);
let Some(count) = p.u32() else { break Tail::Corrupt };
let mut batch = Vec::new();
let mut ok = true;
for _ in 0..count {
match p.record() {
Some(r) => batch.push(r),
None => {
ok = false;
break;
}
}
}
if !ok || !p.0.is_empty() {
break Tail::Corrupt;
}
out.extend(batch);
good = data.len() - cur.0.len();
};
(out, good, tail)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OpenError {
#[error("{0}")]
Corrupt(String),
#[error("{0}")]
Io(String),
}
pub fn run_blocking<T>(f: impl FnOnce() -> T) -> T {
match tokio::runtime::Handle::try_current() {
Ok(h) if h.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
tokio::task::block_in_place(f)
}
_ => f(),
}
}
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_default()
}
fn create_private_dir(dir: &Path) -> io::Result<()> {
let mut b = std::fs::DirBuilder::new();
b.recursive(true);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
b.mode(0o700);
}
b.create(dir)
}
pub struct VectorLog {
path: PathBuf,
file: File,
size: u64,
cap: u64,
}
impl VectorLog {
pub fn open(path: &Path, cap: u64) -> Result<(Self, Vec<LogRecord>), OpenError> {
let io = |e: io::Error| OpenError::Io(format!("{}: {e}", path.display()));
if let Some(parent) = path.parent() {
create_private_dir(parent).map_err(io)?;
}
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(path).map_err(io)?;
let len = file.metadata().map_err(io)?.len();
if len > cap {
return Err(OpenError::Corrupt(format!(
"{} is {len} bytes, over the {cap} byte cap",
path.display()
)));
}
let mut data = Vec::with_capacity(len as usize);
file.read_to_end(&mut data).map_err(io)?;
let (records, size) = if data.is_empty() {
file.write_all(MAGIC).map_err(io)?;
file.sync_data().map_err(io)?;
(Vec::new(), MAGIC.len() as u64)
} else if data.len() < MAGIC.len() || &data[..MAGIC.len()] != MAGIC {
return Err(OpenError::Corrupt(format!("{} is not a vector log", path.display())));
} else {
let body = &data[MAGIC.len()..];
let (records, good, tail) = decode_frames(body);
let size = (MAGIC.len() + good) as u64;
let dropped = body.len() - good;
const KEEP_OVER: usize = 512;
match tail {
Tail::Clean => {}
Tail::Torn => {
tracing::warn!(path = %path.display(), dropped, "vector log ends in a torn frame; dropping it");
}
Tail::Corrupt => {
tracing::warn!(
path = %path.display(), dropped,
"vector log has a corrupt frame; dropping it and everything after it"
);
}
}
if tail == Tail::Corrupt || dropped > KEEP_OVER {
let aside = path.with_extension(format!(
"{}.corrupt.{}",
path.extension().and_then(|e| e.to_str()).unwrap_or("vec"),
now_secs()
));
tracing::warn!(path = %path.display(), dropped, kept = %aside.display(), "keeping a copy of the dropped vector log tail");
std::fs::write(&aside, &body[good..]).map_err(io)?;
}
if tail != Tail::Clean {
file.set_len(size).map_err(io)?;
file.sync_data().map_err(io)?;
}
(records, size)
};
file.seek(SeekFrom::Start(size)).map_err(io)?;
Ok((Self { path: path.to_path_buf(), file, size, cap }, records))
}
pub fn has_room_for(&self, records: &[LogRecord]) -> bool {
records.is_empty() || self.size + frame_len(records) <= self.cap
}
pub fn append(&mut self, records: &[LogRecord]) -> io::Result<()> {
if records.is_empty() {
return Ok(());
}
if !self.has_room_for(records) {
return Err(io::Error::other(format!(
"{} would pass its {} byte cap",
self.path.display(),
self.cap
)));
}
let mut payload = Vec::with_capacity(frame_len(records) as usize);
payload.extend_from_slice(&(records.len() as u32).to_le_bytes());
for r in records {
r.encode(&mut payload);
}
let mut frame = Vec::with_capacity(payload.len() + 8);
frame.extend_from_slice(&(payload.len() as u32).to_le_bytes());
frame.extend_from_slice(&payload);
frame.extend_from_slice(&crc(&payload));
self.file.write_all(&frame)?;
self.file.sync_data()?;
self.size += frame.len() as u64;
Ok(())
}
pub fn size(&self) -> u64 {
self.size
}
}