1use shellexpand::full_with_context_no_errors;
16
17#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
20pub enum JavaError {
21 #[error("inline_java: I/O error: {0}")]
24 Io(String),
25
26 #[error("inline_java: javac compilation failed:\n{0}")]
29 CompilationFailed(String),
30
31 #[error("inline_java: java runtime failed:\n{0}")]
34 RuntimeFailed(String),
35
36 #[error("inline_java: Java String output is not valid UTF-8: {0}")]
39 InvalidUtf8(#[from] std::string::FromUtf8Error),
40
41 #[error("inline_java: Java char is not a valid Unicode scalar value")]
44 InvalidChar,
45}
46
47#[must_use]
63pub fn expand_java_args(raw: &str) -> Vec<String> {
64 if raw.is_empty() {
65 return Vec::new();
66 }
67 let expanded = full_with_context_no_errors(
68 raw,
69 || std::env::var("HOME").ok(),
70 |var| std::env::var(var).ok(),
71 );
72 split_args(&expanded)
73}
74
75fn inject_classpath(args: &mut Vec<String>, extra_cp: &str) {
79 const SPACE_FLAGS: &[&str] = &["-cp", "-classpath", "--class-path"];
80 for i in 0..args.len() {
81 if SPACE_FLAGS.contains(&args[i].as_str()) && i + 1 < args.len() {
82 args[i + 1].push(CP_SEP);
83 args[i + 1].push_str(extra_cp);
84 return;
85 }
86 if let Some(val) = args[i].strip_prefix("--class-path=") {
87 args[i] = format!("--class-path={val}{CP_SEP}{extra_cp}");
88 return;
89 }
90 }
91 args.push("-cp".to_owned());
92 args.push(extra_cp.to_owned());
93}
94
95fn split_args(s: &str) -> Vec<String> {
98 let mut args: Vec<String> = Vec::new();
99 let mut cur = String::new();
100 let mut in_single = false;
101 let mut in_double = false;
102
103 for ch in s.chars() {
104 match ch {
105 '\'' if !in_double => in_single = !in_single,
106 '"' if !in_single => in_double = !in_double,
107 ' ' | '\t' if !in_single && !in_double => {
108 if !cur.is_empty() {
109 args.push(std::mem::take(&mut cur));
110 }
111 }
112 _ => cur.push(ch),
113 }
114 }
115 if !cur.is_empty() {
116 args.push(cur);
117 }
118 args
119}
120
121#[cfg(not(windows))]
123const CP_SEP: char = ':';
124#[cfg(windows)]
125const CP_SEP: char = ';';
126
127pub fn detect_java_version() -> Result<String, JavaError> {
138 let output = std::process::Command::new("javac")
139 .arg("-version")
140 .output()
141 .map_err(|e| JavaError::Io(format!("failed to run `javac -version`: {e}")))?;
142 let stdout = String::from_utf8_lossy(&output.stdout);
145 let stderr = String::from_utf8_lossy(&output.stderr);
146 let raw = if stdout.trim().is_empty() {
147 &*stderr
148 } else {
149 &*stdout
150 };
151 let version_str = raw.trim().strip_prefix("javac ").unwrap_or(raw.trim());
152 let major = version_str
153 .split('.')
154 .next()
155 .and_then(|s| s.parse::<u32>().ok())
156 .ok_or_else(|| {
157 JavaError::Io(format!(
158 "could not parse major version from `javac -version` output: {raw:?}"
159 ))
160 })?;
161 Ok(major.to_string())
162}
163
164#[must_use]
173pub fn base_cache_dir() -> std::path::PathBuf {
174 if let Ok(v) = std::env::var("INLINE_JAVA_CACHE_DIR")
175 && !v.is_empty()
176 {
177 return std::path::PathBuf::from(v);
178 }
179 if let Some(cache) = dirs::cache_dir() {
180 return cache.join("inline_java");
181 }
182 std::env::temp_dir().join("inline_java")
183}
184
185#[allow(clippy::similar_names)]
210pub fn cache_dir(
211 class_name: &str,
212 java_class: &str,
213 javac_raw: &str,
214 java_raw: &str,
215) -> Result<std::path::PathBuf, JavaError> {
216 use std::collections::hash_map::DefaultHasher;
217 use std::hash::{Hash, Hasher};
218
219 let mut h = DefaultHasher::new();
220 java_class.hash(&mut h);
221 expand_java_args(javac_raw).hash(&mut h); std::env::current_dir().ok().hash(&mut h); java_raw.hash(&mut h);
224 detect_java_version()?.hash(&mut h); let hex = format!("{:016x}", h.finish());
227 Ok(base_cache_dir().join(format!("{class_name}_{hex}")))
228}
229
230#[allow(clippy::similar_names)]
266pub fn run_java(
267 class_name: &str,
268 filename: &str,
269 java_class: &str,
270 full_class_name: &str,
271 javac_raw: &str,
272 java_raw: &str,
273 stdin_bytes: &[u8],
274) -> Result<Vec<u8>, JavaError> {
275 use std::io::Write;
276 use std::process::Stdio;
277
278 let tmp_dir = cache_dir(class_name, java_class, javac_raw, java_raw)?;
279 let javac_extra = expand_java_args(javac_raw);
280 let mut java_extra = expand_java_args(java_raw);
281 inject_classpath(&mut java_extra, &tmp_dir.to_string_lossy());
282
283 if !tmp_dir.join(".done").exists() {
284 std::fs::create_dir_all(&tmp_dir).map_err(|e| JavaError::Io(e.to_string()))?;
285
286 let lock_file = std::fs::OpenOptions::new()
287 .create(true)
288 .truncate(false)
289 .write(true)
290 .open(tmp_dir.join(".lock"))
291 .map_err(|e| JavaError::Io(e.to_string()))?;
292 let mut lock = fd_lock::RwLock::new(lock_file);
293 let _guard = lock.write().map_err(|e| JavaError::Io(e.to_string()))?;
294
295 if !tmp_dir.join(".done").exists() {
296 let src = tmp_dir.join(filename);
297 std::fs::write(&src, java_class).map_err(|e| JavaError::Io(e.to_string()))?;
298
299 let mut cmd = std::process::Command::new("javac");
300 for arg in &javac_extra {
301 cmd.arg(arg);
302 }
303 let out = cmd
304 .arg("-d")
305 .arg(&tmp_dir)
306 .arg(&src)
307 .output()
308 .map_err(|e| JavaError::Io(e.to_string()))?;
309 if !out.status.success() {
310 return Err(JavaError::CompilationFailed(
311 String::from_utf8_lossy(&out.stderr).into_owned(),
312 ));
313 }
314
315 std::fs::write(tmp_dir.join(".done"), b"").map_err(|e| JavaError::Io(e.to_string()))?;
316 }
317 }
318
319 let mut cmd = std::process::Command::new("java");
320 for arg in &java_extra {
321 cmd.arg(arg);
322 }
323 let mut child = cmd
324 .arg(full_class_name)
325 .stdin(Stdio::piped())
326 .stdout(Stdio::piped())
327 .stderr(Stdio::piped())
328 .spawn()
329 .map_err(|e| JavaError::Io(e.to_string()))?;
330
331 if stdin_bytes.is_empty() {
333 drop(child.stdin.take());
335 } else if let Some(mut stdin_handle) = child.stdin.take() {
336 stdin_handle
337 .write_all(stdin_bytes)
338 .map_err(|e| JavaError::Io(e.to_string()))?;
339 }
340
341 let out = child
342 .wait_with_output()
343 .map_err(|e| JavaError::Io(e.to_string()))?;
344
345 if !out.status.success() {
346 return Err(JavaError::RuntimeFailed(
347 String::from_utf8_lossy(&out.stderr).into_owned(),
348 ));
349 }
350
351 Ok(out.stdout)
352}
353
354#[cfg(test)]
355mod tests {
356 use super::cache_dir;
357
358 #[test]
363 fn cache_dir_idempotent() {
364 let a = cache_dir("MyClass", "class body", "-cp /usr/lib", "-verbose").unwrap();
365 let b = cache_dir("MyClass", "class body", "-cp /usr/lib", "-verbose").unwrap();
366 assert_eq!(
367 a, b,
368 "cache_dir must return the same path for identical args"
369 );
370 }
371
372 #[test]
377 fn cache_dir_differs_for_different_javac_raw() {
378 let a = cache_dir("MyClass", "class body", "-cp /usr/lib/foo", "").unwrap();
379 let b = cache_dir("MyClass", "class body", "-cp /usr/lib/bar", "").unwrap();
380 assert_ne!(
381 a, b,
382 "cache_dir must differ when javac_raw expands to different args"
383 );
384 }
385
386 #[test]
390 fn cache_dir_differs_for_different_java_class() {
391 let a = cache_dir("MyClass", "class body A", "", "").unwrap();
392 let b = cache_dir("MyClass", "class body B", "", "").unwrap();
393 assert_ne!(a, b, "cache_dir must differ when java_class differs");
394 }
395
396 #[test]
400 fn cache_dir_differs_for_different_java_raw() {
401 let a = cache_dir("MyClass", "class body", "", "-Xmx256m").unwrap();
402 let b = cache_dir("MyClass", "class body", "", "-Xmx512m").unwrap();
403 assert_ne!(a, b, "cache_dir must differ when java_raw differs");
404 }
405
406 #[test]
411 fn cache_dir_path_structure() {
412 let result = cache_dir("InlineJava_abc123", "src", "", "").unwrap();
413 let base = super::base_cache_dir();
414 assert!(
415 result.starts_with(&base),
416 "cache_dir result must be under base_cache_dir ({}); got: {}",
417 base.display(),
418 result.display()
419 );
420 let file_name = result.file_name().unwrap().to_string_lossy();
421 assert!(
422 file_name.starts_with("InlineJava_abc123_"),
423 "cache_dir result filename must start with the class name; got: {file_name}"
424 );
425 }
426
427 #[test]
432 fn detect_java_version_returns_major() {
433 let version = super::detect_java_version().expect("javac must be on PATH");
434 assert!(
435 version.parse::<u32>().is_ok(),
436 "version string must be a plain integer (major); got: {version:?}"
437 );
438 }
439
440 #[test]
444 fn base_cache_dir_respects_env_var() {
445 unsafe { std::env::set_var("INLINE_JAVA_CACHE_DIR", "/custom/cache") };
446 let base = super::base_cache_dir();
447 unsafe { std::env::remove_var("INLINE_JAVA_CACHE_DIR") };
448 assert_eq!(base, std::path::PathBuf::from("/custom/cache"));
449 }
450}