@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,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
+ }