shader_sense/
include.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::{Path, PathBuf},
4};
5
6#[derive(Debug, Default, Clone)]
7pub struct Dependencies {
8    dependencies: HashSet<PathBuf>,
9}
10
11pub struct IncludeHandler {
12    includes: Vec<String>,
13    directory_stack: HashSet<PathBuf>,
14    dependencies: Dependencies,                // TODO: Remove
15    path_remapping: HashMap<PathBuf, PathBuf>, // remapping of path / virtual path
16}
17// std::fs::canonicalize not supported on wasi target... Emulate it.
18// On Windows, std::fs::canonicalize return a /? prefix that break hashmap.
19// https://stackoverflow.com/questions/50322817/how-do-i-remove-the-prefix-from-a-canonical-windows-path
20// Instead use a custom canonicalize.
21pub fn canonicalize(p: &Path) -> std::io::Result<PathBuf> {
22    // https://github.com/antmicro/wasi_ext_lib/blob/main/canonicalize.patch
23    fn __canonicalize(path: &Path, buf: &mut PathBuf) {
24        if path.is_absolute() {
25            buf.clear();
26        }
27        for part in path {
28            if part == ".." {
29                buf.pop();
30            } else if part != "." {
31                buf.push(part);
32                if let Ok(linkpath) = buf.read_link() {
33                    buf.pop();
34                    __canonicalize(&linkpath, buf);
35                }
36            }
37        }
38    }
39    let mut path = if p.is_absolute() {
40        PathBuf::new()
41    } else {
42        PathBuf::from(std::env::current_dir()?)
43    };
44    __canonicalize(p, &mut path);
45    Ok(path)
46}
47
48impl Dependencies {
49    pub fn new() -> Self {
50        Self {
51            dependencies: HashSet::new(),
52        }
53    }
54    pub fn add_dependency(&mut self, relative_path: PathBuf) {
55        self.dependencies.insert(
56            canonicalize(&relative_path).expect("Failed to convert dependency path to absolute"),
57        );
58    }
59    pub fn visit_dependencies<F: FnMut(&Path)>(&self, callback: &mut F) {
60        for dependency in &self.dependencies {
61            callback(&dependency);
62        }
63    }
64}
65
66impl IncludeHandler {
67    pub fn new(
68        file: &Path,
69        includes: Vec<String>,
70        path_remapping: HashMap<PathBuf, PathBuf>,
71    ) -> Self {
72        // Add local path to include path
73        let mut includes_mut = includes;
74        let cwd = file.parent().unwrap();
75        let str = String::from(cwd.to_string_lossy());
76        // TODO: push cwd in first. Or move it elsewhere
77        includes_mut.push(str);
78        Self {
79            includes: includes_mut,
80            directory_stack: HashSet::new(),
81            dependencies: Dependencies::new(),
82            path_remapping: path_remapping,
83        }
84    }
85    pub fn search_in_includes(
86        &mut self,
87        relative_path: &Path,
88        include_callback: &mut dyn FnMut(&Path) -> Option<String>,
89    ) -> Option<(String, PathBuf)> {
90        match self.search_path_in_includes(relative_path) {
91            Some(absolute_path) => include_callback(&absolute_path).map(|e| (e, absolute_path)),
92            None => None,
93        }
94    }
95    pub fn search_path_in_includes(&mut self, relative_path: &Path) -> Option<PathBuf> {
96        self.search_path_in_includes_relative(relative_path)
97            .map(|e| canonicalize(&e).expect("Failed to convert relative path to absolute"))
98    }
99    pub fn search_path_in_includes_relative(&mut self, relative_path: &Path) -> Option<PathBuf> {
100        // Checking for file existence is a bit costly.
101        // Some options are available and have been tested
102        // - path.exists(): approximatively 100us
103        // - path.is_file(): approximatively 40us
104        // - std::fs::exists(&path).unwrap_or(false): approximatively 40us but only stable with Rust>1.81
105        if relative_path.is_file() {
106            Some(PathBuf::from(relative_path))
107        } else {
108            // Check directory stack.
109            for directory_stack in &self.directory_stack {
110                let path = Path::new(directory_stack).join(&relative_path);
111                if path.is_file() {
112                    if let Some(parent) = path.parent() {
113                        self.directory_stack.insert(canonicalize(parent).unwrap());
114                    }
115                    self.dependencies.add_dependency(path.clone());
116                    return Some(path);
117                }
118            }
119            // Check include paths
120            for include_path in &self.includes {
121                let path = Path::new(include_path).join(&relative_path);
122                if path.is_file() {
123                    if let Some(parent) = path.parent() {
124                        self.directory_stack.insert(canonicalize(parent).unwrap());
125                    }
126                    self.dependencies.add_dependency(path.clone());
127                    return Some(path);
128                }
129            }
130            // Check virtual paths
131            if let Some(target_path) =
132                Self::resolve_virtual_path(relative_path, &self.path_remapping)
133            {
134                if target_path.is_file() {
135                    if let Some(parent) = target_path.parent() {
136                        self.directory_stack.insert(canonicalize(parent).unwrap());
137                    }
138                    self.dependencies.add_dependency(target_path.clone());
139                    return Some(target_path);
140                }
141            }
142            return None;
143        }
144    }
145    fn resolve_virtual_path(
146        virtual_path: &Path,
147        virtual_folders: &HashMap<PathBuf, PathBuf>,
148    ) -> Option<PathBuf> {
149        // Virtual path need to start with /
150        // Dxc automatically insert .\ in front of path that are not absolute.
151        // We should simply strip it, but how do we know its a virtual path or a real relative path ?
152        // Instead dirty hack to remove it and try to load it, as its the last step of include, should be fine...
153        let virtual_path = if virtual_path.starts_with("./") || virtual_path.starts_with(".\\") {
154            let mut comp = virtual_path.components();
155            comp.next();
156            Path::new("/").join(comp.as_path())
157        } else {
158            PathBuf::from(virtual_path)
159        };
160        if virtual_path.starts_with("/") || virtual_path.starts_with("\\") {
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        } else {
186            None
187        }
188    }
189    pub fn get_dependencies(&self) -> &Dependencies {
190        return &self.dependencies;
191    }
192}