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
// Existing prefix, boundary detection, and path injection tests
use crate::soft_canonicalize;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_existing_boundary_detection_edge_cases() -> std::io::Result<()> {
// WHITE-BOX: Test edge cases in the existing boundary detection algorithm
let temp_dir = TempDir::new()?;
let base = temp_dir.path();
// Test case 1: Boundary exactly at a symlink
#[cfg(unix)]
{
let real_dir = base.join("real");
fs::create_dir(&real_dir)?;
let symlink_dir = base.join("symlinked");
std::os::unix::fs::symlink(&real_dir, &symlink_dir)?;
// Path where boundary is exactly at the symlink
let test_path = symlink_dir.join("nonexistent.txt");
let result = soft_canonicalize(test_path);
// Handle platform-specific symlink behavior
match result {
Ok(resolved) => {
let canonical_real = fs::canonicalize(&real_dir)?;
let expected = canonical_real.join("nonexistent.txt");
assert_eq!(resolved, expected);
}
Err(e) => {
// On some platforms (like macOS), symlink resolution might hit limits
let error_msg = e.to_string();
if error_msg.contains("Too many levels") || error_msg.contains("symbolic links") {
println!("Platform hit symlink resolution limit (acceptable): {e}");
} else {
return Err(e); // Unexpected error
}
}
}
}
// Test case 2: Empty components in path
let path_with_empty = base.join("").join("test.txt");
let result = soft_canonicalize(path_with_empty)?;
assert!(result.is_absolute());
// Test case 3: Path ending with directory separator
let dir_path = base.join("testdir").join("");
let result = soft_canonicalize(dir_path)?;
assert!(result.is_absolute());
Ok(())
}
#[test]
fn test_existing_count_manipulation() -> std::io::Result<()> {
// WHITE-BOX: Try to exploit the existing_count logic by creating scenarios
// where the count might be manipulated or cause boundary detection issues
let temp_dir = TempDir::new()?;
let base = temp_dir.path();
// Create a deep directory structure
let deep_path = base.join("a").join("b").join("c").join("d");
fs::create_dir_all(deep_path)?;
#[cfg(unix)]
{
// Create symlinks at different depths that might confuse existing_count
let link1 = base.join("link1");
std::os::unix::fs::symlink(base.join("a"), &link1)?;
let link2 = base.join("a").join("link2");
std::os::unix::fs::symlink(base.join("a").join("b"), &link2)?;
// Create a broken symlink in the middle
let broken_link = base.join("a").join("b").join("broken");
std::os::unix::fs::symlink(base.join("nonexistent"), &broken_link)?;
// Test paths that traverse through these mixed scenarios
let test_cases = vec![
link1.join("b").join("c").join("nonexistent.txt"),
base.join("a")
.join("link2")
.join("c")
.join("nonexistent.txt"),
base.join("a")
.join("b")
.join("broken")
.join("after_broken.txt"),
];
for test_path in test_cases {
let result = soft_canonicalize(test_path);
// Should handle all these cases without panicking or returning inconsistent results
match result {
Ok(_) => {
// Good - algorithm handled the scenario
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// Acceptable for broken symlinks
}
Err(e) => {
eprintln!("Unexpected error for complex existing_count scenario: {e}");
return Err(e);
}
}
}
}
Ok(())
}
#[test]
fn test_fast_path_bypass_attempts() -> std::io::Result<()> {
// WHITE-BOX: Try to bypass the fast path optimization with edge cases
let temp_dir = TempDir::new()?;
let base = temp_dir.path();
// Create an existing absolute path with no dot components (should trigger fast path)
let existing_file = base.join("existing.txt");
fs::write(&existing_file, "content")?;
// Verify fast path works
let result = soft_canonicalize(&existing_file)?;
let expected = fs::canonicalize(&existing_file)?;
#[cfg(not(feature = "dunce"))]
{
assert_eq!(result, expected, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let result_str = result.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(!result_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(expected_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(result, expected);
}
}
// Now try to create similar path that might confuse the fast path detection
let tricky_path = existing_file.join("").join("..").join("existing.txt");
let result2 = soft_canonicalize(tricky_path)?;
// Should still resolve correctly even though it bypassed fast path
#[cfg(not(feature = "dunce"))]
{
assert_eq!(result2, expected, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let result2_str = result2.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(!result2_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(expected_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(result2, expected);
}
}
Ok(())
}
#[test]
fn test_canonicalization_bypass_attempts() -> std::io::Result<()> {
// WHITE-BOX: Try to exploit the multiple canonicalization attempts in the algorithm
// to bypass security checks or cause inconsistent behavior
let temp_dir = TempDir::new()?;
let base = temp_dir.path();
// Create scenarios where canonicalization might be bypassed
fs::create_dir(base.join("existing"))?;
#[cfg(unix)]
{
let existing_dir = base.join("existing");
// Create symlinks that might interfere with canonicalization
let link_to_existing = base.join("link_to_existing");
std::os::unix::fs::symlink(&existing_dir, &link_to_existing)?;
// Create a chain where canonicalization might be applied inconsistently
let nested_link = existing_dir.join("nested_link");
std::os::unix::fs::symlink(base.join("target_that_does_not_exist"), &nested_link)?;
// Test paths that go through multiple canonicalization points
let test_cases = vec![
// Through direct symlink
link_to_existing.join("nested_link").join("final_file.txt"),
// Through path components that might trigger different canonicalization paths
existing_dir
.join("..")
.join("existing")
.join("nested_link")
.join("final_file.txt"),
];
for test_path in test_cases {
let result = soft_canonicalize(test_path);
match result {
Ok(resolved) => {
// Should have consistent canonicalization
assert!(resolved.is_absolute());
// Verify that the resolved path makes sense
if let Some(parent) = resolved.parent() {
// The parent should either exist or be a valid non-existing path
assert!(parent.is_absolute());
}
}
Err(e) => {
// Errors are acceptable for broken symlink chains
assert!(
e.kind() == std::io::ErrorKind::NotFound
|| e.to_string().contains("symbolic links")
);
}
}
}
}
Ok(())
}
#[test]
fn test_long_path_component_handling() {
// Test handling of very long path components
// Some systems have limits on individual component length (not just total path length)
use crate::soft_canonicalize;
// Create a very long component name (typically 255 chars is the limit)
let long_component = "a".repeat(300);
let long_path = format!("documents/{long_component}/file.txt");
let result = soft_canonicalize(long_path);
// Should either succeed or fail gracefully with appropriate error
match result {
Ok(resolved) => {
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains(&long_component));
}
Err(e) => {
// If it fails, should be with an appropriate error kind
assert!(
e.kind() == std::io::ErrorKind::InvalidInput
|| e.kind() == std::io::ErrorKind::InvalidData
|| e.kind() == std::io::ErrorKind::NotFound,
"Should fail with appropriate error for long component: {e:?}"
);
}
}
}