1 /*
2  * Copyright (C) 2018-2019 Daniel Scharrer
3  *
4  * This software is provided 'as-is', without any express or implied
5  * warranty.  In no event will the author(s) be held liable for any damages
6  * arising from the use of this software.
7  *
8  * Permission is granted to anyone to use this software for any purpose,
9  * including commercial applications, and to alter it and redistribute it
10  * freely, subject to the following restrictions:
11  *
12  * 1. The origin of this software must not be misrepresented; you must not
13  *    claim that you wrote the original software. If you use this software
14  *    in a product, an acknowledgment in the product documentation would be
15  *    appreciated but is not required.
16  * 2. Altered source versions must be plainly marked as such, and must not be
17  *    misrepresented as being the original software.
18  * 3. This notice may not be removed or altered from any source distribution.
19  */
20 
21 #include "cli/goggalaxy.hpp"
22 
23 #include <set>
24 #include <string>
25 #include <vector>
26 
27 #include <boost/foreach.hpp>
28 #include <boost/lexical_cast.hpp>
29 #include <boost/algorithm/string/predicate.hpp>
30 #include <boost/algorithm/string/trim.hpp>
31 
32 #include "crypto/checksum.hpp"
33 
34 #include "setup/data.hpp"
35 #include "setup/file.hpp"
36 #include "setup/info.hpp"
37 #include "setup/language.hpp"
38 
39 #include "util/log.hpp"
40 
41 namespace gog {
42 
43 namespace {
44 
parse_function_call(const std::string & code,const std::string & name)45 std::vector<std::string> parse_function_call(const std::string & code, const std::string & name) {
46 
47 	std::vector<std::string> arguments;
48 	if(code.empty()) {
49 		return arguments;
50 	}
51 
52 	const char whitespace[] = " \t\r\n";
53 	const char separator[] = " \t\r\n(),'";
54 
55 	size_t start = code.find_first_not_of(whitespace);
56 	if(start == std::string::npos) {
57 		return arguments;
58 	}
59 
60 	size_t end = code.find_first_of(separator, start);
61 	if(end == std::string::npos) {
62 		return arguments;
63 	}
64 
65 	size_t parenthesis = code.find_first_not_of(whitespace, end);
66 	if(parenthesis == std::string::npos || code[parenthesis] != '(') {
67 		return arguments;
68 	}
69 
70 	if(end - start != name.length() || code.compare(start, end - start, name) != 0) {
71 		return arguments;
72 	}
73 
74 	size_t p = parenthesis + 1;
75 	while(true) {
76 
77 		p = code.find_first_not_of(whitespace, p);
78 		if(p == std::string::npos) {
79 			log_warning << "Error parsing function call: " << code;
80 			return arguments;
81 		}
82 
83 		arguments.resize(arguments.size() + 1);
84 
85 		if(code[p] == '\'') {
86 			p++;
87 			while(true) {
88 				size_t string_end = code.find('\'', p);
89 				arguments.back() += code.substr(p, string_end - p);
90 				if(string_end == std::string::npos || string_end + 1 == code.size()) {
91 					log_warning << "Error parsing function call: " << code;
92 					return arguments;
93 				}
94 				p = string_end + 1;
95 				if(code[p] == '\'') {
96 					arguments.back() += '\'';
97 					p++;
98 				} else {
99 					break;
100 				}
101 			}
102 		} else {
103 			size_t token_end = code.find_first_of(separator, p);
104 			arguments.back() = code.substr(p, token_end - p);
105 			if(token_end == std::string::npos || token_end == code.size()) {
106 				log_warning << "Error parsing function call: " << code;
107 				return arguments;
108 			}
109 			p = token_end;
110 		}
111 
112 		p = code.find_first_not_of(whitespace, p);
113 		if(p == std::string::npos) {
114 			log_warning << "Error parsing function call: " << code;
115 			return arguments;
116 		}
117 
118 		if(code[p] == ')') {
119 			break;
120 		} else if(code[p] == ',') {
121 			p++;
122 		} else {
123 			log_warning << "Error parsing function call: " << code;
124 			return arguments;
125 		}
126 
127 	}
128 
129 	p++;
130 	if(p != code.size()) {
131 		p = code.find_first_not_of(whitespace, p);
132 		if(p != std::string::npos) {
133 			if(code[p] != ';' || code.find_first_not_of(whitespace, p + 1) != std::string::npos) {
134 				log_warning << "Error parsing function call: " << code;
135 			}
136 		}
137 	}
138 
139 	return arguments;
140 }
141 
parse_hex(char c)142 int parse_hex(char c) {
143 	if(c >= '0' && c <= '9') {
144 		return c - '0';
145 	} else if(c >= 'a' && c <= 'f') {
146 		return c - 'a' + 10;
147 	} else if(c >= 'A' && c <= 'F') {
148 		return c - 'a' + 10;
149 	} else {
150 		return -1;
151 	}
152 }
153 
parse_checksum(const std::string & string)154 crypto::checksum parse_checksum(const std::string & string) {
155 
156 	crypto::checksum checksum;
157 	checksum.type = crypto::MD5;
158 
159 	if(string.length() != 32) {
160 		// Unknown checksum type
161 		checksum.type = crypto::None;
162 		return checksum;
163 	}
164 
165 	for(size_t i = 0; i < 16; i++) {
166 		int a = parse_hex(string[2 * i]);
167 		int b = parse_hex(string[2 * i + 1]);
168 		if(a < 0 || b < 0) {
169 			checksum.type = crypto::None;
170 			break;
171 		}
172 		checksum.md5[i] = char((a << 4) | b);
173 	}
174 
175 	return checksum;
176 }
177 
178 struct constraint {
179 
180 	std::string name;
181 	bool negated;
182 
constraintgog::__anon288b55d30111::constraint183 	explicit constraint(const std::string & constraint_name, bool is_negated = false)
184 		: name(constraint_name), negated(is_negated) { }
185 
186 };
187 
parse_constraints(const std::string & input)188 std::vector<constraint> parse_constraints(const std::string & input) {
189 
190 	std::vector<constraint> result;
191 
192 	size_t start = 0;
193 
194 	while(start < input.length()) {
195 
196 		start = input.find_first_not_of(" \t\r\n", start);
197 		if(start == std::string::npos) {
198 			break;
199 		}
200 
201 		bool negated = false;
202 		if(input[start] == '!') {
203 			negated = true;
204 			start++;
205 		}
206 
207 		size_t end = input.find('#', start);
208 		if(end == std::string::npos) {
209 			end = input.length();
210 		}
211 
212 		if(end != start) {
213 			std::string token = input.substr(start, end - start);
214 			boost::trim(token);
215 			result.push_back(constraint(token, negated));
216 		}
217 
218 		if(end == std::string::npos) {
219 			end = input.length();
220 		}
221 
222 		start = end + 1;
223 	}
224 
225 	return result;
226 }
227 
create_constraint_expression(std::vector<constraint> & constraints)228 std::string create_constraint_expression(std::vector<constraint> & constraints) {
229 
230 	std::string result;
231 
232 	BOOST_FOREACH(const constraint & entry, constraints) {
233 
234 		if(!result.empty()) {
235 			result += " or ";
236 		}
237 
238 		if(entry.negated) {
239 			result += " not ";
240 		}
241 
242 		result += entry.name;
243 
244 	}
245 
246 	return result;
247 }
248 
249 } // anonymous namespace
250 
parse_galaxy_files(setup::info & info,bool force)251 void parse_galaxy_files(setup::info & info, bool force) {
252 
253 	if(!force) {
254 		bool is_gog = boost::icontains(info.header.app_publisher, "GOG.com");
255 		is_gog = is_gog || boost::icontains(info.header.app_publisher_url, "www.gog.com");
256 		is_gog = is_gog || boost::icontains(info.header.app_support_url, "www.gog.com");
257 		is_gog = is_gog || boost::icontains(info.header.app_updates_url, "www.gog.com");
258 		if(!is_gog) {
259 			return;
260 		}
261 	}
262 
263 	setup::file_entry * file_start = NULL;
264 	size_t remaining_parts = 0;
265 
266 	bool has_language_constraints = false;
267 	std::set<std::string> all_languages;
268 
269 	BOOST_FOREACH(setup::file_entry & file, info.files) {
270 
271 		// Multi-part file info: file checksum, filename, part count
272 		std::vector<std::string> start_info = parse_function_call(file.before_install, "before_install");
273 		if(start_info.empty()) {
274 			start_info = parse_function_call(file.before_install, "before_install_dependency");
275 		}
276 		if(!start_info.empty()) {
277 
278 			if(remaining_parts != 0) {
279 				log_warning << "Incomplete GOG Galaxy file " << file_start->destination;
280 				remaining_parts = 0;
281 			}
282 
283 			// Recover the original filename - parts are named after the MD5 hash of their contents
284 			if(start_info.size() >= 2 && !start_info[1].empty()) {
285 				file.destination = start_info[1];
286 			}
287 
288 			file.checksum = parse_checksum(start_info[0]);
289 			file.size = 0;
290 			if(file.checksum.type == crypto::None) {
291 				log_warning << "Could not parse checksum for GOG Galaxy file " << file.destination
292 				            << ": " << start_info[0];
293 			}
294 
295 			if(start_info.size() < 3) {
296 				log_warning << "Missing part count for GOG Galaxy file " << file.destination;
297 				remaining_parts = 1;
298 			} else {
299 				try {
300 					remaining_parts = boost::lexical_cast<size_t>(start_info[2]);
301 					if(remaining_parts == 0) {
302 						remaining_parts = 1;
303 					}
304 					file_start = &file;
305 				} catch(...) {
306 					log_warning << "Could not parse part count for GOG Galaxy file " << file.destination
307 					            << ": " << start_info[2];
308 				}
309 			}
310 
311 		}
312 
313 		// File part ifo: part checksum, compressed part size, uncompressed part size
314 		std::vector<std::string> part_info = parse_function_call(file.after_install, "after_install");
315 		if(part_info.empty()) {
316 			part_info = parse_function_call(file.after_install, "after_install_dependency");
317 		}
318 		if(!part_info.empty()) {
319 			if(remaining_parts == 0) {
320 				log_warning << "Missing file start for GOG Galaxy file part " << file.destination;
321 			} else if(file.location > info.data_entries.size()) {
322 				log_warning << "Invalid data location for GOG Galaxy file part " << file.destination;
323 				remaining_parts = 0;
324 			} else if(part_info.size() < 3) {
325 				log_warning << "Missing size for GOG Galaxy file part " << file.destination;
326 				remaining_parts = 0;
327 			} else {
328 
329 				remaining_parts--;
330 
331 				setup::data_entry & data = info.data_entries[file.location];
332 
333 				// Ignore file part MD5 checksum, setup already contains a better one for the deflated data
334 
335 				try {
336 					boost::uint64_t compressed_size = boost::lexical_cast<boost::uint64_t>(part_info[1]);
337 					if(data.file.size != compressed_size) {
338 						log_warning << "Unexpected compressed size for GOG Galaxy file part " << file.destination
339 						            << ": " << compressed_size << " != " << data.file.size;
340 					}
341 				} catch(...) {
342 					log_warning << "Could not parse compressed size for GOG Galaxy file part " << file.destination
343 					            << ": " << part_info[1];
344 				}
345 
346 				try {
347 
348 					// GOG Galaxy file parts are deflated, inflate them while extracting
349 					data.uncompressed_size = boost::lexical_cast<boost::uint64_t>(part_info[2]);
350 					data.file.filter = stream::ZlibFilter;
351 
352 					file_start->size += data.uncompressed_size;
353 
354 					if(&file != file_start) {
355 
356 						// Ignore this file entry and instead add the data location to the start file
357 						file.destination.clear();
358 						file_start->additional_locations.push_back(file.location);
359 
360 						if(file.components != file_start->components || file.tasks != file_start->tasks
361 						   || file.languages != file_start->languages || file.check != file_start->check
362 						   || file.options != file_start->options) {
363 							log_warning << "Mismatched constraints for different parts of GOG Galaxy file "
364 							            << file_start->destination << ": " << file.destination;
365 						}
366 
367 					}
368 
369 				} catch(...) {
370 					log_warning << "Could not parse size for GOG Galaxy file part " << file.destination
371 					            << ": " << part_info[1];
372 					remaining_parts = 0;
373 				}
374 
375 			}
376 		} else if(!start_info.empty()) {
377 			log_warning << "Missing part info for GOG Galaxy file " << file.destination;
378 			remaining_parts = 0;
379 		} else if(remaining_parts != 0) {
380 			log_warning << "Incomplete GOG Galaxy file " << file_start->destination;
381 			remaining_parts = 0;
382 		}
383 
384 		if(!file.destination.empty()) {
385 			// languages, architectures, winversions
386 			std::vector<std::string> check = parse_function_call(file.check, "check_if_install");
387 			if(!check.empty() && !check[0].empty()) {
388 				std::vector<constraint> languages = parse_constraints(check[0]);
389 				BOOST_FOREACH(const constraint & language, languages) {
390 					all_languages.insert(language.name);
391 				}
392 			}
393 		}
394 
395 		has_language_constraints = has_language_constraints || !file.languages.empty();
396 
397 	}
398 
399 	if(remaining_parts != 0) {
400 		log_warning << "Incomplete GOG Galaxy file " << file_start->destination;
401 	}
402 
403 	/*
404 	 * GOG Galaxy multi-part files also have their own constraints, convert these to standard
405 	 * Inno Setup ones.
406 	 *
407 	 * Do this in a separate loop to not break constraint checks above.
408 	 */
409 
410 	BOOST_FOREACH(setup::file_entry & file, info.files) {
411 
412 		if(file.destination.empty()) {
413 			continue;
414 		}
415 
416 		// languages, architectures, winversions
417 		std::vector<std::string> check = parse_function_call(file.check, "check_if_install");
418 		if(!check.empty()) {
419 
420 			if(!check[0].empty()) {
421 
422 				std::vector<constraint> languages = parse_constraints(check[0]);
423 
424 				// Ignore constraints that just contain all languages
425 				bool has_all_languages = false;
426 				if(languages.size() >= all_languages.size() && all_languages.size() > 1) {
427 					has_all_languages = true;
428 					BOOST_FOREACH(const std::string & known_language, all_languages) {
429 						bool has_language = false;
430 						BOOST_FOREACH(const constraint & language, languages) {
431 							if(!language.negated && language.name == known_language) {
432 								has_language = true;
433 								break;
434 							}
435 						}
436 						if(!has_language) {
437 							has_all_languages = false;
438 							break;
439 						}
440 					}
441 				}
442 
443 				if(!languages.empty() && !has_all_languages) {
444 					if(!file.languages.empty()) {
445 						log_warning << "Overwriting language constraints for GOG Galaxy file " << file.destination;
446 					}
447 					file.languages = create_constraint_expression(languages);
448 				}
449 
450 			}
451 
452 			if(check.size() >= 2 && !check[1].empty()) {
453 				const setup::file_entry::flags all_arch = setup::file_entry::Bits32 | setup::file_entry::Bits64;
454 				setup::file_entry::flags arch = 0;
455 				if(check[1] != "32#64#") {
456 					std::vector<constraint> architectures = parse_constraints(check[1]);
457 					BOOST_FOREACH(const constraint & architecture, architectures) {
458 						if(architecture.negated && architectures.size() > 1) {
459 							log_warning << "Ignoring architecture for GOG Galaxy file " << file.destination
460 							            << ": !" << architecture.name;
461 						} else if(architecture.name == "32") {
462 							arch |= setup::file_entry::Bits32;
463 						} else if(architecture.name == "64") {
464 							arch |= setup::file_entry::Bits64;
465 						} else {
466 							log_warning << "Unknown architecture for GOG Galaxy file " << file.destination
467 							            << ": " << architecture.name;
468 						}
469 						if(architecture.negated && architectures.size() <= 1) {
470 							arch = all_arch & ~arch;
471 						}
472 					}
473 					if(arch == all_arch) {
474 						arch = 0;
475 					}
476 				}
477 				if((file.options & all_arch) && (file.options & all_arch) != arch) {
478 					log_warning << "Overwriting architecture constraints for GOG Galaxy file " << file.destination;
479 				}
480 				file.options = (file.options & ~all_arch) | arch;
481 			}
482 
483 			if(check.size() >= 3 && !check[2].empty()) {
484 				log_warning << "Ignoring OS constraint for GOG Galaxy file " << file.destination
485 				            << ": " << check[2];
486 			}
487 
488 			if(file.components.empty()) {
489 				file.components = "game";
490 			}
491 
492 		}
493 
494 		// component id, ?
495 		std::vector<std::string> dependency = parse_function_call(file.check, "check_if_install_dependency");
496 		if(!dependency.empty()) {
497 			if(file.components.empty() && !dependency[0].empty()) {
498 				file.components = dependency[0];
499 			}
500 		}
501 
502 	}
503 
504 	if(!all_languages.empty()) {
505 		if(!has_language_constraints) {
506 			info.languages.clear();
507 		}
508 		info.languages.reserve(all_languages.size());
509 		BOOST_FOREACH(const std::string & name, all_languages) {
510 			setup::language_entry language;
511 			language.name = name;
512 			language.dialog_font_size = 0;
513 			language.dialog_font_standard_height = 0;
514 			language.title_font_size = 0;
515 			language.welcome_font_size = 0;
516 			language.copyright_font_size = 0;
517 			language.right_to_left = false;
518 			info.languages.push_back(language);
519 		}
520 	}
521 
522 }
523 
524 } // namespace gog
525