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
32 changes: 32 additions & 0 deletions e2e/cli/test_prepare
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

# Test mise prepare (mise prep) command
Expand Down Expand Up @@ -492,3 +492,35 @@
# Clean up
rm -f tera_input.txt tera_output.txt mise.toml
rm -rf .mise

# Test dir option affects source/output resolution for custom providers
mkdir -p subdir
echo '{"name": "test"}' >subdir/package.json
echo '{"lockfileVersion": 3}' >subdir/package-lock.json

cat >mise.toml <<'EOF'
[prepare.subdir_custom]
dir = "subdir"
sources = ["package.json", "package-lock.json"]
outputs = ["build_output"]
run = "echo SUBDIR_RAN"
EOF

# Sources should resolve against subdir, not project root
assert_contains "mise prepare --list" "subdir_custom"
assert_contains "mise prepare --list" "subdir/package.json"
assert_contains "mise prepare --list" "subdir/package-lock.json"

# Run prepare and verify source hashes are tracked (provider becomes fresh)
mkdir -p subdir/build_output
assert_contains "mise prepare --only subdir_custom" "Prepared: subdir_custom"

# Second run should be fresh (sources hashed correctly via subdir)
assert_contains "mise prepare --only subdir_custom" "up to date"

# Modify source in subdir → should be stale
echo '{"name": "modified"}' >subdir/package.json
assert_contains "mise prepare --only subdir_custom" "Prepared: subdir_custom"

# Clean up
rm -rf subdir mise.toml .mise
11 changes: 6 additions & 5 deletions src/prepare/providers/bun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ impl BunPrepareProvider {
}

fn lockfile_path(&self) -> Option<PathBuf> {
let root = self.base.config_root();
// Bun supports both bun.lockb (binary) and bun.lock (text)
let binary_lock = self.base.project_root.join("bun.lockb");
let binary_lock = root.join("bun.lockb");
if binary_lock.exists() {
return Some(binary_lock);
}
let text_lock = self.base.project_root.join("bun.lock");
let text_lock = root.join("bun.lock");
if text_lock.exists() {
return Some(text_lock);
}
Expand All @@ -44,12 +45,12 @@ impl PrepareProvider for BunPrepareProvider {
if let Some(lockfile) = self.lockfile_path() {
sources.push(lockfile);
}
sources.push(self.base.project_root.join("package.json"));
sources.push(self.base.config_root().join("package.json"));
sources
}

fn outputs(&self) -> Vec<PathBuf> {
vec![self.base.project_root.join("node_modules")]
vec![self.base.config_root().join("node_modules")]
}

fn prepare_command(&self) -> Result<PrepareCommand> {
Expand All @@ -61,7 +62,7 @@ impl PrepareProvider for BunPrepareProvider {
program: "bun".to_string(),
args: vec!["install".to_string()],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand Down
15 changes: 7 additions & 8 deletions src/prepare/providers/bundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,19 @@ impl PrepareProvider for BundlerPrepareProvider {
}

fn sources(&self) -> Vec<PathBuf> {
vec![
self.base.project_root.join("Gemfile.lock"),
self.base.project_root.join("Gemfile"),
]
let root = self.base.config_root();
vec![root.join("Gemfile.lock"), root.join("Gemfile")]
}

fn outputs(&self) -> Vec<PathBuf> {
let root = self.base.config_root();
// Check for vendor/bundle if using --path vendor/bundle
let vendor = self.base.project_root.join("vendor/bundle");
let vendor = root.join("vendor/bundle");
if vendor.exists() {
vec![vendor]
} else {
// Use .bundle directory as fallback indicator
vec![self.base.project_root.join(".bundle")]
vec![root.join(".bundle")]
}
}

Expand All @@ -53,7 +52,7 @@ impl PrepareProvider for BundlerPrepareProvider {
program: "bundle".to_string(),
args: vec!["install".to_string()],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -64,6 +63,6 @@ impl PrepareProvider for BundlerPrepareProvider {
}

fn is_applicable(&self) -> bool {
self.base.project_root.join("Gemfile.lock").exists()
self.base.config_root().join("Gemfile.lock").exists()
}
}
12 changes: 5 additions & 7 deletions src/prepare/providers/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@ impl PrepareProvider for ComposerPrepareProvider {
}

fn sources(&self) -> Vec<PathBuf> {
vec![
self.base.project_root.join("composer.lock"),
self.base.project_root.join("composer.json"),
]
let root = self.base.config_root();
vec![root.join("composer.lock"), root.join("composer.json")]
}

fn outputs(&self) -> Vec<PathBuf> {
vec![self.base.project_root.join("vendor")]
vec![self.base.config_root().join("vendor")]
}

fn prepare_command(&self) -> Result<PrepareCommand> {
Expand All @@ -46,7 +44,7 @@ impl PrepareProvider for ComposerPrepareProvider {
program: "composer".to_string(),
args: vec!["install".to_string()],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -57,6 +55,6 @@ impl PrepareProvider for ComposerPrepareProvider {
}

fn is_applicable(&self) -> bool {
self.base.project_root.join("composer.lock").exists()
self.base.config_root().join("composer.lock").exists()
}
}
3 changes: 2 additions & 1 deletion src/prepare/providers/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ impl CustomPrepareProvider {
let mut paths = vec![];

for pattern in patterns {
let base_dir = self.base.config_root();
Comment on lines 28 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For a minor performance improvement, base_dir can be initialized outside of the loop since it doesn't change between iterations.

Suggested change
for pattern in patterns {
let base_dir = self.base.config_root();
let base_dir = self.base.config_root();
for pattern in patterns {

let full_pattern = if PathBuf::from(pattern).is_relative() {
self.base.project_root.join(pattern)
base_dir.join(pattern)
} else {
PathBuf::from(pattern)
};
Expand Down
10 changes: 5 additions & 5 deletions src/prepare/providers/git_submodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl GitSubmodulePrepareProvider {
/// Handles INI-style sections and comments. Only extracts `path` values
/// from `[submodule "..."]` sections.
fn submodule_paths(&self) -> Vec<PathBuf> {
let gitmodules = self.base.project_root.join(".gitmodules");
let gitmodules = self.base.config_root().join(".gitmodules");
let Ok(content) = std::fs::read_to_string(&gitmodules) else {
return vec![];
};
Expand Down Expand Up @@ -53,7 +53,7 @@ impl GitSubmodulePrepareProvider {
let value = value.trim_start();
value
.strip_prefix('=')
.map(|value| self.base.project_root.join(value.trim()))
.map(|value| self.base.config_root().join(value.trim()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

This code block in submodule_paths is vulnerable to path traversal. The path values from the untrusted .gitmodules file are joined directly with the configuration root without validating that they are relative paths. This allows an attacker to provide malicious paths (e.g., absolute paths or parent directory references like path = /etc/passwd or path = ../../../etc/passwd), which could cause mise to interact with arbitrary files on the user's system, leading to information leakage or modification of file metadata. To remediate this, validate that the path values extracted from .gitmodules are relative and do not traverse outside the intended directory. You can use Path::is_relative() and ensure the resulting path is contained within the config_root. Additionally, for performance, self.base.config_root() is called repeatedly inside this loop; consider calling it once and reusing the result.

} else {
None
}
Expand All @@ -68,7 +68,7 @@ impl PrepareProvider for GitSubmodulePrepareProvider {
}

fn sources(&self) -> Vec<PathBuf> {
vec![self.base.project_root.join(".gitmodules")]
vec![self.base.config_root().join(".gitmodules")]
}

fn outputs(&self) -> Vec<PathBuf> {
Expand All @@ -89,7 +89,7 @@ impl PrepareProvider for GitSubmodulePrepareProvider {
"--recursive".to_string(),
],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -100,7 +100,7 @@ impl PrepareProvider for GitSubmodulePrepareProvider {
}

fn is_applicable(&self) -> bool {
let gitmodules = self.base.project_root.join(".gitmodules");
let gitmodules = self.base.config_root().join(".gitmodules");
gitmodules.exists() && gitmodules.metadata().map(|m| m.len() > 0).unwrap_or(false)
}
}
13 changes: 7 additions & 6 deletions src/prepare/providers/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,18 @@ impl PrepareProvider for GoPrepareProvider {

fn sources(&self) -> Vec<PathBuf> {
// go.mod defines dependencies - changes here trigger downloads
vec![self.base.project_root.join("go.mod")]
vec![self.base.config_root().join("go.mod")]
}

fn outputs(&self) -> Vec<PathBuf> {
let root = self.base.config_root();
// Go downloads modules to GOPATH/pkg/mod, but we can check vendor/ if used
let vendor = self.base.project_root.join("vendor");
let vendor = root.join("vendor");
if vendor.exists() {
vec![vendor]
} else {
// go.sum gets updated after go mod download completes
vec![self.base.project_root.join("go.sum")]
vec![root.join("go.sum")]
}
}

Expand All @@ -48,7 +49,7 @@ impl PrepareProvider for GoPrepareProvider {
}

// Use `go mod vendor` if vendor/ exists, otherwise `go mod download`
let vendor = self.base.project_root.join("vendor");
let vendor = self.base.config_root().join("vendor");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The path to the vendor directory is constructed here and also in the outputs function on line 37. To avoid this duplication and improve maintainability, you could consider creating a private helper method on GoPrepareProvider like fn vendor_path(&self) -> PathBuf that returns self.base.config_root().join("vendor"). Then, both outputs and prepare_command can call this helper.

let (args, desc) = if vendor.exists() {
(
vec!["mod".to_string(), "vendor".to_string()],
Expand All @@ -65,7 +66,7 @@ impl PrepareProvider for GoPrepareProvider {
program: "go".to_string(),
args,
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -77,6 +78,6 @@ impl PrepareProvider for GoPrepareProvider {

fn is_applicable(&self) -> bool {
// Check for go.mod (the source/lockfile), not go.sum (which may be an output)
self.base.project_root.join("go.mod").exists()
self.base.config_root().join("go.mod").exists()
}
}
9 changes: 9 additions & 0 deletions src/prepare/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,13 @@ impl ProviderBase {
pub fn touch_outputs(&self) -> bool {
self.config.touch_outputs.unwrap_or(true)
}

/// Returns the effective root directory for resolving sources/outputs.
/// When `dir` is set in config, returns `project_root/dir`; otherwise `project_root`.
pub fn config_root(&self) -> PathBuf {
match &self.config.dir {
Some(dir) => self.project_root.join(dir),
None => self.project_root.clone(),
}
}
}
12 changes: 5 additions & 7 deletions src/prepare/providers/npm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@ impl PrepareProvider for NpmPrepareProvider {
}

fn sources(&self) -> Vec<PathBuf> {
vec![
self.base.project_root.join("package-lock.json"),
self.base.project_root.join("package.json"),
]
let root = self.base.config_root();
vec![root.join("package-lock.json"), root.join("package.json")]
}

fn outputs(&self) -> Vec<PathBuf> {
vec![self.base.project_root.join("node_modules")]
vec![self.base.config_root().join("node_modules")]
}

fn prepare_command(&self) -> Result<PrepareCommand> {
Expand All @@ -46,7 +44,7 @@ impl PrepareProvider for NpmPrepareProvider {
program: "npm".to_string(),
args: vec!["install".to_string()],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -57,6 +55,6 @@ impl PrepareProvider for NpmPrepareProvider {
}

fn is_applicable(&self) -> bool {
self.base.project_root.join("package-lock.json").exists()
self.base.config_root().join("package-lock.json").exists()
}
}
8 changes: 4 additions & 4 deletions src/prepare/providers/pip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ impl PrepareProvider for PipPrepareProvider {
}

fn sources(&self) -> Vec<PathBuf> {
vec![self.base.project_root.join("requirements.txt")]
vec![self.base.config_root().join("requirements.txt")]
}

fn outputs(&self) -> Vec<PathBuf> {
// Check for .venv directory as output indicator
vec![self.base.project_root.join(".venv")]
vec![self.base.config_root().join(".venv")]
}

fn prepare_command(&self) -> Result<PrepareCommand> {
Expand All @@ -48,7 +48,7 @@ impl PrepareProvider for PipPrepareProvider {
"requirements.txt".to_string(),
],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -59,6 +59,6 @@ impl PrepareProvider for PipPrepareProvider {
}

fn is_applicable(&self) -> bool {
self.base.project_root.join("requirements.txt").exists()
self.base.config_root().join("requirements.txt").exists()
}
}
12 changes: 5 additions & 7 deletions src/prepare/providers/pnpm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@ impl PrepareProvider for PnpmPrepareProvider {
}

fn sources(&self) -> Vec<PathBuf> {
vec![
self.base.project_root.join("pnpm-lock.yaml"),
self.base.project_root.join("package.json"),
]
let root = self.base.config_root();
vec![root.join("pnpm-lock.yaml"), root.join("package.json")]
}

fn outputs(&self) -> Vec<PathBuf> {
vec![self.base.project_root.join("node_modules")]
vec![self.base.config_root().join("node_modules")]
}

fn prepare_command(&self) -> Result<PrepareCommand> {
Expand All @@ -46,7 +44,7 @@ impl PrepareProvider for PnpmPrepareProvider {
program: "pnpm".to_string(),
args: vec!["install".to_string()],
env: self.base.config.env.clone(),
cwd: Some(self.base.project_root.clone()),
cwd: Some(self.base.config_root()),
description: self
.base
.config
Expand All @@ -57,6 +55,6 @@ impl PrepareProvider for PnpmPrepareProvider {
}

fn is_applicable(&self) -> bool {
self.base.project_root.join("pnpm-lock.yaml").exists()
self.base.config_root().join("pnpm-lock.yaml").exists()
}
}
Loading
Loading