use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::Path;
use crc32fast::Hasher as Crc32Hasher;
use super::error::{FuseError, FuseResult};
use super::format::{crc32, HEADER_SIZE, MAGIC, MAX_KEY_LEN, MAX_OBJECT_LEN, VERSION};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuildReport {
pub num_objects: usize,
pub num_keys: usize,
pub file_size: u64,
}
pub struct WriterCore {
pub(crate) objects: Vec<Vec<u8>>,
pub(crate) keys: HashMap<Vec<u8>, usize>,
}
impl WriterCore {
pub fn new() -> Self {
Self {
objects: Vec::new(),
keys: HashMap::new(),
}
}
pub fn add_object(&mut self, raw_bytes: &[u8]) -> usize {
let id = self.objects.len();
self.objects.push(raw_bytes.to_vec());
id
}
pub fn add_key(&mut self, key: &[u8], obj_id: usize) -> FuseResult<()> {
if obj_id >= self.objects.len() {
return Err(FuseError::InvalidArg(format!(
"obj_id {obj_id} out of range (have {} objects)",
self.objects.len()
)));
}
if key.len() > MAX_KEY_LEN {
return Err(FuseError::InvalidArg(format!(
"key is {} bytes, format maximum is {MAX_KEY_LEN}",
key.len()
)));
}
self.keys.insert(key.to_vec(), obj_id);
Ok(())
}
pub fn build(&self, path: impl AsRef<Path>) -> FuseResult<BuildReport> {
let path = path.as_ref();
let tmp = path.with_extension("fsdb.tmp");
if self.objects.len() > u32::MAX as usize {
return Err(FuseError::InvalidArg(format!(
"{} objects exceeds format maximum of {}",
self.objects.len(),
u32::MAX
)));
}
if self.keys.len() > u32::MAX as usize {
return Err(FuseError::InvalidArg(format!(
"{} keys exceeds format maximum of {}",
self.keys.len(),
u32::MAX
)));
}
let mut data_sec: Vec<u8> = Vec::new();
let mut obj_offsets: Vec<u64> = Vec::with_capacity(self.objects.len());
for raw in &self.objects {
if raw.len() > MAX_OBJECT_LEN {
return Err(FuseError::InvalidArg(format!(
"object is {} bytes, format maximum is {MAX_OBJECT_LEN}",
raw.len()
)));
}
obj_offsets.push((HEADER_SIZE + data_sec.len()) as u64);
data_sec.extend_from_slice(&(raw.len() as u32).to_be_bytes());
data_sec.extend_from_slice(&crc32(raw).to_be_bytes());
data_sec.extend_from_slice(raw);
}
let mut sorted: Vec<(&Vec<u8>, usize)> = self.keys.iter().map(|(k, &id)| (k, id)).collect();
sorted.sort_by_key(|(k, _)| k.as_slice());
let mut idx_sec: Vec<u8> = Vec::new();
for (key, obj_id) in &sorted {
idx_sec.extend_from_slice(&(key.len() as u16).to_be_bytes());
idx_sec.extend_from_slice(key);
idx_sec.extend_from_slice(&obj_offsets[*obj_id].to_be_bytes());
}
let mut h = Crc32Hasher::new();
h.update(&data_sec);
h.update(&idx_sec);
let file_crc = h.finalize();
let index_offset = (HEADER_SIZE + data_sec.len()) as u64;
let mut hdr = [0u8; HEADER_SIZE];
hdr[0..4].copy_from_slice(MAGIC.as_slice());
hdr[4] = VERSION;
hdr[8..12].copy_from_slice(&(self.keys.len() as u32).to_be_bytes());
hdr[12..16].copy_from_slice(&(self.objects.len() as u32).to_be_bytes());
hdr[16..24].copy_from_slice(&index_offset.to_be_bytes());
hdr[24..32].copy_from_slice(&(HEADER_SIZE as u64).to_be_bytes());
hdr[32..36].copy_from_slice(&file_crc.to_be_bytes());
{
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&tmp)
.map_err(|e| FuseError::Io(format!("open tmp: {e}")))?;
let mut w = BufWriter::new(&file);
w.write_all(&hdr)
.and_then(|_| w.write_all(&data_sec))
.and_then(|_| w.write_all(&idx_sec))
.and_then(|_| w.flush())
.map_err(|e| FuseError::Io(e.to_string()))?;
file.sync_all().map_err(|e| FuseError::Io(e.to_string()))?;
}
fs::rename(&tmp, path).map_err(|e| FuseError::Io(format!("rename: {e}")))?;
Ok(BuildReport {
num_objects: self.objects.len(),
num_keys: self.keys.len(),
file_size: (HEADER_SIZE + data_sec.len() + idx_sec.len()) as u64,
})
}
#[inline]
pub fn num_objects(&self) -> usize {
self.objects.len()
}
#[inline]
pub fn num_keys(&self) -> usize {
self.keys.len()
}
}
impl Default for WriterCore {
fn default() -> Self {
Self::new()
}
}