subplot/
resource.rs

1//! Resources for subplot.
2//!
3//! This module encapsulates a mechanism for subplot to find resource files.
4//! Resource files come from a number of locations, such as embedded into the
5//! binary, from template paths given on the CLI, or from paths built into the
6//! binary and present on disk.
7
8#[cfg(feature = "unstable-cli")]
9use clap::Parser;
10use std::io::{self, Cursor, Read};
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13
14#[allow(missing_docs)]
15#[derive(Debug)]
16#[cfg_attr(feature = "unstable-cli", derive(Parser))]
17// Options which relate to resource management
18//
19// To use this, include them *flat* in your options struct, and then after
20// parsing, call the [ResourceOpts::handle()] function.
21pub struct ResourceOpts {
22    #[cfg_attr(
23        feature = "unstable-cli",
24        clap(
25            long,
26            number_of_values = 1,
27            help = "Look for code templates and other resources in DIR",
28            name = "DIR"
29        )
30    )]
31    resources: Vec<PathBuf>,
32}
33
34impl ResourceOpts {
35    /// Handle any supplied resource related arguments
36    pub fn handle<P: AsRef<Path>>(&self, doc_path: Option<P>) {
37        for rpath in &self.resources {
38            add_search_path(rpath);
39        }
40        if let Some(doc_path) = doc_path.as_ref() {
41            add_search_path(doc_path);
42        }
43    }
44}
45
46use lazy_static::lazy_static;
47
48lazy_static! {
49    static ref SEARCH_PATHS: Mutex<Vec<PathBuf>> = {
50        let ret = Vec::new();
51        Mutex::new(ret)
52    };
53}
54
55static EMBEDDED_FILES: &[(&str, &[u8])] = include!(concat!(env!("OUT_DIR"), "/embedded_files.rs"));
56
57/// Retrieve the embedded file list
58pub fn embedded_files() -> &'static [(&'static str, &'static [u8])] {
59    EMBEDDED_FILES
60}
61
62/// Put a path at the back of the queue to search in
63pub fn add_search_path<P: AsRef<Path>>(path: P) {
64    SEARCH_PATHS
65        .lock()
66        .expect("Unable to lock SEARCH_PATHS")
67        .push(path.as_ref().into());
68}
69
70/// Open a file for reading, honouring search paths established during
71/// startup, and falling back to potentially embedded file content.
72///
73/// The search path sequence is:
74///
75/// First any path given to `--resources` in the order of the arguments
76/// Second, it's relative to the input document.
77/// Finally we check for an embedded file.
78///
79/// Then we repeat all the above, inserting 'common' as the template name, and
80/// finally repeat the above inserting the real template name in the subpath
81/// too.
82fn open<P: AsRef<Path>>(subpath: P, template: Option<&str>) -> io::Result<Box<dyn Read>> {
83    let subpath = subpath.as_ref();
84    let plain = match internal_open(subpath) {
85        Ok(r) => return Ok(r),
86        Err(e) => e,
87    };
88    let commonpath = Path::new("common").join(subpath);
89    let common = match internal_open(&commonpath) {
90        Ok(r) => return Ok(r),
91        Err(e) => e,
92    };
93    let templated = match template {
94        Some(templ) => {
95            let templpath = Path::new(templ).join(subpath);
96            match internal_open(&templpath) {
97                Ok(r) => return Ok(r),
98                Err(e) => Some(e),
99            }
100        }
101        None => None,
102    };
103    if plain.kind() != io::ErrorKind::NotFound {
104        return Err(plain);
105    }
106    if common.kind() != io::ErrorKind::NotFound {
107        return Err(common);
108    }
109    match templated {
110        Some(e) => Err(e),
111        None => Err(common),
112    }
113}
114
115fn internal_open(subpath: &Path) -> io::Result<Box<dyn Read>> {
116    let search_paths = SEARCH_PATHS.lock().expect("Unable to lock SEARCH_PATHS");
117    let search_paths = search_paths.iter().map(|p| p.as_path());
118    let search_paths = std::iter::empty().chain(search_paths);
119    let mut ret = Err(io::Error::new(
120        io::ErrorKind::NotFound,
121        format!("Unable to find {} in resource paths", subpath.display()),
122    ));
123    for basepath in search_paths {
124        let full_path = basepath.join(subpath);
125        ret = std::fs::File::open(full_path);
126        if ret.is_ok() {
127            break;
128        }
129    }
130
131    match ret {
132        Ok(ret) => Ok(Box::new(ret)),
133        Err(e) => {
134            if let Some(data) = EMBEDDED_FILES.iter().find(|e| Path::new(e.0) == subpath) {
135                Ok(Box::new(Cursor::new(data.1)))
136            } else {
137                Err(e)
138            }
139        }
140    }
141}
142
143/// Read a file, honouring search paths established during startup, and
144/// falling back to potentially embedded file content
145pub fn read_as_string<P: AsRef<Path>>(subpath: P, template: Option<&str>) -> io::Result<String> {
146    let mut f = open(subpath, template)?;
147    let mut ret = String::with_capacity(8192);
148    f.read_to_string(&mut ret)?;
149    Ok(ret)
150}