1use std::cmp::Ordering;
8use std::collections::HashMap;
9use std::sync::{LazyLock, RwLock};
10
11#[cfg(feature = "vendored")]
12use std::ffi::{CStr, CString};
13#[cfg(feature = "vendored")]
14use std::os::raw::c_int;
15#[cfg(feature = "vendored")]
16use std::sync::Mutex;
17
18#[cfg(feature = "vendored")]
19use crate::engine::ffi;
20
21#[cfg(feature = "native")]
22use crate::engine::native::database::{Database, init as db_init};
23
24use crate::units;
25
26#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
28pub enum DefinitionKind {
29 Unit,
31 Prefix,
33 Function,
35 Table,
37 Alias,
39}
40
41impl DefinitionKind {
42 fn from_name(name: &str) -> DefinitionKind {
43 match name {
44 _ if name.ends_with('-') => DefinitionKind::Prefix,
45 _ if name.contains('[') => DefinitionKind::Table,
46 _ if name.contains('(') => DefinitionKind::Function,
47 _ => DefinitionKind::Unit,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Eq)]
54pub struct Definition {
55 pub name: String,
57 pub definition: String,
59 pub kind: DefinitionKind,
61}
62
63impl PartialEq for Definition {
64 fn eq(&self, other: &Self) -> bool {
65 self.canonical_name() == other.canonical_name() && self.kind == other.kind
66 }
67}
68
69impl Ord for Definition {
70 fn cmp(&self, other: &Self) -> Ordering {
71 self.canonical_name()
72 .cmp(other.canonical_name())
73 .then_with(|| self.kind.cmp(&other.kind))
74 }
75}
76
77impl PartialOrd for Definition {
78 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79 Some(self.cmp(other))
80 }
81}
82
83impl Definition {
84 pub fn canonical_name(&self) -> &str {
90 if let Some(idx) = self.name.find('(') {
91 &self.name[..idx]
92 } else if let Some(idx) = self.name.find('[') {
93 &self.name[..idx]
94 } else {
95 self.name.trim_end_matches('-')
96 }
97 }
98}
99
100pub(crate) static DEFINITIONS: LazyLock<RwLock<Vec<Definition>>> = LazyLock::new(|| {
102 #[cfg(feature = "vendored")]
103 {
104 let _guard = ffi::lock();
105 unsafe {
108 gnu_units_sys::mylocale = c"en_US".as_ptr() as *mut std::os::raw::c_char;
109 gnu_units_sys::progname = c"gnu-units".as_ptr() as *mut std::os::raw::c_char;
110 gnu_units_sys::utf8mode = 1;
111 }
112 }
113
114 #[cfg(feature = "native")]
115 let mut native_db = Database::default();
116
117 let content = include_str!("../data/definitions.units");
118 let mut env: HashMap<String, String> = HashMap::new();
119
120 #[cfg(feature = "vendored")]
121 let mut defs = load_core(content, c"definitions.units", &mut env);
122 #[cfg(feature = "native")]
123 let mut defs = load_core(content, c"definitions.units", &mut env, &mut native_db);
124
125 #[cfg(feature = "native")]
126 db_init(native_db);
127
128 defs.reverse();
130 defs.sort();
131 defs.dedup();
132 defs.sort_by(|a, b| a.name.cmp(&b.name));
133 RwLock::new(defs)
134});
135
136pub(crate) fn ensure_definitions() {
138 LazyLock::force(&DEFINITIONS);
139}
140
141#[cfg(feature = "vendored")]
142static FILE_PTRS: LazyLock<Mutex<HashMap<Vec<u8>, usize>>> =
143 LazyLock::new(|| Mutex::new(HashMap::new()));
144
145#[cfg(feature = "vendored")]
146fn intern_filename(filename: &CStr) -> *mut std::os::raw::c_char {
147 let key = filename.to_bytes().to_vec();
148 let mut map = FILE_PTRS.lock().unwrap_or_else(|e| e.into_inner());
149 let addr = *map
150 .entry(key)
151 .or_insert_with(|| CString::new(filename.to_bytes()).unwrap().into_raw() as usize);
152 addr as *mut std::os::raw::c_char
153}
154
155pub(crate) fn replace_operators(s: &str) -> String {
156 let mut out = String::with_capacity(s.len());
157 for ch in s.chars() {
158 let replacement = match ch {
159 '\u{2012}' | '\u{2013}' | '\u{2212}' => "-",
160 '\u{00D7}' | '\u{2A09}' | '\u{00B7}' | '\u{22C5}' => "*",
161 '\u{00F7}' | '\u{2215}' => "/",
162 '\u{2044}' => "|",
163 '\u{00A0}'
164 | '\u{1680}'
165 | '\u{2000}'..='\u{200A}'
166 | '\u{202F}'
167 | '\u{205F}'
168 | '\u{3000}' => " ",
169 '\u{200B}' | '\u{200C}' => "",
170 _ => {
171 out.push(ch);
172 continue;
173 }
174 };
175 out.push_str(replacement);
176 }
177 out
178}
179
180#[cfg_attr(not(feature = "currency-update"), allow(dead_code))]
181pub(crate) fn load_definitions(content: &str, filename: &std::ffi::CStr) -> Vec<Definition> {
182 let mut env: HashMap<String, String> = HashMap::new();
183 #[cfg(feature = "vendored")]
184 {
185 load_core(content, filename, &mut env)
186 }
187 #[cfg(feature = "native")]
188 {
189 let _ = filename;
190 let db_rw = crate::engine::native::database::get();
191 let mut db = db_rw.write().unwrap_or_else(|e| e.into_inner());
192 load_core(content, c"", &mut env, &mut db)
193 }
194}
195
196#[cfg(feature = "vendored")]
197fn load_core(
198 content: &str,
199 filename: &std::ffi::CStr,
200 env: &mut HashMap<String, String>,
201) -> Vec<Definition> {
202 let file_ptr = intern_filename(filename);
203 let utf8mode = unsafe { gnu_units_sys::utf8mode };
206 load_lines_vendored(content, env, file_ptr, utf8mode)
207}
208
209#[cfg(feature = "vendored")]
210fn load_lines_vendored(
211 content: &str,
212 env: &mut HashMap<String, String>,
213 file_ptr: *mut std::os::raw::c_char,
214 utf8mode: i32,
215) -> Vec<Definition> {
216 let mut results = Vec::new();
217 let mut wrong_locale = false;
218 let mut in_utf8 = false;
219 let mut wrong_var = false;
220
221 for (linenum, raw_line) in join_continuations(content) {
222 let line = strip_comment(&raw_line);
223 let line = line.trim();
224
225 if let Some(rest) = line.strip_prefix('!') {
226 let (directive, arg) = split2(rest.trim());
227 match directive {
228 "locale" => {
229 wrong_locale = arg != "en_US";
230 }
231 "endlocale" => {
232 wrong_locale = false;
233 }
234 "utf8" => {
235 in_utf8 = true;
236 }
237 "endutf8" => {
238 in_utf8 = false;
239 }
240 "var" => {
241 let (vname, vals) = split2(arg);
242 wrong_var = match lookup_var(vname, env) {
243 Some(v) => !vals.split_whitespace().any(|w| w == v),
244 None => true,
245 };
246 }
247 "varnot" => {
248 let (vname, vals) = split2(arg);
249 wrong_var = match lookup_var(vname, env) {
250 Some(v) => vals.split_whitespace().any(|w| w == v),
251 None => true,
252 };
253 }
254 "endvar" => {
255 wrong_var = false;
256 }
257 "set" if !wrong_locale && !wrong_var => {
258 let (vname, val) = split2(arg);
259 if lookup_var(vname, env).is_none() {
260 env.insert(vname.to_owned(), val.to_owned());
261 }
262 }
263 "set" | "message" | "prompt" => {}
264 "unitlist" if !(wrong_locale || wrong_var || in_utf8 && utf8mode == 0) => {
265 let (name, def) = split2(arg);
266 if !name.is_empty() {
267 ffi::newalias(name, def, linenum as c_int, file_ptr);
268 results.push(Definition {
269 name: name.to_owned(),
270 definition: def.to_owned(),
271 kind: DefinitionKind::Alias,
272 });
273 }
274 }
275 "unitlist" => {}
276 "include" if !(wrong_locale || wrong_var || in_utf8 && utf8mode == 0) => {
277 results.extend(include_vendored(arg, env, utf8mode));
278 }
279 "include" => {}
280 _ => {}
281 }
282 continue;
283 }
284
285 if wrong_locale || wrong_var || (in_utf8 && utf8mode == 0) {
286 continue;
287 }
288
289 let norm = replace_operators(line);
290 let line = norm.trim();
291 if line.is_empty() {
292 continue;
293 }
294
295 let (raw_name, def) = split2(line);
296 if raw_name == "-" || def.is_empty() {
297 continue;
298 }
299 let name = raw_name.strip_prefix('+').unwrap_or(raw_name);
300 if name.is_empty() {
301 continue;
302 }
303
304 let lnum = linenum as c_int;
305 if name.ends_with('-') {
306 ffi::newprefix(name, def, lnum, file_ptr);
307 } else if name.contains('[') {
308 ffi::newtable(name, def, lnum, file_ptr);
309 } else if name.contains('(') {
310 ffi::newfunction(name, def, lnum, file_ptr);
311 } else {
312 ffi::newunit(name, def, lnum, file_ptr);
313 }
314
315 results.push(Definition {
316 name: name.to_owned(),
317 definition: def.to_owned(),
318 kind: DefinitionKind::from_name(name),
319 });
320 }
321 results
322}
323
324#[cfg(feature = "vendored")]
325fn include_vendored(
326 arg: &str,
327 env: &mut HashMap<String, String>,
328 utf8mode: i32,
329) -> Vec<Definition> {
330 let (content, filename): (&str, &std::ffi::CStr) = match arg {
331 "elements.units" => (units::ELEMENTS, c"elements.units"),
332 #[cfg(feature = "currency-update")]
333 "currency.units" => (units::CURRENCY, c"currency.units"),
334 #[cfg(feature = "currency-update")]
335 "crypto.units" => (units::CRYPTO, c"crypto.units"),
336 #[cfg(feature = "currency-update")]
337 "metal_prices.units" => (units::METAL_PRICES, c"metal_prices.units"),
338 #[cfg(feature = "currency-update")]
339 "cpi.units" => (units::CPI, c"cpi.units"),
340 _ => return vec![],
341 };
342 let file_ptr = intern_filename(filename);
343 load_lines_vendored(content, env, file_ptr, utf8mode)
344}
345
346#[cfg(feature = "native")]
347fn load_core(
348 content: &str,
349 _filename: &std::ffi::CStr,
350 env: &mut HashMap<String, String>,
351 db: &mut Database,
352) -> Vec<Definition> {
353 let mut results = Vec::new();
354 let mut wrong_locale = false;
355 let mut in_utf8 = false;
356 let mut wrong_var = false;
357 const UTF8MODE: i32 = 1;
358
359 for (_linenum, raw_line) in join_continuations(content) {
360 let line = strip_comment(&raw_line);
361 let line = line.trim();
362
363 if let Some(rest) = line.strip_prefix('!') {
364 let (directive, arg) = split2(rest.trim());
365 match directive {
366 "locale" => {
367 wrong_locale = arg != "en_US";
368 }
369 "endlocale" => {
370 wrong_locale = false;
371 }
372 "utf8" => {
373 in_utf8 = true;
374 }
375 "endutf8" => {
376 in_utf8 = false;
377 }
378 "var" => {
379 let (vname, vals) = split2(arg);
380 wrong_var = match lookup_var(vname, env) {
381 Some(v) => !vals.split_whitespace().any(|w| w == v),
382 None => true,
383 };
384 }
385 "varnot" => {
386 let (vname, vals) = split2(arg);
387 wrong_var = match lookup_var(vname, env) {
388 Some(v) => vals.split_whitespace().any(|w| w == v),
389 None => true,
390 };
391 }
392 "endvar" => {
393 wrong_var = false;
394 }
395 "set" if !wrong_locale && !wrong_var => {
396 let (vname, val) = split2(arg);
397 if lookup_var(vname, env).is_none() {
398 env.insert(vname.to_owned(), val.to_owned());
399 }
400 }
401 "set" | "message" | "prompt" => {}
402 "unitlist" if !(wrong_locale || wrong_var || in_utf8 && UTF8MODE == 0) => {
403 let (name, def) = split2(arg);
404 if !name.is_empty() {
405 results.push(Definition {
406 name: name.to_owned(),
407 definition: def.to_owned(),
408 kind: DefinitionKind::Alias,
409 });
410 }
411 }
412 "unitlist" => {}
413 "include" if !(wrong_locale || wrong_var || in_utf8 && UTF8MODE == 0) => {
414 results.extend(include_native(arg, env, db));
415 }
416 "include" => {}
417 _ => {}
418 }
419 continue;
420 }
421
422 if wrong_locale || wrong_var || (in_utf8 && UTF8MODE == 0) {
423 continue;
424 }
425
426 let norm = replace_operators(line);
427 let line = norm.trim();
428 if line.is_empty() {
429 continue;
430 }
431
432 let (raw_name, def) = split2(line);
433 if raw_name == "-" || def.is_empty() {
434 continue;
435 }
436 let name = raw_name.strip_prefix('+').unwrap_or(raw_name);
437 if name.is_empty() {
438 continue;
439 }
440
441 match name {
442 _ if name.ends_with('-') => db.insert_prefix(name, def),
443 _ if name.contains('[') => db.insert_table(name, def),
444 _ if name.contains('(') => db.insert_function(name, def),
445 _ => db.insert_unit(name, def),
446 }
447
448 results.push(Definition {
449 name: name.to_owned(),
450 definition: def.to_owned(),
451 kind: DefinitionKind::from_name(name),
452 });
453 }
454 results
455}
456
457#[cfg(feature = "native")]
458fn include_native(
459 arg: &str,
460 env: &mut HashMap<String, String>,
461 db: &mut Database,
462) -> Vec<Definition> {
463 let (content, _filename): (&str, &std::ffi::CStr) = match arg {
464 "elements.units" => (units::ELEMENTS, c"elements.units"),
465 #[cfg(feature = "currency-update")]
466 "currency.units" => (units::CURRENCY, c"currency.units"),
467 #[cfg(feature = "currency-update")]
468 "crypto.units" => (units::CRYPTO, c"crypto.units"),
469 #[cfg(feature = "currency-update")]
470 "metal_prices.units" => (units::METAL_PRICES, c"metal_prices.units"),
471 #[cfg(feature = "currency-update")]
472 "cpi.units" => (units::CPI, c"cpi.units"),
473 _ => return vec![],
474 };
475 load_core(content, c"", env, db)
476}
477
478fn join_continuations(content: &str) -> Vec<(usize, String)> {
479 let mut lines = Vec::new();
480 let mut iter = content.lines().enumerate();
481 while let Some((idx, line)) = iter.next() {
482 let line = if idx == 0 {
483 line.trim_start_matches('\u{FEFF}')
484 } else {
485 line
486 };
487 let mut buf = line.to_string();
488 while buf.ends_with('\\')
489 && let Some((_, next)) = iter.next()
490 {
491 buf.pop();
492 buf.push_str(next.trim_start());
493 }
494 lines.push((idx + 1, buf));
495 }
496 lines
497}
498
499fn strip_comment(line: &str) -> &str {
500 if let Some(pos) = line.find('#') {
501 return &line[..pos];
502 }
503 line
504}
505
506fn split2(s: &str) -> (&str, &str) {
507 let mut parts = s.splitn(2, char::is_whitespace);
508 let k = parts.next().unwrap_or("");
509 let v = parts.next().unwrap_or("").trim();
510 (k, v)
511}
512
513fn lookup_var(name: &str, env: &HashMap<String, String>) -> Option<String> {
514 env.get(name).cloned().or_else(|| std::env::var(name).ok())
515}