Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 ([#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))
Expand Down
26 changes: 20 additions & 6 deletions crates/vite_path/src/relative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ 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.
#[must_use]
pub fn clean(&self) -> RelativePathBuf {
/// 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.
pub fn clean(&self) -> Result<RelativePathBuf, FromPathError> {
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`.
Expand Down Expand Up @@ -441,6 +441,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();
Expand Down
73 changes: 51 additions & 22 deletions crates/vite_task/src/session/execute/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@ pub struct TrackedPathAccesses {
pub path_writes: FxHashSet<RelativePathBuf>,
}

#[expect(
clippy::disallowed_types,
reason = "fspy strip_path_prefix exposes std::path::Path; convert to RelativePathBuf immediately"
)]
fn normalize_tracked_workspace_path(
stripped_path: &std::path::Path,
resolved_negatives: &[wax::Glob<'static>],
) -> Option<RelativePathBuf> {
// 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().ok()?;

// 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.
Expand Down Expand Up @@ -231,28 +263,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 {
Expand Down Expand Up @@ -300,3 +311,21 @@ 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() {
#[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());
}
}
11 changes: 5 additions & 6 deletions crates/vite_task/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,19 +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<unsafe extern "system" fn(u32) -> i32>,
add: i32,
) -> i32;
}
unsafe {
SetConsoleCtrlHandler(None, 0);
}
SetConsoleCtrlHandler(None, 0);
}
let interrupt_token = tokio_util::sync::CancellationToken::new();
let ct = interrupt_token.clone();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "malformed-fspy-path",
"private": true
}
Original file line number Diff line number Diff line change
@@ -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", "."]] }]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"tasks": {
"read-malformed-path": {
"command": "vtt print-file foo/C:/bar",
"cache": true,
"input": [
{
"auto": true
}
]
}
}
}
Loading