yggdrasil/api/code_runner/
languages.rs1use std::collections::HashMap;
17use std::sync::LazyLock;
18
19use crate::infra::docker::CacheVolume;
20use crate::infra::runner_config::{ResourceLimits, RUNNER_CONFIG};
21
22pub struct LanguageDef {
24 pub image: String,
26 pub run_cmd: String,
28 pub extension: String,
29 pub default_limits: ResourceLimits,
30 pub allow_network: bool,
32 pub cache_volume: Option<CacheVolume>,
35}
36
37pub static LANGUAGES: LazyLock<HashMap<String, LanguageDef>> = LazyLock::new(|| {
40 let mut m = HashMap::new();
41
42 m.insert(
43 "python".to_string(),
44 LanguageDef {
45 image: "yggdrasil-runner-python:latest".to_string(),
46 run_cmd: "python -u /code/main.py".to_string(),
51 extension: "py".to_string(),
52 default_limits: ResourceLimits {
53 cpu_cores: 1.0,
54 memory_mb: 256,
55 timeout_secs: 5,
56 output_bytes: 1_048_576,
57 allow_network: false,
58 },
59 allow_network: false,
60 cache_volume: None, },
62 );
63
64 m.insert(
65 "node".to_string(),
66 LanguageDef {
67 image: "yggdrasil-runner-node:latest".to_string(),
68 run_cmd: "node /code/main.js".to_string(),
69 extension: "js".to_string(),
70 default_limits: ResourceLimits {
71 cpu_cores: 1.0,
72 memory_mb: 256,
73 timeout_secs: 5,
74 output_bytes: 1_048_576,
75 allow_network: false,
76 },
77 allow_network: false,
78 cache_volume: None, },
80 );
81
82 m.insert(
88 "go".to_string(),
89 LanguageDef {
90 image: "yggdrasil-runner-go:latest".to_string(),
91 run_cmd: "go run /code/main.go".to_string(),
92 extension: "go".to_string(),
93 default_limits: ResourceLimits {
94 cpu_cores: 1.0,
95 memory_mb: 256,
96 timeout_secs: 10,
97 output_bytes: 1_048_576,
98 allow_network: false,
99 },
100 allow_network: false,
101 cache_volume: Some(CacheVolume {
102 name: "yggdrasil-gocache".to_string(),
103 mount_path: "/go-cache".to_string(),
104 }),
105 },
106 );
107
108 m.insert(
112 "rust".to_string(),
113 LanguageDef {
114 image: "yggdrasil-runner-rust:latest".to_string(),
115 run_cmd: "/usr/local/bin/run-rust.sh".to_string(),
116 extension: "rs".to_string(),
117 default_limits: ResourceLimits {
118 cpu_cores: 1.0,
119 memory_mb: 512,
120 timeout_secs: 15,
121 output_bytes: 1_048_576,
122 allow_network: false,
123 },
124 allow_network: false,
125 cache_volume: None, },
127 );
128
129 m.insert(
134 "bun".to_string(),
135 LanguageDef {
136 image: "yggdrasil-runner-bun:latest".to_string(),
137 run_cmd: "bun run /code/main.ts".to_string(),
138 extension: "ts".to_string(),
139 default_limits: ResourceLimits {
140 cpu_cores: 1.0,
141 memory_mb: 256,
142 timeout_secs: 5,
143 output_bytes: 1_048_576,
144 allow_network: false,
145 },
146 allow_network: false,
147 cache_volume: None, },
149 );
150
151 m
152});
153
154const LANG_ALIASES: &[(&str, &str)] = &[
160 ("js", "node"),
161 ("javascript", "node"),
162 ("rs", "rust"),
163 ("ts", "bun"),
164 ("typescript", "bun"),
165];
166
167pub fn normalize_lang(lang: &str) -> String {
175 let clean = lang.trim().to_lowercase();
176 for &(from, to) in LANG_ALIASES {
177 if clean == from {
178 return to.to_string();
179 }
180 }
181 clean
182}
183
184pub fn is_supported_lang(lang: &str) -> bool {
188 let canonical = normalize_lang(lang);
189 LANGUAGES.contains_key(&canonical)
190 && RUNNER_CONFIG
191 .languages
192 .as_ref()
193 .is_none_or(|list| list.iter().any(|l| l == &canonical))
194}
195
196pub fn parse_fence_info(info: &str) -> (String, bool, Option<ResourceLimits>) {
205 let tokens: Vec<&str> = info.split_whitespace().collect();
206 if tokens.is_empty() {
207 return ("".to_string(), false, None);
208 }
209 let lang = normalize_lang(tokens[0]);
210 let mut runnable = false;
211 let mut overrides = None;
212
213 if tokens.len() > 1 {
214 for &tok in &tokens[1..] {
215 if tok == "runnable" || tok == "run" {
216 runnable = true;
217 } else if tok.starts_with('{') {
218 if let Ok(limits) = serde_json::from_str::<ResourceLimits>(tok) {
219 overrides = Some(limits);
220 }
221 }
222 }
223 }
224
225 (lang, runnable, overrides)
226}
227
228#[cfg(all(test, feature = "server"))]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn parse_fence_info_runnable_marker() {
234 let (lang, runnable, overrides) = parse_fence_info("python runnable");
235 assert_eq!(lang, "python");
236 assert!(runnable);
237 assert!(overrides.is_none());
238 }
239
240 #[test]
241 fn parse_fence_info_run_alias() {
242 let (lang, runnable, _) = parse_fence_info("node run");
243 assert_eq!(lang, "node");
244 assert!(runnable);
245 }
246
247 #[test]
248 fn parse_fence_info_with_overrides() {
249 let (lang, runnable, overrides) = parse_fence_info(
250 r#"node runnable {"timeout_secs":10,"memory_mb":512,"allow_network":true,"cpu_cores":1.0,"output_bytes":1024}"#,
251 );
252 assert_eq!(lang, "node");
253 assert!(runnable);
254 let limits = overrides.unwrap();
255 assert_eq!(limits.timeout_secs, 10);
256 assert_eq!(limits.memory_mb, 512);
257 assert!(limits.allow_network);
258 }
259
260 #[test]
261 fn parse_fence_info_not_runnable() {
262 let (lang, runnable, _) = parse_fence_info("rust");
263 assert_eq!(lang, "rust");
264 assert!(!runnable);
265 }
266
267 #[test]
268 fn parse_fence_info_empty() {
269 let (lang, runnable, overrides) = parse_fence_info("");
270 assert_eq!(lang, "");
271 assert!(!runnable);
272 assert!(overrides.is_none());
273 }
274
275 #[test]
276 fn parse_fence_info_case_insensitive_lang() {
277 let (lang, _, _) = parse_fence_info("PYTHON runnable");
278 assert_eq!(lang, "python");
279 }
280
281 #[test]
282 fn parse_fence_info_malformed_json_yields_none() {
283 let (lang, runnable, overrides) = parse_fence_info(r#"python runnable {not valid json}"#);
284 assert_eq!(lang, "python");
285 assert!(runnable);
286 assert!(overrides.is_none(), "malformed JSON should yield None");
287 }
288
289 #[test]
290 fn is_supported_lang_default_all_open() {
291 assert!(is_supported_lang("python"));
293 assert!(is_supported_lang("node"));
294 assert!(is_supported_lang("go"));
295 assert!(is_supported_lang("rust"));
296 assert!(!is_supported_lang("ruby"));
298 assert!(!is_supported_lang(""));
299 }
300
301 #[test]
302 fn is_supported_lang_case_and_whitespace_insensitive() {
303 assert!(is_supported_lang(" Python "));
304 assert!(is_supported_lang("NODE"));
305 }
306
307 #[test]
310 fn normalize_lang_canonical_passthrough() {
311 assert_eq!(normalize_lang("python"), "python");
313 assert_eq!(normalize_lang("node"), "node");
314 assert_eq!(normalize_lang("go"), "go");
315 assert_eq!(normalize_lang("rust"), "rust");
316 assert_eq!(normalize_lang("bun"), "bun");
317 }
318
319 #[test]
320 fn normalize_lang_aliases() {
321 assert_eq!(normalize_lang("js"), "node");
323 assert_eq!(normalize_lang("javascript"), "node");
324 assert_eq!(normalize_lang("rs"), "rust");
325 assert_eq!(normalize_lang("ts"), "bun");
326 assert_eq!(normalize_lang("typescript"), "bun");
327 }
328
329 #[test]
330 fn normalize_lang_case_and_whitespace_insensitive() {
331 assert_eq!(normalize_lang(" JS "), "node");
333 assert_eq!(normalize_lang("JavaScript"), "node");
334 assert_eq!(normalize_lang("\tRS\t"), "rust");
335 assert_eq!(normalize_lang("TypeScript"), "bun");
336 }
337
338 #[test]
339 fn normalize_lang_unregistered_passthrough() {
340 assert_eq!(normalize_lang("ruby"), "ruby");
343 assert_eq!(normalize_lang("Brainfuck"), "brainfuck");
344 assert_eq!(normalize_lang(""), "");
345 }
346
347 #[test]
348 fn is_supported_lang_accepts_aliases() {
349 assert!(is_supported_lang("js"));
351 assert!(is_supported_lang("javascript"));
352 assert!(is_supported_lang("rs"));
353 assert!(is_supported_lang("ts"));
354 assert!(is_supported_lang("typescript"));
355 }
356
357 #[test]
358 fn is_supported_lang_accepts_bun_canonical() {
359 assert!(is_supported_lang("bun"));
361 }
362
363 #[test]
364 fn parse_fence_info_normalizes_alias_to_canonical() {
365 let (lang, runnable, _) = parse_fence_info("js runnable");
368 assert_eq!(lang, "node");
369 assert!(runnable);
370
371 let (lang, _, _) = parse_fence_info("typescript runnable");
372 assert_eq!(lang, "bun");
373
374 let (lang, _, _) = parse_fence_info("rs runnable");
375 assert_eq!(lang, "rust");
376
377 let (lang, _, _) = parse_fence_info("JavaScript runnable");
379 assert_eq!(lang, "node");
380 }
381
382 #[test]
383 fn parse_fence_info_bun_canonical_runs_as_ts() {
384 let (lang, runnable, _) = parse_fence_info("bun runnable");
386 assert_eq!(lang, "bun");
387 assert!(runnable);
388 }
389}