use std::collections::HashMap;
use std::path::Path;
use rayon::prelude::*;
pub type Pool = Vec<String>;
#[derive(Debug)]
pub struct PoolStore {
pub pools: HashMap<String, Pool>,
}
const LOCALES: &[&str] = &["en", "fr", "de", "es", "it", "pt"];
fn is_locale_key(key: &str) -> bool {
LOCALES.contains(&key)
}
fn load_one(entry: &std::fs::DirEntry, locale: &str) -> Result<Option<(String, Pool)>, String> {
let name = entry
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
let raw = std::fs::read_to_string(entry.path())
.map_err(|e| format!("cannot open {name}.json: {e}"))?;
let data: serde_json::Value =
serde_json::from_str(&raw).map_err(|e| format!("cannot parse {name}.json: {e}"))?;
let pool = match &data {
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect::<Pool>(),
serde_json::Value::Object(_map) => {
if let Some(arr) = data
.get(locale)
.or_else(|| data.get("en"))
.and_then(|v| v.as_array())
{
arr.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect::<Pool>()
} else if let Some(obj) = data.as_object() {
let has_locale_keys = obj.keys().any(|k| is_locale_key(k));
if !has_locale_keys {
obj.values()
.filter_map(|v| v.as_array())
.flatten()
.filter_map(|v| v.as_str().or_else(|| v.get(0).and_then(|s| s.as_str())))
.map(String::from)
.collect::<Pool>()
} else {
Vec::new()
}
} else {
Vec::new()
}
}
_ => return Ok(None),
};
if pool.is_empty() {
return Ok(None);
}
Ok(Some((name, pool)))
}
impl PoolStore {
pub fn load(pools_dir: &str, locale: &str) -> Result<Self, String> {
let dir = Path::new(pools_dir);
if !dir.is_dir() {
return Err(format!("pools dir not found: {pools_dir}"));
}
let mut entries: Vec<_> = std::fs::read_dir(dir)
.map_err(|e| format!("cannot read {pools_dir}: {e}"))?
.filter_map(|r| r.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.collect();
entries.sort_by_key(|e| e.file_name());
let loaded: Vec<Option<(String, Pool)>> = entries
.par_iter()
.map(|entry| load_one(entry, locale))
.collect::<Result<Vec<_>, String>>()?;
let pools: HashMap<String, Pool> = loaded.into_iter().flatten().collect();
Ok(Self { pools })
}
pub fn get(&self, name: &str) -> Option<&Pool> {
self.pools.get(name)
}
}
#[derive(Debug)]
pub struct Context {
pub pool_store: PoolStore,
pub locale: String,
pub watermark_map: std::collections::HashMap<u64, u64>, }
impl Context {
const WATERMARK_SECRET: &'static str = "DupeHell-WATERMARK-v0.4-educational-only-2026";
pub fn new(domain: &str, locale: &str, pools_dir: &str) -> Result<Self, String> {
let pool_store = PoolStore::load(pools_dir, locale)?;
log::info!(
"Context loaded: domain={domain}, locale={locale}, pools={}",
pool_store.pools.len()
);
Ok(Self {
pool_store,
locale: locale.to_string(),
watermark_map: std::collections::HashMap::new(),
})
}
pub fn enable_watermark(&mut self, domain: &str, size: usize, seed: u64) {
use sha2::{Digest, Sha256};
for &tag in &[
0x53534e, 0x50484f4e, 0x50414e, 0x4d4544, 0x4f4643, 0x504153, 0x414354, 0x424152, 0x494343, 0x555043, ] {
let input = format!(
"{}{}{}{}{}",
Self::WATERMARK_SECRET,
domain,
size,
seed,
tag
);
let hash = Sha256::digest(input.as_bytes());
let wm = u64::from_le_bytes(hash[..8].try_into().unwrap());
self.watermark_map.insert(tag, wm);
}
}
pub fn watermark_3digits(&self, tag: u64) -> u64 {
self.watermark_map.get(&tag).copied().unwrap_or(0) % 1000
}
pub fn watermark_2digits(&self, tag: u64) -> u64 {
self.watermark_map.get(&tag).copied().unwrap_or(0) % 100
}
#[cfg(test)]
pub fn test() -> Self {
Self {
pool_store: PoolStore {
pools: HashMap::new(),
},
locale: "en".to_string(),
watermark_map: HashMap::new(),
}
}
}