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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
26ZipHandler::ZipHandler()
27{
28}
29
30// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
31void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
80void ZipHandler::reset()
81{
82 // Reset the position and go from there
83 if(myZip)
84 myZip->myCdPos = 0;
85}
86
87// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
88bool ZipHandler::hasNext() const
89{
90 return myZip && (myZip->myCdPos < myZip->myEcd.cdSize);
91}
92
93// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
94const string& ZipHandler::next()
95{
96 if(hasNext())
97 {
98 const ZipHeader* header = myZip->nextFile();
99 if(!header || header->uncompressedLength == 0)
100 return next();
101 else
102 return header->filename;
103 }
104 return EmptyString;
105}
106
107// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
108uInt64 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
132string 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
150ZipHandler::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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
167void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
202ZipHandler::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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
213bool 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
230void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
262void ZipHandler::ZipFile::close()
263{
264 if(myStream.is_open())
265 myStream.close();
266}
267
268// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
269void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
326bool 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
344const 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
370void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
410uInt64 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
436void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
449void 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
520ZipHandler::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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
536ZipHandler::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