use crate::persist::StoreFailure;
use std::io::Read;
use std::sync::Arc;
use super::super::codec::{crc, put_i64, put_u32, put_u64};
pub(crate) use super::super::dir::EntryKind;
use super::super::dir::{DirLockGuard, LockKind, StoreDir, StoreFile};
use super::super::error::PersistError;
use crate::value::physical;
use crate::value::{ElementProperty, PropertyMap};
const GRAPH_MAGIC: &[u8; 8] = b"GRSLGRPH";
const GRAPH_FIXED_BYTES: usize = 256;
const LINEAGE_ENTRY_BYTES: usize = 32;
pub(crate) const GRAPH_FILE: &str = "GRAPH";
pub(crate) const GRAPH_COPY: &str = "GRAPH.copy";
pub(crate) const INTENT_FILE: &str = "INTENT";
pub(crate) const MARKS_FILE: &str = "marks";
pub(crate) const SNAPSHOTS_DIR: &str = "snapshots";
pub(crate) const WAL_DIR: &str = "wal";
pub(crate) const WAL_EXTENSION: &str = "wal";
pub(crate) const TMP_PREFIX: &str = "tmp-";
#[derive(Clone)]
pub(crate) struct DirPath {
dir: Arc<dyn StoreDir>,
name: String,
}
impl std::fmt::Debug for DirPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display())
}
}
impl std::fmt::Display for DirPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display())
}
}
impl DirPath {
pub(crate) fn root(dir: Arc<dyn StoreDir>) -> Self {
Self {
dir,
name: String::new(),
}
}
pub(crate) fn store_dir(&self) -> &Arc<dyn StoreDir> {
&self.dir
}
pub(crate) fn name(&self) -> &str {
&self.name
}
pub(crate) fn join(&self, part: impl AsRef<str>) -> Self {
let part = part.as_ref();
Self {
dir: self.dir.clone(),
name: if self.name.is_empty() {
part.to_string()
} else if part.is_empty() {
self.name.clone()
} else {
format!("{}/{part}", self.name)
},
}
}
pub(crate) fn parent(&self) -> Option<Self> {
if self.name.is_empty() {
return None;
}
Some(Self {
dir: self.dir.clone(),
name: self
.name
.rsplit_once('/')
.map_or(String::new(), |(dir, _)| dir.to_string()),
})
}
pub(crate) fn file_name(&self) -> &str {
self.name
.rsplit_once('/')
.map_or(self.name.as_str(), |(_, last)| last)
}
pub(crate) fn relative_to(&self, base: &DirPath) -> String {
if base.name.is_empty() {
return self.name.clone();
}
self.name
.strip_prefix(&base.name)
.and_then(|rest| rest.strip_prefix('/'))
.unwrap_or(&self.name)
.to_string()
}
pub(crate) fn display(&self) -> String {
let location = self.dir.location();
if self.name.is_empty() {
location
} else {
format!("{location}/{}", self.name)
}
}
pub(crate) fn kind(&self) -> Option<EntryKind> {
self.dir.entry_kind(&self.name).ok().flatten()
}
pub(crate) fn exists(&self) -> bool {
self.kind().is_some()
}
pub(crate) fn is_dir(&self) -> bool {
self.kind() == Some(EntryKind::Dir)
}
pub(crate) fn is_file(&self) -> bool {
self.kind() == Some(EntryKind::File)
}
pub(crate) fn list(&self) -> std::io::Result<Vec<(String, EntryKind)>> {
Ok(self
.dir
.list(&self.name)?
.into_iter()
.map(|e| (e.name, e.kind))
.collect())
}
pub(crate) fn len(&self) -> std::io::Result<u64> {
self.dir.file_len(&self.name)
}
pub(crate) fn read_range(&self, offset: u64, len: usize) -> std::io::Result<Vec<u8>> {
self.dir.read_range(&self.name, offset, len)
}
pub(crate) fn open_read(&self) -> std::io::Result<Box<dyn Read + Send>> {
self.dir.open_read(&self.name)
}
pub(crate) fn create_dir_all(&self) -> std::io::Result<()> {
self.dir.create_dir_all(&self.name)
}
pub(crate) fn create(&self, exclusive: bool) -> std::io::Result<Box<dyn StoreFile>> {
self.dir.create(&self.name, exclusive)
}
pub(crate) fn open_write(&self, offset: u64) -> std::io::Result<Box<dyn StoreFile>> {
self.dir.open_write(&self.name, offset)
}
pub(crate) fn append(&self, bytes: &[u8]) -> std::io::Result<()> {
self.dir.append(&self.name, bytes)
}
pub(crate) fn write_atomic(&self, bytes: &[u8]) -> std::io::Result<()> {
self.dir.write_atomic(&self.name, bytes)
}
pub(crate) fn rename_to(&self, to: &DirPath) -> std::io::Result<()> {
self.dir.rename(&self.name, &to.name)
}
pub(crate) fn truncate(&self, len: u64) -> std::io::Result<()> {
self.dir.truncate(&self.name, len)
}
pub(crate) fn remove_file(&self) -> std::io::Result<()> {
self.dir.remove_file(&self.name)
}
pub(crate) fn remove(&self) -> std::io::Result<()> {
self.dir.remove_tree(&self.name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LineageEntry {
pub graph_id: u128,
pub branched_at: u64,
pub time: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct GraphFile {
pub(crate) version: u32,
pub(crate) clean: bool,
pub(crate) graph_id: u128,
pub(crate) created_at: i64,
pub(crate) chunk_bytes: u32,
pub(crate) flags: u32,
pub(crate) lineage: Vec<LineageEntry>,
pub(crate) wal_cut: Option<(u64, u64)>,
pub(crate) backup: Option<BackupMarker>,
pub(crate) wal_head: Option<u64>,
pub(crate) closed_at: Option<u64>,
pub(crate) damage_policy: DamagePolicy,
pub(crate) store_id: u128,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[non_exhaustive]
pub enum DamagePolicy {
#[default]
Maintenance,
Continue,
}
impl DamagePolicy {
pub fn name(self) -> &'static str {
match self {
DamagePolicy::Maintenance => "maintenance",
DamagePolicy::Continue => "continue",
}
}
pub fn parse(text: &str) -> Result<Self, String> {
match text.trim().to_ascii_lowercase().as_str() {
"maintenance" => Ok(DamagePolicy::Maintenance),
"continue" => Ok(DamagePolicy::Continue),
other => Err(format!(
"unknown damage policy {other:?}: expected \"maintenance\" (the default) or \
\"continue\""
)),
}
}
}
impl std::fmt::Display for DamagePolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BackupMarker {
pub commit_seq: u64,
pub taken_at: i64,
}
const FLAG_WAL_CUT: u32 = 1 << 1;
const FLAG_BACKUP: u32 = 1 << 2;
const FLAG_WAL_HEAD: u32 = 1 << 3;
const FLAG_CLOSED_AT: u32 = 1 << 4;
const FLAG_CONTINUE_ON_DAMAGE: u32 = 1 << 5;
const KNOWN_FLAGS: u32 =
FLAG_WAL_CUT | FLAG_BACKUP | FLAG_WAL_HEAD | FLAG_CLOSED_AT | FLAG_CONTINUE_ON_DAMAGE;
impl GraphFile {
pub(crate) fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(GRAPH_FIXED_BYTES + 36 * self.lineage.len());
out.extend_from_slice(GRAPH_MAGIC);
put_u32(&mut out, self.version);
put_u32(&mut out, if self.clean { 0 } else { 1 });
out.extend_from_slice(&self.graph_id.to_be_bytes());
put_i64(&mut out, self.created_at);
put_u32(&mut out, self.lineage.len() as u32);
put_u32(&mut out, self.chunk_bytes);
let (flags, (cut_segment, cut_offset)) = match self.wal_cut {
Some(cut) => (self.flags | FLAG_WAL_CUT, cut),
None => (self.flags & !FLAG_WAL_CUT, (0, 0)),
};
let (flags, (backup_commit, backup_time)) = match self.backup {
Some(marker) => (flags | FLAG_BACKUP, (marker.commit_seq, marker.taken_at)),
None => (flags & !FLAG_BACKUP, (0, 0)),
};
let flags = match self.wal_head {
Some(_) => flags | FLAG_WAL_HEAD,
None => flags & !FLAG_WAL_HEAD,
};
let flags = match self.closed_at {
Some(_) => flags | FLAG_CLOSED_AT,
None => flags & !FLAG_CLOSED_AT,
};
let flags = match self.damage_policy {
DamagePolicy::Continue => flags | FLAG_CONTINUE_ON_DAMAGE,
DamagePolicy::Maintenance => flags & !FLAG_CONTINUE_ON_DAMAGE,
};
put_u32(&mut out, flags);
put_u64(&mut out, cut_segment);
put_u64(&mut out, cut_offset);
put_u64(&mut out, backup_commit);
put_i64(&mut out, backup_time);
put_u64(&mut out, self.wal_head.unwrap_or(0));
put_u64(&mut out, self.closed_at.unwrap_or(0));
out.extend_from_slice(&self.store_id.to_be_bytes());
out.resize(GRAPH_FIXED_BYTES - 4, 0);
let checksum = crc(&out);
put_u32(&mut out, checksum);
let start = out.len();
for entry in &self.lineage {
out.extend_from_slice(&entry.graph_id.to_be_bytes());
put_u64(&mut out, entry.branched_at);
put_i64(&mut out, entry.time);
}
let checksum = crc(&out[start..]);
put_u32(&mut out, checksum);
out
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self, String> {
if bytes.len() < GRAPH_FIXED_BYTES + 4 {
return Err("shorter than its fixed part".into());
}
if &bytes[..8] != GRAPH_MAGIC {
return Err("not a GRAPH file (bad magic)".into());
}
let word = |at: usize| {
u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
};
let long = |at: usize| {
let mut b = [0u8; 8];
b.copy_from_slice(&bytes[at..at + 8]);
b
};
let id = |at: usize| {
let mut b = [0u8; 16];
b.copy_from_slice(&bytes[at..at + 16]);
u128::from_be_bytes(b)
};
if crc(&bytes[..GRAPH_FIXED_BYTES - 4]) != word(GRAPH_FIXED_BYTES - 4) {
return Err("checksum mismatch of the fixed part".into());
}
let version = word(8);
if version == 0 {
return Err("unknown format version 0".into());
}
if version < STORE_VERSION {
return Err(format!("older format version {version}"));
}
let count = word(40) as usize;
let expected = count
.checked_mul(LINEAGE_ENTRY_BYTES)
.and_then(|n| n.checked_add(GRAPH_FIXED_BYTES + 4));
if expected != Some(bytes.len()) {
return Err(format!(
"{count} lineage entries do not match the file size of {} bytes",
bytes.len()
));
}
let entries = &bytes[GRAPH_FIXED_BYTES..bytes.len() - 4];
if crc(entries) != word(bytes.len() - 4) {
return Err("checksum mismatch of the lineage entries".into());
}
let state = word(12);
if state > 1 {
return Err(format!("unknown state {state}"));
}
let mut lineage = Vec::with_capacity(count);
for i in 0..count {
let at = GRAPH_FIXED_BYTES + i * LINEAGE_ENTRY_BYTES;
lineage.push(LineageEntry {
graph_id: id(at),
branched_at: u64::from_le_bytes(long(at + 16)),
time: i64::from_le_bytes(long(at + 24)),
});
}
let graph_id = id(16);
if lineage.first().map(|e| e.graph_id) != Some(graph_id) {
return Err("the first lineage entry is not the graph's lineage".into());
}
let store_id = id(100);
if store_id == 0 {
return Err("no store id (offset 100 is zero)".into());
}
let flags = word(48);
Ok(GraphFile {
version,
clean: state == 0,
graph_id,
created_at: i64::from_le_bytes(long(32)),
chunk_bytes: word(44),
flags: flags & !KNOWN_FLAGS,
lineage,
wal_cut: (flags & FLAG_WAL_CUT != 0)
.then(|| (u64::from_le_bytes(long(52)), u64::from_le_bytes(long(60)))),
backup: (flags & FLAG_BACKUP != 0).then(|| BackupMarker {
commit_seq: u64::from_le_bytes(long(68)),
taken_at: i64::from_le_bytes(long(76)),
}),
wal_head: (flags & FLAG_WAL_HEAD != 0).then(|| u64::from_le_bytes(long(84))),
closed_at: (flags & FLAG_CLOSED_AT != 0).then(|| u64::from_le_bytes(long(92))),
damage_policy: if flags & FLAG_CONTINUE_ON_DAMAGE != 0 {
DamagePolicy::Continue
} else {
DamagePolicy::Maintenance
},
store_id,
})
}
pub(crate) fn older_version(bytes: &[u8]) -> Option<u32> {
if bytes.len() < GRAPH_FIXED_BYTES + 4 || &bytes[..8] != GRAPH_MAGIC {
return None;
}
let word = |at: usize| {
u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
};
if crc(&bytes[..GRAPH_FIXED_BYTES - 4]) != word(GRAPH_FIXED_BYTES - 4) {
return None;
}
let version = word(8);
(1..STORE_VERSION).contains(&version).then_some(version)
}
pub(crate) fn on_chain(&self, graph_id: u128) -> bool {
self.lineage.iter().any(|entry| entry.graph_id == graph_id)
}
pub(crate) fn chain(&self) -> Vec<(u128, u64)> {
self.lineage
.iter()
.enumerate()
.map(|(i, entry)| {
let valid = match i {
0 => u64::MAX,
i => self.lineage[i - 1].branched_at,
};
(entry.graph_id, valid)
})
.collect()
}
}
pub(crate) struct GraphFileRead {
pub(crate) file: GraphFile,
pub(crate) repaired: Option<String>,
}
pub(crate) fn read_graph_file(dir: &DirPath) -> Result<GraphFileRead, PersistError> {
let (main, copy) = decode_graph_copies(dir)?;
match (main, copy) {
(Ok(main), Ok(copy)) if main == copy => Ok(GraphFileRead {
file: main,
repaired: None,
}),
(Ok(main), Ok(copy)) => {
let use_copy = copy.lineage.len() >= main.lineage.len();
let file = if use_copy { copy } else { main };
Ok(GraphFileRead {
file,
repaired: Some(format!(
"GRAPH and GRAPH.copy differ (an interrupted rewrite); {} is used",
if use_copy { "GRAPH.copy" } else { "GRAPH" }
)),
})
}
(Ok(file), Err(err)) => Ok(GraphFileRead {
file,
repaired: Some(format!("GRAPH.copy is not usable ({err}); GRAPH is used")),
}),
(Err(err), Ok(file)) => Ok(GraphFileRead {
file,
repaired: Some(format!("GRAPH is not usable ({err}); GRAPH.copy is used")),
}),
(Err(main), Err(_)) => Err(PersistError::NotAStore {
path: dir.display().to_string(),
reason: format!("no valid GRAPH file ({main})"),
}),
}
}
pub(crate) type Decoded = Result<GraphFile, String>;
pub(crate) fn decode_graph_copies(dir: &DirPath) -> Result<(Decoded, Decoded), PersistError> {
let mut older = None;
let mut read = |name: &str| -> Result<GraphFile, String> {
let bytes = read_bounded(&dir.join(name), 64 << 20).map_err(|err| err.to_string())?;
GraphFile::decode(&bytes).inspect_err(|_| {
older = older.or(GraphFile::older_version(&bytes));
})
};
let main = read(GRAPH_FILE);
let copy = read(GRAPH_COPY);
if let (Err(_), Err(_), Some(found)) = (&main, ©, older) {
return Err(PersistError::OlderFormat {
path: dir.display().to_string(),
found,
supported: STORE_VERSION,
});
}
Ok((main, copy))
}
pub(crate) fn refuse_older_format(dir: &DirPath) -> Result<(), PersistError> {
decode_graph_copies(dir).map(|_| ())
}
pub(crate) fn write_graph_file(dir: &DirPath, file: &GraphFile) -> Result<(), PersistError> {
let bytes = file.encode();
write_atomic(dir, GRAPH_COPY, &bytes)?;
write_atomic(dir, GRAPH_FILE, &bytes)
}
pub(crate) fn write_atomic(dir: &DirPath, name: &str, bytes: &[u8]) -> Result<(), PersistError> {
dir.join(name)
.write_atomic(bytes)
.map_err(|err| PersistError::io("writing a store file", err))
}
pub(crate) fn sync_dir(dir: &DirPath) -> Result<(), PersistError> {
dir.store_dir()
.sync_dir(dir.name())
.map_err(|err| PersistError::io("syncing a store directory", err))
}
pub(crate) fn read_bounded(path: &DirPath, max: u64) -> std::io::Result<Vec<u8>> {
path.store_dir().read(path.name(), max)
}
pub(crate) fn snapshot_name(commit_seq: u64) -> String {
format!("{commit_seq:020}")
}
pub(crate) fn wal_name(first: u64) -> String {
format!("{first:020}.{WAL_EXTENSION}")
}
pub(crate) fn parse_seq(name: &str) -> Option<u64> {
(name.len() == 20 && name.bytes().all(|b| b.is_ascii_digit()))
.then(|| name.parse().ok())
.flatten()
}
pub(crate) fn list_snapshots(dir: &DirPath) -> Result<Vec<(u64, DirPath)>, PersistError> {
let mut out = list_snapshot_dirs(dir)?;
out.retain(|(_, path)| path.join(super::snapshot_dir::MANIFEST_FILE).exists());
Ok(out)
}
pub(crate) fn list_snapshot_dirs(dir: &DirPath) -> Result<Vec<(u64, DirPath)>, PersistError> {
let root = dir.join(SNAPSHOTS_DIR);
let entries = root
.list()
.map_err(|err| PersistError::io("listing snapshots", err))?;
let mut out: Vec<(u64, DirPath)> = entries
.into_iter()
.filter(|(_, kind)| *kind == EntryKind::Dir)
.filter_map(|(name, _)| Some((parse_seq(&name)?, root.join(&name))))
.collect();
out.sort_by_key(|(seq, _)| *seq);
Ok(out)
}
pub(crate) fn list_holders(dir: &DirPath) -> Result<Vec<(u64, DirPath)>, PersistError> {
let mut out = list_snapshot_dirs(dir)?;
out.retain(|(_, path)| !path.join(super::snapshot_dir::MANIFEST_FILE).exists());
Ok(out)
}
pub(crate) fn list_wal(dir: &DirPath) -> Result<Vec<(u64, DirPath)>, PersistError> {
let root = dir.join(WAL_DIR);
let entries = root
.list()
.map_err(|err| PersistError::io("listing the WAL", err))?;
let suffix = format!(".{WAL_EXTENSION}");
let mut out: Vec<(u64, DirPath)> = entries
.into_iter()
.filter(|(_, kind)| *kind == EntryKind::File)
.filter_map(|(name, _)| {
let seq = parse_seq(name.strip_suffix(&suffix)?)?;
Some((seq, root.join(&name)))
})
.collect();
out.sort_by_key(|(seq, _)| *seq);
Ok(out)
}
pub(crate) fn wal_after(segments: &[(u64, DirPath)], snapshot_seq: u64) -> &[(u64, DirPath)] {
let next = snapshot_seq.saturating_add(1);
let start = segments
.iter()
.rposition(|(first, _)| *first <= next)
.unwrap_or(0);
&segments[start..]
}
pub(crate) struct DirLock {
_lock: DirLockGuard,
}
impl std::fmt::Debug for DirLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("DirLock")
}
}
impl DirLock {
pub(crate) fn exclusive(dir: &DirPath) -> Result<Self, PersistError> {
match dir.store_dir().lock(LockKind::Writer) {
Ok(Some(lock)) => Ok(Self { _lock: lock }),
Ok(None) => Err(PersistError::Locked {
path: dir.display(),
}),
Err(err) => Err(PersistError::io("opening the store's LOCK file", err)),
}
}
}
pub(crate) struct BackupPin {
_lock: DirLockGuard,
}
impl BackupPin {
pub(crate) fn shared(dir: &DirPath) -> Result<Self, PersistError> {
match dir.store_dir().lock(LockKind::BackupShared) {
Ok(Some(lock)) => Ok(Self { _lock: lock }),
Ok(None) => Err(PersistError::store(
"back up the store",
StoreFailure::PruneRunning,
"a prune is deleting its files; back up again when it has finished",
)),
Err(err) => Err(PersistError::io("pinning the store for a backup", err)),
}
}
pub(crate) fn exclusive(dir: &DirPath) -> Result<Self, PersistError> {
match dir.store_dir().lock(LockKind::BackupExclusive) {
Ok(Some(lock)) => Ok(Self { _lock: lock }),
Ok(None) => Err(PersistError::store(
"prune the store",
StoreFailure::BackupRunning,
"a backup is copying its files; prune again when it has finished",
)),
Err(err) => Err(PersistError::io("opening the store's BACKUP file", err)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Intent {
pub(crate) op: String,
pub(crate) files: Vec<String>,
pub(crate) started_at: i64,
pub(crate) params: Vec<(String, String)>,
}
impl Intent {
fn to_json(&self) -> String {
let mut object = PropertyMap::default();
object.insert("op", ElementProperty::from(self.op.as_str()));
object.insert(
"files",
ElementProperty::Array(
self.files
.iter()
.map(|f| ElementProperty::from(f.as_str()))
.collect(),
),
);
object.insert("started_at", ElementProperty::from(self.started_at));
for (key, value) in &self.params {
object.insert(key.as_str(), ElementProperty::from(value.as_str()));
}
physical::to_json_text(&ElementProperty::Object(Box::new(object))).unwrap_or_default()
}
fn from_json(text: &str) -> Result<Self, String> {
let value =
physical::lift_untyped(physical::read_json(text, 8).map_err(|e| e.to_string())?);
let ElementProperty::Object(object) = value else {
return Err("not a JSON object".into());
};
let op = match object.get("op") {
Some(ElementProperty::String(op)) => op.to_string(),
_ => return Err("no \"op\"".into()),
};
let mut files = Vec::new();
if let Some(ElementProperty::Array(items)) = object.get("files") {
for item in items {
match item {
ElementProperty::String(path) => files.push(path.to_string()),
_ => return Err("a file that is not a string".into()),
}
}
}
let started_at = match object.get("started_at") {
Some(ElementProperty::Number(n)) => n.as_i64().unwrap_or(0),
_ => 0,
};
let mut params = Vec::new();
for (key, value) in object.iter() {
if let ElementProperty::String(value) = value
&& key.as_str() != "op"
{
params.push((key.to_string(), value.to_string()));
}
}
Ok(Self {
op,
files,
started_at,
params,
})
}
pub(crate) fn param(&self, key: &str) -> Option<&str> {
self.params
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub(crate) fn write(&self, dir: &DirPath) -> Result<(), PersistError> {
write_atomic(dir, INTENT_FILE, self.to_json().as_bytes())
}
pub(crate) fn read(dir: &DirPath) -> Result<Option<Self>, PersistError> {
let path = dir.join(INTENT_FILE);
if !path.exists() {
return Ok(None);
}
let bytes = read_bounded(&path, 64 << 20)
.map_err(|err| PersistError::io("reading the INTENT file", err))?;
let text = String::from_utf8(bytes)
.map_err(|_| PersistError::corrupt(INTENT_FILE, 0, "the INTENT file is not UTF-8"))?;
Self::from_json(&text)
.map(Some)
.map_err(|reason| PersistError::corrupt(INTENT_FILE, 0, reason))
}
pub(crate) fn clear(dir: &DirPath) -> Result<(), PersistError> {
match dir.join(INTENT_FILE).remove_file() {
Ok(()) => sync_dir(dir),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(PersistError::io("removing the INTENT file", err)),
}
}
}
pub(crate) fn safe_relative(dir: &DirPath, relative: &str) -> Option<DirPath> {
if relative.is_empty()
|| relative.starts_with('/')
|| relative.contains('\\')
|| relative.contains('\0')
|| relative.contains(':')
|| relative
.split('/')
.any(|part| part.is_empty() || part == "." || part == "..")
{
return None;
}
Some(dir.join(relative))
}
pub(crate) fn remove_path(path: &DirPath) -> std::io::Result<()> {
path.remove()
}
pub(crate) fn has_entries(dir: &DirPath) -> bool {
dir.exists() && dir.list().map(|e| !e.is_empty()).unwrap_or(true)
}
pub(crate) fn undo_new_dir(dir: &DirPath, existed: bool) {
if let Ok(entries) = dir.list() {
for (name, _) in entries {
let _ = dir.join(&name).remove();
}
}
if !existed {
let _ = dir.remove();
}
}
pub(crate) const STORE_VERSION: u32 = 2;