/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "common/debug.h" #include "common/str.h" #include "common/memstream.h" #include "draci/barchive.h" #include "draci/draci.h" namespace Draci { const char BArchive::_magicNumber[] = "BAR!"; const char BArchive::_dfwMagicNumber[] = "BS"; /** * @brief Loads a DFW archive * @param path Path to input file * * Tries to load the file as a DFW archive if opening as BAR fails. Should only be called * from openArchive(). Only one of the game files appears to use this format (HRA.DFW) * and this file is compressed using a simple run-length scheme. * * archive format: header * index table * file0, file1, ... * * header format: [uint16LE] file count * [uint16LE] index table size * [2 bytes] magic number "BS" * * index table format: entry0, entry1, ... * * entry format: [uint16LE] compressed size (not including the 2 bytes for the * "uncompressed size" field) * [uint32LE] fileN offset from start of file * * file format: [uint16LE] uncompressed size * [uint16LE] compressed size (the same as in the index table entry) * [byte] stopper mark (for run-length compression) * [multiple bytes] compressed data */ void BArchive::openDFW(const Common::String &path) { byte *table; uint16 tableSize; byte buf[2]; _f.open(path); if (!_f.isOpen()) { debugC(2, kDraciArchiverDebugLevel, "Error opening file"); return; } _fileCount = _f.readUint16LE(); tableSize = _f.readUint16LE(); _f.read(buf, 2); if (memcmp(buf, _dfwMagicNumber, 2) == 0) { debugC(2, kDraciArchiverDebugLevel, "Success"); _isDFW = true; } else { debugC(2, kDraciArchiverDebugLevel, "Not a DFW archive"); _f.close(); return; } debugC(2, kDraciArchiverDebugLevel, "Archive info (DFW): %d files", _fileCount); // Read in index table table = new byte[tableSize]; _f.read(table, tableSize); // Read in file headers, but do not read the actual data yet // The data will be read on demand to save memory _files = new BAFile[_fileCount]; Common::MemoryReadStream tableReader(table, tableSize); for (uint i = 0; i < _fileCount; ++i) { _files[i]._compLength = tableReader.readUint16LE(); _files[i]._offset = tableReader.readUint32LE(); // Seek to the current file _f.seek(_files[i]._offset); _files[i]._length = _f.readUint16LE(); // Read in uncompressed length _f.readUint16LE(); // Compressed length again (already read from the index table) _files[i]._stopper = _f.readByte(); _files[i]._data = NULL; // File data will be read in on demand _files[i]._crc = 0; // Dummy value; not used in DFW archives } // Indicate that the archive was successfully opened _opened = true; // Cleanup delete[] table; } /** * @brief BArchive open method * @param path Path to input file * * Opens a BAR (Bob's Archiver) archive, which is the game's archiving format. * BAR archives have a .DFW file extension, due to a historical interface. * * archive format: header, * file0, file1, ... * footer * * header format: [4 bytes] magic number "BAR!" * [uint16LE] file count (number of archived streams), * [uint32LE] footer offset from start of file * * file format: [2 bytes] compressed length * [2 bytes] original length * [1 byte] compression type * [1 byte] CRC * [multiple bytes] actual data * * footer format: [array of uint32LE] offsets of individual files from start of archive * (last entry is footer offset again) */ void BArchive::openArchive(const Common::String &path) { byte buf[4]; byte *footer; uint32 footerOffset, footerSize; // Close previously opened archive (if any) closeArchive(); debugCN(2, kDraciArchiverDebugLevel, "Loading archive %s: ", path.c_str()); _f.open(path); if (_f.isOpen()) { debugC(2, kDraciArchiverDebugLevel, "Success"); } else { debugC(2, kDraciArchiverDebugLevel, "Error"); return; } // Save path for reading in files later on _path = path; // Read archive header debugCN(2, kDraciArchiverDebugLevel, "Checking for BAR magic number: "); _f.read(buf, 4); if (memcmp(buf, _magicNumber, 4) == 0) { debugC(2, kDraciArchiverDebugLevel, "Success"); // Indicate this archive is a BAR _isDFW = false; } else { debugC(2, kDraciArchiverDebugLevel, "Not a BAR archive"); debugCN(2, kDraciArchiverDebugLevel, "Retrying as DFW: "); _f.close(); // Try to open as DFW openDFW(_path); return; } _fileCount = _f.readUint16LE(); footerOffset = _f.readUint32LE(); footerSize = _f.size() - footerOffset; debugC(2, kDraciArchiverDebugLevel, "Archive info: %d files, %d data bytes", _fileCount, footerOffset - _archiveHeaderSize); // Read in footer footer = new byte[footerSize]; _f.seek(footerOffset); _f.read(footer, footerSize); Common::MemoryReadStream reader(footer, footerSize); // Read in file headers, but do not read the actual data yet // The data will be read on demand to save memory _files = new BAFile[_fileCount]; for (uint i = 0; i < _fileCount; i++) { uint32 fileOffset; fileOffset = reader.readUint32LE(); _f.seek(fileOffset); // Seek to next file in archive _files[i]._compLength = _f.readUint16LE(); // Compressed size // should be the same as uncompressed _files[i]._length = _f.readUint16LE(); // Original size _files[i]._offset = fileOffset; // Offset of file from start byte compressionType = _f.readByte(); assert(compressionType == 0 && "Compression type flag is non-zero (file is compressed)"); _files[i]._crc = _f.readByte(); // CRC checksum of the file _files[i]._data = NULL; // File data will be read in on demand _files[i]._stopper = 0; // Dummy value; not used in BAR files, needed in DFW } // Last footer item should be equal to footerOffset uint32 footerOffset2 = reader.readUint32LE(); assert(footerOffset2 == footerOffset && "Footer offset mismatch"); // Indicate that the archive has been successfully opened _opened = true; delete[] footer; } /** * @brief BArchive close method * * Closes the currently opened archive. It can be called explicitly to * free up memory. */ void BArchive::closeArchive() { if (!_opened) { return; } for (uint i = 0; i < _fileCount; ++i) { if (_files[i]._data) { delete[] _files[i]._data; } } delete[] _files; _f.close(); _opened = false; _files = NULL; _fileCount = 0; } /** * @brief On-demand BAR file loader * @param i Index of file inside an archive * @return Pointer to a BAFile coresponding to the opened file or NULL (on failure) * * Loads individual BAR files from an archive to memory on demand. * Should not be called directly. */ BAFile *BArchive::loadFileBAR(uint i) { // Else open archive and read in requested file if (!_f.isOpen()) { debugC(2, kDraciArchiverDebugLevel, "Error"); return NULL; } // Read in the file (without the file header) _f.seek(_files[i]._offset + _fileHeaderSize); _files[i]._data = new byte[_files[i]._length]; _f.read(_files[i]._data, _files[i]._length); // Calculate CRC byte tmp = 0; for (uint j = 0; j < _files[i]._length; j++) { tmp ^= _files[i]._data[j]; } debugC(2, kDraciArchiverDebugLevel, "Read %d bytes", _files[i]._length); assert(tmp == _files[i]._crc && "CRC checksum mismatch"); return _files + i; } /** * @brief On-demand DFW file loader * @param i Index of file inside an archive * @return Pointer to a BAFile coresponding to the opened file or NULL (on failure) * * Loads individual DFW files from an archive to memory on demand. * Should not be called directly. */ BAFile *BArchive::loadFileDFW(uint i) { byte *buf; // Else open archive and read in requested file if (!_f.isOpen()) { debugC(2, kDraciArchiverDebugLevel, "Error"); return NULL; } // Seek to raw data of the file // Five bytes are for the header (uncompressed and compressed length, stopper mark) _f.seek(_files[i]._offset + 5); // Since we are seeking directly to raw data, we subtract 3 bytes from the length // (to take account the compressed length and stopper mark) uint16 compressedLength = _files[i]._compLength - 3; uint16 uncompressedLength = _files[i]._length; debugC(2, kDraciArchiverDebugLevel, "File info (DFW): uncompressed %d bytes, compressed %d bytes", uncompressedLength, compressedLength); // Allocate a buffer for the file data buf = new byte[compressedLength]; // Read in file data into the buffer _f.read(buf, compressedLength); // Allocate the space for the uncompressed file byte *dst; dst = _files[i]._data = new byte[uncompressedLength]; Common::MemoryReadStream data(buf, compressedLength); // Uncompress file byte current, what; byte stopper = _files[i]._stopper; uint repeat; uint len = 0; // Sanity check (counts uncompressed bytes) current = data.readByte(); // Read initial byte while (!data.eos()) { if (current != stopper) { *dst++ = current; ++len; } else { // Inflate block repeat = data.readByte(); what = data.readByte(); len += repeat; for (uint j = 0; j < repeat; ++j) { *dst++ = what; } } current = data.readByte(); } assert(len == _files[i]._length && "Uncompressed file not of the expected length"); delete[] buf; return _files + i; } /** * Clears the cache of the open files inside the archive without closing it. * If the files are subsequently accessed, they are read from the disk. */ void BArchive::clearCache() { // Delete all cached data for (uint i = 0; i < _fileCount; ++i) { _files[i].close(); } } const BAFile *BArchive::getFile(uint i) { // Check whether requested file exists if (i >= _fileCount) { return NULL; } debugCN(2, kDraciArchiverDebugLevel, "Accessing file %d from archive %s... ", i, _path.c_str()); // Check if file has already been opened and return that if (_files[i]._data) { debugC(2, kDraciArchiverDebugLevel, "Cached"); return _files + i; } BAFile *file; // file will be NULL if something goes wrong if (_isDFW) { file = loadFileDFW(i); } else { file = loadFileBAR(i); } return file; } } // End of namespace Draci