Skip to main content

gnu_units/
definitions.rs

1//! Parses the embedded `definitions.units` file and registers entries into
2//! either the vendored C-library database (via FFI) or the pure-Rust native
3//! database, depending on the active feature.
4//!
5//! Produces a sorted [`Vec<Definition>`] exposed through the public API.
6
7use 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/// The kind of a definition entry in the GNU units database.
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
28pub enum DefinitionKind {
29    /// A plain unit (e.g. `meter`, `second`).
30    Unit,
31    /// A unit prefix (e.g. `kilo-`, `mega-`).
32    Prefix,
33    /// A conversion function (e.g. `tempC(x)`).
34    Function,
35    /// A piecewise unit table (e.g. `dBV[...]`).
36    Table,
37    /// A unit list alias (defined via `!unitlist`).
38    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/// A single entry from the GNU units definitions database.
53#[derive(Debug, Clone, Eq)]
54pub struct Definition {
55    /// The name of the unit, prefix, function, table, or alias.
56    pub name: String,
57    /// The definition string for this entry.
58    pub definition: String,
59    /// What kind of definition this is.
60    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    /// Returns the canonical lookup name used for deduplication:
85    /// - Prefixes: trailing `-` stripped (`kilo-` → `kilo`)
86    /// - Functions: stripped from `(` onward (`tempC(x)` → `tempC`)
87    /// - Tables: stripped from `[` onward (`gasmark[degR]` → `gasmark`)
88    /// - Units and aliases: full name as-is
89    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
100/// Sorted list of all known definitions, populated once at start-up.
101pub(crate) static DEFINITIONS: LazyLock<RwLock<Vec<Definition>>> = LazyLock::new(|| {
102    #[cfg(feature = "vendored")]
103    {
104        let _guard = ffi::lock();
105        // SAFETY: static C strings live for the process lifetime.
106        // LazyLock guarantees single-threaded execution here.
107        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    // Emulate C last-write-wins: reverse so last-in-file entries come first.
129    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
136/// Forces the definitions to be loaded exactly once.
137pub(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    // SAFETY: utf8mode is a simple i32 global set during initialization.
204    // It is only read here after initialization is complete.
205    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}