@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/THIRD_PARTY_NOTICES.md +302 -0
  4. package/crates/runtime-core/Cargo.lock +5099 -0
  5. package/crates/runtime-core/Cargo.toml +66 -0
  6. package/crates/runtime-core/README.md +47 -0
  7. package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
  8. package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
  9. package/crates/runtime-core/src/capabilities.rs +118 -0
  10. package/crates/runtime-core/src/compiler.rs +658 -0
  11. package/crates/runtime-core/src/contract.rs +5 -0
  12. package/crates/runtime-core/src/deterministic.rs +1051 -0
  13. package/crates/runtime-core/src/error.rs +58 -0
  14. package/crates/runtime-core/src/filesystem.rs +547 -0
  15. package/crates/runtime-core/src/filesystem_quota.rs +167 -0
  16. package/crates/runtime-core/src/go_compiler_session.rs +297 -0
  17. package/crates/runtime-core/src/interactive.rs +1019 -0
  18. package/crates/runtime-core/src/judge_package.rs +1539 -0
  19. package/crates/runtime-core/src/lib.rs +98 -0
  20. package/crates/runtime-core/src/memory.rs +84 -0
  21. package/crates/runtime-core/src/meter.rs +549 -0
  22. package/crates/runtime-core/src/module_imports.rs +149 -0
  23. package/crates/runtime-core/src/module_policy.rs +714 -0
  24. package/crates/runtime-core/src/output.rs +204 -0
  25. package/crates/runtime-core/src/run/mod.rs +208 -0
  26. package/crates/runtime-core/src/run/native.rs +260 -0
  27. package/crates/runtime-core/src/run/web.rs +229 -0
  28. package/crates/runtime-core/src/run/web_runtime.rs +109 -0
  29. package/crates/runtime-core/src/types.rs +268 -0
  30. package/crates/runtime-core/src/web.rs +83 -0
  31. package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
  32. package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
  33. package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
  34. package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
  35. package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
  36. package/dist/go-stage.mjs +193 -0
  37. package/dist/index.d.ts +221 -0
  38. package/dist/index.js +4393 -0
  39. package/dist/java-stage.mjs +111 -0
  40. package/dist/python-stage.mjs +90 -0
  41. package/dist/rustc-stage.mjs +301 -0
  42. package/dist/server-build-stage.mjs +2564 -0
  43. package/dist/server-runner-stage.mjs +155 -0
  44. package/licenses/fflate-MIT.txt +21 -0
  45. package/licenses/runtime-core-dependencies.html +6253 -0
  46. package/licenses/runtime-core-dependencies.json +3041 -0
  47. package/licenses/wasmer-sdk-MIT.txt +21 -0
  48. package/licenses/wasmer-sdk-dependencies.html +6901 -0
  49. package/licenses/wasmer-sdk-dependencies.json +3013 -0
  50. package/package.json +70 -0
  51. package/rust-toolchain.toml +5 -0
  52. package/testdata/wojjdg02-v2-text.hex +1 -0
  53. package/vendor/shared-buffer/Cargo.toml +22 -0
  54. package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
  55. package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
  56. package/vendor/shared-buffer/README.md +34 -0
  57. package/vendor/shared-buffer/src/lib.rs +58 -0
  58. package/vendor/shared-buffer/src/mmap.rs +250 -0
  59. package/vendor/shared-buffer/src/owned.rs +389 -0
  60. package/vendor/virtual-fs/Cargo.toml +181 -0
  61. package/vendor/virtual-fs/LICENSE +25 -0
  62. package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
  63. package/vendor/virtual-fs/src/arc_file.rs +182 -0
  64. package/vendor/virtual-fs/src/arc_fs.rs +68 -0
  65. package/vendor/virtual-fs/src/buffer_file.rs +103 -0
  66. package/vendor/virtual-fs/src/builder.rs +232 -0
  67. package/vendor/virtual-fs/src/combine_file.rs +101 -0
  68. package/vendor/virtual-fs/src/cow_file.rs +345 -0
  69. package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
  70. package/vendor/virtual-fs/src/empty_fs.rs +81 -0
  71. package/vendor/virtual-fs/src/filesystems.rs +108 -0
  72. package/vendor/virtual-fs/src/host_fs.rs +1390 -0
  73. package/vendor/virtual-fs/src/lib.rs +782 -0
  74. package/vendor/virtual-fs/src/limiter.rs +252 -0
  75. package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
  76. package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
  77. package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
  78. package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
  79. package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
  80. package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
  81. package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
  82. package/vendor/virtual-fs/src/null_file.rs +87 -0
  83. package/vendor/virtual-fs/src/ops.rs +364 -0
  84. package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
  85. package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
  86. package/vendor/virtual-fs/src/pipe.rs +603 -0
  87. package/vendor/virtual-fs/src/random_file.rs +88 -0
  88. package/vendor/virtual-fs/src/special_file.rs +108 -0
  89. package/vendor/virtual-fs/src/static_file.rs +133 -0
  90. package/vendor/virtual-fs/src/static_fs.rs +460 -0
  91. package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
  92. package/vendor/virtual-fs/src/trace_fs.rs +258 -0
  93. package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
  94. 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
+ }