1- use std:: io:: Write ;
2- use zip:: write:: SimpleFileOptions ;
3- use zip:: { ZipArchive , ZipWriter } ;
4-
5- /// Create a ZIP file with entries that have absolute paths (starting with /)
6- /// This simulates the problematic ZIP file mentioned in the bug report
7- fn create_zip_with_absolute_paths ( ) -> Vec < u8 > {
8- let buf = Vec :: new ( ) ;
9- let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
10- let options = SimpleFileOptions :: default ( ) ;
11-
12- // Create entries with absolute paths - this should cause "Invalid file path" error
13- writer. add_directory ( "/_/" , options) . unwrap ( ) ;
14- writer. start_file ( "/_/file1.txt" , options) . unwrap ( ) ;
15- writer. write_all ( b"File 1 content" ) . unwrap ( ) ;
16- writer. start_file ( "/_/subdir/file2.txt" , options) . unwrap ( ) ;
17- writer. write_all ( b"File 2 content" ) . unwrap ( ) ;
18-
19- writer. finish ( ) . unwrap ( ) . into_inner ( )
20- }
1+ #[ cfg( all(
2+ test,
3+ not( all( feature = "deflate-zopfli" , not( feature = "deflate-flate2" ) ) )
4+ ) ) ]
5+ pub mod tests {
6+ use std:: io:: Write ;
7+ use zip:: write:: SimpleFileOptions ;
8+ use zip:: { ZipArchive , ZipWriter } ;
9+
10+ /// Create a ZIP file with entries that have absolute paths (starting with /)
11+ /// This simulates the problematic ZIP file mentioned in the bug report
12+ fn create_zip_with_absolute_paths ( ) -> Vec < u8 > {
13+ let buf = Vec :: new ( ) ;
14+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
15+ let options = SimpleFileOptions :: default ( ) ;
16+
17+ // Create entries with absolute paths - this should cause "Invalid file path" error
18+ writer. add_directory ( "/_/" , options) . unwrap ( ) ;
19+ writer. start_file ( "/_/file1.txt" , options) . unwrap ( ) ;
20+ writer. write_all ( b"File 1 content" ) . unwrap ( ) ;
21+ writer. start_file ( "/_/subdir/file2.txt" , options) . unwrap ( ) ;
22+ writer. write_all ( b"File 2 content" ) . unwrap ( ) ;
23+
24+ writer. finish ( ) . unwrap ( ) . into_inner ( )
25+ }
2126
22- /// Create a ZIP file with entries that have Windows-style absolute paths
23- fn create_zip_with_windows_absolute_paths ( ) -> Vec < u8 > {
24- let buf = Vec :: new ( ) ;
25- let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
26- let options = SimpleFileOptions :: default ( ) ;
27+ /// Create a ZIP file with entries that have Windows-style absolute paths
28+ fn create_zip_with_windows_absolute_paths ( ) -> Vec < u8 > {
29+ let buf = Vec :: new ( ) ;
30+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
31+ let options = SimpleFileOptions :: default ( ) ;
2732
28- // Create entries with Windows absolute paths
29- writer. add_directory ( "C:\\ temp\\ " , options) . unwrap ( ) ;
30- writer. start_file ( "C:\\ temp\\ file1.txt" , options) . unwrap ( ) ;
31- writer. write_all ( b"File 1 content" ) . unwrap ( ) ;
33+ // Create entries with Windows absolute paths
34+ writer. add_directory ( "C:\\ temp\\ " , options) . unwrap ( ) ;
35+ writer. start_file ( "C:\\ temp\\ file1.txt" , options) . unwrap ( ) ;
36+ writer. write_all ( b"File 1 content" ) . unwrap ( ) ;
3237
33- writer. finish ( ) . unwrap ( ) . into_inner ( )
34- }
38+ writer. finish ( ) . unwrap ( ) . into_inner ( )
39+ }
3540
36- /// Create a ZIP file that more closely simulates the soldeer registry issue
37- /// with an underscore directory at the root with absolute path
38- fn create_zip_like_soldeer_issue ( ) -> Vec < u8 > {
39- let buf = Vec :: new ( ) ;
40- let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
41- let options = SimpleFileOptions :: default ( ) ;
42-
43- // Simulate the soldeer registry structure with absolute paths
44- writer. add_directory ( "/_/" , options) . unwrap ( ) ;
45- writer. add_directory ( "/_/forge-std/" , options) . unwrap ( ) ;
46- writer. start_file ( "/_/forge-std/src/Test.sol" , options) . unwrap ( ) ;
47- writer. write_all ( b"// SPDX-License-Identifier: MIT\n pragma solidity ^0.8.0;\n " ) . unwrap ( ) ;
48- writer. start_file ( "/_/forge-std/lib/ds-test/src/test.sol" , options) . unwrap ( ) ;
49- writer. write_all ( b"// Test contract\n " ) . unwrap ( ) ;
50-
51- writer. finish ( ) . unwrap ( ) . into_inner ( )
52- }
41+ /// Create a ZIP file that more closely simulates the soldeer registry issue
42+ /// with an underscore directory at the root with absolute path
43+ fn create_zip_like_soldeer_issue ( ) -> Vec < u8 > {
44+ let buf = Vec :: new ( ) ;
45+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
46+ let options = SimpleFileOptions :: default ( ) ;
47+
48+ // Simulate the soldeer registry structure with absolute paths
49+ writer. add_directory ( "/_/" , options) . unwrap ( ) ;
50+ writer. add_directory ( "/_/forge-std/" , options) . unwrap ( ) ;
51+ writer
52+ . start_file ( "/_/forge-std/src/Test.sol" , options)
53+ . unwrap ( ) ;
54+ writer
55+ . write_all ( b"// SPDX-License-Identifier: MIT\n pragma solidity ^0.8.0;\n " )
56+ . unwrap ( ) ;
57+ writer
58+ . start_file ( "/_/forge-std/lib/ds-test/src/test.sol" , options)
59+ . unwrap ( ) ;
60+ writer. write_all ( b"// Test contract\n " ) . unwrap ( ) ;
61+
62+ writer. finish ( ) . unwrap ( ) . into_inner ( )
63+ }
5364
54- #[ test]
55- fn test_extract_zip_with_absolute_paths ( ) {
56- let zip_data = create_zip_with_absolute_paths ( ) ;
57- let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
58-
59- // After fix: should extract successfully, stripping the leading /
60- let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
61- archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
62-
63- // Files should be extracted with the absolute path prefix stripped
64- assert ! ( temp_dir. path( ) . join( "_" ) . exists( ) ) ;
65- assert ! ( temp_dir. path( ) . join( "_/file1.txt" ) . exists( ) ) ;
66- assert ! ( temp_dir. path( ) . join( "_/subdir/file2.txt" ) . exists( ) ) ;
67-
68- // Verify file contents
69- let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "_/file1.txt" ) ) . unwrap ( ) ;
70- assert_eq ! ( content, "File 1 content" ) ;
71- }
65+ #[ test]
66+ fn test_extract_zip_with_absolute_paths ( ) {
67+ let zip_data = create_zip_with_absolute_paths ( ) ;
68+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
7269
73- #[ test]
74- fn test_extract_zip_with_windows_absolute_paths ( ) {
75- let zip_data = create_zip_with_windows_absolute_paths ( ) ;
76- let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
77-
78- // After fix: should extract successfully, stripping the C:\ prefix
79- let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
80- archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
81-
82- // Files should be extracted with the Windows absolute path prefix stripped
83- assert ! ( temp_dir. path( ) . join( "temp" ) . exists( ) ) ;
84- assert ! ( temp_dir. path( ) . join( "temp/file1.txt" ) . exists( ) ) ;
85-
86- // Verify file contents
87- let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "temp/file1.txt" ) ) . unwrap ( ) ;
88- assert_eq ! ( content, "File 1 content" ) ;
89- }
70+ // After fix: should extract successfully, stripping the leading /
71+ let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
72+ archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
9073
91- #[ test]
92- fn test_extract_soldeer_like_zip ( ) {
93- let zip_data = create_zip_like_soldeer_issue ( ) ;
94- let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
95-
96- // This should now work without "Invalid file path" error
97- let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
98- archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
99-
100- // Verify the structure is extracted correctly with absolute prefix stripped
101- assert ! ( temp_dir. path( ) . join( "_" ) . exists( ) ) ;
102- assert ! ( temp_dir. path( ) . join( "_/forge-std" ) . exists( ) ) ;
103- assert ! ( temp_dir. path( ) . join( "_/forge-std/src/Test.sol" ) . exists( ) ) ;
104- assert ! ( temp_dir. path( ) . join( "_/forge-std/lib/ds-test/src/test.sol" ) . exists( ) ) ;
105-
106- // Verify file contents
107- let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "_/forge-std/src/Test.sol" ) ) . unwrap ( ) ;
108- assert ! ( content. contains( "SPDX-License-Identifier" ) ) ;
109- }
74+ // Files should be extracted with the absolute path prefix stripped
75+ assert ! ( temp_dir. path( ) . join( "_" ) . exists( ) ) ;
76+ assert ! ( temp_dir. path( ) . join( "_/file1.txt" ) . exists( ) ) ;
77+ assert ! ( temp_dir. path( ) . join( "_/subdir/file2.txt" ) . exists( ) ) ;
11078
111- #[ test]
112- fn test_individual_file_access_with_absolute_paths ( ) {
113- let zip_data = create_zip_with_absolute_paths ( ) ;
114- let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
115-
116- // Test accessing individual files
117- for i in 0 ..archive. len ( ) {
118- let file = archive. by_index ( i) . unwrap ( ) ;
119- println ! ( "File name: {}" , file. name( ) ) ;
120-
121- // After our fix, enclosed_name should return a safe relative path
122- let enclosed_name = file. enclosed_name ( ) ;
123- println ! ( "Enclosed name: {:?}" , enclosed_name) ;
124-
125- // Should now return Some with the absolute prefix stripped
126- assert ! ( enclosed_name. is_some( ) ) ;
127- let path = enclosed_name. unwrap ( ) ;
128-
129- // Verify the path doesn't start with / or contain absolute components
130- assert ! ( !path. is_absolute( ) ) ;
131- assert ! ( !path. to_string_lossy( ) . starts_with( '/' ) ) ;
79+ // Verify file contents
80+ let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "_/file1.txt" ) ) . unwrap ( ) ;
81+ assert_eq ! ( content, "File 1 content" ) ;
13282 }
133- }
13483
135- #[ test]
136- fn test_security_still_prevents_directory_traversal ( ) {
137- let buf = Vec :: new ( ) ;
138- let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
139- let options = SimpleFileOptions :: default ( ) ;
140-
141- // Create a ZIP with directory traversal attempts
142- writer. start_file ( "../../../etc/passwd" , options) . unwrap ( ) ;
143- writer. write_all ( b"malicious content" ) . unwrap ( ) ;
144- writer. start_file ( "foo/../../../etc/shadow" , options) . unwrap ( ) ;
145- writer. write_all ( b"more malicious content" ) . unwrap ( ) ;
146-
147- let zip_data = writer. finish ( ) . unwrap ( ) . into_inner ( ) ;
148- let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
149-
150- // These should still fail due to directory traversal protection
151- for i in 0 ..archive. len ( ) {
152- let file = archive. by_index ( i) . unwrap ( ) ;
153- let enclosed_name = file. enclosed_name ( ) ;
154-
155- // Directory traversal attempts should still return None
156- assert ! ( enclosed_name. is_none( ) , "Directory traversal should still be blocked for: {}" , file. name( ) ) ;
84+ #[ test]
85+ fn test_extract_zip_with_windows_absolute_paths ( ) {
86+ let zip_data = create_zip_with_windows_absolute_paths ( ) ;
87+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
88+
89+ // After fix: should extract successfully, stripping the C:\ prefix
90+ let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
91+ archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
92+
93+ // Files should be extracted with the Windows absolute path prefix stripped
94+ assert ! ( temp_dir. path( ) . join( "temp" ) . exists( ) ) ;
95+ assert ! ( temp_dir. path( ) . join( "temp/file1.txt" ) . exists( ) ) ;
96+
97+ // Verify file contents
98+ let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "temp/file1.txt" ) ) . unwrap ( ) ;
99+ assert_eq ! ( content, "File 1 content" ) ;
100+ }
101+
102+ #[ test]
103+ fn test_extract_soldeer_like_zip ( ) {
104+ let zip_data = create_zip_like_soldeer_issue ( ) ;
105+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
106+
107+ // This should now work without "Invalid file path" error
108+ let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
109+ archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
110+
111+ // Verify the structure is extracted correctly with absolute prefix stripped
112+ assert ! ( temp_dir. path( ) . join( "_" ) . exists( ) ) ;
113+ assert ! ( temp_dir. path( ) . join( "_/forge-std" ) . exists( ) ) ;
114+ assert ! ( temp_dir. path( ) . join( "_/forge-std/src/Test.sol" ) . exists( ) ) ;
115+ assert ! ( temp_dir
116+ . path( )
117+ . join( "_/forge-std/lib/ds-test/src/test.sol" )
118+ . exists( ) ) ;
119+
120+ // Verify file contents
121+ let content =
122+ std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "_/forge-std/src/Test.sol" ) ) . unwrap ( ) ;
123+ assert ! ( content. contains( "SPDX-License-Identifier" ) ) ;
124+ }
125+
126+ #[ test]
127+ fn test_individual_file_access_with_absolute_paths ( ) {
128+ let zip_data = create_zip_with_absolute_paths ( ) ;
129+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
130+
131+ // Test accessing individual files
132+ for i in 0 ..archive. len ( ) {
133+ let file = archive. by_index ( i) . unwrap ( ) ;
134+ println ! ( "File name: {}" , file. name( ) ) ;
135+
136+ // After our fix, enclosed_name should return a safe relative path
137+ let enclosed_name = file. enclosed_name ( ) ;
138+ println ! ( "Enclosed name: {:?}" , enclosed_name) ;
139+
140+ // Should now return Some with the absolute prefix stripped
141+ assert ! ( enclosed_name. is_some( ) ) ;
142+ let path = enclosed_name. unwrap ( ) ;
143+
144+ // Verify the path doesn't start with / or contain absolute components
145+ assert ! ( !path. is_absolute( ) ) ;
146+ assert ! ( !path. to_string_lossy( ) . starts_with( '/' ) ) ;
147+ }
157148 }
158- }
149+
150+ #[ test]
151+ fn test_security_still_prevents_directory_traversal ( ) {
152+ let buf = Vec :: new ( ) ;
153+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
154+ let options = SimpleFileOptions :: default ( ) ;
155+
156+ // Create a ZIP with directory traversal attempts
157+ writer. start_file ( "../../../etc/passwd" , options) . unwrap ( ) ;
158+ writer. write_all ( b"malicious content" ) . unwrap ( ) ;
159+ writer
160+ . start_file ( "foo/../../../etc/shadow" , options)
161+ . unwrap ( ) ;
162+ writer. write_all ( b"more malicious content" ) . unwrap ( ) ;
163+
164+ let zip_data = writer. finish ( ) . unwrap ( ) . into_inner ( ) ;
165+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
166+
167+ // These should still fail due to directory traversal protection
168+ for i in 0 ..archive. len ( ) {
169+ let file = archive. by_index ( i) . unwrap ( ) ;
170+ let enclosed_name = file. enclosed_name ( ) ;
171+
172+ // Directory traversal attempts should still return None
173+ assert ! (
174+ enclosed_name. is_none( ) ,
175+ "Directory traversal should still be blocked for: {}" ,
176+ file. name( )
177+ ) ;
178+ }
179+ }
180+ }
0 commit comments