Skip to content
Open
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
25 changes: 20 additions & 5 deletions crates/vite_path/src/relative.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 +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();
Expand Down
65 changes: 43 additions & 22 deletions crates/vite_task/src/session/execute/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::{
collections::hash_map::Entry,
io::Write,
path::Path,
process::{ExitStatus, Stdio},
time::{Duration, Instant},
};
Expand Down Expand Up @@ -57,6 +58,34 @@ pub struct TrackedPathAccesses {
pub path_writes: FxHashSet<RelativePathBuf>,
}

fn normalize_tracked_workspace_path(
stripped_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 +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 {
Expand Down Expand Up @@ -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());
}
}
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
}
]
}
}
}