1#![deny(clippy::print_stdout, clippy::print_stderr, clippy::unwrap_used)]
2
3pub mod compression;
6mod error;
7
8use blake3::Hash;
9pub use error::PluginError;
10use std::{
11 io::{Read, Seek},
12 path::{Path, PathBuf},
13};
14pub use tmc_langs_csharp::CSharpPlugin;
15use tmc_langs_framework::{Archive, LanguagePlugin, TmcError};
16pub use tmc_langs_framework::{
17 Compression, ExerciseDesc, ExercisePackagingConfiguration, Language,
18 NothingIsStudentFilePolicy, RunResult, StudentFilePolicy, StyleValidationResult,
19 StyleValidationStrategy,
20};
21#[cfg(not(target_env = "musl"))]
23pub use tmc_langs_java::{AntPlugin, MavenPlugin};
24pub use tmc_langs_make::MakePlugin;
25pub use tmc_langs_notests::NoTestsPlugin;
26pub use tmc_langs_python3::Python3Plugin;
27pub use tmc_langs_r::RPlugin;
28
29pub fn extract_project(
32 compressed_project: impl std::io::Read + std::io::Seek,
33 target_location: &Path,
34 compression: Compression,
35 clean: bool,
36) -> Result<(), PluginError> {
37 let mut archive = Archive::new(compressed_project, compression)?;
38 if let Ok(plugin) = PluginType::from_exercise(target_location) {
39 plugin.extract_project(&mut archive, target_location, clean)?;
40 } else if let Ok(plugin) = PluginType::from_archive(&mut archive) {
41 plugin.extract_project(&mut archive, target_location, clean)?;
42 } else {
43 log::debug!("no matching language plugin found",);
44 archive.extract(target_location)?;
45 }
46 Ok(())
47}
48
49pub fn compress_project(
51 path: &Path,
52 compression: Compression,
53 deterministic: bool,
54 naive: bool,
55 hash: bool,
56 size_limit_mb: u32,
57) -> Result<(Vec<u8>, Option<Hash>), PluginError> {
58 let (compressed, hash) = if naive {
59 compression.compress(path, hash)?
60 } else {
61 let policy = get_student_file_policy(path)?;
62 compression::compress_student_files(
63 policy.as_ref(),
64 path,
65 compression,
66 deterministic,
67 hash,
68 size_limit_mb,
69 )?
70 };
71 Ok((compressed, hash))
72}
73
74pub enum Plugin {
76 CSharp(CSharpPlugin),
77 Make(MakePlugin),
78 #[cfg(not(target_env = "musl"))]
80 Maven(MavenPlugin),
81 NoTests(NoTestsPlugin),
82 Python3(Python3Plugin),
83 R(RPlugin),
84 #[cfg(not(target_env = "musl"))]
86 Ant(AntPlugin),
87}
88
89impl Plugin {
90 pub fn from_exercise(path: &Path) -> Result<Self, PluginError> {
92 let plugin = match PluginType::from_exercise(path)? {
93 PluginType::NoTests => Plugin::NoTests(NoTestsPlugin::new()),
94 PluginType::CSharp => Plugin::CSharp(CSharpPlugin::new()),
95 PluginType::Make => Plugin::Make(MakePlugin::new()),
96 PluginType::Python3 => Plugin::Python3(Python3Plugin::new()),
97 PluginType::R => Plugin::R(RPlugin::new()),
98 #[cfg(not(target_env = "musl"))]
100 PluginType::Maven => Plugin::Maven(MavenPlugin::new()?),
101 #[cfg(not(target_env = "musl"))]
103 PluginType::Ant => Plugin::Ant(AntPlugin::new()?),
104 };
105 Ok(plugin)
106 }
107
108 pub fn clean(&self, path: &Path) -> Result<(), TmcError> {
109 match self {
110 Plugin::CSharp(plugin) => plugin.clean(path),
111 Plugin::Make(plugin) => plugin.clean(path),
112 #[cfg(not(target_env = "musl"))]
114 Plugin::Maven(plugin) => plugin.clean(path),
115 Plugin::NoTests(plugin) => plugin.clean(path),
116 Plugin::Python3(plugin) => plugin.clean(path),
117 Plugin::R(plugin) => plugin.clean(path),
118 #[cfg(not(target_env = "musl"))]
120 Plugin::Ant(plugin) => plugin.clean(path),
121 }
122 }
123
124 pub fn scan_exercise(
125 &self,
126 path: &Path,
127 exercise_name: String,
128 ) -> Result<ExerciseDesc, TmcError> {
129 match self {
130 Plugin::CSharp(plugin) => plugin.scan_exercise(path, exercise_name),
131 Plugin::Make(plugin) => plugin.scan_exercise(path, exercise_name),
132 #[cfg(not(target_env = "musl"))]
134 Plugin::Maven(plugin) => plugin.scan_exercise(path, exercise_name),
135 Plugin::NoTests(plugin) => plugin.scan_exercise(path, exercise_name),
136 Plugin::Python3(plugin) => plugin.scan_exercise(path, exercise_name),
137 Plugin::R(plugin) => plugin.scan_exercise(path, exercise_name),
138 #[cfg(not(target_env = "musl"))]
140 Plugin::Ant(plugin) => plugin.scan_exercise(path, exercise_name),
141 }
142 }
143
144 pub fn run_tests(&self, path: &Path) -> Result<RunResult, TmcError> {
145 match self {
146 Plugin::CSharp(plugin) => plugin.run_tests(path),
147 Plugin::Make(plugin) => plugin.run_tests(path),
148 #[cfg(not(target_env = "musl"))]
150 Plugin::Maven(plugin) => plugin.run_tests(path),
151 Plugin::NoTests(plugin) => plugin.run_tests(path),
152 Plugin::Python3(plugin) => plugin.run_tests(path),
153 Plugin::R(plugin) => plugin.run_tests(path),
154 #[cfg(not(target_env = "musl"))]
156 Plugin::Ant(plugin) => plugin.run_tests(path),
157 }
158 }
159
160 pub fn check_code_style(
161 &self,
162 path: &Path,
163 locale: Language,
164 ) -> Result<Option<StyleValidationResult>, TmcError> {
165 match self {
166 Plugin::CSharp(plugin) => plugin.check_code_style(path, locale),
167 Plugin::Make(plugin) => plugin.check_code_style(path, locale),
168 #[cfg(not(target_env = "musl"))]
170 Plugin::Maven(plugin) => plugin.check_code_style(path, locale),
171 Plugin::NoTests(plugin) => plugin.check_code_style(path, locale),
172 Plugin::Python3(plugin) => plugin.check_code_style(path, locale),
173 Plugin::R(plugin) => plugin.check_code_style(path, locale),
174 #[cfg(not(target_env = "musl"))]
176 Plugin::Ant(plugin) => plugin.check_code_style(path, locale),
177 }
178 }
179}
180
181#[derive(Clone, Copy)]
183pub enum PluginType {
184 CSharp,
185 Make,
186 #[cfg(not(target_env = "musl"))]
188 Maven,
189 NoTests,
190 Python3,
191 R,
192 #[cfg(not(target_env = "musl"))]
194 Ant,
195}
196
197macro_rules! delegate_plugin_type {
198 ($self:ident, $($args:tt)*) => {
199 match $self {
200 Self::CSharp => CSharpPlugin::$($args)*,
201 Self::Make => MakePlugin::$($args)*,
202 #[cfg(not(target_env = "musl"))]
204 Self::Maven => MavenPlugin::$($args)*,
205 Self::NoTests => NoTestsPlugin::$($args)*,
206 Self::Python3 => Python3Plugin::$($args)*,
207 Self::R => RPlugin::$($args)*,
208 #[cfg(not(target_env = "musl"))]
210 Self::Ant => AntPlugin::$($args)*,
211 }
212 };
213}
214
215impl PluginType {
216 pub fn from_exercise(path: &Path) -> Result<Self, PluginError> {
217 let (plugin_name, plugin_type) = if NoTestsPlugin::is_exercise_type_correct(path) {
218 (NoTestsPlugin::PLUGIN_NAME, PluginType::NoTests)
219 } else if CSharpPlugin::is_exercise_type_correct(path) {
220 (CSharpPlugin::PLUGIN_NAME, PluginType::CSharp)
221 } else if MakePlugin::is_exercise_type_correct(path) {
222 (MakePlugin::PLUGIN_NAME, PluginType::Make)
223 } else if Python3Plugin::is_exercise_type_correct(path) {
224 (Python3Plugin::PLUGIN_NAME, PluginType::Python3)
225 } else if RPlugin::is_exercise_type_correct(path) {
226 (RPlugin::PLUGIN_NAME, PluginType::R)
227 } else {
228 #[cfg(not(target_env = "musl"))]
230 if MavenPlugin::is_exercise_type_correct(path) {
231 (MavenPlugin::PLUGIN_NAME, PluginType::Maven)
232 } else if AntPlugin::is_exercise_type_correct(path) {
233 (AntPlugin::PLUGIN_NAME, PluginType::Ant)
235 } else {
236 return Err(PluginError::PluginNotFound(path.to_path_buf()));
237 }
238 #[cfg(target_env = "musl")]
239 return Err(PluginError::PluginNotFound(path.to_path_buf()));
240 };
241 log::info!("Detected project at {} as {}", path.display(), plugin_name);
242 Ok(plugin_type)
243 }
244
245 pub fn from_archive<R: Read + Seek>(archive: &mut Archive<R>) -> Result<Self, PluginError> {
246 let (plugin_name, plugin_type) = if NoTestsPlugin::is_archive_type_correct(archive) {
247 (NoTestsPlugin::PLUGIN_NAME, PluginType::NoTests)
248 } else if CSharpPlugin::is_archive_type_correct(archive) {
249 (CSharpPlugin::PLUGIN_NAME, PluginType::CSharp)
250 } else if MakePlugin::is_archive_type_correct(archive) {
251 (MakePlugin::PLUGIN_NAME, PluginType::Make)
252 } else if Python3Plugin::is_archive_type_correct(archive) {
253 (Python3Plugin::PLUGIN_NAME, PluginType::Python3)
254 } else if RPlugin::is_archive_type_correct(archive) {
255 (RPlugin::PLUGIN_NAME, PluginType::R)
256 } else {
257 #[cfg(not(target_env = "musl"))]
259 if MavenPlugin::is_archive_type_correct(archive) {
260 (MavenPlugin::PLUGIN_NAME, PluginType::Maven)
261 } else if AntPlugin::is_archive_type_correct(archive) {
262 (AntPlugin::PLUGIN_NAME, PluginType::Ant)
264 } else {
265 return Err(PluginError::PluginNotFoundInArchive);
266 }
267 #[cfg(target_env = "musl")]
268 return Err(PluginError::PluginNotFoundInArchive);
269 };
270 log::info!("Detected project in archive as {plugin_name}");
271 Ok(plugin_type)
272 }
273
274 pub fn get_exercise_packaging_configuration(
275 self,
276 exercise_path: &Path,
277 ) -> Result<ExercisePackagingConfiguration, TmcError> {
278 delegate_plugin_type!(self, get_exercise_packaging_configuration(exercise_path))
279 }
280
281 pub fn extract_project<R: Read + Seek>(
282 self,
283 archive: &mut Archive<R>,
284 target_location: &Path,
285 clean: bool,
286 ) -> Result<(), TmcError> {
287 delegate_plugin_type!(self, extract_project(archive, target_location, clean))
288 }
289
290 pub fn extract_student_files(
291 self,
292 compressed_project: impl std::io::Read + std::io::Seek,
293 compression: Compression,
294 target_location: &Path,
295 ) -> Result<(), TmcError> {
296 delegate_plugin_type!(
297 self,
298 extract_student_files(compressed_project, compression, target_location)
299 )
300 }
301
302 pub fn find_project_dir_in_archive<R: Read + Seek>(
303 self,
304 archive: &mut Archive<R>,
305 ) -> Result<PathBuf, TmcError> {
306 delegate_plugin_type!(self, find_project_dir_in_archive(archive))
307 }
308
309 pub fn safe_find_project_dir_in_archive<R: Read + Seek>(
310 self,
311 archive: &mut Archive<R>,
312 ) -> Result<PathBuf, TmcError> {
313 Ok(delegate_plugin_type!(
314 self,
315 safe_find_project_dir_in_archive(archive)
316 ))
317 }
318
319 pub fn get_available_points(self, exercise_path: &Path) -> Result<Vec<String>, TmcError> {
320 delegate_plugin_type!(self, get_available_points(exercise_path))
321 }
322}
323
324pub fn get_student_file_policy(path: &Path) -> Result<Box<dyn StudentFilePolicy>, PluginError> {
325 let policy: Box<dyn StudentFilePolicy> = match PluginType::from_exercise(path)? {
326 PluginType::NoTests => Box::new(<NoTestsPlugin as LanguagePlugin>::StudentFilePolicy::new(
327 path,
328 )?),
329 PluginType::CSharp => Box::new(<CSharpPlugin as LanguagePlugin>::StudentFilePolicy::new(
330 path,
331 )?),
332 PluginType::Make => Box::new(<MakePlugin as LanguagePlugin>::StudentFilePolicy::new(
333 path,
334 )?),
335 PluginType::Python3 => Box::new(<Python3Plugin as LanguagePlugin>::StudentFilePolicy::new(
336 path,
337 )?),
338 PluginType::R => Box::new(<RPlugin as LanguagePlugin>::StudentFilePolicy::new(path)?),
339 #[cfg(not(target_env = "musl"))]
341 PluginType::Maven => Box::new(<MavenPlugin as LanguagePlugin>::StudentFilePolicy::new(
342 path,
343 )?),
344 #[cfg(not(target_env = "musl"))]
346 PluginType::Ant => Box::new(<AntPlugin as LanguagePlugin>::StudentFilePolicy::new(path)?),
347 };
348 Ok(policy)
349}