@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,167 @@
|
|
|
1
|
+
use crate::RunError;
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
3
|
+
use virtual_fs::{FsError, limiter::FsMemoryLimiter};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug)]
|
|
6
|
+
pub(crate) struct VfsQuota {
|
|
7
|
+
state: Mutex<VfsQuotaState>,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#[derive(Debug)]
|
|
11
|
+
struct VfsQuotaState {
|
|
12
|
+
bytes: usize,
|
|
13
|
+
entries: usize,
|
|
14
|
+
byte_ceiling: Option<usize>,
|
|
15
|
+
entry_ceiling: Option<usize>,
|
|
16
|
+
peak_bytes: usize,
|
|
17
|
+
peak_entries: usize,
|
|
18
|
+
exceeded: bool,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
22
|
+
pub(crate) struct VfsMetrics {
|
|
23
|
+
pub bytes: u64,
|
|
24
|
+
pub entries: u64,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl VfsQuota {
|
|
28
|
+
pub fn new() -> Arc<Self> {
|
|
29
|
+
Arc::new(Self {
|
|
30
|
+
state: Mutex::new(VfsQuotaState {
|
|
31
|
+
bytes: 0,
|
|
32
|
+
entries: 0,
|
|
33
|
+
byte_ceiling: None,
|
|
34
|
+
entry_ceiling: None,
|
|
35
|
+
peak_bytes: 0,
|
|
36
|
+
peak_entries: 0,
|
|
37
|
+
exceeded: false,
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Seal the mounted filesystem as the baseline and allow only the supplied
|
|
43
|
+
/// additional live file data and inode counts during guest execution.
|
|
44
|
+
pub fn seal(&self, write_bytes: usize, entries: usize) -> Result<(), RunError> {
|
|
45
|
+
let mut state = self
|
|
46
|
+
.state
|
|
47
|
+
.lock()
|
|
48
|
+
.map_err(|error| RunError::Runtime(error.to_string()))?;
|
|
49
|
+
if state.byte_ceiling.is_some() || state.entry_ceiling.is_some() {
|
|
50
|
+
return Err(RunError::Runtime(
|
|
51
|
+
"runtime filesystem quota was sealed more than once".to_string(),
|
|
52
|
+
));
|
|
53
|
+
}
|
|
54
|
+
state.byte_ceiling = Some(state.bytes.checked_add(write_bytes).ok_or_else(|| {
|
|
55
|
+
RunError::InvalidRequest("filesystem byte quota overflows host range".to_string())
|
|
56
|
+
})?);
|
|
57
|
+
state.entry_ceiling = Some(state.entries.checked_add(entries).ok_or_else(|| {
|
|
58
|
+
RunError::InvalidRequest("filesystem entry quota overflows host range".to_string())
|
|
59
|
+
})?);
|
|
60
|
+
state.peak_bytes = state.bytes;
|
|
61
|
+
state.peak_entries = state.entries;
|
|
62
|
+
Ok(())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn exceeded(&self) -> bool {
|
|
66
|
+
self.state.lock().expect("VFS quota lock poisoned").exceeded
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub fn metrics(&self) -> VfsMetrics {
|
|
70
|
+
let state = self.state.lock().expect("VFS quota lock poisoned");
|
|
71
|
+
VfsMetrics {
|
|
72
|
+
bytes: state.peak_bytes as u64,
|
|
73
|
+
entries: state.peak_entries as u64,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl FsMemoryLimiter for VfsQuota {
|
|
79
|
+
fn on_grow(&self, grown_bytes: usize) -> Result<(), FsError> {
|
|
80
|
+
let mut state = self.state.lock().map_err(|_| FsError::Lock)?;
|
|
81
|
+
let next = state
|
|
82
|
+
.bytes
|
|
83
|
+
.checked_add(grown_bytes)
|
|
84
|
+
.ok_or(FsError::WriteZero)?;
|
|
85
|
+
if state.byte_ceiling.is_some_and(|limit| next > limit) {
|
|
86
|
+
state.exceeded = true;
|
|
87
|
+
return Err(FsError::WriteZero);
|
|
88
|
+
}
|
|
89
|
+
state.bytes = next;
|
|
90
|
+
state.peak_bytes = state.peak_bytes.max(next);
|
|
91
|
+
Ok(())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn on_shrink(&self, shrunk_bytes: usize) {
|
|
95
|
+
let mut state = self.state.lock().expect("VFS quota lock poisoned");
|
|
96
|
+
state.bytes = state
|
|
97
|
+
.bytes
|
|
98
|
+
.checked_sub(shrunk_bytes)
|
|
99
|
+
.expect("VFS byte accounting underflow");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn on_create_entry(&self) -> Result<(), FsError> {
|
|
103
|
+
let mut state = self.state.lock().map_err(|_| FsError::Lock)?;
|
|
104
|
+
let next = state.entries.checked_add(1).ok_or(FsError::WriteZero)?;
|
|
105
|
+
if state.entry_ceiling.is_some_and(|limit| next > limit) {
|
|
106
|
+
state.exceeded = true;
|
|
107
|
+
return Err(FsError::WriteZero);
|
|
108
|
+
}
|
|
109
|
+
state.entries = next;
|
|
110
|
+
state.peak_entries = state.peak_entries.max(next);
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn on_remove_entry(&self) {
|
|
115
|
+
let mut state = self.state.lock().expect("VFS quota lock poisoned");
|
|
116
|
+
state.entries = state
|
|
117
|
+
.entries
|
|
118
|
+
.checked_sub(1)
|
|
119
|
+
.expect("VFS entry accounting underflow");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[cfg(test)]
|
|
124
|
+
mod tests {
|
|
125
|
+
use super::*;
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn seals_a_baseline_and_trips_sticky_limits() {
|
|
129
|
+
let quota = VfsQuota::new();
|
|
130
|
+
quota.on_grow(10).unwrap();
|
|
131
|
+
quota.on_create_entry().unwrap();
|
|
132
|
+
quota.seal(4, 1).unwrap();
|
|
133
|
+
|
|
134
|
+
quota.on_grow(4).unwrap();
|
|
135
|
+
quota.on_create_entry().unwrap();
|
|
136
|
+
assert_eq!(
|
|
137
|
+
quota.metrics(),
|
|
138
|
+
VfsMetrics {
|
|
139
|
+
bytes: 14,
|
|
140
|
+
entries: 2
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
assert_eq!(quota.on_grow(1), Err(FsError::WriteZero));
|
|
144
|
+
assert!(quota.exceeded());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn deletion_releases_live_storage_headroom() {
|
|
149
|
+
let quota = VfsQuota::new();
|
|
150
|
+
quota.on_grow(10).unwrap();
|
|
151
|
+
quota.on_create_entry().unwrap();
|
|
152
|
+
quota.seal(4, 1).unwrap();
|
|
153
|
+
quota.on_shrink(10);
|
|
154
|
+
quota.on_remove_entry();
|
|
155
|
+
|
|
156
|
+
quota.on_grow(14).unwrap();
|
|
157
|
+
quota.on_create_entry().unwrap();
|
|
158
|
+
assert!(!quota.exceeded());
|
|
159
|
+
assert_eq!(
|
|
160
|
+
quota.metrics(),
|
|
161
|
+
VfsMetrics {
|
|
162
|
+
bytes: 14,
|
|
163
|
+
entries: 1
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
use crate::filesystem::immutable_compiler_files;
|
|
2
|
+
use crate::{
|
|
3
|
+
CompilePipelineResponse, CompilerToolchain, GoCompilerSessionConfig, GoCompilerSessionRequest,
|
|
4
|
+
GoCompilerSessionResponse, GoCompilerSourceDelta, RunError, RunFailure,
|
|
5
|
+
};
|
|
6
|
+
use serde_bytes::ByteBuf;
|
|
7
|
+
use std::collections::BTreeMap;
|
|
8
|
+
use std::sync::{Arc, Mutex};
|
|
9
|
+
use virtual_fs::FileSystem;
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Default)]
|
|
12
|
+
struct SourceState {
|
|
13
|
+
generation: u32,
|
|
14
|
+
files: BTreeMap<String, ByteBuf>,
|
|
15
|
+
in_flight: bool,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// A digest-bound, process-local Go compiler handle.
|
|
19
|
+
///
|
|
20
|
+
/// The WebC package and Go standard library cross the JS/Wasm boundary once at
|
|
21
|
+
/// construction. Each later request carries only a monotonic `/work` source
|
|
22
|
+
/// delta and stage argv. The immutable filesystem is shared by copy-on-write
|
|
23
|
+
/// build overlays, while stores, instances, process state, outputs, and source
|
|
24
|
+
/// snapshots remain isolated per build.
|
|
25
|
+
pub struct GoCompilerSession {
|
|
26
|
+
digest: String,
|
|
27
|
+
toolchain: CompilerToolchain,
|
|
28
|
+
standard_library: Arc<dyn FileSystem + Send + Sync>,
|
|
29
|
+
source: Mutex<SourceState>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl GoCompilerSession {
|
|
33
|
+
pub fn new(config: GoCompilerSessionConfig) -> Result<Self, RunError> {
|
|
34
|
+
validate_digest(&config.digest)?;
|
|
35
|
+
if config.standard_library_files.is_empty() {
|
|
36
|
+
return Err(RunError::InvalidRequest(
|
|
37
|
+
"Go compiler session requires a non-empty standard library".to_string(),
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
crate::run::validate_mounted_files(&config.standard_library_files, "Go standard library")?;
|
|
41
|
+
for path in config.standard_library_files.keys() {
|
|
42
|
+
validate_standard_library_path(path)?;
|
|
43
|
+
}
|
|
44
|
+
let standard_library = immutable_compiler_files(config.standard_library_files)?;
|
|
45
|
+
let toolchain = CompilerToolchain::new(config.toolchain)?;
|
|
46
|
+
Ok(Self {
|
|
47
|
+
digest: config.digest,
|
|
48
|
+
toolchain,
|
|
49
|
+
standard_library,
|
|
50
|
+
source: Mutex::new(SourceState::default()),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn digest(&self) -> &str {
|
|
55
|
+
&self.digest
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn generation(&self) -> Result<u32, RunError> {
|
|
59
|
+
Ok(self
|
|
60
|
+
.source
|
|
61
|
+
.lock()
|
|
62
|
+
.map_err(|error| RunError::Runtime(error.to_string()))?
|
|
63
|
+
.generation)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub async fn compile_pipeline_response(
|
|
67
|
+
&self,
|
|
68
|
+
request: GoCompilerSessionRequest,
|
|
69
|
+
) -> Result<GoCompilerSessionResponse, RunError> {
|
|
70
|
+
if request.digest != self.digest {
|
|
71
|
+
return Err(RunError::InvalidRequest(
|
|
72
|
+
"Go compiler request digest does not match its hydrated session".to_string(),
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
let generation = request.source_delta.generation;
|
|
76
|
+
let next_files = self.prepare_source_delta(request.source_delta)?;
|
|
77
|
+
let result = self
|
|
78
|
+
.toolchain
|
|
79
|
+
.compile_pipeline_with_base(
|
|
80
|
+
next_files.clone(),
|
|
81
|
+
request.stages,
|
|
82
|
+
self.standard_library.clone(),
|
|
83
|
+
)
|
|
84
|
+
.await;
|
|
85
|
+
let response = match result {
|
|
86
|
+
Ok(result) => CompilePipelineResponse {
|
|
87
|
+
ok: true,
|
|
88
|
+
result: Some(result),
|
|
89
|
+
error: None,
|
|
90
|
+
},
|
|
91
|
+
Err(error) => CompilePipelineResponse {
|
|
92
|
+
ok: false,
|
|
93
|
+
result: None,
|
|
94
|
+
error: Some(RunFailure {
|
|
95
|
+
code: error.code(),
|
|
96
|
+
message: error.to_string(),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
self.commit_source_delta(generation, next_files)?;
|
|
101
|
+
Ok(GoCompilerSessionResponse {
|
|
102
|
+
digest: self.digest.clone(),
|
|
103
|
+
generation,
|
|
104
|
+
response,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn prepare_source_delta(
|
|
109
|
+
&self,
|
|
110
|
+
delta: GoCompilerSourceDelta,
|
|
111
|
+
) -> Result<BTreeMap<String, ByteBuf>, RunError> {
|
|
112
|
+
let mut state = self
|
|
113
|
+
.source
|
|
114
|
+
.lock()
|
|
115
|
+
.map_err(|error| RunError::Runtime(error.to_string()))?;
|
|
116
|
+
if state.in_flight {
|
|
117
|
+
return Err(RunError::InvalidRequest(
|
|
118
|
+
"Go compiler session accepts one build at a time".to_string(),
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
let next = apply_source_delta(&state, delta)?;
|
|
122
|
+
state.in_flight = true;
|
|
123
|
+
Ok(next)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn commit_source_delta(
|
|
127
|
+
&self,
|
|
128
|
+
generation: u32,
|
|
129
|
+
files: BTreeMap<String, ByteBuf>,
|
|
130
|
+
) -> Result<(), RunError> {
|
|
131
|
+
let mut state = self
|
|
132
|
+
.source
|
|
133
|
+
.lock()
|
|
134
|
+
.map_err(|error| RunError::Runtime(error.to_string()))?;
|
|
135
|
+
if !state.in_flight || generation != state.generation.saturating_add(1) {
|
|
136
|
+
return Err(RunError::Runtime(
|
|
137
|
+
"Go compiler session source state changed during a build".to_string(),
|
|
138
|
+
));
|
|
139
|
+
}
|
|
140
|
+
state.generation = generation;
|
|
141
|
+
state.files = files;
|
|
142
|
+
state.in_flight = false;
|
|
143
|
+
Ok(())
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn apply_source_delta(
|
|
148
|
+
state: &SourceState,
|
|
149
|
+
delta: GoCompilerSourceDelta,
|
|
150
|
+
) -> Result<BTreeMap<String, ByteBuf>, RunError> {
|
|
151
|
+
let expected = state.generation.checked_add(1).ok_or_else(|| {
|
|
152
|
+
RunError::InvalidRequest("Go compiler session generation is exhausted".to_string())
|
|
153
|
+
})?;
|
|
154
|
+
if delta.generation != expected {
|
|
155
|
+
return Err(RunError::InvalidRequest(format!(
|
|
156
|
+
"Go compiler source delta generation {} does not follow {}",
|
|
157
|
+
delta.generation, state.generation
|
|
158
|
+
)));
|
|
159
|
+
}
|
|
160
|
+
crate::run::validate_mounted_files(&delta.upsert_files, "Go source delta")?;
|
|
161
|
+
for path in delta.upsert_files.keys() {
|
|
162
|
+
validate_source_path(path)?;
|
|
163
|
+
}
|
|
164
|
+
let mut previous_remove = None;
|
|
165
|
+
for path in &delta.remove_paths {
|
|
166
|
+
validate_source_path(path)?;
|
|
167
|
+
if previous_remove.is_some_and(|previous: &String| previous >= path) {
|
|
168
|
+
return Err(RunError::InvalidRequest(
|
|
169
|
+
"Go compiler source removals must be strictly sorted and unique".to_string(),
|
|
170
|
+
));
|
|
171
|
+
}
|
|
172
|
+
if delta.upsert_files.contains_key(path) {
|
|
173
|
+
return Err(RunError::InvalidRequest(format!(
|
|
174
|
+
"Go compiler source delta both removes and upserts '{path}'"
|
|
175
|
+
)));
|
|
176
|
+
}
|
|
177
|
+
previous_remove = Some(path);
|
|
178
|
+
}
|
|
179
|
+
let mut next = state.files.clone();
|
|
180
|
+
for path in delta.remove_paths {
|
|
181
|
+
next.remove(&path);
|
|
182
|
+
}
|
|
183
|
+
next.extend(delta.upsert_files);
|
|
184
|
+
crate::run::validate_mounted_files(&next, "Go source snapshot")?;
|
|
185
|
+
Ok(next)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn validate_digest(digest: &str) -> Result<(), RunError> {
|
|
189
|
+
if digest.len() != 64
|
|
190
|
+
|| !digest
|
|
191
|
+
.bytes()
|
|
192
|
+
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
|
193
|
+
{
|
|
194
|
+
return Err(RunError::InvalidRequest(
|
|
195
|
+
"Go compiler session digest must be a lowercase SHA-256".to_string(),
|
|
196
|
+
));
|
|
197
|
+
}
|
|
198
|
+
Ok(())
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn validate_standard_library_path(path: &str) -> Result<(), RunError> {
|
|
202
|
+
if !crate::filesystem::is_normalized_guest_path(path) || !path.starts_with("/go/") {
|
|
203
|
+
return Err(RunError::InvalidRequest(format!(
|
|
204
|
+
"Go standard-library file must be under /go: {path}"
|
|
205
|
+
)));
|
|
206
|
+
}
|
|
207
|
+
Ok(())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn validate_source_path(path: &str) -> Result<(), RunError> {
|
|
211
|
+
if !crate::filesystem::is_normalized_guest_path(path) || !path.starts_with("/work/") {
|
|
212
|
+
return Err(RunError::InvalidRequest(format!(
|
|
213
|
+
"Go compiler mutable file must be under /work: {path}"
|
|
214
|
+
)));
|
|
215
|
+
}
|
|
216
|
+
Ok(())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[cfg(test)]
|
|
220
|
+
mod tests {
|
|
221
|
+
use super::{
|
|
222
|
+
SourceState, apply_source_delta, validate_digest, validate_source_path,
|
|
223
|
+
validate_standard_library_path,
|
|
224
|
+
};
|
|
225
|
+
use crate::{GoCompilerSourceDelta, RunError};
|
|
226
|
+
use serde_bytes::ByteBuf;
|
|
227
|
+
use std::collections::BTreeMap;
|
|
228
|
+
|
|
229
|
+
#[test]
|
|
230
|
+
fn validates_digest_and_filesystem_boundaries() {
|
|
231
|
+
assert!(validate_digest(&"a".repeat(64)).is_ok());
|
|
232
|
+
assert!(validate_digest(&"A".repeat(64)).is_err());
|
|
233
|
+
assert!(validate_digest("short").is_err());
|
|
234
|
+
assert!(validate_standard_library_path("/go/pkg/fmt.a").is_ok());
|
|
235
|
+
assert!(validate_standard_library_path("/work/fmt.a").is_err());
|
|
236
|
+
assert!(validate_source_path("/work/main.go").is_ok());
|
|
237
|
+
assert!(validate_source_path("/go/pkg/fmt.a").is_err());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn source_state_starts_empty_and_generation_zero() {
|
|
242
|
+
let state = SourceState::default();
|
|
243
|
+
assert_eq!(state.generation, 0);
|
|
244
|
+
assert!(state.files.is_empty());
|
|
245
|
+
assert!(!state.in_flight);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn source_delta_applies_upserts_and_sorted_removals_monotonically() {
|
|
250
|
+
let state = SourceState {
|
|
251
|
+
generation: 1,
|
|
252
|
+
files: BTreeMap::from([
|
|
253
|
+
("/work/main.go".to_string(), ByteBuf::from(vec![1])),
|
|
254
|
+
("/work/old.go".to_string(), ByteBuf::from(vec![2])),
|
|
255
|
+
]),
|
|
256
|
+
in_flight: false,
|
|
257
|
+
};
|
|
258
|
+
let delta = GoCompilerSourceDelta {
|
|
259
|
+
generation: 2,
|
|
260
|
+
upsert_files: BTreeMap::from([
|
|
261
|
+
("/work/main.go".to_string(), ByteBuf::from(vec![3])),
|
|
262
|
+
("/work/new.go".to_string(), ByteBuf::from(vec![4])),
|
|
263
|
+
]),
|
|
264
|
+
remove_paths: vec!["/work/old.go".to_string()],
|
|
265
|
+
};
|
|
266
|
+
let next = apply_source_delta(&state, delta).unwrap();
|
|
267
|
+
assert_eq!(
|
|
268
|
+
next.keys().map(String::as_str).collect::<Vec<_>>(),
|
|
269
|
+
["/work/main.go", "/work/new.go"]
|
|
270
|
+
);
|
|
271
|
+
assert_eq!(next["/work/main.go"].as_ref(), &[3]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn source_delta_rejects_stale_generations_and_ambiguous_updates() {
|
|
276
|
+
let state = SourceState::default();
|
|
277
|
+
let stale = GoCompilerSourceDelta {
|
|
278
|
+
generation: 2,
|
|
279
|
+
upsert_files: BTreeMap::new(),
|
|
280
|
+
remove_paths: Vec::new(),
|
|
281
|
+
};
|
|
282
|
+
assert!(apply_source_delta(&state, stale).is_err());
|
|
283
|
+
|
|
284
|
+
let ambiguous = GoCompilerSourceDelta {
|
|
285
|
+
generation: 1,
|
|
286
|
+
upsert_files: BTreeMap::from([("/work/main.go".to_string(), ByteBuf::from(vec![1]))]),
|
|
287
|
+
remove_paths: vec!["/work/main.go".to_string()],
|
|
288
|
+
};
|
|
289
|
+
assert!(apply_source_delta(&state, ambiguous).is_err());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[test]
|
|
293
|
+
fn invalid_request_error_code_remains_stable() {
|
|
294
|
+
let error = validate_source_path("../main.go").unwrap_err();
|
|
295
|
+
assert!(matches!(error, RunError::InvalidRequest(_)));
|
|
296
|
+
}
|
|
297
|
+
}
|