1 | //============================================================================ |
2 | // |
3 | // SSSS tt lll lll |
4 | // SS SS tt ll ll |
5 | // SS tttttt eeee ll ll aaaa |
6 | // SSSS tt ee ee ll ll aa |
7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
8 | // SS SS tt ee ll ll aa aa |
9 | // SSSS ttt eeeee llll llll aaaaa |
10 | // |
11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
12 | // and the Stella Team |
13 | // |
14 | // See the file "License.txt" for information on usage and redistribution of |
15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
16 | //============================================================================ |
17 | |
18 | #if defined(ZIP_SUPPORT) |
19 | |
20 | #include <zlib.h> |
21 | |
22 | #include "Bankswitch.hxx" |
23 | #include "ZipHandler.hxx" |
24 | |
25 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
26 | ZipHandler::ZipHandler() |
27 | { |
28 | } |
29 | |
30 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
31 | void ZipHandler::open(const string& filename) |
32 | { |
33 | // Close already open file (if any) and add to cache |
34 | addToCache(); |
35 | |
36 | // Ensure we start with a nullptr result |
37 | myZip.reset(); |
38 | |
39 | ZipFilePtr ptr = findCached(filename); |
40 | if(ptr) |
41 | { |
42 | // Only a previously used entry will exist in the cache, so we know it's valid |
43 | myZip = std::move(ptr); |
44 | |
45 | // Was already initialized; we just need to re-open it |
46 | if(!myZip->open()) |
47 | throw runtime_error(errorMessage(ZipError::FILE_ERROR)); |
48 | } |
49 | else |
50 | { |
51 | // Allocate memory for the ZipFile structure |
52 | ptr = make_unique<ZipFile>(filename); |
53 | if(ptr == nullptr) |
54 | throw runtime_error(errorMessage(ZipError::OUT_OF_MEMORY)); |
55 | |
56 | // Open the file and initialize it |
57 | if(!ptr->open()) |
58 | throw runtime_error(errorMessage(ZipError::FILE_ERROR)); |
59 | try |
60 | { |
61 | ptr->initialize(); |
62 | } |
63 | catch(const ZipError& err) |
64 | { |
65 | throw runtime_error(errorMessage(err)); |
66 | } |
67 | |
68 | myZip = std::move(ptr); |
69 | |
70 | // Count ROM files (we do it here so it will be cached) |
71 | while(hasNext()) |
72 | if(Bankswitch::isValidRomName(next())) |
73 | myZip->myRomfiles++; |
74 | } |
75 | |
76 | reset(); // Reset iterator to beginning for subsequent use |
77 | } |
78 | |
79 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
80 | void ZipHandler::reset() |
81 | { |
82 | // Reset the position and go from there |
83 | if(myZip) |
84 | myZip->myCdPos = 0; |
85 | } |
86 | |
87 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
88 | bool ZipHandler::hasNext() const |
89 | { |
90 | return myZip && (myZip->myCdPos < myZip->myEcd.cdSize); |
91 | } |
92 | |
93 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
94 | const string& ZipHandler::next() |
95 | { |
96 | if(hasNext()) |
97 | { |
98 | const ZipHeader* = myZip->nextFile(); |
99 | if(!header || header->uncompressedLength == 0) |
100 | return next(); |
101 | else |
102 | return header->filename; |
103 | } |
104 | return EmptyString; |
105 | } |
106 | |
107 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
108 | uInt64 ZipHandler::decompress(ByteBuffer& image) |
109 | { |
110 | if(myZip && myZip->myHeader.uncompressedLength > 0) |
111 | { |
112 | uInt64 length = myZip->myHeader.uncompressedLength; |
113 | image = make_unique<uInt8[]>(length); |
114 | if(image == nullptr) |
115 | throw runtime_error(errorMessage(ZipError::OUT_OF_MEMORY)); |
116 | |
117 | try |
118 | { |
119 | myZip->decompress(image, length); |
120 | return length; |
121 | } |
122 | catch(const ZipError& err) |
123 | { |
124 | throw runtime_error(errorMessage(err)); |
125 | } |
126 | } |
127 | else |
128 | throw runtime_error("Invalid ZIP archive" ); |
129 | } |
130 | |
131 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
132 | string ZipHandler::errorMessage(ZipError err) const |
133 | { |
134 | static const char* const zip_error_s[] = { |
135 | "ZIP NONE" , |
136 | "ZIP OUT_OF_MEMORY" , |
137 | "ZIP FILE_ERROR" , |
138 | "ZIP BAD_SIGNATURE" , |
139 | "ZIP DECOMPRESS_ERROR" , |
140 | "ZIP FILE_TRUNCATED" , |
141 | "ZIP FILE_CORRUPT" , |
142 | "ZIP UNSUPPORTED" , |
143 | "ZIP LZMA_UNSUPPORTED" , |
144 | "ZIP BUFFER_TOO_SMALL" |
145 | }; |
146 | return zip_error_s[int(err)]; |
147 | } |
148 | |
149 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
150 | ZipHandler::ZipFilePtr ZipHandler::findCached(const string& filename) |
151 | { |
152 | for(size_t cachenum = 0; cachenum < myZipCache.size(); ++cachenum) |
153 | { |
154 | // If we have a valid entry and it matches our filename, |
155 | // use it and remove from the cache |
156 | if(myZipCache[cachenum] && (filename == myZipCache[cachenum]->myFilename)) |
157 | { |
158 | ZipFilePtr result; |
159 | std::swap(myZipCache[cachenum], result); |
160 | return result; |
161 | } |
162 | } |
163 | return ZipFilePtr(); |
164 | } |
165 | |
166 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
167 | void ZipHandler::addToCache() |
168 | { |
169 | if(myZip == nullptr) |
170 | return; |
171 | |
172 | // Close the open file |
173 | myZip->close(); |
174 | |
175 | // Find the first nullptr entry in the cache |
176 | size_t cachenum; |
177 | for(cachenum = 0; cachenum < myZipCache.size(); ++cachenum) |
178 | if(myZipCache[cachenum] == nullptr) |
179 | break; |
180 | |
181 | // If no room left in the cache, free the bottommost entry |
182 | if(cachenum == myZipCache.size()) |
183 | { |
184 | cachenum--; |
185 | myZipCache[cachenum].reset(); |
186 | } |
187 | |
188 | for( ; cachenum > 0; --cachenum) |
189 | myZipCache[cachenum] = std::move(myZipCache[cachenum - 1]); |
190 | myZipCache[0] = std::move(myZip); |
191 | |
192 | #if 0 |
193 | cerr << "\nCACHE contents:\n" ; |
194 | for(cachenum = 0; cachenum < myZipCache.size(); ++cachenum) |
195 | if(myZipCache[cachenum] != nullptr) |
196 | cerr << " " << cachenum << " : " << myZipCache[cachenum]->filename << endl; |
197 | cerr << endl; |
198 | #endif |
199 | } |
200 | |
201 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
202 | ZipHandler::ZipFile::ZipFile(const string& filename) |
203 | : myFilename(filename), |
204 | myLength(0), |
205 | myRomfiles(0), |
206 | myCdPos(0), |
207 | myBuffer(make_unique<uInt8[]>(DECOMPRESS_BUFSIZE)) |
208 | { |
209 | std::fill(myBuffer.get(), myBuffer.get() + DECOMPRESS_BUFSIZE, 0); |
210 | } |
211 | |
212 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
213 | bool ZipHandler::ZipFile::open() |
214 | { |
215 | myStream.open(myFilename, fstream::in | fstream::binary); |
216 | if(!myStream.is_open()) |
217 | { |
218 | myLength = 0; |
219 | return false; |
220 | } |
221 | myStream.exceptions( std::ios_base::failbit | std::ios_base::badbit | std::ios_base::eofbit ); |
222 | myStream.seekg(0, std::ios::end); |
223 | myLength = myStream.tellg(); |
224 | myStream.seekg(0, std::ios::beg); |
225 | |
226 | return true; |
227 | } |
228 | |
229 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
230 | void ZipHandler::ZipFile::initialize() |
231 | { |
232 | try |
233 | { |
234 | // Read ecd data |
235 | readEcd(); |
236 | |
237 | // Verify that we can work with this zipfile (no disk spanning allowed) |
238 | if(myEcd.diskNumber != myEcd.cdStartDiskNumber || |
239 | myEcd.cdDiskEntries != myEcd.cdTotalEntries) |
240 | throw ZipError::UNSUPPORTED; |
241 | |
242 | // Allocate memory for the central directory |
243 | myCd = make_unique<uInt8[]>(myEcd.cdSize + 1); |
244 | if(myCd == nullptr) |
245 | throw ZipError::OUT_OF_MEMORY; |
246 | |
247 | // Read the central directory |
248 | uInt64 read_length = 0; |
249 | bool success = readStream(myCd, myEcd.cdStartDiskOffset, myEcd.cdSize, read_length); |
250 | if(!success) |
251 | throw ZipError::FILE_ERROR; |
252 | else if(read_length != myEcd.cdSize) |
253 | throw ZipError::FILE_TRUNCATED; |
254 | } |
255 | catch(const ZipError&) |
256 | { |
257 | throw; |
258 | } |
259 | } |
260 | |
261 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
262 | void ZipHandler::ZipFile::close() |
263 | { |
264 | if(myStream.is_open()) |
265 | myStream.close(); |
266 | } |
267 | |
268 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
269 | void ZipHandler::ZipFile::readEcd() |
270 | { |
271 | uInt64 buflen = 1024; |
272 | ByteBuffer buffer; |
273 | |
274 | // We may need multiple tries |
275 | while(buflen < 65536) |
276 | { |
277 | uInt64 read_length; |
278 | |
279 | // Max out the buf length at the size of the file |
280 | if(buflen > myLength) |
281 | buflen = myLength; |
282 | |
283 | // Allocate buffer |
284 | buffer = make_unique<uInt8[]>(buflen + 1); |
285 | if(buffer == nullptr) |
286 | throw ZipError::OUT_OF_MEMORY; |
287 | |
288 | // Read in one buffers' worth of data |
289 | bool success = readStream(buffer, myLength - buflen, buflen, read_length); |
290 | if(!success || read_length != buflen) |
291 | throw ZipError::FILE_ERROR; |
292 | |
293 | // Find the ECD signature |
294 | Int32 offset; |
295 | for(offset = Int32(buflen - EcdReader::minimumLength()); offset >= 0; --offset) |
296 | { |
297 | EcdReader reader(buffer.get() + offset); |
298 | if(reader.signatureCorrect() && ((reader.totalLength() + offset) <= buflen)) |
299 | break; |
300 | } |
301 | |
302 | // If we found it, fill out the data |
303 | if(offset >= 0) |
304 | { |
305 | // Extract ECD info |
306 | EcdReader const reader(buffer.get() + offset); |
307 | myEcd.diskNumber = reader.thisDiskNo(); |
308 | myEcd.cdStartDiskNumber = reader.dirStartDisk(); |
309 | myEcd.cdDiskEntries = reader.dirDiskEntries(); |
310 | myEcd.cdTotalEntries = reader.dirTotalEntries(); |
311 | myEcd.cdSize = reader.dirSize(); |
312 | myEcd.cdStartDiskOffset = reader.dirOffset(); |
313 | return; |
314 | } |
315 | |
316 | // Didn't find it; expand our search |
317 | if(buflen < myLength) |
318 | buflen *= 2; |
319 | else |
320 | throw ZipError::BAD_SIGNATURE; |
321 | } |
322 | throw ZipError::OUT_OF_MEMORY; |
323 | } |
324 | |
325 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
326 | bool ZipHandler::ZipFile::readStream(ByteBuffer& out, uInt64 offset, |
327 | uInt64 length, uInt64& actual) |
328 | { |
329 | try |
330 | { |
331 | myStream.seekg(offset); |
332 | myStream.read(reinterpret_cast<char*>(out.get()), length); |
333 | |
334 | actual = myStream.gcount(); |
335 | return true; |
336 | } |
337 | catch(...) |
338 | { |
339 | return false; |
340 | } |
341 | } |
342 | |
343 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
344 | const ZipHandler::ZipHeader* ZipHandler::ZipFile::nextFile() |
345 | { |
346 | // Make sure we have enough data |
347 | // If we're at or past the end, we're done |
348 | CentralDirEntryReader const reader(myCd.get() + myCdPos); |
349 | if(!reader.signatureCorrect() || ((myCdPos + reader.totalLength()) > myEcd.cdSize)) |
350 | return nullptr; |
351 | |
352 | // Extract file header info |
353 | myHeader.versionCreated = reader.versionCreated(); |
354 | myHeader.versionNeeded = reader.versionNeeded(); |
355 | myHeader.bitFlag = reader.generalFlag(); |
356 | myHeader.compression = reader.compressionMethod(); |
357 | myHeader.crc = reader.crc32(); |
358 | myHeader.compressedLength = reader.compressedSize(); |
359 | myHeader.uncompressedLength = reader.uncompressedSize(); |
360 | myHeader.startDiskNumber = reader.startDisk(); |
361 | myHeader.localHeaderOffset = reader.headerOffset(); |
362 | myHeader.filename = reader.filename(); |
363 | |
364 | // Advance the position |
365 | myCdPos += reader.totalLength(); |
366 | return &myHeader; |
367 | } |
368 | |
369 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
370 | void ZipHandler::ZipFile::decompress(ByteBuffer& out, uInt64 length) |
371 | { |
372 | // If we don't have enough buffer, error |
373 | if(length < myHeader.uncompressedLength) |
374 | throw ZipError::BUFFER_TOO_SMALL; |
375 | |
376 | // Make sure the info in the header aligns with what we know |
377 | if(myHeader.startDiskNumber != myEcd.diskNumber) |
378 | throw ZipError::UNSUPPORTED; |
379 | |
380 | try |
381 | { |
382 | // Get the compressed data offset |
383 | uInt64 offset = getCompressedDataOffset(); |
384 | |
385 | // Handle compression types |
386 | switch(myHeader.compression) |
387 | { |
388 | case 0: |
389 | decompressDataType0(offset, out, length); |
390 | break; |
391 | |
392 | case 8: |
393 | decompressDataType8(offset, out, length); |
394 | break; |
395 | |
396 | case 14: |
397 | throw ZipError::LZMA_UNSUPPORTED; // FIXME - LZMA format not yet supported |
398 | |
399 | default: |
400 | throw ZipError::UNSUPPORTED; |
401 | } |
402 | } |
403 | catch(const ZipError&) |
404 | { |
405 | throw; |
406 | } |
407 | } |
408 | |
409 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
410 | uInt64 ZipHandler::ZipFile::getCompressedDataOffset() |
411 | { |
412 | // Don't support a number of features |
413 | GeneralFlagReader const flags(myHeader.bitFlag); |
414 | if(myHeader.startDiskNumber != myEcd.diskNumber || |
415 | myHeader.versionNeeded > 63 || flags.patchData() || |
416 | flags.encrypted() || flags.strongEncryption()) |
417 | throw ZipError::UNSUPPORTED; |
418 | |
419 | // Read the fixed-sized part of the local file header |
420 | uInt64 read_length = 0; |
421 | bool success = readStream(myBuffer, myHeader.localHeaderOffset, 0x1e, read_length); |
422 | if(!success) |
423 | throw ZipError::FILE_ERROR; |
424 | else if(read_length != LocalFileHeaderReader::minimumLength()) |
425 | throw ZipError::FILE_TRUNCATED; |
426 | |
427 | // Compute the final offset |
428 | LocalFileHeaderReader reader(&myBuffer[0]); |
429 | if(!reader.signatureCorrect()) |
430 | throw ZipError::BAD_SIGNATURE; |
431 | |
432 | return myHeader.localHeaderOffset + reader.totalLength(); |
433 | } |
434 | |
435 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
436 | void ZipHandler::ZipFile::decompressDataType0( |
437 | uInt64 offset, ByteBuffer& out, uInt64 length) |
438 | { |
439 | // The data is uncompressed; just read it |
440 | uInt64 read_length = 0; |
441 | bool success = readStream(out, offset, myHeader.compressedLength, read_length); |
442 | if(!success) |
443 | throw ZipError::FILE_ERROR; |
444 | else if(read_length != myHeader.compressedLength) |
445 | throw ZipError::FILE_TRUNCATED; |
446 | } |
447 | |
448 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
449 | void ZipHandler::ZipFile::decompressDataType8( |
450 | uInt64 offset, ByteBuffer& out, uInt64 length) |
451 | { |
452 | uInt64 input_remaining = myHeader.compressedLength; |
453 | |
454 | // Reset the stream |
455 | z_stream stream; |
456 | stream.zalloc = Z_NULL; |
457 | stream.zfree = Z_NULL; |
458 | stream.opaque = Z_NULL; |
459 | stream.avail_in = 0; |
460 | stream.next_out = reinterpret_cast<Bytef *>(out.get()); |
461 | stream.avail_out = uInt32(length); // TODO - use zip64 |
462 | |
463 | // Initialize the decompressor |
464 | int zerr = inflateInit2(&stream, -MAX_WBITS); |
465 | if(zerr != Z_OK) |
466 | throw ZipError::DECOMPRESS_ERROR; |
467 | |
468 | // Loop until we're done |
469 | for(;;) |
470 | { |
471 | // Read in the next chunk of data |
472 | uInt64 read_length = 0; |
473 | bool success = readStream(myBuffer, offset, |
474 | std::min(input_remaining, uInt64(sizeof(myBuffer.get()))), read_length); |
475 | if(!success) |
476 | { |
477 | inflateEnd(&stream); |
478 | throw ZipError::FILE_ERROR; |
479 | } |
480 | offset += read_length; |
481 | |
482 | // If we read nothing, but still have data left, the file is truncated |
483 | if(read_length == 0 && input_remaining > 0) |
484 | { |
485 | inflateEnd(&stream); |
486 | throw ZipError::FILE_TRUNCATED; |
487 | } |
488 | |
489 | // Fill out the input data |
490 | stream.next_in = myBuffer.get(); |
491 | stream.avail_in = uInt32(read_length); // TODO - use zip64 |
492 | input_remaining -= read_length; |
493 | |
494 | // Add a dummy byte at end of compressed data |
495 | if(input_remaining == 0) |
496 | stream.avail_in++; |
497 | |
498 | // Now inflate |
499 | zerr = inflate(&stream, Z_NO_FLUSH); |
500 | if(zerr == Z_STREAM_END) |
501 | break; |
502 | else if(zerr != Z_OK) |
503 | { |
504 | inflateEnd(&stream); |
505 | throw ZipError::DECOMPRESS_ERROR; |
506 | } |
507 | } |
508 | |
509 | // Finish decompression |
510 | zerr = inflateEnd(&stream); |
511 | if(zerr != Z_OK) |
512 | throw ZipError::DECOMPRESS_ERROR; |
513 | |
514 | // If anything looks funny, report an error |
515 | if(stream.avail_out > 0 || input_remaining > 0) |
516 | throw ZipError::DECOMPRESS_ERROR; |
517 | } |
518 | |
519 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
520 | ZipHandler::ZipHeader::ZipHeader() |
521 | : versionCreated(0), |
522 | versionNeeded(0), |
523 | bitFlag(0), |
524 | compression(0), |
525 | fileTime(0), |
526 | fileDate(0), |
527 | crc(0), |
528 | compressedLength(0), |
529 | uncompressedLength(0), |
530 | startDiskNumber(0), |
531 | localHeaderOffset(0) |
532 | { |
533 | } |
534 | |
535 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
536 | ZipHandler::ZipEcd::ZipEcd() |
537 | : diskNumber(0), |
538 | cdStartDiskNumber(0), |
539 | cdDiskEntries(0), |
540 | cdTotalEntries(0), |
541 | cdSize(0), |
542 | cdStartDiskOffset(0) |
543 | { |
544 | } |
545 | |
546 | #endif /* ZIP_SUPPORT */ |
547 | |