use std::path::{Path, PathBuf};
use cbvault_format::cbh::Entity;
use cbvault_format::cbh::bytes::NameBuf;
use cbvault_format::error::{Error, Result};
use cbvault_format::file::DbFile;
const MAGIC: i32 = 1_234_567_890;
const HEADER: u64 = 28;
const MAX_DATA: i32 = 64 << 10;
const DELETED: i32 = -999;
const NODE_HEAD: usize = 9;
struct Node<'a> {
left: i32,
right: i32,
data: &'a [u8],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Player,
Tournament,
Annotator,
Source,
Team,
}
impl Kind {
const fn ext(self) -> &'static str {
match self {
Kind::Player => ".cbp",
Kind::Tournament => ".cbt",
Kind::Annotator => ".cbc",
Kind::Source => ".cbs",
Kind::Team => ".cbe",
}
}
const fn min_data(self) -> i32 {
match self {
Kind::Player => 50,
Kind::Tournament => 0x4a,
Kind::Annotator => 45,
Kind::Source => 25,
Kind::Team => 50,
}
}
const fn name_width(self) -> usize {
match self {
Kind::Player => 30,
Kind::Tournament => 40,
Kind::Annotator => 45,
Kind::Source => 25,
Kind::Team => 50,
}
}
const fn second_width(self) -> usize {
match self {
Kind::Player => 20,
Kind::Tournament => 30,
_ => 0,
}
}
}
struct Namebase {
file: DbFile,
header: u64,
record: u64,
count: u32,
root: i32,
kind: Kind,
}
impl Namebase {
fn open(path: PathBuf, kind: Kind) -> Result<Self> {
let file = DbFile::open(path.clone())?;
let len = file.size()?;
if len < HEADER {
return Err(Error::corrupt(&path, 0, format!("{len}-byte file is shorter than its header")));
}
let head = file.read(0, HEADER as usize)?;
let int = |o: usize| i32::from_le_bytes(head[o..o + 4].try_into().unwrap_or_default());
if int(0x08) != MAGIC {
return Err(Error::corrupt(&path, 0x08, "bad magic"));
}
let data = int(0x0c);
if !(kind.min_data()..=MAX_DATA).contains(&data) {
return Err(Error::corrupt(&path, 0x0c, format!("record data size {data}")));
}
let extra = int(0x18);
if extra != 0 && extra != 4 {
return Err(Error::corrupt(&path, 0x18, format!("{extra} extra header bytes")));
}
let header = HEADER + extra as u64;
let record = NODE_HEAD as u64 + data as u64;
Ok(Namebase {
file,
header,
record,
count: u32::try_from(len.saturating_sub(header) / record).unwrap_or(u32::MAX),
root: int(0x04),
kind,
})
}
fn count(&self) -> u32 {
self.count
}
fn node(&self, id: i32) -> Result<Option<Node<'_>>> {
if id < 0 || u64::from(id as u32) >= u64::from(self.count) {
return Ok(None);
}
let at = self.header + u64::from(id as u32) * self.record;
let Some(slice) = self.file.slice_at(at, self.record as usize) else {
return Err(Error::corrupt(
self.file.path(),
at,
"an entity name is borrowed from the memory map; build with the `mmap` feature",
));
};
Ok(node_of(slice))
}
fn name(&self, id: u32) -> Result<Option<Name<'_>>> {
let Some(node) = self.node(id as i32)? else { return Ok(None) };
let last = self.key(&node);
if last.is_empty() {
return Ok(None);
}
Ok(Some(Name { last, first: self.second(&node) }))
}
fn key<'a>(&self, node: &Node<'a>) -> &'a [u8] {
field(node.data, 0, self.kind.name_width())
}
fn second<'a>(&self, node: &Node<'a>) -> &'a [u8] {
let width = self.kind.second_width();
if width == 0 {
return &[];
}
field(node.data, self.kind.name_width(), width)
}
fn descend(&self, name: &str) -> Result<Option<u32>> {
let want = name.as_bytes();
let mut at = self.root;
for _ in 0..=self.count {
let Some(node) = self.node(at)? else { return Ok(None) };
match self.key(&node).cmp(want) {
std::cmp::Ordering::Equal => return Ok(Some(at as u32)),
std::cmp::Ordering::Less => at = node.right,
std::cmp::Ordering::Greater => at = node.left,
}
}
Ok(None)
}
fn scan(&self, name: &str) -> Result<Option<u32>> {
let want = name.as_bytes();
for id in 0..self.count {
let Some(node) = self.node(id as i32)? else { continue };
if self.key(&node) == want {
return Ok(Some(id));
}
}
Ok(None)
}
fn find_all(&self, name: &str) -> Result<Vec<u32>> {
if name.is_empty() {
return Ok(Vec::new());
}
let want = name.as_bytes();
let mut found = Vec::new();
let mut stack = vec![self.root];
let mut seen: Vec<u32> = Vec::with_capacity(self.count as usize);
let budget = self.count.saturating_mul(3).saturating_add(8);
let mut pops = 0u32;
while let Some(at) = stack.pop() {
if pops > budget {
break;
}
pops += 1;
let Some(node) = self.node(at)? else { continue };
if seen.contains(&(at as u32)) {
continue;
}
seen.push(at as u32);
stack.push(node.right);
stack.push(node.left);
if self.key(&node) == want {
found.push(at as u32);
}
}
if found.is_empty() {
if let Some(id) = self.scan(name)? {
found.push(id);
}
}
found.sort_unstable();
found.dedup();
Ok(found)
}
fn find_with(&self, name: &str) -> Result<Option<Found>> {
if name.is_empty() {
return Ok(None);
}
if let Some(id) = self.descend(name)? {
return Ok(Some(Found { id, via: Via::Tree }));
}
Ok(self.scan(name)?.map(|id| Found { id, via: Via::Scan }))
}
}
fn node_of(bytes: &[u8]) -> Option<Node<'_>> {
if bytes.len() < NODE_HEAD {
return None;
}
let int = |o: usize| i32::from_le_bytes(bytes[o..o + 4].try_into().unwrap_or_default());
let left = int(0);
if left == DELETED {
return None;
}
Some(Node { left, right: int(4), data: &bytes[NODE_HEAD..] })
}
fn field(data: &[u8], at: usize, width: usize) -> &[u8] {
if at >= data.len() {
return &[];
}
let end = at.saturating_add(width).min(data.len());
let field = &data[at..end];
let stop = field.iter().position(|&b| b == 0).unwrap_or(field.len());
&field[..stop]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Via {
Tree,
Scan,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Found {
pub id: u32,
pub via: Via,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Name<'a> {
last: &'a [u8],
first: &'a [u8],
}
impl<'a> Name<'a> {
pub const fn blank() -> Name<'a> {
Name { last: &[], first: &[] }
}
pub const fn of(last: &'a [u8]) -> Name<'a> {
Name { last, first: &[] }
}
pub const fn pair(last: &'a [u8], first: &'a [u8]) -> Name<'a> {
Name { last, first }
}
#[inline]
pub fn last(&self) -> &'a [u8] {
self.last
}
#[inline]
pub fn first(&self) -> &'a [u8] {
self.first
}
#[inline]
pub fn is_empty(&self) -> bool {
self.last.is_empty() && self.first.is_empty()
}
#[inline]
pub fn as_str(&self) -> Option<&'a str> {
if !self.first.is_empty() {
return None;
}
std::str::from_utf8(self.last).ok()
}
#[inline]
pub fn decode_last<'b>(&self, buf: &'b mut NameBuf) -> &'b str {
buf.set(self.last);
buf.as_str()
}
#[inline]
pub fn decode_first<'b>(&self, buf: &'b mut NameBuf) -> &'b str {
if self.first.is_empty() {
return "";
}
buf.set(self.first);
buf.as_str()
}
pub fn push_text(&self, out: &mut String) {
let mut buf = NameBuf::new();
if self.first.is_empty() {
out.push_str(self.decode_last(&mut buf));
return;
}
if !self.last.is_empty() {
out.push_str(self.decode_last(&mut buf));
out.push_str(", ");
}
out.push_str(self.decode_first(&mut buf));
}
}
pub struct Entities {
players: Namebase,
tournaments: Namebase,
annotators: Namebase,
sources: Namebase,
teams: Option<Namebase>,
}
impl std::fmt::Debug for Entities {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Entities")
.field("players", &self.players.count())
.field("tournaments", &self.tournaments.count())
.field("annotators", &self.annotators.count())
.field("sources", &self.sources.count())
.field("teams", &self.teams.as_ref().map(Namebase::count))
.finish()
}
}
impl Entities {
pub fn open(stem: &Path) -> Result<Self> {
let file = |kind: Kind| Namebase::open(super::sibling(stem, kind.ext()), kind);
let teams = match file(Kind::Team) {
Ok(teams) => Some(teams),
Err(Error::Io { .. }) => None,
Err(e) => return Err(e),
};
Ok(Entities {
players: file(Kind::Player)?,
tournaments: file(Kind::Tournament)?,
annotators: file(Kind::Annotator)?,
sources: file(Kind::Source)?,
teams,
})
}
pub fn counts(&self) -> [u64; 4] {
[
u64::from(self.players.count()),
u64::from(self.tournaments.count()),
u64::from(self.annotators.count()),
u64::from(self.sources.count()),
]
}
pub fn team_count(&self) -> u64 {
self.teams.as_ref().map_or(0, |teams| u64::from(teams.count()))
}
fn of(&self, entity: Entity) -> Option<&Namebase> {
Some(match entity {
Entity::Player => &self.players,
Entity::Tournament => &self.tournaments,
Entity::Annotator => &self.annotators,
Entity::Source => &self.sources,
Entity::Team => self.teams.as_ref()?,
})
}
pub fn name(&self, entity: Entity, id: u32) -> Result<Option<Name<'_>>> {
match self.of(entity) {
Some(file) => file.name(id),
None => Ok(None),
}
}
pub fn for_each(&self, entity: Entity, mut f: impl FnMut(u32, Name<'_>)) -> Result<()> {
let Some(file) = self.of(entity) else { return Ok(()) };
for id in 0..file.count() {
if let Some(name) = file.name(id)? {
f(id, name);
}
}
Ok(())
}
pub fn find_player_with(&self, name: &str) -> Result<Option<Found>> {
self.players.find_with(name)
}
pub fn find_player(&self, name: &str) -> Result<Option<u32>> {
Ok(self.find_player_with(name)?.map(|found| found.id))
}
pub fn find_players(&self, name: &str) -> Result<Vec<u32>> {
self.players.find_all(name)
}
pub fn find_tournaments(&self, name: &str) -> Result<Vec<u32>> {
self.tournaments.find_all(name)
}
pub fn find_annotators(&self, name: &str) -> Result<Vec<u32>> {
self.annotators.find_all(name)
}
pub fn find_sources(&self, name: &str) -> Result<Vec<u32>> {
self.sources.find_all(name)
}
pub fn find_teams(&self, name: &str) -> Result<Vec<u32>> {
Ok(self.teams.as_ref().map(|t| t.find_all(name)).transpose()?.unwrap_or_default())
}
pub fn find_tournament_with(&self, name: &str) -> Result<Option<Found>> {
self.tournaments.find_with(name)
}
pub fn find_tournament(&self, name: &str) -> Result<Option<u32>> {
Ok(self.find_tournament_with(name)?.map(|found| found.id))
}
pub fn find_annotator_with(&self, name: &str) -> Result<Option<Found>> {
self.annotators.find_with(name)
}
pub fn find_annotator(&self, name: &str) -> Result<Option<u32>> {
Ok(self.find_annotator_with(name)?.map(|found| found.id))
}
pub fn find_source_with(&self, name: &str) -> Result<Option<Found>> {
self.sources.find_with(name)
}
pub fn find_source(&self, name: &str) -> Result<Option<u32>> {
Ok(self.find_source_with(name)?.map(|found| found.id))
}
pub fn find_team(&self, name: &str) -> Result<Option<u32>> {
let Some(teams) = &self.teams else { return Ok(None) };
Ok(teams.find_with(name)?.map(|found| found.id))
}
pub fn player_text(&self, id: u32, buf: &mut String) -> Result<bool> {
let Some(name) = self.players.name(id)? else { return Ok(false) };
name.push_text(buf);
Ok(true)
}
pub fn entity_text(&self, entity: Entity, id: u32, buf: &mut String) -> Result<bool> {
let Some(name) = self.name(entity, id)? else { return Ok(false) };
name.push_text(buf);
Ok(true)
}
}