/* 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 3 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, see .
*
*/
// The following code is based on unshield
// Original copyright:
// Copyright (c) 2003 David Eriksson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include "common/archive.h"
#include "common/debug.h"
#include "common/hash-str.h"
#include "common/compression/installshield_cab.h"
#include "common/memstream.h"
#include "common/substream.h"
#include "common/ptr.h"
#include "common/compression/deflate.h"
#include "common/file.h"
namespace Common {
namespace {
bool inflateZlibInstallShield(byte *dst, uint dstLen, const byte *src, uint srcLen) {
if (!dst || !dstLen || !src || !srcLen)
return false;
// See if we have sync bytes. If so, just use our function for that.
if (srcLen >= 4 && READ_BE_UINT32(src + srcLen - 4) == 0xFFFF)
return inflateZlibHeaderless(dst, &dstLen, src, srcLen);
// Otherwise, we have some custom code we get to use here.
uint32 bytesRead = 0, bytesProcessed = 0;
while (dstLen > 0 && bytesRead < srcLen) {
uint16 chunkSize = READ_LE_UINT16(src + bytesRead);
bytesRead += 2;
uint zlibLen = dstLen;
if (!inflateZlibHeaderless(dst + bytesProcessed, &zlibLen, src + bytesRead, chunkSize)) {
return false;
}
bytesProcessed += zlibLen;
dstLen -= zlibLen;
bytesRead += chunkSize;
}
return true;
}
class InstallShieldCabinet : public Archive {
public:
InstallShieldCabinet();
bool open(const Path *baseName, Common::Archive *archive, const FSNode *node);
void close();
// Archive API implementation
bool hasFile(const Path &path) const override;
int listMembers(ArchiveMemberList &list) const override;
const ArchiveMemberPtr getMember(const Path &path) const override;
SeekableReadStream *createReadStreamForMember(const Path &path) const override;
private:
enum Flags { kSplit = 1, kObfuscated = 2, kCompressed = 4, kInvalid = 8 };
struct FileEntry {
uint32 uncompressedSize;
uint32 compressedSize;
uint32 offset;
uint16 flags;
uint16 volume;
};
struct VolumeHeader {
int version;
uint32 cabDescriptorOffset;
uint32 dataOffset;
uint32 firstFileIndex;
uint32 lastFileIndex;
uint32 firstFileOffset;
uint32 firstFileSizeUncompressed;
uint32 firstFileSizeCompressed;
uint32 lastFileOffset;
uint32 lastFileSizeUncompressed;
uint32 lastFileSizeCompressed;
};
int _version;
typedef HashMap FileMap;
FileMap _map;
Path _baseName;
Common::Array _volumeHeaders;
Common::Archive *_archive;
static bool readVolumeHeader(SeekableReadStream *volumeStream, VolumeHeader &inVolumeHeader);
Path getHeaderName() const;
Path getVolumeName(uint volume) const;
};
InstallShieldCabinet::InstallShieldCabinet() : _version(0), _archive(nullptr) {
}
bool InstallShieldCabinet::open(const Path *baseName, Common::Archive *archive, const FSNode *node) {
// Store the base name so we can generate file names
if (baseName) {
_baseName = *baseName;
_archive = archive;
} else if (node) {
_baseName = node->getPath();
_archive = nullptr;
} else {
return false;
}
String strippedName = _baseName.baseName();
if (strippedName.hasSuffix(".cab") || strippedName.hasSuffix(".hdr")) {
strippedName.erase(strippedName.size() - 5, String::npos);
_baseName = _baseName.getParent().appendComponent(strippedName);
}
uint fileIndex = 0;
ScopedPtr file;
// First, open all the .cab files and read their headers
uint volume = 1;
for (;;) {
if (_archive) {
file.reset(_archive->createReadStreamForMember(getVolumeName(volume++)));
if (!file.get()) {
break;
}
} else {
file.reset(new Common::File());
if (!((Common::File *)file.get())->open(Common::FSNode(getVolumeName(volume++)))) {
break;
}
}
_volumeHeaders.push_back(VolumeHeader());
readVolumeHeader(file.get(), _volumeHeaders.back());
}
// Try to open a header (.hdr) file to get the file list
if (_archive) {
file.reset(_archive->createReadStreamForMember(getHeaderName()));
if (!file) {
// No header file is present, file list is in first .cab file
file.reset(_archive->createReadStreamForMember(getVolumeName(1)));
}
} else {
file.reset(new Common::File());
if (!((Common::File *)file.get())->open(Common::FSNode(getHeaderName()))) {
// No header file is present, file list is in first .cab file
if (!((Common::File *)file.get())->open(Common::FSNode(getVolumeName(1)))) {
file.reset(nullptr);
}
}
}
if (!file) {
close();
return false;
}
VolumeHeader headerHeader;
if (!readVolumeHeader(file.get(), headerHeader)) {
close();
return false;
}
_version = headerHeader.version;
file->seek(headerHeader.cabDescriptorOffset);
file->skip(12);
uint32 fileTableOffset = file->readUint32LE();
file->skip(4);
uint32 fileTableSize = file->readUint32LE();
uint32 fileTableSize2 = file->readUint32LE();
uint32 directoryCount = file->readUint32LE();
file->skip(8);
uint32 fileCount = file->readUint32LE();
if (fileTableSize != fileTableSize2)
warning("file table sizes do not match");
// We're ignoring file groups and components since we
// should not need them. Moving on to the files...
if (_version >= 6) {
uint32 fileTableOffset2 = file->readUint32LE();
for (uint32 j = 0; j < fileCount; j++) {
file->seek(headerHeader.cabDescriptorOffset + fileTableOffset + fileTableOffset2 + j * 0x57);
FileEntry entry;
entry.flags = file->readUint16LE();
entry.uncompressedSize = file->readUint32LE();
file->skip(4);
entry.compressedSize = file->readUint32LE();
file->skip(4);
entry.offset = file->readUint32LE();
file->skip(36);
uint32 nameOffset = file->readUint32LE();
/* uint32 directoryIndex = */ file->readUint16LE();
file->skip(12);
/* entry.linkPrev = */ file->readUint32LE();
/* entry.linkNext = */ file->readUint32LE();
/* entry.linkFlags = */ file->readByte();
entry.volume = file->readUint16LE();
// Make sure the entry has a name and data inside the cab
if (nameOffset == 0 || entry.offset == 0 || (entry.flags & kInvalid))
continue;
// Then let's get the string
file->seek(headerHeader.cabDescriptorOffset + fileTableOffset + nameOffset);
Path fileName(file->readString(), '\\');
// Entries can appear in multiple volumes (sometimes erroneously).
// We keep the one with the lowest volume ID
if (!_map.contains(fileName) || _map[fileName].volume > entry.volume)
_map[fileName] = entry;
}
} else {
file->seek(headerHeader.cabDescriptorOffset + fileTableOffset);
uint32 fileTableCount = directoryCount + fileCount;
Array fileTableOffsets;
fileTableOffsets.resize(fileTableCount);
for (uint32 j = 0; j < fileTableCount; j++)
fileTableOffsets[j] = file->readUint32LE();
for (uint32 j = directoryCount; j < fileCount + directoryCount; j++) {
file->seek(headerHeader.cabDescriptorOffset + fileTableOffset + fileTableOffsets[j]);
uint32 nameOffset = file->readUint32LE();
/* uint32 directoryIndex = */ file->readUint32LE();
// First read in data needed by us to get at the file data
FileEntry entry;
entry.flags = file->readUint16LE();
entry.uncompressedSize = file->readUint32LE();
entry.compressedSize = file->readUint32LE();
file->skip(20);
entry.offset = file->readUint32LE();
entry.volume = 0;
// Make sure the entry has a name and data inside the cab
if (nameOffset == 0 || entry.offset == 0 || (entry.flags & kInvalid))
continue;
for (uint i = 1; i < _volumeHeaders.size() + 1; ++i) {
// Check which volume the file is in
VolumeHeader &volumeHeader = _volumeHeaders[i - 1];
if (fileIndex >= volumeHeader.firstFileIndex && fileIndex <= volumeHeader.lastFileIndex) {
entry.volume = i;
// Check if the file is split across volumes
if (fileIndex == volumeHeader.lastFileIndex &&
entry.compressedSize != headerHeader.lastFileSizeCompressed &&
headerHeader.lastFileSizeCompressed != 0) {
entry.flags |= kSplit;
}
break;
}
}
// Then let's get the string
file->seek(headerHeader.cabDescriptorOffset + fileTableOffset + nameOffset);
Path fileName(file->readString(), '\\');
if (entry.volume == 0) {
warning("Couldn't find the volume for file %s", fileName.toString('\\').c_str());
close();
return false;
}
++fileIndex;
// Entries can appear in multiple volumes (sometimes erroneously).
// We keep the one with the lowest volume ID
if (!_map.contains(fileName) || _map[fileName].volume > entry.volume)
_map[fileName] = entry;
}
}
return true;
}
void InstallShieldCabinet::close() {
_baseName.clear();
_map.clear();
_volumeHeaders.clear();
_version = 0;
}
bool InstallShieldCabinet::hasFile(const Path &path) const {
return _map.contains(path);
}
int InstallShieldCabinet::listMembers(ArchiveMemberList &list) const {
for (const auto &file : _map)
list.push_back(getMember(file._key));
return _map.size();
}
const ArchiveMemberPtr InstallShieldCabinet::getMember(const Path &path) const {
return ArchiveMemberPtr(new GenericArchiveMember(path, *this));
}
SeekableReadStream *InstallShieldCabinet::createReadStreamForMember(const Path &path) const {
if (!_map.contains(path))
return nullptr;
const FileEntry &entry = _map[path];
if (entry.flags & kObfuscated) {
warning("Cannot extract obfuscated file %s", path.toString().c_str());
return nullptr;
}
ScopedPtr stream;
if (_archive) {
stream.reset(_archive->createReadStreamForMember(getVolumeName((entry.volume))));
} else {
stream.reset(new Common::File());
if (!((Common::File *)stream.get())->open(Common::FSNode(getVolumeName((entry.volume))))) {
stream.reset(nullptr);
}
}
if (!stream) {
warning("Failed to open volume for file '%s'", path.toString().c_str());
return nullptr;
}
byte *src = nullptr;
if (entry.flags & kSplit) {
// File is split across volumes
src = (byte *)malloc(entry.compressedSize);
uint bytesRead = 0;
uint volume = entry.volume;
// Read the first part of the split file
stream->seek(entry.offset);
stream->read(src, _volumeHeaders[volume - 1].lastFileSizeCompressed);
bytesRead += _volumeHeaders[volume - 1].lastFileSizeCompressed;
// Then, iterate through the next volumes until we've read all the data for the file
while (bytesRead < entry.compressedSize) {
if (_archive) {
stream.reset(_archive->createReadStreamForMember(getVolumeName((++volume))));
} else {
if (!((Common::File *)stream.get())->open(Common::FSNode(getVolumeName((++volume))))) {
stream.reset(nullptr);
}
}
if (!stream.get()) {
warning("Failed to read split file %s", path.toString().c_str());
free(src);
return nullptr;
}
stream->seek(_volumeHeaders[volume - 1].firstFileOffset);
stream->read(src + bytesRead, _volumeHeaders[volume - 1].firstFileSizeCompressed);
bytesRead += _volumeHeaders[volume - 1].firstFileSizeCompressed;
}
}
// Uncompressed file
if (!(entry.flags & kCompressed)) {
if (src == nullptr) {
// File not split, return a substream
return new SeekableSubReadStream(stream.release(), entry.offset, entry.offset + entry.uncompressedSize, DisposeAfterUse::YES);
} else {
// File split, return the assembled data
return new MemoryReadStream(src, entry.uncompressedSize, DisposeAfterUse::YES);
}
}
byte *dst = (byte *)malloc(entry.uncompressedSize);
if (!src) {
src = (byte *)malloc(entry.compressedSize);
stream->seek(entry.offset);
stream->read(src, entry.compressedSize);
}
// Entries with size 0 are valid, and do not need to be inflated
if (entry.compressedSize != 0) {
if (!inflateZlibInstallShield(dst, entry.uncompressedSize, src, entry.compressedSize)) {
warning("failed to inflate CAB file '%s'", path.toString().c_str());
free(dst);
free(src);
return nullptr;
}
}
free(src);
return new MemoryReadStream(dst, entry.uncompressedSize, DisposeAfterUse::YES);
}
bool InstallShieldCabinet::readVolumeHeader(SeekableReadStream *volumeStream, InstallShieldCabinet::VolumeHeader &inVolumeHeader) {
// Check for the cab signature
volumeStream->seek(0);
uint32 signature = volumeStream->readUint32LE();
if (signature != 0x28635349) {
warning("InstallShieldCabinet signature doesn't match: expecting %x but got %x", 0x28635349, signature);
return false;
}
// We support cabinet versions 5 - 13, but do not deobfuscate obfuscated files
uint32 magicBytes = volumeStream->readUint32LE();
int shift = magicBytes >> 24;
inVolumeHeader.version = shift == 1 ? (magicBytes >> 12) & 0xf : (magicBytes & 0xffff) / 100;
inVolumeHeader.version = (inVolumeHeader.version == 0) ? 5 : inVolumeHeader.version;
if (inVolumeHeader.version < 5 || inVolumeHeader.version > 13) {
warning("Unsupported CAB version %d, magic bytes %08x", inVolumeHeader.version, magicBytes);
return false;
}
/* uint32 volumeInfo = */ volumeStream->readUint32LE();
inVolumeHeader.cabDescriptorOffset = volumeStream->readUint32LE();
/* uint32 cabDescriptorSize = */ volumeStream->readUint32LE();
// Read the version-specific part of the header
if (inVolumeHeader.version == 5) {
inVolumeHeader.dataOffset = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.firstFileIndex = volumeStream->readUint32LE();
inVolumeHeader.lastFileIndex = volumeStream->readUint32LE();
inVolumeHeader.firstFileOffset = volumeStream->readUint32LE();
inVolumeHeader.firstFileSizeUncompressed = volumeStream->readUint32LE();
inVolumeHeader.firstFileSizeCompressed = volumeStream->readUint32LE();
inVolumeHeader.lastFileOffset = volumeStream->readUint32LE();
inVolumeHeader.lastFileSizeUncompressed = volumeStream->readUint32LE();
inVolumeHeader.lastFileSizeCompressed = volumeStream->readUint32LE();
} else {
inVolumeHeader.dataOffset = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.firstFileIndex = volumeStream->readUint32LE();
inVolumeHeader.lastFileIndex = volumeStream->readUint32LE();
inVolumeHeader.firstFileOffset = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.firstFileSizeUncompressed = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.firstFileSizeCompressed = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.lastFileOffset = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.lastFileSizeUncompressed = volumeStream->readUint32LE();
volumeStream->skip(4);
inVolumeHeader.lastFileSizeCompressed = volumeStream->readUint32LE();
volumeStream->skip(4);
}
return true;
}
Path InstallShieldCabinet::getHeaderName() const {
return _baseName.append("1.hdr");
}
Path InstallShieldCabinet::getVolumeName(uint volume) const {
return _baseName.append(String::format("%d.cab", volume));
}
} // End of anonymous namespace
Archive *makeInstallShieldArchive(const Path &baseName) {
return makeInstallShieldArchive(baseName, SearchMan);
}
Archive *makeInstallShieldArchive(const Common::Path &baseName, Common::Archive &archive) {
InstallShieldCabinet *cab = new InstallShieldCabinet();
if (!cab->open(&baseName, &archive, nullptr)) {
delete cab;
return nullptr;
}
return cab;
}
Archive *makeInstallShieldArchive(const FSNode &baseName) {
InstallShieldCabinet *cab = new InstallShieldCabinet();
if (!cab->open(nullptr, nullptr, &baseName)) {
delete cab;
return nullptr;
}
return cab;
}
} // End of namespace Common