use crate::compression::{ZstdDictionaries, ZstdDictionary};
use crate::encryption::EncryptionProvider;
use crate::file::{DICT_TMP_SUFFIX, DictDirEntry, DictId};
use crate::fs::{Fs, FsFile, FsOpenOptions, SyncMode};
#[cfg(not(feature = "std"))]
use crate::io::{Read, Write};
use crate::path::{Path, PathBuf};
use alloc::sync::Arc;
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::io::{Read, Write};
#[must_use]
pub fn path_of(folder: &Path, id: DictId) -> PathBuf {
folder.join(id.to_string())
}
pub fn write(
fs: &dyn Fs,
folder: &Path,
dict: &ZstdDictionary,
encryption: Option<&dyn EncryptionProvider>,
sync_mode: SyncMode,
) -> crate::Result<()> {
let final_path = path_of(folder, dict.id());
if fs.exists(&final_path)? {
match read_one(fs, folder, dict.id(), encryption) {
Ok(held) => {
if held.raw() != dict.raw() {
return Err(crate::Error::ZstdDictMismatch {
expected: dict.id(),
got: Some(held.id()),
});
}
return sync_entries(fs, folder, sync_mode);
}
Err(e) if is_damage(&e, dict.id()) => {}
Err(e) => return Err(e),
}
}
if !fs.exists(folder)? {
fs.create_dir_all(folder)?;
}
let sealed = encryption.map(|e| e.encrypt(dict.raw())).transpose()?;
let bytes = match &sealed {
Some(ciphertext) => ciphertext.as_slice(),
None => dict.raw(),
};
let tmp_path = folder.join(format!("{}{DICT_TMP_SUFFIX}", dict.id()));
let mut file = fs.open(
&tmp_path,
&FsOpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.write_origin(crate::fs::WriteOrigin::Metadata),
)?;
let written = file
.write_all(bytes)
.map_err(crate::io::Error::from)
.and_then(|()| file.flush().map_err(crate::io::Error::from))
.and_then(|()| FsFile::sync_all_with(&*file, sync_mode));
drop(file);
if let Err(e) = written {
let _ = fs.remove_file(&tmp_path);
return Err(e.into());
}
if let Err(e) = fs.rename(&tmp_path, &final_path) {
let _ = fs.remove_file(&tmp_path);
return Err(e.into());
}
sync_entries(fs, folder, sync_mode)
}
fn sync_entries(fs: &dyn Fs, folder: &Path, sync_mode: SyncMode) -> crate::Result<()> {
fs.sync_directory_with(folder, crate::file::folder_sync_mode(fs, folder, sync_mode))?;
if let Some(parent) = folder.parent() {
fs.sync_directory_with(parent, sync_mode)?;
}
Ok(())
}
pub fn read_one(
fs: &dyn Fs,
folder: &Path,
id: DictId,
encryption: Option<&dyn EncryptionProvider>,
) -> crate::Result<ZstdDictionary> {
let mut file = fs.open(&path_of(folder, id), &FsOpenOptions::new().read(true))?;
let mut raw = Vec::new();
file.read_to_end(&mut raw)?;
if let Some(e) = encryption {
raw = match e.decrypt_vec(raw) {
Ok(plain) => plain,
Err(crate::Error::Decrypt(_)) => {
return Err(crate::Error::ZstdDictCorrupt { id, got: None });
}
Err(other) => return Err(other),
};
}
let dict = ZstdDictionary::new(&raw);
if dict.id() != id {
return Err(crate::Error::ZstdDictCorrupt {
id,
got: Some(dict.id()),
});
}
Ok(dict)
}
pub fn read_all(
fs: &dyn Fs,
folder: &Path,
encryption: Option<&dyn EncryptionProvider>,
) -> crate::Result<ZstdDictionaries> {
if !fs.exists(folder)? {
return Ok(ZstdDictionaries::new());
}
let mut set = ZstdDictionaries::new();
for dirent in fs.read_dir(folder)? {
if let DictDirEntry::Dict(id) = DictDirEntry::classify(&dirent.file_name) {
set = set.with(Arc::new(read_one(fs, folder, id, encryption)?));
}
}
Ok(set)
}
fn is_damage(err: &crate::Error, id: DictId) -> bool {
matches!(err, crate::Error::ZstdDictCorrupt { id: filed, .. } if *filed == id)
}
pub fn read_all_skipping_damaged(
fs: &dyn Fs,
folder: &Path,
encryption: Option<&dyn EncryptionProvider>,
) -> crate::Result<(ZstdDictionaries, Vec<DictId>)> {
let mut set = ZstdDictionaries::new();
let mut damaged = Vec::new();
if !fs.exists(folder)? {
return Ok((set, damaged));
}
for dirent in fs.read_dir(folder)? {
let DictDirEntry::Dict(id) = DictDirEntry::classify(&dirent.file_name) else {
continue;
};
match read_one(fs, folder, id, encryption) {
Ok(dict) => set = set.with(Arc::new(dict)),
Err(e) if is_damage(&e, id) => damaged.push(id),
Err(e) => return Err(e),
}
}
Ok((set, damaged))
}
pub fn set_aside_if_damaged(
fs: &dyn Fs,
folder: &Path,
id: DictId,
encryption: Option<&dyn EncryptionProvider>,
sync_mode: SyncMode,
) -> crate::Result<Option<PathBuf>> {
if !is_still_damaged(fs, folder, id, encryption)? {
return Ok(None);
}
let aside = damaged_path(folder, id);
fs.rename(&path_of(folder, id), &aside)?;
fs.sync_directory_with(folder, sync_mode)?;
Ok(Some(aside))
}
pub fn is_still_damaged(
fs: &dyn Fs,
folder: &Path,
id: DictId,
encryption: Option<&dyn EncryptionProvider>,
) -> crate::Result<bool> {
match read_one(fs, folder, id, encryption) {
Ok(_) => Ok(false),
Err(crate::Error::Io(e)) if e.kind() == crate::io::ErrorKind::NotFound => Ok(false),
Err(e) if is_damage(&e, id) => Ok(true),
Err(e) => Err(e),
}
}
#[must_use]
pub fn damaged_path(folder: &Path, id: DictId) -> PathBuf {
folder.join(format!("{id}{}", crate::file::DICT_DAMAGED_SUFFIX))
}
pub fn list_ids(fs: &dyn Fs, folder: &Path) -> crate::Result<Vec<DictId>> {
if !fs.exists(folder)? {
return Ok(Vec::new());
}
let mut ids = Vec::new();
for dirent in fs.read_dir(folder)? {
if let DictDirEntry::Dict(id) = DictDirEntry::classify(&dirent.file_name) {
ids.push(id);
}
}
Ok(ids)
}
pub fn remove(fs: &dyn Fs, folder: &Path, id: DictId, sync_mode: SyncMode) -> crate::Result<()> {
match fs.remove_file(&path_of(folder, id)) {
Ok(()) => {}
Err(e) if e.kind() == crate::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e.into()),
}
fs.sync_directory_with(folder, sync_mode)?;
Ok(())
}
pub fn sweep_temps(fs: &dyn Fs, folder: &Path) -> crate::Result<()> {
if !fs.exists(folder)? {
return Ok(());
}
for dirent in fs.read_dir(folder)? {
let name = dirent.file_name;
if let DictDirEntry::Tmp(id) = DictDirEntry::classify(&name) {
let path = folder.join(&name);
if let Err(e) = fs.remove_file(&path) {
log::warn!("Could not remove unpublished dictionary temp {id}: {e:?}");
}
}
}
Ok(())
}
#[cfg(test)]
#[expect(clippy::unwrap_used, reason = "test code")]
mod tests;