Skip to main content

docling_core/
assets.rs

1//! Runtime-asset path resolution shared by every crate.
2//!
3//! All optional runtime assets (ONNX models, OCR dictionaries, the chunker
4//! tokenizer) live under one directory, `.models/` — dot-prefixed like
5//! `.pdfium/`, so the plain `models` name stays free for source code.
6//! Resolution is CWD-relative with an executable-directory fallback.
7
8/// Resolve a default (CWD-relative) asset path. If it doesn't exist relative
9/// to the current directory, a `.models/…` path is tried under
10/// `$DOCLING_RS_MODELS_DIR` (#285 — a whole-directory override, so the
11/// engine's *own* selection logic — the OCR language pair, the int8
12/// preference chains — keeps working against a relocated model set; the
13/// Python bindings point it at their per-user cache). After that, try next
14/// to the executable and one level above it (following symlinks — the layout
15/// `scripts/install/install.sh` produces: `/usr/local/bin/docling-rs` →
16/// `/usr/local/docling.rs/bin/docling-rs` with `.models/` and `.pdfium/` in
17/// `/usr/local/docling.rs`). Returns `rel` unchanged when nothing exists
18/// anywhere, so callers' error messages keep the familiar path. Explicit
19/// per-file env overrides never reach this.
20pub fn resolve(rel: &str) -> String {
21    if std::path::Path::new(rel).exists() {
22        return rel.to_string();
23    }
24    if let Some(stripped) = rel.strip_prefix(".models/") {
25        if let Some(dir) = crate::env::nonempty("DOCLING_RS_MODELS_DIR") {
26            let p = std::path::Path::new(&dir).join(stripped);
27            if p.exists() {
28                return p.to_string_lossy().into_owned();
29            }
30        }
31    }
32    let dir = std::env::current_exe()
33        .ok()
34        .and_then(|p| p.canonicalize().ok())
35        .and_then(|p| p.parent().map(std::path::Path::to_path_buf));
36    if let Some(dir) = dir {
37        for base in [Some(dir.as_path()), dir.parent()].into_iter().flatten() {
38            let p = base.join(rel);
39            if p.exists() {
40                return p.to_string_lossy().into_owned();
41            }
42        }
43    }
44    rel.to_string()
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn missing_everywhere_returns_input() {
53        assert_eq!(
54            resolve(".models/definitely/not/there.onnx"),
55            ".models/definitely/not/there.onnx"
56        );
57    }
58
59    /// `.models/…` resolves under `$DOCLING_RS_MODELS_DIR` when the file
60    /// exists there (#285) — and only then; a miss falls through unchanged.
61    #[test]
62    fn models_dir_override_applies_to_dot_models_paths() {
63        let dir = std::env::temp_dir().join(format!("docling-assets-{}", std::process::id()));
64        std::fs::create_dir_all(dir.join("sub")).unwrap();
65        std::fs::write(dir.join("sub/x.onnx"), b"x").unwrap();
66        std::env::set_var("DOCLING_RS_MODELS_DIR", &dir);
67        assert_eq!(
68            resolve(".models/sub/x.onnx"),
69            dir.join("sub/x.onnx").to_string_lossy()
70        );
71        // Missing under the override → the input comes back unchanged.
72        assert_eq!(resolve(".models/sub/y.onnx"), ".models/sub/y.onnx");
73        // Non-.models assets are not redirected.
74        assert_eq!(resolve(".pdfium/lib/nope.so"), ".pdfium/lib/nope.so");
75        std::env::remove_var("DOCLING_RS_MODELS_DIR");
76        let _ = std::fs::remove_dir_all(&dir);
77    }
78}