@wasm-oj/server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/THIRD_PARTY_NOTICES.md +302 -0
- package/crates/runtime-core/Cargo.lock +5099 -0
- package/crates/runtime-core/Cargo.toml +66 -0
- package/crates/runtime-core/README.md +47 -0
- package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
- package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
- package/crates/runtime-core/src/capabilities.rs +118 -0
- package/crates/runtime-core/src/compiler.rs +658 -0
- package/crates/runtime-core/src/contract.rs +5 -0
- package/crates/runtime-core/src/deterministic.rs +1051 -0
- package/crates/runtime-core/src/error.rs +58 -0
- package/crates/runtime-core/src/filesystem.rs +547 -0
- package/crates/runtime-core/src/filesystem_quota.rs +167 -0
- package/crates/runtime-core/src/go_compiler_session.rs +297 -0
- package/crates/runtime-core/src/interactive.rs +1019 -0
- package/crates/runtime-core/src/judge_package.rs +1539 -0
- package/crates/runtime-core/src/lib.rs +98 -0
- package/crates/runtime-core/src/memory.rs +84 -0
- package/crates/runtime-core/src/meter.rs +549 -0
- package/crates/runtime-core/src/module_imports.rs +149 -0
- package/crates/runtime-core/src/module_policy.rs +714 -0
- package/crates/runtime-core/src/output.rs +204 -0
- package/crates/runtime-core/src/run/mod.rs +208 -0
- package/crates/runtime-core/src/run/native.rs +260 -0
- package/crates/runtime-core/src/run/web.rs +229 -0
- package/crates/runtime-core/src/run/web_runtime.rs +109 -0
- package/crates/runtime-core/src/types.rs +268 -0
- package/crates/runtime-core/src/web.rs +83 -0
- package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
- package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
- package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
- package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
- package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
- package/dist/go-stage.mjs +193 -0
- package/dist/index.d.ts +221 -0
- package/dist/index.js +4393 -0
- package/dist/java-stage.mjs +111 -0
- package/dist/python-stage.mjs +90 -0
- package/dist/rustc-stage.mjs +301 -0
- package/dist/server-build-stage.mjs +2564 -0
- package/dist/server-runner-stage.mjs +155 -0
- package/licenses/fflate-MIT.txt +21 -0
- package/licenses/runtime-core-dependencies.html +6253 -0
- package/licenses/runtime-core-dependencies.json +3041 -0
- package/licenses/wasmer-sdk-MIT.txt +21 -0
- package/licenses/wasmer-sdk-dependencies.html +6901 -0
- package/licenses/wasmer-sdk-dependencies.json +3013 -0
- package/package.json +70 -0
- package/rust-toolchain.toml +5 -0
- package/testdata/wojjdg02-v2-text.hex +1 -0
- package/vendor/shared-buffer/Cargo.toml +22 -0
- package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
- package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
- package/vendor/shared-buffer/README.md +34 -0
- package/vendor/shared-buffer/src/lib.rs +58 -0
- package/vendor/shared-buffer/src/mmap.rs +250 -0
- package/vendor/shared-buffer/src/owned.rs +389 -0
- package/vendor/virtual-fs/Cargo.toml +181 -0
- package/vendor/virtual-fs/LICENSE +25 -0
- package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
- package/vendor/virtual-fs/src/arc_file.rs +182 -0
- package/vendor/virtual-fs/src/arc_fs.rs +68 -0
- package/vendor/virtual-fs/src/buffer_file.rs +103 -0
- package/vendor/virtual-fs/src/builder.rs +232 -0
- package/vendor/virtual-fs/src/combine_file.rs +101 -0
- package/vendor/virtual-fs/src/cow_file.rs +345 -0
- package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
- package/vendor/virtual-fs/src/empty_fs.rs +81 -0
- package/vendor/virtual-fs/src/filesystems.rs +108 -0
- package/vendor/virtual-fs/src/host_fs.rs +1390 -0
- package/vendor/virtual-fs/src/lib.rs +782 -0
- package/vendor/virtual-fs/src/limiter.rs +252 -0
- package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
- package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
- package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
- package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
- package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
- package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
- package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
- package/vendor/virtual-fs/src/null_file.rs +87 -0
- package/vendor/virtual-fs/src/ops.rs +364 -0
- package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
- package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
- package/vendor/virtual-fs/src/pipe.rs +603 -0
- package/vendor/virtual-fs/src/random_file.rs +88 -0
- package/vendor/virtual-fs/src/special_file.rs +108 -0
- package/vendor/virtual-fs/src/static_file.rs +133 -0
- package/vendor/virtual-fs/src/static_fs.rs +460 -0
- package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
- package/vendor/virtual-fs/src/trace_fs.rs +258 -0
- package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
- package/vendor/virtual-fs/src/zero_file.rs +90 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "wasm-oj-runtime-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
rust-version = "1.97.1"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
description = "Portable deterministic, metered WASI/WASIX runner for WASM-OJ."
|
|
8
|
+
publish = false
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
crate-type = ["cdylib", "rlib"]
|
|
12
|
+
|
|
13
|
+
[[bin]]
|
|
14
|
+
name = "wasm-oj-runner"
|
|
15
|
+
path = "src/bin/wasm-oj-runner.rs"
|
|
16
|
+
required-features = ["native"]
|
|
17
|
+
|
|
18
|
+
[[bin]]
|
|
19
|
+
name = "wasm-oj-compiler"
|
|
20
|
+
path = "src/bin/wasm-oj-compiler.rs"
|
|
21
|
+
required-features = ["native"]
|
|
22
|
+
|
|
23
|
+
[features]
|
|
24
|
+
default = ["native"]
|
|
25
|
+
native = ["wasmer/sys-default", "wasmer-wasix/sys-default"]
|
|
26
|
+
web = [
|
|
27
|
+
"wasmer/js-default",
|
|
28
|
+
"wasmer-wasix/js-default",
|
|
29
|
+
"dep:js-sys",
|
|
30
|
+
"dep:console_error_panic_hook",
|
|
31
|
+
"dep:serde-wasm-bindgen",
|
|
32
|
+
"dep:wasm-bindgen",
|
|
33
|
+
"dep:wasm-bindgen-futures",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[dependencies]
|
|
37
|
+
base64 = { version = "=0.23.1", default-features = false, features = ["std"] }
|
|
38
|
+
futures = "=0.3.33"
|
|
39
|
+
radix-wasm-instrument = "1.0.0"
|
|
40
|
+
meter-wasmparser = { package = "wasmparser", version = "0.107.0" }
|
|
41
|
+
serde = { version = "1.0.229", features = ["derive"] }
|
|
42
|
+
serde_bytes = "0.11.19"
|
|
43
|
+
serde_json = "1.0.151"
|
|
44
|
+
sha2 = "0.11.0"
|
|
45
|
+
thiserror = "2.0.20"
|
|
46
|
+
wasm-encoder = { version = "0.245.1", features = ["wasmparser"] }
|
|
47
|
+
wasmparser = "0.245.1"
|
|
48
|
+
wasmer = { version = "=7.2.1", default-features = false }
|
|
49
|
+
wasmer-types = "=7.2.1"
|
|
50
|
+
wasmer-wasix = { version = "=0.702.1", default-features = false }
|
|
51
|
+
virtual-fs = { version = "=0.702.1", default-features = false, features = ["tracking"] }
|
|
52
|
+
webc = "=12.0.0"
|
|
53
|
+
tokio = { version = "1.53.1", default-features = false, features = ["io-util", "rt"] }
|
|
54
|
+
js-sys = { version = "=0.3.104", optional = true }
|
|
55
|
+
console_error_panic_hook = { version = "0.1.7", optional = true }
|
|
56
|
+
serde-wasm-bindgen = { version = "0.6.5", optional = true }
|
|
57
|
+
wasm-bindgen = { version = "=0.2.127", optional = true }
|
|
58
|
+
wasm-bindgen-futures = { version = "=0.4.77", optional = true }
|
|
59
|
+
|
|
60
|
+
[dev-dependencies]
|
|
61
|
+
flate2 = "1"
|
|
62
|
+
wat = "1.255.0"
|
|
63
|
+
|
|
64
|
+
[patch.crates-io]
|
|
65
|
+
shared-buffer = { path = "../../vendor/shared-buffer" }
|
|
66
|
+
virtual-fs = { path = "../../vendor/virtual-fs" }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# wasm-oj-runtime-core
|
|
2
|
+
|
|
3
|
+
Portable deterministic WASI/WASIX execution core for WASM-OJ.
|
|
4
|
+
|
|
5
|
+
The crate exposes `run(RunRequest) -> Result<RunResult, RunError>` and the serialization-friendly `run_response(RunRequest) -> RunResponse`. Both native and `wasm32-unknown-unknown` builds apply the same module transforms, deterministic imports, filesystem construction, metering model, and result contract.
|
|
6
|
+
|
|
7
|
+
```rust
|
|
8
|
+
use wasm_oj_runtime_core::{
|
|
9
|
+
run, DeterminismConfig, ResourcePolicy, RunRequest,
|
|
10
|
+
};
|
|
11
|
+
use std::collections::BTreeMap;
|
|
12
|
+
|
|
13
|
+
let result = run(RunRequest {
|
|
14
|
+
wasm,
|
|
15
|
+
args: vec![],
|
|
16
|
+
env: BTreeMap::new(),
|
|
17
|
+
stdin: b"1 2\n".to_vec(),
|
|
18
|
+
files: BTreeMap::new(),
|
|
19
|
+
output_paths: Vec::new(),
|
|
20
|
+
cwd: None,
|
|
21
|
+
startup_entropy_bytes: 0,
|
|
22
|
+
determinism: DeterminismConfig {
|
|
23
|
+
random_seed: 0x5eed_1234,
|
|
24
|
+
realtime_epoch_ms: 946_684_800_000,
|
|
25
|
+
clock_step_ns: 1_000_000,
|
|
26
|
+
},
|
|
27
|
+
resources: ResourcePolicy {
|
|
28
|
+
instruction_budget: 10_000_000_000,
|
|
29
|
+
logical_time_limit_ms: 60_000,
|
|
30
|
+
memory_limit_bytes: 256 * 1024 * 1024,
|
|
31
|
+
output_limit_bytes: 4 * 1024 * 1024,
|
|
32
|
+
filesystem_write_limit_bytes: 64 * 1024 * 1024,
|
|
33
|
+
filesystem_entry_limit: 4_096,
|
|
34
|
+
},
|
|
35
|
+
})?;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Instruction, logical-time, memory, output, and filesystem limits are enforced inside the core. Sleep and clock polling fast-forward a shared virtual clock and never wait for host time. A hard emergency wall deadline must live outside an in-process Wasmer call; `ServerRunner` therefore invokes the native runner binary as a killable child process, while the browser adapter invokes the WebAssembly build in a replaceable Worker.
|
|
39
|
+
|
|
40
|
+
Build targets:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm run runtime:build-native
|
|
44
|
+
pnpm run runtime:build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The native CLI accepts WASM-OJ contract 2 `wasm-oj-v2/run-request` JSON on stdin or through `--request REQUEST.json`, with binary fields encoded as base64. The browser binding uses the same logical `RunRequest` and `RunResult` fields through its JavaScript interface, with binary values transferred as typed arrays; the transports are intentionally host-specific.
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use std::collections::BTreeMap;
|
|
4
|
+
use std::io::{self, Read};
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
use wasm_oj_runtime_core::{
|
|
7
|
+
CompileRequest, CompileResponse, CompilerToolchain, CompilerToolchainConfig,
|
|
8
|
+
ExecutionTermination, RunFailure, WASM_OJ_COMPILE_BATCH_SCHEMA,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
#[derive(Deserialize)]
|
|
12
|
+
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
13
|
+
struct CliCompileBatch {
|
|
14
|
+
schema: String,
|
|
15
|
+
/// Path to the compiler WebC package on the host filesystem.
|
|
16
|
+
package_path: Option<String>,
|
|
17
|
+
/// Inline base64 alternative for small packages.
|
|
18
|
+
package_base64: Option<String>,
|
|
19
|
+
memory_limit_bytes: u64,
|
|
20
|
+
/// Path to a bounded WOJGO002 immutable shared-file archive.
|
|
21
|
+
shared_files_archive_path: Option<String>,
|
|
22
|
+
shared_files_base64: Option<BTreeMap<String, String>>,
|
|
23
|
+
requests: Vec<CliCompileRequest>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[derive(Deserialize)]
|
|
27
|
+
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
28
|
+
struct CliCompileRequest {
|
|
29
|
+
command: String,
|
|
30
|
+
#[serde(default)]
|
|
31
|
+
args: Vec<String>,
|
|
32
|
+
#[serde(default)]
|
|
33
|
+
env: BTreeMap<String, String>,
|
|
34
|
+
#[serde(default)]
|
|
35
|
+
stdin_base64: String,
|
|
36
|
+
#[serde(default)]
|
|
37
|
+
files_base64: BTreeMap<String, String>,
|
|
38
|
+
cwd: Option<String>,
|
|
39
|
+
#[serde(default)]
|
|
40
|
+
output_paths: Vec<String>,
|
|
41
|
+
output_limit_bytes: u64,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Serialize)]
|
|
45
|
+
#[serde(rename_all = "camelCase")]
|
|
46
|
+
struct CliCompileResult {
|
|
47
|
+
code: i32,
|
|
48
|
+
stdout_base64: String,
|
|
49
|
+
stderr_base64: String,
|
|
50
|
+
output_files_base64: BTreeMap<String, String>,
|
|
51
|
+
termination: ExecutionTermination,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Serialize)]
|
|
55
|
+
#[serde(rename_all = "camelCase")]
|
|
56
|
+
struct CliCompileResponse {
|
|
57
|
+
ok: bool,
|
|
58
|
+
result: Option<CliCompileResult>,
|
|
59
|
+
error: Option<RunFailure>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[derive(Serialize)]
|
|
63
|
+
#[serde(rename_all = "camelCase")]
|
|
64
|
+
struct CliCompileBatchResponse {
|
|
65
|
+
ok: bool,
|
|
66
|
+
responses: Vec<CliCompileResponse>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Deserialize)]
|
|
70
|
+
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
71
|
+
struct GoArchiveEntry {
|
|
72
|
+
import_path: String,
|
|
73
|
+
archive_path: String,
|
|
74
|
+
sha256: String,
|
|
75
|
+
offset: u64,
|
|
76
|
+
length: u64,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const MAX_SHARED_ARCHIVE_BYTES: u64 = 512 * 1024 * 1024;
|
|
80
|
+
const MAX_SHARED_ARCHIVE_INDEX_BYTES: usize = 4 * 1024 * 1024;
|
|
81
|
+
const MAX_SHARED_ARCHIVE_ENTRIES: usize = 4_096;
|
|
82
|
+
|
|
83
|
+
fn main() {
|
|
84
|
+
match execute() {
|
|
85
|
+
Ok(true) => {}
|
|
86
|
+
Ok(false) => std::process::exit(1),
|
|
87
|
+
Err(error) => {
|
|
88
|
+
eprintln!("wasm-oj-compiler: {error}");
|
|
89
|
+
std::process::exit(2);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn execute() -> Result<bool, String> {
|
|
95
|
+
let input = read_input()?;
|
|
96
|
+
let batch: CliCompileBatch =
|
|
97
|
+
serde_json::from_slice(&input).map_err(|error| format!("invalid request JSON: {error}"))?;
|
|
98
|
+
if batch.schema != WASM_OJ_COMPILE_BATCH_SCHEMA {
|
|
99
|
+
return Err(format!(
|
|
100
|
+
"unsupported request schema '{}'; expected '{WASM_OJ_COMPILE_BATCH_SCHEMA}'",
|
|
101
|
+
batch.schema
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
let package = match (&batch.package_path, &batch.package_base64) {
|
|
105
|
+
(Some(path), None) => std::fs::read(path)
|
|
106
|
+
.map_err(|error| format!("failed to read compiler package '{path}': {error}"))?,
|
|
107
|
+
(None, Some(encoded)) => STANDARD
|
|
108
|
+
.decode(encoded)
|
|
109
|
+
.map_err(|error| format!("invalid packageBase64: {error}"))?,
|
|
110
|
+
_ => return Err("exactly one of packagePath or packageBase64 is required".to_string()),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
114
|
+
.enable_all()
|
|
115
|
+
.worker_threads(2)
|
|
116
|
+
.build()
|
|
117
|
+
.map_err(|error| format!("failed to initialize Tokio: {error}"))?;
|
|
118
|
+
let _guard = runtime.enter();
|
|
119
|
+
let toolchain = CompilerToolchain::new(CompilerToolchainConfig {
|
|
120
|
+
package,
|
|
121
|
+
memory_limit_bytes: batch.memory_limit_bytes,
|
|
122
|
+
})
|
|
123
|
+
.map_err(|error| format!("failed to load compiler toolchain: {error}"))?;
|
|
124
|
+
|
|
125
|
+
let requests = batch
|
|
126
|
+
.requests
|
|
127
|
+
.into_iter()
|
|
128
|
+
.map(decode_request)
|
|
129
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
130
|
+
let (ok, responses) =
|
|
131
|
+
if batch.shared_files_archive_path.is_some() || batch.shared_files_base64.is_some() {
|
|
132
|
+
let mut files = match batch.shared_files_archive_path {
|
|
133
|
+
Some(path) => decode_shared_archive(&path)?,
|
|
134
|
+
None => BTreeMap::new(),
|
|
135
|
+
};
|
|
136
|
+
for (path, bytes) in decode_files(batch.shared_files_base64.unwrap_or_default())? {
|
|
137
|
+
if files.insert(path.clone(), bytes).is_some() {
|
|
138
|
+
return Err(format!(
|
|
139
|
+
"shared file '{path}' is declared by both archive and inline input"
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
match runtime.block_on(toolchain.compile_pipeline(files, requests)) {
|
|
144
|
+
Ok(result) => {
|
|
145
|
+
let ok = result.stages.iter().all(|stage| stage.code == 0);
|
|
146
|
+
let responses = result
|
|
147
|
+
.stages
|
|
148
|
+
.into_iter()
|
|
149
|
+
.map(|result| encode_response(RunResponseLike::success(result)))
|
|
150
|
+
.collect();
|
|
151
|
+
(ok, responses)
|
|
152
|
+
}
|
|
153
|
+
Err(error) => (
|
|
154
|
+
false,
|
|
155
|
+
vec![encode_response(RunResponseLike::failure(error))],
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
let mut responses = Vec::with_capacity(requests.len());
|
|
160
|
+
let mut ok = true;
|
|
161
|
+
for request in requests {
|
|
162
|
+
let response = runtime.block_on(toolchain.compile_response(request));
|
|
163
|
+
ok = ok && response.ok;
|
|
164
|
+
responses.push(encode_response(response));
|
|
165
|
+
}
|
|
166
|
+
(ok, responses)
|
|
167
|
+
};
|
|
168
|
+
let batch_response = CliCompileBatchResponse { ok, responses };
|
|
169
|
+
serde_json::to_writer(io::stdout().lock(), &batch_response)
|
|
170
|
+
.map_err(|error| format!("failed to serialize response: {error}"))?;
|
|
171
|
+
Ok(batch_response.ok)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn decode_shared_archive(path: &str) -> Result<BTreeMap<String, serde_bytes::ByteBuf>, String> {
|
|
175
|
+
let mut archive = std::fs::File::open(path)
|
|
176
|
+
.map_err(|error| format!("failed to open shared file archive '{path}': {error}"))?;
|
|
177
|
+
let metadata = archive
|
|
178
|
+
.metadata()
|
|
179
|
+
.map_err(|error| format!("failed to inspect shared file archive '{path}': {error}"))?;
|
|
180
|
+
if !metadata.is_file() || metadata.len() > MAX_SHARED_ARCHIVE_BYTES {
|
|
181
|
+
return Err(format!(
|
|
182
|
+
"shared file archive must be a regular file no larger than {MAX_SHARED_ARCHIVE_BYTES} bytes"
|
|
183
|
+
));
|
|
184
|
+
}
|
|
185
|
+
let mut header = [0_u8; 12];
|
|
186
|
+
archive
|
|
187
|
+
.read_exact(&mut header)
|
|
188
|
+
.map_err(|error| format!("failed to read shared file archive header '{path}': {error}"))?;
|
|
189
|
+
if &header[..8] != b"WOJGO002" {
|
|
190
|
+
return Err("shared file archive has an invalid WOJGO002 header".to_string());
|
|
191
|
+
}
|
|
192
|
+
let index_length = u32::from_le_bytes(
|
|
193
|
+
header[8..12]
|
|
194
|
+
.try_into()
|
|
195
|
+
.map_err(|_| "shared file archive has a truncated index length".to_string())?,
|
|
196
|
+
) as usize;
|
|
197
|
+
if index_length == 0 || index_length > MAX_SHARED_ARCHIVE_INDEX_BYTES {
|
|
198
|
+
return Err("shared file archive index exceeds its size boundary".to_string());
|
|
199
|
+
}
|
|
200
|
+
let data_offset = 12_usize
|
|
201
|
+
.checked_add(index_length)
|
|
202
|
+
.ok_or_else(|| "shared file archive index length overflows".to_string())?;
|
|
203
|
+
if u64::try_from(data_offset).map_err(|_| "shared archive index exceeds host range")?
|
|
204
|
+
> metadata.len()
|
|
205
|
+
{
|
|
206
|
+
return Err("shared file archive index exceeds the file boundary".to_string());
|
|
207
|
+
}
|
|
208
|
+
let mut index = vec![0_u8; index_length];
|
|
209
|
+
archive
|
|
210
|
+
.read_exact(&mut index)
|
|
211
|
+
.map_err(|error| format!("failed to read shared file archive index '{path}': {error}"))?;
|
|
212
|
+
let entries: Vec<GoArchiveEntry> = serde_json::from_slice(&index)
|
|
213
|
+
.map_err(|error| format!("shared file archive has an invalid index: {error}"))?;
|
|
214
|
+
if entries.is_empty() || entries.len() > MAX_SHARED_ARCHIVE_ENTRIES {
|
|
215
|
+
return Err("shared file archive entry count exceeds its boundary".to_string());
|
|
216
|
+
}
|
|
217
|
+
let mut files = BTreeMap::new();
|
|
218
|
+
let mut expected_offset = 0_u64;
|
|
219
|
+
for entry in entries {
|
|
220
|
+
if entry.offset != expected_offset
|
|
221
|
+
|| entry.length <= 8
|
|
222
|
+
|| entry.sha256.len() != 64
|
|
223
|
+
|| !entry
|
|
224
|
+
.sha256
|
|
225
|
+
.bytes()
|
|
226
|
+
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
|
|
227
|
+
|| entry.archive_path != format!("/go/pkg/{}.a", entry.import_path)
|
|
228
|
+
{
|
|
229
|
+
return Err("shared file archive contains a non-canonical entry".to_string());
|
|
230
|
+
}
|
|
231
|
+
let end = u64::try_from(data_offset)
|
|
232
|
+
.map_err(|_| "shared archive index exceeds host range")?
|
|
233
|
+
.checked_add(entry.offset)
|
|
234
|
+
.and_then(|offset| offset.checked_add(entry.length))
|
|
235
|
+
.ok_or_else(|| "shared file archive entry boundary overflows".to_string())?;
|
|
236
|
+
if end > metadata.len() {
|
|
237
|
+
return Err("shared file archive entry exceeds the file boundary".to_string());
|
|
238
|
+
}
|
|
239
|
+
let length =
|
|
240
|
+
usize::try_from(entry.length).map_err(|_| "shared file length exceeds host range")?;
|
|
241
|
+
let mut bytes = vec![0_u8; length];
|
|
242
|
+
archive.read_exact(&mut bytes).map_err(|error| {
|
|
243
|
+
format!(
|
|
244
|
+
"failed to read shared archive entry '{}': {error}",
|
|
245
|
+
entry.archive_path
|
|
246
|
+
)
|
|
247
|
+
})?;
|
|
248
|
+
if files
|
|
249
|
+
.insert(entry.archive_path, serde_bytes::ByteBuf::from(bytes))
|
|
250
|
+
.is_some()
|
|
251
|
+
{
|
|
252
|
+
return Err("shared file archive contains a duplicate guest path".to_string());
|
|
253
|
+
}
|
|
254
|
+
expected_offset = expected_offset
|
|
255
|
+
.checked_add(entry.length)
|
|
256
|
+
.ok_or_else(|| "shared file archive lengths overflow".to_string())?;
|
|
257
|
+
}
|
|
258
|
+
let covered_end = u64::try_from(data_offset)
|
|
259
|
+
.map_err(|_| "shared archive index exceeds host range")?
|
|
260
|
+
.checked_add(expected_offset)
|
|
261
|
+
.ok_or_else(|| "shared archive size overflows".to_string())?;
|
|
262
|
+
if covered_end != metadata.len() {
|
|
263
|
+
return Err("shared file archive contains trailing or missing bytes".to_string());
|
|
264
|
+
}
|
|
265
|
+
Ok(files)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn decode_request(encoded: CliCompileRequest) -> Result<CompileRequest, String> {
|
|
269
|
+
let files = decode_files(encoded.files_base64)?;
|
|
270
|
+
Ok(CompileRequest {
|
|
271
|
+
command: encoded.command,
|
|
272
|
+
args: encoded.args,
|
|
273
|
+
env: encoded.env,
|
|
274
|
+
stdin: STANDARD
|
|
275
|
+
.decode(encoded.stdin_base64)
|
|
276
|
+
.map_err(|error| format!("invalid stdinBase64: {error}"))?,
|
|
277
|
+
files,
|
|
278
|
+
cwd: encoded.cwd,
|
|
279
|
+
output_paths: encoded.output_paths,
|
|
280
|
+
output_limit_bytes: encoded.output_limit_bytes,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fn decode_files(
|
|
285
|
+
encoded: BTreeMap<String, String>,
|
|
286
|
+
) -> Result<BTreeMap<String, serde_bytes::ByteBuf>, String> {
|
|
287
|
+
encoded
|
|
288
|
+
.into_iter()
|
|
289
|
+
.map(|(path, contents)| {
|
|
290
|
+
let bytes = STANDARD
|
|
291
|
+
.decode(contents)
|
|
292
|
+
.map_err(|error| format!("invalid base64 for guest file '{path}': {error}"))?;
|
|
293
|
+
Ok((path, serde_bytes::ByteBuf::from(bytes)))
|
|
294
|
+
})
|
|
295
|
+
.collect()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
struct RunResponseLike;
|
|
299
|
+
|
|
300
|
+
impl RunResponseLike {
|
|
301
|
+
fn success(result: wasm_oj_runtime_core::CompileResult) -> CompileResponse {
|
|
302
|
+
CompileResponse {
|
|
303
|
+
ok: true,
|
|
304
|
+
result: Some(result),
|
|
305
|
+
error: None,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn failure(error: wasm_oj_runtime_core::RunError) -> CompileResponse {
|
|
310
|
+
CompileResponse {
|
|
311
|
+
ok: false,
|
|
312
|
+
result: None,
|
|
313
|
+
error: Some(RunFailure {
|
|
314
|
+
code: error.code(),
|
|
315
|
+
message: error.to_string(),
|
|
316
|
+
}),
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn encode_response(response: CompileResponse) -> CliCompileResponse {
|
|
322
|
+
CliCompileResponse {
|
|
323
|
+
ok: response.ok,
|
|
324
|
+
result: response.result.map(|result| CliCompileResult {
|
|
325
|
+
code: result.code,
|
|
326
|
+
stdout_base64: STANDARD.encode(result.stdout),
|
|
327
|
+
stderr_base64: STANDARD.encode(result.stderr),
|
|
328
|
+
output_files_base64: result
|
|
329
|
+
.output_files
|
|
330
|
+
.into_iter()
|
|
331
|
+
.map(|(path, bytes)| (path, STANDARD.encode(bytes)))
|
|
332
|
+
.collect(),
|
|
333
|
+
termination: result.termination,
|
|
334
|
+
}),
|
|
335
|
+
error: response.error,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fn read_input() -> Result<Vec<u8>, String> {
|
|
340
|
+
let mut arguments = std::env::args_os();
|
|
341
|
+
let _program = arguments.next();
|
|
342
|
+
let first = arguments.next();
|
|
343
|
+
let second = arguments.next();
|
|
344
|
+
let trailing = arguments.next();
|
|
345
|
+
match (first, second, trailing) {
|
|
346
|
+
(None, None, None) => {
|
|
347
|
+
let mut input = Vec::new();
|
|
348
|
+
io::stdin()
|
|
349
|
+
.lock()
|
|
350
|
+
.read_to_end(&mut input)
|
|
351
|
+
.map_err(|error| format!("failed to read stdin: {error}"))?;
|
|
352
|
+
Ok(input)
|
|
353
|
+
}
|
|
354
|
+
(Some(flag), Some(path), None) if flag == "--request" => {
|
|
355
|
+
let path = PathBuf::from(path);
|
|
356
|
+
std::fs::read(&path)
|
|
357
|
+
.map_err(|error| format!("failed to read {}: {error}", path.display()))
|
|
358
|
+
}
|
|
359
|
+
_ => Err("usage: wasm-oj-compiler [--request REQUEST.json]".to_string()),
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[cfg(test)]
|
|
364
|
+
mod tests {
|
|
365
|
+
use super::decode_shared_archive;
|
|
366
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
367
|
+
|
|
368
|
+
static NEXT_ARCHIVE_ID: AtomicU64 = AtomicU64::new(0);
|
|
369
|
+
|
|
370
|
+
fn archive_with_magic(magic: &[u8; 8]) -> (std::path::PathBuf, &'static [u8]) {
|
|
371
|
+
let payload = b"!<arch>\n!";
|
|
372
|
+
let index = serde_json::to_vec(&serde_json::json!([{
|
|
373
|
+
"importPath": "fmt",
|
|
374
|
+
"archivePath": "/go/pkg/fmt.a",
|
|
375
|
+
"sha256": "a".repeat(64),
|
|
376
|
+
"offset": 0,
|
|
377
|
+
"length": payload.len(),
|
|
378
|
+
}]))
|
|
379
|
+
.unwrap();
|
|
380
|
+
let mut archive = Vec::with_capacity(12 + index.len() + payload.len());
|
|
381
|
+
archive.extend_from_slice(magic);
|
|
382
|
+
archive.extend_from_slice(&(u32::try_from(index.len()).unwrap()).to_le_bytes());
|
|
383
|
+
archive.extend_from_slice(&index);
|
|
384
|
+
archive.extend_from_slice(payload);
|
|
385
|
+
let nonce = std::time::SystemTime::now()
|
|
386
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
387
|
+
.unwrap()
|
|
388
|
+
.as_nanos();
|
|
389
|
+
let path = std::env::temp_dir().join(format!(
|
|
390
|
+
"wasm-oj-go-archive-test-{}-{nonce}-{}.bin",
|
|
391
|
+
std::process::id(),
|
|
392
|
+
NEXT_ARCHIVE_ID.fetch_add(1, Ordering::Relaxed),
|
|
393
|
+
));
|
|
394
|
+
std::fs::write(&path, archive).unwrap();
|
|
395
|
+
(path, payload)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#[test]
|
|
399
|
+
fn decodes_bounded_go_archive_without_base64_transport() {
|
|
400
|
+
let (path, payload) = archive_with_magic(b"WOJGO002");
|
|
401
|
+
|
|
402
|
+
let files = decode_shared_archive(path.to_str().unwrap()).unwrap();
|
|
403
|
+
std::fs::remove_file(path).unwrap();
|
|
404
|
+
assert_eq!(files["/go/pkg/fmt.a"].as_ref(), payload);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#[test]
|
|
408
|
+
fn rejects_the_retired_go_archive_magic() {
|
|
409
|
+
let retired_magic: [u8; 8] = [b"FORG".as_slice(), b"EGO1".as_slice()]
|
|
410
|
+
.concat()
|
|
411
|
+
.try_into()
|
|
412
|
+
.unwrap();
|
|
413
|
+
let (path, _) = archive_with_magic(&retired_magic);
|
|
414
|
+
let error = decode_shared_archive(path.to_str().unwrap()).unwrap_err();
|
|
415
|
+
std::fs::remove_file(path).unwrap();
|
|
416
|
+
assert!(error.contains("invalid WOJGO002 header"), "{error}");
|
|
417
|
+
}
|
|
418
|
+
}
|