use std::{
fmt,
io::{self, Seek, SeekFrom, Write},
mem,
};
use bitflags::bitflags;
use bstr::{BStr, ByteSlice};
mod hash;
const CHUNK_ID: &[u8; 4] = b"CQDB";
const BYTEORDER_CHECK: u32 = 0x62445371;
const NUM_TABLES: usize = 256;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Flag: u32 {
const NONE = 0;
const ONEWAY = 0x00000001;
}
}
#[inline(always)]
fn read_u32_le(buf: &[u8], offset: usize) -> u32 {
let b = &buf[offset..offset + 4];
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
}
#[inline(always)]
fn pack_u32(value: u32) -> [u8; 4] {
value.to_le_bytes()
}
#[derive(Debug, Clone, Copy, Default)]
struct ReadTable {
offset: usize,
num: u32,
}
#[derive(Clone)]
pub struct CQDB<'a> {
buffer: &'a [u8],
header: Header,
tables: [ReadTable; NUM_TABLES],
bwd_offset: usize,
num: u32,
}
#[derive(Debug, Clone)]
#[repr(C)]
struct Header {
chunk_id: [u8; 4],
size: u32,
flag: u32,
byteorder: u32,
bwd_size: u32,
bwd_offset: u32,
}
#[derive(Debug, Clone, Default)]
struct Table {
size: usize,
num: u32,
bucket: Vec<Bucket>,
}
#[repr(C)]
struct TableRef {
offset: u32,
num: u32,
}
#[derive(Debug, Default, Clone, Copy)]
#[repr(C)]
struct Bucket {
hash: u32,
offset: u32,
}
pub struct CQDBWriter<T: Write + Seek> {
writer: T,
flag: Flag,
begin: u32,
current: u32,
tables: [Table; NUM_TABLES],
bwd: Vec<u32>,
bwd_num: u32,
bwd_size: u32,
}
impl<'a> fmt::Debug for CQDB<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CQDB")
.field("header", &self.header)
.field("bwd_offset", &self.bwd_offset)
.field("num", &self.num)
.finish()
}
}
impl<T: Write + Seek + fmt::Debug> fmt::Debug for CQDBWriter<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CQDBWriter")
.field("writer", &self.writer)
.field("flag", &self.flag)
.field("begin", &self.begin)
.field("current", &self.current)
.field("bwd", &self.bwd)
.field("bwd_num", &self.bwd_num)
.field("bwd_size", &self.bwd_size)
.finish()
}
}
impl<'a> CQDB<'a> {
pub fn new(buf: &'a [u8]) -> io::Result<Self> {
let min_size = mem::size_of::<Header>() + mem::size_of::<TableRef>() * NUM_TABLES;
if buf.len() < min_size {
return Err(io::Error::other("invalid file format"));
}
if &buf[0..4] != CHUNK_ID {
return Err(io::Error::other("invalid file format, magic mismatch"));
}
let chunk_size = read_u32_le(buf, 4);
let flag = read_u32_le(buf, 8);
let byte_order = read_u32_le(buf, 12);
if byte_order != BYTEORDER_CHECK {
return Err(io::Error::other("invalid file format, byte order mismatch"));
}
let bwd_size = read_u32_le(buf, 16);
let bwd_offset_raw = read_u32_le(buf, 20);
let header = Header {
chunk_id: *CHUNK_ID,
size: chunk_size,
flag,
byteorder: byte_order,
bwd_size,
bwd_offset: bwd_offset_raw,
};
let mut num_db = 0u32;
let mut tables = [ReadTable::default(); NUM_TABLES];
let mut index = 24; for table in &mut tables {
let table_offset = read_u32_le(buf, index) as usize;
index += 4;
let table_num = read_u32_le(buf, index);
index += 4;
if table_offset > 0 {
let end = (table_num as usize)
.checked_mul(8)
.and_then(|bytes| table_offset.checked_add(bytes));
match end {
Some(end) if end <= buf.len() => {
table.offset = table_offset;
table.num = table_num;
}
_ => return Err(io::Error::other("invalid table data: out of bounds")),
}
}
num_db += table_num / 2;
}
let bwd_offset = if bwd_offset_raw > 0 {
let off = bwd_offset_raw as usize;
let end = (bwd_size as usize)
.checked_mul(4)
.and_then(|bytes| off.checked_add(bytes));
match end {
Some(end) if end <= buf.len() => off,
_ => {
return Err(io::Error::other(
"invalid backward link data: out of bounds",
));
}
}
} else {
0
};
Ok(Self {
buffer: buf,
header,
tables,
bwd_offset,
num: num_db,
})
}
#[inline]
pub fn num(&self) -> u32 {
self.num
}
#[inline]
pub fn to_id(&self, s: &str) -> Option<u32> {
let hash = crate::hash::jhash(s.as_bytes(), s.len() as u32 + 1, 0);
let table = &self.tables[(hash % NUM_TABLES as u32) as usize];
if table.num > 0 {
let n = table.num;
let base = table.offset;
let mut k = (hash >> 8) % n;
loop {
let bk = &self.buffer[base + (k as usize) * 8..][..8];
let bucket_offset = u32::from_le_bytes([bk[4], bk[5], bk[6], bk[7]]);
if bucket_offset > 0 {
let bucket_hash = u32::from_le_bytes([bk[0], bk[1], bk[2], bk[3]]);
if bucket_hash == hash {
let rec_start = bucket_offset as usize;
let rec = self.buffer.get(rec_start..rec_start + 8)?;
let value = u32::from_le_bytes([rec[0], rec[1], rec[2], rec[3]]);
let ksize = (u32::from_le_bytes([rec[4], rec[5], rec[6], rec[7]]) as usize)
.checked_sub(1)?; let key_end = rec_start.checked_add(8 + ksize)?;
if s.as_bytes() == self.buffer.get(rec_start + 8..key_end)? {
return Some(value);
}
}
} else {
break;
}
k = (k + 1) % n;
}
}
None
}
#[inline]
pub fn to_str(&'a self, id: u32) -> Option<&'a BStr> {
if self.bwd_offset > 0 && id < self.header.bwd_size {
let offset = read_u32_le(self.buffer, self.bwd_offset + (id as usize) * 4);
if offset > 0 {
let index = offset as usize + 4; let rec = self.buffer.get(index..index + 4)?;
let value_size = (u32::from_le_bytes([rec[0], rec[1], rec[2], rec[3]]) as usize)
.checked_sub(1)?; let start = index + 4;
let end = start.checked_add(value_size)?;
return Some(self.buffer.get(start..end)?.as_bstr());
}
}
None
}
pub fn iter(&'a self) -> Iter<'a> {
Iter { db: self, next: 0 }
}
}
pub struct Iter<'a> {
db: &'a CQDB<'a>,
next: u32,
}
impl<'a> Iterator for Iter<'a> {
type Item = io::Result<(u32, &'a BStr)>;
fn next(&mut self) -> Option<Self::Item> {
let id = self.next;
if let Some(s) = self.db.to_str(id) {
self.next += 1;
return Some(Ok((id, s)));
}
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = if self.db.bwd_offset > 0 {
self.db.header.bwd_size.saturating_sub(self.next) as usize
} else {
0
};
(0, Some(remaining))
}
}
impl<'a> IntoIterator for &'a CQDB<'a> {
type Item = io::Result<(u32, &'a BStr)>;
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<T: Write + Seek> CQDBWriter<T> {
pub fn new(writer: T) -> io::Result<Self> {
Self::with_flag(writer, Flag::NONE)
}
pub fn with_flag(mut writer: T, flag: Flag) -> io::Result<Self> {
let begin = writer.stream_position()? as u32;
let current = (mem::size_of::<Header>() + mem::size_of::<TableRef>() * NUM_TABLES) as u32;
writer.seek(SeekFrom::Start((begin + current) as u64))?;
Ok(Self {
writer,
flag,
begin,
current,
tables: std::array::from_fn(|_| Table::default()),
bwd: Vec::new(),
bwd_num: 0,
bwd_size: 0,
})
}
pub fn put<K: AsRef<[u8]>>(&mut self, key: K, id: u32) -> io::Result<()> {
let key = key.as_ref();
let key_size = key.len() as u32 + 1; let hash = crate::hash::jhash(key, key_size, 0);
let table = &mut self.tables[hash as usize % 256];
let record_len = 8 + key.len() + 1;
if record_len <= 264 {
let mut buf = [0u8; 264]; buf[0..4].copy_from_slice(&pack_u32(id));
buf[4..8].copy_from_slice(&pack_u32(key_size));
buf[8..8 + key.len()].copy_from_slice(key);
self.writer.write_all(&buf[..record_len])?;
} else {
self.writer.write_all(&pack_u32(id))?;
self.writer.write_all(&pack_u32(key_size))?;
self.writer.write_all(key)?;
self.writer.write_all(b"\0")?;
}
if table.size <= table.num as usize {
table.size = (table.size + 1) * 2;
table.bucket.resize(table.size, Bucket::default());
}
table.bucket[table.num as usize].hash = hash;
table.bucket[table.num as usize].offset = self.current;
table.num += 1;
if !self.flag.contains(Flag::ONEWAY) {
if self.bwd_size <= id {
let mut size = self.bwd_size;
while size <= id {
size = (size + 1) * 2;
}
self.bwd.resize(size as usize, 0);
self.bwd_size = size;
}
if self.bwd_num <= id {
self.bwd_num = id + 1;
}
self.bwd[id as usize] = self.current;
}
self.current += 4 + 4 + key_size;
Ok(())
}
fn close(&mut self) -> io::Result<()> {
let mut header = Header {
chunk_id: *CHUNK_ID,
flag: self.flag.bits(),
byteorder: BYTEORDER_CHECK,
bwd_offset: 0,
bwd_size: self.bwd_num,
size: 0,
};
let mut dst: Vec<Bucket> = Vec::new();
#[cfg(not(target_endian = "little"))]
let mut write_buf: Vec<u8> = Vec::new();
for i in 0..NUM_TABLES {
let table = &self.tables[i];
if table.bucket.is_empty() {
continue;
}
let n = table.num * 2;
let n_usize = n as usize;
dst.clear();
dst.resize(n_usize, Bucket::default());
for j in 0..table.num as usize {
let src = &table.bucket[j];
let mut k = (src.hash >> 8) % n;
while dst[k as usize].offset != 0 {
k = (k + 1) % n;
}
dst[k as usize].hash = src.hash;
dst[k as usize].offset = src.offset;
}
#[cfg(target_endian = "little")]
{
let bytes =
unsafe { std::slice::from_raw_parts(dst.as_ptr() as *const u8, n_usize * 8) };
self.writer.write_all(bytes)?;
}
#[cfg(not(target_endian = "little"))]
{
write_buf.clear();
write_buf.reserve(n_usize * 8);
for bucket in &dst[..n_usize] {
write_buf.extend_from_slice(&pack_u32(bucket.hash));
write_buf.extend_from_slice(&pack_u32(bucket.offset));
}
self.writer.write_all(&write_buf)?;
}
}
if !self.flag.contains(Flag::ONEWAY) && self.bwd_size > 0 {
let current_offset = self.writer.stream_position()? as u32;
header.bwd_offset = current_offset - self.begin;
#[cfg(target_endian = "little")]
{
let bytes = unsafe {
std::slice::from_raw_parts(
self.bwd.as_ptr() as *const u8,
self.bwd_num as usize * 4,
)
};
self.writer.write_all(bytes)?;
}
#[cfg(not(target_endian = "little"))]
{
write_buf.clear();
write_buf.reserve(self.bwd_num as usize * 4);
for i in 0..self.bwd_num as usize {
write_buf.extend_from_slice(&pack_u32(self.bwd[i]));
}
self.writer.write_all(&write_buf)?;
}
}
let offset = self.writer.stream_position()? as u32;
header.size = offset - self.begin;
self.writer.seek(SeekFrom::Start(self.begin as u64))?;
let mut hdr_buf = [0u8; 24 + NUM_TABLES * 8];
hdr_buf[0..4].copy_from_slice(&header.chunk_id);
hdr_buf[4..8].copy_from_slice(&pack_u32(header.size));
hdr_buf[8..12].copy_from_slice(&pack_u32(header.flag));
hdr_buf[12..16].copy_from_slice(&pack_u32(header.byteorder));
hdr_buf[16..20].copy_from_slice(&pack_u32(header.bwd_size));
hdr_buf[20..24].copy_from_slice(&pack_u32(header.bwd_offset));
for i in 0..NUM_TABLES {
let table_num = self.tables[i].num;
let table_offset = if table_num > 0 { self.current } else { 0 };
let off = 24 + i * 8;
hdr_buf[off..off + 4].copy_from_slice(&pack_u32(table_offset));
hdr_buf[off + 4..off + 8].copy_from_slice(&pack_u32(table_num * 2));
self.current += table_num * 2 * std::mem::size_of::<Bucket>() as u32;
}
self.writer.write_all(&hdr_buf)?;
self.writer.seek(SeekFrom::Start(offset as u64))?;
Ok(())
}
}
impl<T: Write + Seek> Drop for CQDBWriter<T> {
fn drop(&mut self) {
if let Ok(()) = self.close() {}
}
}