use std::fmt;
use std::path::{Path, PathBuf};
use lora_wal::{SyncMode, WalConfig};
pub const DEFAULT_DATABASE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct DatabaseOpenOptions {
pub database_dir: PathBuf,
pub sync_mode: SyncMode,
pub segment_target_bytes: u64,
pub max_database_bytes: u64,
}
impl Default for DatabaseOpenOptions {
fn default() -> Self {
Self {
database_dir: PathBuf::from("."),
sync_mode: SyncMode::Group { interval_ms: 1_000 },
segment_target_bytes: 8 * 1024 * 1024,
max_database_bytes: DEFAULT_DATABASE_MAX_BYTES,
}
}
}
impl DatabaseOpenOptions {
pub fn with_database_dir(mut self, database_dir: impl Into<PathBuf>) -> Self {
self.database_dir = database_dir.into();
self
}
pub fn wal_config_for(&self, name: &DatabaseName) -> WalConfig {
WalConfig::Enabled {
dir: self.database_path_for(name),
sync_mode: self.sync_mode,
segment_target_bytes: self.segment_target_bytes,
}
}
pub fn database_path_for(&self, name: &DatabaseName) -> PathBuf {
self.database_dir.join(name.relative_path())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DatabaseName {
raw: String,
relative_path: PathBuf,
}
impl DatabaseName {
pub fn parse(value: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
let value = value.as_ref();
if value.is_empty() {
return Err(DatabaseNameError::Empty);
}
if value.starts_with('/') || value.starts_with('\\') || looks_like_windows_absolute(value) {
return Err(DatabaseNameError::AbsolutePath(value.to_string()));
}
let mut parts: Vec<&str> = value.split(['/', '\\']).collect();
if parts.iter().any(|part| part.is_empty()) {
return Err(DatabaseNameError::InvalidCharacters(value.to_string()));
}
while parts.first() == Some(&".") {
parts.remove(0);
}
while parts.last() == Some(&".") {
parts.pop();
}
if parts.is_empty() {
return Err(DatabaseNameError::Reserved(value.to_string()));
}
let mut path = PathBuf::new();
for (idx, part) in parts.iter().enumerate() {
if *part == "." {
continue;
}
if *part == ".." {
return Err(DatabaseNameError::Reserved(value.to_string()));
}
let is_basename = idx == parts.len() - 1;
let serialized = serialize_component(part, is_basename)
.ok_or_else(|| DatabaseNameError::InvalidCharacters(value.to_string()))?;
path.push(serialized);
}
if path.as_os_str().is_empty() {
return Err(DatabaseNameError::Reserved(value.to_string()));
}
Ok(Self {
raw: value.to_string(),
relative_path: path,
})
}
pub fn as_str(&self) -> &str {
&self.raw
}
pub fn relative_path(&self) -> &Path {
&self.relative_path
}
}
impl fmt::Display for DatabaseName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.raw.fmt(f)
}
}
impl TryFrom<&str> for DatabaseName {
type Error = DatabaseNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DatabaseNameError {
Empty,
Reserved(String),
AbsolutePath(String),
InvalidCharacters(String),
}
impl fmt::Display for DatabaseNameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "database name must not be empty"),
Self::Reserved(name) => write!(f, "database name '{name}' is reserved"),
Self::AbsolutePath(name) => write!(
f,
"invalid database name '{name}': use a relative path under database_dir"
),
Self::InvalidCharacters(name) => write!(
f,
"invalid database name '{name}': use relative path components containing only letters, digits, '+', '_', '-', with an optional .loradb suffix on the basename"
),
}
}
}
impl std::error::Error for DatabaseNameError {}
pub fn resolve_database_path(
database_name: &str,
database_dir: impl AsRef<Path>,
) -> Result<PathBuf, DatabaseNameError> {
let name = DatabaseName::parse(database_name)?;
Ok(database_dir.as_ref().join(name.relative_path()))
}
fn serialize_component(component: &str, is_basename: bool) -> Option<String> {
if component.is_empty() {
return None;
}
if is_basename {
if let Some(stem) = component.strip_suffix(".loradb") {
return (!stem.is_empty() && is_portable_component(stem))
.then(|| component.to_string());
}
return is_portable_component(component).then(|| format!("{component}.loradb"));
}
is_portable_component(component).then(|| component.to_string())
}
fn is_portable_component(value: &str) -> bool {
value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'_' | b'-'))
}
fn looks_like_windows_absolute(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\')
}