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