shader_sense/
include.rs

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