@@ -269,7 +269,8 @@ impl LocalLoader {
269
269
} => {
270
270
let src = Path :: new ( source) ;
271
271
let dest = dest_root. join ( destination. trim_start_matches ( '/' ) ) ;
272
- self . copy_file_or_directory ( src, & dest, exclude_files) . await
272
+ self . copy_file_or_directory ( src, & dest, destination, exclude_files)
273
+ . await
273
274
}
274
275
}
275
276
}
@@ -291,7 +292,7 @@ impl LocalLoader {
291
292
. await ?;
292
293
} else {
293
294
// "single/file.txt"
294
- self . copy_single_file ( & path, & dest) . await ?;
295
+ self . copy_single_file ( & path, & dest, glob_or_path ) . await ?;
295
296
}
296
297
} else if looks_like_glob_pattern ( glob_or_path) {
297
298
// "glob/pattern/*"
@@ -308,6 +309,7 @@ impl LocalLoader {
308
309
& self ,
309
310
src : & Path ,
310
311
dest : & Path ,
312
+ guest_dest : & str ,
311
313
exclude_files : & [ String ] ,
312
314
) -> Result < ( ) > {
313
315
let src_path = self . app_root . join ( src) ;
@@ -321,7 +323,7 @@ impl LocalLoader {
321
323
. await ?;
322
324
} else {
323
325
// { source = "host/file.txt", destination = "guest/file.txt" }
324
- self . copy_single_file ( & src_path, dest) . await ?;
326
+ self . copy_single_file ( & src_path, dest, guest_dest ) . await ?;
325
327
}
326
328
Ok ( ( ) )
327
329
}
@@ -368,13 +370,14 @@ impl LocalLoader {
368
370
369
371
let relative_path = src. strip_prefix ( src_prefix) ?;
370
372
let dest = dest_root. join ( relative_path) ;
371
- self . copy_single_file ( & src, & dest) . await ?;
373
+ self . copy_single_file ( & src, & dest, & relative_path. to_string_lossy ( ) )
374
+ . await ?;
372
375
}
373
376
Ok ( ( ) )
374
377
}
375
378
376
379
// Copy a single file from `src` to `dest`, creating parent directories.
377
- async fn copy_single_file ( & self , src : & Path , dest : & Path ) -> Result < ( ) > {
380
+ async fn copy_single_file ( & self , src : & Path , dest : & Path , guest_dest : & str ) -> Result < ( ) > {
378
381
// Sanity checks: src is in app_root...
379
382
src. strip_prefix ( & self . app_root ) ?;
380
383
// ...and dest is in the Copy root.
@@ -394,17 +397,43 @@ impl LocalLoader {
394
397
quoted_path( & dest_parent)
395
398
)
396
399
} ) ?;
397
- crate :: fs:: copy ( src, dest) . await . with_context ( || {
398
- format ! (
399
- "Failed to copy {} to {}" ,
400
- quoted_path( src) ,
401
- quoted_path( dest)
402
- )
403
- } ) ?;
400
+ crate :: fs:: copy ( src, dest)
401
+ . await
402
+ . or_else ( |e| Self :: failed_to_copy_single_file_error ( src, dest, guest_dest, e) ) ?;
404
403
tracing:: debug!( "Copied {src:?} to {dest:?}" ) ;
405
404
Ok ( ( ) )
406
405
}
407
406
407
+ fn failed_to_copy_single_file_error < T > (
408
+ src : & Path ,
409
+ dest : & Path ,
410
+ guest_dest : & str ,
411
+ e : anyhow:: Error ,
412
+ ) -> anyhow:: Result < T > {
413
+ let src_text = quoted_path ( src) ;
414
+ let dest_text = quoted_path ( dest) ;
415
+ let base_msg = format ! ( "Failed to copy {src_text} to working path {dest_text}" ) ;
416
+
417
+ if let Some ( io_error) = e. downcast_ref :: < std:: io:: Error > ( ) {
418
+ if Self :: is_directory_like ( guest_dest)
419
+ || io_error. kind ( ) == std:: io:: ErrorKind :: NotFound
420
+ {
421
+ return Err ( anyhow:: anyhow!(
422
+ r#""{guest_dest}" is not a valid destination file name"#
423
+ ) )
424
+ . context ( base_msg) ;
425
+ }
426
+ }
427
+
428
+ Err ( e) . with_context ( || format ! ( "{base_msg} (for destination path \" {guest_dest}\" )" ) )
429
+ }
430
+
431
+ /// Does a guest path appear to be a directory name, e.g. "/" or ".."? This is for guest
432
+ /// paths *only* and does not consider Windows separators.
433
+ fn is_directory_like ( guest_path : & str ) -> bool {
434
+ guest_path. ends_with ( '/' ) || guest_path. ends_with ( '.' ) || guest_path. ends_with ( ".." )
435
+ }
436
+
408
437
// Resolve the given direct mount directory, checking that it is valid for
409
438
// direct mounting and returning its canonicalized source path.
410
439
async fn resolve_direct_mount ( & self , mount : & WasiFilesMount ) -> Result < ContentPath > {
@@ -584,3 +613,33 @@ fn warn_if_component_load_slothful() -> sloth::SlothGuard {
584
613
let message = "Loading Wasm components is taking a few seconds..." ;
585
614
sloth:: warn_if_slothful ( SLOTH_WARNING_DELAY_MILLIS , format ! ( "{message}\n " ) )
586
615
}
616
+
617
+ #[ cfg( test) ]
618
+ mod test {
619
+ use super :: * ;
620
+
621
+ #[ tokio:: test]
622
+ async fn bad_destination_filename_is_explained ( ) -> anyhow:: Result < ( ) > {
623
+ let app_root = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) )
624
+ . join ( "tests" )
625
+ . join ( "file-errors" ) ;
626
+ let wd = tempfile:: tempdir ( ) ?;
627
+ let loader = LocalLoader :: new (
628
+ & app_root,
629
+ FilesMountStrategy :: Copy ( wd. path ( ) . to_owned ( ) ) ,
630
+ None ,
631
+ )
632
+ . await ?;
633
+ let err = loader
634
+ . load_file ( app_root. join ( "bad.toml" ) )
635
+ . await
636
+ . expect_err ( "loader should not have succeeded" ) ;
637
+ let err_ctx = format ! ( "{err:#}" ) ;
638
+ assert ! (
639
+ err_ctx. contains( r#""/" is not a valid destination file name"# ) ,
640
+ "expected error to show destination file name but got {}" ,
641
+ err_ctx
642
+ ) ;
643
+ Ok ( ( ) )
644
+ }
645
+ }
0 commit comments