1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
//! Path validation and shader-path helpers for `Config`.
//!
//! Covers:
//! - Traversal-safe path validation (`validate_config_path`, `validate_shader_name`)
//! - Shader directory / file path resolution (`shaders_dir`, `shader_path`, etc.)
//! - Shader-channel and cubemap accessors
//! - Shader config overrides
//! - Shader install-prompt helpers
use super::config_struct::Config;
use crate::error::ConfigError;
use crate::types::{CursorShaderConfig, ShaderConfig, ShaderInstallPrompt};
use std::fs;
use std::path::{Path, PathBuf};
impl Config {
/// Validate that `path` (which must already exist on disk) resolves — via
/// `canonicalize` — to a location inside `expected_base`.
///
/// Uses `std::fs::canonicalize` so symlinks are fully resolved before the
/// containment check. Returns the canonical path on success, or a
/// [`ConfigError::PathTraversal`] error if the resolved path escapes the
/// expected directory.
///
/// # Errors
///
/// Returns `ConfigError::PathTraversal` when the canonical path does not
/// start with the canonical `expected_base`.
/// Returns `ConfigError::Io` if either path cannot be canonicalized.
pub fn validate_config_path(path: &Path, expected_base: &Path) -> Result<PathBuf, ConfigError> {
let canonical = fs::canonicalize(path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!("cannot canonicalize {}: {e}", path.display()),
)
})?;
let canonical_base = fs::canonicalize(expected_base).unwrap_or_else(|_| {
// If the base doesn't exist yet (first run), use the un-resolved path.
expected_base.to_path_buf()
});
if !canonical.starts_with(&canonical_base) {
return Err(ConfigError::PathTraversal(format!(
"path '{}' resolves to '{}' which is outside the expected directory '{}'",
path.display(),
canonical.display(),
canonical_base.display(),
)));
}
Ok(canonical)
}
/// Lexically check that a **relative** shader name does not contain `..`
/// components that would escape the shaders directory.
///
/// This is a compile-time / pre-existence check used by [`Self::shader_path`]
/// and [`Self::checked_shader_path`] before the file is opened. Because the
/// shader file might not exist yet (e.g., when the user is composing its name
/// in the settings UI), we cannot call `canonicalize` here.
///
/// Returns `Ok(())` when safe, `Err(ConfigError::PathTraversal)` when the
/// name contains a parent-directory component.
fn validate_shader_name(shader_name: &str) -> Result<(), ConfigError> {
use std::path::Component;
let path = Path::new(shader_name);
// Reject any component that steps upward.
for component in path.components() {
if component == Component::ParentDir {
return Err(ConfigError::PathTraversal(format!(
"shader name '{shader_name}' contains a parent-directory component ('..') \
which would escape the shaders directory",
)));
}
}
Ok(())
}
/// Get the shaders directory path (using XDG convention)
pub fn shaders_dir() -> PathBuf {
#[cfg(target_os = "windows")]
{
if let Some(config_dir) = dirs::config_dir() {
config_dir.join("par-term").join("shaders")
} else {
PathBuf::from("shaders")
}
}
#[cfg(not(target_os = "windows"))]
{
if let Some(home_dir) = dirs::home_dir() {
home_dir.join(".config").join("par-term").join("shaders")
} else {
PathBuf::from("shaders")
}
}
}
/// Get the full path to a shader file.
///
/// If `shader_name` is an absolute path it is returned as-is (the user
/// explicitly chose a location outside the shaders directory).
///
/// For relative names the path is resolved under [`Self::shaders_dir`].
/// Any relative name that contains `..` components is rejected: the
/// function logs a warning and falls back to returning the shaders directory
/// itself so that callers always receive a valid `PathBuf` without breaking
/// existing call sites. Use [`Self::checked_shader_path`] when you need a
/// hard error instead of a fallback.
pub fn shader_path(shader_name: &str) -> PathBuf {
let path = PathBuf::from(shader_name);
if path.is_absolute() {
return path;
}
// Lexical traversal check for relative names.
if let Err(e) = Self::validate_shader_name(shader_name) {
log::warn!("{e} — falling back to shaders directory");
return Self::shaders_dir();
}
Self::shaders_dir().join(shader_name)
}
/// Get the full path to a shader file, returning an error if the name
/// would escape the shaders directory.
///
/// This is a strict variant of [`Self::shader_path`] for callers that
/// prefer a hard error over a silent fallback.
pub fn checked_shader_path(shader_name: &str) -> Result<PathBuf, ConfigError> {
let path = PathBuf::from(shader_name);
if path.is_absolute() {
return Ok(path);
}
Self::validate_shader_name(shader_name)?;
Ok(Self::shaders_dir().join(shader_name))
}
/// Resolve a texture path, expanding ~ to home directory
/// and resolving relative paths relative to the shaders directory.
/// Returns the expanded path or the original if expansion fails
pub fn resolve_texture_path(path: &str) -> PathBuf {
if path.starts_with("~/")
&& let Some(home) = dirs::home_dir()
{
return home.join(&path[2..]);
}
let path_buf = PathBuf::from(path);
if path_buf.is_absolute() {
path_buf
} else {
Self::shaders_dir().join(path)
}
}
/// Get the channel texture paths as an array of Options
/// Returns [channel0, channel1, channel2, channel3] for iChannel0-3
pub fn shader_channel_paths(&self) -> [Option<PathBuf>; 4] {
[
self.shader
.custom_shader_channel0
.as_ref()
.map(|p| Self::resolve_texture_path(p)),
self.shader
.custom_shader_channel1
.as_ref()
.map(|p| Self::resolve_texture_path(p)),
self.shader
.custom_shader_channel2
.as_ref()
.map(|p| Self::resolve_texture_path(p)),
self.shader
.custom_shader_channel3
.as_ref()
.map(|p| Self::resolve_texture_path(p)),
]
}
/// Get the cubemap path prefix (resolved)
/// Returns None if not configured, otherwise the resolved path prefix
pub fn shader_cubemap_path(&self) -> Option<PathBuf> {
self.shader
.custom_shader_cubemap
.as_ref()
.map(|p| Self::resolve_texture_path(p))
}
/// Get the user override config for a specific shader (if any)
pub fn get_shader_override(&self, shader_name: &str) -> Option<&ShaderConfig> {
self.shader_configs.get(shader_name)
}
/// Get the user override config for a specific cursor shader (if any)
pub fn get_cursor_shader_override(&self, shader_name: &str) -> Option<&CursorShaderConfig> {
self.cursor_shader_configs.get(shader_name)
}
/// Get or create a mutable reference to a shader's config override
pub fn get_or_create_shader_override(&mut self, shader_name: &str) -> &mut ShaderConfig {
self.shader_configs
.entry(shader_name.to_string())
.or_default()
}
/// Get or create a mutable reference to a cursor shader's config override
pub fn get_or_create_cursor_shader_override(
&mut self,
shader_name: &str,
) -> &mut CursorShaderConfig {
self.cursor_shader_configs
.entry(shader_name.to_string())
.or_default()
}
/// Remove a shader config override (revert to defaults)
pub fn remove_shader_override(&mut self, shader_name: &str) {
self.shader_configs.remove(shader_name);
}
/// Remove a cursor shader config override (revert to defaults)
pub fn remove_cursor_shader_override(&mut self, shader_name: &str) {
self.cursor_shader_configs.remove(shader_name);
}
/// Check if the shaders folder is missing or empty
/// Returns true if user should be prompted to install shaders
pub fn should_prompt_shader_install(&self) -> bool {
// Only prompt if the preference is set to "ask"
if self.shader_install_prompt != ShaderInstallPrompt::Ask {
return false;
}
let shaders_dir = Self::shaders_dir();
// Check if directory doesn't exist
if !shaders_dir.exists() {
return true;
}
// Check if directory is empty or has no .glsl files
if let Ok(entries) = std::fs::read_dir(&shaders_dir) {
for entry in entries.flatten() {
if let Some(ext) = entry.path().extension()
&& ext == "glsl"
{
return false; // Found at least one shader
}
}
}
true // Directory exists but has no .glsl files
}
/// Check if shaders should be prompted (version-aware logic)
///
/// # Arguments
/// * `current_version` - The application version (from root crate's `VERSION` constant)
pub fn should_prompt_shader_install_versioned(&self, current_version: &str) -> bool {
if self.shader_install_prompt != ShaderInstallPrompt::Ask {
return false;
}
// Check if already prompted for this version
if let Some(ref prompted) = self.integration_versions.shaders_prompted_version
&& prompted == current_version
{
return false;
}
// Check if installed and up to date
if let Some(ref installed) = self.integration_versions.shaders_installed_version
&& installed == current_version
{
return false;
}
// Also check if shaders folder exists and has files
let shaders_dir = Self::shaders_dir();
!shaders_dir.exists() || !Self::has_shader_files(&shaders_dir)
}
/// Check if a directory contains shader files (.glsl)
fn has_shader_files(dir: &PathBuf) -> bool {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if let Some(ext) = entry.path().extension()
&& ext == "glsl"
{
return true;
}
}
}
false
}
}