use genfile_core::
{
TemplateArchive,
FileContent,
WriteMode,
MemoryFileSystem,
HandlebarsRenderer,
validate_path,
};
use std::path::{ Path, PathBuf };
#[ test ]
fn validate_path_accepts_simple_filename()
{
assert!( validate_path( Path::new( "file.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "readme.md" ) ).is_ok() );
}
#[ test ]
fn validate_path_accepts_nested_path()
{
assert!( validate_path( Path::new( "src/lib.rs" ) ).is_ok() );
assert!( validate_path( Path::new( "a/b/c/d.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "deeply/nested/directory/structure/file.txt" ) ).is_ok() );
}
#[ test ]
fn validate_path_accepts_current_dir_prefix()
{
assert!( validate_path( Path::new( "./file.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "./src/lib.rs" ) ).is_ok() );
}
#[ test ]
fn validate_path_accepts_current_dir_in_middle()
{
assert!( validate_path( Path::new( "foo/./bar.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "a/./b/./c.txt" ) ).is_ok() );
}
#[ test ]
fn validate_path_rejects_parent_dir_prefix()
{
let result = validate_path( Path::new( "../etc/passwd" ) );
assert!( result.is_err() );
let msg = format!( "{}", result.unwrap_err() );
assert!( msg.contains( "directory traversal" ) );
}
#[ test ]
fn validate_path_rejects_parent_dir_suffix()
{
let result = validate_path( Path::new( "foo/../bar" ) );
assert!( result.is_err() );
}
#[ test ]
fn validate_path_rejects_nested_parent_dirs()
{
assert!( validate_path( Path::new( "../../etc/passwd" ) ).is_err() );
assert!( validate_path( Path::new( "foo/../../bar" ) ).is_err() );
assert!( validate_path( Path::new( "a/b/c/../../../d" ) ).is_err() );
}
#[ test ]
fn validate_path_rejects_parent_in_middle()
{
assert!( validate_path( Path::new( "a/../b" ) ).is_err() );
assert!( validate_path( Path::new( "src/../etc/passwd" ) ).is_err() );
}
#[ test ]
fn validate_path_error_message_includes_path()
{
let result = validate_path( Path::new( "../malicious/file.txt" ) );
assert!( result.is_err() );
let err = result.unwrap_err();
let msg = format!( "{err}" );
assert!( msg.contains( "directory traversal" ) );
assert!( msg.contains( "../malicious/file.txt" ) );
}
#[ test ]
fn materialize_blocks_parent_dir_traversal()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_file(
PathBuf::from( "../etc/passwd" ),
FileContent::Text( "malicious content".to_string() ),
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/safe-output" ),
&renderer,
&mut fs
);
assert!( result.is_err() );
let err = result.unwrap_err();
let msg = format!( "{err}" );
assert!( msg.contains( "directory traversal" ) );
}
#[ test ]
fn materialize_blocks_nested_parent_dirs()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_text_file(
PathBuf::from( "foo/../../etc/passwd" ),
"malicious",
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!( result.is_err() );
}
#[ test ]
fn materialize_blocks_parent_in_middle()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_text_file(
PathBuf::from( "src/../etc/passwd" ),
"malicious",
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!( result.is_err() );
}
#[ test ]
fn materialize_allows_safe_paths()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_text_file(
PathBuf::from( "src/lib.rs" ),
"safe content",
WriteMode::Rewrite
);
archive.add_text_file(
PathBuf::from( "README.md" ),
"# Safe",
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!( result.is_ok() );
let report = result.unwrap();
assert_eq!( report.files_created.len(), 2 );
}
#[ test ]
fn materialize_allows_current_dir_references()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_text_file(
PathBuf::from( "./src/main.rs" ),
"content",
WriteMode::Rewrite
);
archive.add_text_file(
PathBuf::from( "foo/./bar.txt" ),
"content",
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!( result.is_ok() );
}
#[ test ]
fn materialize_with_multiple_files_stops_on_first_malicious()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_text_file(
PathBuf::from( "file1.txt" ),
"safe",
WriteMode::Rewrite
);
archive.add_text_file(
PathBuf::from( "file2.txt" ),
"safe",
WriteMode::Rewrite
);
archive.add_text_file(
PathBuf::from( "../malicious.txt" ),
"bad",
WriteMode::Rewrite
);
archive.add_text_file(
PathBuf::from( "file3.txt" ),
"safe",
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!( result.is_err() );
}
#[ test ]
fn materialize_validates_before_rendering()
{
let mut archive = TemplateArchive::new( "test" );
archive.add_text_file(
PathBuf::from( "../etc/{{filename}}" ),
"template content",
WriteMode::Rewrite
);
archive.set_value( "filename", genfile_core::Value::String( "passwd".to_string() ) );
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!( result.is_err() );
let err = result.unwrap_err();
let msg = format!( "{err}" );
assert!( msg.contains( "directory traversal" ) );
}
#[ test ]
fn validate_path_works_with_deeply_nested_structures()
{
assert!( validate_path( Path::new( "a/b/c/d/e/f/g/h/i/j/file.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "a/b/../c/d/e/f/file.txt" ) ).is_err() );
assert!( validate_path( Path::new( "a/b/c/d/e/../../../f/file.txt" ) ).is_err() );
}
#[ test ]
fn validate_path_handles_various_extensions()
{
assert!( validate_path( Path::new( "file.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "image.png" ) ).is_ok() );
assert!( validate_path( Path::new( "script.sh" ) ).is_ok() );
assert!( validate_path( Path::new( "Cargo.toml" ) ).is_ok() );
assert!( validate_path( Path::new( ".gitignore" ) ).is_ok() );
assert!( validate_path( Path::new( "no_extension" ) ).is_ok() );
}
#[ test ]
fn validate_path_handles_unicode()
{
assert!( validate_path( Path::new( "файл.txt" ) ).is_ok() );
assert!( validate_path( Path::new( "文件.md" ) ).is_ok() );
assert!( validate_path( Path::new( "καταχώριση.rs" ) ).is_ok() );
assert!( validate_path( Path::new( "../файл.txt" ) ).is_err() );
}
#[ test ]
fn spec_requirement_path_traversal_validation()
{
let archive = TemplateArchive::new( "security-test" );
let malicious_paths = vec![
"../etc/passwd",
"../../root/.ssh/id_rsa",
"foo/../../../etc/shadow",
"./../malicious",
];
for malicious_path in malicious_paths
{
let mut test_archive = archive.clone();
test_archive.add_text_file(
PathBuf::from( malicious_path ),
"malicious content",
WriteMode::Rewrite
);
let renderer = HandlebarsRenderer::new();
let mut fs = MemoryFileSystem::new();
let result = test_archive.materialize_with_components(
Path::new( "/output" ),
&renderer,
&mut fs
);
assert!(
result.is_err(),
"Path '{malicious_path}' should be rejected but was allowed"
);
}
}