1#![deny(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_used)]
2
3#[cfg(target_env = "musl")]
6compile_error!("The Java plugin does not work on musl");
7
8mod ant_plugin;
9mod ant_policy;
10mod error;
11mod java_plugin;
12mod maven_plugin;
13mod maven_policy;
14
15pub use self::{
16 ant_plugin::AntPlugin, ant_policy::AntStudentFilePolicy, error::JavaError,
17 maven_plugin::MavenPlugin, maven_policy::MavenStudentFilePolicy,
18};
19use j4rs::{ClasspathEntry, Instance, InvocationArg, Jvm, JvmBuilder, errors::J4RsError};
20use serde::Deserialize;
21use std::{fmt::Display, path::PathBuf};
22use tempfile::TempPath;
23use tmc_langs_framework::ExitStatus;
24use tmc_langs_util::file_util;
25
26#[cfg(windows)]
27const SEPARATOR: &str = ";";
28#[cfg(not(windows))]
29const SEPARATOR: &str = ":";
30
31const TMC_JUNIT_RUNNER_BYTES: &[u8] = include_bytes!("../deps/tmc-junit-runner-0.2.8.jar");
33const TMC_CHECKSTYLE_RUNNER_BYTES: &[u8] =
34 include_bytes!("../deps/tmc-checkstyle-runner-3.0.3-20200520.064542-3.jar");
35const J4RS_BYTES: &[u8] = include_bytes!("../deps/j4rs-0.22.0-jar-with-dependencies.jar");
36
37struct JvmWrapper {
38 jvm: Jvm,
39 stdout_path: TempPath,
40 stderr_path: TempPath,
41}
42
43impl JvmWrapper {
44 pub fn with<R>(&self, f: impl FnOnce(&Jvm) -> Result<R, J4RsError>) -> Result<R, JavaError> {
45 let res = match f(&self.jvm) {
46 Ok(res) => res,
47 Err(err) => {
48 let stdout = file_util::read_file_to_string_lossy(&self.stdout_path)?;
49 let stderr = file_util::read_file_to_string_lossy(&self.stderr_path)?;
50 let _ = file_util::create_file(&self.stdout_path);
52 let _ = file_util::create_file(&self.stderr_path);
53 return Err(JavaError::J4rs {
54 stdout: Some(stdout),
55 stderr: Some(stderr),
56 source: err,
57 });
58 }
59 };
60 Ok(res)
61 }
62}
63
64fn tmc_dir() -> Result<PathBuf, JavaError> {
65 let home_dir = dirs::cache_dir().ok_or(JavaError::HomeDir)?;
66 Ok(home_dir.join("tmc"))
67}
68
69fn get_junit_runner_path() -> Result<PathBuf, JavaError> {
71 let jar_dir = tmc_dir()?;
72
73 let junit_path = jar_dir.join("tmc-junit-runner.jar");
74 if let Ok(bytes) = file_util::read_file(&junit_path) {
75 if TMC_CHECKSTYLE_RUNNER_BYTES != bytes.as_slice() {
76 log::debug!("updating tmc junit runner jar");
77 file_util::write_to_file(TMC_JUNIT_RUNNER_BYTES, &junit_path)?;
78 }
79 } else {
80 log::debug!("failed to read tmc junit runner jar, writing");
81 file_util::write_to_file(TMC_JUNIT_RUNNER_BYTES, &junit_path)?;
82 }
83 Ok(junit_path)
84}
85
86fn get_checkstyle_runner_path() -> Result<PathBuf, JavaError> {
88 let jar_dir = tmc_dir()?;
89
90 let checkstyle_path = jar_dir.join("tmc-checkstyle-runner.jar");
91 if let Ok(bytes) = file_util::read_file(&checkstyle_path) {
92 if TMC_CHECKSTYLE_RUNNER_BYTES != bytes.as_slice() {
93 log::debug!("updating checkstyle runner jar");
94 file_util::write_to_file(TMC_CHECKSTYLE_RUNNER_BYTES, &checkstyle_path)?;
95 }
96 } else {
97 log::debug!("failed to read checkstyle runner jar, writing");
98 file_util::write_to_file(TMC_CHECKSTYLE_RUNNER_BYTES, &checkstyle_path)?;
99 }
100 Ok(checkstyle_path)
101}
102
103fn initialize_jassets() -> Result<PathBuf, JavaError> {
105 let jar_dir = tmc_dir()?;
106 let jassets_dir = jar_dir.join("jassets");
107
108 let j4rs_path = jassets_dir.join("j4rs.jar");
109
110 if let Ok(bytes) = file_util::read_file(&j4rs_path) {
111 if J4RS_BYTES != bytes.as_slice() {
112 log::debug!("updating j4rs jar");
113 file_util::write_to_file(J4RS_BYTES, &j4rs_path)?;
114 }
115 } else {
116 log::debug!("failed to read j4rs jar, writing");
117 file_util::write_to_file(J4RS_BYTES, &j4rs_path)?;
118 }
119 Ok(j4rs_path)
120}
121
122fn instantiate_jvm() -> Result<JvmWrapper, JavaError> {
124 let junit_runner_path = crate::get_junit_runner_path()?;
125 log::debug!("junit runner at {}", junit_runner_path.display());
126 let junit_runner_path = junit_runner_path
127 .to_str()
128 .ok_or_else(|| JavaError::InvalidUtf8Path(junit_runner_path.clone()))?;
129 let junit_runner = ClasspathEntry::new(junit_runner_path);
130
131 let checkstyle_runner_path = crate::get_checkstyle_runner_path()?;
132 log::debug!("checkstyle runner at {}", checkstyle_runner_path.display());
133 let checkstyle_runner_path = checkstyle_runner_path
134 .to_str()
135 .ok_or_else(|| JavaError::InvalidUtf8Path(checkstyle_runner_path.clone()))?;
136 let checkstyle_runner = ClasspathEntry::new(checkstyle_runner_path);
137
138 let j4rs_path = crate::initialize_jassets()?;
139 log::debug!("initialized jassets at {}", j4rs_path.display());
140
141 let tmc_dir = tmc_dir()?;
142
143 let catch = std::panic::catch_unwind(|| -> Result<Jvm, JavaError> {
145 let jvm = JvmBuilder::new()
146 .with_base_path(
147 tmc_dir
148 .to_str()
149 .ok_or_else(|| JavaError::InvalidUtf8Path(tmc_dir.clone()))?,
150 )
151 .classpath_entry(junit_runner)
152 .classpath_entry(checkstyle_runner)
153 .skip_setting_native_lib()
154 .java_opt(j4rs::JavaOpt::new("-Dfile.encoding=UTF-8"))
155 .build()
156 .map_err(JavaError::j4rs)?;
157 Ok(jvm)
158 });
159 let jvm = match catch {
160 Ok(jvm_result) => jvm_result?,
161 Err(jvm_panic) => {
162 let error_message = if let Some(string) = jvm_panic.downcast_ref::<&str>() {
164 string.to_string()
165 } else if let Ok(string) = jvm_panic.downcast::<String>() {
166 *string
167 } else {
168 "J4rs panicked without an error message".to_string()
169 };
170
171 return Err(JavaError::J4rsPanic(error_message));
172 }
173 };
174
175 let stdout_path = file_util::named_temp_file()?.into_temp_path();
177 let out = create_print_stream(
178 &jvm,
179 stdout_path
180 .to_str()
181 .expect("Temp path shouldn't contain invalid UTF-8"),
182 )?;
183 jvm.invoke_static("java.lang.System", "setOut", &[InvocationArg::from(out)])
184 .map_err(JavaError::j4rs)?;
185 let stderr_path = file_util::named_temp_file()?.into_temp_path();
186 let err = create_print_stream(
187 &jvm,
188 stderr_path
189 .to_str()
190 .expect("Temp path shouldn't contain invalid UTF-8"),
191 )?;
192 jvm.invoke_static("java.lang.System", "setErr", &[InvocationArg::from(err)])
193 .map_err(JavaError::j4rs)?;
194
195 match jvm.invoke_static(
197 "java.lang.System",
198 "getProperty",
199 &[InvocationArg::try_from("java.version".to_string()).expect("should never fail")],
200 ) {
201 Ok(version) => match jvm.to_rust::<String>(version).map_err(JavaError::j4rs) {
202 Ok(version) => log::info!("Java version: {version}"),
203 Err(err) => log::error!("Error while trying to convert Java version: {err}"),
204 },
205 Err(err) => log::error!("Error while trying to read Java version: {err}"),
206 }
207
208 Ok(JvmWrapper {
209 jvm,
210 stdout_path,
211 stderr_path,
212 })
213}
214
215fn create_print_stream(jvm: &Jvm, path: &str) -> Result<Instance, JavaError> {
216 let file = jvm
217 .create_instance(
218 "java.io.File",
219 &[InvocationArg::try_from(path).map_err(JavaError::j4rs)?],
220 )
221 .map_err(JavaError::j4rs)?;
222 jvm.invoke(&file, "createNewFile", InvocationArg::empty())
223 .map_err(JavaError::j4rs)?;
224 let file_output_stream = jvm
225 .create_instance("java.io.FileOutputStream", &[InvocationArg::from(file)])
226 .map_err(JavaError::j4rs)?;
227 let print_stream = jvm
228 .create_instance(
229 "java.io.PrintStream",
230 &[InvocationArg::from(file_output_stream)],
231 )
232 .map_err(JavaError::j4rs)?;
233 Ok(print_stream)
234}
235
236#[derive(Deserialize, Debug)]
237#[serde(rename_all = "camelCase")]
238struct TestMethod {
239 class_name: String,
240 method_name: String,
241 points: Vec<String>,
242}
243
244#[derive(Debug)]
245struct CompileResult {
246 pub status_code: ExitStatus,
247 pub stdout: Vec<u8>,
248 pub stderr: Vec<u8>,
249}
250
251#[derive(Debug)]
252struct TestRun {
253 pub test_results: PathBuf,
254 pub stdout: Vec<u8>,
255 pub stderr: Vec<u8>,
256}
257
258#[derive(Debug, Deserialize)]
259#[serde(rename_all = "camelCase")]
260struct TestCase {
261 class_name: String,
262 method_name: String,
263 point_names: Vec<String>,
264 status: TestCaseStatus,
265 message: Option<String>,
266 exception: Option<CaughtException>,
267}
268
269#[derive(Debug, Deserialize)]
270#[serde(rename_all = "camelCase")]
271struct CaughtException {
272 message: Option<String>,
275 stack_trace: Vec<StackTrace>,
276 }
279
280#[derive(Debug, Deserialize, PartialEq, Eq)]
281#[serde(rename_all = "UPPERCASE")]
282enum TestCaseStatus {
283 Passed,
284 Failed,
285 Running,
286 NotStarted,
287}
288
289#[derive(Debug, Deserialize)]
290#[serde(rename_all = "camelCase")]
291struct StackTrace {
292 declaring_class: String,
293 file_name: Option<String>,
294 line_number: i32,
295 method_name: String,
296}
297
298impl Display for StackTrace {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 let start = self
301 .file_name
302 .as_ref()
303 .map(|f| format!("{}:{}", f, self.line_number))
304 .unwrap_or_else(|| self.line_number.to_string());
305 write!(
308 f,
309 "{}: {}.{}",
310 start, self.declaring_class, self.method_name
311 )
312 }
313}