Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/e2e/tests/fixture/nested/icp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
networks:
- name: local
mode: managed
gateway:
port: 0

canisters:
- name: frontend
build:
steps:
- type: pre-built
path: wasms/canister.wasm

sync:
steps:
- type: plugin
path: wasms/plugin.wasm
# A multi-component (nested) directory. The host preopens it under a
# multi-segment WASI guest name, which broke `canonicalize`/`realpath`
# in the plugin's scan step.
dirs:
- src/frontend/dist
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
body { color: rebeccapurple; }
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<html><body><h1>nested dir</h1></body></html>
24 changes: 24 additions & 0 deletions crates/e2e/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ fn basic_deploy() {
);
}

/// Deploy a fixture whose `dirs` entry is a *nested* path (`src/frontend/dist`).
/// The host preopens it under a multi-segment WASI guest name; the plugin's scan
/// step must not call `canonicalize`/`realpath` on it (WASI returns ENOENT for
/// any path under a multi-component preopen, even though plain access works).
#[test]
fn nested_dir_deploy() {
let tmp = setup_project("tests/fixture/nested");
let project = tmp.path();
let _network = LocalNetwork::start(project);

icp_cmd(project).arg("deploy").assert().success();

let assets = list_assets(project);

assert!(
assets.iter().any(|a| a.key == "/index.html"),
"expected /index.html in canister asset list; got: {assets:#?}",
);
assert!(
assets.iter().any(|a| a.key == "/assets/style.css"),
"expected /assets/style.css (nested subdir) in canister asset list; got: {assets:#?}",
);
}

#[test]
fn basic_deploy_with_proxy() {
let tmp = setup_project("tests/fixture/basic");
Expand Down
28 changes: 24 additions & 4 deletions crates/sync-core/src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,33 @@ pub struct AssetSource {
pub key: String,
}

/// Builds an absolute root for `dir` (a manifest-relative directory the host
/// preopened) by prepending `/` and dropping `.` / redundant components.
///
/// We deliberately avoid [`Path::canonicalize`] here. Under WASI it calls
/// `realpath`, which returns `ENOENT` ("No such file or directory") for *any*
/// path beneath a preopen whose guest name has more than one component (e.g.
/// `src/frontend/dist`) — even though ordinary access (`read_dir`, `metadata`,
/// `read`) through that preopen works fine. Single-component dirs like `dist`
/// happen to canonicalize to `/dist`; this helper produces the same shape
/// (`/src/frontend/dist`) for nested dirs without touching `realpath`.
///
/// The host guarantees `dir` is relative and free of `..` components, so keeping
/// only `Normal` components cannot escape the preopen.
fn absolute_root(dir: &str) -> PathBuf {
let mut root = PathBuf::from("/");
for component in Path::new(dir).components() {
if let std::path::Component::Normal(c) = component {
root.push(c);
}
}
root
}

/// Scans `dir` for asset files.
pub fn scan(dir: &str) -> Result<Vec<AssetSource>, String> {
let mut out = Vec::new();
let root = Path::new(dir);
let root_abs = root
.canonicalize()
.map_err(|e| format!("canonicalize {}: {e}", root.display()))?;
let root_abs = absolute_root(dir);
walk(&root_abs, &root_abs, &mut out)?;
Ok(out)
}
Expand Down