use std::fs::{self, File, OpenOptions};
use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::clock::now_rfc3339;
use crate::inbox::{BackendError, Carried, Disposition, Inbox, InboxBackend, Sender, Undelivered};
use crate::schema::{Message, SchemaId};
pub const CARRY_SCHEMA_VERSION: u32 = 1;
const STORE_KIND: &str = "onemessagebus-carry-store";
const WHAT: &str = "a carry store";
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Header {
schema_version: u32,
kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CarriedEntry {
pub ts: String,
pub schema: SchemaId,
pub message: Value,
}
#[derive(Debug, Clone, Copy)]
pub struct Carry;
impl Carry {
#[must_use]
pub fn sender<M: Message + 'static, D: Carried>(store: impl Into<PathBuf>) -> Sender<M, D> {
Sender::over(Carrier::<M, D> {
store: store.into(),
types: PhantomData,
})
}
pub fn read(store: &Path) -> Result<Vec<CarriedEntry>, BackendError> {
if store.is_dir() {
return Err(absent(store, "it is a directory"));
}
let mut file = match File::open(store) {
Ok(file) => file,
Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => {
return Err(absent(store, "nothing is there"))
}
Err(failure) => return Err(BackendError::io("open", store, &failure)),
};
file.lock_shared()
.map_err(|failure| BackendError::io("lock", store, &failure))?;
let contents = contents_of(&mut file, store)?;
let (_, entries) = parse(store, &contents)?;
Ok(entries)
}
}
struct Carrier<M, D> {
store: PathBuf,
types: PhantomData<fn(M) -> D>,
}
impl<M: Message, D: Carried> InboxBackend<M, D> for Carrier<M, D> {
fn send(&self, message: M) -> Result<D, Undelivered> {
let entry = CarriedEntry {
ts: now_rfc3339(),
schema: M::SCHEMA,
message: serde_json::to_value(&message).map_err(|failure| BackendError::Encoding {
what: "message",
why: failure.to_string(),
})?,
};
let mut line = serde_json::to_string(&entry).map_err(|failure| BackendError::Encoding {
what: "message",
why: failure.to_string(),
})?;
line.push('\n');
let store = &self.store;
if store.is_dir() {
return Err(absent(store, "it is a directory").into());
}
create_store(store)?;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(store)
.map_err(|failure| BackendError::io("open", store, &failure))?;
file.lock()
.map_err(|failure| BackendError::io("lock", store, &failure))?;
let contents = contents_of(&mut file, store)?;
parse(store, &contents)?;
file.seek(SeekFrom::End(0))
.and_then(|_| file.write_all(line.as_bytes()))
.and_then(|()| file.sync_data())
.map_err(|failure| BackendError::io("append to", store, &failure))?;
Ok(D::carried())
}
}
impl<M: Message + Send + 'static, D: Disposition> Inbox<M, D> {
pub fn adopt_carried(&self, store: impl AsRef<Path>) -> Result<usize, Undelivered> {
let store = store.as_ref();
if let Some(closed) = self.closed() {
return Err(Undelivered::Closed(closed));
}
if store.is_dir() {
return Err(absent(store, "it is a directory").into());
}
let mut file = match OpenOptions::new().read(true).write(true).open(store) {
Ok(file) => file,
Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => return Ok(0),
Err(failure) => return Err(BackendError::io("open", store, &failure).into()),
};
file.lock()
.map_err(|failure| BackendError::io("lock", store, &failure))?;
let contents = contents_of(&mut file, store)?;
let (header, entries) = parse(store, &contents)?;
let messages = entries
.into_iter()
.enumerate()
.map(|(index, entry)| message_of::<M>(store, index, entry))
.collect::<Result<Vec<M>, BackendError>>()?;
if messages.is_empty() {
return Ok(0);
}
self.shared()
.offer_all(messages, || {
file.set_len(header as u64)
.and_then(|()| file.sync_data())
.map_err(|failure| BackendError::io("drain", store, &failure))
})
.map_err(|refused| match refused {
Ok(closed) => Undelivered::Closed(closed),
Err(failure) => Undelivered::Backend(failure),
})
}
}
fn message_of<M: Message>(
store: &Path,
index: usize,
entry: CarriedEntry,
) -> Result<M, BackendError> {
let unreadable = |why: String| BackendError::Unreadable {
at: store.to_path_buf(),
file: store.to_path_buf(),
why: format!("record {}: {why}", index + 1),
};
if entry.schema != M::SCHEMA {
return Err(unreadable(format!(
"it carries a {}, and this inbox takes {}",
entry.schema,
M::SCHEMA
)));
}
serde_json::from_value(entry.message)
.map_err(|failure| unreadable(format!("it is not a {}: {failure}", M::SCHEMA)))
}
fn absent(store: &Path, why: &str) -> BackendError {
BackendError::Absent {
what: WHAT,
path: store.to_path_buf(),
why: why.to_owned(),
}
}
fn header_line() -> Result<String, BackendError> {
let mut line = serde_json::to_string(&Header {
schema_version: CARRY_SCHEMA_VERSION,
kind: STORE_KIND.to_owned(),
})
.map_err(|failure| BackendError::Encoding {
what: "store header",
why: failure.to_string(),
})?;
line.push('\n');
Ok(line)
}
fn contents_of(file: &mut File, store: &Path) -> Result<String, BackendError> {
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|failure| BackendError::io("read", store, &failure))?;
Ok(contents)
}
fn parse(store: &Path, contents: &str) -> Result<(usize, Vec<CarriedEntry>), BackendError> {
let Some((header, records)) = contents.split_once('\n') else {
return Err(absent(
store,
if contents.is_empty() {
"it is empty, and a carry store starts with its header line"
} else {
"it has no header line"
},
));
};
match serde_json::from_str::<Header>(header) {
Ok(read) if read.kind == STORE_KIND && read.schema_version == CARRY_SCHEMA_VERSION => {}
Ok(read) if read.kind == STORE_KIND => {
return Err(BackendError::Unreadable {
at: store.to_path_buf(),
file: store.to_path_buf(),
why: format!(
"it declares schema_version {}, and this build reads {CARRY_SCHEMA_VERSION}",
read.schema_version
),
})
}
_ => {
return Err(absent(
store,
"its first line is not a carry store's header",
))
}
}
if !records.is_empty() && !records.ends_with('\n') {
return Err(BackendError::Unreadable {
at: store.to_path_buf(),
file: store.to_path_buf(),
why: "it ends in a torn record".to_owned(),
});
}
let entries = records
.lines()
.enumerate()
.map(|(index, line)| {
let unreadable = |why: String| BackendError::Unreadable {
at: store.to_path_buf(),
file: store.to_path_buf(),
why: format!("record {}: {why}", index + 1),
};
let entry = serde_json::from_str::<CarriedEntry>(line)
.map_err(|failure| unreadable(failure.to_string()))?;
if !is_stamp(&entry.ts) {
return Err(unreadable(format!(
"its ts {:?} is not RFC 3339 with millisecond precision in UTC",
entry.ts
)));
}
Ok(entry)
})
.collect::<Result<Vec<_>, _>>()?;
Ok((header.len() + 1, entries))
}
fn is_stamp(ts: &str) -> bool {
const SHAPE: &[u8; 24] = b"dddd-dd-ddTdd:dd:dd.dddZ";
let bytes = ts.as_bytes();
let shaped = bytes.len() == SHAPE.len()
&& bytes
.iter()
.zip(SHAPE.iter())
.all(|(byte, shape)| match shape {
b'd' => byte.is_ascii_digit(),
literal => byte == literal,
});
if !shaped {
return false;
}
let number = |from: usize, to: usize| {
bytes[from..to]
.iter()
.fold(0_u32, |total, digit| total * 10 + u32::from(digit - b'0'))
};
let (year, month, day) = (number(0, 4), number(5, 7), number(8, 10));
let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let days = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if leap => 29,
2 => 28,
_ => 0,
};
(1..=days).contains(&day) && number(11, 13) < 24 && number(14, 16) < 60 && number(17, 19) < 60
}
fn create_store(store: &Path) -> Result<(), BackendError> {
static STAGED: AtomicU64 = AtomicU64::new(0);
if store.exists() {
return Ok(());
}
let mut staging = store.as_os_str().to_owned();
staging.push(format!(
".{}-{}.staging",
std::process::id(),
STAGED.fetch_add(1, Ordering::Relaxed)
));
let staging = PathBuf::from(staging);
fs::write(&staging, header_line()?)
.map_err(|failure| BackendError::io("write", &staging, &failure))?;
let linked = fs::hard_link(&staging, store);
let _ = fs::remove_file(&staging);
match linked {
Ok(()) => Ok(()),
Err(failure) if failure.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(failure) => Err(BackendError::io("create", store, &failure)),
}
}