Skip to content

Commit 0f5e013

Browse files
committed
improved unzip logic: project dir detection, student file detection
1 parent 1e56793 commit 0f5e013

File tree

29 files changed

+505
-71
lines changed

29 files changed

+505
-71
lines changed

tmc-langs-csharp/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ tmc-langs-framework = { path = "../tmc-langs-framework" }
99
walkdir = "2"
1010
dirs = "3"
1111
thiserror = "1"
12-
zip = "0.5"
1312
serde_json = "1"
1413
log = "0.4"
1514
serde = { version = "1", features = ["derive"] }

tmc-langs-csharp/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use std::path::PathBuf;
33
use std::process::ExitStatus;
44
use thiserror::Error;
5-
use tmc_langs_framework::TmcError;
5+
use tmc_langs_framework::{zip, TmcError};
66

77
#[derive(Debug, Error)]
88
pub enum CSharpError {

tmc-langs-csharp/src/plugin.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ use tmc_langs_framework::{
1010
ExerciseDesc, RunResult, RunStatus, Strategy, TestDesc, TestResult, ValidationResult,
1111
},
1212
plugin::Language,
13+
zip::ZipArchive,
1314
CommandWithTimeout, LanguagePlugin, OutputWithTimeout, TmcError,
1415
};
1516

1617
use std::collections::HashMap;
1718
use std::env;
18-
use std::ffi::OsString;
19+
use std::ffi::{OsStr, OsString};
1920
use std::fs::{self, File};
20-
use std::io::{BufReader, Cursor, Read, Write};
21+
use std::io::{BufReader, Cursor, Read, Seek, Write};
2122
use std::path::{Path, PathBuf};
2223
use std::process::Command;
2324
use std::time::Duration;
2425
use walkdir::WalkDir;
25-
use zip::ZipArchive;
2626

2727
const TMC_CSHARP_RUNNER: &[u8] = include_bytes!("../tmc-csharp-runner-1.0.1.zip");
2828

@@ -122,12 +122,33 @@ impl LanguagePlugin for CSharpPlugin {
122122
// checks the directories in src for csproj files
123123
fn is_exercise_type_correct(path: &Path) -> bool {
124124
WalkDir::new(path.join("src"))
125+
.min_depth(2)
125126
.max_depth(2)
126127
.into_iter()
127128
.filter_map(|e| e.ok())
128129
.any(|e| e.path().extension() == Some(&OsString::from("csproj")))
129130
}
130131

132+
/// Finds .csproj files and checks whether they are in a X/src/ directory, returning X if so.
133+
fn find_project_dir_in_zip<R: Read + Seek>(
134+
zip_archive: &mut ZipArchive<R>,
135+
) -> Result<PathBuf, TmcError> {
136+
for i in 0..zip_archive.len() {
137+
let file = zip_archive.by_index(i)?;
138+
let file_path = file.sanitized_name();
139+
if file_path.extension() == Some(OsStr::new("csproj")) {
140+
if let Some(csproj_parent) = file_path.parent().and_then(Path::parent) {
141+
if csproj_parent.file_name() == Some(OsStr::new("src")) {
142+
if let Some(src_parent) = csproj_parent.parent() {
143+
return Ok(src_parent.to_path_buf());
144+
}
145+
}
146+
}
147+
}
148+
}
149+
Err(TmcError::NoProjectDirInZip)
150+
}
151+
131152
fn get_student_file_policy(project_path: &Path) -> Self::StudentFilePolicy {
132153
Self::StudentFilePolicy::new(project_path.to_path_buf())
133154
}
@@ -306,6 +327,22 @@ mod test {
306327
assert!(!is);
307328
}
308329

330+
#[test]
331+
fn finds_project_dir_in_zip() {
332+
let file = File::open("tests/data/Zipped.zip").unwrap();
333+
let mut zip = ZipArchive::new(file).unwrap();
334+
let dir = CSharpPlugin::find_project_dir_in_zip(&mut zip).unwrap();
335+
assert_eq!(dir, Path::new("Outer/Inner/PassingProject"))
336+
}
337+
338+
#[test]
339+
fn no_project_dir_in_zip() {
340+
let file = File::open("tests/data/test.zip").unwrap();
341+
let mut zip = ZipArchive::new(file).unwrap();
342+
let dir = CSharpPlugin::find_project_dir_in_zip(&mut zip);
343+
assert!(dir.is_err())
344+
}
345+
309346
#[test]
310347
fn scans_exercise() {
311348
init();
2.93 KB
Binary file not shown.

tmc-langs-csharp/tests/data/test.zip

1.18 KB
Binary file not shown.

tmc-langs-framework/src/error.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::io::zip;
1+
use crate::io::tmc_zip;
22

33
use std::path::PathBuf;
44
use thiserror::Error;
@@ -45,6 +45,10 @@ pub enum TmcError {
4545
SetPermissions(PathBuf, #[source] std::io::Error),
4646
#[error("Invalid parameter value: {0}")]
4747
InvalidParam(String),
48+
#[error("File {0} not in given project root {1}")]
49+
FileNotInProject(PathBuf, PathBuf),
50+
#[error("Path {0} is not absolute")]
51+
PathNotAbsolute(PathBuf),
4852

4953
#[error("Path {0} contained invalid UTF8")]
5054
UTF8(PathBuf),
@@ -67,7 +71,7 @@ pub enum TmcError {
6771
#[error(transparent)]
6872
YamlDeserialization(#[from] serde_yaml::Error),
6973
#[error(transparent)]
70-
ZipError(#[from] zip::ZipError),
74+
ZipError(#[from] tmc_zip::ZipError),
7175
#[error(transparent)]
7276
WalkDir(#[from] walkdir::Error),
7377
}

tmc-langs-framework/src/io.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
33
pub mod file_util;
44
pub mod submission_processing;
5-
pub mod zip;
5+
pub mod tmc_zip;

tmc-langs-framework/src/io/file_util.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(source: P, target: Q) -> Result<(),
9999
entry_path.display(),
100100
target.display()
101101
);
102+
if let Some(parent) = target.parent() {
103+
fs::create_dir_all(parent)
104+
.map_err(|e| TmcError::CreateDir(entry.path().to_path_buf(), e))?;
105+
}
106+
println!("{} {}", entry_path.display(), entry_path.exists());
107+
println!(
108+
"{} {}",
109+
target.parent().unwrap().display(),
110+
target.parent().unwrap().exists()
111+
);
102112
std::fs::copy(&entry_path, &target).map_err(|e| {
103113
TmcError::FileCopy(entry_path.to_path_buf(), target.to_path_buf(), e)
104114
})?;

tmc-langs-framework/src/io/zip.rs renamed to tmc-langs-framework/src/io/tmc_zip.rs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Contains functions for zipping and unzipping projects.
22
33
use crate::policy::StudentFilePolicy;
4-
use crate::{Result, TmcError};
4+
use crate::{LanguagePlugin, Result, TmcError};
55
use std::collections::HashSet;
66
use std::fs::{self, File};
77
use std::io::{Cursor, Read, Seek, Write};
@@ -48,8 +48,15 @@ pub fn zip(policy: Box<dyn StudentFilePolicy>, root_directory: &Path) -> Result<
4848
Ok(cursor.into_inner())
4949
}
5050

51-
/// Finds a project directory in the given zip and unzips it.
52-
pub fn unzip<P: StudentFilePolicy>(policy: P, zip: &Path, target: &Path) -> Result<()> {
51+
// todo: remove
52+
/// Finds a project directory in the given zip and unzips it according to the given student policy. Also cleans unnecessary non-student files.
53+
///
54+
/// First a project directory is found within the directory. Only files within the project directory are unzipped.
55+
///
56+
pub fn unzip<P>(policy: P, zip: &Path, target: &Path) -> Result<()>
57+
where
58+
P: StudentFilePolicy,
59+
{
5360
log::debug!("Unzipping {} to {}", zip.display(), target.display());
5461

5562
let file = File::open(zip).map_err(|e| TmcError::OpenFile(zip.to_path_buf(), e))?;
@@ -60,33 +67,37 @@ pub fn unzip<P: StudentFilePolicy>(policy: P, zip: &Path, target: &Path) -> Resu
6067

6168
let tmc_project_yml = policy.get_tmc_project_yml()?;
6269

63-
let mut unzipped_paths = HashSet::new();
70+
// for each file in the zip, contains its path if unzipped
71+
// used to clean non-student files not in the zip later
72+
let mut unzip_paths = HashSet::new();
6473

6574
for i in 0..zip_archive.len() {
6675
let mut file = zip_archive.by_index(i)?;
6776
let file_path = file.sanitized_name();
68-
if !file_path.starts_with(&project_dir) {
69-
log::trace!("skip {}, not in project dir", file.name());
70-
continue;
71-
}
72-
let relative = file_path.strip_prefix(&project_dir).unwrap();
77+
let relative = match file_path.strip_prefix(&project_dir) {
78+
Ok(relative) => relative,
79+
_ => {
80+
log::trace!("skip {}, not in project dir", file.name());
81+
continue;
82+
}
83+
};
7384
let path_in_target = target.join(&relative);
7485
log::trace!("processing {:?} -> {:?}", file_path, path_in_target);
7586

7687
if file.is_dir() {
7788
log::trace!("creating {:?}", path_in_target);
7889
fs::create_dir_all(&path_in_target)
7990
.map_err(|e| TmcError::CreateDir(path_in_target.clone(), e))?;
80-
unzipped_paths.insert(
91+
unzip_paths.insert(
8192
path_in_target
8293
.canonicalize()
83-
.map_err(|e| TmcError::Canonicalize(path_in_target, e))?,
94+
.map_err(|e| TmcError::Canonicalize(path_in_target.clone(), e))?,
8495
);
8596
} else {
8697
let mut write = true;
8798
let mut file_contents = vec![];
8899
file.read_to_end(&mut file_contents)
89-
.map_err(|e| TmcError::FileRead(file_path, e))?;
100+
.map_err(|e| TmcError::FileRead(file_path.clone(), e))?;
90101
// always overwrite .tmcproject.yml
91102
if path_in_target.exists()
92103
&& !path_in_target
@@ -102,42 +113,44 @@ pub fn unzip<P: StudentFilePolicy>(policy: P, zip: &Path, target: &Path) -> Resu
102113
.map_err(|e| TmcError::FileRead(path_in_target.clone(), e))?;
103114
if file_contents == target_file_contents
104115
|| (policy.is_student_file(&path_in_target, &target, &tmc_project_yml)?
105-
&& !policy.is_updating_forced(&path_in_target, &tmc_project_yml)?)
116+
&& !policy.is_updating_forced(&relative, &tmc_project_yml)?)
106117
{
107118
write = false;
108119
}
109120
}
110121
if write {
111122
log::trace!("writing to {}", path_in_target.display());
112-
if let Some(res) = path_in_target.parent() {
113-
fs::create_dir_all(res)
114-
.map_err(|e| TmcError::CreateDir(res.to_path_buf(), e))?;
123+
if let Some(parent) = path_in_target.parent() {
124+
fs::create_dir_all(parent)
125+
.map_err(|e| TmcError::CreateDir(parent.to_path_buf(), e))?;
115126
}
116127
let mut overwrite_target = File::create(&path_in_target)
117128
.map_err(|e| TmcError::CreateFile(path_in_target.clone(), e))?;
118129
overwrite_target
119130
.write_all(&file_contents)
120131
.map_err(|e| TmcError::Write(path_in_target.clone(), e))?;
121-
unzipped_paths.insert(
122-
path_in_target
123-
.canonicalize()
124-
.map_err(|e| TmcError::Canonicalize(path_in_target, e))?,
125-
);
126132
}
127133
}
134+
unzip_paths.insert(
135+
path_in_target
136+
.canonicalize()
137+
.map_err(|e| TmcError::Canonicalize(path_in_target.clone(), e))?,
138+
);
128139
}
129140

130141
// delete non-student files that were not in zip
131142
log::debug!("deleting non-student files not in zip");
143+
log::debug!("{:?}", unzip_paths);
132144
for entry in WalkDir::new(target).into_iter().filter_map(|e| e.ok()) {
133-
if !unzipped_paths.contains(
145+
if !unzip_paths.contains(
134146
&entry
135147
.path()
136148
.canonicalize()
137149
.map_err(|e| TmcError::Canonicalize(entry.path().to_path_buf(), e))?,
138150
) && (policy.is_updating_forced(entry.path(), &tmc_project_yml)?
139-
|| !policy.is_student_file(entry.path(), &project_dir, &tmc_project_yml)?)
151+
|| !policy.is_student_file(entry.path(), &target, &tmc_project_yml)?)
140152
{
153+
log::debug!("rm {} {}", entry.path().display(), target.display());
141154
if entry.path().is_dir() {
142155
// delete if empty
143156
if WalkDir::new(entry.path()).max_depth(1).into_iter().count() == 1 {
@@ -211,6 +224,7 @@ fn contains_tmcnosubmit(entry: &DirEntry) -> bool {
211224

212225
#[cfg(test)]
213226
mod test {
227+
/*
214228
use super::*;
215229
use crate::policy::EverythingIsStudentFilePolicy;
216230
use std::collections::HashSet;
@@ -319,4 +333,5 @@ mod test {
319333
.unwrap();
320334
assert!(temp.path().join("src").exists());
321335
}
336+
*/
322337
}

tmc-langs-framework/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod policy;
99
pub use error::TmcError;
1010
pub use plugin::LanguagePlugin;
1111
pub use policy::StudentFilePolicy;
12+
pub use zip;
1213

1314
use domain::TmcProjectYml;
1415
use std::io::Read;

0 commit comments

Comments
 (0)