Skip to content

Commit 984a243

Browse files
committed
Make sure we don't use .tmcproject.yml from submissions
1 parent d937c43 commit 984a243

File tree

2 files changed

+127
-11
lines changed

2 files changed

+127
-11
lines changed

crates/tmc-langs-framework/src/plugin.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ pub trait LanguagePlugin {
217217
/// Extracts student files from the compressed project.
218218
/// It finds the project dir from the zip and extracts the student files from there.
219219
/// Overwrites all files.
220+
/// Important: does not extract .tmcproject.yml from the students' submission as they control that file and they could use it to modify the test files.
220221
fn extract_student_files(
221222
compressed_project: impl Read + Seek,
222223
compression: Compression,
@@ -230,15 +231,6 @@ pub trait LanguagePlugin {
230231
let project_dir = Self::find_project_dir_in_archive(&mut archive)?;
231232
log::debug!("Project directory in archive: {}", project_dir.display());
232233

233-
// extract config file if any
234-
let tmc_project_yml_path = project_dir.join(".tmcproject.yml");
235-
let tmc_project_yml_path = tmc_project_yml_path
236-
.to_str()
237-
.ok_or_else(|| TmcError::ProjectDirInvalidUtf8(project_dir.clone()))?;
238-
if let Ok(mut file) = archive.by_path(tmc_project_yml_path) {
239-
let target_path = target_location.join(".tmcproject.yml");
240-
file_util::read_to_file(&mut file, target_path)?;
241-
}
242234
let policy = Self::StudentFilePolicy::new(target_location)?;
243235

244236
let mut iter = archive.iter()?;
@@ -1111,4 +1103,28 @@ force_update:
11111103
MockPlugin::extract_student_files(buf, Compression::Zip, temp.path()).unwrap();
11121104
assert!(temp.path().join("src/file").exists());
11131105
}
1106+
1107+
#[test]
1108+
fn extract_student_files_does_not_extract_tmcproject_yml() {
1109+
init();
1110+
1111+
let temp = tempfile::tempdir().unwrap();
1112+
// create a project with a .tmcproject.yml in the project root and a student file under src
1113+
file_to(&temp, "dir/src/student_file", "");
1114+
file_to(&temp, "dir/.tmcproject.yml", "some: yaml");
1115+
let zip = dir_to_zip(&temp);
1116+
1117+
// extract student files
1118+
MockPlugin::extract_student_files(
1119+
std::io::Cursor::new(zip),
1120+
Compression::Zip,
1121+
&temp.path().join("extracted"),
1122+
)
1123+
.unwrap();
1124+
1125+
// ensure student files are extracted
1126+
assert!(temp.path().join("extracted/src/student_file").exists());
1127+
// ensure .tmcproject.yml is NOT extracted
1128+
assert!(!temp.path().join("extracted/.tmcproject.yml").exists());
1129+
}
11141130
}

crates/tmc-langs/src/submission_packaging.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub fn prepare_submission(
5353
file_util::LOCK_FILE_NAME,
5454
];
5555
if let Some((stub_zip, compression)) = stub_archive {
56-
// if defined, extract and use as the base
56+
// This branch is used when a student downloads their old submission, and we take the files from the stub (the exercise template) instead of the clone path. This makes sure they cannot see the hidden tests in the downloaded file.
5757
extract_with_filter(
5858
plugin,
5959
stub_zip,
@@ -70,7 +70,7 @@ pub fn prepare_submission(
7070
false,
7171
)?;
7272
} else {
73-
// else, copy clone path
73+
// This code branch is used when we package a submission for the sandbox. We use the clone path because it contains the hidden tests, and we want the sandbox to run them.
7474
for entry in WalkDir::new(stub_clone_path).min_depth(1) {
7575
let entry = entry?;
7676

@@ -101,8 +101,25 @@ pub fn prepare_submission(
101101
log::debug!("extracting student files");
102102
let file = file_util::open_file(submission.archive)?;
103103
if submission.extract_naively {
104+
// If the stub contains a .tmcproject.yml, preserve it so the student's cannot override it
105+
let tmcproject_path = extract_dest_path.join(".tmcproject.yml");
106+
let preserved_tmcproject = if tmcproject_path.exists() {
107+
Some(
108+
std::fs::read(&tmcproject_path)
109+
.map_err(|e| FileError::FileRead(tmcproject_path.clone(), e))?,
110+
)
111+
} else {
112+
None
113+
};
114+
104115
extract_project_overwrite(file, &extract_dest_path, submission.compression)?;
116+
117+
if let Some(bytes) = preserved_tmcproject {
118+
// restore the stub's .tmcproject.yml
119+
file_util::write_to_file(&bytes, &tmcproject_path)?;
120+
}
105121
} else {
122+
// This code branch is used when we package a submission for the sandbox. This extraction method makes sure we don't allow the student to update the files they are not allowed to edit.
106123
plugin.extract_student_files(file, submission.compression, &extract_dest_path)?;
107124
}
108125

@@ -645,6 +662,89 @@ mod test {
645662
);
646663
}
647664

665+
#[test]
666+
fn stub_tmcproject_yml_overrides_student_in_naive_mode() {
667+
init();
668+
669+
// Copy an existing Python exercise fixture to a temp clone path
670+
let temp = tempfile::tempdir().unwrap();
671+
let clone_root = temp.path().join("some_course");
672+
file_util::create_dir_all(&clone_root).unwrap();
673+
let src_clone = Path::new(PYTHON_CLONE);
674+
file_util::copy(src_clone, &clone_root).unwrap();
675+
676+
let stub_ex_dir = clone_root.join("PythonExercise");
677+
// Ensure stub has its own .tmcproject.yml
678+
file_util::write_to_file(b"key: stub", stub_ex_dir.join(".tmcproject.yml")).unwrap();
679+
680+
// Create a submission zip that attempts to include its own .tmcproject.yml
681+
let sub_zip_path = temp.path().join("submission.zip");
682+
let sub_zip_file = file_util::create_file(&sub_zip_path).unwrap();
683+
let mut zw = zip::ZipWriter::new(sub_zip_file);
684+
let opts = SimpleFileOptions::default();
685+
zw.add_directory("PythonExercise", opts).unwrap();
686+
zw.add_directory("PythonExercise/src", opts).unwrap();
687+
zw.start_file("PythonExercise/src/__init__.py", opts)
688+
.unwrap();
689+
std::io::Write::write_all(&mut zw, b"print('student')\n").unwrap();
690+
zw.start_file("PythonExercise/.tmcproject.yml", opts)
691+
.unwrap();
692+
std::io::Write::write_all(&mut zw, b"key: student\n").unwrap();
693+
zw.finish().unwrap();
694+
695+
let output_arch = temp.path().join("out.tar");
696+
prepare_submission(
697+
PrepareSubmission {
698+
archive: &sub_zip_path,
699+
compression: Compression::Zip,
700+
extract_naively: true,
701+
},
702+
&output_arch,
703+
false,
704+
TmcParams::new(),
705+
&stub_ex_dir,
706+
None,
707+
Compression::Tar,
708+
)
709+
.unwrap();
710+
assert!(output_arch.exists());
711+
712+
// Unpack and verify that .tmcproject.yml content is from stub, not student
713+
let output_file = file_util::open_file(&output_arch).unwrap();
714+
let mut archive = tar::Archive::new(output_file);
715+
let output_extracted = temp.path().join("output");
716+
archive.unpack(&output_extracted).unwrap();
717+
718+
// Verify .tmcproject.yml content is from stub, not student
719+
let yml =
720+
fs::read_to_string(output_extracted.join("some_course/PythonExercise/.tmcproject.yml"))
721+
.unwrap();
722+
assert!(yml.contains("key: stub"));
723+
assert!(!yml.contains("key: student"));
724+
725+
// Verify other expected files are present
726+
assert!(
727+
output_extracted
728+
.join("some_course/PythonExercise/src/__init__.py")
729+
.exists()
730+
);
731+
assert!(
732+
output_extracted
733+
.join("some_course/PythonExercise/src/__main__.py")
734+
.exists()
735+
);
736+
assert!(
737+
output_extracted
738+
.join("some_course/PythonExercise/test/test_greeter.py")
739+
.exists()
740+
);
741+
assert!(
742+
output_extracted
743+
.join("some_course/PythonExercise/__init__.py")
744+
.exists()
745+
);
746+
}
747+
648748
#[test]
649749
fn prepare_make_submission() {
650750
init();

0 commit comments

Comments
 (0)