pacenotes_server/reports/
validation.rs1pub const MAX_BODY_BYTES: usize = 8 * 1024;
3pub const MAX_CONFLICTS: usize = 32;
5pub const MIN_PACKAGES: usize = 2;
7pub const MAX_PACKAGES: usize = 8;
8pub const MAX_FILE_BYTES: usize = 260;
10pub const MAX_INSTALLER_LEN: usize = 32;
11
12#[derive(Debug, PartialEq, Eq)]
14pub struct Validated {
15 pub game: String,
16 pub installer: String,
17 pub conflicts: Vec<pacenotes_shared::Conflict>,
18 pub dropped: usize,
20}
21
22pub fn validate(report: pacenotes_shared::ConflictReport) -> Result<Validated, &'static str> {
25 if !pacenotes_shared::valid_id(&report.game) {
26 return Err("game is not a valid game id");
27 }
28 let installer_ok = (1..=MAX_INSTALLER_LEN).contains(&report.installer.len())
29 && report
30 .installer
31 .bytes()
32 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'-'));
33 if !installer_ok {
34 return Err("installer must be 1-32 characters of [0-9A-Za-z.+-]");
35 }
36
37 let mut conflicts = Vec::new();
38 let mut dropped = 0;
39 for conflict in report.conflicts {
40 if conflicts.len() < MAX_CONFLICTS && is_usable(&conflict) {
41 conflicts.push(conflict);
42 } else {
43 dropped += 1;
44 }
45 }
46 if conflicts.is_empty() {
47 return Err("the report holds no valid conflict");
48 }
49 Ok(Validated {
50 game: report.game,
51 installer: report.installer,
52 conflicts,
53 dropped,
54 })
55}
56
57fn is_usable(conflict: &pacenotes_shared::Conflict) -> bool {
58 conflict.file.len() <= MAX_FILE_BYTES
59 && pacenotes_shared::valid_name(&conflict.file)
60 && (MIN_PACKAGES..=MAX_PACKAGES).contains(&conflict.packages.len())
61 && conflict
62 .packages
63 .iter()
64 .all(|reference| pacenotes_shared::PackageRef::parse(reference).is_some())
65}
66
67#[cfg(test)]
68mod tests {
69 const PAIR: [&str; 2] = [
70 "public/language/polish",
71 "public/codriver/polish/male/Kamil",
72 ];
73
74 fn report(
75 game: &str,
76 installer: &str,
77 conflicts: Vec<(&str, Vec<&str>)>,
78 ) -> pacenotes_shared::ConflictReport {
79 pacenotes_shared::ConflictReport {
80 game: game.into(),
81 installer: installer.into(),
82 conflicts: conflicts
83 .into_iter()
84 .map(|(file, packages)| pacenotes_shared::Conflict {
85 file: file.into(),
86 packages: packages.into_iter().map(String::from).collect(),
87 })
88 .collect(),
89 }
90 }
91
92 #[test]
93 fn a_good_report_passes_untouched() {
94 let validated = super::validate(report(
95 "rbr",
96 "0.2.0",
97 vec![("Plugins/Pacenote/PaceNote.ini", PAIR.to_vec())],
98 ))
99 .unwrap();
100 assert_eq!(validated.game, "rbr");
101 assert_eq!(validated.conflicts.len(), 1);
102 assert_eq!(validated.dropped, 0);
103 }
104
105 #[test]
106 fn whole_report_problems_reject_it() {
107 let one_conflict = || vec![("a/b.ini", PAIR.to_vec())];
108 for (game, installer, conflicts) in [
109 ("../etc", "0.2.0", one_conflict()),
110 ("rbr", "", one_conflict()),
111 ("rbr", "0.2.0; rm -rf", one_conflict()),
112 ("rbr", &"9".repeat(33), one_conflict()),
113 ("rbr", "0.2.0", vec![]),
114 ] {
115 assert!(
116 super::validate(report(game, installer, conflicts)).is_err(),
117 "{game} {installer}"
118 );
119 }
120 }
121
122 #[test]
123 fn bad_entries_are_dropped_and_counted_not_fatal() {
124 let long = "a/".repeat(140) + "x.ini";
125 let nine: Vec<&str> = std::iter::repeat_n(PAIR[0], 9).collect();
126 let validated = super::validate(report(
127 "rbr",
128 "0.2.0",
129 vec![
130 ("Plugins/Pacenote/PaceNote.ini", PAIR.to_vec()),
131 ("C:\\Windows\\system.ini", PAIR.to_vec()),
132 ("../escape.ini", PAIR.to_vec()),
133 (long.as_str(), PAIR.to_vec()),
134 ("a.ini", vec![PAIR[0]]),
135 ("a.ini", nine),
136 ("a.ini", vec![PAIR[0], "no-separator"]),
137 ],
138 ))
139 .unwrap();
140 assert_eq!(validated.conflicts.len(), 1);
141 assert_eq!(validated.dropped, 6);
142 }
143
144 #[test]
145 fn conflicts_beyond_the_cap_are_dropped() {
146 let many: Vec<(&str, Vec<&str>)> = (0..40).map(|_| ("a.ini", PAIR.to_vec())).collect();
147 let validated = super::validate(report("rbr", "0.2.0", many)).unwrap();
148 assert_eq!(validated.conflicts.len(), super::MAX_CONFLICTS);
149 assert_eq!(validated.dropped, 40 - super::MAX_CONFLICTS);
150 }
151}