@@ -4,6 +4,7 @@ use crate::{
4
4
source,
5
5
} ;
6
6
use anyhow:: Result ;
7
+ use flate2:: read:: GzDecoder ;
7
8
use forc_tracing:: println_action_green;
8
9
use futures:: TryStreamExt ;
9
10
use ipfs_api:: IpfsApi ;
@@ -130,6 +131,18 @@ impl fmt::Display for Pinned {
130
131
}
131
132
132
133
impl Cid {
134
+ fn extract_archive < R : std:: io:: Read > ( & self , reader : R , dst : & Path ) -> Result < ( ) > {
135
+ let dst_dir = dst. join ( self . 0 . to_string ( ) ) ;
136
+ std:: fs:: create_dir_all ( & dst_dir) ?;
137
+ let mut archive = Archive :: new ( reader) ;
138
+
139
+ for entry in archive. entries ( ) ? {
140
+ let mut entry = entry?;
141
+ entry. unpack_in ( & dst_dir) ?;
142
+ }
143
+
144
+ Ok ( ( ) )
145
+ }
133
146
/// Using local node, fetches the content described by this cid.
134
147
async fn fetch_with_client ( & self , ipfs_client : & IpfsClient , dst : & Path ) -> Result < ( ) > {
135
148
let cid_path = format ! ( "/ipfs/{}" , self . 0 ) ;
@@ -140,8 +153,7 @@ impl Cid {
140
153
. try_concat ( )
141
154
. await ?;
142
155
// After collecting bytes of the archive, we unpack it to the dst.
143
- let mut archive = Archive :: new ( bytes. as_slice ( ) ) ;
144
- archive. unpack ( dst) ?;
156
+ self . extract_archive ( bytes. as_slice ( ) , dst) ?;
145
157
Ok ( ( ) )
146
158
}
147
159
@@ -150,19 +162,18 @@ impl Cid {
150
162
let client = reqwest:: Client :: new ( ) ;
151
163
// We request the content to be served to us in tar format by the public gateway.
152
164
let fetch_url = format ! (
153
- "{}/ipfs/{}?download=true&format=tar& filename={}.tar" ,
165
+ "{}/ipfs/{}?download=true&filename={}.tar.gz " ,
154
166
gateway_url, self . 0 , self . 0
155
167
) ;
156
168
let req = client. get ( & fetch_url) ;
157
169
let res = req. send ( ) . await ?;
158
170
if !res. status ( ) . is_success ( ) {
159
171
anyhow:: bail!( "Failed to fetch from {fetch_url:?}" ) ;
160
172
}
161
- let bytes: Vec < _ > = res. text ( ) . await ?. bytes ( ) . collect ( ) ;
162
-
163
- // After collecting bytes of the archive, we unpack it to the dst.
164
- let mut archive = Archive :: new ( bytes. as_slice ( ) ) ;
165
- archive. unpack ( dst) ?;
173
+ let bytes: Vec < _ > = res. bytes ( ) . await ?. into_iter ( ) . collect ( ) ;
174
+ let tar = GzDecoder :: new ( bytes. as_slice ( ) ) ;
175
+ // After collecting and decoding bytes of the archive, we unpack it to the dst.
176
+ self . extract_archive ( tar, dst) ?;
166
177
Ok ( ( ) )
167
178
}
168
179
}
@@ -225,18 +236,154 @@ fn pkg_cache_dir(cid: &Cid) -> PathBuf {
225
236
fn ipfs_client ( ) -> IpfsClient {
226
237
IpfsClient :: default ( )
227
238
}
239
+ #[ cfg( test) ]
240
+ mod tests {
241
+ use super :: * ;
242
+ use anyhow:: Result ;
243
+ use std:: io:: Cursor ;
244
+ use tar:: Header ;
245
+ use tempfile:: TempDir ;
246
+
247
+ fn create_header ( path : & str , size : u64 ) -> Header {
248
+ let mut header = Header :: new_gnu ( ) ;
249
+ header. set_path ( path) . unwrap ( ) ;
250
+ header. set_size ( size) ;
251
+ header. set_mode ( 0o755 ) ;
252
+ header. set_cksum ( ) ;
253
+ header
254
+ }
255
+
256
+ fn create_test_tar ( files : & [ ( & str , & str ) ] ) -> Vec < u8 > {
257
+ let mut ar = tar:: Builder :: new ( Vec :: new ( ) ) ;
258
+
259
+ // Add root project directory
260
+ let header = create_header ( "test-project/" , 0 ) ;
261
+ ar. append ( & header, & mut std:: io:: empty ( ) ) . unwrap ( ) ;
262
+
263
+ // Add files
264
+ for ( path, content) in files {
265
+ let full_path = format ! ( "test-project/{}" , path) ;
266
+ let header = create_header ( & full_path, content. len ( ) as u64 ) ;
267
+ ar. append ( & header, content. as_bytes ( ) ) . unwrap ( ) ;
268
+ }
269
+
270
+ ar. into_inner ( ) . unwrap ( )
271
+ }
272
+
273
+ fn create_test_cid ( ) -> Cid {
274
+ let cid = cid:: Cid :: from_str ( "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" ) . unwrap ( ) ;
275
+ Cid ( cid)
276
+ }
277
+
278
+ #[ test]
279
+ fn test_basic_extraction ( ) -> Result < ( ) > {
280
+ let temp_dir = TempDir :: new ( ) ?;
281
+ let cid = create_test_cid ( ) ;
228
282
229
- #[ test]
230
- fn test_source_ipfs_pinned_parsing ( ) {
231
- let string = "ipfs+QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" ;
283
+ let tar_content = create_test_tar ( & [ ( "test.txt" , "hello world" ) ] ) ;
284
+
285
+ cid. extract_archive ( Cursor :: new ( tar_content) , temp_dir. path ( ) ) ?;
286
+
287
+ let extracted_path = temp_dir
288
+ . path ( )
289
+ . join ( cid. 0 . to_string ( ) )
290
+ . join ( "test-project" )
291
+ . join ( "test.txt" ) ;
292
+
293
+ assert ! ( extracted_path. exists( ) ) ;
294
+ assert_eq ! ( std:: fs:: read_to_string( extracted_path) ?, "hello world" ) ;
295
+
296
+ Ok ( ( ) )
297
+ }
232
298
233
- let expected = Pinned ( Cid ( cid :: Cid :: from_str (
234
- "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" ,
235
- )
236
- . unwrap ( ) ) ) ;
299
+ # [ test ]
300
+ fn test_nested_files ( ) -> Result < ( ) > {
301
+ let temp_dir = TempDir :: new ( ) ? ;
302
+ let cid = create_test_cid ( ) ;
237
303
238
- let parsed = Pinned :: from_str ( string) . unwrap ( ) ;
239
- assert_eq ! ( parsed, expected) ;
240
- let serialized = expected. to_string ( ) ;
241
- assert_eq ! ( & serialized, string) ;
304
+ let tar_content =
305
+ create_test_tar ( & [ ( "src/main.sw" , "contract {};" ) , ( "README.md" , "# Test" ) ] ) ;
306
+
307
+ cid. extract_archive ( Cursor :: new ( tar_content) , temp_dir. path ( ) ) ?;
308
+
309
+ let base = temp_dir. path ( ) . join ( cid. 0 . to_string ( ) ) . join ( "test-project" ) ;
310
+ assert_eq ! (
311
+ std:: fs:: read_to_string( base. join( "src/main.sw" ) ) ?,
312
+ "contract {};"
313
+ ) ;
314
+ assert_eq ! ( std:: fs:: read_to_string( base. join( "README.md" ) ) ?, "# Test" ) ;
315
+
316
+ Ok ( ( ) )
317
+ }
318
+
319
+ #[ test]
320
+ fn test_invalid_tar ( ) {
321
+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
322
+ let cid = create_test_cid ( ) ;
323
+
324
+ let result = cid. extract_archive ( Cursor :: new ( b"not a tar file" ) , temp_dir. path ( ) ) ;
325
+
326
+ assert ! ( result. is_err( ) ) ;
327
+ }
328
+
329
+ #[ test]
330
+ fn test_source_ipfs_pinned_parsing ( ) {
331
+ let string = "ipfs+QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" ;
332
+ let expected = Pinned ( Cid ( cid:: Cid :: from_str (
333
+ "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" ,
334
+ )
335
+ . unwrap ( ) ) ) ;
336
+ let parsed = Pinned :: from_str ( string) . unwrap ( ) ;
337
+ assert_eq ! ( parsed, expected) ;
338
+ let serialized = expected. to_string ( ) ;
339
+ assert_eq ! ( & serialized, string) ;
340
+ }
341
+
342
+ #[ test]
343
+ fn test_path_traversal_prevention ( ) -> Result < ( ) > {
344
+ let temp_dir = TempDir :: new ( ) ?;
345
+ let cid = create_test_cid ( ) ;
346
+
347
+ // Create a known directory structure
348
+ let target_dir = temp_dir. path ( ) . join ( "target" ) ;
349
+ std:: fs:: create_dir ( & target_dir) ?;
350
+
351
+ // Create our canary file in a known location
352
+ let canary_content = "sensitive content" ;
353
+ let canary_path = target_dir. join ( "canary.txt" ) ;
354
+ std:: fs:: write ( & canary_path, canary_content) ?;
355
+
356
+ // Create tar with malicious path targeting our specific canary file
357
+ let mut header = tar:: Header :: new_gnu ( ) ;
358
+ let malicious_path = b"../../target/canary.txt" ;
359
+ header. as_gnu_mut ( ) . unwrap ( ) . name [ ..malicious_path. len ( ) ] . copy_from_slice ( malicious_path) ;
360
+ header. set_size ( 17 ) ;
361
+ header. set_mode ( 0o644 ) ;
362
+ header. set_cksum ( ) ;
363
+
364
+ let mut ar = tar:: Builder :: new ( Vec :: new ( ) ) ;
365
+ ar. append ( & header, b"malicious content" . as_slice ( ) ) ?;
366
+
367
+ // Add safe file
368
+ let mut safe_header = tar:: Header :: new_gnu ( ) ;
369
+ safe_header. set_path ( "safe.txt" ) ?;
370
+ safe_header. set_size ( 12 ) ;
371
+ safe_header. set_mode ( 0o644 ) ;
372
+ safe_header. set_cksum ( ) ;
373
+ ar. append ( & safe_header, b"safe content" . as_slice ( ) ) ?;
374
+
375
+ // Extract to a subdirectory of temp_dir
376
+ let tar_content = ar. into_inner ( ) ?;
377
+ let extract_dir = temp_dir. path ( ) . join ( "extract" ) ;
378
+ std:: fs:: create_dir ( & extract_dir) ?;
379
+ cid. extract_archive ( Cursor :: new ( tar_content) , & extract_dir) ?;
380
+
381
+ // Verify canary file was not modified
382
+ assert_eq ! (
383
+ std:: fs:: read_to_string( & canary_path) ?,
384
+ canary_content,
385
+ "Canary file was modified - path traversal protection failed!"
386
+ ) ;
387
+ Ok ( ( ) )
388
+ }
242
389
}
0 commit comments