shader_sense/
include.rs

1//! Include handler for all languages
2use std::{
3    collections::{HashMap, HashSet},
4    path::{Path, PathBuf},
5};
6
7/// Include handler for all languages
8#[derive(Debug, Default, Clone)]
9pub struct IncludeHandler {
10    includes: HashSet<PathBuf>, // Dont store in stack to compute them before.
11    directory_stack: Vec<PathBuf>, // Vec for keeping insertion order. Might own duplicate.
12    visited_dependencies: HashMap<PathBuf, usize>,
13    path_remapping: HashMap<PathBuf, PathBuf>, // remapping of path / virtual path
14}
15
16/// Canonicalize a path, the custom way.
17///
18/// [`std::fs::canonicalize`] not supported on wasi target, so we emulate it.
19/// On Windows, [`std::fs::canonicalize`] return a /? prefix that break hashmap.
20/// Instead use a custom canonicalize.
21// https://stackoverflow.com/questions/50322817/how-do-i-remove-the-prefix-from-a-canonical-windows-path
22pub fn canonicalize(p: &Path) -> std::io::Result<PathBuf> {
23    // https://github.com/antmicro/wasi_ext_lib/blob/main/canonicalize.patch
24    fn __canonicalize(path: &Path, buf: &mut PathBuf) {
25        if path.is_absolute() {
26            buf.clear();
27        }
28        for part in path {
29            if part == ".." {
30                buf.pop();
31            } else if part != "." {
32                buf.push(part);
33                // read_link here is heavy.
34                // Is it heavier than std::fs::canonicalize though ?
35                // Check Dunce aswell.
36                if let Ok(linkpath) = buf.read_link() {
37                    buf.pop();
38                    __canonicalize(&linkpath, buf);
39                }
40            }
41        }
42    }
43    let mut path = if p.is_absolute() {
44        PathBuf::new()
45    } else {
46        PathBuf::from(std::env::current_dir()?)
47    };
48    __canonicalize(p, &mut path);
49    Ok(path)
50}
51
52impl IncludeHandler {
53    /// Maximum limit for including to avoid recursion stack overflow
54    pub const DEPTH_LIMIT: usize = 30;
55
56    /// Create handler with empty config
57    pub fn main_without_config(file: &Path) -> Self {
58        Self::main(file, Vec::new(), HashMap::new())
59    }
60    /// Create handler with given config
61    pub fn main(
62        file_path: &Path,
63        includes: Vec<PathBuf>,
64        path_remapping: HashMap<PathBuf, PathBuf>,
65    ) -> Self {
66        // Add local path to directory stack
67        let cwd = file_path.parent().unwrap();
68        let mut directory_stack = Vec::new();
69        directory_stack.push(cwd.into());
70        let mut visited_dependencies = HashMap::new();
71        visited_dependencies.insert(file_path.into(), 1);
72        Self {
73            includes: includes.into_iter().collect(),
74            directory_stack: directory_stack,
75            visited_dependencies: visited_dependencies,
76            path_remapping: path_remapping,
77        }
78    }
79    /// Get all includes of handler
80    pub fn get_includes(&self) -> &HashSet<PathBuf> {
81        &self.includes
82    }
83    /// Get the number of time a file has been visited
84    pub fn get_visited_count(&self, path: &Path) -> usize {
85        self.visited_dependencies.get(path).cloned().unwrap_or(0)
86    }
87    /// Search for a given relative path in all includes and call the callback on the resolved absolute path to handle loading of the file.
88    pub fn search_in_includes(
89        &mut self,
90        relative_path: &Path,
91        include_callback: &mut dyn FnMut(&Path) -> Option<String>,
92    ) -> Option<(String, PathBuf)> {
93        match self.search_path_in_includes(relative_path) {
94            Some(absolute_path) => include_callback(&absolute_path).map(|e| (e, absolute_path)),
95            None => None,
96        }
97    }
98    /// Push a file on directory stack for include context.
99    pub fn push_directory_stack(&mut self, canonical_path: &Path) {
100        match self.visited_dependencies.get_mut(canonical_path) {
101            Some(visited_dependency_count) => *visited_dependency_count += 1,
102            None => {
103                self.visited_dependencies.insert(canonical_path.into(), 1);
104                if let Some(parent) = canonical_path.parent() {
105                    // Reduce amount of include in stack.
106                    if let Some(last) = self.directory_stack.last() {
107                        if last != parent {
108                            self.directory_stack.push(parent.into());
109                        }
110                    }
111                }
112            }
113        }
114    }
115    /// Search a path in include. Return an absolute canonicalized path.
116    pub fn search_path_in_includes(&mut self, relative_path: &Path) -> Option<PathBuf> {
117        self.search_path_in_includes_relative(relative_path)
118            .map(|e| canonicalize(&e).unwrap())
119    }
120    /// Search for a path in includes.
121    ///
122    /// It will look:
123    /// 1. in the directory stack for context by looking at the last one before the first one.
124    /// 2. in the given include path if not found on stack.
125    /// 3. in the given virtual path if not found in includes.
126    fn search_path_in_includes_relative(&self, relative_path: &Path) -> Option<PathBuf> {
127        // Checking for file existence is a bit costly.
128        // Some options are available and have been tested
129        // - path.exists(): approximatively 100us
130        // - path.is_file(): approximatively 40us
131        // - std::fs::exists(&path).unwrap_or(false): approximatively 40us but only stable with Rust>1.81
132        if relative_path.is_file() {
133            Some(PathBuf::from(relative_path))
134        } else {
135            // Check directory stack.
136            // Reverse order to check first the latest added folders.
137            // Might own duplicate, should use an ordered hashset instead.
138            for directory_stack in self.directory_stack.iter().rev() {
139                let path = directory_stack.join(&relative_path);
140                if path.is_file() {
141                    return Some(path);
142                }
143            }
144            // Check include paths
145            for include_path in &self.includes {
146                let path = include_path.join(&relative_path);
147                if path.is_file() {
148                    return Some(path);
149                }
150            }
151            // Check virtual paths
152            if let Some(target_path) =
153                Self::resolve_virtual_path(relative_path, &self.path_remapping)
154            {
155                if target_path.is_file() {
156                    return Some(target_path);
157                }
158            }
159            return None;
160        }
161    }
162    /// Check for path if its found in virtual paths
163    fn resolve_virtual_path(
164        virtual_path: &Path,
165        virtual_folders: &HashMap<PathBuf, PathBuf>,
166    ) -> Option<PathBuf> {
167        // Virtual path need to start with /
168        // Dxc automatically insert .\ in front of path that are not absolute.
169        // We should simply strip it, but how do we know its a virtual path or a real relative path ?
170        // Instead dirty hack to remove it and try to load it, as its the last step of include, should be fine...
171        let virtual_path = if virtual_path.starts_with("./") || virtual_path.starts_with(".\\") {
172            let mut comp = virtual_path.components();
173            comp.next();
174            Path::new("/").join(comp.as_path())
175        } else {
176            PathBuf::from(virtual_path)
177        };
178        // Browse possible mapping & find a match.
179        for (virtual_folder, target_path) in virtual_folders {
180            let mut path_components = virtual_path.components();
181            let mut found = true;
182            for virtual_folder_component in virtual_folder.components() {
183                match path_components.next() {
184                    Some(component) => {
185                        if component != virtual_folder_component {
186                            found = false;
187                            break;
188                        }
189                    }
190                    None => {
191                        found = false;
192                        break;
193                    }
194                }
195            }
196            if found {
197                let resolved_path = target_path.join(path_components.as_path());
198                return Some(resolved_path.into());
199            }
200        }
201        None
202    }
203}