-
-
Notifications
You must be signed in to change notification settings - Fork 945
feat(template): add read_file() function #6400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6c17eb9
7bfa038
b9e5db9
ff03000
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| #!/usr/bin/env bash | ||
| # shellcheck disable=SC2016 | ||
|
|
||
| # Test basic read_file functionality | ||
| cat <<EOF >test_version.txt | ||
| 1.2.3 | ||
| EOF | ||
|
|
||
| cat <<EOF >mise.toml | ||
| [env] | ||
| VERSION = "{{ read_file(path='test_version.txt') | trim }}" | ||
| EOF | ||
|
|
||
| assert_contains "mise env -s bash | grep VERSION" "export VERSION=1.2.3" | ||
|
|
||
| # Test read_file with relative path | ||
| mkdir -p subdir | ||
| cat <<EOF >subdir/info.txt | ||
| test-content | ||
| EOF | ||
|
|
||
| cat <<EOF >mise.toml | ||
| [env] | ||
| CONTENT = "{{ read_file(path='subdir/info.txt') | trim }}" | ||
| EOF | ||
|
|
||
| assert_contains "mise env -s bash | grep CONTENT" "export CONTENT=test-content" | ||
|
|
||
| # Test read_file with multiple lines | ||
| cat <<EOF >multiline.txt | ||
| line1 | ||
| line2 | ||
| line3 | ||
| EOF | ||
|
|
||
| cat <<EOF >mise.toml | ||
| [env] | ||
| MULTILINE = "{{ read_file(path='multiline.txt') | replace(from='\n', to=' ') | trim }}" | ||
| EOF | ||
|
|
||
| assert_contains "mise env -s bash | grep MULTILINE" "export MULTILINE='line1 line2 line3'" | ||
|
|
||
| # Test read_file with vars | ||
| cat <<EOF >config.txt | ||
| production | ||
| EOF | ||
|
|
||
| cat <<EOF >mise.toml | ||
| [vars] | ||
| CONFIG_FILE = "config.txt" | ||
|
|
||
| [env] | ||
| ENVIRONMENT = "{{ read_file(path=vars.CONFIG_FILE) | trim }}" | ||
| EOF | ||
|
|
||
| assert_contains "mise env -s bash | grep ENVIRONMENT" "export ENVIRONMENT=production" | ||
|
|
||
| # Test read_file with templated content (should read raw content) | ||
| cat <<EOF >template_test.txt | ||
| {{ env.USER }} | ||
| EOF | ||
|
|
||
| cat <<EOF >mise.toml | ||
| [env] | ||
| RAW_CONTENT = "{{ read_file(path='template_test.txt') | trim }}" | ||
| EOF | ||
|
|
||
| assert_contains "mise env -s bash | grep RAW_CONTENT" "export RAW_CONTENT='{{ env.USER }}'" | ||
|
|
||
| # Test read_file is relative to config_root (not CWD) | ||
| # Create a nested project structure with config files at different levels | ||
| TEST_ROOT=$PWD | ||
| mkdir -p project/subdir | ||
| cd project | ||
|
|
||
| # Create version files at each level | ||
| echo "1.0.0" >version.txt | ||
| echo "2.0.0" >subdir/version.txt | ||
|
|
||
| # Create a data file only in parent | ||
| echo "parent-only-data" >parent-data.txt | ||
|
|
||
| # Create mise.toml in parent directory | ||
| cat <<EOF >mise.toml | ||
| [env] | ||
| PARENT_VERSION = "{{ read_file(path='version.txt') | trim }}" | ||
| PARENT_DATA = "{{ read_file(path='parent-data.txt') | trim }}" | ||
| EOF | ||
|
|
||
| # Create mise.toml in subdirectory that reads its own version.txt | ||
| cat <<EOF >subdir/mise.toml | ||
| [env] | ||
| SUB_VERSION = "{{ read_file(path='version.txt') | trim }}" | ||
| # This should fail if uncommented - file doesn't exist relative to subdir: | ||
| # SUB_PARENT_DATA = "{{ read_file(path='parent-data.txt') | trim }}" | ||
| EOF | ||
|
|
||
| # Test from parent directory - should read parent's version.txt (1.0.0) | ||
| assert_contains "mise env -s bash | grep PARENT_VERSION" "export PARENT_VERSION=1.0.0" | ||
| assert_contains "mise env -s bash | grep PARENT_DATA" "export PARENT_DATA=parent-only-data" | ||
|
|
||
| # Test from subdirectory - should read both configs, each with their own files | ||
| cd subdir | ||
| assert_contains "mise env -s bash | grep PARENT_VERSION" "export PARENT_VERSION=1.0.0" | ||
| assert_contains "mise env -s bash | grep SUB_VERSION" "export SUB_VERSION=2.0.0" | ||
| assert_contains "mise env -s bash | grep PARENT_DATA" "export PARENT_DATA=parent-only-data" | ||
|
|
||
| # Now test that running mise from a completely different directory still works | ||
| PROJECT_DIR=$TEST_ROOT/project | ||
| cd /tmp | ||
| assert_contains "mise env -s bash -C $PROJECT_DIR | grep PARENT_VERSION" "export PARENT_VERSION=1.0.0" | ||
| assert_contains "mise env -s bash -C $PROJECT_DIR/subdir | grep SUB_VERSION" "export SUB_VERSION=2.0.0" | ||
|
|
||
| # Go back to original test directory | ||
| cd "$TEST_ROOT" | ||
|
|
||
| # Cleanup | ||
| rm -f test_version.txt multiline.txt config.txt template_test.txt | ||
| rm -rf subdir project |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -303,7 +303,8 @@ static TERA: Lazy<Tera> = Lazy::new(|| { | |
| pub fn get_tera(dir: Option<&Path>) -> Tera { | ||
| let mut tera = TERA.clone(); | ||
| let dir = dir.map(PathBuf::from); | ||
| tera.register_function("exec", tera_exec(dir, env::PRISTINE_ENV.clone())); | ||
| tera.register_function("exec", tera_exec(dir.clone(), env::PRISTINE_ENV.clone())); | ||
| tera.register_function("read_file", tera_read_file(dir)); | ||
|
|
||
| tera | ||
| } | ||
|
|
@@ -374,6 +375,32 @@ pub fn tera_exec( | |
| } | ||
| } | ||
|
|
||
| pub fn tera_read_file( | ||
| dir: Option<PathBuf>, | ||
| ) -> impl Fn(&HashMap<String, Value>) -> tera::Result<Value> { | ||
| move |args: &HashMap<String, Value>| -> tera::Result<Value> { | ||
| match args.get("path") { | ||
| Some(Value::String(path_str)) => { | ||
| let path = if let Some(ref base_dir) = dir { | ||
| // Resolve relative to config directory | ||
| base_dir.join(path_str) | ||
| } else { | ||
| // Use path as-is if no directory context | ||
| PathBuf::from(path_str) | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Tera Template Engine Path Traversal VulnerabilityThe |
||
|
|
||
| match std::fs::read_to_string(&path) { | ||
| Ok(contents) => Ok(Value::String(contents)), | ||
| Err(e) => { | ||
| Err(format!("Failed to read file '{}': {}", path.display(), e).into()) | ||
| } | ||
| } | ||
|
Comment on lines
+392
to
+397
|
||
| } | ||
| _ => Err("read_file path must be a string".into()), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::config::Config; | ||
|
|
@@ -654,6 +681,37 @@ mod tests { | |
| assert_eq!(s.trim(), "ok"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| #[cfg(unix)] | ||
| async fn test_read_file() { | ||
| use std::fs; | ||
| use tempfile::TempDir; | ||
|
|
||
| let _config = Config::get().await.unwrap(); | ||
|
|
||
| // Create a temp directory and test file | ||
| let temp_dir = TempDir::new().unwrap(); | ||
| let test_file_path = temp_dir.path().join("test.txt"); | ||
| fs::write(&test_file_path, "test content\nwith multiple lines").unwrap(); | ||
|
|
||
| // Test with the temp file | ||
| let mut tera_ctx = BASE_CONTEXT.clone(); | ||
| tera_ctx.insert("config_root", &temp_dir.path().to_str().unwrap()); | ||
| tera_ctx.insert("cwd", temp_dir.path().to_str().unwrap()); | ||
| let mut tera = get_tera(Some(temp_dir.path())); | ||
|
|
||
| let s = tera | ||
| .render_str(r#"{{ read_file(path="test.txt") }}"#, &tera_ctx) | ||
| .unwrap(); | ||
| assert_eq!(s, "test content\nwith multiple lines"); | ||
|
|
||
| // Test with trim filter | ||
| let s = tera | ||
| .render_str(r#"{{ read_file(path="test.txt") | trim }}"#, &tera_ctx) | ||
| .unwrap(); | ||
| assert_eq!(s, "test content\nwith multiple lines"); | ||
| } | ||
|
|
||
| fn render(s: &str) -> String { | ||
| let config_root = Path::new("/"); | ||
| let mut tera_ctx = BASE_CONTEXT.clone(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When no directory context is provided, the function uses the path as-is, which could allow reading files from anywhere on the filesystem. Consider either requiring a base directory or defaulting to a safe directory like the current working directory to prevent unintended file access.