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