1use std::collections::HashMap;
8use std::path::Path;
9
10pub type Pool = Vec<String>;
12
13#[derive(Clone, Debug)]
15pub struct PoolStore {
16 pub pools: HashMap<String, Pool>,
17}
18
19const LOCALES: &[&str] = &["en", "fr", "de", "es", "it", "pt"];
21
22fn is_locale_key(key: &str) -> bool {
23 LOCALES.contains(&key)
24}
25
26impl PoolStore {
27 pub fn load(pools_dir: &str, locale: &str) -> Result<Self, String> {
30 let dir = Path::new(pools_dir);
31 if !dir.is_dir() {
32 return Err(format!("pools dir not found: {pools_dir}"));
33 }
34 let mut pools = HashMap::new();
35 let mut entries: Vec<_> = std::fs::read_dir(dir)
36 .map_err(|e| format!("cannot read {pools_dir}: {e}"))?
37 .filter_map(|r| r.ok())
38 .filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
39 .collect();
40 entries.sort_by_key(|e| e.file_name());
41
42 for entry in entries {
43 let name = entry
44 .path()
45 .file_stem()
46 .unwrap()
47 .to_string_lossy()
48 .to_string();
49 let raw = std::fs::read_to_string(entry.path())
55 .map_err(|e| format!("cannot open {name}.json: {e}"))?;
56 let data: serde_json::Value =
57 serde_json::from_str(&raw).map_err(|e| format!("cannot parse {name}.json: {e}"))?;
58
59 match &data {
60 serde_json::Value::Array(arr) => {
61 let pool = arr
62 .iter()
63 .filter_map(|v| v.as_str())
64 .map(String::from)
65 .collect::<Pool>();
66 pools.insert(name, pool);
67 }
68 serde_json::Value::Object(_map) => {
69 let pool = if let Some(arr) = data
70 .get(locale)
71 .or_else(|| data.get("en"))
72 .and_then(|v| v.as_array())
73 {
74 arr.iter()
75 .filter_map(|v| v.as_str())
76 .map(String::from)
77 .collect::<Pool>()
78 } else if let Some(obj) = data.as_object() {
79 let has_locale_keys = obj.keys().any(|k| is_locale_key(k));
82 if !has_locale_keys {
83 obj.values()
84 .filter_map(|v| v.as_array())
85 .flatten()
86 .filter_map(|v| {
87 v.as_str().or_else(|| v.get(0).and_then(|s| s.as_str()))
88 })
89 .map(String::from)
90 .collect::<Pool>()
91 } else {
92 Vec::new()
93 }
94 } else {
95 Vec::new()
96 };
97 if !pool.is_empty() {
98 pools.insert(name, pool);
99 }
100 }
101 _ => {}
102 };
103 }
104 Ok(Self { pools })
105 }
106
107 pub fn get(&self, name: &str) -> Option<&Pool> {
108 self.pools.get(name)
109 }
110}
111
112#[derive(Clone, Debug)]
114pub struct Context {
115 pub pool_store: PoolStore,
116 pub locale: String,
117 pub watermark_map: std::collections::HashMap<u64, u64>, }
119
120impl Context {
121 const WATERMARK_SECRET: &'static str = "DupeHell-WATERMARK-v0.4-educational-only-2026";
122
123 pub fn new(domain: &str, locale: &str, pools_dir: &str) -> Result<Self, String> {
125 let pool_store = PoolStore::load(pools_dir, locale)?;
126 log::info!(
127 "Context loaded: domain={domain}, locale={locale}, pools={}",
128 pool_store.pools.len()
129 );
130 Ok(Self {
131 pool_store,
132 locale: locale.to_string(),
133 watermark_map: std::collections::HashMap::new(),
134 })
135 }
136
137 pub fn enable_watermark(&mut self, domain: &str, size: usize, seed: u64) {
139 use sha2::{Digest, Sha256};
140 for &tag in &[
141 0x53534e, 0x50484f4e, 0x50414e, 0x4d4544, 0x4f4643, 0x504153, 0x414354, 0x424152, 0x494343, 0x555043, ] {
152 let input = format!(
153 "{}{}{}{}{}",
154 Self::WATERMARK_SECRET,
155 domain,
156 size,
157 seed,
158 tag
159 );
160 let hash = Sha256::digest(input.as_bytes());
161 let wm = u64::from_le_bytes(hash[..8].try_into().unwrap());
162 self.watermark_map.insert(tag, wm);
163 }
164 }
165
166 pub fn watermark_3digits(&self, tag: u64) -> u64 {
168 self.watermark_map.get(&tag).copied().unwrap_or(0) % 1000
169 }
170
171 pub fn watermark_2digits(&self, tag: u64) -> u64 {
173 self.watermark_map.get(&tag).copied().unwrap_or(0) % 100
174 }
175
176 #[cfg(test)]
179 pub fn test() -> Self {
180 Self {
181 pool_store: PoolStore {
182 pools: HashMap::new(),
183 },
184 locale: "en".to_string(),
185 watermark_map: HashMap::new(),
186 }
187 }
188}