@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,58 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use thiserror::Error;
|
|
3
|
+
|
|
4
|
+
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
5
|
+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
6
|
+
pub enum RunErrorCode {
|
|
7
|
+
InvalidRequest,
|
|
8
|
+
CompileError,
|
|
9
|
+
InstructionLimitExceeded,
|
|
10
|
+
MemoryLimitExceeded,
|
|
11
|
+
OutputLimitExceeded,
|
|
12
|
+
Trap,
|
|
13
|
+
WasiExit,
|
|
14
|
+
WasiUnsupported,
|
|
15
|
+
IoError,
|
|
16
|
+
RuntimeError,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Error)]
|
|
20
|
+
pub enum RunError {
|
|
21
|
+
#[error("invalid run request: {0}")]
|
|
22
|
+
InvalidRequest(String),
|
|
23
|
+
#[error("module compilation failed: {0}")]
|
|
24
|
+
Compile(String),
|
|
25
|
+
#[error("instruction budget {0} exceeded")]
|
|
26
|
+
InstructionLimit(u64),
|
|
27
|
+
#[error("memory limit {0} bytes exceeded")]
|
|
28
|
+
MemoryLimit(u64),
|
|
29
|
+
#[error("output limit {0} bytes exceeded")]
|
|
30
|
+
OutputLimit(u64),
|
|
31
|
+
#[error("guest trapped: {0}")]
|
|
32
|
+
Trap(String),
|
|
33
|
+
#[error("guest exited with WASI errno {0}")]
|
|
34
|
+
WasiExit(String),
|
|
35
|
+
#[error("unsupported WASI behavior: {0}")]
|
|
36
|
+
WasiUnsupported(String),
|
|
37
|
+
#[error("I/O failed: {0}")]
|
|
38
|
+
Io(String),
|
|
39
|
+
#[error("runtime failed: {0}")]
|
|
40
|
+
Runtime(String),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl RunError {
|
|
44
|
+
pub const fn code(&self) -> RunErrorCode {
|
|
45
|
+
match self {
|
|
46
|
+
Self::InvalidRequest(_) => RunErrorCode::InvalidRequest,
|
|
47
|
+
Self::Compile(_) => RunErrorCode::CompileError,
|
|
48
|
+
Self::InstructionLimit(_) => RunErrorCode::InstructionLimitExceeded,
|
|
49
|
+
Self::MemoryLimit(_) => RunErrorCode::MemoryLimitExceeded,
|
|
50
|
+
Self::OutputLimit(_) => RunErrorCode::OutputLimitExceeded,
|
|
51
|
+
Self::Trap(_) => RunErrorCode::Trap,
|
|
52
|
+
Self::WasiExit(_) => RunErrorCode::WasiExit,
|
|
53
|
+
Self::WasiUnsupported(_) => RunErrorCode::WasiUnsupported,
|
|
54
|
+
Self::Io(_) => RunErrorCode::IoError,
|
|
55
|
+
Self::Runtime(_) => RunErrorCode::RuntimeError,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
use crate::deterministic::COMPILER_DETERMINISM;
|
|
2
|
+
use crate::filesystem_quota::{VfsMetrics, VfsQuota};
|
|
3
|
+
use crate::{DeterminismConfig, ResourcePolicy, RunError};
|
|
4
|
+
use std::path::Path;
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
use virtual_fs::{
|
|
7
|
+
AsyncReadExt, AsyncWriteExt, FileSystem, FsError, OverlayFileSystem, TmpFileSystem,
|
|
8
|
+
create_dir_all,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const NANOSECONDS_PER_MILLISECOND: u64 = 1_000_000;
|
|
12
|
+
const COMPILER_FILESYSTEM_WRITE_LIMIT_BYTES: usize = 512 * 1024 * 1024;
|
|
13
|
+
const COMPILER_FILESYSTEM_ENTRY_LIMIT: usize = 65_536;
|
|
14
|
+
|
|
15
|
+
pub fn runtime_project_files(
|
|
16
|
+
files: &std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
17
|
+
output_paths: &[String],
|
|
18
|
+
determinism: &DeterminismConfig,
|
|
19
|
+
resources: &ResourcePolicy,
|
|
20
|
+
) -> Result<RuntimeProjectFilesystem, RunError> {
|
|
21
|
+
quota_project_files_at_timestamp(
|
|
22
|
+
files,
|
|
23
|
+
output_paths,
|
|
24
|
+
determinism
|
|
25
|
+
.realtime_epoch_ms
|
|
26
|
+
.saturating_mul(NANOSECONDS_PER_MILLISECOND),
|
|
27
|
+
usize::try_from(resources.filesystem_write_limit_bytes).map_err(|_| {
|
|
28
|
+
RunError::InvalidRequest("filesystem write limit exceeds host range".to_string())
|
|
29
|
+
})?,
|
|
30
|
+
usize::try_from(resources.filesystem_entry_limit).map_err(|_| {
|
|
31
|
+
RunError::InvalidRequest("filesystem entry limit exceeds host range".to_string())
|
|
32
|
+
})?,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[derive(Debug)]
|
|
37
|
+
pub struct RuntimeProjectFilesystem {
|
|
38
|
+
filesystem: Arc<dyn FileSystem + Send + Sync>,
|
|
39
|
+
quota: Arc<VfsQuota>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl RuntimeProjectFilesystem {
|
|
43
|
+
pub fn filesystem(&self) -> Arc<dyn FileSystem + Send + Sync> {
|
|
44
|
+
self.filesystem.clone()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn quota_exceeded(&self) -> bool {
|
|
48
|
+
self.quota.exceeded()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn metrics(&self) -> VfsMetrics {
|
|
52
|
+
self.quota.metrics()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Mount compiler inputs at WASM-OJ's contract-fixed build epoch.
|
|
57
|
+
///
|
|
58
|
+
/// Execution determinism is deliberately absent from the build identity. Input
|
|
59
|
+
/// metadata must therefore remain invariant when callers change run seeds or
|
|
60
|
+
/// clocks; otherwise compiler features such as C/C++ `__TIMESTAMP__` could make
|
|
61
|
+
/// one cache key describe multiple artifacts.
|
|
62
|
+
pub fn compiler_project_files(
|
|
63
|
+
files: &std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
64
|
+
output_paths: &[String],
|
|
65
|
+
) -> Result<RuntimeProjectFilesystem, RunError> {
|
|
66
|
+
quota_project_files_at_timestamp(
|
|
67
|
+
files,
|
|
68
|
+
output_paths,
|
|
69
|
+
COMPILER_DETERMINISM
|
|
70
|
+
.realtime_epoch_ms
|
|
71
|
+
.saturating_mul(NANOSECONDS_PER_MILLISECOND),
|
|
72
|
+
COMPILER_FILESYSTEM_WRITE_LIMIT_BYTES,
|
|
73
|
+
COMPILER_FILESYSTEM_ENTRY_LIMIT,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Hydrate content-verified immutable compiler files once for a persistent
|
|
78
|
+
/// compiler session.
|
|
79
|
+
///
|
|
80
|
+
/// Per-build filesystems overlay this base instead of remounting or copying
|
|
81
|
+
/// the immutable distribution for every source edit.
|
|
82
|
+
pub fn immutable_compiler_files(
|
|
83
|
+
files: std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
84
|
+
) -> Result<Arc<dyn FileSystem + Send + Sync>, RunError> {
|
|
85
|
+
let fs = TmpFileSystem::with_fixed_timestamp(
|
|
86
|
+
COMPILER_DETERMINISM
|
|
87
|
+
.realtime_epoch_ms
|
|
88
|
+
.saturating_mul(NANOSECONDS_PER_MILLISECOND),
|
|
89
|
+
);
|
|
90
|
+
mount_project_files(&fs, &files)?;
|
|
91
|
+
Ok(Arc::new(fs))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Build an isolated mutable compiler filesystem over one hydrated immutable
|
|
95
|
+
/// distribution. Reads share base bytes; attempted writes use copy-on-write
|
|
96
|
+
/// storage and therefore cannot mutate later builds.
|
|
97
|
+
pub fn compiler_project_files_with_base(
|
|
98
|
+
files: &std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
99
|
+
output_paths: &[String],
|
|
100
|
+
immutable: Arc<dyn FileSystem + Send + Sync>,
|
|
101
|
+
) -> Result<RuntimeProjectFilesystem, RunError> {
|
|
102
|
+
let quota = VfsQuota::new();
|
|
103
|
+
let primary = TmpFileSystem::with_fixed_timestamp(
|
|
104
|
+
COMPILER_DETERMINISM
|
|
105
|
+
.realtime_epoch_ms
|
|
106
|
+
.saturating_mul(NANOSECONDS_PER_MILLISECOND),
|
|
107
|
+
);
|
|
108
|
+
primary.set_memory_limiter(quota.clone());
|
|
109
|
+
mount_project_files(&primary, files)?;
|
|
110
|
+
let filesystem: Arc<dyn FileSystem + Send + Sync> =
|
|
111
|
+
Arc::new(OverlayFileSystem::new(primary, [immutable]));
|
|
112
|
+
create_output_parent_directories(&filesystem, output_paths)?;
|
|
113
|
+
quota.seal(
|
|
114
|
+
COMPILER_FILESYSTEM_WRITE_LIMIT_BYTES,
|
|
115
|
+
COMPILER_FILESYSTEM_ENTRY_LIMIT,
|
|
116
|
+
)?;
|
|
117
|
+
Ok(RuntimeProjectFilesystem { filesystem, quota })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn quota_project_files_at_timestamp(
|
|
121
|
+
files: &std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
122
|
+
output_paths: &[String],
|
|
123
|
+
timestamp_ns: u64,
|
|
124
|
+
write_limit_bytes: usize,
|
|
125
|
+
entry_limit: usize,
|
|
126
|
+
) -> Result<RuntimeProjectFilesystem, RunError> {
|
|
127
|
+
let quota = VfsQuota::new();
|
|
128
|
+
let fs = TmpFileSystem::with_fixed_timestamp(timestamp_ns);
|
|
129
|
+
fs.set_memory_limiter(quota.clone());
|
|
130
|
+
mount_project_files(&fs, files)?;
|
|
131
|
+
let filesystem: Arc<dyn FileSystem + Send + Sync> = Arc::new(fs);
|
|
132
|
+
create_output_parent_directories(&filesystem, output_paths)?;
|
|
133
|
+
quota.seal(write_limit_bytes, entry_limit)?;
|
|
134
|
+
Ok(RuntimeProjectFilesystem { filesystem, quota })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn mount_project_files(
|
|
138
|
+
fs: &TmpFileSystem,
|
|
139
|
+
files: &std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
140
|
+
) -> Result<(), RunError> {
|
|
141
|
+
for (raw_path, contents) in files {
|
|
142
|
+
let path = Path::new(raw_path);
|
|
143
|
+
if !is_normalized_guest_path(raw_path) {
|
|
144
|
+
return Err(RunError::InvalidRequest(format!(
|
|
145
|
+
"guest file path must be absolute and may not contain '..': {raw_path}"
|
|
146
|
+
)));
|
|
147
|
+
}
|
|
148
|
+
if let Some(parent) = path.parent() {
|
|
149
|
+
create_dir_all(fs, parent)
|
|
150
|
+
.map_err(|error| RunError::Io(format!("failed to create {parent:?}: {error}")))?;
|
|
151
|
+
}
|
|
152
|
+
let mut file = fs
|
|
153
|
+
.new_open_options()
|
|
154
|
+
.create(true)
|
|
155
|
+
.truncate(true)
|
|
156
|
+
.write(true)
|
|
157
|
+
.open(path)
|
|
158
|
+
.map_err(|error| RunError::Io(format!("failed to create {raw_path}: {error}")))?;
|
|
159
|
+
futures::executor::block_on(async {
|
|
160
|
+
file.write_all(contents.as_ref()).await?;
|
|
161
|
+
file.flush().await
|
|
162
|
+
})
|
|
163
|
+
.map_err(|error| RunError::Io(format!("failed to mount {raw_path}: {error}")))?;
|
|
164
|
+
}
|
|
165
|
+
Ok(())
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pub fn read_files(
|
|
169
|
+
fs: &Arc<dyn FileSystem + Send + Sync>,
|
|
170
|
+
paths: &[String],
|
|
171
|
+
) -> Result<std::collections::BTreeMap<String, serde_bytes::ByteBuf>, RunError> {
|
|
172
|
+
let mut files = std::collections::BTreeMap::new();
|
|
173
|
+
for raw_path in paths {
|
|
174
|
+
if !is_normalized_guest_path(raw_path) {
|
|
175
|
+
return Err(RunError::InvalidRequest(format!(
|
|
176
|
+
"output path must be absolute and normalized: {raw_path}"
|
|
177
|
+
)));
|
|
178
|
+
}
|
|
179
|
+
let mut file = match fs.new_open_options().read(true).open(Path::new(raw_path)) {
|
|
180
|
+
Ok(file) => file,
|
|
181
|
+
Err(FsError::EntryNotFound) => continue,
|
|
182
|
+
Err(error) => {
|
|
183
|
+
return Err(RunError::Io(format!(
|
|
184
|
+
"failed to open compiler output {raw_path}: {error}"
|
|
185
|
+
)));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
let mut contents = Vec::new();
|
|
189
|
+
futures::executor::block_on(file.read_to_end(&mut contents)).map_err(|error| {
|
|
190
|
+
RunError::Io(format!(
|
|
191
|
+
"failed to read compiler output {raw_path}: {error}"
|
|
192
|
+
))
|
|
193
|
+
})?;
|
|
194
|
+
files.insert(raw_path.clone(), serde_bytes::ByteBuf::from(contents));
|
|
195
|
+
}
|
|
196
|
+
Ok(files)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pub fn create_output_parent_directories(
|
|
200
|
+
fs: &Arc<dyn FileSystem + Send + Sync>,
|
|
201
|
+
paths: &[String],
|
|
202
|
+
) -> Result<(), RunError> {
|
|
203
|
+
for raw_path in paths {
|
|
204
|
+
if !is_normalized_guest_path(raw_path) || raw_path == "/" {
|
|
205
|
+
return Err(RunError::InvalidRequest(format!(
|
|
206
|
+
"output path must be an absolute normalized file path: {raw_path}"
|
|
207
|
+
)));
|
|
208
|
+
}
|
|
209
|
+
if let Some(parent) = Path::new(raw_path).parent() {
|
|
210
|
+
create_dir_all(fs.as_ref(), parent).map_err(|error| {
|
|
211
|
+
RunError::Io(format!(
|
|
212
|
+
"failed to create output directory {parent:?}: {error}"
|
|
213
|
+
))
|
|
214
|
+
})?;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
Ok(())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pub fn read_files_bounded(
|
|
221
|
+
fs: &Arc<dyn FileSystem + Send + Sync>,
|
|
222
|
+
paths: &[String],
|
|
223
|
+
limit: usize,
|
|
224
|
+
) -> Result<
|
|
225
|
+
(
|
|
226
|
+
std::collections::BTreeMap<String, serde_bytes::ByteBuf>,
|
|
227
|
+
bool,
|
|
228
|
+
),
|
|
229
|
+
RunError,
|
|
230
|
+
> {
|
|
231
|
+
let mut files = std::collections::BTreeMap::new();
|
|
232
|
+
let mut total = 0usize;
|
|
233
|
+
for raw_path in paths {
|
|
234
|
+
let path = Path::new(raw_path);
|
|
235
|
+
let metadata = match fs.metadata(path) {
|
|
236
|
+
Ok(metadata) => metadata,
|
|
237
|
+
Err(FsError::EntryNotFound) => continue,
|
|
238
|
+
Err(error) => {
|
|
239
|
+
return Err(RunError::Io(format!(
|
|
240
|
+
"failed to inspect runtime output {raw_path}: {error}"
|
|
241
|
+
)));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
let length = usize::try_from(metadata.len()).map_err(|_| {
|
|
245
|
+
RunError::Io(format!(
|
|
246
|
+
"runtime output {raw_path} exceeds the host size range"
|
|
247
|
+
))
|
|
248
|
+
})?;
|
|
249
|
+
if length > limit.saturating_sub(total) {
|
|
250
|
+
return Ok((std::collections::BTreeMap::new(), true));
|
|
251
|
+
}
|
|
252
|
+
let mut file = fs
|
|
253
|
+
.new_open_options()
|
|
254
|
+
.read(true)
|
|
255
|
+
.open(path)
|
|
256
|
+
.map_err(|error| {
|
|
257
|
+
RunError::Io(format!("failed to open runtime output {raw_path}: {error}"))
|
|
258
|
+
})?;
|
|
259
|
+
let mut contents = Vec::with_capacity(length);
|
|
260
|
+
futures::executor::block_on(file.read_to_end(&mut contents)).map_err(|error| {
|
|
261
|
+
RunError::Io(format!("failed to read runtime output {raw_path}: {error}"))
|
|
262
|
+
})?;
|
|
263
|
+
if contents.len() != length {
|
|
264
|
+
return Err(RunError::Io(format!(
|
|
265
|
+
"runtime output {raw_path} changed while it was collected"
|
|
266
|
+
)));
|
|
267
|
+
}
|
|
268
|
+
total += contents.len();
|
|
269
|
+
files.insert(raw_path.clone(), serde_bytes::ByteBuf::from(contents));
|
|
270
|
+
}
|
|
271
|
+
Ok((files, false))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
pub fn is_normalized_guest_path(path: &str) -> bool {
|
|
275
|
+
path.starts_with('/')
|
|
276
|
+
&& !path.contains('\\')
|
|
277
|
+
&& !path
|
|
278
|
+
.split('/')
|
|
279
|
+
.any(|component| component == "." || component == "..")
|
|
280
|
+
&& (path == "/" || (!path.ends_with('/') && !path.contains("//")))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[cfg(test)]
|
|
284
|
+
mod tests {
|
|
285
|
+
use super::{
|
|
286
|
+
NANOSECONDS_PER_MILLISECOND, compiler_project_files, compiler_project_files_with_base,
|
|
287
|
+
immutable_compiler_files, is_normalized_guest_path, runtime_project_files,
|
|
288
|
+
};
|
|
289
|
+
use crate::deterministic::COMPILER_DETERMINISM;
|
|
290
|
+
use crate::{DeterminismConfig, ResourcePolicy};
|
|
291
|
+
use serde_bytes::ByteBuf;
|
|
292
|
+
use std::collections::BTreeMap;
|
|
293
|
+
use std::io::SeekFrom;
|
|
294
|
+
use std::path::Path;
|
|
295
|
+
use std::sync::Arc;
|
|
296
|
+
use virtual_fs::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, FileSystem};
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn validates_guest_paths_independently_of_host_path_semantics() {
|
|
300
|
+
assert!(is_normalized_guest_path("/project/build/main.pyc"));
|
|
301
|
+
assert!(is_normalized_guest_path("/"));
|
|
302
|
+
assert!(!is_normalized_guest_path("project/main.py"));
|
|
303
|
+
assert!(!is_normalized_guest_path("/project/../secret"));
|
|
304
|
+
assert!(!is_normalized_guest_path("/project//main.py"));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn mounts_seekable_runtime_files() {
|
|
309
|
+
let files = BTreeMap::from([(
|
|
310
|
+
"/runtime/library.zip".to_string(),
|
|
311
|
+
ByteBuf::from(b"abcdef".to_vec()),
|
|
312
|
+
)]);
|
|
313
|
+
let project = runtime_project_files(&files, &[], &determinism(), &resources()).unwrap();
|
|
314
|
+
let fs = project.filesystem();
|
|
315
|
+
let mut file = fs
|
|
316
|
+
.new_open_options()
|
|
317
|
+
.read(true)
|
|
318
|
+
.open(Path::new("/runtime/library.zip"))
|
|
319
|
+
.unwrap();
|
|
320
|
+
let bytes = futures::executor::block_on(async {
|
|
321
|
+
file.seek(SeekFrom::Start(2)).await.unwrap();
|
|
322
|
+
let mut bytes = Vec::new();
|
|
323
|
+
file.read_to_end(&mut bytes).await.unwrap();
|
|
324
|
+
bytes
|
|
325
|
+
});
|
|
326
|
+
assert_eq!(bytes, b"cdef");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn fixes_implicit_metadata_updates_to_the_deterministic_epoch() {
|
|
331
|
+
let config = determinism();
|
|
332
|
+
let expected = config.realtime_epoch_ms * 1_000_000;
|
|
333
|
+
let files = BTreeMap::from([(
|
|
334
|
+
"/project/input.txt".to_string(),
|
|
335
|
+
ByteBuf::from(b"input".to_vec()),
|
|
336
|
+
)]);
|
|
337
|
+
let project = runtime_project_files(&files, &[], &config, &resources()).unwrap();
|
|
338
|
+
let fs = project.filesystem();
|
|
339
|
+
|
|
340
|
+
for path in ["/", "/project", "/project/input.txt"] {
|
|
341
|
+
let metadata = fs.metadata(Path::new(path)).unwrap();
|
|
342
|
+
assert_eq!(metadata.accessed(), expected);
|
|
343
|
+
assert_eq!(metadata.created(), expected);
|
|
344
|
+
assert_eq!(metadata.modified(), expected);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let mut file = fs
|
|
348
|
+
.new_open_options()
|
|
349
|
+
.read(true)
|
|
350
|
+
.write(true)
|
|
351
|
+
.open(Path::new("/project/input.txt"))
|
|
352
|
+
.unwrap();
|
|
353
|
+
futures::executor::block_on(file.write_all(b"updated")).unwrap();
|
|
354
|
+
let metadata = fs.metadata(Path::new("/project/input.txt")).unwrap();
|
|
355
|
+
assert_eq!(metadata.accessed(), expected);
|
|
356
|
+
assert_eq!(metadata.modified(), expected);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
fn rejects_file_growth_before_any_partial_sparse_write() {
|
|
361
|
+
let mut limits = resources();
|
|
362
|
+
limits.filesystem_write_limit_bytes = 4;
|
|
363
|
+
limits.filesystem_entry_limit = 1;
|
|
364
|
+
let project =
|
|
365
|
+
runtime_project_files(&BTreeMap::new(), &[], &determinism(), &limits).unwrap();
|
|
366
|
+
let fs = project.filesystem();
|
|
367
|
+
let mut file = fs
|
|
368
|
+
.new_open_options()
|
|
369
|
+
.create(true)
|
|
370
|
+
.write(true)
|
|
371
|
+
.open(Path::new("/large.bin"))
|
|
372
|
+
.unwrap();
|
|
373
|
+
let error = futures::executor::block_on(async {
|
|
374
|
+
file.seek(SeekFrom::Start(8)).await.unwrap();
|
|
375
|
+
file.write_all(b"x").await.unwrap_err()
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
assert_eq!(error.kind(), std::io::ErrorKind::WriteZero);
|
|
379
|
+
assert_eq!(fs.metadata(Path::new("/large.bin")).unwrap().len(), 0);
|
|
380
|
+
assert!(project.quota_exceeded());
|
|
381
|
+
assert_eq!(
|
|
382
|
+
project.metrics(),
|
|
383
|
+
super::VfsMetrics {
|
|
384
|
+
bytes: 0,
|
|
385
|
+
entries: 1
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#[test]
|
|
391
|
+
fn rejects_new_inodes_without_removing_the_existing_entry() {
|
|
392
|
+
let mut limits = resources();
|
|
393
|
+
limits.filesystem_entry_limit = 1;
|
|
394
|
+
let project =
|
|
395
|
+
runtime_project_files(&BTreeMap::new(), &[], &determinism(), &limits).unwrap();
|
|
396
|
+
let fs = project.filesystem();
|
|
397
|
+
fs.new_open_options()
|
|
398
|
+
.create(true)
|
|
399
|
+
.write(true)
|
|
400
|
+
.open(Path::new("/first"))
|
|
401
|
+
.unwrap();
|
|
402
|
+
let error = fs
|
|
403
|
+
.new_open_options()
|
|
404
|
+
.create(true)
|
|
405
|
+
.write(true)
|
|
406
|
+
.open(Path::new("/second"))
|
|
407
|
+
.unwrap_err();
|
|
408
|
+
|
|
409
|
+
assert_eq!(error, virtual_fs::FsError::WriteZero);
|
|
410
|
+
assert!(fs.metadata(Path::new("/first")).is_ok());
|
|
411
|
+
assert_eq!(
|
|
412
|
+
fs.metadata(Path::new("/second")),
|
|
413
|
+
Err(virtual_fs::FsError::EntryNotFound)
|
|
414
|
+
);
|
|
415
|
+
assert!(project.quota_exceeded());
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#[test]
|
|
419
|
+
fn exact_limit_overwrite_truncate_and_delete_preserve_accounting() {
|
|
420
|
+
let mut limits = resources();
|
|
421
|
+
limits.filesystem_write_limit_bytes = 4;
|
|
422
|
+
limits.filesystem_entry_limit = 1;
|
|
423
|
+
let project =
|
|
424
|
+
runtime_project_files(&BTreeMap::new(), &[], &determinism(), &limits).unwrap();
|
|
425
|
+
let fs = project.filesystem();
|
|
426
|
+
let mut first = fs
|
|
427
|
+
.new_open_options()
|
|
428
|
+
.create(true)
|
|
429
|
+
.read(true)
|
|
430
|
+
.write(true)
|
|
431
|
+
.open(Path::new("/first"))
|
|
432
|
+
.unwrap();
|
|
433
|
+
|
|
434
|
+
futures::executor::block_on(async {
|
|
435
|
+
first.write_all(b"1234").await.unwrap();
|
|
436
|
+
first.seek(SeekFrom::Start(0)).await.unwrap();
|
|
437
|
+
first.write_all(b"abcd").await.unwrap();
|
|
438
|
+
});
|
|
439
|
+
first.set_len(0).unwrap();
|
|
440
|
+
futures::executor::block_on(first.write_all(b"5678")).unwrap();
|
|
441
|
+
drop(first);
|
|
442
|
+
fs.remove_file(Path::new("/first")).unwrap();
|
|
443
|
+
|
|
444
|
+
let mut second = fs
|
|
445
|
+
.new_open_options()
|
|
446
|
+
.create(true)
|
|
447
|
+
.write(true)
|
|
448
|
+
.open(Path::new("/second"))
|
|
449
|
+
.unwrap();
|
|
450
|
+
futures::executor::block_on(second.write_all(b"wxyz")).unwrap();
|
|
451
|
+
|
|
452
|
+
assert!(!project.quota_exceeded());
|
|
453
|
+
assert_eq!(
|
|
454
|
+
project.metrics(),
|
|
455
|
+
super::VfsMetrics {
|
|
456
|
+
bytes: 4,
|
|
457
|
+
entries: 1
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
assert_eq!(fs.metadata(Path::new("/second")).unwrap().len(), 4);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[test]
|
|
464
|
+
fn compiler_metadata_uses_the_contract_build_epoch() {
|
|
465
|
+
let files = BTreeMap::from([(
|
|
466
|
+
"/project/main.c".to_string(),
|
|
467
|
+
ByteBuf::from(b"const char *stamp = __TIMESTAMP__;\n".to_vec()),
|
|
468
|
+
)]);
|
|
469
|
+
let first = compiler_project_files(&files, &[]).unwrap().filesystem();
|
|
470
|
+
let second = compiler_project_files(&files, &[]).unwrap().filesystem();
|
|
471
|
+
let expected = COMPILER_DETERMINISM.realtime_epoch_ms * NANOSECONDS_PER_MILLISECOND;
|
|
472
|
+
|
|
473
|
+
for filesystem in [first, second] {
|
|
474
|
+
for path in ["/", "/project", "/project/main.c"] {
|
|
475
|
+
let metadata = filesystem.metadata(Path::new(path)).unwrap();
|
|
476
|
+
assert_eq!(metadata.accessed(), expected);
|
|
477
|
+
assert_eq!(metadata.created(), expected);
|
|
478
|
+
assert_eq!(metadata.modified(), expected);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#[test]
|
|
484
|
+
fn compiler_builds_share_one_immutable_base_without_cross_build_mutation() {
|
|
485
|
+
let immutable = immutable_compiler_files(BTreeMap::from([(
|
|
486
|
+
"/go/pkg/fmt.a".to_string(),
|
|
487
|
+
ByteBuf::from(b"immutable-archive".to_vec()),
|
|
488
|
+
)]))
|
|
489
|
+
.unwrap();
|
|
490
|
+
let sources = BTreeMap::from([(
|
|
491
|
+
"/work/main.go".to_string(),
|
|
492
|
+
ByteBuf::from(b"package main".to_vec()),
|
|
493
|
+
)]);
|
|
494
|
+
let first = compiler_project_files_with_base(&sources, &[], immutable.clone())
|
|
495
|
+
.unwrap()
|
|
496
|
+
.filesystem();
|
|
497
|
+
let mut mutable_view = first
|
|
498
|
+
.new_open_options()
|
|
499
|
+
.read(true)
|
|
500
|
+
.write(true)
|
|
501
|
+
.open(Path::new("/go/pkg/fmt.a"))
|
|
502
|
+
.unwrap();
|
|
503
|
+
futures::executor::block_on(mutable_view.write_all(b"build-local")).unwrap();
|
|
504
|
+
mutable_view.set_len(b"build-local".len() as u64).unwrap();
|
|
505
|
+
drop(mutable_view);
|
|
506
|
+
|
|
507
|
+
assert_eq!(read_all(&first, "/go/pkg/fmt.a"), b"build-local");
|
|
508
|
+
assert_eq!(read_all(&immutable, "/go/pkg/fmt.a"), b"immutable-archive");
|
|
509
|
+
|
|
510
|
+
let second = compiler_project_files_with_base(&sources, &[], immutable)
|
|
511
|
+
.unwrap()
|
|
512
|
+
.filesystem();
|
|
513
|
+
assert_eq!(read_all(&second, "/go/pkg/fmt.a"), b"immutable-archive");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
fn read_all(fs: &Arc<dyn FileSystem + Send + Sync>, path: &str) -> Vec<u8> {
|
|
517
|
+
let mut file = fs
|
|
518
|
+
.new_open_options()
|
|
519
|
+
.read(true)
|
|
520
|
+
.open(Path::new(path))
|
|
521
|
+
.unwrap();
|
|
522
|
+
futures::executor::block_on(async {
|
|
523
|
+
let mut bytes = Vec::new();
|
|
524
|
+
file.read_to_end(&mut bytes).await.unwrap();
|
|
525
|
+
bytes
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fn determinism() -> DeterminismConfig {
|
|
530
|
+
DeterminismConfig {
|
|
531
|
+
random_seed: 7,
|
|
532
|
+
realtime_epoch_ms: 946_684_800_000,
|
|
533
|
+
clock_step_ns: 1_000_000,
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
fn resources() -> ResourcePolicy {
|
|
538
|
+
ResourcePolicy {
|
|
539
|
+
instruction_budget: 1_000_000,
|
|
540
|
+
logical_time_limit_ms: 60_000,
|
|
541
|
+
memory_limit_bytes: 2 * 65_536,
|
|
542
|
+
output_limit_bytes: 1_024,
|
|
543
|
+
filesystem_write_limit_bytes: 64 * 1024 * 1024,
|
|
544
|
+
filesystem_entry_limit: 4_096,
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|