use std::sync::{Arc, OnceLock};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use super::error::check_limit;
use super::{
DictionaryError, DictionaryId, DictionaryLimits, DictionaryResult, NoriDictionary,
UserDictionary, UserDictionaryLimits,
};
mod hash;
use crate::cache::Cache;
pub use hash::ResourceHash;
pub const DEFAULT_NORI_DICTIONARY: &str = "lucene-10.5.1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DictionaryRequest {
Name(String),
Sha256(ResourceHash),
}
impl std::fmt::Display for DictionaryRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Name(name) => formatter.write_str(name),
Self::Sha256(hash) => write!(formatter, "sha256:{hash}"),
}
}
}
#[derive(Clone)]
pub enum DictionaryBytes {
Static(&'static [u8]),
Shared(Arc<[u8]>),
}
impl AsRef<[u8]> for DictionaryBytes {
fn as_ref(&self) -> &[u8] {
match self {
Self::Static(bytes) => bytes,
Self::Shared(bytes) => bytes,
}
}
}
pub struct DictionaryArtifact {
pub sha256: ResourceHash,
pub bytes: DictionaryBytes,
}
pub trait DictionaryResolver: Send + Sync {
fn resolve(&self, request: &DictionaryRequest) -> DictionaryResult<Option<DictionaryArtifact>>;
}
impl<F> DictionaryResolver for F
where
F: Fn(&DictionaryRequest) -> DictionaryResult<Option<DictionaryArtifact>> + Send + Sync,
{
fn resolve(&self, request: &DictionaryRequest) -> DictionaryResult<Option<DictionaryArtifact>> {
self(request)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ResourceLimits {
pub max_cached_dictionaries: usize,
pub max_cached_encoded_bytes: usize,
pub max_cached_user_dictionaries: usize,
pub max_cached_user_source_bytes: usize,
pub dictionary: DictionaryLimits,
pub user_dictionary: UserDictionaryLimits,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_cached_dictionaries: 2,
max_cached_encoded_bytes: 32 * 1024 * 1024,
max_cached_user_dictionaries: 64,
max_cached_user_source_bytes: 8 * 1024 * 1024,
dictionary: DictionaryLimits::default(),
user_dictionary: UserDictionaryLimits::default(),
}
}
}
pub struct ResolvedDictionary {
sha256: ResourceHash,
bytes: DictionaryBytes,
model: Arc<NoriDictionary>,
}
impl std::fmt::Debug for ResolvedDictionary {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ResolvedDictionary")
.field("sha256", &self.sha256)
.field("model", &self.model)
.finish_non_exhaustive()
}
}
impl ResolvedDictionary {
pub fn sha256(&self) -> ResourceHash {
self.sha256
}
pub fn model(&self) -> &Arc<NoriDictionary> {
&self.model
}
pub fn bytes(&self) -> &[u8] {
self.bytes.as_ref()
}
}
pub struct ResolvedUserDictionary {
sha256: ResourceHash,
model_id: DictionaryId,
dictionary: Option<Arc<UserDictionary>>,
empty_source: Option<Arc<str>>,
}
impl ResolvedUserDictionary {
pub fn sha256(&self) -> ResourceHash {
self.sha256
}
pub fn model_id(&self) -> DictionaryId {
self.model_id
}
pub fn dictionary(&self) -> Option<&Arc<UserDictionary>> {
self.dictionary.as_ref()
}
pub fn source(&self) -> &str {
match &self.dictionary {
Some(dictionary) => dictionary.source(),
None => self.empty_source.as_deref().expect("retained empty rules"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceCacheStats {
pub dictionaries: usize,
pub dictionary_encoded_bytes: usize,
pub user_dictionaries: usize,
pub user_source_bytes: usize,
}
#[derive(Default)]
struct State {
dictionaries: Cache<ResourceHash, ResolvedDictionary>,
users: Cache<(DictionaryId, ResourceHash), ResolvedUserDictionary>,
}
struct Inner {
resolver: Arc<dyn DictionaryResolver>,
limits: ResourceLimits,
state: Mutex<State>,
}
#[derive(Clone)]
pub struct NoriResources(Arc<Inner>);
impl Default for NoriResources {
fn default() -> Self {
static RESOURCES: OnceLock<NoriResources> = OnceLock::new();
RESOURCES
.get_or_init(|| {
Self::with_resolver(Arc::new(BundledResolver), ResourceLimits::default())
})
.clone()
}
}
impl NoriResources {
pub fn with_resolver(resolver: Arc<dyn DictionaryResolver>, limits: ResourceLimits) -> Self {
Self(Arc::new(Inner {
resolver,
limits,
state: Mutex::new(State::default()),
}))
}
pub fn limits(&self) -> ResourceLimits {
self.0.limits
}
pub fn cache_stats(&self) -> ResourceCacheStats {
let state = self.0.state.lock();
ResourceCacheStats {
dictionaries: state.dictionaries.len(),
dictionary_encoded_bytes: state.dictionaries.weight(),
user_dictionaries: state.users.len(),
user_source_bytes: state.users.weight(),
}
}
pub fn load_default(&self) -> DictionaryResult<Arc<ResolvedDictionary>> {
self.load(&DictionaryRequest::Name(DEFAULT_NORI_DICTIONARY.into()))
}
pub fn load(&self, request: &DictionaryRequest) -> DictionaryResult<Arc<ResolvedDictionary>> {
if let DictionaryRequest::Sha256(hash) = request {
if let Some(cached) = self.0.state.lock().dictionaries.get(hash) {
return Ok(cached);
}
}
let artifact = self
.0
.resolver
.resolve(request)?
.ok_or_else(|| DictionaryError::ResourceMissing(request.to_string()))?;
check_limit(
"encoded bytes",
artifact.bytes.as_ref().len(),
self.0.limits.dictionary.max_encoded_bytes,
)?;
let hash = ResourceHash::of(artifact.bytes.as_ref());
verify_hash(artifact.sha256, hash)?;
if let DictionaryRequest::Sha256(expected) = request {
verify_hash(*expected, hash)?;
}
let mut state = self.0.state.lock();
if let Some(cached) = state.dictionaries.get(&hash) {
return Ok(cached);
}
let model = NoriDictionary::from_bytes(artifact.bytes.as_ref(), self.0.limits.dictionary)?;
let resolved = Arc::new(ResolvedDictionary {
sha256: hash,
bytes: artifact.bytes,
model,
});
state.dictionaries.insert(
hash,
resolved.clone(),
resolved.bytes().len(),
self.0.limits.max_cached_dictionaries,
self.0.limits.max_cached_encoded_bytes,
);
Ok(resolved)
}
pub fn compile_user(
&self,
source: &str,
model: &NoriDictionary,
) -> DictionaryResult<Arc<ResolvedUserDictionary>> {
check_limit(
"user dictionary bytes",
source.len(),
self.0.limits.user_dictionary.max_bytes,
)?;
let hash = ResourceHash::of(source.as_bytes());
let key = (model.id(), hash);
let mut state = self.0.state.lock();
if let Some(cached) = state.users.get(&key) {
return Ok(cached);
}
let dictionary = UserDictionary::compile(source, model, self.0.limits.user_dictionary)?;
let empty_source = dictionary.is_none().then(|| Arc::from(source));
let resolved = Arc::new(ResolvedUserDictionary {
sha256: hash,
model_id: model.id(),
dictionary,
empty_source,
});
state.users.insert(
key,
resolved.clone(),
source.len(),
self.0.limits.max_cached_user_dictionaries,
self.0.limits.max_cached_user_source_bytes,
);
Ok(resolved)
}
}
fn verify_hash(expected: ResourceHash, actual: ResourceHash) -> DictionaryResult<()> {
if expected != actual {
return Err(DictionaryError::ResourceHashMismatch { expected, actual });
}
Ok(())
}
struct BundledResolver;
impl DictionaryResolver for BundledResolver {
fn resolve(&self, request: &DictionaryRequest) -> DictionaryResult<Option<DictionaryArtifact>> {
let hash = uqa_nori_data::BUNDLE_SHA256.parse()?;
let matches = match request {
DictionaryRequest::Name(name) => name == DEFAULT_NORI_DICTIONARY,
DictionaryRequest::Sha256(expected) => *expected == hash,
};
Ok(matches.then_some(DictionaryArtifact {
sha256: hash,
bytes: DictionaryBytes::Static(uqa_nori_data::BUNDLE),
}))
}
}
#[cfg(test)]
mod tests;