Skip to content

Commit 3275148

Browse files
committed
Add configurable archive size limit
1 parent 030aa0b commit 3275148

File tree

10 files changed

+150
-21
lines changed

10 files changed

+150
-21
lines changed

crates/bindings/tmc-langs-node/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ fn paste(mut cx: FunctionContext) -> JsResult<JsValue> {
612612

613613
let locale = locale.map(|l| Language::from_639_3(&l).expect("Invalid locale"));
614614
let res = with_client(client_name, client_version, |client| {
615-
Ok(client.paste(exercise_id, &submission_path, paste_message, locale)?)
615+
Ok(client.paste(exercise_id, &submission_path, paste_message, locale, 500)?)
616616
});
617617
convert_res(&mut cx, res)
618618
}
@@ -636,6 +636,7 @@ fn request_code_review(mut cx: FunctionContext) -> JsResult<JsValue> {
636636
&submission_path,
637637
message_for_reviewer,
638638
Some(locale),
639+
500,
639640
)?)
640641
});
641642
convert_res(&mut cx, res)
@@ -654,7 +655,7 @@ fn reset_exercise(mut cx: FunctionContext) -> JsResult<JsValue> {
654655

655656
let res = with_client(client_name, client_version, |client| {
656657
if save_old_state {
657-
client.submit(exercise_id, &exercise_path, None)?;
658+
client.submit(exercise_id, &exercise_path, 500, None)?;
658659
}
659660
tmc_langs::reset(client, exercise_id, &exercise_path)
660661
});
@@ -701,7 +702,7 @@ fn submit(mut cx: FunctionContext) -> JsResult<JsValue> {
701702

702703
let locale = locale.map(|l| Language::from_639_3(&l).expect("Invalid locale"));
703704
let temp = with_client(client_name, client_version, |client| {
704-
let new_submission = client.submit(exercise_id, &submission_path, locale)?;
705+
let new_submission = client.submit(exercise_id, &submission_path, 500, locale)?;
705706
if dont_block {
706707
Ok(Temp::NewSubmission(new_submission))
707708
} else {

crates/tmc-langs-cli/src/lib.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::{
2525
};
2626
use tmc_langs::{
2727
CommandError, Compression, Credentials, DownloadOrUpdateTmcCourseExercisesResult,
28-
DownloadResult, Language, StyleValidationResult, TmcConfig, UpdatedExercise,
28+
DownloadResult, Language, StyleValidationResult, TmcConfig, TmcProjectYml, UpdatedExercise,
2929
file_util::{self, Lock, LockOptions},
3030
mooc::{MoocClient, MoocClientError},
3131
tmc::{TestMyCodeClient, TestMyCodeClientError, request::FeedbackAnswer},
@@ -894,9 +894,16 @@ fn run_tmc_inner(
894894
let mut lock = Lock::dir(&submission_path, LockOptions::Read)?;
895895
let _guard = lock.lock()?;
896896

897+
let tmc_project_yml = TmcProjectYml::load_or_default(&submission_path)?;
897898
let locale = locale.map(|l| l.0);
898899
let new_submission = client
899-
.paste(exercise_id, &submission_path, paste_message, locale)
900+
.paste(
901+
exercise_id,
902+
&submission_path,
903+
paste_message,
904+
locale,
905+
tmc_project_yml.get_submission_size_limit_mb(),
906+
)
900907
.context("Failed to get paste with comment")?;
901908
CliOutput::finished_with_data("sent paste", DataKind::NewSubmission(new_submission))
902909
}
@@ -910,12 +917,14 @@ fn run_tmc_inner(
910917
let mut lock = Lock::dir(&submission_path, LockOptions::Read)?;
911918
let _guard = lock.lock()?;
912919

920+
let tmc_project_yml = TmcProjectYml::load_or_default(&submission_path)?;
913921
let new_submission = client
914922
.request_code_review(
915923
exercise_id,
916924
&submission_path,
917925
message_for_reviewer,
918926
Some(locale),
927+
tmc_project_yml.get_submission_size_limit_mb(),
919928
)
920929
.context("Failed to request code review")?;
921930
CliOutput::finished_with_data(
@@ -934,7 +943,13 @@ fn run_tmc_inner(
934943

935944
if save_old_state {
936945
// submit current state
937-
client.submit(exercise_id, &exercise_path, None)?;
946+
let tmc_project_yml = TmcProjectYml::load_or_default(&exercise_path)?;
947+
client.submit(
948+
exercise_id,
949+
&exercise_path,
950+
tmc_project_yml.get_submission_size_limit_mb(),
951+
None,
952+
)?;
938953
}
939954
tmc_langs::reset(client, exercise_id, &exercise_path)?;
940955
CliOutput::finished("reset exercise")
@@ -987,8 +1002,14 @@ fn run_tmc_inner(
9871002
let _guard = lock.lock()?;
9881003

9891004
let locale = locale.map(|l| l.0);
1005+
let tmc_project_yml = TmcProjectYml::load_or_default(&submission_path)?;
9901006
let new_submission = client
991-
.submit(exercise_id, &submission_path, locale)
1007+
.submit(
1008+
exercise_id,
1009+
&submission_path,
1010+
tmc_project_yml.get_submission_size_limit_mb(),
1011+
locale,
1012+
)
9921013
.context("Failed to submit")?;
9931014

9941015
if dont_block {

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,12 @@ pub enum Compression {
293293
}
294294

295295
impl Compression {
296-
pub fn compress(self, path: &Path, hash: bool) -> Result<(Vec<u8>, Option<Hash>), TmcError> {
296+
pub fn compress(
297+
self,
298+
path: &Path,
299+
hash: bool,
300+
size_limit_mb: u32,
301+
) -> Result<(Vec<u8>, Option<Hash>), TmcError> {
297302
let mut hasher = if hash { Some(Hasher::new()) } else { None };
298303
let buf = match self {
299304
Self::Tar => {
@@ -365,8 +370,22 @@ impl Compression {
365370
}
366371
};
367372
let hash = hasher.map(|h| h.finalize());
373+
Self::enforce_archive_size_limit(&buf, size_limit_mb)?;
368374
Ok((buf, hash))
369375
}
376+
377+
pub fn enforce_archive_size_limit(data: &[u8], size_limit_mb: u32) -> Result<(), TmcError> {
378+
if let Ok(data_len_b) = u64::try_from(data.len()) {
379+
let size_limit_b = u64::from(size_limit_mb) * 1000 * 1000;
380+
if data_len_b <= size_limit_b {
381+
return Ok(());
382+
}
383+
}
384+
Err(TmcError::ArchiveSizeLimitExceeded {
385+
limit: size_limit_mb,
386+
actual: data.len(),
387+
})
388+
}
370389
}
371390

372391
fn walk_dir_for_compression(

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ pub enum TmcError {
2929
ZstdRead(#[source] std::io::Error),
3030
#[error("Failed to write zstd archive")]
3131
ZstdWrite(#[source] std::io::Error),
32+
#[error(
33+
"Archive size limit exceeded when compressing proejct (limit: {limit} MB, archive: {actual} MB)"
34+
)]
35+
ArchiveSizeLimitExceeded { limit: u32, actual: usize },
3236

3337
#[error("Path {0} is not valid UTF-8")]
3438
InvalidUtf8(PathBuf),

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use tmc_langs_util::{
1414
file_util::{self, Lock, LockOptions},
1515
};
1616

17+
const DEFAULT_SUBMISSION_SIZE_LIMIT_MB: u32 = 500;
18+
1719
/// Extra data from a `.tmcproject.yml` file.
1820
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
1921
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
@@ -60,6 +62,11 @@ pub struct TmcProjectYml {
6062
#[serde(default)]
6163
#[serde(skip_serializing_if = "Option::is_none")]
6264
pub sandbox_image: Option<String>,
65+
66+
/// Overrides the default archive size limit (500 Mb).
67+
#[serde(default)]
68+
#[serde(skip_serializing_if = "Option::is_none")]
69+
pub submission_size_limit_mb: Option<u32>,
6370
}
6471

6572
impl TmcProjectYml {
@@ -120,6 +127,9 @@ impl TmcProjectYml {
120127
minimum_python_version: old.minimum_python_version.or(with.minimum_python_version),
121128
sandbox_image: old.sandbox_image.or(with.sandbox_image),
122129
no_tests: old.no_tests.or(with.no_tests),
130+
submission_size_limit_mb: old
131+
.submission_size_limit_mb
132+
.or(with.submission_size_limit_mb),
123133
};
124134
*self = new;
125135
}
@@ -132,6 +142,11 @@ impl TmcProjectYml {
132142
serde_yaml::to_writer(guard.get_file_mut(), &self)?;
133143
Ok(())
134144
}
145+
146+
pub fn get_submission_size_limit_mb(&self) -> u32 {
147+
self.submission_size_limit_mb
148+
.unwrap_or(DEFAULT_SUBMISSION_SIZE_LIMIT_MB)
149+
}
135150
}
136151

137152
/// Python version from TmcProjectYml.
@@ -300,6 +315,24 @@ mod test {
300315
assert_eq!(no_tests.points, &["1", "notests"]);
301316
}
302317

318+
#[test]
319+
fn deserialize_submission_size_limit_mb() {
320+
init();
321+
322+
let submission_size_limit_mb = r#"submission_size_limit_mb: 100
323+
"#;
324+
325+
let cfg: TmcProjectYml = deserialize::yaml_from_str(submission_size_limit_mb).unwrap();
326+
let submission_size_limit_mb = cfg.submission_size_limit_mb.unwrap();
327+
assert_eq!(submission_size_limit_mb, 100);
328+
let submission_size_limit_mb = cfg.get_submission_size_limit_mb();
329+
assert_eq!(submission_size_limit_mb, 100);
330+
331+
let cfg: TmcProjectYml = deserialize::yaml_from_str("").unwrap();
332+
let submission_size_limit_mb = cfg.get_submission_size_limit_mb();
333+
assert_eq!(submission_size_limit_mb, DEFAULT_SUBMISSION_SIZE_LIMIT_MB);
334+
}
335+
303336
#[test]
304337
fn deserialize_python_ver() {
305338
init();

crates/tmc-langs-plugins/src/compression.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub fn compress_student_files(
1919
compression: Compression,
2020
deterministic: bool,
2121
hash: bool,
22+
size_limit_mb: u32,
2223
) -> Result<(Vec<u8>, Option<Hash>), TmcError> {
2324
let mut hasher = if hash { Some(Hasher::new()) } else { None };
2425
let mut writer = ArchiveBuilder::new(Cursor::new(vec![]), compression, deterministic);
@@ -59,7 +60,9 @@ pub fn compress_student_files(
5960
}
6061
let hash = hasher.map(|h| h.finalize());
6162
let cursor = writer.finish()?;
62-
Ok((cursor.into_inner(), hash))
63+
let data = cursor.into_inner();
64+
Compression::enforce_archive_size_limit(&data, size_limit_mb)?;
65+
Ok((data, hash))
6366
}
6467

6568
// ensures the / separator is used
@@ -195,7 +198,7 @@ mod test {
195198
fs::{self, *},
196199
};
197200
use tempfile::tempdir;
198-
use tmc_langs_framework::EverythingIsStudentFilePolicy;
201+
use tmc_langs_framework::{EverythingIsStudentFilePolicy, TmcProjectYml};
199202

200203
fn init() {
201204
use log::*;
@@ -228,12 +231,14 @@ mod test {
228231
File::create(missing_file_path).unwrap();
229232

230233
let path = temp.path().join("exercise-name");
234+
let tmcprojectyml = TmcProjectYml::load_or_default(&path).unwrap();
231235
let (zipped, _hash) = compress_student_files(
232236
&EverythingIsStudentFilePolicy::new(&path).unwrap(),
233237
&path,
234238
Compression::Zip,
235239
true,
236240
false,
241+
tmcprojectyml.get_submission_size_limit_mb(),
237242
)
238243
.unwrap();
239244
let mut archive = ZipArchive::new(Cursor::new(zipped)).unwrap();

crates/tmc-langs-plugins/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ pub fn compress_project(
5454
deterministic: bool,
5555
naive: bool,
5656
hash: bool,
57+
size_limit_mb: u32,
5758
) -> Result<(Vec<u8>, Option<Hash>), PluginError> {
5859
let (compressed, hash) = if naive {
59-
compression.compress(path, hash)?
60+
compression.compress(path, hash, size_limit_mb)?
6061
} else {
6162
let policy = get_student_file_policy(path)?;
6263
compression::compress_student_files(
@@ -65,9 +66,9 @@ pub fn compress_project(
6566
compression,
6667
deterministic,
6768
hash,
69+
size_limit_mb,
6870
)?
6971
};
70-
7172
Ok((compressed, hash))
7273
}
7374

crates/tmc-langs/src/lib.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,13 @@ pub fn download_old_submission(
150150

151151
if save_old_state {
152152
// submit old exercise
153-
client.submit(exercise_id, output_path, None)?;
153+
let tmc_project_yml = TmcProjectYml::load_or_default(output_path)?;
154+
client.submit(
155+
exercise_id,
156+
output_path,
157+
tmc_project_yml.get_submission_size_limit_mb(),
158+
None,
159+
)?;
154160
log::debug!("finished submission");
155161
}
156162

@@ -185,8 +191,14 @@ pub fn submit_exercise(
185191
let exercise_path =
186192
ProjectsConfig::get_tmc_exercise_download_target(projects_dir, course_slug, exercise_slug);
187193

194+
let tmc_project_yml = TmcProjectYml::load_or_default(&exercise_path)?;
188195
client
189-
.submit(exercise.id, exercise_path.as_path(), locale)
196+
.submit(
197+
exercise.id,
198+
exercise_path.as_path(),
199+
tmc_project_yml.get_submission_size_limit_mb(),
200+
locale,
201+
)
190202
.map_err(Into::into)
191203
}
192204

@@ -207,8 +219,15 @@ pub fn paste_exercise(
207219
let exercise_path =
208220
ProjectsConfig::get_tmc_exercise_download_target(projects_dir, course_slug, exercise_slug);
209221

222+
let tmc_project_yml = TmcProjectYml::load_or_default(&exercise_path)?;
210223
client
211-
.paste(exercise.id, exercise_path.as_path(), paste_message, locale)
224+
.paste(
225+
exercise.id,
226+
exercise_path.as_path(),
227+
paste_message,
228+
locale,
229+
tmc_project_yml.get_submission_size_limit_mb(),
230+
)
212231
.map_err(Into::into)
213232
}
214233

@@ -846,8 +865,15 @@ pub fn compress_project_to(
846865
compression
847866
);
848867

849-
let (data, _hash) =
850-
tmc_langs_plugins::compress_project(source, compression, deterministic, naive, false)?;
868+
let tmc_project_yml = TmcProjectYml::load_or_default(source)?;
869+
let (data, _hash) = tmc_langs_plugins::compress_project(
870+
source,
871+
compression,
872+
deterministic,
873+
naive,
874+
false,
875+
tmc_project_yml.get_submission_size_limit_mb(),
876+
)?;
851877
file_util::write_to_file(data, target)?;
852878
Ok(())
853879
}
@@ -868,8 +894,15 @@ pub fn compress_project_to_with_hash(
868894
compression
869895
);
870896

871-
let (data, hash) =
872-
tmc_langs_plugins::compress_project(source, compression, deterministic, naive, true)?;
897+
let tmc_project_yml = TmcProjectYml::load_or_default(source)?;
898+
let (data, hash) = tmc_langs_plugins::compress_project(
899+
source,
900+
compression,
901+
deterministic,
902+
naive,
903+
true,
904+
tmc_project_yml.get_submission_size_limit_mb(),
905+
)?;
873906
let hash = hash.expect("set hash to true");
874907
file_util::write_to_file(data, target)?;
875908
Ok(hash.to_string())

0 commit comments

Comments
 (0)