use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::core::limits::MAX_CONFIG_FILE_LEN;
use crate::core::map::MemoryMap;
use crate::error::{Result, StoreError};
use crate::store::migration::STORE_FORMAT_VERSION;
use crate::types::{Entry, Key, Value};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoreFile {
pub version: u32,
pub records: Vec<StoreRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoreRecord {
pub key: String,
pub value: String,
}
pub fn load_map(path: &Path) -> Result<MemoryMap> {
let raw = read_store_file(path)?;
let parsed = parse_store_file(&raw)?;
file_to_map(parsed)
}
pub fn save_map(path: &Path, map: &MemoryMap) -> Result<()> {
let file = map_to_file(map);
let payload = serialize_store_file(&file)?;
write_atomic(path, payload.as_bytes())
}
pub fn parse_store_file(input: &str) -> Result<StoreFile> {
let parsed: StoreFile =
serde_json::from_str(input).map_err(|error| StoreError::Deserialize {
message: format!("invalid store JSON: {error}"),
})?;
validate_store_file(&parsed)?;
Ok(parsed)
}
pub fn serialize_store_file(file: &StoreFile) -> Result<String> {
validate_store_file(file)?;
serde_json::to_string_pretty(file).map_err(|error| {
StoreError::Serialize {
message: format!("failed to serialize store: {error}"),
}
.into()
})
}
#[must_use]
pub fn map_to_file(map: &MemoryMap) -> StoreFile {
let records = map
.entries()
.into_iter()
.map(|entry| StoreRecord {
key: entry.key.into_inner(),
value: entry.value.into_inner(),
})
.collect();
StoreFile {
version: STORE_FORMAT_VERSION,
records,
}
}
pub fn file_to_map(file: StoreFile) -> Result<MemoryMap> {
validate_store_file(&file)?;
let mut entries = Vec::with_capacity(file.records.len());
for record in file.records {
let entry = Entry::try_new(record.key, record.value)?;
entries.push(entry);
}
Ok(entries.into_iter().collect())
}
pub fn validate_store_file(file: &StoreFile) -> Result<()> {
if file.version != STORE_FORMAT_VERSION {
return Err(StoreError::UnsupportedVersion {
version: file.version,
}
.into());
}
for record in &file.records {
let _ = Key::new(record.key.clone())?;
let _ = Value::new(record.value.clone())?;
}
Ok(())
}
fn read_store_file(path: &Path) -> Result<String> {
let metadata = fs::metadata(path).map_err(|source| StoreError::Read {
path: path.to_path_buf(),
source,
})?;
let size = usize::try_from(metadata.len()).map_err(|_| StoreError::Malformed {
reason: "file size exceeds supported platform limits".to_owned(),
})?;
if size > MAX_CONFIG_FILE_LEN {
return Err(StoreError::Malformed {
reason: format!(
"store file too large: {} bytes exceeds max {}",
size, MAX_CONFIG_FILE_LEN
),
}
.into());
}
fs::read_to_string(path).map_err(|source| {
StoreError::Read {
path: path.to_path_buf(),
source,
}
.into()
})
}
fn write_atomic(target: &Path, bytes: &[u8]) -> Result<()> {
let parent = target
.parent()
.ok_or_else(|| StoreError::malformed("target path must contain a parent directory"))?;
fs::create_dir_all(parent).map_err(|source| StoreError::PreparePath {
path: parent.to_path_buf(),
source,
})?;
let file_name = target
.file_name()
.ok_or_else(|| StoreError::malformed("target path must contain a file name"))?;
let temp_path = parent.join(format!(".{}.tmp", file_name.to_string_lossy()));
fs::write(&temp_path, bytes).map_err(|source| StoreError::Write {
path: temp_path.clone(),
source,
})?;
fs::rename(&temp_path, target).map_err(|source| StoreError::AtomicPersist {
path: target.to_path_buf(),
reason: format!("rename failed: {source}"),
})?;
Ok(())
}
#[must_use]
pub fn empty_store_file() -> StoreFile {
StoreFile {
version: STORE_FORMAT_VERSION,
records: Vec::new(),
}
}
pub fn initialize_if_missing(path: &Path) -> Result<()> {
if path.exists() {
return Ok(());
}
let file = empty_store_file();
let payload = serialize_store_file(&file)?;
write_atomic(path, payload.as_bytes())
}
#[must_use]
pub fn exists(path: &Path) -> bool {
path.is_file()
}
pub fn remove(path: &Path) -> Result<()> {
if !path.exists() {
return Ok(());
}
fs::remove_file(path).map_err(|source| {
StoreError::Write {
path: PathBuf::from(path),
source: io::Error::new(source.kind(), source.to_string()),
}
.into()
})
}