Skip to content

Commit 63972c2

Browse files
authored
Merge pull request #2460 from itowlson/better-error-on-bad-dest-file-name
Check for illegal file name when copying single file
2 parents d6b9713 + 60fc2ba commit 63972c2

File tree

4 files changed

+85
-12
lines changed

4 files changed

+85
-12
lines changed

crates/loader/src/local.rs

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ impl LocalLoader {
269269
} => {
270270
let src = Path::new(source);
271271
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
273274
}
274275
}
275276
}
@@ -291,7 +292,7 @@ impl LocalLoader {
291292
.await?;
292293
} else {
293294
// "single/file.txt"
294-
self.copy_single_file(&path, &dest).await?;
295+
self.copy_single_file(&path, &dest, glob_or_path).await?;
295296
}
296297
} else if looks_like_glob_pattern(glob_or_path) {
297298
// "glob/pattern/*"
@@ -308,6 +309,7 @@ impl LocalLoader {
308309
&self,
309310
src: &Path,
310311
dest: &Path,
312+
guest_dest: &str,
311313
exclude_files: &[String],
312314
) -> Result<()> {
313315
let src_path = self.app_root.join(src);
@@ -321,7 +323,7 @@ impl LocalLoader {
321323
.await?;
322324
} else {
323325
// { 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?;
325327
}
326328
Ok(())
327329
}
@@ -368,13 +370,14 @@ impl LocalLoader {
368370

369371
let relative_path = src.strip_prefix(src_prefix)?;
370372
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?;
372375
}
373376
Ok(())
374377
}
375378

376379
// 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<()> {
378381
// Sanity checks: src is in app_root...
379382
src.strip_prefix(&self.app_root)?;
380383
// ...and dest is in the Copy root.
@@ -394,17 +397,43 @@ impl LocalLoader {
394397
quoted_path(&dest_parent)
395398
)
396399
})?;
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))?;
404403
tracing::debug!("Copied {src:?} to {dest:?}");
405404
Ok(())
406405
}
407406

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+
408437
// Resolve the given direct mount directory, checking that it is valid for
409438
// direct mounting and returning its canonicalized source path.
410439
async fn resolve_direct_mount(&self, mount: &WasiFilesMount) -> Result<ContentPath> {
@@ -584,3 +613,33 @@ fn warn_if_component_load_slothful() -> sloth::SlothGuard {
584613
let message = "Loading Wasm components is taking a few seconds...";
585614
sloth::warn_if_slothful(SLOTH_WARNING_DELAY_MILLIS, format!("{message}\n"))
586615
}
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+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
spin_manifest_version = 2
2+
3+
[application]
4+
name = "file-errors"
5+
6+
[[trigger.http]]
7+
route = "/..."
8+
component = "bad"
9+
10+
[component.bad]
11+
source = "dummy.wasm.txt"
12+
files = [{ source = "host.txt", destination = "/" }]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This file needs to exist for manifests to validate, but is never used.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Super excited for being copied from the host to the working directory!

0 commit comments

Comments
 (0)