From 1d487494c09588eaa65ce9f8f4bc39aa3f62ab76 Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:35:20 +0900 Subject: [PATCH 1/7] test(fspy): add malformed path repro --- crates/vite_task/src/session/execute/spawn.rs | 65 ++++++++++++------- .../fixtures/malformed-fspy-path/package.json | 4 ++ .../malformed-fspy-path/snapshots.toml | 7 ++ ...alformed observed path does not panic.snap | 7 ++ .../malformed-fspy-path/vite-task.json | 13 ++++ 5 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/package.json create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots.toml create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots/malformed observed path does not panic.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/vite-task.json diff --git a/crates/vite_task/src/session/execute/spawn.rs b/crates/vite_task/src/session/execute/spawn.rs index 6a3b7842..1b00f6c4 100644 --- a/crates/vite_task/src/session/execute/spawn.rs +++ b/crates/vite_task/src/session/execute/spawn.rs @@ -3,6 +3,7 @@ use std::{ collections::hash_map::Entry, io::Write, + path::Path, process::{ExitStatus, Stdio}, time::{Duration, Instant}, }; @@ -57,6 +58,34 @@ pub struct TrackedPathAccesses { pub path_writes: FxHashSet, } +fn normalize_tracked_workspace_path( + stripped_path: &Path, + resolved_negatives: &[wax::Glob<'static>], +) -> Option { + // On Windows, paths are possible to be still absolute after stripping the workspace root. + // For example: c:\workspace\subdir\c:\workspace\subdir + // Just ignore those accesses. + let relative = RelativePathBuf::new(stripped_path).ok()?; + + // Clean `..` components — fspy may report paths like + // `packages/sub-pkg/../shared/dist/output.js`. Normalize them for + // consistent behavior across platforms and clean user-facing messages. + let relative = relative.clean(); + + // Skip .git directory accesses (workaround for tools like oxlint) + if relative.as_path().strip_prefix(".git").is_ok() { + return None; + } + + if !resolved_negatives.is_empty() + && resolved_negatives.iter().any(|neg| neg.is_match(relative.as_str())) + { + return None; + } + + Some(relative) +} + /// How the child process is awaited after stdout/stderr are drained. enum ChildWait { /// fspy tracking enabled — fspy manages cancellation internally. @@ -231,28 +260,7 @@ pub async fn spawn_with_tracking( let Ok(stripped_path) = strip_result else { return None; }; - // On Windows, paths are possible to be still absolute after stripping the workspace root. - // For example: c:\workspace\subdir\c:\workspace\subdir - // Just ignore those accesses. - let relative = RelativePathBuf::new(stripped_path).ok()?; - - // Clean `..` components — fspy may report paths like - // `packages/sub-pkg/../shared/dist/output.js`. Normalize them for - // consistent behavior across platforms and clean user-facing messages. - let relative = relative.clean(); - - // Skip .git directory accesses (workaround for tools like oxlint) - if relative.as_path().strip_prefix(".git").is_ok() { - return None; - } - - if !resolved_negatives.is_empty() - && resolved_negatives.iter().any(|neg| neg.is_match(relative.as_str())) - { - return None; - } - - Some(relative) + normalize_tracked_workspace_path(stripped_path, resolved_negatives) }); let Some(relative_path) = relative_path else { @@ -300,3 +308,16 @@ pub async fn spawn_with_tracking( } } } + +#[cfg(test)] +mod tests { + #[cfg(windows)] + use super::*; + + #[cfg(windows)] + #[test] + fn malformed_windows_drive_path_after_workspace_strip_is_ignored() { + let relative_path = normalize_tracked_workspace_path(Path::new(r"foo\C:\bar"), &[]); + assert!(relative_path.is_none()); + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/package.json new file mode 100644 index 00000000..13bc35eb --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/package.json @@ -0,0 +1,4 @@ +{ + "name": "malformed-fspy-path", + "private": true +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots.toml new file mode 100644 index 00000000..429b5303 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots.toml @@ -0,0 +1,7 @@ +# Windows-only repro for issue 325: a malformed observed path must not panic +# when fspy input inference normalizes workspace-relative accesses. + +[[e2e]] +name = "malformed observed path does not panic" +platform = "windows" +steps = [{ argv = ["vt", "run", "read-malformed-path"], envs = [["TEMP", "."], ["TMP", "."]] }] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots/malformed observed path does not panic.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots/malformed observed path does not panic.snap new file mode 100644 index 00000000..fe34e746 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/snapshots/malformed observed path does not panic.snap @@ -0,0 +1,7 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +> TEMP=. TMP=. vt run read-malformed-path +$ vtt print-file foo/C:/bar +foo/C:/bar: not found diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/vite-task.json new file mode 100644 index 00000000..c6790a86 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/malformed-fspy-path/vite-task.json @@ -0,0 +1,13 @@ +{ + "tasks": { + "read-malformed-path": { + "command": "vtt print-file foo/C:/bar", + "cache": true, + "input": [ + { + "auto": true + } + ] + } + } +} From 9e5debab4115abd5608eac348822dfa42f71c17e Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:43:45 +0900 Subject: [PATCH 2/7] fix(fspy): handle malformed tracked paths --- CHANGELOG.md | 1 + crates/vite_path/src/relative.rs | 25 +++++++++++++++---- crates/vite_task/src/session/execute/spawn.rs | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 851dba69..b11a2241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#325](https://github.com/voidzero-dev/vite-task/issues/325)) - **Fixed** `vp run --cache` now supports running without a task specifier and opens the interactive task selector, matching bare `vp run` behavior ([#312](https://github.com/voidzero-dev/vite-task/pull/313)) - **Fixed** Ctrl-C now prevents future tasks from being scheduled and prevents caching of in-flight task results ([#309](https://github.com/voidzero-dev/vite-task/pull/309)) - **Added** `--concurrency-limit` flag to limit the number of tasks running at the same time (defaults to 4) ([#288](https://github.com/voidzero-dev/vite-task/pull/288), [#309](https://github.com/voidzero-dev/vite-task/pull/309)) diff --git a/crates/vite_path/src/relative.rs b/crates/vite_path/src/relative.rs index 3d3bd8fe..c0364957 100644 --- a/crates/vite_path/src/relative.rs +++ b/crates/vite_path/src/relative.rs @@ -72,16 +72,17 @@ impl RelativePath { /// yields `a/c` instead of the correct `x/c`. Use /// [`std::fs::canonicalize`] when you need symlink-correct resolution. /// - /// # Panics + /// # Errors /// - /// Panics if the cleaned path is no longer a valid relative path, which - /// should never happen in practice. + /// Returns an error if the cleaned path is no longer a valid relative path. + /// This can happen on Windows when malformed inputs such as `foo/C:/bar` + /// are cleaned into drive-prefixed paths. #[must_use] - pub fn clean(&self) -> RelativePathBuf { + pub fn clean(&self) -> Result { use path_clean::PathClean as _; let cleaned = self.as_path().clean(); - RelativePathBuf::new(cleaned).expect("cleaning a relative path preserves relativity") + RelativePathBuf::new(cleaned) } /// Returns a path that, when joined onto `base`, yields `self`. @@ -441,6 +442,20 @@ mod tests { assert_eq!(joined_path.as_str(), "baz"); } + #[test] + fn clean() { + let rel_path = RelativePathBuf::new("../foo/../bar").unwrap(); + let cleaned = rel_path.clean().unwrap(); + assert_eq!(cleaned.as_str(), "../bar"); + } + + #[cfg(windows)] + #[test] + fn clean_malformed_drive_path() { + let rel_path = RelativePathBuf::new(r"foo\C:\bar").unwrap(); + let_assert!(Err(FromPathError::NonRelative) = rel_path.clean()); + } + #[test] fn strip_prefix() { let rel_path = RelativePathBuf::new("foo/bar/baz").unwrap(); diff --git a/crates/vite_task/src/session/execute/spawn.rs b/crates/vite_task/src/session/execute/spawn.rs index 1b00f6c4..4803d5c2 100644 --- a/crates/vite_task/src/session/execute/spawn.rs +++ b/crates/vite_task/src/session/execute/spawn.rs @@ -70,7 +70,7 @@ fn normalize_tracked_workspace_path( // Clean `..` components — fspy may report paths like // `packages/sub-pkg/../shared/dist/output.js`. Normalize them for // consistent behavior across platforms and clean user-facing messages. - let relative = relative.clean(); + let relative = relative.clean().ok()?; // Skip .git directory accesses (workaround for tools like oxlint) if relative.as_path().strip_prefix(".git").is_ok() { From e7eeaf1ec24b3e52fffc3eaa973eaf89bdfaacb1 Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:09:32 +0900 Subject: [PATCH 3/7] docs(changelog): point to pr instead of issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b11a2241..b92d24d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#325](https://github.com/voidzero-dev/vite-task/issues/325)) +- **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#325](https://github.com/voidzero-dev/vite-task/pull/330)) - **Fixed** `vp run --cache` now supports running without a task specifier and opens the interactive task selector, matching bare `vp run` behavior ([#312](https://github.com/voidzero-dev/vite-task/pull/313)) - **Fixed** Ctrl-C now prevents future tasks from being scheduled and prevents caching of in-flight task results ([#309](https://github.com/voidzero-dev/vite-task/pull/309)) - **Added** `--concurrency-limit` flag to limit the number of tasks running at the same time (defaults to 4) ([#288](https://github.com/voidzero-dev/vite-task/pull/288), [#309](https://github.com/voidzero-dev/vite-task/pull/309)) From a87a4fa1a6af06a6bc196ee20d1422325e431f1c Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:10:49 +0900 Subject: [PATCH 4/7] docs(changelog): correct pr number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b92d24d3..3eef1d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#325](https://github.com/voidzero-dev/vite-task/pull/330)) +- **Fixed** Windows file access tracking no longer panics when a task touches malformed paths that cannot be represented as workspace-relative inputs ([#330](https://github.com/voidzero-dev/vite-task/pull/330)) - **Fixed** `vp run --cache` now supports running without a task specifier and opens the interactive task selector, matching bare `vp run` behavior ([#312](https://github.com/voidzero-dev/vite-task/pull/313)) - **Fixed** Ctrl-C now prevents future tasks from being scheduled and prevents caching of in-flight task results ([#309](https://github.com/voidzero-dev/vite-task/pull/309)) - **Added** `--concurrency-limit` flag to limit the number of tasks running at the same time (defaults to 4) ([#288](https://github.com/voidzero-dev/vite-task/pull/288), [#309](https://github.com/voidzero-dev/vite-task/pull/309)) From 96e505ef0b6a0346434cc67bba03eea88ee37724 Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:38:17 +0900 Subject: [PATCH 5/7] fix: satisfy clippy warnings --- crates/vite_path/src/relative.rs | 1 - crates/vite_task/src/session/execute/spawn.rs | 10 +++++++--- crates/vite_task/src/session/mod.rs | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/vite_path/src/relative.rs b/crates/vite_path/src/relative.rs index c0364957..25b38804 100644 --- a/crates/vite_path/src/relative.rs +++ b/crates/vite_path/src/relative.rs @@ -77,7 +77,6 @@ impl RelativePath { /// Returns an error if the cleaned path is no longer a valid relative path. /// This can happen on Windows when malformed inputs such as `foo/C:/bar` /// are cleaned into drive-prefixed paths. - #[must_use] pub fn clean(&self) -> Result { use path_clean::PathClean as _; diff --git a/crates/vite_task/src/session/execute/spawn.rs b/crates/vite_task/src/session/execute/spawn.rs index 4803d5c2..6ff56cfd 100644 --- a/crates/vite_task/src/session/execute/spawn.rs +++ b/crates/vite_task/src/session/execute/spawn.rs @@ -3,7 +3,6 @@ use std::{ collections::hash_map::Entry, io::Write, - path::Path, process::{ExitStatus, Stdio}, time::{Duration, Instant}, }; @@ -58,8 +57,12 @@ pub struct TrackedPathAccesses { pub path_writes: FxHashSet, } +#[expect( + clippy::disallowed_types, + reason = "fspy strip_path_prefix exposes std::path::Path; convert to RelativePathBuf immediately" +)] fn normalize_tracked_workspace_path( - stripped_path: &Path, + stripped_path: &std::path::Path, resolved_negatives: &[wax::Glob<'static>], ) -> Option { // On Windows, paths are possible to be still absolute after stripping the workspace root. @@ -317,7 +320,8 @@ mod tests { #[cfg(windows)] #[test] fn malformed_windows_drive_path_after_workspace_strip_is_ignored() { - let relative_path = normalize_tracked_workspace_path(Path::new(r"foo\C:\bar"), &[]); + let relative_path = + normalize_tracked_workspace_path(std::path::Path::new(r"foo\C:\bar"), &[]); assert!(relative_path.is_none()); } } diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 1becb8b6..570a451e 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -355,6 +355,8 @@ impl<'a> Session<'a> { add: i32, ) -> i32; } + // SAFETY: Passing (None, FALSE) clears the inherited + // CTRL_C ignore flag. unsafe { SetConsoleCtrlHandler(None, 0); } From b72ae12b43485e2d4a270ffe5d217e0bde896046 Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:54:26 +0900 Subject: [PATCH 6/7] chore: simplify unsafe block --- crates/vite_task/src/session/mod.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 570a451e..a6968c90 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -345,21 +345,18 @@ impl<'a> Session<'a> { // created with CREATE_NEW_PROCESS_GROUP, which sets a per-process // flag that silently drops CTRL_C_EVENT before it reaches // registered handlers. Clear it so our handler fires. + // + // SAFETY: Passing (None, FALSE) clears the inherited + // CTRL_C ignore flag. #[cfg(windows)] - { - // SAFETY: Passing (None, FALSE) clears the inherited - // CTRL_C ignore flag. + unsafe { unsafe extern "system" { fn SetConsoleCtrlHandler( handler: Option i32>, add: i32, ) -> i32; } - // SAFETY: Passing (None, FALSE) clears the inherited - // CTRL_C ignore flag. - unsafe { - SetConsoleCtrlHandler(None, 0); - } + SetConsoleCtrlHandler(None, 0); } let interrupt_token = tokio_util::sync::CancellationToken::new(); let ct = interrupt_token.clone(); From 4d1e786b302f523d6595532c9a75014e2a13748b Mon Sep 17 00:00:00 2001 From: SegaraRai <29276700+SegaraRai@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:56:22 +0900 Subject: [PATCH 7/7] chore: clippy --- crates/vite_task/src/session/execute/spawn.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/vite_task/src/session/execute/spawn.rs b/crates/vite_task/src/session/execute/spawn.rs index 6ff56cfd..9c1ccb8e 100644 --- a/crates/vite_task/src/session/execute/spawn.rs +++ b/crates/vite_task/src/session/execute/spawn.rs @@ -320,6 +320,10 @@ mod tests { #[cfg(windows)] #[test] fn malformed_windows_drive_path_after_workspace_strip_is_ignored() { + #[expect( + clippy::disallowed_types, + reason = "normalize_tracked_workspace_path requires std::path::Path for fspy strip_path_prefix output" + )] let relative_path = normalize_tracked_workspace_path(std::path::Path::new(r"foo\C:\bar"), &[]); assert!(relative_path.is_none());