1 /*
2 * Copyright 2011-2013 Arx Libertatis Team (see the AUTHORS file)
3 *
4 * This file is part of Arx Libertatis.
5 *
6 * Arx Libertatis is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * Arx Libertatis is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with Arx Libertatis. If not, see <http://www.gnu.org/licenses/>.
18 */
19 /* Based on:
20 ===========================================================================
21 ARX FATALIS GPL Source Code
22 Copyright (C) 1999-2010 Arkane Studios SA, a ZeniMax Media company.
23
24 This file is part of the Arx Fatalis GPL Source Code ('Arx Fatalis Source Code').
25
26 Arx Fatalis Source Code is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
27 License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
28
29 Arx Fatalis Source Code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
30 warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
31
32 You should have received a copy of the GNU General Public License along with Arx Fatalis Source Code. If not, see
33 <http://www.gnu.org/licenses/>.
34
35 In addition, the Arx Fatalis Source Code is also subject to certain additional terms. You should have received a copy of these
36 additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Arx
37 Fatalis Source Code. If not, please request a copy in writing from Arkane Studios at the address below.
38
39 If you have questions concerning this license or the applicable additional terms, you may contact in writing Arkane Studios, c/o
40 ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
41 ===========================================================================
42 */
43
44 #include "io/SaveBlock.h"
45
46 #include <cstdlib>
47
48 #include <boost/algorithm/string/case_conv.hpp>
49
50 #include <zlib.h>
51
52 #include "io/log/Logger.h"
53 #include "io/fs/Filesystem.h"
54 #include "io/Blast.h"
55
56 #include "platform/Platform.h"
57
58 using std::string;
59 using std::vector;
60 using std::min;
61
62 static const u32 SAV_VERSION_OLD = (1<<16) | 0;
63 static const u32 SAV_VERSION_RELEASE = (1<<16) | 1;
64 static const u32 SAV_VERSION_DEFLATE = (2<<16) | 0;
65 static const u32 SAV_VERSION_NOEXT = (2<<16) | 1;
66
67 static const u32 SAV_COMP_NONE = 0;
68 static const u32 SAV_COMP_IMPLODE = 1;
69 static const u32 SAV_COMP_DEFLATE = 2;
70
71 static const u32 SAV_SIZE_UNKNOWN = 0xffffffff;
72
73 #ifdef ARX_DEBUG
74 static const char BADSAVCHAR[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\\/.";
75 #endif
76
compressionName() const77 const char * SaveBlock::File::compressionName() const {
78 switch(comp) {
79 case None: return "none";
80 case ImplodeCrypt: return "implode+crypt";
81 case Deflate: return "deflate";
82 default: return "(unknown)";
83 }
84 }
85
loadOffsets(std::istream & handle,u32 version)86 bool SaveBlock::File::loadOffsets(std::istream & handle, u32 version) {
87
88 if(version < SAV_VERSION_DEFLATE) {
89 // ignore the size, calculate from chunks
90 handle.seekg(4, std::istream::cur);
91 uncompressedSize = size_t(-1);
92 } else {
93 u32 uncompressed;
94 if(fs::read(handle, uncompressed).fail()) {
95 return false;
96 }
97 uncompressedSize = (uncompressed == SAV_SIZE_UNKNOWN) ? size_t(-1) : uncompressed;
98 }
99
100 u32 nChunks;
101 if(!fs::read(handle, nChunks)) {
102 return false;
103 }
104 if(version < SAV_VERSION_DEFLATE && nChunks == 0) {
105 nChunks = 1;
106 }
107 chunks.resize(nChunks);
108
109 if(version < SAV_VERSION_DEFLATE) {
110 // ignored
111 handle.seekg(4, std::istream::cur);
112 comp = File::ImplodeCrypt;
113 } else {
114 u32 compid;
115 if(!fs::read(handle, compid)) {
116 return false;
117 }
118 switch(compid) {
119 case SAV_COMP_NONE: comp = File::None; break;
120 case SAV_COMP_IMPLODE: comp = File::ImplodeCrypt; break;
121 case SAV_COMP_DEFLATE: comp = File::Deflate; break;
122 default: comp = File::Unknown;
123 }
124 }
125
126 size_t size = 0;
127 for(size_t i = 0; i < nChunks; i++) {
128
129 u32 chunkSize;
130 if(!fs::read(handle, chunkSize)) {
131 return false;
132 }
133 size += chunkSize;
134
135 u32 chunkOffset;
136 if(!fs::read(handle, chunkOffset)) {
137 return false;
138 }
139
140 chunks[i].size = chunkSize;
141 chunks[i].offset = chunkOffset;
142 }
143
144 storedSize = size;
145
146 return true;
147 }
148
writeEntry(std::ostream & handle,const std::string & name) const149 void SaveBlock::File::writeEntry(std::ostream & handle, const std::string & name) const {
150
151 handle.write(name.c_str(), name.length() + 1);
152
153 u32 _uncompressedSize = (uncompressedSize == size_t(-1)) ? SAV_SIZE_UNKNOWN : uncompressedSize;
154 fs::write(handle, _uncompressedSize);
155
156 u32 nChunks = chunks.size();
157 fs::write(handle, nChunks);
158
159 u32 _comp;
160 switch(comp) {
161 case File::None: _comp = SAV_COMP_NONE; break;
162 case File::ImplodeCrypt: _comp = SAV_COMP_IMPLODE; break;
163 case File::Deflate: _comp = SAV_COMP_DEFLATE; break;
164 case File::Unknown: _comp = (u32)-1; break;
165 }
166 fs::write(handle, _comp);
167
168 for(File::ChunkList::const_iterator chunk = chunks.begin(); chunk != chunks.end(); ++chunk) {
169
170 u32 chunkSize = chunk->size;
171 fs::write(handle, chunkSize);
172
173 u32 chunkOffset = chunk->offset;
174 fs::write(handle, chunkOffset);
175
176 }
177
178 }
179
loadData(std::istream & handle,size_t & size,const std::string & name) const180 char * SaveBlock::File::loadData(std::istream & handle, size_t & size, const std::string & name) const {
181
182 LogDebug("Loading " << name << ' ' << storedSize << "b in " << chunks.size() << " chunks, "
183 << compressionName() << " -> " << (int)uncompressedSize << "b");
184
185 char * buf = (char*)malloc(storedSize);
186 char * p = buf;
187
188 for(File::ChunkList::const_iterator chunk = chunks.begin();
189 chunk != chunks.end(); ++chunk) {
190 handle.seekg(chunk->offset + 4);
191 handle.read(p, chunk->size);
192 p += chunk->size;
193 }
194
195 arx_assert(p == buf + storedSize);
196
197 switch(comp) {
198
199 case File::None: {
200 arx_assert(uncompressedSize == storedSize);
201 size = uncompressedSize;
202 return buf;
203 }
204
205 case File::ImplodeCrypt: {
206 unsigned char * crypt = (unsigned char *)buf;
207 for(size_t i = 0; i < storedSize; i += 2) {
208 crypt[i] = ~crypt[i];
209 }
210 char * uncompressed = blastMemAlloc(buf, storedSize, size);
211 free(buf);
212 if(!uncompressed) {
213 LogError << "Error decompressing imploded " << name;
214 return NULL;
215 }
216 arx_assert(uncompressedSize == (size_t)-1 || size == uncompressedSize);
217 return uncompressed;
218 }
219
220 case File::Deflate: {
221 arx_assert(uncompressedSize != (size_t)-1);
222 uLongf decompressedSize = uncompressedSize;
223 char * uncompressed = (char*)malloc(uncompressedSize);
224 int ret = uncompress((Bytef*)uncompressed, &decompressedSize, (const Bytef*)buf, storedSize);
225 if(ret != Z_OK) {
226 LogError << "Error decompressing deflated " << name << ": " << zError(ret) << " (" << ret << ')';
227 free(buf);
228 free(uncompressed);
229 size = 0;
230 return NULL;
231 }
232 if(decompressedSize != uncompressedSize) {
233 LogError << "Unexpedect uncompressed size " << decompressedSize << " while loading "
234 << name << ", expected " << uncompressedSize;
235 }
236 size = decompressedSize;
237 free(buf);
238 return uncompressed;
239 }
240
241 default: {
242 LogError << "Error decompressing " << name << ": unknown format";
243 free(buf);
244 size = 0;
245 return NULL;
246 }
247
248 }
249 }
250
SaveBlock(const fs::path & _savefile)251 SaveBlock::SaveBlock(const fs::path & _savefile) : savefile(_savefile), totalSize(0), usedSize(0), chunkCount(0) { }
252
~SaveBlock()253 SaveBlock::~SaveBlock() { }
254
loadFileTable()255 bool SaveBlock::loadFileTable() {
256
257 handle.seekg(0);
258
259 u32 fatOffset;
260 if(fs::read(handle, fatOffset).fail()) {
261 return false;
262 }
263 if(handle.seekg(fatOffset + 4).fail()) {
264 LogError << "Cannot seek to FAT";
265 return false;
266 }
267 totalSize = fatOffset;
268
269 u32 version;
270 if(fs::read(handle, version).fail()) {
271 return false;
272 }
273 if(version != SAV_VERSION_DEFLATE && version != SAV_VERSION_RELEASE && version != SAV_VERSION_NOEXT) {
274 LogWarning << "Unexpected savegame version: " << (version >> 16) << '.' << (version & 0xffff) << " for " << savefile;
275 }
276
277 u32 nFiles;
278 if(fs::read(handle, nFiles).fail()) {
279 return false;
280 }
281 nFiles = (version == SAV_VERSION_OLD) ? nFiles - 1 : nFiles;
282 size_t hashMapSize = 1;
283 while(hashMapSize < nFiles) {
284 hashMapSize <<= 1;
285 }
286 if(nFiles > (hashMapSize * 3) / 4) {
287 hashMapSize <<= 1;
288 }
289 files.rehash(hashMapSize);
290
291 if(version == SAV_VERSION_OLD) {
292 char c;
293 do {
294 c = static_cast<char>(handle.get());
295 } while(c != '\0' && handle.good());
296 File dummy;
297 if(!dummy.loadOffsets(handle, version)) {
298 return false;
299 }
300 }
301
302 usedSize = 0;
303 chunkCount = 0;
304
305 for(u32 i = 0; i < nFiles; i++) {
306
307 // Read the file name.
308 string name;
309 if(fs::read(handle, name).fail()) {
310 return false;
311 }
312 if(version < SAV_VERSION_NOEXT) {
313 boost::to_lower(name);
314 if(name.size() > 4 && !name.compare(name.size() - 4, 4, ".sav", 4)) {
315 name.resize(name.size() - 4);
316 }
317 }
318
319 File & file = files[name];
320
321 if(!file.loadOffsets(handle, version)) {
322 return false;
323 }
324
325 usedSize += file.storedSize, chunkCount += file.chunks.size();
326 }
327
328 return true;
329 }
330
writeFileTable(const std::string & important)331 void SaveBlock::writeFileTable(const std::string & important) {
332
333 LogDebug("writeFileTable " << savefile);
334
335 u32 fatOffset = totalSize;
336 handle.seekp(fatOffset + 4);
337
338 fs::write(handle, SAV_VERSION_NOEXT);
339
340 u32 nFiles = files.size();
341 fs::write(handle, nFiles);
342
343 Files::const_iterator ifile = files.find(important);
344 if(ifile != files.end()) {
345 ifile->second.writeEntry(handle, ifile->first);
346 }
347
348 for(Files::const_iterator file = files.begin(); file != files.end(); ++file) {
349 if(file != ifile) {
350 file->second.writeEntry(handle, file->first);
351 }
352 }
353
354 handle.seekp(0);
355 fs::write(handle, fatOffset);
356
357 }
358
open(bool writable)359 bool SaveBlock::open(bool writable) {
360
361 LogDebug("opening savefile " << savefile << " witable=" << writable);
362
363 fs::fstream::openmode mode = fs::fstream::in | fs::fstream::binary | fs::fstream::ate;
364 if(writable) {
365 mode |= fs::fstream::out;
366 }
367
368 handle.clear();
369 handle.open(savefile, mode);
370 if(!handle.is_open()) {
371 handle.clear();
372 if(writable) {
373 handle.open(savefile, mode | fs::fstream::trunc);
374 }
375 if(!handle.is_open()) {
376 LogError << "Could not open " << savefile << " for "
377 << (writable ? "reading/writing" : "reading");
378 return false;
379 }
380 }
381
382 if(handle.tellg() > 0 && !loadFileTable()) {
383 LogError << "Broken save file";
384 return false;
385 }
386
387 return true;
388 }
389
flush(const string & important)390 bool SaveBlock::flush(const string & important) {
391
392 arx_assert_msg(important.find_first_of(BADSAVCHAR) == string::npos,
393 "bad save filename: \"%s\"", important.c_str());
394
395 if((usedSize * 2 < totalSize || chunkCount > (files.size() * 4 / 3))) {
396 defragment();
397 }
398
399 writeFileTable(important);
400
401 handle.flush();
402
403 return handle.good();
404 }
405
defragment()406 bool SaveBlock::defragment() {
407
408 LogDebug("defragmenting " << savefile << " save: using " << usedSize << " / " << totalSize
409 << " b for " << files.size() << " files in " << chunkCount << " chunks");
410
411 fs::path tempFileName = savefile;
412 int i = 0;
413
414 do {
415 std::ostringstream oss;
416 oss << "defrag" << i++;
417 tempFileName.set_ext(oss.str());
418 } while(fs::exists(tempFileName));
419
420 fs::ofstream tempFile(tempFileName, fs::fstream::out | fs::fstream::binary | fs::fstream::trunc);
421 if(!tempFile.is_open()) {
422 return false;
423 }
424
425 totalSize = 0;
426 tempFile.seekp(4);
427
428 for(Files::iterator file = files.begin(); file != files.end(); ++file) {
429
430 if(file->second.storedSize == 0) {
431 continue;
432 }
433
434 char * buf = new char[file->second.storedSize];
435 char * p = buf;
436
437 for(File::ChunkList::iterator chunk = file->second.chunks.begin();
438 chunk != file->second.chunks.end(); ++chunk) {
439 handle.seekg(chunk->offset + 4);
440 handle.read(p, chunk->size);
441 p += chunk->size;
442 }
443
444 arx_assert(p == buf + file->second.storedSize);
445
446 tempFile.write(buf, file->second.storedSize);
447
448 file->second.chunks.resize(1);
449 file->second.chunks.front().offset = totalSize;
450 file->second.chunks.front().size = file->second.storedSize;
451
452 delete[] buf;
453
454 totalSize += file->second.storedSize;
455 }
456
457 usedSize = totalSize, chunkCount = files.size();
458
459 if(tempFile.fail()) {
460 fs::remove(tempFileName);
461 handle.close(), files.clear();
462 LogWarning << "Defragmenting failed: " << tempFileName;
463 return false;
464 }
465
466 tempFile.flush(), tempFile.close(), handle.close();
467
468 if(!fs::rename(tempFileName, savefile, true)) {
469 LogWarning << "Failed to move defragmented savegame " << tempFileName << " to " << savefile;
470 return false;
471 }
472
473 handle.open(savefile, fs::fstream::in | fs::fstream::out | fs::fstream::binary);
474 return handle.is_open();
475 }
476
save(const string & name,const char * data,size_t size)477 bool SaveBlock::save(const string & name, const char * data, size_t size) {
478
479 if(!handle) {
480 return false;
481 }
482
483 arx_assert_msg(name.find_first_of(BADSAVCHAR) == string::npos,
484 "bad save filename: \"%s\"", name.c_str());
485
486 File * file = &files[name];
487
488 file->uncompressedSize = size;
489
490 if(size == 0) {
491 file->comp = File::None;
492 file->storedSize = 0;
493 return true;
494 }
495
496 uLongf compressedSize = size - 1;
497 char * compressed = new char[compressedSize];
498 const char * p;
499 if(compress2((Bytef*)compressed, &compressedSize, (const Bytef*)data, size, 1) == Z_OK) {
500 file->comp = File::Deflate;
501 file->storedSize = compressedSize;
502 p = compressed;
503 } else {
504 file->comp = File::None;
505 file->storedSize = size;
506 p = data;
507 }
508
509 LogDebug("saving " << name << " " << file->uncompressedSize << " " << file->storedSize);
510
511 size_t remaining = file->storedSize;
512
513 for(File::ChunkList::iterator chunk = file->chunks.begin();
514 chunk != file->chunks.end(); ++chunk) {
515
516 handle.seekp(chunk->offset + 4);
517
518 if(chunk->size > remaining) {
519 usedSize -= chunk->size - remaining;
520 chunk->size = remaining;
521 }
522
523 handle.write(p, chunk->size);
524 p += chunk->size;
525 remaining -= chunk->size;
526
527 if(remaining == 0) {
528 file->chunks.erase(++chunk, file->chunks.end());
529 delete[] compressed;
530 return true;
531 }
532 }
533
534 file->chunks.push_back(File::Chunk(remaining, totalSize));
535 handle.seekp(totalSize + 4);
536 handle.write(p, remaining);
537 totalSize += remaining, usedSize += remaining, chunkCount++;
538
539 delete[] compressed;
540
541 return !handle.fail();
542 }
543
load(const string & name,size_t & size)544 char * SaveBlock::load(const string & name, size_t & size) {
545
546 arx_assert_msg(name.find_first_of(BADSAVCHAR) == string::npos,
547 "bad save filename: \"%s\"", name.c_str());
548
549 Files::const_iterator file = files.find(name);
550
551 return (file == files.end()) ? NULL : file->second.loadData(handle, size, name);
552 }
553
hasFile(const string & name) const554 bool SaveBlock::hasFile(const string & name) const {
555 arx_assert_msg(name.find_first_of(BADSAVCHAR) == string::npos,
556 "bad save filename: \"%s\"", name.c_str());
557 return (files.find(name) != files.end());
558 }
559
getFiles() const560 vector<string> SaveBlock::getFiles() const {
561
562 vector<string> result;
563
564 for(Files::const_iterator file = files.begin(); file != files.end(); ++file) {
565 result.push_back(file->first);
566 }
567
568 return result;
569 }
570
load(const fs::path & savefile,const std::string & filename,size_t & size)571 char * SaveBlock::load(const fs::path & savefile, const std::string & filename, size_t & size) {
572
573 arx_assert_msg(filename.find_first_of(BADSAVCHAR) == string::npos,
574 "bad save filename: \"%s\"", filename.c_str());
575
576 LogDebug("reading savefile " << savefile);
577
578 size = 0;
579
580 fs::ifstream handle(savefile, fs::fstream::in | fs::fstream::binary);
581 if(!handle.is_open()) {
582 LogWarning << "Cannot open save file " << savefile;
583 return NULL;
584 }
585
586 u32 fatOffset;
587 if(fs::read(handle, fatOffset).fail()) {
588 return NULL;
589 }
590 if(handle.seekg(fatOffset + 4).fail()) {
591 LogError << "Cannot seek to FAT";
592 return NULL;
593 }
594
595 u32 version;
596 if(fs::read(handle, version).fail()) {
597 return NULL;
598 }
599 if(version != SAV_VERSION_DEFLATE && version != SAV_VERSION_RELEASE && version != SAV_VERSION_NOEXT) {
600 LogWarning << "Unexpected savegame version: " << version << " for " << savefile;
601 }
602
603 u32 nFiles;
604 if(fs::read(handle, nFiles).fail()) {
605 return NULL;
606 }
607
608 File file;
609
610 for(u32 i = 0; i < nFiles; i++) {
611
612 // Read the file name.
613 string name;
614 if(fs::read(handle, name).fail()) {
615 return NULL;
616 }
617 if(version < SAV_VERSION_NOEXT) {
618 boost::to_lower(name);
619 if(name.size() > 4 && !name.compare(name.size() - 4, 4, ".sav", 4)) {
620 name.resize(name.size() - 4);
621 }
622 }
623
624 if(!file.loadOffsets(handle, version)) {
625 return NULL;
626 }
627
628 if(!i && version == SAV_VERSION_OLD) {
629 continue;
630 }
631
632 if(name != filename) {
633 file.chunks.clear();
634 continue;
635 }
636
637 return file.loadData(handle, size, name);
638 }
639
640 return NULL;
641 }
642