use crate::persist::StoreFailure;
use std::collections::VecDeque;
use std::io::{Read, Write};
use super::super::StoreFile;
use super::super::codec::{FORMAT_VERSION, crc, crc_append};
use super::super::encoder::{EncodeStats, SegmentOut, SnapshotEncoder};
use super::super::error::PersistError;
use super::super::manifest::{FIXED_BYTES, Manifest, ManifestFixed, is_element_kind};
use super::super::segment::{KIND_EDGE, KIND_VERTEX};
use super::super::snapshot::{
COPY_BUFFER, PACKED_MAGIC, PackedReader, SECTION_MANIFEST, SECTION_SCHEMA, SnapshotInfo,
WriteOptions, check_segment_stream, extra_file_prefix, section_kind,
};
use super::files::{
DirPath, SNAPSHOTS_DIR, TMP_PREFIX, parse_seq, read_bounded, safe_relative, snapshot_name,
sync_dir,
};
use crate::graph::PersistentStorage;
pub(crate) const MANIFEST_FILE: &str = "manifest";
pub(crate) const SCHEMA_FILE: &str = "schema.json";
const MAX_META_FILE_BYTES: u64 = 256 << 20;
pub(crate) fn segment_path(snapshot: &DirPath, relative: &str) -> Option<DirPath> {
let Some((holder, file)) = relative.split_once('/') else {
return safe_relative(snapshot, relative);
};
if parse_seq(holder).is_none() || file.contains('/') {
return None;
}
let parent = snapshot.parent()?;
let near = safe_relative(&parent, relative)?;
if parent.name() != SNAPSHOTS_DIR && !near.exists() {
let store = DirPath::root(snapshot.store_dir().clone()).join(SNAPSHOTS_DIR);
return safe_relative(&store, relative);
}
Some(near)
}
pub(crate) fn self_contained_manifest(bytes: Vec<u8>, manifest: &Manifest) -> Vec<u8> {
if !manifest
.segments
.iter()
.any(|s| is_element_kind(s.kind) && s.path.contains('/'))
{
return bytes;
}
let mut flat = manifest.clone();
flat.fixed.flags &= !super::super::manifest::FLAG_INCREMENTAL;
let (mut vertices, mut edges) = (0usize, 0usize);
for segment in &mut flat.segments {
let (prefix, counter) = match segment.kind {
KIND_VERTEX => ("v", &mut vertices),
KIND_EDGE => ("e", &mut edges),
_ => continue,
};
segment.path = format!("{prefix}-{:06}.seg", *counter);
*counter += 1;
}
flat.encode()
}
pub(crate) struct NewSnapshotDir {
root: DirPath,
tmp: DirPath,
final_dir: DirPath,
done: bool,
}
impl NewSnapshotDir {
pub(crate) fn begin(store: &DirPath, commit_seq: u64) -> Result<Self, PersistError> {
let root = store.join(SNAPSHOTS_DIR);
let final_dir = root.join(snapshot_name(commit_seq));
if final_dir.exists() {
return Err(PersistError::store(
"write the snapshot",
StoreFailure::SnapshotExists,
format!("a snapshot at commit {commit_seq} already exists"),
));
}
let tmp = root.join(format!("{TMP_PREFIX}{:032x}", crate::value::uuid::new_v7()));
tmp.create_dir_all()
.map_err(|err| PersistError::io("creating a snapshot directory", err))?;
Ok(Self {
root,
tmp,
final_dir,
done: false,
})
}
pub(crate) fn tmp(&self) -> &DirPath {
&self.tmp
}
pub(crate) fn finish(mut self) -> Result<(DirPath, SnapshotInfo), PersistError> {
sync_dir(&self.tmp)?;
let info = verify_snapshot_dir(&self.tmp)?;
self.tmp
.rename_to(&self.final_dir)
.map_err(|err| PersistError::io("renaming the new snapshot", err))?;
self.done = true;
sync_dir(&self.root)?;
Ok((self.final_dir.clone(), info))
}
}
impl Drop for NewSnapshotDir {
fn drop(&mut self) {
if !self.done {
let _ = self.tmp.remove();
}
}
}
pub(crate) struct NewFile(FileOut);
impl NewFile {
pub(crate) fn create(path: &DirPath) -> Result<Self, PersistError> {
let file = path
.create(true)
.map_err(|err| PersistError::io("writing a snapshot file", err))?;
Ok(Self(FileOut(std::io::BufWriter::with_capacity(
COPY_BUFFER,
file,
))))
}
pub(crate) fn put(&mut self, bytes: &[u8]) -> Result<(), PersistError> {
self.0.put(bytes)
}
pub(crate) fn finish(self) -> Result<(), PersistError> {
let mut file = (self.0)
.0
.into_inner()
.map_err(|err| PersistError::io("writing a snapshot file", err.into_error()))?;
file.sync()
.map_err(|err| PersistError::io("writing a snapshot file", err))
}
}
pub(crate) fn write_file(
path: &DirPath,
fill: impl FnOnce(&mut FileOut) -> Result<(), PersistError>,
) -> Result<(), PersistError> {
let file = path
.create(true)
.map_err(|err| PersistError::io("writing a snapshot file", err))?;
let mut out = FileOut(std::io::BufWriter::with_capacity(COPY_BUFFER, file));
fill(&mut out)?;
let mut file = out
.0
.into_inner()
.map_err(|err| PersistError::io("writing a snapshot file", err.into_error()))?;
file.sync()
.map_err(|err| PersistError::io("writing a snapshot file", err))
}
pub(crate) struct FileOut(std::io::BufWriter<Box<dyn StoreFile>>);
impl SegmentOut for FileOut {
fn put(&mut self, bytes: &[u8]) -> Result<(), PersistError> {
self.0
.write_all(bytes)
.map_err(|err| PersistError::io("writing a snapshot file", err))
}
}
impl Write for FileOut {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
}
pub(crate) fn write_graph_snapshot<S: PersistentStorage + ?Sized>(
tmp: &DirPath,
graph: &S,
options: &WriteOptions,
) -> Result<EncodeStats, PersistError> {
let encoder = SnapshotEncoder::new(graph, options);
let mut index = Vec::new();
let mut property_counts = [0u64; 2];
for (slot, kind) in [KIND_VERTEX, KIND_EDGE].into_iter().enumerate() {
let totals = encoder.plan_kind(kind, &mut |plan| {
write_file(&tmp.join(&plan.entry.path), |out| encoder.emit(&plan, out))?;
index.push(plan.index());
Ok(())
})?;
property_counts[slot] = totals.property_count;
}
let schema = encoder.schema();
let (_, manifest, _) =
encoder.manifest(&index, property_counts[0], property_counts[1], &schema)?;
write_file(&tmp.join(SCHEMA_FILE), |out| out.put(&schema))?;
write_file(&tmp.join(MANIFEST_FILE), |out| out.put(&manifest))?;
Ok(encoder.stats())
}
pub(crate) fn import_packed(
store: &DirPath,
packed: impl Read,
) -> Result<(DirPath, SnapshotInfo), PersistError> {
let mut reader = PackedReader::open(packed)?;
let snapshot = NewSnapshotDir::begin(store, reader.manifest.fixed.commit_seq)?;
for entry in reader.manifest.segments.clone() {
if entry.path.contains('/') {
return Err(PersistError::corrupt(
"packed snapshot",
0,
"an incremental segment reference cannot be imported",
));
}
let path = safe_relative(snapshot.tmp(), &entry.path)
.ok_or_else(|| PersistError::corrupt("packed snapshot", 0, "unsafe segment path"))?;
write_file(&path, |out| reader.copy_segment(out).map(|_| ()))?;
}
let schema = std::mem::take(&mut reader.schema);
let manifest = std::mem::take(&mut reader.manifest_bytes);
reader.finish()?;
write_file(&snapshot.tmp().join(SCHEMA_FILE), |out| out.put(&schema))?;
write_file(&snapshot.tmp().join(MANIFEST_FILE), |out| {
out.put(&manifest)
})?;
snapshot.finish()
}
pub(crate) fn read_manifest(snapshot: &DirPath) -> Result<(Vec<u8>, Manifest), PersistError> {
let location = format!("{}/{MANIFEST_FILE}", snapshot.display());
let bytes = read_bounded(&snapshot.join(MANIFEST_FILE), MAX_META_FILE_BYTES)
.map_err(|err| PersistError::io("reading a snapshot manifest", err))?;
let manifest = Manifest::decode(&bytes).map_err(|err| err.at(&location, 0))?;
Ok((bytes, manifest))
}
pub(crate) fn read_listing(snapshot: &DirPath) -> Result<SnapshotInfo, PersistError> {
let location = format!("{}/{MANIFEST_FILE}", snapshot.display());
let file = snapshot.join(MANIFEST_FILE);
let len = file
.len()
.map_err(|err| PersistError::io("reading a snapshot manifest", err))?;
let head_result = file
.read_range(0, FIXED_BYTES)
.map_err(|err| err.to_string())
.and_then(|head| ManifestFixed::decode(&head).map_err(|err| err.reason));
let fixed = match head_result {
Ok(fixed) => fixed,
Err(head_err) => {
let tail = len
.checked_sub(FIXED_BYTES as u64)
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::UnexpectedEof))
.and_then(|at| file.read_range(at, FIXED_BYTES))
.map_err(|_| PersistError::corrupt(&location, 0, head_err.clone()))?;
ManifestFixed::decode(&tail)
.map_err(|_| PersistError::corrupt(&location, 0, head_err))?
}
};
Ok(SnapshotInfo::from_fixed(&fixed))
}
fn crc_of_stream(reader: &mut dyn Read, max: u64) -> std::io::Result<(u64, u32)> {
let mut buf = vec![0u8; COPY_BUFFER];
let (mut len, mut value) = (0u64, 0u32);
let limit = max.saturating_add(1);
while len < limit {
let want = buf
.len()
.min(usize::try_from(limit - len).unwrap_or(usize::MAX));
let n = match reader.read(&mut buf[..want]) {
Ok(0) => break,
Ok(n) => n,
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
};
value = crc_append(value, &buf[..n]);
len += n as u64;
}
Ok((len, value))
}
pub(crate) fn verify_snapshot_dir(snapshot: &DirPath) -> Result<SnapshotInfo, PersistError> {
let (_, manifest) = read_manifest(snapshot)?;
let schema = read_bounded(&snapshot.join(SCHEMA_FILE), MAX_META_FILE_BYTES)
.map_err(|err| PersistError::io("reading a snapshot's schema.json", err))?;
if schema.len() as u64 != manifest.fixed.schema_len || crc(&schema) != manifest.fixed.schema_crc
{
return Err(PersistError::corrupt(
format!("{}/{SCHEMA_FILE}", snapshot.display()),
0,
"length or checksum differs from the manifest",
));
}
for (number, entry) in manifest.segments.iter().enumerate() {
let location = format!("{}/{}", snapshot.display(), entry.path);
let path = segment_path(snapshot, &entry.path)
.ok_or_else(|| PersistError::corrupt(&location, 0, "unsafe segment path"))?;
let mut reader = path
.open_read()
.map_err(|err| PersistError::io("reading a snapshot segment", err))?;
if !is_element_kind(entry.kind) {
let (len, file_crc) = crc_of_stream(&mut reader, entry.byte_len)
.map_err(|err| PersistError::io("reading a snapshot segment", err))?;
if len != entry.byte_len || file_crc != entry.crc {
return Err(PersistError::corrupt(
&location,
0,
"length or checksum differs from the manifest",
));
}
continue;
}
check_segment_stream(&manifest, number, &mut reader, &location)?;
}
Ok(SnapshotInfo::from_manifest(&manifest))
}
#[cfg_attr(not(feature = "persist-zip"), allow(dead_code))]
pub(crate) fn check_snapshot_file(
manifest: &Manifest,
path: &str,
reader: &mut dyn Read,
location: &str,
) -> Result<(), PersistError> {
let mismatch =
|| PersistError::corrupt(location, 0, "length or checksum differs from the manifest");
if path == SCHEMA_FILE {
let (len, value) = crc_of_stream(reader, MAX_META_FILE_BYTES)
.map_err(|err| PersistError::io("reading a snapshot's schema.json", err))?;
if len != manifest.fixed.schema_len || value != manifest.fixed.schema_crc {
return Err(mismatch());
}
return Ok(());
}
let Some(number) = manifest.segments.iter().position(|s| s.path == path) else {
return Err(PersistError::corrupt(
location,
0,
"a file the snapshot's manifest does not name",
));
};
let entry = &manifest.segments[number];
if !is_element_kind(entry.kind) {
let (len, value) = crc_of_stream(reader, entry.byte_len)
.map_err(|err| PersistError::io("reading a snapshot segment", err))?;
if len != entry.byte_len || value != entry.crc {
return Err(mismatch());
}
return Ok(());
}
check_segment_stream(manifest, number, reader, location)
}
enum Part {
Bytes(Vec<u8>),
File(DirPath),
}
pub(crate) struct DirSnapshotReader {
parts: VecDeque<Part>,
current: Option<Box<dyn Read + Send>>,
crc: u32,
trailer: Option<std::io::Cursor<[u8; 4]>>,
done: bool,
}
impl DirSnapshotReader {
pub(crate) fn open(snapshot: &DirPath) -> Result<Self, PersistError> {
let (manifest_bytes, manifest) = read_manifest(snapshot)?;
let manifest_bytes = self_contained_manifest(manifest_bytes, &manifest);
let mut parts = VecDeque::new();
let mut head = Vec::with_capacity(16);
head.extend_from_slice(PACKED_MAGIC);
head.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
head.extend_from_slice(&(2 + manifest.segments.len() as u32).to_le_bytes());
let section = |kind: u8, len: u64| {
let mut out = Vec::with_capacity(9);
out.push(kind);
out.extend_from_slice(&len.to_le_bytes());
out
};
head.extend_from_slice(§ion(SECTION_MANIFEST, manifest_bytes.len() as u64));
parts.push_back(Part::Bytes(head));
parts.push_back(Part::Bytes(manifest_bytes));
let schema_path = snapshot.join(SCHEMA_FILE);
let schema_len = schema_path
.len()
.map_err(|err| PersistError::io("reading a snapshot's schema.json", err))?;
parts.push_back(Part::Bytes(section(SECTION_SCHEMA, schema_len)));
parts.push_back(Part::File(schema_path));
for entry in &manifest.segments {
let location = format!("{}/{}", snapshot.display(), entry.path);
let path = segment_path(snapshot, &entry.path)
.ok_or_else(|| PersistError::corrupt(&location, 0, "unsafe segment path"))?;
let len = path
.len()
.map_err(|err| PersistError::io("reading a snapshot segment", err))?;
if is_element_kind(entry.kind) {
parts.push_back(Part::Bytes(section(section_kind(entry.kind), len)));
} else {
let prefix = extra_file_prefix(&entry.path);
let mut bytes = section(section_kind(entry.kind), prefix.len() as u64 + len);
bytes.extend_from_slice(&prefix);
parts.push_back(Part::Bytes(bytes));
}
parts.push_back(Part::File(path));
}
Ok(Self {
parts,
current: None,
crc: 0,
trailer: None,
done: false,
})
}
}
impl Read for DirSnapshotReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
loop {
if let Some(trailer) = &mut self.trailer {
return trailer.read(buf);
}
if self.done {
return Ok(0);
}
if let Some(current) = &mut self.current {
let n = current.read(buf)?;
if n > 0 {
self.crc = crc_append(self.crc, &buf[..n]);
return Ok(n);
}
self.current = None;
}
match self.parts.pop_front() {
Some(Part::Bytes(bytes)) => {
self.current = Some(Box::new(std::io::Cursor::new(bytes)))
}
Some(Part::File(path)) => self.current = Some(path.open_read()?),
None => {
self.done = true;
self.trailer = Some(std::io::Cursor::new(self.crc.to_le_bytes()));
}
}
}
}
}