Initial commit
This commit is contained in:
406
common/formats/cue.cpp
Normal file
406
common/formats/cue.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/formats/cue.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
enum {
|
||||
kContextHeader,
|
||||
kContextFiles,
|
||||
kContextTracks,
|
||||
};
|
||||
|
||||
static String nexttok(const char *s, const char **newP = nullptr) {
|
||||
String res;
|
||||
|
||||
// Scan first non-whitespace
|
||||
while (*s && (*s == ' ' || *s == '\t')) // If we see a whitespace
|
||||
s++;
|
||||
|
||||
if (*s == '"') { // If it is a string then scan till end quote
|
||||
s++; // skip the quote
|
||||
|
||||
while (*s && *s != '"')
|
||||
res += *s++;
|
||||
|
||||
if (*s == '"')
|
||||
s++;
|
||||
} else {
|
||||
while (*s && *s != ' ' && *s != '\t' && *s != '\n' && *s != '\r')
|
||||
res += *s++;
|
||||
}
|
||||
|
||||
if (newP)
|
||||
*newP = s;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
CueSheet::CueSheet(const char *sheet) {
|
||||
parse(sheet);
|
||||
}
|
||||
|
||||
CueSheet::CueSheet(SeekableReadStream *stream) {
|
||||
int size = stream->size();
|
||||
char *data = (char *)calloc(size + 1, 1); // null-terminated string
|
||||
|
||||
stream->read(data, size);
|
||||
|
||||
parse(data);
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
void CueSheet::parse(const char *sheet) {
|
||||
String line;
|
||||
_context = kContextHeader;
|
||||
const char *s = sheet;
|
||||
|
||||
while (*s) {
|
||||
line.clear();
|
||||
|
||||
// Get a line
|
||||
while (*s && *s != '\n' && *s != '\r') // If we see a whitespace
|
||||
line += *s++;
|
||||
|
||||
// Skip any newlines and line feeds
|
||||
while (*s == '\n' || *s == '\r') {
|
||||
if (*s == '\n')
|
||||
_lineNum++;
|
||||
|
||||
s++;
|
||||
}
|
||||
|
||||
|
||||
if (line.empty())
|
||||
continue;
|
||||
|
||||
String firstToken = nexttok(line.c_str()); // Peek into comments
|
||||
|
||||
// Skip non-conformant comments
|
||||
if (firstToken[0] == ';' || (firstToken.size() > 1 && firstToken[0] == '/' && firstToken[1] == '/'))
|
||||
continue;
|
||||
|
||||
switch (_context) {
|
||||
case kContextHeader:
|
||||
parseHeaderContext(line.c_str());
|
||||
break;
|
||||
|
||||
case kContextFiles:
|
||||
parseFilesContext(line.c_str());
|
||||
break;
|
||||
|
||||
case kContextTracks:
|
||||
parseTracksContext(line.c_str());
|
||||
break;
|
||||
|
||||
default:
|
||||
error("CueSheet: bad context %d at line %d", _context, _lineNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CueSheet::LookupTable {
|
||||
const char *key;
|
||||
int value;
|
||||
};
|
||||
|
||||
int CueSheet::lookupInTable(const LookupTable *table, const char *key) {
|
||||
while (table->key) {
|
||||
if (!strcmp(key, table->key))
|
||||
return table->value;
|
||||
|
||||
table++;
|
||||
}
|
||||
|
||||
error("CueSheet::lookupInTable(): Unknown lookup token %s at line %d", key, _lineNum);
|
||||
}
|
||||
|
||||
int CueSheet::parseMSF(const char *str) {
|
||||
int min = 0, sec = 0, frm = 0;
|
||||
|
||||
if (sscanf(str, "%d:%d:%d", &min, &sec, &frm) != 3)
|
||||
warning("Malformed MSF at line %d: %s", _lineNum, str);
|
||||
|
||||
return frm + 75 * (sec + 60 * min);
|
||||
}
|
||||
|
||||
static const CueSheet::LookupTable fileTypes[] = {
|
||||
{ "BINARY", CueSheet::kFileTypeBinary },
|
||||
{ "AIFF", CueSheet::kFileTypeAIFF },
|
||||
{ "WAVE", CueSheet::kFileTypeWave },
|
||||
{ "MP3", CueSheet::kFileTypeMP3 },
|
||||
{ "MOTOROLA", CueSheet::kFileTypeMotorola },
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
void CueSheet::parseHeaderContext(const char *line) {
|
||||
const char *s = line;
|
||||
|
||||
String command = nexttok(s, &s);
|
||||
|
||||
if (command == "FILE") {
|
||||
_files.push_back(CueFile());
|
||||
|
||||
_currentFile++;
|
||||
|
||||
_files[_currentFile].name = nexttok(s, &s);
|
||||
|
||||
String type = nexttok(s, &s);
|
||||
|
||||
_files[_currentFile].type = (FileType)lookupInTable(fileTypes, type.c_str());
|
||||
|
||||
_context = kContextFiles;
|
||||
|
||||
_currentTrack = -1;
|
||||
|
||||
debug(5, "File: %s, type: %s (%d)", _files[_currentFile].name.c_str(), type.c_str(), _files[_currentFile].type);
|
||||
} else if (command == "TITLE") {
|
||||
_metadata.title = nexttok(s, &s);
|
||||
|
||||
debug(5, "Title: %s", _metadata.title.c_str());
|
||||
} else if (command == "PERFORMER") {
|
||||
_metadata.performer = nexttok(s, &s);
|
||||
|
||||
debug(5, "Performer: %s", _metadata.performer.c_str());
|
||||
} else if (command == "CATALOG") {
|
||||
_metadata.catalog = nexttok(s, &s);
|
||||
|
||||
debug(5, "Catalog: %s", _metadata.catalog.c_str());
|
||||
} else if (command == "REM") {
|
||||
String subcommand = nexttok(s, &s);
|
||||
|
||||
if (subcommand == "DATE") {
|
||||
_metadata.date = nexttok(s, &s);
|
||||
debug(5, "Date: %s", _metadata.date.c_str());
|
||||
} else if (subcommand == "GENRE") {
|
||||
_metadata.genre = nexttok(s, &s);
|
||||
debug(5, "Genre: %s", _metadata.genre.c_str());
|
||||
} else if (subcommand == "COMMENT") {
|
||||
debug(5, "Skipping Comment: %s", s);
|
||||
} else {
|
||||
warning("CueSheet: Unprocessed REM subcommand %s at line %d", subcommand.c_str(), _lineNum);
|
||||
}
|
||||
} else {
|
||||
warning("CueSheet: Unprocessed command %s at line %d", command.c_str(), _lineNum);
|
||||
}
|
||||
}
|
||||
|
||||
static const CueSheet::LookupTable trackTypes[] = {
|
||||
{ "AUDIO", CueSheet::kTrackTypeAudio }, // Audio (sector size: 2352)
|
||||
{ "CDG", CueSheet::kTrackTypeCDG }, // Karaoke CD+G (sector size: 2448)
|
||||
{ "MODE1_RAW", CueSheet::kTrackTypeMode1_Raw }, // CD-ROM Mode 1 data (raw) (sector size: 2352), used by cdrdao
|
||||
{ "MODE1/2048", CueSheet::kTrackTypeMode1_2048 }, // CD-ROM Mode 1 data (cooked) (sector size: 2048)
|
||||
{ "MODE1/2352", CueSheet::kTrackTypeMode1_2352 }, // CD-ROM Mode 1 data (raw) (sector size: 2352)
|
||||
{ "MODE2_RAW", CueSheet::kTrackTypeMode2_Raw }, // CD-ROM Mode 2 data (raw) (sector size: 2352), used by cdrdao
|
||||
{ "MODE2/2048", CueSheet::kTrackTypeMode2_2048 }, // CD-ROM Mode 2 XA form-1 data (sector size: 2048)
|
||||
{ "MODE2/2324", CueSheet::kTrackTypeMode2_2324 }, // CD-ROM Mode 2 XA form-2 data (sector size: 2324)
|
||||
{ "MODE2/2336", CueSheet::kTrackTypeMode2_2366 }, // CD-ROM Mode 2 data (sector size: 2336)
|
||||
{ "MODE2/2352", CueSheet::kTrackTypeMode2_2352 }, // CD-ROM Mode 2 data (raw) (sector size: 2352)
|
||||
{ "CDI/2336", CueSheet::kTrackTypeCDI_2336 }, // CDI Mode 2 data
|
||||
{ "CDI/2352", CueSheet::kTrackTypeCDI_2352 }, // CDI Mode 2 data
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
static const CueSheet::LookupTable trackTypesSectorSizes[] = {
|
||||
{ "AUDIO", 2352 },
|
||||
{ "CDG", 2448 },
|
||||
{ "MODE1_RAW", 2352 },
|
||||
{ "MODE1/2048", 2048 },
|
||||
{ "MODE1/2352", 2352 },
|
||||
{ "MODE2_RAW", 2352 },
|
||||
{ "MODE2/2048", 2048 },
|
||||
{ "MODE2/2324", 2324 },
|
||||
{ "MODE2/2336", 2336 },
|
||||
{ "MODE2/2352", 2352 },
|
||||
{ "CDI/2336", 2336 },
|
||||
{ "CDI/2352", 2352 },
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
void CueSheet::parseFilesContext(const char *line) {
|
||||
const char *s = line;
|
||||
|
||||
String command = nexttok(s, &s);
|
||||
|
||||
if (command == "TRACK") {
|
||||
int trackNum = atoi(nexttok(s, &s).c_str());
|
||||
String trackType = nexttok(s, &s);
|
||||
|
||||
// We have to add + 2 here because _currentTrack is a counter
|
||||
// for the array, which is 0-indexed, while the disc's track
|
||||
// numbers are 1-indexed. The next track, in disc numbering,
|
||||
// will be two greater than _currentTrack.
|
||||
if (trackNum < 0 || (_currentTrack > 0 && _currentTrack + 2 != trackNum)) {
|
||||
warning("CueSheet: Incorrect track number. Expected %d but got %d at line %d", _currentTrack + 1, trackNum, _lineNum);
|
||||
} else {
|
||||
_tracks.push_back(CueTrack());
|
||||
|
||||
// Array is 0-indexed, track numbers are 1-indexed
|
||||
_currentTrack = trackNum - 1;
|
||||
_tracks[_currentTrack].number = trackNum;
|
||||
_tracks[_currentTrack].type = (TrackType)lookupInTable(trackTypes, trackType.c_str());
|
||||
_tracks[_currentTrack].size = lookupInTable(trackTypesSectorSizes, trackType.c_str());
|
||||
_tracks[_currentTrack].file = _files[_currentFile];
|
||||
|
||||
debug(5, "Track: %d type: %s (%d)", trackNum, trackType.c_str(), _tracks[_currentTrack].type);
|
||||
}
|
||||
|
||||
_context = kContextTracks;
|
||||
} else {
|
||||
warning("CueSheet: Unprocessed file command %s at line %d", command.c_str(), _lineNum);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static const CueSheet::LookupTable trackFlags[] = {
|
||||
{ "4CH", CueSheet::kTrackFlag4ch },
|
||||
{ "DCP", CueSheet::kTrackFlagDCP },
|
||||
{ "PRE", CueSheet::kTrackFlagPre },
|
||||
{ "SCMS", CueSheet::kTrackFlagSCMS },
|
||||
{ 0, 0 }
|
||||
};
|
||||
|
||||
void CueSheet::parseTracksContext(const char *line) {
|
||||
const char *s = line;
|
||||
|
||||
String command = nexttok(s, &s);
|
||||
|
||||
if (command == "TRACK") {
|
||||
parseFilesContext(line);
|
||||
} else if (command == "TITLE") {
|
||||
_tracks[_currentTrack].title = nexttok(s, &s);
|
||||
|
||||
debug(5, "Track title: %s", _tracks[_currentTrack].title.c_str());
|
||||
} else if (command == "INDEX") {
|
||||
int indexNum = atoi(nexttok(s, &s).c_str());
|
||||
int frames = parseMSF(nexttok(s, &s).c_str());
|
||||
|
||||
for (int i = (int)_tracks[_currentTrack].indices.size(); i <= indexNum; i++) {
|
||||
// -1 indicates "no index" to let callers guard against
|
||||
// interpreting these as real values
|
||||
_tracks[_currentTrack].indices.push_back(-1);
|
||||
}
|
||||
|
||||
_tracks[_currentTrack].indices[indexNum] = frames;
|
||||
|
||||
debug(5, "Index: %d, frames: %d", indexNum, frames);
|
||||
} else if (command == "PREGAP") {
|
||||
_tracks[_currentTrack].pregap = parseMSF(nexttok(s, &s).c_str());
|
||||
|
||||
debug(5, "Track pregap: %d", _tracks[_currentTrack].pregap);
|
||||
} else if (command == "FLAGS") {
|
||||
String flag;
|
||||
uint32 flags = 0;
|
||||
|
||||
while (*s) {
|
||||
flag = nexttok(s, &s);
|
||||
|
||||
flags |= lookupInTable(trackFlags, flag.c_str());
|
||||
}
|
||||
|
||||
_tracks[_currentTrack].flags = flags;
|
||||
|
||||
debug(5, "Track flags: %d", _tracks[_currentTrack].flags);
|
||||
} else if (command == "FILE") {
|
||||
parseHeaderContext(line);
|
||||
} else if (command == "PERFORMER") {
|
||||
_tracks[_currentTrack].performer = nexttok(s, &s);
|
||||
|
||||
debug(5, "Track performer: %s", _tracks[_currentTrack].performer.c_str());
|
||||
} else {
|
||||
warning("CueSheet: Unprocessed track command %s at line %d", command.c_str(), _lineNum);
|
||||
}
|
||||
}
|
||||
|
||||
Array<CueSheet::CueFile> CueSheet::files() {
|
||||
return _files;
|
||||
}
|
||||
|
||||
Array<CueSheet::CueTrack> CueSheet::tracks() {
|
||||
return _tracks;
|
||||
}
|
||||
|
||||
CueSheet::CueTrack *CueSheet::getTrack(int tracknum) {
|
||||
for (uint i = 0; i < _tracks.size(); i++) {
|
||||
if (_tracks[i].number == tracknum) {
|
||||
return &_tracks[i];
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CueSheet::CueTrack *CueSheet::getTrackAtFrame(int frame) {
|
||||
for (uint i = 0; i < _tracks.size(); i++) {
|
||||
// Inside pregap
|
||||
if (_tracks[i].indices[0] >= 0 && frame >= _tracks[i].indices[0] && frame < _tracks[i].indices.back()) {
|
||||
debug(5, "CueSheet::getTrackAtFrame: Returning track %i (pregap)", _tracks[i].number);
|
||||
return &_tracks[i];
|
||||
}
|
||||
|
||||
// Between index 1 and the start of the subsequent track.
|
||||
// Index 0 of the next track is the start of its pregap.
|
||||
// For tracks which have pregaps, we want to use that as the
|
||||
// frame to determine the edge of the track; otherwise, we'll
|
||||
// need to use the start of index 1, eg the start of the track.
|
||||
if (i+1 < _tracks.size()) {
|
||||
int nextIndex;
|
||||
CueSheet::CueTrack nextTrack = _tracks[i+1];
|
||||
// If there's more than one index, *and* index 0 looks
|
||||
// nullish, use index 1
|
||||
if (nextTrack.indices.size() > 1 && nextTrack.indices[0] == -1) {
|
||||
nextIndex = nextTrack.indices[1];
|
||||
// Otherwise, use index 0
|
||||
} else {
|
||||
nextIndex = nextTrack.indices[0];
|
||||
}
|
||||
|
||||
if (frame >= _tracks[i].indices.back() && frame < nextIndex) {
|
||||
debug(5, "CueSheet::getTrackAtFrame: Returning track %i (inside content)", _tracks[i].number);
|
||||
return &_tracks[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found within any tracks, but could be in the final track.
|
||||
// Note: this looks weird, but is correct (or correct-ish).
|
||||
// The cuesheet tracks the *starting* index of a song, but not
|
||||
// the *duration* of a track. Without having access to the raw
|
||||
// disc data, we don't actually know how long this track is.
|
||||
// As a result, if we see any frame that comes after the
|
||||
// start of the final track, we need to assume it's
|
||||
// a part of that track.
|
||||
if (frame > _tracks.back().indices.back()) {
|
||||
debug(5, "CueSheet::getTrackAtFrame: Returning final track");
|
||||
return &_tracks.back();
|
||||
}
|
||||
|
||||
// Still not found; could indicate a gap between indices
|
||||
warning("CueSheet::getTrackAtFrame: Not returning a track; does the cuesheet have a gap between indices?");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
126
common/formats/cue.h
Normal file
126
common/formats/cue.h
Normal file
@@ -0,0 +1,126 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_FORMATS_CUE_H
|
||||
#define COMMON_FORMATS_CUE_H
|
||||
|
||||
#include "common/array.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
class SeekableReadStream;
|
||||
|
||||
/**
|
||||
* A class able to load and parse Cue sheets
|
||||
*/
|
||||
class CueSheet {
|
||||
public:
|
||||
CueSheet(const char *sheet);
|
||||
CueSheet(SeekableReadStream *stream);
|
||||
|
||||
public:
|
||||
enum FileType {
|
||||
kFileTypeBinary,
|
||||
kFileTypeAIFF,
|
||||
kFileTypeWave,
|
||||
kFileTypeMP3,
|
||||
kFileTypeMotorola,
|
||||
};
|
||||
|
||||
enum TrackType {
|
||||
kTrackTypeAudio, // Audio (sector size: 2352)
|
||||
kTrackTypeCDG, // Karaoke CD+G (sector size: 2448)
|
||||
kTrackTypeMode1_Raw, // CD-ROM Mode 1 data (raw) (sector size: 2352), used by cdrdao
|
||||
kTrackTypeMode1_2048, // CD-ROM Mode 1 data (cooked) (sector size: 2048)
|
||||
kTrackTypeMode1_2352, // CD-ROM Mode 1 data (raw) (sector size: 2352)
|
||||
kTrackTypeMode2_Raw, // CD-ROM Mode 2 data (raw) (sector size: 2352), used by cdrdao
|
||||
kTrackTypeMode2_2048, // CD-ROM Mode 2 XA form-1 data (sector size: 2048)
|
||||
kTrackTypeMode2_2324, // CD-ROM Mode 2 XA form-2 data (sector size: 2324)
|
||||
kTrackTypeMode2_2366, // CD-ROM Mode 2 data (sector size: 2336)
|
||||
kTrackTypeMode2_2352, // CD-ROM Mode 2 data (raw) (sector size: 2352)
|
||||
kTrackTypeCDI_2336, // CDI Mode 2 data
|
||||
kTrackTypeCDI_2352, // CDI Mode 2 data
|
||||
};
|
||||
|
||||
enum TrackFlags {
|
||||
kTrackFlag4ch = 1 << 0, // Four channel audio
|
||||
kTrackFlagDCP = 1 << 1, // Digital copy permitted
|
||||
kTrackFlagPre = 1 << 2, // Pre-emphasis enabled, for audio tracks only
|
||||
kTrackFlagSCMS = 1 << 3, // Serial copy management system
|
||||
};
|
||||
|
||||
struct CueFile {
|
||||
String name;
|
||||
FileType type = kFileTypeBinary;
|
||||
};
|
||||
|
||||
struct CueTrack {
|
||||
int number = 0;
|
||||
CueFile file;
|
||||
TrackType type;
|
||||
String title;
|
||||
String performer;
|
||||
Array<int> indices;
|
||||
int pregap = 0;
|
||||
uint32 flags;
|
||||
int size = 2352;
|
||||
};
|
||||
|
||||
struct CueMetadata {
|
||||
String title;
|
||||
String date;
|
||||
String genre;
|
||||
String performer;
|
||||
String catalog;
|
||||
};
|
||||
|
||||
struct LookupTable;
|
||||
|
||||
Array<CueFile> files();
|
||||
Array<CueTrack> tracks();
|
||||
CueTrack *getTrack(int tracknum);
|
||||
CueTrack *getTrackAtFrame(int frame);
|
||||
|
||||
private:
|
||||
void parse(const char *sheet);
|
||||
|
||||
int lookupInTable(const LookupTable *table, const char *key);
|
||||
int parseMSF(const char *str);
|
||||
|
||||
void parseHeaderContext(const char *line);
|
||||
void parseFilesContext(const char *line);
|
||||
void parseTracksContext(const char *line);
|
||||
|
||||
private:
|
||||
int _lineNum = 0;
|
||||
int _context;
|
||||
int _currentFile = -1;
|
||||
int _currentTrack = -1;
|
||||
|
||||
Array<CueFile> _files;
|
||||
Array<CueTrack> _tracks;
|
||||
CueMetadata _metadata;
|
||||
|
||||
};
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
961
common/formats/disk_image.cpp
Normal file
961
common/formats/disk_image.cpp
Normal file
@@ -0,0 +1,961 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/stream.h"
|
||||
#include "common/substream.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/md5.h"
|
||||
#include "common/algorithm.h"
|
||||
#include "common/bitstream.h"
|
||||
|
||||
#include "common/formats/disk_image.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
const uint kNibTrackLen = 256 * 26;
|
||||
|
||||
static bool detectDOS33(Common::SeekableReadStream &f, uint size) {
|
||||
uint count = 0;
|
||||
uint dos32 = 0, dos33 = 0;
|
||||
uint32 window = 0;
|
||||
|
||||
while (count++ < size) {
|
||||
window &= 0xffff;
|
||||
window <<= 8;
|
||||
window |= f.readByte();
|
||||
|
||||
if (f.err() || f.eos())
|
||||
return false;
|
||||
|
||||
if (window == 0xd5aa96)
|
||||
++dos33;
|
||||
else if (window == 0xd5aab5)
|
||||
++dos32;
|
||||
}
|
||||
|
||||
return dos33 > dos32;
|
||||
}
|
||||
|
||||
static bool readSector_NIB(byte outBuf[], uint outBufSize, const byte inBuf[], uint inBufSize, uint &pos, const byte minNibble, const byte lookup[], const uint track, const uint sector) {
|
||||
uint z = inBufSize - (pos % inBufSize);
|
||||
if (z < outBufSize) {
|
||||
memcpy(outBuf, inBuf + (pos % inBufSize), z);
|
||||
memcpy(outBuf + z, inBuf, outBufSize - z);
|
||||
} else
|
||||
memcpy(outBuf, inBuf + (pos % inBufSize), outBufSize);
|
||||
pos += outBufSize;
|
||||
|
||||
byte oldVal = 0;
|
||||
for (uint n = 0; n < outBufSize; ++n) {
|
||||
// expand
|
||||
if (outBuf[n] == 0xd5) {
|
||||
// Early end of block.
|
||||
pos -= (outBufSize - n);
|
||||
debug(2, "NIB: early end of block @ %x (%d, %d)", n, track, sector);
|
||||
return false;
|
||||
}
|
||||
|
||||
byte val = 0x40;
|
||||
|
||||
if (outBuf[n] >= minNibble)
|
||||
val = lookup[outBuf[n] - minNibble];
|
||||
|
||||
if (val == 0x40) {
|
||||
// Badly-encoded nibbles, stop trying to decode here.
|
||||
pos -= (outBufSize - n);
|
||||
debug(2, "NIB: bad nibble %02x @ %x (%d, %d)", outBuf[n], n, track, sector);
|
||||
return false;
|
||||
}
|
||||
|
||||
// undo checksum
|
||||
oldVal = val ^ oldVal;
|
||||
outBuf[n] = oldVal;
|
||||
}
|
||||
|
||||
byte checksum = inBuf[pos++ % inBufSize];
|
||||
if (checksum < minNibble || oldVal != lookup[checksum - minNibble]) {
|
||||
debug(2, "NIB: checksum mismatch @ (%d, %d)", track, sector);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4-and-4 encoding (odd-even)
|
||||
static uint8 read44(byte *buffer, uint size, uint &pos) {
|
||||
// 1s in the other fields, so we can just AND
|
||||
uint8 ret = buffer[pos++ % size];
|
||||
return ((ret << 1) | 1) & buffer[pos++ % size];
|
||||
}
|
||||
|
||||
static bool decodeTrack(Common::SeekableReadStream &stream, uint trackLen, bool dos33, byte *const diskImage, uint tracks, Common::BitArray &goodSectors) {
|
||||
// starting at 0xaa, 64 is invalid (see below)
|
||||
const byte c_5and3_lookup[] = { 64, 0, 64, 1, 2, 3, 64, 64, 64, 64, 64, 4, 5, 6, 64, 64, 7, 8, 64, 9, 10, 11, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 12, 13, 64, 64, 14, 15, 64, 16, 17, 18, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 19, 20, 64, 21, 22, 23, 64, 64, 64, 64, 64, 24, 25, 26, 64, 64, 27, 28, 64, 29, 30, 31 };
|
||||
// starting at 0x96, 64 is invalid (see below)
|
||||
const byte c_6and2_lookup[] = { 0, 1, 64, 64, 2, 3, 64, 4, 5, 6, 64, 64, 64, 64, 64, 64, 7, 8, 64, 64, 64, 9, 10, 11, 12, 13, 64, 64, 14, 15, 16, 17, 18, 19, 64, 20, 21, 22, 23, 24, 25, 26, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 27, 64, 28, 29, 30, 64, 64, 64, 31, 64, 64, 32, 33, 64, 34, 35, 36, 37, 38, 39, 40, 64, 64, 64, 64, 64, 41, 42, 43, 64, 44, 45, 46, 47, 48, 49, 50, 64, 64, 51, 52, 53, 54, 55, 56, 64, 57, 58, 59, 60, 61, 62, 63 };
|
||||
|
||||
const uint sectorsPerTrack = (dos33 ? 16 : 13);
|
||||
const uint bytesPerSector = 256;
|
||||
|
||||
bool sawAddress = false;
|
||||
uint8 volNo = 0, track = 0, sector = 0;
|
||||
|
||||
byte *const buffer = (byte *)malloc(trackLen);
|
||||
uint firstGoodTrackPos = 0;
|
||||
uint pos = 0;
|
||||
|
||||
if (stream.read(buffer, trackLen) < trackLen) {
|
||||
free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (pos >= trackLen + firstGoodTrackPos)
|
||||
break;
|
||||
|
||||
// Read until we find two sync bytes.
|
||||
if (buffer[pos++ % trackLen] != 0xd5 || buffer[pos++ % trackLen] != 0xaa)
|
||||
continue;
|
||||
|
||||
byte prologue = buffer[pos++ % trackLen];
|
||||
|
||||
if (sawAddress && prologue == (dos33 ? 0x96 : 0xb5)) {
|
||||
sawAddress = false;
|
||||
}
|
||||
|
||||
if (!sawAddress) {
|
||||
sawAddress = true;
|
||||
|
||||
// We should always find the address field first.
|
||||
if (prologue != (dos33 ? 0x96 : 0xb5)) {
|
||||
// Accept a DOS 3.3(?) header at the start.
|
||||
if (prologue != (dos33 ? 0xb5 : 0x96) && prologue != 0xad && prologue != 0xfd)
|
||||
debug(2, "NIB: unknown field prologue %02x", prologue);
|
||||
sawAddress = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
volNo = read44(buffer, trackLen, pos);
|
||||
track = read44(buffer, trackLen, pos);
|
||||
sector = read44(buffer, trackLen, pos);
|
||||
uint8 checksum = read44(buffer, trackLen, pos);
|
||||
if ((volNo ^ track ^ sector) != checksum) {
|
||||
debug(2, "NIB: invalid checksum (volNo %d, track %d, sector %d)", volNo, track, sector);
|
||||
sawAddress = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (track >= tracks || sector >= sectorsPerTrack) {
|
||||
debug(2, "NIB: sector out of bounds @ (%d, %d)", track, sector);
|
||||
sawAddress = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!firstGoodTrackPos)
|
||||
firstGoodTrackPos = pos;
|
||||
|
||||
// Epilogue is de/aa plus a gap, but we don't care.
|
||||
continue;
|
||||
}
|
||||
|
||||
sawAddress = false;
|
||||
|
||||
// We should always find the data field after an address field.
|
||||
// TODO: we ignore volNo?
|
||||
byte *output = diskImage + (track * sectorsPerTrack + sector) * bytesPerSector;
|
||||
|
||||
if (dos33) {
|
||||
// We hardcode the DOS 3.3 mapping here. TODO: Do we also need raw/prodos?
|
||||
int raw2dos[16] = { 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 };
|
||||
sector = raw2dos[sector];
|
||||
output = diskImage + (track * sectorsPerTrack + sector) * bytesPerSector;
|
||||
|
||||
byte inbuffer[342];
|
||||
|
||||
if (!readSector_NIB(inbuffer, sizeof(inbuffer), buffer, trackLen, pos, 0x96, c_6and2_lookup, track, sector))
|
||||
continue;
|
||||
|
||||
for (uint n = 0; n < 256; ++n) {
|
||||
output[n] = inbuffer[86 + n] << 2;
|
||||
if (n < 86) { // use first pair of bits
|
||||
output[n] |= ((inbuffer[n] & 1) << 1);
|
||||
output[n] |= ((inbuffer[n] & 2) >> 1);
|
||||
} else if (n < 86*2) { // second pair
|
||||
output[n] |= ((inbuffer[n-86] & 4) >> 1);
|
||||
output[n] |= ((inbuffer[n-86] & 8) >> 3);
|
||||
} else { // third pair
|
||||
output[n] |= ((inbuffer[n-86*2] & 0x10) >> 3);
|
||||
output[n] |= ((inbuffer[n-86*2] & 0x20) >> 5);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 5-and-3 uses 410 on-disk bytes, decoding to just over 256 bytes
|
||||
byte inbuffer[410];
|
||||
|
||||
if (!readSector_NIB(inbuffer, sizeof(inbuffer), buffer, trackLen, pos, 0xaa, c_5and3_lookup, track, sector))
|
||||
continue;
|
||||
|
||||
// 8 bytes of nibbles expand to 5 bytes
|
||||
// so we have 51 of these batches (255 bytes), plus 2 bytes of 'leftover' nibbles for byte 256
|
||||
for (uint n = 0; n < 51; ++n) {
|
||||
// e.g. figure 3.18 of Beneath Apple DOS
|
||||
byte lowbits1 = inbuffer[51*3 - n];
|
||||
byte lowbits2 = inbuffer[51*2 - n];
|
||||
byte lowbits3 = inbuffer[51*1 - n];
|
||||
byte lowbits4 = (lowbits1 & 2) << 1 | (lowbits2 & 2) | (lowbits3 & 2) >> 1;
|
||||
byte lowbits5 = (lowbits1 & 1) << 2 | (lowbits2 & 1) << 1 | (lowbits3 & 1);
|
||||
output[250 - 5*n] = (inbuffer[n + 51*3 + 1] << 3) | ((lowbits1 >> 2) & 0x7);
|
||||
output[251 - 5*n] = (inbuffer[n + 51*4 + 1] << 3) | ((lowbits2 >> 2) & 0x7);
|
||||
output[252 - 5*n] = (inbuffer[n + 51*5 + 1] << 3) | ((lowbits3 >> 2) & 0x7);
|
||||
output[253 - 5*n] = (inbuffer[n + 51*6 + 1] << 3) | lowbits4;
|
||||
output[254 - 5*n] = (inbuffer[n + 51*7 + 1] << 3) | lowbits5;
|
||||
}
|
||||
output[255] = (inbuffer[409] << 3) | (inbuffer[0] & 0x7);
|
||||
}
|
||||
|
||||
goodSectors.set(track * sectorsPerTrack + sector);
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void printGoodSectors(const Common::BitArray &goodSectors, uint sectorsPerTrack) {
|
||||
if (gDebugLevel < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool foundBadSector = false;
|
||||
for (uint i = 0; i < goodSectors.size(); ++i) {
|
||||
if (!goodSectors.get(i)) {
|
||||
if (!foundBadSector) {
|
||||
debugN(1, "Bad/missing sectors:");
|
||||
foundBadSector = true;
|
||||
}
|
||||
debugN(1, " (%d, %d)", i / sectorsPerTrack, i % sectorsPerTrack);
|
||||
}
|
||||
}
|
||||
if (foundBadSector) {
|
||||
debugN(1, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
static bool readImage_NIB(
|
||||
const char *filename,
|
||||
Common::SeekableReadStream &f,
|
||||
byte *diskImage,
|
||||
bool dos33,
|
||||
uint startTrack = 0,
|
||||
uint tracksToRead = 35) {
|
||||
|
||||
if (f.size() != 35 * kNibTrackLen) {
|
||||
warning("NIB: image '%s' has invalid size of %d bytes", filename, (int)f.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint sectorsPerTrack = (dos33 ? 16 : 13);
|
||||
Common::BitArray goodSectors(35 * sectorsPerTrack);
|
||||
|
||||
f.seek(startTrack * kNibTrackLen);
|
||||
uint endTrack = startTrack + tracksToRead - 1;
|
||||
for (uint track = startTrack; track <= endTrack; ++track) {
|
||||
if (!decodeTrack(f, kNibTrackLen, dos33, diskImage, 35, goodSectors)) {
|
||||
warning("NIB: error decoding track %d in '%s'", track, filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
printGoodSectors(goodSectors, sectorsPerTrack);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Common::SeekableReadStream *readImageToStream_NIB(Common::File &f, bool dos33, uint tracks = 35) {
|
||||
const uint sectorsPerTrack = (dos33 ? 16 : 13);
|
||||
const uint imageSize = tracks * sectorsPerTrack * 256;
|
||||
byte *const diskImage = (byte *)calloc(imageSize, 1);
|
||||
|
||||
if (!readImage_NIB(f.getName(), f, diskImage, dos33, 0, tracks)) {
|
||||
warning("NIB: error reading '%s'", f.getName());
|
||||
free(diskImage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
static int getVersion_WOZ(const char *filename, Common::SeekableReadStream &f) {
|
||||
f.seek(0);
|
||||
const uint32 fileId = f.readUint32BE();
|
||||
|
||||
if (f.eos() || f.err()) {
|
||||
warning("WOZ: error reading '%s'", filename);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fileId == MKTAG('W', 'O', 'Z', '1'))
|
||||
return 1;
|
||||
else if (fileId == MKTAG('W', 'O', 'Z', '2'))
|
||||
return 2;
|
||||
|
||||
warning("WOZ: unsupported ID '%s' found in '%s'", tag2str(fileId), filename);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Common::SeekableReadStream *readTrack_WOZ(const char *filename, Common::SeekableReadStream &f, uint track, bool woz2) {
|
||||
f.seek(88 + track * 4);
|
||||
const byte index = f.readByte();
|
||||
|
||||
if (index == 0xff) {
|
||||
warning("WOZ: track %u not found in '%s', skipping", track, filename);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32 offset, byteSize, bitSize;
|
||||
|
||||
if (woz2) {
|
||||
f.seek(256 + index * 8);
|
||||
offset = f.readUint16LE() << 9;
|
||||
byteSize = f.readUint16LE() << 9;
|
||||
bitSize = f.readUint32LE();
|
||||
} else {
|
||||
offset = 256 + index * 6656;
|
||||
f.seek(offset + 6646);
|
||||
byteSize = f.readUint16LE();
|
||||
bitSize = f.readUint16LE();
|
||||
}
|
||||
|
||||
f.seek(offset);
|
||||
|
||||
if (f.eos() || f.err() || byteSize == 0) {
|
||||
warning("WOZ: failed to read track %u in '%s', aborting", track, filename);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
byte *inBuf = (byte *)malloc(byteSize);
|
||||
byte *outBuf = (byte *)malloc(byteSize);
|
||||
uint32 outSize = 0;
|
||||
if (!inBuf || !outBuf) {
|
||||
warning("WOZ: failed to create buffers of size %u for track %u in '%s'", byteSize, track, filename);
|
||||
free(inBuf);
|
||||
free(outBuf);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (f.read(inBuf, byteSize) < byteSize) {
|
||||
warning("WOZ: error reading track %u in '%s'", track, filename);
|
||||
free(inBuf);
|
||||
free(outBuf);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Common::BitStreamMemory8MSB bitStream(new Common::BitStreamMemoryStream(inBuf, byteSize, DisposeAfterUse::YES), DisposeAfterUse::YES);
|
||||
|
||||
byte nibble = 0;
|
||||
bool stop = false;
|
||||
for (;;) {
|
||||
nibble = (nibble << 1) | bitStream.getBit();
|
||||
|
||||
if (nibble & 0x80) {
|
||||
if (stop)
|
||||
break;
|
||||
nibble = 0;
|
||||
}
|
||||
|
||||
if (bitStream.pos() == bitSize) {
|
||||
bitStream.rewind();
|
||||
if (stop) {
|
||||
warning("WOZ: failed to find sync point for track %u in '%s'", track, filename);
|
||||
break;
|
||||
}
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
nibble = 0;
|
||||
uint32 bitsRead = 0;
|
||||
do {
|
||||
nibble = (nibble << 1) | bitStream.getBit();
|
||||
++bitsRead;
|
||||
|
||||
if (nibble & 0x80) {
|
||||
outBuf[outSize++] = nibble;
|
||||
nibble = 0;
|
||||
}
|
||||
|
||||
if (bitStream.pos() == bitSize)
|
||||
bitStream.rewind();
|
||||
} while (bitsRead < bitSize);
|
||||
|
||||
if (nibble != 0)
|
||||
warning("WOZ: failed to sync track %u in '%s'", track, filename);
|
||||
|
||||
if (outSize == 0) {
|
||||
warning("WOZ: track %u in '%s' is empty", track, filename);
|
||||
free(outBuf);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new Common::MemoryReadStream(outBuf, outSize, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
static bool readImage_WOZ(
|
||||
const char *filename,
|
||||
Common::SeekableReadStream &f,
|
||||
byte *diskImage,
|
||||
bool dos33,
|
||||
uint startTrack = 0,
|
||||
uint tracksToRead = 35) {
|
||||
|
||||
int version = getVersion_WOZ(filename, f);
|
||||
if (version == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint sectorsPerTrack = (dos33 ? 16 : 13);
|
||||
Common::BitArray goodSectors(35 * sectorsPerTrack);
|
||||
|
||||
uint endTrack = startTrack + tracksToRead - 1;
|
||||
for (uint track = startTrack; track <= endTrack; ++track) {
|
||||
StreamPtr stream(readTrack_WOZ(filename, f, track, version == 2));
|
||||
|
||||
if (stream) {
|
||||
if (!decodeTrack(*stream, stream->size(), dos33, diskImage, 35, goodSectors)) {
|
||||
warning("WOZ: error decoding track %d in '%s'", track, filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printGoodSectors(goodSectors, sectorsPerTrack);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Common::SeekableReadStream *readImageToStream_WOZ(Common::File &f, bool dos33, uint tracks = 35) {
|
||||
const uint sectorsPerTrack = (dos33 ? 16 : 13);
|
||||
const uint imageSize = tracks * sectorsPerTrack * 256;
|
||||
byte *const diskImage = (byte *)calloc(imageSize, 1);
|
||||
|
||||
if (!readImage_WOZ(f.getName(), f, diskImage, dos33, 0, tracks)) {
|
||||
warning("WOZ: error reading '%s'", f.getName());
|
||||
free(diskImage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new Common::MemoryReadStream(diskImage, imageSize, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
bool DiskImage::open(const Common::Path &filename) {
|
||||
Common::File *file = new Common::File();
|
||||
debug(1, "Opening '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
|
||||
if (file->open(filename)) {
|
||||
if (open(filename.baseName(), file)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
warning("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
|
||||
}
|
||||
delete file;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiskImage::open(const Common::FSNode &node) {
|
||||
Common::File *file = new Common::File();
|
||||
debug(1, "Opening '%s'", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
if (file->open(node)) {
|
||||
if (open(node.getFileName(), file)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
warning("Failed to open '%s'", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
}
|
||||
delete file;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DiskImage::open(const Common::String &name, Common::File *f) {
|
||||
if (name.hasSuffixIgnoreCase(".dsk") ||
|
||||
name.hasSuffixIgnoreCase(".do")) {
|
||||
_tracks = 35;
|
||||
_sectorsPerTrack = 16;
|
||||
_bytesPerSector = 256;
|
||||
_inputStream = f;
|
||||
} else if (name.hasSuffixIgnoreCase(".d13")) {
|
||||
_tracks = 35;
|
||||
_sectorsPerTrack = 13;
|
||||
_bytesPerSector = 256;
|
||||
_inputStream = f;
|
||||
} else if (name.hasSuffixIgnoreCase(".nib")) {
|
||||
_tracks = 35;
|
||||
|
||||
if (detectDOS33(*f, kNibTrackLen))
|
||||
_sectorsPerTrack = 16;
|
||||
else
|
||||
_sectorsPerTrack = 13;
|
||||
|
||||
_bytesPerSector = 256;
|
||||
if (_lazyDecoding) {
|
||||
// store the file stream and create an empty decode stream.
|
||||
// tracks will be decoded into the decode stream as they're read.
|
||||
uint32 imageSize = _tracks * _sectorsPerTrack * _bytesPerSector;
|
||||
_inputStream = f;
|
||||
_decodeBuffer = (byte *)calloc(imageSize, 1);
|
||||
_decodeStream = new Common::MemoryReadStream(_decodeBuffer, imageSize, DisposeAfterUse::YES);
|
||||
_decodedTracks.set_size(_tracks);
|
||||
_encoding = DiskImageEncodingNib;
|
||||
} else {
|
||||
// decode the entire image
|
||||
_inputStream = readImageToStream_NIB(*f, (_sectorsPerTrack == 16));
|
||||
}
|
||||
} else if (name.hasSuffixIgnoreCase(".woz")) {
|
||||
_tracks = 35;
|
||||
_sectorsPerTrack = 13;
|
||||
_bytesPerSector = 256;
|
||||
|
||||
int version = getVersion_WOZ(name.c_str(), *f);
|
||||
|
||||
if (version > 0) {
|
||||
StreamPtr bitStream(readTrack_WOZ(name.c_str(), *f, 0, version == 2));
|
||||
if (bitStream) {
|
||||
if (detectDOS33(*bitStream, bitStream->size()))
|
||||
_sectorsPerTrack = 16;
|
||||
|
||||
if (_lazyDecoding) {
|
||||
// store the file stream and create an empty decode stream.
|
||||
// tracks will be decoded into the decode stream as they're read.
|
||||
uint32 imageSize = _tracks * _sectorsPerTrack * _bytesPerSector;
|
||||
_inputStream = f;
|
||||
_decodeBuffer = (byte *)calloc(imageSize, 1);
|
||||
_decodeStream = new Common::MemoryReadStream(_decodeBuffer, imageSize, DisposeAfterUse::YES);
|
||||
_decodedTracks.set_size(_tracks);
|
||||
_encoding = DiskImageEncodingWoz;
|
||||
} else {
|
||||
// decode the entire image
|
||||
_inputStream = readImageToStream_WOZ(*f, (_sectorsPerTrack == 16), _tracks);
|
||||
}
|
||||
} else {
|
||||
warning("WOZ: failed to load bitstream for track 0 in '%s'", name.c_str());
|
||||
}
|
||||
}
|
||||
} else if (name.hasSuffixIgnoreCase(".xfd")) {
|
||||
_tracks = 40;
|
||||
_sectorsPerTrack = 18;
|
||||
_bytesPerSector = 128;
|
||||
_inputStream = f;
|
||||
} else if (name.hasSuffixIgnoreCase(".img")) {
|
||||
_tracks = 40;
|
||||
_sectorsPerTrack = 8;
|
||||
_bytesPerSector = 512;
|
||||
_firstSector = 1;
|
||||
_inputStream = f;
|
||||
}
|
||||
|
||||
if (_inputStream == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int expectedSize = _tracks * _sectorsPerTrack * _bytesPerSector;
|
||||
if (getDiskStream()->size() != expectedSize) {
|
||||
warning("Unrecognized disk image '%s' of size %d bytes (expected %d bytes)", name.c_str(), (int)getDiskStream()->size(), expectedSize);
|
||||
if (_inputStream != f) {
|
||||
delete _inputStream;
|
||||
}
|
||||
_inputStream = nullptr;
|
||||
delete _decodeStream;
|
||||
_decodeStream = nullptr;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (_inputStream != f) {
|
||||
delete f;
|
||||
}
|
||||
|
||||
_name = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
const DataBlockPtr DiskImage::getDataBlock(uint track, uint sector, uint offset, uint size) const {
|
||||
return DataBlockPtr(new DiskImage::DataBlock(this, track, sector, offset, size, _sectorLimit));
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *DiskImage::createReadStream(uint track, uint sector, uint offset, uint size, uint sectorLimit) const {
|
||||
const uint bytesToRead = size * _bytesPerSector + _bytesPerSector - offset;
|
||||
byte *const data = (byte *)malloc(bytesToRead);
|
||||
uint dataOffset = 0;
|
||||
|
||||
if (sectorLimit == 0)
|
||||
sectorLimit = _sectorsPerTrack;
|
||||
|
||||
if (sector < _firstSector || sector >= sectorLimit + _firstSector) {
|
||||
warning("Sector %u is out of bounds for %u-sector %u-based reading", sector, sectorLimit, _firstSector);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// lazy decoding not supported for createReadStream(), because decoding
|
||||
// tracks here requires removing way too many existing const keywords.
|
||||
// if it's ever needed, enable this code and remove all those consts.
|
||||
#if 0
|
||||
if (_decodeBuffer != nullptr) {
|
||||
// lazy decoding
|
||||
uint32 bytesPerTrack = _sectorsPerTrack * _bytesPerSector;
|
||||
uint32 endTrack = track + (((sector - _firstSector) * _bytesPerSector + offset + bytesToRead - 1) / bytesPerTrack);
|
||||
for (uint32 t = track; t <= endTrack; t++) {
|
||||
if (!_decodedTracks.get(t)) {
|
||||
decodeTrack(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
sector -= _firstSector;
|
||||
Common::SeekableReadStream *stream = getDiskStream();
|
||||
while (dataOffset < bytesToRead) {
|
||||
uint bytesRemInTrack = (sectorLimit - 1 - sector) * _bytesPerSector + _bytesPerSector - offset;
|
||||
stream->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset);
|
||||
|
||||
if (bytesToRead - dataOffset < bytesRemInTrack)
|
||||
bytesRemInTrack = bytesToRead - dataOffset;
|
||||
|
||||
if (stream->read(data + dataOffset, bytesRemInTrack) < bytesRemInTrack) {
|
||||
warning("Error reading disk image at track %d; sector %d", track, sector);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
++track;
|
||||
|
||||
sector = 0;
|
||||
offset = 0;
|
||||
|
||||
dataOffset += bytesRemInTrack;
|
||||
}
|
||||
|
||||
return new Common::MemoryReadStream(data, bytesToRead, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *DiskImage::releaseStream() {
|
||||
Common::SeekableReadStream *stream = getDiskStream();
|
||||
|
||||
// reset class
|
||||
if (stream != _inputStream) {
|
||||
delete _inputStream;
|
||||
}
|
||||
_inputStream = nullptr;
|
||||
_decodeStream = nullptr;
|
||||
_decodeBuffer = nullptr;
|
||||
_decodedTracks.clear();
|
||||
_encoding = DiskImageEncodingNone;
|
||||
_lazyDecoding = false;
|
||||
|
||||
_tracks = 0;
|
||||
_sectorsPerTrack = 0;
|
||||
_bytesPerSector = 0;
|
||||
_sectorLimit = 0;
|
||||
_firstSector = 0;
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
uint32 DiskImage::read(void *dataPtr, uint32 diskPosition, uint32 dataSize) {
|
||||
Common::SeekableReadStream *stream;
|
||||
if (_decodeBuffer != nullptr) {
|
||||
// lazy decoding
|
||||
uint32 bytesPerTrack = _sectorsPerTrack * _bytesPerSector;
|
||||
uint32 startTrack = diskPosition / bytesPerTrack;
|
||||
uint32 endTrack = (diskPosition + dataSize - 1) / bytesPerTrack;
|
||||
for (uint32 t = startTrack; t <= endTrack; t++) {
|
||||
if (!_decodedTracks.get(t)) {
|
||||
decodeTrack(t);
|
||||
}
|
||||
}
|
||||
stream = _decodeStream;
|
||||
}
|
||||
else {
|
||||
stream = _inputStream;
|
||||
}
|
||||
stream->seek(diskPosition);
|
||||
return stream->read(dataPtr, dataSize);
|
||||
}
|
||||
|
||||
void DiskImage::decodeTrack(uint track) {
|
||||
switch (_encoding) {
|
||||
case DiskImageEncodingNib:
|
||||
readImage_NIB(_name.c_str(), *_inputStream, _decodeBuffer, (_sectorsPerTrack == 16), track, 1);
|
||||
break;
|
||||
case DiskImageEncodingWoz:
|
||||
readImage_WOZ(_name.c_str(), *_inputStream, _decodeBuffer, (_sectorsPerTrack == 16), track, 1);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
_decodedTracks.set(track);
|
||||
}
|
||||
|
||||
int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes) {
|
||||
Common::File f;
|
||||
|
||||
if (!f.open(node))
|
||||
return -1;
|
||||
|
||||
const uint tracks = md5Bytes / (13 * 256) + 1;
|
||||
|
||||
if (node.getName().matchString("*.nib", true) && f.size() == 35 * kNibTrackLen) {
|
||||
bool isDOS33 = detectDOS33(f, kNibTrackLen);
|
||||
|
||||
Common::SeekableReadStream *stream = readImageToStream_NIB(f, isDOS33, tracks);
|
||||
if (stream) {
|
||||
md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
|
||||
delete stream;
|
||||
return 35 * (isDOS33 ? 16 : 13) * 256;
|
||||
}
|
||||
|
||||
return -1;
|
||||
} else if (node.getName().matchString("*.woz", true)) {
|
||||
int version = getVersion_WOZ(f.getName(), f);
|
||||
|
||||
if (version > 0) {
|
||||
StreamPtr nibbles(readTrack_WOZ(f.getName(), f, 0, version == 2));
|
||||
if (nibbles) {
|
||||
bool isDOS33 = detectDOS33(*nibbles, nibbles->size());
|
||||
StreamPtr stream(readImageToStream_WOZ(f, isDOS33, tracks));
|
||||
if (stream) {
|
||||
md5 = Common::computeStreamMD5AsString(*stream, md5Bytes);
|
||||
return 35 * (isDOS33 ? 16 : 13) * 256;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
} else {
|
||||
md5 = Common::computeStreamMD5AsString(f, md5Bytes);
|
||||
return f.size();
|
||||
}
|
||||
}
|
||||
|
||||
const DataBlockPtr Files_Plain::getDataBlock(const Common::Path &filename, uint offset) const {
|
||||
return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset));
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *Files_Plain::createReadStream(const Common::Path &filename, uint offset) const {
|
||||
Common::File *f(new Common::File());
|
||||
|
||||
if (!f->open(filename))
|
||||
error("Failed to open '%s'", filename.toString(Common::Path::kNativeSeparator).c_str());
|
||||
|
||||
if (offset == 0)
|
||||
return f;
|
||||
else
|
||||
return new Common::SeekableSubReadStream(f, offset, f->size(), DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
Files_AppleDOS::~Files_AppleDOS() {
|
||||
delete _disk;
|
||||
}
|
||||
|
||||
Files_AppleDOS::Files_AppleDOS() :
|
||||
_disk(nullptr) {
|
||||
}
|
||||
|
||||
void Files_AppleDOS::readSectorList(TrackSector start, Common::Array<TrackSector> &list) {
|
||||
TrackSector index = start;
|
||||
|
||||
while (index.track != 0) {
|
||||
Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(index.track, index.sector));
|
||||
|
||||
stream->readByte();
|
||||
index.track = stream->readByte();
|
||||
index.sector = stream->readByte();
|
||||
|
||||
stream->seek(9, SEEK_CUR);
|
||||
|
||||
// This only handles sequential files
|
||||
TrackSector ts;
|
||||
ts.track = stream->readByte();
|
||||
ts.sector = stream->readByte();
|
||||
|
||||
while (ts.track != 0) {
|
||||
list.push_back(ts);
|
||||
|
||||
ts.track = stream->readByte();
|
||||
ts.sector = stream->readByte();
|
||||
|
||||
if (stream->err())
|
||||
error("Error reading sector list");
|
||||
|
||||
if (stream->eos())
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Files_AppleDOS::readVTOC() {
|
||||
Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(0x11, 0x00));
|
||||
stream->readByte();
|
||||
byte track = stream->readByte();
|
||||
|
||||
if (!track) {
|
||||
// VTOC probably obfuscated, try track 0x10
|
||||
stream.reset(_disk->createReadStream(0x10, 0x00));
|
||||
stream->readByte();
|
||||
track = stream->readByte();
|
||||
}
|
||||
|
||||
if (!track)
|
||||
error("VTOC not found");
|
||||
|
||||
byte sector = stream->readByte();
|
||||
|
||||
while (track != 0) {
|
||||
char name[kFilenameLen + 1] = { };
|
||||
|
||||
stream.reset(_disk->createReadStream(track, sector));
|
||||
stream->readByte();
|
||||
track = stream->readByte();
|
||||
sector = stream->readByte();
|
||||
stream->seek(8, SEEK_CUR);
|
||||
|
||||
for (uint i = 0; i < 7; ++i) {
|
||||
TOCEntry entry;
|
||||
TrackSector sectorList;
|
||||
sectorList.track = stream->readByte();
|
||||
sectorList.sector = stream->readByte();
|
||||
entry.type = stream->readByte();
|
||||
stream->read(name, kFilenameLen);
|
||||
|
||||
// Convert to ASCII
|
||||
for (uint j = 0; j < kFilenameLen; j++)
|
||||
name[j] &= 0x7f;
|
||||
|
||||
// Strip trailing spaces
|
||||
for (int j = kFilenameLen - 1; j >= 0; --j) {
|
||||
if (name[j] == ' ')
|
||||
name[j] = 0;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
entry.totalSectors = stream->readUint16BE();
|
||||
|
||||
// 0 is empty slot, 255 is deleted file
|
||||
if (sectorList.track != 0 && sectorList.track != 255) {
|
||||
readSectorList(sectorList, entry.sectors);
|
||||
_toc[name] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DataBlockPtr Files_AppleDOS::getDataBlock(const Common::Path &filename, uint offset) const {
|
||||
return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset));
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *Files_AppleDOS::createReadStreamText(const TOCEntry &entry) const {
|
||||
byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize);
|
||||
byte *p = buf;
|
||||
|
||||
for (uint i = 0; i < entry.sectors.size(); ++i) {
|
||||
Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[i].track, entry.sectors[i].sector));
|
||||
|
||||
assert(stream->size() == kSectorSize);
|
||||
|
||||
while (true) {
|
||||
byte textChar = stream->readByte();
|
||||
|
||||
if (stream->eos() || textChar == 0)
|
||||
break;
|
||||
|
||||
if (stream->err())
|
||||
error("Error reading text file");
|
||||
|
||||
*p++ = textChar;
|
||||
}
|
||||
}
|
||||
|
||||
return new Common::MemoryReadStream(buf, p - buf, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *Files_AppleDOS::createReadStreamBinary(const TOCEntry &entry) const {
|
||||
byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize);
|
||||
|
||||
Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[0].track, entry.sectors[0].sector));
|
||||
|
||||
if (entry.type == kFileTypeBinary)
|
||||
stream->readUint16LE(); // Skip start address
|
||||
|
||||
uint16 size = stream->readUint16LE();
|
||||
uint16 offset = 0;
|
||||
uint16 sectorIdx = 1;
|
||||
|
||||
while (true) {
|
||||
offset += stream->read(buf + offset, size - offset);
|
||||
|
||||
if (offset == size)
|
||||
break;
|
||||
|
||||
if (stream->err())
|
||||
error("Error reading binary file");
|
||||
|
||||
assert(stream->eos());
|
||||
|
||||
if (sectorIdx == entry.sectors.size())
|
||||
error("Not enough sectors for binary file size");
|
||||
|
||||
stream.reset(_disk->createReadStream(entry.sectors[sectorIdx].track, entry.sectors[sectorIdx].sector));
|
||||
++sectorIdx;
|
||||
}
|
||||
|
||||
return new Common::MemoryReadStream(buf, size, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *Files_AppleDOS::createReadStream(const Common::Path &filename, uint offset) const {
|
||||
if (!_toc.contains(filename))
|
||||
error("Failed to locate '%s'", filename.toString().c_str());
|
||||
|
||||
const TOCEntry &entry = _toc[filename];
|
||||
|
||||
Common::SeekableReadStream *stream;
|
||||
|
||||
switch(entry.type) {
|
||||
case kFileTypeText:
|
||||
stream = createReadStreamText(entry);
|
||||
break;
|
||||
case kFileTypeAppleSoft:
|
||||
case kFileTypeBinary:
|
||||
stream = createReadStreamBinary(entry);
|
||||
break;
|
||||
default:
|
||||
error("Unsupported file type %i", entry.type);
|
||||
}
|
||||
|
||||
return new Common::SeekableSubReadStream(stream, offset, stream->size(), DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
bool Files_AppleDOS::open(const Common::Path &filename) {
|
||||
_disk = new DiskImage();
|
||||
if (!_disk->open(filename))
|
||||
return false;
|
||||
|
||||
readVTOC();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
232
common/formats/disk_image.h
Normal file
232
common/formats/disk_image.h
Normal file
@@ -0,0 +1,232 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/bitarray.h"
|
||||
#include "common/ptr.h"
|
||||
#include "common/file.h"
|
||||
#include "common/debug.h"
|
||||
|
||||
#ifndef COMMON_FORMATS_DISK_IMAGE_H
|
||||
#define COMMON_FORMATS_DISK_IMAGE_H
|
||||
|
||||
namespace Common {
|
||||
|
||||
// Disk image parsers / decoders
|
||||
//
|
||||
// These classes handle floppy disk image files. Multiple formats are supported.
|
||||
// An image's file name extension determines its format. DiskImage::open selects
|
||||
// the format and expected disk size. Data can be read by track/sector/offset
|
||||
// or by the calculated stream position. Several file systems can also be read.
|
||||
//
|
||||
// Supported image formats:
|
||||
// .do Apple II disk sectors. 35 tracks, 16 sectors, no encoding.
|
||||
// .dsk Same as .do. Note that alternative sector orders are not handled.
|
||||
// Currently, if clients want to support images with a different sector
|
||||
// order, then they will have to detect and handle it themselves.
|
||||
// .d13 Apple II disk sectors. 35 tracks, 13 sectors, no encoding.
|
||||
// .nib Apple II disk nibbles. 35 tracks, 13 or 16 sectors, nibble-encoded.
|
||||
// .woz Apple II comprehensive disk bitstream. 35 tracks, 13 or 16 sectors.
|
||||
// This encoding format takes a noticeable amount of time to decode.
|
||||
// .img PC disk sectors. 40 tracks, 8 sectors, no encoding.
|
||||
// .xfd Atari disk sectors. 40 tracks, 18 sectors, no encoding.
|
||||
//
|
||||
// For encoded formats, the default behavior is to decode every track when
|
||||
// opening an image. Lazy decoding can also be enabled, causing tracks to be
|
||||
// decoded when they are first accessed. This can significantly speed up access
|
||||
// if the format is expensive to decode but only a little data is needed.
|
||||
//
|
||||
// This code was originally part of the ADL engine.
|
||||
|
||||
class SeekableReadStream;
|
||||
class String;
|
||||
|
||||
/**
|
||||
* Disk image formats that require decoding to read their sectors
|
||||
*/
|
||||
enum DiskImageEncoding {
|
||||
DiskImageEncodingNone,
|
||||
DiskImageEncodingNib,
|
||||
DiskImageEncodingWoz
|
||||
};
|
||||
|
||||
// Used for disk image detection
|
||||
int32 computeMD5(const Common::FSNode &node, Common::String &md5, uint32 md5Bytes);
|
||||
|
||||
class DataBlock {
|
||||
public:
|
||||
virtual ~DataBlock() { }
|
||||
|
||||
virtual Common::SeekableReadStream *createReadStream() const = 0;
|
||||
};
|
||||
|
||||
typedef Common::SharedPtr<DataBlock> DataBlockPtr;
|
||||
typedef Common::ScopedPtr<Common::SeekableReadStream> StreamPtr;
|
||||
|
||||
class Files {
|
||||
public:
|
||||
virtual ~Files() { }
|
||||
|
||||
virtual const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const = 0;
|
||||
virtual Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const = 0;
|
||||
virtual bool exists(const Common::Path &filename) const = 0;
|
||||
|
||||
protected:
|
||||
class DataBlock : public Common::DataBlock {
|
||||
public:
|
||||
DataBlock(const Files *files, const Common::Path &filename, uint offset) :
|
||||
_files(files),
|
||||
_filename(filename),
|
||||
_offset(offset) { }
|
||||
|
||||
Common::SeekableReadStream *createReadStream() const override {
|
||||
return _files->createReadStream(_filename, _offset);
|
||||
}
|
||||
|
||||
private:
|
||||
const Common::Path _filename;
|
||||
uint _offset;
|
||||
const Files *_files;
|
||||
};
|
||||
};
|
||||
|
||||
class DiskImage {
|
||||
public:
|
||||
DiskImage() :
|
||||
_inputStream(nullptr),
|
||||
_decodeStream(nullptr),
|
||||
_decodeBuffer(nullptr),
|
||||
_encoding(DiskImageEncodingNone),
|
||||
_lazyDecoding(false),
|
||||
_tracks(0),
|
||||
_sectorsPerTrack(0),
|
||||
_bytesPerSector(0),
|
||||
_sectorLimit(0),
|
||||
_firstSector(0) { }
|
||||
|
||||
~DiskImage() {
|
||||
delete _inputStream;
|
||||
delete _decodeStream; // frees _decodeBuffer
|
||||
}
|
||||
|
||||
bool open(const Common::Path &filename);
|
||||
bool open(const Common::FSNode &node);
|
||||
const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const;
|
||||
Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0, uint sectorsUsed = 0) const;
|
||||
Common::SeekableReadStream *releaseStream();
|
||||
Common::SeekableReadStream *getDiskStream() const { return _decodeBuffer ? _decodeStream : _inputStream; }
|
||||
uint32 read(void *dataPtr, uint32 diskPosition, uint32 dataSize);
|
||||
void setLazyDecoding(bool lazyDecoding) { _lazyDecoding = lazyDecoding; }
|
||||
void setSectorLimit(uint sectorLimit) { _sectorLimit = sectorLimit; } // Maximum number of sectors to read per track before stepping
|
||||
uint getBytesPerSector() const { return _bytesPerSector; }
|
||||
uint getSectorsPerTrack() const { return _sectorsPerTrack; }
|
||||
uint getTracks() const { return _tracks; }
|
||||
|
||||
protected:
|
||||
class DataBlock : public Common::DataBlock {
|
||||
public:
|
||||
DataBlock(const DiskImage *disk, uint track, uint sector, uint offset, uint size, uint sectorLimit) :
|
||||
_track(track),
|
||||
_sector(sector),
|
||||
_offset(offset),
|
||||
_size(size),
|
||||
_sectorLimit(sectorLimit),
|
||||
_disk(disk) { }
|
||||
|
||||
Common::SeekableReadStream *createReadStream() const override {
|
||||
return _disk->createReadStream(_track, _sector, _offset, _size, _sectorLimit);
|
||||
}
|
||||
|
||||
private:
|
||||
uint _track, _sector, _offset, _size;
|
||||
uint _sectorLimit;
|
||||
const DiskImage *_disk;
|
||||
};
|
||||
|
||||
Common::String _name;
|
||||
Common::SeekableReadStream *_inputStream;
|
||||
Common::SeekableReadStream *_decodeStream;
|
||||
byte *_decodeBuffer;
|
||||
Common::BitArray _decodedTracks;
|
||||
DiskImageEncoding _encoding;
|
||||
bool _lazyDecoding;
|
||||
|
||||
uint _tracks, _sectorsPerTrack, _bytesPerSector, _firstSector;
|
||||
uint _sectorLimit;
|
||||
|
||||
private:
|
||||
bool open(const Common::String &name, Common::File *f);
|
||||
void decodeTrack(uint track);
|
||||
};
|
||||
|
||||
// Data in plain files
|
||||
class Files_Plain : public Files {
|
||||
public:
|
||||
const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const override;
|
||||
Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const override;
|
||||
bool exists(const Common::Path &filename) const override { return Common::File::exists(filename); }
|
||||
};
|
||||
|
||||
// Data in files contained in Apple DOS 3.3 disk image
|
||||
class Files_AppleDOS : public Files {
|
||||
public:
|
||||
Files_AppleDOS();
|
||||
~Files_AppleDOS() override;
|
||||
|
||||
bool open(const Common::Path &filename);
|
||||
const DataBlockPtr getDataBlock(const Common::Path &filename, uint offset = 0) const override;
|
||||
Common::SeekableReadStream *createReadStream(const Common::Path &filename, uint offset = 0) const override;
|
||||
bool exists(const Common::Path &filename) const override { return _toc.contains(filename); }
|
||||
|
||||
private:
|
||||
enum FileType {
|
||||
kFileTypeText = 0,
|
||||
kFileTypeAppleSoft = 2,
|
||||
kFileTypeBinary = 4
|
||||
};
|
||||
|
||||
enum {
|
||||
kSectorSize = 256,
|
||||
kFilenameLen = 30
|
||||
};
|
||||
|
||||
struct TrackSector {
|
||||
byte track;
|
||||
byte sector;
|
||||
};
|
||||
|
||||
struct TOCEntry {
|
||||
byte type;
|
||||
uint16 totalSectors;
|
||||
Common::Array<TrackSector> sectors;
|
||||
};
|
||||
|
||||
void readVTOC();
|
||||
void readSectorList(TrackSector start, Common::Array<TrackSector> &list);
|
||||
Common::SeekableReadStream *createReadStreamText(const TOCEntry &entry) const;
|
||||
Common::SeekableReadStream *createReadStreamBinary(const TOCEntry &entry) const;
|
||||
|
||||
DiskImage *_disk;
|
||||
Common::HashMap<Common::Path, TOCEntry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> _toc;
|
||||
};
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
152
common/formats/formatinfo.cpp
Normal file
152
common/formats/formatinfo.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/formats/formatinfo.h"
|
||||
|
||||
#include "common/translation.h"
|
||||
#include "common/util.h"
|
||||
|
||||
#include "image/bmp.h"
|
||||
#include "image/png.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
namespace FormatInfo {
|
||||
|
||||
struct FormatTypeInfo {
|
||||
const char *_shortExtension; ///< Extension limited to 3 characters
|
||||
const char *_longExtension; ///< Extension with no length limit
|
||||
};
|
||||
|
||||
const FormatTypeInfo g_formatTypeInfo[kCount] = {
|
||||
{nullptr, nullptr},
|
||||
|
||||
// Image formats
|
||||
{"bmp", nullptr},
|
||||
{"png", nullptr},
|
||||
{"jpg", nullptr},
|
||||
{"pct", "pict"},
|
||||
|
||||
// Binary formats
|
||||
{"application/octet-stream", nullptr},
|
||||
};
|
||||
|
||||
ImageFormatCharacteristics::ImageFormatCharacteristics()
|
||||
: _lossiness(kLossinessUnknown), _supportAlpha(false), _supportPalette(false), _maxBitDepth(0) {
|
||||
}
|
||||
|
||||
ImageSaveProperties::ImageSaveProperties() : _qualityLevel(75) {
|
||||
}
|
||||
|
||||
bool getImageFormatCharacteristics(FormatID format, ImageFormatCharacteristics &outCharacteristics) {
|
||||
outCharacteristics = ImageFormatCharacteristics();
|
||||
|
||||
switch (format) {
|
||||
case kPICT:
|
||||
outCharacteristics._supportPalette = true;
|
||||
outCharacteristics._maxBitDepth = 24;
|
||||
outCharacteristics._lossiness = kLossinessEither;
|
||||
break;
|
||||
case kBMP:
|
||||
outCharacteristics._supportPalette = true;
|
||||
outCharacteristics._maxBitDepth = 24;
|
||||
outCharacteristics._lossiness = kLossinessLossless;
|
||||
break;
|
||||
case kPNG:
|
||||
outCharacteristics._supportPalette = true;
|
||||
outCharacteristics._supportAlpha = true;
|
||||
outCharacteristics._maxBitDepth = 32;
|
||||
outCharacteristics._lossiness = kLossinessLossless;
|
||||
break;
|
||||
case kJPEG:
|
||||
outCharacteristics._maxBitDepth = 24;
|
||||
outCharacteristics._lossiness = kLossinessLossy;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *getFormatExtension(Common::FormatInfo::FormatID format, bool limitTo3Characters) {
|
||||
const FormatTypeInfo &typeInfo = g_formatTypeInfo[format];
|
||||
|
||||
if (!limitTo3Characters && typeInfo._longExtension != nullptr)
|
||||
return typeInfo._longExtension;
|
||||
|
||||
return typeInfo._shortExtension;
|
||||
}
|
||||
|
||||
Common::U32String getFormatSaveDescription(Common::FormatInfo::FormatID format) {
|
||||
switch (format) {
|
||||
case kBMP:
|
||||
return _("Bitmap Image File");
|
||||
case kPNG:
|
||||
return _("PNG Image File");
|
||||
case kJPEG:
|
||||
return _("JPEG Image File");
|
||||
case kPICT:
|
||||
return _("QuickDraw PICT Image File");
|
||||
case kUntypedBinary:
|
||||
default:
|
||||
return _("Data File");
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_PNG
|
||||
static bool savePNG(Common::WriteStream &stream, const Graphics::Surface &surf, const byte *palette, const ImageSaveProperties &props) {
|
||||
return Image::writePNG(stream, surf, palette);
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool saveBMP(Common::WriteStream &stream, const Graphics::Surface &surf, const byte *palette, const ImageSaveProperties &props) {
|
||||
return Image::writeBMP(stream, surf, palette);
|
||||
}
|
||||
|
||||
ImageSaveCallback_t getImageSaveFunction(FormatID format) {
|
||||
switch (format) {
|
||||
#ifdef USE_PNG
|
||||
case kPNG:
|
||||
return savePNG;
|
||||
#endif
|
||||
case kBMP:
|
||||
return saveBMP;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
FormatSupportLevel getFormatSupportLevel(Common::FormatInfo::FormatID format) {
|
||||
switch (format) {
|
||||
case kPICT:
|
||||
return kFormatSupportLevelAvoid;
|
||||
case kPNG:
|
||||
case kJPEG:
|
||||
return kFormatSupportLevelPreferred;
|
||||
default:
|
||||
return kFormatSupportLevelDefault;
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace FormatInfo
|
||||
|
||||
} // End of namespace Common
|
||||
119
common/formats/formatinfo.h
Normal file
119
common/formats/formatinfo.h
Normal file
@@ -0,0 +1,119 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_FORMATS_FORMATINFO_H
|
||||
#define COMMON_FORMATS_FORMATINFO_H
|
||||
|
||||
#include "common/str.h"
|
||||
#include "image/bmp.h"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
struct Surface;
|
||||
|
||||
} // End of namespace Graphics
|
||||
|
||||
namespace Common {
|
||||
|
||||
namespace FormatInfo {
|
||||
|
||||
enum Lossiness {
|
||||
kLossinessUnknown,
|
||||
|
||||
kLossinessLossless,
|
||||
kLossinessLossy,
|
||||
kLossinessEither,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Values representing support for saving or sharing file types on the system.
|
||||
*/
|
||||
enum FormatSupportLevel {
|
||||
kFormatSupportLevelNone, ///< Not supported at all
|
||||
kFormatSupportLevelAvoid, ///< Can be saved/loaded but most likely unsupported even by third-party tools
|
||||
kFormatSupportLevelDefault, ///< Not natively supported by the operating system, support by third-party tools is either available or unknown
|
||||
kFormatSupportLevelSupported, ///< Well-supported by the operating system
|
||||
kFormatSupportLevelPreferred, ///< Supported by operating system, preferred over other formats if lossless conversion is possible
|
||||
};
|
||||
|
||||
enum FormatID {
|
||||
kFormatUnknown,
|
||||
|
||||
// Image formats
|
||||
kBMP, kFirstImageFormat = kBMP,
|
||||
kPNG,
|
||||
kJPEG,
|
||||
kPICT, // Macintosh PICT
|
||||
|
||||
// Binary formats
|
||||
kUntypedBinary, kFirstBinaryFormat = kUntypedBinary,
|
||||
|
||||
// End of list
|
||||
kCount,
|
||||
|
||||
kLastImageFormat = kFirstBinaryFormat - 1,
|
||||
kLastBinaryFormat = kCount - 1,
|
||||
};
|
||||
|
||||
struct ImageFormatCharacteristics {
|
||||
ImageFormatCharacteristics();
|
||||
|
||||
Lossiness _lossiness;
|
||||
bool _supportAlpha;
|
||||
bool _supportPalette;
|
||||
uint _maxBitDepth;
|
||||
};
|
||||
|
||||
struct ImageSaveProperties {
|
||||
ImageSaveProperties();
|
||||
|
||||
uint8 _qualityLevel;
|
||||
};
|
||||
|
||||
typedef bool (*ImageSaveCallback_t)(Common::WriteStream &stream, const Graphics::Surface &surf, const byte *palette, const ImageSaveProperties &saveProperties);
|
||||
|
||||
/**
|
||||
* Returns the default file extension for a format
|
||||
*
|
||||
* @param format The file format
|
||||
* @param limitTo3Characters If true, limit the extension to 3 characters
|
||||
* @return
|
||||
*/
|
||||
const char *getFormatExtension(Common::FormatInfo::FormatID format, bool limitTo3Characters);
|
||||
|
||||
Common::U32String getFormatSaveDescription(Common::FormatInfo::FormatID format);
|
||||
|
||||
ImageSaveCallback_t getImageSaveFunction(FormatID format);
|
||||
|
||||
|
||||
bool getImageFormatCharacteristics(FormatID format, ImageFormatCharacteristics &outCharacteristics);
|
||||
|
||||
/**
|
||||
* Returns the OS's level of support of a file format.
|
||||
*/
|
||||
FormatSupportLevel getFormatSupportLevel(Common::FormatInfo::FormatID format);
|
||||
|
||||
} // End of namespace FormatInfo
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
124
common/formats/iff_container.cpp
Normal file
124
common/formats/iff_container.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/formats/iff_container.h"
|
||||
#include "common/substream.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
IFFParser::IFFParser(ReadStream *stream, bool disposeStream, IFF_ID formHeaderID) : _stream(stream), _disposeStream(disposeStream), _formHeaderID(formHeaderID) {
|
||||
setInputStream(stream);
|
||||
}
|
||||
|
||||
IFFParser::~IFFParser() {
|
||||
if (_disposeStream) {
|
||||
delete _stream;
|
||||
}
|
||||
_stream = nullptr;
|
||||
}
|
||||
|
||||
void IFFParser::setInputStream(ReadStream *stream) {
|
||||
assert(stream);
|
||||
_formChunk.setInputStream(stream);
|
||||
_chunk.setInputStream(stream);
|
||||
|
||||
_formChunk.readHeader();
|
||||
if (_formChunk.id != _formHeaderID) {
|
||||
error("IFFParser input is not a FORM type IFF file");
|
||||
}
|
||||
_formSize = _formChunk.size;
|
||||
_formType = _formChunk.readUint32BE();
|
||||
}
|
||||
|
||||
void IFFParser::parse(IFFCallback &callback) {
|
||||
bool stop;
|
||||
do {
|
||||
_chunk.feed();
|
||||
_formChunk.incBytesRead(_chunk.size);
|
||||
|
||||
if (_formChunk.hasReadAll()) {
|
||||
break;
|
||||
}
|
||||
|
||||
_formChunk.incBytesRead(8);
|
||||
_chunk.readHeader();
|
||||
|
||||
// invoke the callback
|
||||
SubReadStream stream(&_chunk, _chunk.size);
|
||||
IFFChunk chunk(_chunk.id, _chunk.size, &stream);
|
||||
stop = callback(chunk);
|
||||
|
||||
// eats up all the remaining data in the chunk
|
||||
while (!stream.eos()) {
|
||||
stream.readByte();
|
||||
}
|
||||
|
||||
} while (!stop);
|
||||
}
|
||||
|
||||
|
||||
PackBitsReadStream::PackBitsReadStream(Common::ReadStream &input) : _input(&input) {
|
||||
}
|
||||
|
||||
PackBitsReadStream::~PackBitsReadStream() {
|
||||
}
|
||||
|
||||
bool PackBitsReadStream::eos() const {
|
||||
return _input->eos();
|
||||
}
|
||||
|
||||
uint32 PackBitsReadStream::read(void *dataPtr, uint32 dataSize) {
|
||||
byte *out = (byte *)dataPtr;
|
||||
uint32 left = dataSize;
|
||||
|
||||
uint32 lenR = 0, lenW = 0;
|
||||
while (left > 0 && !_input->eos()) {
|
||||
lenR = _input->readByte();
|
||||
|
||||
if (lenR == 128) {
|
||||
// no-op
|
||||
lenW = 0;
|
||||
} else if (lenR <= 127) {
|
||||
// literal run
|
||||
lenR++;
|
||||
lenW = MIN(lenR, left);
|
||||
for (uint32 j = 0; j < lenW; j++) {
|
||||
*out++ = _input->readByte();
|
||||
}
|
||||
for (; lenR > lenW; lenR--) {
|
||||
_input->readByte();
|
||||
}
|
||||
} else { // len > 128
|
||||
// expand run
|
||||
lenW = MIN((256 - lenR) + 1, left);
|
||||
byte val = _input->readByte();
|
||||
memset(out, val, lenW);
|
||||
out += lenW;
|
||||
}
|
||||
|
||||
left -= lenW;
|
||||
}
|
||||
|
||||
return dataSize - left;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
284
common/formats/iff_container.h
Normal file
284
common/formats/iff_container.h
Normal file
@@ -0,0 +1,284 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_IFF_CONTAINER_H
|
||||
#define COMMON_IFF_CONTAINER_H
|
||||
|
||||
#include "common/scummsys.h"
|
||||
#include "common/endian.h"
|
||||
#include "common/func.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/**
|
||||
* @defgroup common_iff Interchange File Format (IFF)
|
||||
* @ingroup common
|
||||
*
|
||||
* @brief API for operations on IFF container files.
|
||||
*
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
typedef uint32 IFF_ID;
|
||||
|
||||
#define ID_FORM MKTAG('F','O','R','M')
|
||||
/* EA IFF 85 group identifier */
|
||||
#define ID_CAT MKTAG('C','A','T',' ')
|
||||
/* EA IFF 85 group identifier */
|
||||
#define ID_LIST MKTAG('L','I','S','T')
|
||||
/* EA IFF 85 group identifier */
|
||||
#define ID_PROP MKTAG('P','R','O','P')
|
||||
/* EA IFF 85 group identifier */
|
||||
#define ID_END MKTAG('E','N','D',' ')
|
||||
/* unofficial END-of-FORM identifier (see Amiga RKM Devices Ed.3
|
||||
page 376) */
|
||||
#define ID_ILBM MKTAG('I','L','B','M')
|
||||
/* EA IFF 85 raster bitmap form */
|
||||
#define ID_DEEP MKTAG('D','E','E','P')
|
||||
/* Chunky pixel image files (Used in TV Paint) */
|
||||
#define ID_RGB8 MKTAG('R','G','B','8')
|
||||
/* RGB image forms, Turbo Silver (Impulse) */
|
||||
#define ID_RGBN MKTAG('R','G','B','N')
|
||||
/* RGB image forms, Turbo Silver (Impulse) */
|
||||
#define ID_PBM MKTAG('P','B','M',' ')
|
||||
/* 256-color chunky format (DPaint 2 ?) */
|
||||
#define ID_ACBM MKTAG('A','C','B','M')
|
||||
/* Amiga Contiguous Bitmap (AmigaBasic) */
|
||||
#define ID_8SVX MKTAG('8','S','V','X')
|
||||
/* Amiga 8 bits voice */
|
||||
#define ID_DATA MKTAG('D','A','T','A')
|
||||
/* Replaces FORM in Nancy Drew IFFs */
|
||||
|
||||
/* generic */
|
||||
|
||||
#define ID_FVER MKTAG('F','V','E','R')
|
||||
/* AmigaOS version string */
|
||||
#define ID_JUNK MKTAG('J','U','N','K')
|
||||
/* always ignore this chunk */
|
||||
#define ID_ANNO MKTAG('A','N','N','O')
|
||||
/* EA IFF 85 Generic Annotation chunk */
|
||||
#define ID_AUTH MKTAG('A','U','T','H')
|
||||
/* EA IFF 85 Generic Author chunk */
|
||||
#define ID_CHRS MKTAG('C','H','R','S')
|
||||
/* EA IFF 85 Generic character string chunk */
|
||||
#define ID_NAME MKTAG('N','A','M','E')
|
||||
/* EA IFF 85 Generic Name of art, music, etc. chunk */
|
||||
#define ID_TEXT MKTAG('T','E','X','T')
|
||||
/* EA IFF 85 Generic unformatted ASCII text chunk */
|
||||
#define ID_copy MKTAG('(','c',')',' ')
|
||||
/* EA IFF 85 Generic Copyright text chunk */
|
||||
|
||||
/* IFF chunks */
|
||||
|
||||
#define ID_BMHD MKTAG('B','M','H','D')
|
||||
/* IFF BitmapHeader */
|
||||
#define ID_CMAP MKTAG('C','M','A','P')
|
||||
/* IFF 8bit RGB colormap */
|
||||
#define ID_GRAB MKTAG('G','R','A','B')
|
||||
/* IFF "hotspot" coordiantes */
|
||||
#define ID_DEST MKTAG('D','E','S','T')
|
||||
/* IFF destination image info */
|
||||
#define ID_SPRT MKTAG('S','P','R','T')
|
||||
/* IFF sprite identifier */
|
||||
#define ID_CAMG MKTAG('C','A','M','G')
|
||||
/* Amiga viewportmodes */
|
||||
#define ID_BODY MKTAG('B','O','D','Y')
|
||||
/* IFF image data */
|
||||
#define ID_CRNG MKTAG('C','R','N','G')
|
||||
/* color cycling */
|
||||
#define ID_CCRT MKTAG('C','C','R','T')
|
||||
/* color cycling */
|
||||
#define ID_CLUT MKTAG('C','L','U','T')
|
||||
/* Color Lookup Table chunk */
|
||||
#define ID_DPI MKTAG('D','P','I',' ')
|
||||
/* Dots per inch chunk */
|
||||
#define ID_DPPV MKTAG('D','P','P','V')
|
||||
/* DPaint perspective chunk (EA) */
|
||||
#define ID_DRNG MKTAG('D','R','N','G')
|
||||
/* DPaint IV enhanced color cycle chunk (EA) */
|
||||
#define ID_EPSF MKTAG('E','P','S','F')
|
||||
/* Encapsulated Postscript chunk */
|
||||
#define ID_CMYK MKTAG('C','M','Y','K')
|
||||
/* Cyan, Magenta, Yellow, & Black color map (Soft-Logik) */
|
||||
#define ID_CNAM MKTAG('C','N','A','M')
|
||||
/* Color naming chunk (Soft-Logik) */
|
||||
#define ID_PCHG MKTAG('P','C','H','G')
|
||||
/* Line by line palette control information (Sebastiano Vigna) */
|
||||
#define ID_PRVW MKTAG('P','R','V','W')
|
||||
/* A mini duplicate IFF used for preview (Gary Bonham) */
|
||||
#define ID_XBMI MKTAG('X','B','M','I')
|
||||
/* eXtended BitMap Information (Soft-Logik) */
|
||||
#define ID_CTBL MKTAG('C','T','B','L')
|
||||
/* Newtek Dynamic Ham color chunk */
|
||||
#define ID_DYCP MKTAG('D','Y','C','P')
|
||||
/* Newtek Dynamic Ham chunk */
|
||||
#define ID_SHAM MKTAG('S','H','A','M')
|
||||
/* Sliced HAM color chunk */
|
||||
#define ID_ABIT MKTAG('A','B','I','T')
|
||||
/* ACBM body chunk */
|
||||
#define ID_DCOL MKTAG('D','C','O','L')
|
||||
/* unofficial direct color */
|
||||
#define ID_DPPS MKTAG('D','P','P','S')
|
||||
/* ? */
|
||||
#define ID_TINY MKTAG('T','I','N','Y')
|
||||
/* ? */
|
||||
#define ID_DPPV MKTAG('D','P','P','V')
|
||||
/* ? */
|
||||
|
||||
/* 8SVX chunks */
|
||||
|
||||
#define ID_VHDR MKTAG('V','H','D','R')
|
||||
/* 8SVX Voice8Header */
|
||||
|
||||
|
||||
/**
|
||||
* Represents a IFF chunk available to client code.
|
||||
*
|
||||
* Client code must *not* deallocate _stream when done.
|
||||
*/
|
||||
struct IFFChunk {
|
||||
IFF_ID _type;
|
||||
uint32 _size;
|
||||
ReadStream *_stream;
|
||||
|
||||
IFFChunk(IFF_ID type, uint32 size, ReadStream *stream) : _type(type), _size(size), _stream(stream) {
|
||||
assert(_stream);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parser for IFF containers.
|
||||
*/
|
||||
class IFFParser {
|
||||
|
||||
/**
|
||||
* This private class implements IFF chunk navigation.
|
||||
*/
|
||||
class IFFChunkNav : public ReadStream {
|
||||
protected:
|
||||
ReadStream *_input;
|
||||
uint32 _bytesRead;
|
||||
public:
|
||||
IFF_ID id;
|
||||
uint32 size;
|
||||
|
||||
IFFChunkNav() : _input(nullptr) {
|
||||
}
|
||||
void setInputStream(ReadStream *input) {
|
||||
_input = input;
|
||||
size = _bytesRead = 0;
|
||||
}
|
||||
void incBytesRead(uint32 inc) {
|
||||
_bytesRead += inc;
|
||||
if (_bytesRead > size) {
|
||||
error("Chunk overread");
|
||||
}
|
||||
}
|
||||
void readHeader() {
|
||||
id = _input->readUint32BE();
|
||||
size = _input->readUint32BE();
|
||||
_bytesRead = 0;
|
||||
}
|
||||
bool hasReadAll() const {
|
||||
return (size - _bytesRead) == 0;
|
||||
}
|
||||
void feed() {
|
||||
if (size % 2) {
|
||||
size++;
|
||||
}
|
||||
while (!hasReadAll()) {
|
||||
readByte();
|
||||
}
|
||||
}
|
||||
// ReadStream implementation
|
||||
bool eos() const { return _input->eos(); }
|
||||
bool err() const { return _input->err(); }
|
||||
void clearErr() { _input->clearErr(); }
|
||||
|
||||
uint32 read(void *dataPtr, uint32 dataSize) {
|
||||
incBytesRead(dataSize);
|
||||
return _input->read(dataPtr, dataSize);
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
IFFChunkNav _formChunk; ///< The root chunk of the file.
|
||||
IFFChunkNav _chunk; ///< The current chunk.
|
||||
|
||||
uint32 _formSize;
|
||||
IFF_ID _formType;
|
||||
IFF_ID _formHeaderID;
|
||||
|
||||
ReadStream *_stream;
|
||||
bool _disposeStream;
|
||||
|
||||
void setInputStream(ReadStream *stream);
|
||||
|
||||
public:
|
||||
IFFParser(ReadStream *stream, bool disposeStream = false, IFF_ID formHeaderID = ID_FORM);
|
||||
~IFFParser();
|
||||
|
||||
/**
|
||||
* Callback type for the parser.
|
||||
*/
|
||||
typedef Functor1< IFFChunk&, bool > IFFCallback;
|
||||
|
||||
/**
|
||||
* Parse the IFF container, invoking the callback on each chunk encountered.
|
||||
* The callback can interrupt the parsing by returning 'true'.
|
||||
*/
|
||||
void parse(IFFCallback &callback);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Decode a given PackBits encoded stream.
|
||||
*
|
||||
* PackBits is an RLE compression algorithm introduced by Apple. It is also
|
||||
* used to encode ILBM and PBM subtypes of IFF files, and some flavors of
|
||||
* TIFF.
|
||||
*
|
||||
* As there is no compression across row boundaries in the above formats,
|
||||
* read() will extract a *new* line on each call, discarding any alignment
|
||||
* or padding.
|
||||
*/
|
||||
class PackBitsReadStream : public Common::ReadStream {
|
||||
|
||||
protected:
|
||||
Common::ReadStream *_input;
|
||||
|
||||
public:
|
||||
PackBitsReadStream(Common::ReadStream &input);
|
||||
~PackBitsReadStream();
|
||||
|
||||
virtual bool eos() const;
|
||||
|
||||
uint32 read(void *dataPtr, uint32 dataSize);
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif
|
||||
499
common/formats/ini-file.cpp
Normal file
499
common/formats/ini-file.cpp
Normal file
@@ -0,0 +1,499 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/formats/ini-file.h"
|
||||
#include "common/file.h"
|
||||
#include "common/savefile.h"
|
||||
#include "common/system.h"
|
||||
#include "common/textconsole.h"
|
||||
#include "common/macresman.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
bool INIFile::isValidChar(char c) const {
|
||||
if (_allowNonEnglishCharacters) {
|
||||
// Chars that can break parsing are never allowed
|
||||
return !(c == '[' || c == ']' || c == '=' || c == '#' || c == '\r' || c == '\n');
|
||||
} else {
|
||||
// Only some chars are allowed
|
||||
return isAlnum(c) || c == '-' || c == '_' || c == '.' || c == ' ' || c == ':';
|
||||
}
|
||||
}
|
||||
|
||||
bool INIFile::isValidName(const String &name) const {
|
||||
if (name.empty())
|
||||
return false;
|
||||
|
||||
const char *p = name.c_str();
|
||||
while (*p && isValidChar(*p)) {
|
||||
p++;
|
||||
}
|
||||
|
||||
return *p == 0;
|
||||
}
|
||||
|
||||
INIFile::INIFile() {
|
||||
_allowNonEnglishCharacters = false;
|
||||
_suppressValuelessLineWarning = false;
|
||||
_requireKeyValueDelimiter = false;
|
||||
}
|
||||
|
||||
void INIFile::clear() {
|
||||
_sections.clear();
|
||||
}
|
||||
|
||||
bool INIFile::loadFromFile(const Path &filename) {
|
||||
File file;
|
||||
if (file.open(filename))
|
||||
return loadFromStream(file);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool INIFile::loadFromFileOrDataFork(const Path &filename) {
|
||||
SeekableReadStream *file = Common::MacResManager::openFileOrDataFork(filename);
|
||||
if (file)
|
||||
return loadFromStream(*file);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool INIFile::loadFromSaveFile(const String &filename) {
|
||||
assert(g_system);
|
||||
SaveFileManager *saveFileMan = g_system->getSavefileManager();
|
||||
SeekableReadStream *loadFile;
|
||||
|
||||
assert(saveFileMan);
|
||||
if (!(loadFile = saveFileMan->openForLoading(filename)))
|
||||
return false;
|
||||
|
||||
bool status = loadFromStream(*loadFile);
|
||||
delete loadFile;
|
||||
return status;
|
||||
}
|
||||
|
||||
bool INIFile::loadFromStream(SeekableReadStream &stream) {
|
||||
static const byte UTF8_BOM[] = {0xEF, 0xBB, 0xBF};
|
||||
Section section;
|
||||
KeyValue kv;
|
||||
String comment;
|
||||
int lineno = 0;
|
||||
section.name = _defaultSectionName;
|
||||
|
||||
// TODO: Detect if a section occurs multiple times (or likewise, if
|
||||
// a key occurs multiple times inside one section).
|
||||
|
||||
while (!stream.eos() && !stream.err()) {
|
||||
lineno++;
|
||||
|
||||
// Read a line
|
||||
String line = stream.readLine();
|
||||
|
||||
// Skip UTF-8 byte-order mark if added by a text editor.
|
||||
if (lineno == 1 && memcmp(line.c_str(), UTF8_BOM, 3) == 0) {
|
||||
line.erase(0, 3);
|
||||
}
|
||||
|
||||
line.trim();
|
||||
|
||||
if (line.size() == 0) {
|
||||
// Do nothing
|
||||
} else if (line[0] == '#' || line[0] == ';' || line.hasPrefix("//")) {
|
||||
// Accumulate comments here. Once we encounter either the start
|
||||
// of a new section, or a key-value-pair, we associate the value
|
||||
// of the 'comment' variable with that entity. The semicolon and
|
||||
// C++-style comments are used for Living Books games in Mohawk.
|
||||
comment += line;
|
||||
comment += "\n";
|
||||
} else if (line[0] == '(') {
|
||||
// HACK: The following is a hack added by Kirben to support the
|
||||
// "map.ini" used in the HE SCUMM game "SPY Fox in Hold the Mustard".
|
||||
//
|
||||
// It would be nice if this hack could be restricted to that game,
|
||||
// but the current design of this class doesn't allow to do that
|
||||
// in a nice fashion (a "isMustard" parameter is *not* a nice
|
||||
// solution).
|
||||
comment += line;
|
||||
comment += "\n";
|
||||
} else if (line[0] == '[') {
|
||||
// It's a new section which begins here.
|
||||
const char *p = line.c_str() + 1;
|
||||
// Get the section name, and check whether it's valid (that
|
||||
// is, verify that it only consists of alphanumerics,
|
||||
// periods, dashes and underscores). Mohawk Living Books games
|
||||
// can have periods in their section names.
|
||||
// WinAGI games can have colons in their section names.
|
||||
while (*p && ((_allowNonEnglishCharacters && *p != ']') || isAlnum(*p) || *p == '-' || *p == '_' || *p == '.' || *p == ' ' || *p == ':'))
|
||||
p++;
|
||||
|
||||
if (*p == '\0') {
|
||||
warning("INIFile::loadFromStream: missing ] in line %d", lineno);
|
||||
return false;
|
||||
}
|
||||
else if (*p != ']') {
|
||||
warning("INIFile::loadFromStream: Invalid character '%c' occurred in section name in line %d", *p, lineno);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Previous section is finished now, store it.
|
||||
if (!section.name.empty())
|
||||
_sections.push_back(section);
|
||||
|
||||
section.name = String(line.c_str() + 1, p);
|
||||
section.keys.clear();
|
||||
section.comment = comment;
|
||||
comment.clear();
|
||||
|
||||
if (!isValidName(section.name)) {
|
||||
warning("Invalid section name: %s", section.name.c_str());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// This line should be a line with a 'key=value' pair, or an empty one.
|
||||
|
||||
// If no section has been set, this config file is invalid!
|
||||
if (section.name.empty()) {
|
||||
warning("INIFile::loadFromStream: Key/value pair found outside a section in line %d", lineno);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split string at '=' into 'key' and 'value'. First, find the "=" delimeter.
|
||||
const char *p = strchr(line.c_str(), '=');
|
||||
if (!p) {
|
||||
if (!_suppressValuelessLineWarning)
|
||||
warning("Config file buggy: Junk found in line %d: '%s'", lineno, line.c_str());
|
||||
|
||||
// there is no '=' on this line. skip if delimiter is required.
|
||||
if (_requireKeyValueDelimiter)
|
||||
continue;
|
||||
|
||||
kv.key = line;
|
||||
kv.value.clear();
|
||||
} else {
|
||||
// Extract the key/value pair
|
||||
kv.key = String(line.c_str(), p);
|
||||
kv.value = String(p + 1);
|
||||
}
|
||||
|
||||
// Trim of spaces
|
||||
kv.key.trim();
|
||||
kv.value.trim();
|
||||
|
||||
// Store comment
|
||||
kv.comment = comment;
|
||||
comment.clear();
|
||||
|
||||
if (!isValidName(kv.key)) {
|
||||
warning("Invalid key name: %s", kv.key.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
section.keys.push_back(kv);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last section
|
||||
if (!section.name.empty())
|
||||
_sections.push_back(section);
|
||||
|
||||
return (!stream.err() || stream.eos());
|
||||
}
|
||||
|
||||
bool INIFile::saveToFile(const Path &filename) {
|
||||
DumpFile file;
|
||||
if (file.open(filename))
|
||||
return saveToStream(file);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool INIFile::saveToSaveFile(const String &filename) {
|
||||
assert(g_system);
|
||||
SaveFileManager *saveFileMan = g_system->getSavefileManager();
|
||||
WriteStream *saveFile;
|
||||
|
||||
assert(saveFileMan);
|
||||
if (!(saveFile = saveFileMan->openForSaving(filename)))
|
||||
return false;
|
||||
|
||||
bool status = saveToStream(*saveFile);
|
||||
delete saveFile;
|
||||
return status;
|
||||
}
|
||||
|
||||
bool INIFile::saveToStream(WriteStream &stream) {
|
||||
for (auto &curSection : _sections) {
|
||||
// Write out the section comment, if any
|
||||
if (!curSection.comment.empty()) {
|
||||
stream.writeString(curSection.comment);
|
||||
}
|
||||
|
||||
// Write out the section name
|
||||
stream.writeByte('[');
|
||||
stream.writeString(curSection.name);
|
||||
stream.writeByte(']');
|
||||
stream.writeByte('\n');
|
||||
|
||||
// Write out the key/value pairs
|
||||
for (auto &kv : curSection.keys) {
|
||||
// Write out the comment, if any
|
||||
if (!kv.comment.empty()) {
|
||||
stream.writeString(kv.comment);
|
||||
}
|
||||
// Write out the key/value pair
|
||||
stream.writeString(kv.key);
|
||||
stream.writeByte('=');
|
||||
stream.writeString(kv.value);
|
||||
stream.writeByte('\n');
|
||||
}
|
||||
}
|
||||
|
||||
stream.flush();
|
||||
return !stream.err();
|
||||
}
|
||||
|
||||
void INIFile::addSection(const String §ion) {
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Section *s = getSection(section);
|
||||
if (s)
|
||||
return;
|
||||
|
||||
Section newSection;
|
||||
newSection.name = section;
|
||||
_sections.push_back(newSection);
|
||||
}
|
||||
|
||||
void INIFile::removeSection(const String §ion) {
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
for (List<Section>::iterator i = _sections.begin(); i != _sections.end(); ++i) {
|
||||
if (section.equalsIgnoreCase(i->name)) {
|
||||
_sections.erase(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool INIFile::hasSection(const String §ion) const {
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const Section *s = getSection(section);
|
||||
return s != nullptr;
|
||||
}
|
||||
|
||||
void INIFile::renameSection(const String &oldName, const String &newName) {
|
||||
if (!isValidName(oldName)) {
|
||||
warning("Invalid section name: %s", oldName.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidName(newName)) {
|
||||
warning("Invalid section name: %s", newName.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Section *os = getSection(oldName);
|
||||
const Section *ns = getSection(newName);
|
||||
if (os) {
|
||||
// HACK: For now we just print a warning, for more info see the TODO
|
||||
// below.
|
||||
if (ns)
|
||||
warning("INIFile::renameSection: Section name \"%s\" already used", newName.c_str());
|
||||
else
|
||||
os->name = newName;
|
||||
}
|
||||
// TODO: Check here whether there already is a section with the
|
||||
// new name. Not sure how to cope with that case, we could:
|
||||
// - simply remove the existing "newName" section
|
||||
// - error out
|
||||
// - merge the two sections "oldName" and "newName"
|
||||
}
|
||||
|
||||
void INIFile::setDefaultSectionName(const String &name) {
|
||||
_defaultSectionName = name;
|
||||
}
|
||||
|
||||
bool INIFile::hasKey(const String &key, const String §ion) const {
|
||||
if (!isValidName(key)) {
|
||||
warning("Invalid key name: %s", key.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const Section *s = getSection(section);
|
||||
if (!s)
|
||||
return false;
|
||||
return s->hasKey(key);
|
||||
}
|
||||
|
||||
void INIFile::removeKey(const String &key, const String §ion) {
|
||||
if (!isValidName(key)) {
|
||||
warning("Invalid key name: %s", key.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
Section *s = getSection(section);
|
||||
if (s)
|
||||
s->removeKey(key);
|
||||
}
|
||||
|
||||
bool INIFile::getKey(const String &key, const String §ion, String &value) const {
|
||||
if (!isValidName(key)) {
|
||||
warning("Invalid key name: %s", key.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return false;
|
||||
}
|
||||
const Section *s = getSection(section);
|
||||
if (!s)
|
||||
return false;
|
||||
const KeyValue *kv = s->getKey(key);
|
||||
if (!kv)
|
||||
return false;
|
||||
value = kv->value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void INIFile::setKey(const String &key, const String §ion, const String &value) {
|
||||
if (!isValidName(key)) {
|
||||
warning("Invalid key name: %s", key.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidName(section)) {
|
||||
warning("Invalid section name: %s", section.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Verify that value is valid, too. In particular, it shouldn't
|
||||
// contain CR or LF...
|
||||
|
||||
Section *s = getSection(section);
|
||||
if (!s) {
|
||||
KeyValue newKV;
|
||||
newKV.key = key;
|
||||
newKV.value = value;
|
||||
|
||||
Section newSection;
|
||||
newSection.name = section;
|
||||
newSection.keys.push_back(newKV);
|
||||
|
||||
_sections.push_back(newSection);
|
||||
} else {
|
||||
s->setKey(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const INIFile::SectionKeyList INIFile::getKeys(const String §ion) const {
|
||||
const Section *s = getSection(section);
|
||||
|
||||
return s->getKeys();
|
||||
}
|
||||
|
||||
INIFile::Section *INIFile::getSection(const String §ion) {
|
||||
for (auto &curSection : _sections) {
|
||||
if (section.equalsIgnoreCase(curSection.name)) {
|
||||
return &curSection;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const INIFile::Section *INIFile::getSection(const String §ion) const {
|
||||
for (const auto &curSection : _sections) {
|
||||
if (section.equalsIgnoreCase(curSection.name)) {
|
||||
return &curSection;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool INIFile::Section::hasKey(const String &key) const {
|
||||
return getKey(key) != nullptr;
|
||||
}
|
||||
|
||||
const INIFile::KeyValue* INIFile::Section::getKey(const String &key) const {
|
||||
for (const auto &curKey : keys) {
|
||||
if (key.equalsIgnoreCase(curKey.key)) {
|
||||
return &curKey;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void INIFile::Section::setKey(const String &key, const String &value) {
|
||||
for (auto &curKey : keys) {
|
||||
if (key.equalsIgnoreCase(curKey.key)) {
|
||||
curKey.value = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
KeyValue newKV;
|
||||
newKV.key = key;
|
||||
newKV.value = value;
|
||||
keys.push_back(newKV);
|
||||
}
|
||||
|
||||
void INIFile::Section::removeKey(const String &key) {
|
||||
for (List<KeyValue>::iterator i = keys.begin(); i != keys.end(); ++i) {
|
||||
if (key.equalsIgnoreCase(i->key)) {
|
||||
keys.erase(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void INIFile::allowNonEnglishCharacters() {
|
||||
_allowNonEnglishCharacters = true;
|
||||
}
|
||||
|
||||
void INIFile::suppressValuelessLineWarning() {
|
||||
_suppressValuelessLineWarning = true;
|
||||
}
|
||||
|
||||
void INIFile::requireKeyValueDelimiter() {
|
||||
_requireKeyValueDelimiter = true;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
165
common/formats/ini-file.h
Normal file
165
common/formats/ini-file.h
Normal file
@@ -0,0 +1,165 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_INI_FILE_H
|
||||
#define COMMON_INI_FILE_H
|
||||
|
||||
#include "common/hash-str.h"
|
||||
#include "common/list.h"
|
||||
#include "common/path.h"
|
||||
#include "common/str.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/**
|
||||
* @defgroup common_ini_file INI files
|
||||
* @ingroup common
|
||||
*
|
||||
* @brief API for operations on INI configuration files.
|
||||
*
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
class SeekableReadStream;
|
||||
class WriteStream;
|
||||
|
||||
/**
|
||||
* This class allows for reading and writing INI-style config files.
|
||||
*
|
||||
* Lines starting with a '#' are ignored (i.e. treated as comments).
|
||||
* Some effort is made to preserve comments, though.
|
||||
*
|
||||
* This class makes no attempts to provide fast access to key/value pairs.
|
||||
* In particular, it stores all sections and key/value pairs in lists, not
|
||||
* in dictionaries/maps. This makes it very easy to read/write the data
|
||||
* from/to files, but of course is not appropriate for fast access.
|
||||
* The main reason is that this class is indeed geared toward doing precisely
|
||||
* that.
|
||||
*/
|
||||
class INIFile {
|
||||
public:
|
||||
struct KeyValue {
|
||||
String key; /*!< Key of the configuration entry, whitespace trimmed. */
|
||||
String value; /*!< Value of the configuration entry, whitespace trimmed. */
|
||||
String comment; /*!< Comment within an INI file, including #s and newlines. */
|
||||
};
|
||||
|
||||
typedef List<KeyValue> SectionKeyList; /*!< A list of all key/value pairs in this section. */
|
||||
|
||||
/** A section in an INI file.
|
||||
*
|
||||
* Corresponds to the following:
|
||||
* @code
|
||||
* [mySection]
|
||||
* key=value
|
||||
* @endcode
|
||||
* Comments are also stored, for convenience of users who prefer to edit
|
||||
* INI files manually.
|
||||
*/
|
||||
struct Section {
|
||||
String name; /*!< Name of the section, whitespace trimmed. */
|
||||
List<KeyValue> keys; /*!< List of all keys in this section. */
|
||||
String comment; /*!< Comment within the section, including #s and newlines. */
|
||||
|
||||
bool hasKey(const String &key) const; /*!< Check whether the section has a @p key. */
|
||||
const KeyValue* getKey(const String &key) const; /*!< Get the value assigned to a @p key. */
|
||||
void setKey(const String &key, const String &value); /*!< Assign a @p value to a @p key. */
|
||||
void removeKey(const String &key); /*!< Remove a @p key from this section. */
|
||||
const SectionKeyList getKeys() const { return keys; } /*!< Get a list of all keys in the section. */
|
||||
};
|
||||
|
||||
typedef List<Section> SectionList; /*!< A list of all sections in this INI file. */
|
||||
|
||||
public:
|
||||
INIFile();
|
||||
~INIFile() {}
|
||||
|
||||
// TODO: Maybe add a copy constructor etc.?
|
||||
|
||||
/**
|
||||
* Check whether the given string is a valid section or key name.
|
||||
* For that, it must only consist of letters, numbers, dashes, and
|
||||
* underscores. In particular, whitespace and "#", "=", "[", "]"
|
||||
* are not valid.
|
||||
*/
|
||||
bool isValidName(const String &name) const;
|
||||
|
||||
/** Reset everything stored in this INI file. */
|
||||
void clear();
|
||||
|
||||
bool loadFromFile(const Path &filename); /*!< Load configuration from a file. */
|
||||
bool loadFromFileOrDataFork(const Path &filename); /*!< Load configuration from a file in MacBinary format. */
|
||||
bool loadFromSaveFile(const String &filename); /*!< Load configuration from a save file. */
|
||||
bool loadFromStream(SeekableReadStream &stream); /*!< Load configuration from a @ref SeekableReadStream. */
|
||||
bool saveToFile(const Path &filename); /*!< Save the current configuration to a file. */
|
||||
bool saveToSaveFile(const String &filename); /*!< Save the current configuration to a save file. */
|
||||
bool saveToStream(WriteStream &stream); /*!< Save the current configuration to a @ref WriteStream. */
|
||||
|
||||
bool hasSection(const String §ion) const; /*!< Check whether the INI file has a section with the specified name. */
|
||||
void addSection(const String §ion); /*!< Add a section with the specified name to the INI file. */
|
||||
void removeSection(const String §ion); /*!< Remove the @p section from the INI file. */
|
||||
void renameSection(const String &oldName, const String &newName); /*!< Rename the INI file from @p oldName to @p newName. */
|
||||
|
||||
void setDefaultSectionName(const String &name); /*!< Set initial section name for section-less INI files. */
|
||||
|
||||
bool hasKey(const String &key, const String §ion) const; /*!< Check whether the @p section has a @p key. */
|
||||
bool getKey(const String &key, const String §ion, String &value) const; /*!< Get the @p value of a @p key in a @p section. */
|
||||
void setKey(const String &key, const String §ion, const String &value); /*!< Assign a @p value to a @p key in a @p section. */
|
||||
void removeKey(const String &key, const String §ion); /*!< Remove a @p key from this @p section. */
|
||||
|
||||
const SectionList getSections() const { return _sections; } /*!< Get a list of sections in this INI file. */
|
||||
const SectionKeyList getKeys(const String §ion) const; /*!< Get a list of keys in a @p section. */
|
||||
|
||||
void listKeyValues(StringMap &kv); /*!< Get a list of all key/value pairs in this INI file. */
|
||||
|
||||
void allowNonEnglishCharacters(); /*!< Allow non-English characters in this INI file. */
|
||||
void suppressValuelessLineWarning(); /*!< Disable warnings for lines that contain only keys. */
|
||||
|
||||
/**
|
||||
* Requires that every key/value line have a delimiter character.
|
||||
* Otherwise, lines that don't contain a delimiter are interpreted as
|
||||
* if the entire line is a key with an empty value. This can cause
|
||||
* unexpected junk lines to reach engines. (bug #13920)
|
||||
*
|
||||
* It may be better if instead this were the default behavior for
|
||||
* clients to disable if needed, but first we would need to identify
|
||||
* everything that depends on the current behavior.
|
||||
*/
|
||||
void requireKeyValueDelimiter();
|
||||
|
||||
private:
|
||||
String _defaultSectionName;
|
||||
SectionList _sections;
|
||||
bool _allowNonEnglishCharacters;
|
||||
bool _suppressValuelessLineWarning;
|
||||
bool _requireKeyValueDelimiter;
|
||||
|
||||
Section *getSection(const String §ion);
|
||||
const Section *getSection(const String §ion) const;
|
||||
bool isValidChar(char c) const; /*!< Is given char allowed in section or key names*/
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
1234
common/formats/json.cpp
Normal file
1234
common/formats/json.cpp
Normal file
File diff suppressed because it is too large
Load Diff
175
common/formats/json.h
Normal file
175
common/formats/json.h
Normal file
@@ -0,0 +1,175 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Files JSON.h and JSONValue.h part of the SimpleJSON Library - https://github.com/MJPA/SimpleJSON
|
||||
*
|
||||
* Copyright (C) 2010 Mike Anchor
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef COMMON_JSON_H
|
||||
#define COMMON_JSON_H
|
||||
|
||||
#include "common/array.h"
|
||||
#include "common/hashmap.h"
|
||||
#include "common/hash-str.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/str.h"
|
||||
|
||||
// Win32 incompatibilities
|
||||
#if (defined(WIN32) && !defined(__GNUC__))
|
||||
static inline bool isnan(double x) {
|
||||
return x != x;
|
||||
}
|
||||
|
||||
static inline bool isinf(double x) {
|
||||
return !isnan(x) && isnan(x - x);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Simple function to check a string 's' has at least 'n' characters
|
||||
static inline bool simplejson_wcsnlen(const char *s, size_t n) {
|
||||
if (s == nullptr)
|
||||
return false;
|
||||
|
||||
const char *save = s;
|
||||
while (n-- > 0) {
|
||||
if (*(save++) == 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace Common {
|
||||
|
||||
// Custom types
|
||||
class JSONValue;
|
||||
typedef Array<JSONValue*> JSONArray;
|
||||
typedef HashMap<String, JSONValue*> JSONObject;
|
||||
|
||||
class JSON;
|
||||
|
||||
enum JSONType { JSONType_Null, JSONType_String, JSONType_Bool, JSONType_Number, JSONType_IntegerNumber, JSONType_Array, JSONType_Object };
|
||||
|
||||
class JSONValue {
|
||||
friend class JSON;
|
||||
|
||||
public:
|
||||
JSONValue(/*NULL*/);
|
||||
JSONValue(const char *charValue);
|
||||
JSONValue(const String &stringValue);
|
||||
JSONValue(bool boolValue);
|
||||
JSONValue(double numberValue);
|
||||
JSONValue(long long int numberValue);
|
||||
JSONValue(const JSONArray &arrayValue);
|
||||
JSONValue(const JSONObject &objectValue);
|
||||
JSONValue(const JSONValue &source);
|
||||
~JSONValue();
|
||||
|
||||
bool isNull() const;
|
||||
bool isString() const;
|
||||
bool isBool() const;
|
||||
bool isNumber() const;
|
||||
bool isIntegerNumber() const;
|
||||
bool isArray() const;
|
||||
bool isObject() const;
|
||||
|
||||
const String &asString() const;
|
||||
bool asBool() const;
|
||||
double asNumber() const;
|
||||
long long int asIntegerNumber() const;
|
||||
const JSONArray &asArray() const;
|
||||
const JSONObject &asObject() const;
|
||||
|
||||
size_t countChildren() const;
|
||||
bool hasChild(size_t index) const;
|
||||
JSONValue *child(size_t index);
|
||||
bool hasChild(const char *name) const;
|
||||
JSONValue *child(const char *name);
|
||||
Array<String> objectKeys() const;
|
||||
|
||||
String stringify(bool const prettyprint = false) const;
|
||||
protected:
|
||||
static JSONValue *parse(const char **data);
|
||||
|
||||
private:
|
||||
static String stringifyString(const String &str);
|
||||
static uint32 decodeUtf8Char(String::const_iterator &begin, const String::const_iterator &end);
|
||||
static uint8 decodeUtf8Byte(uint8 state, uint32 &codepoint, uint8 byte);
|
||||
String stringifyImpl(size_t const indentDepth) const;
|
||||
static String indent(size_t depth);
|
||||
|
||||
JSONType _type;
|
||||
|
||||
union {
|
||||
bool _boolValue;
|
||||
double _numberValue;
|
||||
long long int _integerValue;
|
||||
String *_stringValue;
|
||||
JSONArray *_arrayValue;
|
||||
JSONObject *_objectValue;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
class JSON {
|
||||
friend class JSONValue;
|
||||
|
||||
public:
|
||||
/** Prepares raw bytes in a given stream to be parsed with Common::JSON::parse(). */
|
||||
static char *zeroTerminateContents(Common::MemoryWriteStreamDynamic &stream);
|
||||
|
||||
static JSONValue *parse(const Common::String &data) {
|
||||
return parse(data.c_str());
|
||||
}
|
||||
static JSONValue *parse(const char *data);
|
||||
static String stringify(const JSONValue *value);
|
||||
protected:
|
||||
static bool skipWhitespace(const char **data);
|
||||
static bool extractString(const char **data, String &str);
|
||||
static uint32 parseUnicode(const char **data);
|
||||
static double parseInt(const char **data);
|
||||
static double parseDecimal(const char **data);
|
||||
private:
|
||||
JSON();
|
||||
};
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
3068
common/formats/markdown.cpp
Normal file
3068
common/formats/markdown.cpp
Normal file
File diff suppressed because it is too large
Load Diff
154
common/formats/markdown.h
Normal file
154
common/formats/markdown.h
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (c) 2009, Natacha Porté
|
||||
* Copyright (c) 2011, Vicent Marti
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef COMMON_FORMATS_MARKDOWN_H
|
||||
#define COMMON_FORMATS_MARKDOWN_H
|
||||
|
||||
#include "common/scummsys.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
class String;
|
||||
|
||||
#define SUNDOWN_VERSION "1.16.0"
|
||||
#define SUNDOWN_VER_MAJOR 1
|
||||
#define SUNDOWN_VER_MINOR 16
|
||||
#define SUNDOWN_VER_REVISION 0
|
||||
|
||||
/********************
|
||||
* TYPE DEFINITIONS *
|
||||
********************/
|
||||
|
||||
struct SDDataBuffer;
|
||||
|
||||
/* mkd_autolink - type of autolink */
|
||||
enum MKDAutolink {
|
||||
MKDA_NOT_AUTOLINK, /* used internally when it is not an autolink*/
|
||||
MKDA_NORMAL, /* normal http/http/ftp/mailto/etc link */
|
||||
MKDA_EMAIL, /* e-mail link without explit mailto: */
|
||||
};
|
||||
|
||||
enum mkd_tableflags {
|
||||
MKD_TABLE_ALIGN_L = 1,
|
||||
MKD_TABLE_ALIGN_R = 2,
|
||||
MKD_TABLE_ALIGN_CENTER = 3,
|
||||
MKD_TABLE_ALIGNMASK = 3,
|
||||
MKD_TABLE_HEADER = 4
|
||||
};
|
||||
|
||||
enum mkd_extensions {
|
||||
MKDEXT_NO_INTRA_EMPHASIS = (1 << 0),
|
||||
MKDEXT_TABLES = (1 << 1),
|
||||
MKDEXT_FENCED_CODE = (1 << 2),
|
||||
MKDEXT_AUTOLINK = (1 << 3),
|
||||
MKDEXT_STRIKETHROUGH = (1 << 4),
|
||||
MKDEXT_SPACE_HEADERS = (1 << 6),
|
||||
MKDEXT_SUPERSCRIPT = (1 << 7),
|
||||
MKDEXT_LAX_SPACING = (1 << 8),
|
||||
};
|
||||
|
||||
struct SDStack {
|
||||
void **item;
|
||||
size_t size;
|
||||
size_t asize;
|
||||
};
|
||||
|
||||
/* sd_callbacks - functions for rendering parsed data */
|
||||
struct SDCallbacks {
|
||||
/* block level callbacks - NULL skips the block */
|
||||
void (*blockcode)(SDDataBuffer *ob, const SDDataBuffer *text, const SDDataBuffer *lang, void *opaque);
|
||||
void (*blockquote)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
void (*blockhtml)(SDDataBuffer *ob,const SDDataBuffer *text, void *opaque);
|
||||
void (*header)(SDDataBuffer *ob, const SDDataBuffer *text, int level, void *opaque);
|
||||
void (*hrule)(SDDataBuffer *ob, void *opaque);
|
||||
void (*list_start)(SDDataBuffer *ob, const SDDataBuffer *text, int flags, void *opaque);
|
||||
void (*list)(SDDataBuffer *ob, const SDDataBuffer *text, int flags, void *opaque);
|
||||
void (*listitem)(SDDataBuffer *ob, const SDDataBuffer *text, int flags, void *opaque);
|
||||
void (*paragraph)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
void (*table)(SDDataBuffer *ob, const SDDataBuffer *header, const SDDataBuffer *body, void *opaque);
|
||||
void (*table_row)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
void (*table_cell)(SDDataBuffer *ob, const SDDataBuffer *text, int flags, void *opaque);
|
||||
|
||||
|
||||
/* span level callbacks - NULL or return 0 prints the span verbatim */
|
||||
int (*autolink)(SDDataBuffer *ob, const SDDataBuffer *link, MKDAutolink type, void *opaque);
|
||||
int (*codespan)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
int (*double_emphasis)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
int (*emphasis)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
int (*image)(SDDataBuffer *ob, const SDDataBuffer *link, const SDDataBuffer *title, const SDDataBuffer *alt, const SDDataBuffer *ext, void *opaque);
|
||||
int (*linebreak)(SDDataBuffer *ob, void *opaque);
|
||||
int (*link)(SDDataBuffer *ob, const SDDataBuffer *link, const SDDataBuffer *title, const SDDataBuffer *content, void *opaque);
|
||||
int (*raw_html_tag)(SDDataBuffer *ob, const SDDataBuffer *tag, void *opaque);
|
||||
int (*triple_emphasis)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
int (*strikethrough)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
int (*superscript)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
|
||||
/* low level callbacks - NULL copies input directly into the output */
|
||||
void (*entity)(SDDataBuffer *ob, const SDDataBuffer *entity, void *opaque);
|
||||
void (*normal_text)(SDDataBuffer *ob, const SDDataBuffer *text, void *opaque);
|
||||
|
||||
/* header and footer */
|
||||
void (*doc_header)(SDDataBuffer *ob, void *opaque);
|
||||
void (*doc_footer)(SDDataBuffer *ob, void *opaque);
|
||||
};
|
||||
|
||||
/*********
|
||||
* FLAGS *
|
||||
*********/
|
||||
|
||||
/* list/listitem flags */
|
||||
#define MKD_LIST_ORDERED 1
|
||||
#define MKD_LI_BLOCK 2 /* <li> containing block data */
|
||||
|
||||
#define REF_TABLE_SIZE 8
|
||||
|
||||
struct LinkRef;
|
||||
|
||||
class SDMarkdown {
|
||||
public:
|
||||
SDCallbacks _cb;
|
||||
void *_opaque;
|
||||
|
||||
LinkRef *_refs[REF_TABLE_SIZE];
|
||||
byte _active_char[256];
|
||||
SDStack _work_bufs[2];
|
||||
uint _ext_flags;
|
||||
size_t _max_nesting;
|
||||
int _in_link_body;
|
||||
|
||||
SDMarkdown(uint extensions, size_t max_nesting, const SDCallbacks *callbacks, void *opaque);
|
||||
~SDMarkdown();
|
||||
|
||||
Common::String render(const byte *document, size_t doc_size);
|
||||
|
||||
void version(int *major, int *minor, int *revision);
|
||||
};
|
||||
|
||||
/* SDDataBuffer: character array buffer */
|
||||
struct SDDataBuffer {
|
||||
byte *data; /* actual character data */
|
||||
size_t size; /* size of the string */
|
||||
size_t asize; /* allocated size (0 = volatile buffer) */
|
||||
size_t unit; /* reallocation unit size (0 = read-only buffer) */
|
||||
};
|
||||
|
||||
/* sd_bufput: appends raw data to a buffer */
|
||||
void sd_bufput(SDDataBuffer *, const void *, size_t);
|
||||
|
||||
} // end of namespace Common
|
||||
|
||||
#endif // COMMON_FORMATS_MARKDOWN_H
|
||||
19
common/formats/module.mk
Normal file
19
common/formats/module.mk
Normal file
@@ -0,0 +1,19 @@
|
||||
MODULE := common/formats
|
||||
|
||||
MODULE_OBJS := \
|
||||
cue.o \
|
||||
disk_image.o \
|
||||
formatinfo.o \
|
||||
iff_container.o \
|
||||
ini-file.o \
|
||||
json.o \
|
||||
markdown.o \
|
||||
prodos.o \
|
||||
quicktime.o \
|
||||
winexe.o \
|
||||
winexe_ne.o \
|
||||
winexe_pe.o \
|
||||
xmlparser.o
|
||||
|
||||
# Include common rules
|
||||
include $(srcdir)/rules.mk
|
||||
432
common/formats/prodos.cpp
Normal file
432
common/formats/prodos.cpp
Normal file
@@ -0,0 +1,432 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/formats/prodos.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
// --- ProDOSFile methods ---
|
||||
|
||||
ProDOSFile::ProDOSFile(char name[16], uint8 type, uint16 tBlk, uint32 eof, uint16 bPtr, Common::File *disk)
|
||||
: _type(type)
|
||||
, _totalBlocks(tBlk)
|
||||
, _eof(eof)
|
||||
, _blockPtr(bPtr)
|
||||
, _disk(disk) {
|
||||
Common::strlcpy(_name, name, 16);
|
||||
}
|
||||
|
||||
/* For debugging purposes, this prints the meta data of a file */
|
||||
|
||||
void ProDOSFile::printInfo() {
|
||||
debug("File: %s", _name);
|
||||
debug("Type: %02X", _type);
|
||||
debug("data: %d", _blockPtr);
|
||||
debug("Blocks: %d", _totalBlocks);
|
||||
debug("Size: %u\n", _eof);
|
||||
}
|
||||
|
||||
/* For Common::Archive, this method just returns a string of the name */
|
||||
|
||||
Common::String ProDOSFile::getName() const {
|
||||
return Common::String(_name);
|
||||
}
|
||||
|
||||
Common::String ProDOSFile::getFileName() const {
|
||||
return Common::String(_name);
|
||||
}
|
||||
|
||||
Common::Path ProDOSFile::getPathInArchive() const {
|
||||
return Common::Path(_name);
|
||||
}
|
||||
|
||||
/* This method is used to get a single block of data from the disk,
|
||||
* but is not strictly 512 bytes. This is so that it can get only what
|
||||
* it needs when in the final block. It then adds it into the allocated
|
||||
* memory starting at memOffset
|
||||
*/
|
||||
|
||||
void ProDOSFile::getDataBlock(byte *memOffset, int offset, int size) const {
|
||||
|
||||
// All this method needs to do is read (size) of data at (offset) into (memOffset)
|
||||
_disk->seek(offset);
|
||||
_disk->read(memOffset, size);
|
||||
}
|
||||
|
||||
/* To put together a sapling file, you need to loop through the index
|
||||
* block, adding to the file data one block at a time. This method also
|
||||
* returns the size of data it got, just to make it a little simpler to
|
||||
* determine the new position within the byte data.
|
||||
*/
|
||||
|
||||
int ProDOSFile::parseIndexBlock(byte *memOffset, int blockNum, int rem) const {
|
||||
int dataSize; // For most of the blocks, this will be kBlockSize, but the last one will be the calculated remainder
|
||||
int readSize = 0; // This keeps track of the new pointer position to read data to, by updating the size of data read last
|
||||
int dataOffset; // Where in the disk to read from
|
||||
int diskPos; // Current position of cursor
|
||||
|
||||
for (int i = 0; i < blockNum; i++) {
|
||||
dataSize = (i == (blockNum - 1)) ? rem : ProDOSDisk::kBlockSize;
|
||||
dataOffset = _disk->readByte(); // Low byte is first
|
||||
|
||||
/* The cursor needs to know where to get the next pointer from in the index block,
|
||||
* but it also needs to jump to the offset of data to read it, so we need to preserve
|
||||
* the position in the index block it was in before.
|
||||
*/
|
||||
diskPos = _disk->pos();
|
||||
|
||||
_disk->skip(255); // The high bytes are stored at the end of the block
|
||||
dataOffset = (dataOffset + (_disk->readByte() << 8)) * ProDOSDisk::kBlockSize; // High byte is second
|
||||
|
||||
getDataBlock(memOffset + readSize, dataOffset, dataSize);
|
||||
readSize += dataSize;
|
||||
|
||||
// And now we resume the position before this call
|
||||
_disk->seek(diskPos);
|
||||
}
|
||||
return readSize;
|
||||
}
|
||||
|
||||
/* Extracting file data is a little tricky, as the blocks are spread out in the disk. There are 3 types
|
||||
* of regular files. Seed, Sapling, and Tree. A Seed file only needs a single data block, while a
|
||||
* Sapling needs an index block to manage up to 256 data blocks, and a Tree file needs an index block
|
||||
* to manage up to 128 (only uses half the block) index blocks. This is also an Archive method as it
|
||||
* returns a read stream of the file contents.
|
||||
*/
|
||||
|
||||
Common::SeekableReadStream *ProDOSFile::createReadStream() const {
|
||||
|
||||
// We know the total byte size of the data, so we can allocate the full amount right away
|
||||
byte *finalData = (byte *)malloc(_eof);
|
||||
|
||||
/* For a seed, this is a direct pointer to data. For a sapling it is an index file,
|
||||
* and for a tree it is a master index file.
|
||||
*/
|
||||
int indexBlock = _blockPtr * ProDOSDisk::kBlockSize;
|
||||
|
||||
/* For a sapling or tree, the size needs to be calculated, as they are made from multiple blocks.
|
||||
* _totalBlocks *includes* the index block, so the blocks before the oef block are _totalBlocks-2
|
||||
*/
|
||||
int remainder = _eof - ((_totalBlocks - 2) * ProDOSDisk::kBlockSize);
|
||||
|
||||
// For a seed file, the end of file value is also the size in the block, because it's just the one block
|
||||
if (_type == kFileTypeSeed) {
|
||||
getDataBlock(finalData, indexBlock, _eof);
|
||||
|
||||
} else if (_type == kFileTypeSapling) {
|
||||
_disk->seek(indexBlock);
|
||||
parseIndexBlock(finalData, _totalBlocks - 1, remainder);
|
||||
|
||||
} else {
|
||||
// If it's not a seed and not a sapling, it's a tree.
|
||||
_disk->seek(indexBlock);
|
||||
|
||||
/* A sapling can have an index block of up to 256, so if it is a tree,
|
||||
* that means it has more than 256 blocks
|
||||
*/
|
||||
int indexNum = (_totalBlocks - 1) / 256;
|
||||
int indexNumR = (_totalBlocks - 1) % 256;
|
||||
|
||||
/* However, to know how many index blocks there are, we need to know the remainder
|
||||
* so we can figure out if it's ex. 2 index blocks, or 2 and some portion of a 3rd
|
||||
*/
|
||||
indexNum += indexNumR;
|
||||
int blockNum;
|
||||
int indexOffset;
|
||||
int readSize = 0;
|
||||
|
||||
// Now we can loop through the master index file, parsing the individual index files similar to a sapling
|
||||
for (int i = 0; i < indexNum; i++) {
|
||||
blockNum = (i == indexNum - 1) ? indexNumR : 256;
|
||||
|
||||
indexOffset = _disk->readByte();
|
||||
int diskPos = _disk->pos();
|
||||
|
||||
_disk->skip(255);
|
||||
indexOffset = (indexOffset + (_disk->readByte() << 8)) * ProDOSDisk::kBlockSize;
|
||||
|
||||
_disk->seek(indexOffset);
|
||||
readSize += parseIndexBlock(finalData + readSize, blockNum, remainder);
|
||||
|
||||
_disk->seek(diskPos);
|
||||
}
|
||||
}
|
||||
return new Common::MemoryReadStream(finalData, _eof, DisposeAfterUse::YES);
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *ProDOSFile::createReadStreamForAltStream(Common::AltStreamType altStreamType) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- ProDOSDisk methods ---
|
||||
|
||||
/* The time and date are compressed into 16bit words, so to make them useable
|
||||
* we have to decompress them by masking the other bits and then shifting
|
||||
* to the lowest bit so that they can be stored in 8 bits each.
|
||||
*/
|
||||
|
||||
void ProDOSDisk::getDate(Date *d, uint16 date) {
|
||||
d->_day = date & 0x001f;
|
||||
d->_month = (date & 0x01e0) >> 5;
|
||||
d->_year = (date & 0xfe00) >> 9;
|
||||
}
|
||||
|
||||
void ProDOSDisk::getTime(Time *t, uint16 time) {
|
||||
t->_minute = time & 0x003f;
|
||||
t->_hour = (time & 0x1f00) >> 8;
|
||||
}
|
||||
|
||||
/* Adds most of the header data to a directory header struct */
|
||||
|
||||
void ProDOSDisk::getHeader(DirHeader *h) {
|
||||
|
||||
/* The type and nameLen fields are stored in the same byte,
|
||||
* so we need to split the byte, and shift the high bits to
|
||||
* make it readable as an int
|
||||
*/
|
||||
uint8 tempByte = _disk.readByte();
|
||||
h->_nameLen = tempByte & 0xf;
|
||||
h->_type = (tempByte & 0xf0) >> 4;
|
||||
|
||||
/* The name field is stored in 15 bytes with no null character (unused chars default to 0).
|
||||
* To make it easier to use the name, we will add a terminator regardless.
|
||||
*/
|
||||
_disk.read(h->_name, 15);
|
||||
h->_name[15] = 0;
|
||||
_disk.read(h->_reserved, 8);
|
||||
|
||||
// The time and date can be decompressed into structs right away
|
||||
getDate(&(h->_date), _disk.readUint16LE());
|
||||
getTime(&(h->_time), _disk.readUint16LE());
|
||||
|
||||
h->_ver = _disk.readByte();
|
||||
h->_minVer = _disk.readByte();
|
||||
h->_access = _disk.readByte();
|
||||
h->_entryLen = _disk.readByte();
|
||||
h->_entriesPerBlock = _disk.readByte();
|
||||
h->_fileCount = _disk.readUint16LE();
|
||||
}
|
||||
|
||||
/* Since a subdirectory header is mostly the same a volume header, we will reuse the code where we can */
|
||||
|
||||
void ProDOSDisk::getDirectoryHeader(DirHeader *h) {
|
||||
getHeader(h);
|
||||
h->_parentBlockPtr = _disk.readUint16LE();
|
||||
h->_parentEntryIndex = _disk.readByte();
|
||||
h->_parentEntryLen = _disk.readUint16LE();
|
||||
}
|
||||
|
||||
/* This is a little sneaky, but since the bulk of the header is the same, we're just going to pretend the volume header
|
||||
* is a directory header for the purose of filling it out with the same code
|
||||
*/
|
||||
|
||||
void ProDOSDisk::getVolumeHeader(VolHeader *h) {
|
||||
getHeader((DirHeader *)h);
|
||||
h->_bitmapPtr = _disk.readUint16LE();
|
||||
h->_volBlocks = _disk.readUint16LE();
|
||||
_volBlocks = h->_volBlocks;
|
||||
}
|
||||
|
||||
/* Getting a file entry header is very similar to getting a header, but with different data. */
|
||||
|
||||
void ProDOSDisk::getFileEntry(FileEntry *f) {
|
||||
uint8 tempByte = _disk.readByte();
|
||||
f->_nameLen = tempByte & 0xf;
|
||||
f->_type = (tempByte & 0xf0) >> 4;
|
||||
|
||||
_disk.read(f->_name, 15);
|
||||
f->_name[15] = 0;
|
||||
f->_ext = _disk.readByte();
|
||||
f->_blockPtr = _disk.readUint16LE();
|
||||
f->_totalBlocks = _disk.readUint16LE();
|
||||
|
||||
// The file size in bytes is stored as a long (3 bytes), lowest to highest
|
||||
f->_eof = _disk.readByte() + (_disk.readByte() << 8) + (_disk.readByte() << 16);
|
||||
|
||||
getDate(&(f->_date), _disk.readUint16LE());
|
||||
getTime(&(f->_time), _disk.readUint16LE());
|
||||
|
||||
f->_ver = _disk.readByte();
|
||||
f->_minVer = _disk.readByte();
|
||||
f->_access = _disk.readByte();
|
||||
f->_varUse = _disk.readUint16LE();
|
||||
|
||||
getDate(&(f->_modDate), _disk.readUint16LE());
|
||||
getTime(&(f->_modTime), _disk.readUint16LE());
|
||||
|
||||
f->_dirHeadPtr = _disk.readUint16LE();
|
||||
}
|
||||
|
||||
/* This is basically a loop based on the number of total files indicated by the header (including deleted file entries),
|
||||
* which parses the file entry, and if it is a regular file (ie. active and not a pascal area) then it will create a file object.
|
||||
* If it is instead a subdirectory file entry, it will use this same function to search in that directory creating files
|
||||
* and continue like that until all directories have been explored. Along the way it puts together the current file path,
|
||||
* which is stored with the file object so that the engine can search by path name.
|
||||
*/
|
||||
|
||||
void ProDOSDisk::searchDirectory(DirHeader *h, uint16 p, uint16 n, Common::String path) {
|
||||
// NB: p for previous set, but not currently used. This debug message silences any set-but-unused compiler warnings
|
||||
debug(10, "searchDirectory(h:%p prev: %d next:%d, path:%s", (void *)h, p, n, path.c_str());
|
||||
int currPos;
|
||||
int parsedFiles = 0;
|
||||
|
||||
for (int i = 0; i < h->_fileCount; i++) {
|
||||
// When we have read all the files for a given block (_entriesPerBlock), we need to change to the next block of the directory
|
||||
if (parsedFiles == h->_entriesPerBlock) {
|
||||
parsedFiles = 0;
|
||||
_disk.seek(n * kBlockSize);
|
||||
p = _disk.readUint16LE();
|
||||
n = _disk.readUint16LE();
|
||||
}
|
||||
|
||||
FileEntry fileEntry;
|
||||
getFileEntry(&fileEntry);
|
||||
parsedFiles++;
|
||||
currPos = _disk.pos();
|
||||
|
||||
// It is a regular file if (dead < file type < pascal) and the file has a size
|
||||
if ((kFileTypeDead < fileEntry._type) && (fileEntry._type < kFileTypePascal) && (fileEntry._eof > 0)) {
|
||||
Common::String fileName = path + fileEntry._name;
|
||||
ProDOSFile *currFile = new ProDOSFile(fileEntry._name, fileEntry._type, fileEntry._totalBlocks, fileEntry._eof, fileEntry._blockPtr, &_disk);
|
||||
|
||||
_files.setVal(fileName, Common::SharedPtr<ProDOSFile>(currFile));
|
||||
_disk.seek(currPos);
|
||||
|
||||
// Otherwise, if it is a subdirectory, we want to explore that subdirectory
|
||||
} else if (fileEntry._type == kFileTypeSubDir) {
|
||||
_disk.seek(fileEntry._blockPtr * kBlockSize);
|
||||
|
||||
uint16 subP = _disk.readUint16LE();
|
||||
uint16 subN = _disk.readUint16LE();
|
||||
DirHeader subHead;
|
||||
getDirectoryHeader(&subHead);
|
||||
|
||||
// Give it a temporary new path name by sticking the name of the subdirectory on to the end of the current path
|
||||
Common::String subPath = Common::String(path + subHead._name + '/');
|
||||
searchDirectory(&subHead, subP, subN, path);
|
||||
|
||||
_disk.seek(currPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The volume bitmap is a bitmap spanning as many blocks as is required to store 1 bit for every
|
||||
* block on the disk. There are 8 bits per byte and 512 bytes per block, so it needs
|
||||
* ((total_blocks / 4096) + 1 (if remainder)) * 512 bytes.
|
||||
*/
|
||||
|
||||
void ProDOSDisk::getVolumeBitmap(VolHeader *h) {
|
||||
int currPos = _disk.pos();
|
||||
int bitmapSize;
|
||||
|
||||
bitmapSize = _volBlocks / 4096;
|
||||
if ((_volBlocks % 4096) > 0) {
|
||||
bitmapSize++;
|
||||
}
|
||||
|
||||
_volBitmap = (byte *)malloc(bitmapSize * kBlockSize);
|
||||
_disk.seek(h->_bitmapPtr * kBlockSize);
|
||||
_disk.read(_volBitmap, bitmapSize);
|
||||
|
||||
_disk.seek(currPos);
|
||||
}
|
||||
|
||||
/* Gets the volume information and parses the filesystem, adding file objects to a map as it goes */
|
||||
|
||||
bool ProDOSDisk::open(const Common::Path &filename) {
|
||||
_disk.open(filename);
|
||||
_disk.read(_loader1, kBlockSize);
|
||||
_disk.read(_loader2, kBlockSize);
|
||||
|
||||
uint16 prev = _disk.readUint16LE(); // This is always going to be 0 for the volume header, but there's also no reason to skip it
|
||||
uint16 next = _disk.readUint16LE();
|
||||
|
||||
VolHeader header;
|
||||
getVolumeHeader(&header);
|
||||
getVolumeBitmap(&header);
|
||||
|
||||
Common::String pathName; // This is so that the path name starts blank, and then for every directory searched it adds the directory name to the path
|
||||
searchDirectory((DirHeader *)&header, prev, next, pathName);
|
||||
|
||||
return true; // When I get to error checking on this, the bool will be useful
|
||||
}
|
||||
|
||||
/* Constructor simply calls open(), and if it is successful it prints a statement */
|
||||
|
||||
ProDOSDisk::ProDOSDisk(const Common::Path &filename) {
|
||||
if (open(filename)) {
|
||||
//debug("%s has been loaded", filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/* Destructor closes the disk and clears the map of files */
|
||||
|
||||
ProDOSDisk::~ProDOSDisk() {
|
||||
_disk.close();
|
||||
_files.clear();
|
||||
free(_volBitmap); // Should this be free() or delete?
|
||||
}
|
||||
|
||||
// --- Common::Archive methods ---
|
||||
|
||||
// Very simple, just checks if the dictionary contains the path name
|
||||
bool ProDOSDisk::hasFile(const Common::Path &path) const {
|
||||
Common::String name = path.toString();
|
||||
return _files.contains(name);
|
||||
}
|
||||
|
||||
/* To create a list of files in the Archive, we define an iterator for the object type
|
||||
* used by the Archive member, and then loop through the hashmap, adding the object
|
||||
* pointer returned as the value from the given path. This also returns the size.
|
||||
*/
|
||||
|
||||
int ProDOSDisk::listMembers(Common::ArchiveMemberList &list) const {
|
||||
int f = 0;
|
||||
for (const auto &file : _files) {
|
||||
list.push_back(Common::ArchiveMemberList::value_type(file._value));
|
||||
++f;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// If the dictionary contains the path name (could probably call hasFile() instead), get the object
|
||||
const Common::ArchiveMemberPtr ProDOSDisk::getMember(const Common::Path &path) const {
|
||||
Common::String name = path.toString();
|
||||
if (!_files.contains(name)) {
|
||||
return Common::ArchiveMemberPtr();
|
||||
}
|
||||
return _files.getValOrDefault(name);
|
||||
}
|
||||
|
||||
/* This method is called on Archive members as it searches for the correct one,
|
||||
* so if this member is not the correct one, we return a null pointer.
|
||||
*/
|
||||
|
||||
Common::SeekableReadStream *ProDOSDisk::createReadStreamForMember(const Common::Path &path) const {
|
||||
Common::String name = path.toString();
|
||||
if (!_files.contains(name)) {
|
||||
return nullptr;
|
||||
}
|
||||
Common::SharedPtr<ProDOSFile> f = _files.getValOrDefault(name);
|
||||
return f->createReadStream();
|
||||
}
|
||||
|
||||
} // Namespace Common
|
||||
218
common/formats/prodos.h
Normal file
218
common/formats/prodos.h
Normal file
@@ -0,0 +1,218 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_PRODOS_H
|
||||
#define COMMON_PRODOS_H
|
||||
|
||||
#include "common/memstream.h"
|
||||
#include "common/file.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/error.h"
|
||||
|
||||
/* Quick note about ProDOS:
|
||||
* This disk code handles inactive, seedling, sapling, tree, and subdirectory files.
|
||||
* It does *not* handle sparse files at the moment. If a sparse file exists, it may not
|
||||
* be read correctly. It also does not do anything with Pascal files, but those should not
|
||||
* matter for game engines anyway.
|
||||
*/
|
||||
|
||||
namespace Common {
|
||||
|
||||
// These values define for ProDOS how to read the file entry, and also whether it's a keyblock (if it is a directory header, it's the keyblock of that directory)
|
||||
enum FileType : char {
|
||||
kFileTypeDead = 0,
|
||||
kFileTypeSeed = 1,
|
||||
kFileTypeSapling = 2,
|
||||
kFileTypeTree = 3,
|
||||
kFileTypePascal = 4,
|
||||
kFileTypeSubDir = 0x0D,
|
||||
kFileTypeSubHead = 0x0E,
|
||||
kFileTypeVolHead = 0x0F
|
||||
};
|
||||
|
||||
/* File extensions for all the ProDOS supported file types
|
||||
* NOTE: ProDOS user defined files are F1-F8. If they are ever required,
|
||||
* they can be added to this enum.
|
||||
*/
|
||||
enum FileExt {
|
||||
kFileExtNull = 0,
|
||||
kFileExtBad = 1,
|
||||
kFileExtTxt = 4,
|
||||
kFileExtBin = 6,
|
||||
kFileExtGfx = 8,
|
||||
kFileExtDir = 0xF,
|
||||
kFileExtDB = 0x19,
|
||||
kFileExtWord = 0x1A,
|
||||
kFileExtSpread = 0x1B,
|
||||
kFileExtSTART = 0xB3,
|
||||
kFileExtPascal = 0xEF,
|
||||
kFileExtPDCI = 0xF0,
|
||||
kFileExtPDRes = 0xF9,
|
||||
kFileExtIBProg = 0xFA,
|
||||
kFileExtIBVar = 0xFB,
|
||||
kFileExtAPSProg = 0xFC,
|
||||
kFileExtAPSVar = 0xFD,
|
||||
kFileExtEDASM = 0xFE,
|
||||
kFileExtSYS = 0xFF
|
||||
};
|
||||
|
||||
/* A ProDOS file simply contains meta data about the file and the ability to
|
||||
* find and put together the data blocks that make up the file contents.
|
||||
* This implements Common::ArchiveMember so that it can be used directly in
|
||||
* the Archive methods in ProDOSDisk.
|
||||
*/
|
||||
|
||||
class ProDOSFile : public Common::ArchiveMember {
|
||||
public:
|
||||
ProDOSFile(char name[16], uint8 type, uint16 tBlk, uint32 eof, uint16 bPtr, Common::File *disk);
|
||||
~ProDOSFile() {}; // File does not need a destructor, because the file it reads from is a pointer to Disk, and Disk has a destructor
|
||||
|
||||
// -- These are the Common::ArchiveMember related functions --
|
||||
Common::String getName() const override; // Returns _name
|
||||
Common::Path getPathInArchive() const override; // Returns _name
|
||||
Common::String getFileName() const override; // Returns _name
|
||||
Common::SeekableReadStream *createReadStream() const override; // This is what the archive needs to create a file
|
||||
Common::SeekableReadStream *createReadStreamForAltStream(Common::AltStreamType altStreamType) const override;
|
||||
void getDataBlock(byte *memOffset, int offset, int size) const; // Gets data up to the size of a single data block (512 bytes)
|
||||
int parseIndexBlock(byte *memOffset, int blockNum, int cSize) const; // Uses getDataBlock() on every pointer in the index file, adding them to byte * memory block
|
||||
|
||||
// For debugging purposes, just prints the metadata
|
||||
void printInfo();
|
||||
|
||||
private:
|
||||
char _name[16];
|
||||
uint8 _type; // As defined by enum FileType
|
||||
uint16 _blockPtr; // Block index in volume of index block or data
|
||||
uint16 _totalBlocks;
|
||||
uint32 _eof; // End Of File, used generally as size (exception being sparse files)
|
||||
Common::File *_disk; // This is a pointer because it is the same _disk as in ProDosDisk, passed to the file object
|
||||
};
|
||||
|
||||
/* This class defines the entire disk volume. Upon using the open() method,
|
||||
* it will parse the volume and add them to a hashmap where the file path
|
||||
* returns a file object. This implements Common::Archive to allow regular
|
||||
* file operations to work with it.
|
||||
*/
|
||||
|
||||
class ProDOSDisk : public Common::Archive {
|
||||
public:
|
||||
static const int kBlockSize = 512; // A ProDOS block is always 512 bytes (should this be an enum?)
|
||||
|
||||
ProDOSDisk(const Common::Path &filename);
|
||||
~ProDOSDisk(); // Frees the memory used in the dictionary and the volume bitmap
|
||||
|
||||
// Called from the constructor, it parses the volume and fills the hashmap with files
|
||||
bool open(const Common::Path &filename);
|
||||
|
||||
// These are the Common::Archive related methods
|
||||
bool hasFile(const Common::Path &path) const override;
|
||||
int listMembers(Common::ArchiveMemberList &list) const override;
|
||||
const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
|
||||
Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
|
||||
|
||||
private:
|
||||
byte _loader1[kBlockSize]; // There's not much reason for these to be needed, but I included them just in case
|
||||
byte _loader2[kBlockSize];
|
||||
Common::String _name; // Name of volume
|
||||
Common::File _disk; // The volume file itself
|
||||
int _volBlocks; // Total blocks in volume
|
||||
byte *_volBitmap; // This can determine if the volume is corrupt as it contains a bit for every block, where 0 = unused, 1 = used
|
||||
Common::HashMap<Common::String, Common::SharedPtr<ProDOSFile>> _files; // Hashmap of files in the volume, where key=Path, Value=ProDOSFile
|
||||
|
||||
struct Date {
|
||||
uint8 _day;
|
||||
uint8 _month;
|
||||
uint8 _year;
|
||||
};
|
||||
|
||||
struct Time {
|
||||
uint8 _hour;
|
||||
uint8 _minute;
|
||||
};
|
||||
|
||||
struct VolHeader {
|
||||
uint8 _type; // Not really important for a volume header, as this will always be F
|
||||
uint8 _nameLen;
|
||||
char _name[16];
|
||||
byte _reserved[8]; // Extra space reserved for possible future uses, not important
|
||||
Date _date;
|
||||
Time _time;
|
||||
uint8 _ver;
|
||||
uint8 _minVer; // Should pretty much always be 0 as far as I know
|
||||
uint8 _access; // If this ends up useful, there should be an enum for the access values
|
||||
uint8 _entryLen; // Always 27 in ProDOS 1.0
|
||||
uint8 _entriesPerBlock; // Always 0D in ProDOS 1.0
|
||||
uint16 _fileCount; // Number of files across all data blocks in this directory
|
||||
uint16 _bitmapPtr; // Block pointer to the keyblock of the bitmap for the entire volume
|
||||
uint16 _volBlocks; // Blocks in entire volume
|
||||
};
|
||||
|
||||
struct DirHeader {
|
||||
uint8 _type;
|
||||
uint8 _nameLen;
|
||||
char _name[16];
|
||||
byte _reserved[8];
|
||||
Date _date;
|
||||
Time _time;
|
||||
uint8 _ver;
|
||||
uint8 _minVer;
|
||||
uint8 _access;
|
||||
uint8 _entryLen;
|
||||
uint8 _entriesPerBlock;
|
||||
uint16 _fileCount;
|
||||
uint16 _parentBlockPtr; // These values allow ProDOS to navigate back out of a directory, but they aren't really needed by the class to navigate
|
||||
uint8 _parentEntryIndex; // Index in the current directory
|
||||
uint8 _parentEntryLen; // This is always 27 in ProDOS 1.0
|
||||
};
|
||||
|
||||
struct FileEntry {
|
||||
uint8 _type; // 0 = inactive, 1-3 = file, 4 = pascal area, 14 = subdirectory, 15 = volume directory
|
||||
uint8 _nameLen;
|
||||
char _name[16];
|
||||
uint8 _ext; // File extension, uses the enum FileExt
|
||||
uint16 _blockPtr; // Block pointer to data for seedling, index block for sapling, or master block for tree
|
||||
uint16 _totalBlocks; // Really important to remember this is the total *including* the index block
|
||||
uint32 _eof; // This is a long (3 bytes, read low to high) value representing the total readable data in a file (unless it's a sparse file, be careful!)
|
||||
Date _date;
|
||||
Time _time;
|
||||
uint8 _ver;
|
||||
uint8 _minVer;
|
||||
uint8 _access;
|
||||
uint16 _varUse;
|
||||
Date _modDate;
|
||||
Time _modTime;
|
||||
uint16 _dirHeadPtr; // Pointer to the key block of the directory that contains this file entry
|
||||
};
|
||||
|
||||
void getDate(Date *d, uint16 date); // Decompresses the date into a struct
|
||||
void getTime(Time *t, uint16 time); // Decompresses the time into a struct
|
||||
void getHeader(DirHeader *h); // Adds the main header values to the struct
|
||||
void getDirectoryHeader(DirHeader *h); // Uses getHeader and then fills in the values for the parent directory
|
||||
void getVolumeHeader(VolHeader *dir); // Uses getHeader and then fills in the volume related information (there is no parent directory to this one)
|
||||
void getFileEntry(FileEntry *f); // Adds all of the file entry information to the struct
|
||||
void searchDirectory(DirHeader *h, uint16 p, uint16 n, Common::String path); // Recursively searches all files within a directory, by calling itself for subdirectories
|
||||
void getVolumeBitmap(VolHeader *h); // Puts together the volume bitmap
|
||||
};
|
||||
|
||||
|
||||
} // Namespace Common
|
||||
|
||||
#endif
|
||||
1366
common/formats/quicktime.cpp
Normal file
1366
common/formats/quicktime.cpp
Normal file
File diff suppressed because it is too large
Load Diff
453
common/formats/quicktime.h
Normal file
453
common/formats/quicktime.h
Normal file
@@ -0,0 +1,453 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
//
|
||||
// Heavily based on ffmpeg code.
|
||||
//
|
||||
// Copyright (c) 2001 Fabrice Bellard.
|
||||
// First version by Francois Revol revol@free.fr
|
||||
// Seek function by Gael Chardon gael.dev@4now.net
|
||||
//
|
||||
|
||||
#ifndef COMMON_QUICKTIME_H
|
||||
#define COMMON_QUICKTIME_H
|
||||
|
||||
#include "common/array.h"
|
||||
#include "common/scummsys.h"
|
||||
#include "common/path.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/rational.h"
|
||||
#include "common/types.h"
|
||||
#include "common/rect.h"
|
||||
|
||||
namespace Common {
|
||||
class MacResManager;
|
||||
|
||||
/**
|
||||
* @defgroup common_quicktime Quicktime file parser
|
||||
* @ingroup common
|
||||
*
|
||||
* @brief Parser for QuickTime/MPEG-4 files.
|
||||
*
|
||||
* @details File parser used in engines:
|
||||
* - groovie
|
||||
* - mohawk
|
||||
* - mtropolis
|
||||
* - sci
|
||||
* @{
|
||||
*/
|
||||
|
||||
class QuickTimeParser {
|
||||
public:
|
||||
QuickTimeParser();
|
||||
virtual ~QuickTimeParser();
|
||||
|
||||
/**
|
||||
* Load a QuickTime file
|
||||
* @param filename the filename to load
|
||||
*/
|
||||
bool parseFile(const Path &filename);
|
||||
|
||||
/**
|
||||
* Load a QuickTime file from a SeekableReadStream
|
||||
* @param stream the stream to load
|
||||
* @param disposeFileHandle whether to delete the stream after use
|
||||
*/
|
||||
bool parseStream(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle = DisposeAfterUse::YES);
|
||||
|
||||
/**
|
||||
* Close a QuickTime file
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Flattens edit lists to a single edit containing the first edit contiguously through the last edit.
|
||||
* Used to work around bad edit offsets.
|
||||
*/
|
||||
void flattenEditLists();
|
||||
|
||||
/**
|
||||
* Set the beginning offset of the video so we can modify the offsets in the stco
|
||||
* atom of videos inside the Mohawk/mTropolis archives
|
||||
* @param offset the beginning offset of the video
|
||||
*/
|
||||
void setChunkBeginOffset(uint32 offset) { _beginOffset = offset; }
|
||||
|
||||
/**
|
||||
* Returns the movie time scale
|
||||
*/
|
||||
uint32 getTimeScale() const { return _timeScale; }
|
||||
|
||||
/** Find out if this parser has an open file handle */
|
||||
bool isOpen() const { return _fd != nullptr; }
|
||||
|
||||
enum class QTVRType {
|
||||
OTHER,
|
||||
OBJECT,
|
||||
PANORAMA
|
||||
};
|
||||
|
||||
protected:
|
||||
// This is the file handle from which data is read from. It can be the actual file handle or a decompressed stream.
|
||||
SeekableReadStream *_fd;
|
||||
|
||||
struct TimeToSampleEntry {
|
||||
int count;
|
||||
int duration; // media time
|
||||
};
|
||||
|
||||
struct SampleToChunkEntry {
|
||||
uint32 first;
|
||||
uint32 count;
|
||||
uint32 id;
|
||||
};
|
||||
|
||||
struct EditListEntry {
|
||||
uint32 trackDuration; // movie time
|
||||
uint32 timeOffset; // movie time
|
||||
int32 mediaTime; // media time
|
||||
Rational mediaRate;
|
||||
};
|
||||
|
||||
public:
|
||||
struct Track;
|
||||
|
||||
protected:
|
||||
class SampleDesc {
|
||||
public:
|
||||
SampleDesc(Track *parentTrack, uint32 codecTag);
|
||||
virtual ~SampleDesc();
|
||||
|
||||
uint32 getCodecTag() const { return _codecTag; }
|
||||
|
||||
SeekableReadStream *_extraData;
|
||||
byte _objectTypeMP4;
|
||||
|
||||
protected:
|
||||
Track *_parentTrack;
|
||||
uint32 _codecTag;
|
||||
};
|
||||
|
||||
public:
|
||||
enum CodecType {
|
||||
CODEC_TYPE_MOV_OTHER,
|
||||
CODEC_TYPE_VIDEO,
|
||||
CODEC_TYPE_AUDIO,
|
||||
CODEC_TYPE_MIDI,
|
||||
CODEC_TYPE_PANO
|
||||
};
|
||||
|
||||
enum class GraphicsMode {
|
||||
COPY = 0x0, // Directly copy the source image over the destination.
|
||||
DITHER_COPY = 0x40, // Dither the image (if needed), otherwise copy.
|
||||
BLEND = 0x20, // Blend source and destination pixel colors using opcolor values.
|
||||
TRANSPARENT = 0x24, // Replace destination with source if not equal to opcolor.
|
||||
STRAIGHT_ALPHA = 0x100, // Blend source and destination pixels, with the proportion controlled by the alpha channel.
|
||||
PREMUL_WHITE_ALPHA = 0x101, // Blend after removing pre-multiplied white from the source.
|
||||
PREMUL_BLACK_ALPHA = 0x102, // Blend after removing pre-multiplied black from the source.
|
||||
STRAIGHT_ALPHA_BLEND = 0x104, // Similar to straight alpha, but the alpha for each channel is combined with the corresponding opcolor channel.
|
||||
COMPOSITION = 0x103 // Render offscreen and then dither-copy to the main screen (tracks only).
|
||||
};
|
||||
|
||||
struct PanoramaNode {
|
||||
uint32 nodeID = 0;
|
||||
uint32 timestamp = 0;
|
||||
};
|
||||
|
||||
struct PanoramaInformation {
|
||||
String name;
|
||||
uint32 defNodeID = 0;
|
||||
float defZoom = 0.0f;
|
||||
Array<PanoramaNode> nodes;
|
||||
};
|
||||
|
||||
struct PanoSampleHeader {
|
||||
uint32 nodeID;
|
||||
|
||||
float defHPan;
|
||||
float defVPan;
|
||||
float defZoom;
|
||||
|
||||
// Constraints for this node; zero for default
|
||||
float minHPan;
|
||||
float minVPan;
|
||||
float maxHPan;
|
||||
float maxVPan;
|
||||
float minZoom;
|
||||
float maxZoom;
|
||||
|
||||
int32 nameStrOffset;
|
||||
int32 commentStrOffset;
|
||||
};
|
||||
|
||||
struct PanoHotSpot {
|
||||
uint16 id;
|
||||
uint32 type;
|
||||
uint32 typeData; // for link and navg, the ID in the link and navg table
|
||||
|
||||
// Canonical view for this hotspot
|
||||
float viewHPan;
|
||||
float viewVPan;
|
||||
float viewZoom;
|
||||
|
||||
Rect rect;
|
||||
|
||||
int32 mouseOverCursorID;
|
||||
int32 mouseDownCursorID;
|
||||
int32 mouseUpCursorID;
|
||||
|
||||
int32 nameStrOffset;
|
||||
int32 commentStrOffset;
|
||||
};
|
||||
|
||||
struct PanoHotSpotTable {
|
||||
Array<PanoHotSpot> hotSpots;
|
||||
|
||||
PanoHotSpot *get(uint16 id) {
|
||||
for (uint i = 0; i < hotSpots.size(); i++)
|
||||
if (hotSpots[i].id == id)
|
||||
return &hotSpots[i];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct PanoStringTable {
|
||||
String strings;
|
||||
|
||||
String getString(int32 offset) const;
|
||||
void debugPrint(int level, uint32 debugChannel, String prefix) const;
|
||||
};
|
||||
|
||||
struct PanoLink {
|
||||
uint16 id;
|
||||
uint16 toNodeID;
|
||||
|
||||
// Values to set at the destination node
|
||||
float toHPan;
|
||||
float toVPan;
|
||||
float toZoom;
|
||||
|
||||
int32 nameStrOffset;
|
||||
int32 commentStrOffset;
|
||||
};
|
||||
|
||||
struct PanoLinkTable {
|
||||
Array<PanoLink> links;
|
||||
|
||||
PanoLink *get(uint16 id) {
|
||||
for (uint i = 0; i < links.size(); i++)
|
||||
if (links[i].id == id)
|
||||
return &links[i];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct PanoNavigation {
|
||||
uint16 id;
|
||||
|
||||
// Info for Navigable Movie Controller
|
||||
float navgHPan; // the object's orientation in the scene
|
||||
float navgVPan;
|
||||
float navgZoom;
|
||||
|
||||
Rect zoomRect; // Starting rect for zoom out transitions
|
||||
|
||||
// Values to set at the destination node
|
||||
int32 nameStrOffset;
|
||||
int32 commentStrOffset;
|
||||
};
|
||||
|
||||
struct PanoNavigationTable {
|
||||
Array<PanoNavigation> navs;
|
||||
|
||||
PanoNavigation *get(uint16 id) {
|
||||
for (uint i = 0; i < navs.size(); i++)
|
||||
if (navs[i].id == id)
|
||||
return &navs[i];
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct PanoTrackSample {
|
||||
PanoSampleHeader hdr;
|
||||
PanoHotSpotTable hotSpotTable;
|
||||
PanoStringTable strTable;
|
||||
PanoLinkTable linkTable;
|
||||
PanoNavigationTable navTable;
|
||||
};
|
||||
|
||||
struct Track {
|
||||
Track();
|
||||
~Track();
|
||||
|
||||
uint32 chunkCount;
|
||||
uint32 *chunkOffsets;
|
||||
int timeToSampleCount;
|
||||
TimeToSampleEntry *timeToSample;
|
||||
uint32 sampleToChunkCount;
|
||||
SampleToChunkEntry *sampleToChunk;
|
||||
uint32 sampleSize;
|
||||
uint32 sampleCount;
|
||||
uint32 *sampleSizes;
|
||||
uint32 keyframeCount;
|
||||
uint32 *keyframes;
|
||||
int32 timeScale; // media time
|
||||
|
||||
uint16 width;
|
||||
uint16 height;
|
||||
CodecType codecType;
|
||||
|
||||
Array<SampleDesc *> sampleDescs;
|
||||
|
||||
Common::Array<EditListEntry> editList;
|
||||
|
||||
uint32 frameCount; // from stts
|
||||
uint32 duration; // movie time
|
||||
uint32 mediaDuration; // media time
|
||||
Rational scaleFactorX;
|
||||
Rational scaleFactorY;
|
||||
|
||||
Common::String volume;
|
||||
Common::String filename;
|
||||
Common::String path;
|
||||
Common::String directory;
|
||||
int16 nlvlFrom;
|
||||
int16 nlvlTo;
|
||||
|
||||
PanoramaInformation panoInfo;
|
||||
Array<PanoTrackSample> panoSamples;
|
||||
|
||||
GraphicsMode graphicsMode; // Transfer mode
|
||||
uint16 opcolor[3]; // RGB values used in the transfer mode specified by graphicsMode.
|
||||
|
||||
uint16 soundBalance; // Controls the sound mix between the computer's two speakers, usually set to 0.
|
||||
|
||||
uint targetTrack;
|
||||
};
|
||||
|
||||
enum class MovieType {
|
||||
kStandardObject = 1,
|
||||
kOldNavigableMovieScene,
|
||||
kObjectInScene
|
||||
};
|
||||
|
||||
struct Navigation {
|
||||
uint16 columns = 1;
|
||||
uint16 rows = 1;
|
||||
uint16 loop_size = 0; // Number of frames shot at each position
|
||||
uint16 frame_duration = 1;
|
||||
|
||||
MovieType movie_type = MovieType::kStandardObject;
|
||||
|
||||
uint16 loop_ticks = 0; // Number of ticks before next frame of loop is displayed
|
||||
|
||||
float field_of_view = 1.0f;
|
||||
|
||||
float startHPan = 1.0f;
|
||||
float startVPan = 1.0f;
|
||||
float endHPan = 1.0f;
|
||||
float endVPan = 1.0f;
|
||||
float initialHPan = 1.0f;
|
||||
float initialVPan = 1.0f;
|
||||
};
|
||||
|
||||
protected:
|
||||
virtual SampleDesc *readSampleDesc(Track *track, uint32 format, uint32 descSize) = 0;
|
||||
|
||||
uint32 _timeScale; // movie time
|
||||
uint32 _duration; // movie time
|
||||
Rational _scaleFactorX;
|
||||
Rational _scaleFactorY;
|
||||
Array<Track *> _tracks;
|
||||
Navigation _nav;
|
||||
QTVRType _qtvrType;
|
||||
uint16 _winX;
|
||||
uint16 _winY;
|
||||
|
||||
Track *_panoTrack;
|
||||
|
||||
void init();
|
||||
|
||||
private:
|
||||
struct Atom {
|
||||
uint32 type;
|
||||
uint32 offset;
|
||||
uint32 size;
|
||||
};
|
||||
|
||||
struct ParseTable {
|
||||
int (QuickTimeParser::*func)(Atom atom);
|
||||
uint32 type;
|
||||
};
|
||||
|
||||
DisposeAfterUse::Flag _disposeFileHandle;
|
||||
const ParseTable *_parseTable;
|
||||
uint32 _beginOffset;
|
||||
MacResManager *_resFork;
|
||||
bool _foundMOOV;
|
||||
|
||||
void initParseTable();
|
||||
|
||||
bool parsePanoramaAtoms();
|
||||
|
||||
int readDefault(Atom atom);
|
||||
int readLeaf(Atom atom);
|
||||
int readDREF(Atom atom);
|
||||
int readELST(Atom atom);
|
||||
int readHDLR(Atom atom);
|
||||
int readMDHD(Atom atom);
|
||||
int readMOOV(Atom atom);
|
||||
int readMVHD(Atom atom);
|
||||
int readTKHD(Atom atom);
|
||||
int readTRAK(Atom atom);
|
||||
int readSMHD(Atom atom);
|
||||
int readSTCO(Atom atom);
|
||||
int readSTSC(Atom atom);
|
||||
int readSTSD(Atom atom);
|
||||
int readSTSS(Atom atom);
|
||||
int readSTSZ(Atom atom);
|
||||
int readSTTS(Atom atom);
|
||||
int readVMHD(Atom atom);
|
||||
int readCMOV(Atom atom);
|
||||
int readWAVE(Atom atom);
|
||||
int readESDS(Atom atom);
|
||||
int readSMI(Atom atom);
|
||||
int readCTYP(Atom atom);
|
||||
int readWLOC(Atom atom);
|
||||
int readNAVG(Atom atom);
|
||||
int readGMIN(Atom atom);
|
||||
int readPINF(Atom atom);
|
||||
|
||||
int readPHDR(Atom atom);
|
||||
int readPHOT(Atom atom);
|
||||
int readSTRT(Atom atom);
|
||||
int readPLNK(Atom atom);
|
||||
int readPNAV(Atom atom);
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
272
common/formats/winexe.cpp
Normal file
272
common/formats/winexe.cpp
Normal file
@@ -0,0 +1,272 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/file.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/str.h"
|
||||
#include "common/ustr.h"
|
||||
#include "common/formats/winexe.h"
|
||||
#include "common/formats/winexe_ne.h"
|
||||
#include "common/formats/winexe_pe.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
WinResourceID &WinResourceID::operator=(const String &x) {
|
||||
_name = x;
|
||||
_idType = kIDTypeString;
|
||||
return *this;
|
||||
}
|
||||
|
||||
WinResourceID &WinResourceID::operator=(uint32 x) {
|
||||
_id = x;
|
||||
_idType = kIDTypeNumerical;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool WinResourceID::operator==(const String &x) const {
|
||||
return _idType == kIDTypeString && _name.equalsIgnoreCase(x);
|
||||
}
|
||||
|
||||
bool WinResourceID::operator==(const uint32 &x) const {
|
||||
return _idType == kIDTypeNumerical && _id == x;
|
||||
}
|
||||
|
||||
bool WinResourceID::operator==(const WinResourceID &x) const {
|
||||
if (_idType != x._idType)
|
||||
return false;
|
||||
if (_idType == kIDTypeString)
|
||||
return _name.equalsIgnoreCase(x._name);
|
||||
if (_idType == kIDTypeNumerical)
|
||||
return _id == x._id;
|
||||
return true;
|
||||
}
|
||||
|
||||
String WinResourceID::getString() const {
|
||||
if (_idType != kIDTypeString)
|
||||
return "";
|
||||
|
||||
return _name;
|
||||
}
|
||||
|
||||
uint32 WinResourceID::getID() const {
|
||||
if (_idType != kIDTypeNumerical)
|
||||
return 0xffffffff;
|
||||
|
||||
return _id;
|
||||
}
|
||||
|
||||
String WinResourceID::toString() const {
|
||||
if (_idType == kIDTypeString)
|
||||
return _name;
|
||||
else if (_idType == kIDTypeNumerical)
|
||||
return String::format("0x%08x", _id);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool WinResources::loadFromEXE(const Path &fileName) {
|
||||
if (fileName.empty())
|
||||
return false;
|
||||
|
||||
File *file = new File();
|
||||
|
||||
if (!file->open(fileName)) {
|
||||
delete file;
|
||||
return false;
|
||||
}
|
||||
|
||||
return loadFromEXE(file);
|
||||
}
|
||||
|
||||
bool WinResources::loadFromCompressedEXE(const Path &fileName) {
|
||||
// Based on https://www.cabextract.org.uk/libmspack/doc/szdd_kwaj_format.html
|
||||
|
||||
// TODO: Merge this with with loadFromEXE() so the handling of the compressed
|
||||
// EXE's is transparent
|
||||
|
||||
File file;
|
||||
|
||||
if (!file.open(fileName))
|
||||
return false;
|
||||
|
||||
// First part of the signature
|
||||
if (file.readUint32BE() != MKTAG('S','Z','D','D'))
|
||||
return false;
|
||||
|
||||
// Second part of the signature
|
||||
if (file.readUint32BE() != 0x88F02733)
|
||||
return false;
|
||||
|
||||
// Compression mode must be 'A'
|
||||
if (file.readByte() != 'A')
|
||||
return false;
|
||||
|
||||
file.readByte(); // file name character change
|
||||
uint32 unpackedLength = file.readUint32LE();
|
||||
|
||||
byte *window = new byte[0x1000]();
|
||||
int pos = 0x1000 - 16;
|
||||
|
||||
byte *unpackedData = (byte *)malloc(unpackedLength);
|
||||
assert(unpackedData);
|
||||
byte *dataPos = unpackedData;
|
||||
|
||||
uint32 remaining = unpackedLength;
|
||||
|
||||
// Apply simple LZSS decompression
|
||||
for (;;) {
|
||||
byte controlByte = file.readByte();
|
||||
|
||||
if (remaining == 0 || file.eos())
|
||||
break;
|
||||
|
||||
for (byte i = 0; i < 8; i++) {
|
||||
if (controlByte & (1 << i)) {
|
||||
*dataPos++ = window[pos++] = file.readByte();
|
||||
pos &= 0xFFF;
|
||||
if (--remaining == 0)
|
||||
break;
|
||||
} else {
|
||||
int matchPos = file.readByte();
|
||||
int matchLen = file.readByte();
|
||||
matchPos |= (matchLen & 0xF0) << 4;
|
||||
matchLen = (matchLen & 0xF) + 3;
|
||||
if ((uint32)matchLen > remaining)
|
||||
matchLen = remaining;
|
||||
remaining -= matchLen;
|
||||
|
||||
while (matchLen--) {
|
||||
*dataPos++ = window[pos++] = window[matchPos++];
|
||||
pos &= 0xFFF;
|
||||
matchPos &= 0xFFF;
|
||||
}
|
||||
|
||||
if (remaining == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete[] window;
|
||||
SeekableReadStream *stream = new MemoryReadStream(unpackedData, unpackedLength);
|
||||
|
||||
return loadFromEXE(stream);
|
||||
}
|
||||
|
||||
|
||||
WinResources *WinResources::createFromEXE(const Path &fileName) {
|
||||
WinResources *exe;
|
||||
|
||||
// First try loading via the NE code
|
||||
exe = new Common::NEResources();
|
||||
if (exe->loadFromEXE(fileName)) {
|
||||
return exe;
|
||||
}
|
||||
delete exe;
|
||||
|
||||
// Then try loading via the PE code
|
||||
exe = new Common::PEResources();
|
||||
if (exe->loadFromEXE(fileName)) {
|
||||
return exe;
|
||||
}
|
||||
delete exe;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WinResources *WinResources::createFromEXE(SeekableReadStream *stream) {
|
||||
WinResources *exe;
|
||||
|
||||
// First try loading via the NE code
|
||||
stream->seek(0);
|
||||
exe = new Common::NEResources();
|
||||
if (exe->loadFromEXE(stream, DisposeAfterUse::NO)) {
|
||||
return exe;
|
||||
}
|
||||
delete exe;
|
||||
|
||||
// Then try loading via the PE code
|
||||
stream->seek(0);
|
||||
exe = new Common::PEResources();
|
||||
if (exe->loadFromEXE(stream, DisposeAfterUse::NO)) {
|
||||
return exe;
|
||||
}
|
||||
delete exe;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WinResources::VersionInfo *WinResources::getVersionResource(const WinResourceID &id) {
|
||||
VersionInfo *info = nullptr;
|
||||
|
||||
SeekableReadStream *res = getResource(kWinVersion, id);
|
||||
if (res) {
|
||||
info = parseVersionInfo(res);
|
||||
delete res;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
WinResources::VersionInfo::VersionInfo() {
|
||||
fileVersion[0] = fileVersion[1] = fileVersion[2] = fileVersion[3] = 0;
|
||||
productVersion[0] = productVersion[1] = productVersion[2] = productVersion[3] = 0;
|
||||
fileFlagsMask = 0;
|
||||
fileFlags = 0;
|
||||
fileOS = 0;
|
||||
fileType = 0;
|
||||
fileSubtype = 0;
|
||||
fileDate[0] = fileDate[1] = 0;
|
||||
}
|
||||
|
||||
|
||||
bool WinResources::VersionInfo::readVSVersionInfo(SeekableReadStream *res) {
|
||||
// Signature check
|
||||
if (res->readUint32LE() != 0xFEEF04BD)
|
||||
return false;
|
||||
|
||||
res->readUint32LE(); // struct version
|
||||
|
||||
// The versions are stored a bit weird
|
||||
fileVersion[1] = res->readUint16LE();
|
||||
fileVersion[0] = res->readUint16LE();
|
||||
fileVersion[3] = res->readUint16LE();
|
||||
fileVersion[2] = res->readUint16LE();
|
||||
productVersion[1] = res->readUint16LE();
|
||||
productVersion[0] = res->readUint16LE();
|
||||
productVersion[3] = res->readUint16LE();
|
||||
productVersion[2] = res->readUint16LE();
|
||||
|
||||
fileFlagsMask = res->readUint32LE();
|
||||
fileFlags = res->readUint32LE();
|
||||
fileOS = res->readUint32LE();
|
||||
fileType = res->readUint32LE();
|
||||
fileSubtype = res->readUint32LE();
|
||||
fileDate[0] = res->readUint32LE();
|
||||
fileDate[1] = res->readUint32LE();
|
||||
|
||||
hash.setVal("File:", Common::U32String::format("%d.%d.%d.%d", fileVersion[0], fileVersion[1], fileVersion[2], fileVersion[3]));
|
||||
hash.setVal("Prod:", Common::U32String::format("%d.%d.%d.%d", productVersion[0], productVersion[1], productVersion[2], productVersion[3]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
179
common/formats/winexe.h
Normal file
179
common/formats/winexe.h
Normal file
@@ -0,0 +1,179 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_WINEXE_H
|
||||
#define COMMON_WINEXE_H
|
||||
|
||||
#include "common/hash-str.h"
|
||||
#include "common/str.h"
|
||||
#include "common/types.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/**
|
||||
* @defgroup common_winexe Windows resources
|
||||
* @ingroup common
|
||||
*
|
||||
* @brief API for managing Windows resources.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
class Path;
|
||||
class SeekableReadStream;
|
||||
|
||||
/** The default Windows resources. */
|
||||
enum WinResourceType {
|
||||
kWinCursor = 0x01,
|
||||
kWinBitmap = 0x02,
|
||||
kWinIcon = 0x03,
|
||||
kWinMenu = 0x04,
|
||||
kWinDialog = 0x05,
|
||||
kWinString = 0x06,
|
||||
kWinFontDir = 0x07,
|
||||
kWinFont = 0x08,
|
||||
kWinAccelerator = 0x09,
|
||||
kWinRCData = 0x0A,
|
||||
kWinMessageTable = 0x0B,
|
||||
kWinGroupCursor = 0x0C,
|
||||
kWinGroupIcon = 0x0E,
|
||||
kWinNameTable = 0x0F,
|
||||
kWinVersion = 0x10,
|
||||
kWinDlgInclude = 0x11,
|
||||
kWinPlugPlay = 0x13,
|
||||
kWinVXD = 0x14,
|
||||
kWinAniCursor = 0x15,
|
||||
kWinAniIcon = 0x16,
|
||||
kWinHTML = 0x17,
|
||||
kWinManifest = 0x18
|
||||
};
|
||||
|
||||
class WinResourceID {
|
||||
public:
|
||||
WinResourceID() { _idType = kIDTypeNull; _id = 0; }
|
||||
WinResourceID(String x) { _idType = kIDTypeString; _name = x; }
|
||||
WinResourceID(uint32 x) { _idType = kIDTypeNumerical; _id = x; }
|
||||
|
||||
WinResourceID &operator=(const String &x);
|
||||
WinResourceID &operator=(uint32 x);
|
||||
|
||||
bool operator==(const String &x) const;
|
||||
bool operator==(const uint32 &x) const;
|
||||
bool operator==(const WinResourceID &x) const;
|
||||
|
||||
String getString() const;
|
||||
uint32 getID() const;
|
||||
String toString() const;
|
||||
|
||||
private:
|
||||
/** An ID Type. */
|
||||
enum IDType {
|
||||
kIDTypeNull, ///< No type set
|
||||
kIDTypeNumerical, ///< A numerical ID.
|
||||
kIDTypeString ///< A string ID.
|
||||
} _idType;
|
||||
|
||||
String _name; ///< The resource's string ID.
|
||||
uint32 _id; ///< The resource's numerical ID.
|
||||
};
|
||||
|
||||
struct WinResourceID_Hash {
|
||||
uint operator()(const WinResourceID &id) const { return id.toString().hash(); }
|
||||
};
|
||||
|
||||
struct WinResourceID_EqualTo {
|
||||
bool operator()(const WinResourceID &id1, const WinResourceID &id2) const { return id1 == id2; }
|
||||
};
|
||||
|
||||
/**
|
||||
* A class able to load resources from a Windows Executable, such
|
||||
* as cursors, bitmaps, and sounds.
|
||||
*/
|
||||
class WinResources {
|
||||
public:
|
||||
virtual ~WinResources() {};
|
||||
|
||||
/** Clear all information. */
|
||||
virtual void clear() = 0;
|
||||
|
||||
/** Load from an EXE file. */
|
||||
virtual bool loadFromEXE(const Path &fileName);
|
||||
|
||||
/** Load from a Windows compressed EXE file. */
|
||||
virtual bool loadFromCompressedEXE(const Path &fileName);
|
||||
|
||||
/** Load from a stream. */
|
||||
virtual bool loadFromEXE(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle = DisposeAfterUse::YES) = 0;
|
||||
|
||||
/** Return a list of IDs for a given type. */
|
||||
virtual const Array<WinResourceID> getIDList(const WinResourceID &type) const = 0;
|
||||
|
||||
/** Return a list of languages for a given type and ID. */
|
||||
virtual const Array<WinResourceID> getLangList(const WinResourceID &type, const WinResourceID &id) const {
|
||||
Array<WinResourceID> array;
|
||||
return array;
|
||||
}
|
||||
|
||||
/** Return a stream to the specified resource, taking the first language found (or 0 if non-existent). */
|
||||
virtual SeekableReadStream *getResource(const WinResourceID &type, const WinResourceID &id) = 0;
|
||||
|
||||
/** Return a stream to the specified resource (or 0 if non-existent). */
|
||||
virtual SeekableReadStream *getResource(const WinResourceID &type, const WinResourceID &id, const WinResourceID &lang) {
|
||||
return getResource(type, id);
|
||||
}
|
||||
|
||||
static WinResources *createFromEXE(const Path &fileName);
|
||||
static WinResources *createFromEXE(SeekableReadStream *stream);
|
||||
|
||||
typedef Common::HashMap<Common::String, Common::U32String, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> VersionHash;
|
||||
|
||||
/** The structure of the version resource inside an NE EXE */
|
||||
struct VersionInfo {
|
||||
VersionInfo();
|
||||
|
||||
uint16 fileVersion[4];
|
||||
uint16 productVersion[4];
|
||||
uint32 fileFlagsMask;
|
||||
uint32 fileFlags;
|
||||
uint32 fileOS;
|
||||
uint32 fileType;
|
||||
uint32 fileSubtype;
|
||||
uint32 fileDate[2];
|
||||
|
||||
VersionHash hash;
|
||||
|
||||
bool readVSVersionInfo(SeekableReadStream *res);
|
||||
};
|
||||
|
||||
VersionInfo *getVersionResource(const WinResourceID &id);
|
||||
|
||||
/** Get a string from a string resource. */
|
||||
virtual String loadString(uint32 stringID) = 0;
|
||||
|
||||
protected:
|
||||
virtual VersionInfo *parseVersionInfo(SeekableReadStream *stream) = 0;
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
344
common/formats/winexe_ne.cpp
Normal file
344
common/formats/winexe_ne.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/str.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/formats/winexe_ne.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
NEResources::NEResources() {
|
||||
_exe = nullptr;
|
||||
}
|
||||
|
||||
NEResources::~NEResources() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void NEResources::clear() {
|
||||
if (_exe) {
|
||||
if (_disposeFileHandle == DisposeAfterUse::YES)
|
||||
delete _exe;
|
||||
_exe = nullptr;
|
||||
}
|
||||
|
||||
_resources.clear();
|
||||
}
|
||||
|
||||
bool NEResources::loadFromEXE(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle) {
|
||||
clear();
|
||||
|
||||
if (!stream)
|
||||
return false;
|
||||
|
||||
_exe = stream;
|
||||
_disposeFileHandle = disposeFileHandle;
|
||||
|
||||
uint32 offsetResourceTable = getResourceTableOffset();
|
||||
if (offsetResourceTable == 0xFFFFFFFF)
|
||||
return false;
|
||||
if (offsetResourceTable == 0)
|
||||
return true;
|
||||
|
||||
if (!readResourceTable(offsetResourceTable))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32 NEResources::getResourceTableOffset() {
|
||||
if (!_exe)
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
if (!_exe->seek(0))
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
// 'MZ'
|
||||
if (_exe->readUint16BE() != 0x4D5A)
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
if (!_exe->seek(60))
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
uint32 offsetSegmentEXE = _exe->readUint16LE();
|
||||
if (!_exe->seek(offsetSegmentEXE))
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
// 'NE'
|
||||
if (_exe->readUint16BE() != 0x4E45)
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
if (!_exe->seek(offsetSegmentEXE + 36))
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
uint32 offsetResourceTable = _exe->readUint16LE();
|
||||
if (offsetResourceTable == 0)
|
||||
// No resource table
|
||||
return 0;
|
||||
|
||||
// Offset relative to the segment _exe header
|
||||
offsetResourceTable += offsetSegmentEXE;
|
||||
if (!_exe->seek(offsetResourceTable))
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
return offsetResourceTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* A "resource ID -> name" mapping found in some NE executables, stored as a resource.
|
||||
* Not described by any documentation I could find, but the corresponding #define can be encountered in some old header files.
|
||||
* Mentioned by this blog post https://www.toptensoftware.com/blog/win3mu-5/ (section "The Mysterious Resource Type #15")
|
||||
*
|
||||
* The name table records have the following layout:
|
||||
*
|
||||
* struct
|
||||
* {
|
||||
* WORD lengthEntry; // length of this entry (including this field)
|
||||
* WORD resourceType; // see WinResourceType constants
|
||||
* WORD resourceId; // ordinal of resource | 0x8000
|
||||
* BYTE paddingZero; // maybe ??
|
||||
* CHAR szName[]; // null-terminated name
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
bool NEResources::readNameTable(uint32 offset, uint32 size) {
|
||||
if (!_exe)
|
||||
return false;
|
||||
|
||||
uint32 curPos = _exe->pos();
|
||||
if (!_exe->seek(offset)) {
|
||||
_exe->seek(curPos);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 offsetEnd = offset + size;
|
||||
while (_exe->pos() < offsetEnd) {
|
||||
uint16 lengthEntry = _exe->readUint16LE();
|
||||
if (lengthEntry == 0)
|
||||
break;
|
||||
|
||||
uint16 resourceType = _exe->readUint16LE();
|
||||
uint16 resourceId = _exe->readUint16LE();
|
||||
resourceId &= 0x7FFF;
|
||||
_exe->skip(1); // paddingZero
|
||||
|
||||
Common::String name = _exe->readString('\0', lengthEntry - 7);
|
||||
if (name.size() + 8 != lengthEntry) {
|
||||
warning("NEResources::readNameTable(): unexpected string length in name table (expected = %d, found = %d, string = %s)",
|
||||
lengthEntry - 7, name.size() + 1, name.c_str());
|
||||
}
|
||||
|
||||
debug(2, "NEResources::readNameTable(): resourceType = %d, resourceId = %d, name = %s",
|
||||
resourceType, resourceId, name.c_str());
|
||||
_nameTable[resourceType][resourceId] = name;
|
||||
}
|
||||
|
||||
_exe->seek(curPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *const s_resTypeNames[] = {
|
||||
"", "cursor", "bitmap", "icon", "menu", "dialog", "string",
|
||||
"font_dir", "font", "accelerator", "rc_data", "msg_table",
|
||||
"group_cursor", "", "group_icon", "name_table", "version", "dlg_include",
|
||||
"", "plug_play", "vxd", "ani_cursor", "ani_icon", "html",
|
||||
"manifest"
|
||||
};
|
||||
|
||||
bool NEResources::readResourceTable(uint32 offset) {
|
||||
if (!_exe)
|
||||
return false;
|
||||
|
||||
if (!_exe->seek(offset))
|
||||
return false;
|
||||
|
||||
uint32 align = 1 << _exe->readUint16LE();
|
||||
uint16 typeID = _exe->readUint16LE();
|
||||
|
||||
while (typeID != 0) {
|
||||
// High bit of the type means integer type
|
||||
WinResourceID type;
|
||||
if (typeID & 0x8000)
|
||||
type = typeID & 0x7FFF;
|
||||
else
|
||||
type = getResourceString(*_exe, offset + typeID);
|
||||
|
||||
uint16 resCount = _exe->readUint16LE();
|
||||
|
||||
debug(2, "%d resources in table", resCount);
|
||||
|
||||
_exe->skip(4); // reserved
|
||||
|
||||
if (resCount > 256) {
|
||||
warning("NEResources::readResourceTable(): resource table looks malformed, %d entries > 256", resCount);
|
||||
resCount = 256;
|
||||
}
|
||||
|
||||
for (int i = 0; i < resCount; i++) {
|
||||
Resource res;
|
||||
|
||||
// Resource properties
|
||||
res.offset = _exe->readUint16LE() * align;
|
||||
res.size = _exe->readUint16LE() * align;
|
||||
res.flags = _exe->readUint16LE();
|
||||
uint16 id = _exe->readUint16LE();
|
||||
res.handle = _exe->readUint16LE();
|
||||
res.usage = _exe->readUint16LE();
|
||||
|
||||
res.type = type;
|
||||
|
||||
// High bit means integer type
|
||||
if (id & 0x8000)
|
||||
res.id = id & 0x7FFF;
|
||||
else
|
||||
res.id = getResourceString(*_exe, offset + id);
|
||||
|
||||
if (typeID & 0x8000 && ((typeID & 0x7FFF) < ARRAYSIZE(s_resTypeNames)) && s_resTypeNames[typeID & 0x7FFF][0] != 0)
|
||||
debug(2, "Found resource %s %s", s_resTypeNames[typeID & 0x7FFF], res.id.toString().c_str());
|
||||
else
|
||||
debug(2, "Found resource %s %s", type.toString().c_str(), res.id.toString().c_str());
|
||||
|
||||
_resources.push_back(res);
|
||||
if (type == kWinNameTable)
|
||||
readNameTable(res.offset, res.size);
|
||||
}
|
||||
|
||||
typeID = _exe->readUint16LE();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String NEResources::getResourceString(SeekableReadStream &exe, uint32 offset) {
|
||||
uint32 curPos = exe.pos();
|
||||
|
||||
if (!exe.seek(offset)) {
|
||||
exe.seek(curPos);
|
||||
return "";
|
||||
}
|
||||
|
||||
uint8 length = exe.readByte();
|
||||
|
||||
String string;
|
||||
for (uint16 i = 0; i < length; i++) {
|
||||
char b = (char)exe.readByte();
|
||||
|
||||
// Do not read beyond end of string
|
||||
if (!b) {
|
||||
break;
|
||||
}
|
||||
|
||||
string += b;
|
||||
}
|
||||
|
||||
exe.seek(curPos);
|
||||
return string;
|
||||
}
|
||||
|
||||
const NEResources::Resource *NEResources::findResource(const WinResourceID &type, const WinResourceID &id) const {
|
||||
for (const auto &resource : _resources) {
|
||||
if (resource.type == type &&
|
||||
(resource.id == id ||
|
||||
(!_nameTable.empty() &&
|
||||
_nameTable.contains(resource.type) &&
|
||||
_nameTable[resource.type].contains(resource.id) &&
|
||||
_nameTable[resource.type][resource.id] == id.toString())))
|
||||
return &resource;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SeekableReadStream *NEResources::getResource(const WinResourceID &type, const WinResourceID &id) {
|
||||
const Resource *res = findResource(type, id);
|
||||
|
||||
if (!res)
|
||||
return nullptr;
|
||||
|
||||
_exe->seek(res->offset);
|
||||
return _exe->readStream(res->size);
|
||||
}
|
||||
|
||||
const Array<WinResourceID> NEResources::getIDList(const WinResourceID &type) const {
|
||||
Array<WinResourceID> idArray;
|
||||
|
||||
for (const auto &resource : _resources)
|
||||
if (resource.type == type)
|
||||
idArray.push_back(resource.id);
|
||||
|
||||
return idArray;
|
||||
}
|
||||
|
||||
String NEResources::loadString(uint32 stringID) {
|
||||
// This is how the resource ID is calculated
|
||||
String string;
|
||||
SeekableReadStream *stream = getResource(kWinString, (stringID >> 4) + 1);
|
||||
|
||||
if (!stream)
|
||||
return string;
|
||||
|
||||
// Skip over strings we don't care about
|
||||
uint32 startString = stringID & ~0xF;
|
||||
|
||||
for (uint32 i = startString; i < stringID; i++)
|
||||
stream->skip(stream->readByte());
|
||||
|
||||
byte size = stream->readByte();
|
||||
while (size--)
|
||||
string += (char)stream->readByte();
|
||||
|
||||
delete stream;
|
||||
return string;
|
||||
}
|
||||
|
||||
WinResources::VersionInfo *NEResources::parseVersionInfo(SeekableReadStream *res) {
|
||||
VersionInfo *info = new VersionInfo;
|
||||
|
||||
while (res->pos() < res->size() && !res->eos()) {
|
||||
while (res->pos() % 4 && !res->eos()) // Pad to 4
|
||||
res->readByte();
|
||||
|
||||
/* uint16 len = */ res->readUint16LE();
|
||||
/* uint16 valLen = */ res->readUint16LE();
|
||||
uint16 c;
|
||||
|
||||
Common::String key;
|
||||
while ((c = res->readByte()) != 0 && !res->eos())
|
||||
key += c;
|
||||
|
||||
while (res->pos() % 4 && !res->eos()) // Pad to 4
|
||||
res->readByte();
|
||||
|
||||
if (res->eos())
|
||||
break;
|
||||
|
||||
if (key == "VS_VERSION_INFO") {
|
||||
if (!info->readVSVersionInfo(res))
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
119
common/formats/winexe_ne.h
Normal file
119
common/formats/winexe_ne.h
Normal file
@@ -0,0 +1,119 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_WINEXE_NE_H
|
||||
#define COMMON_WINEXE_NE_H
|
||||
|
||||
#include "common/list.h"
|
||||
#include "common/str.h"
|
||||
#include "common/formats/winexe.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/**
|
||||
* @defgroup common_winexe_ne Windows New Executable resources
|
||||
* @ingroup common_winexe
|
||||
*
|
||||
* @brief API for managing Windows New Executable resources.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
template<class T> class Array;
|
||||
class SeekableReadStream;
|
||||
|
||||
/**
|
||||
* A class able to load resources from a Windows New Executable, such
|
||||
* as cursors, bitmaps, and sounds.
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/New_Executable for more info.
|
||||
*/
|
||||
class NEResources : public WinResources {
|
||||
public:
|
||||
NEResources();
|
||||
~NEResources();
|
||||
|
||||
/** Clear all information. */
|
||||
void clear();
|
||||
|
||||
/** Load from an EXE file. */
|
||||
using WinResources::loadFromEXE;
|
||||
|
||||
/** Load from a stream. */
|
||||
bool loadFromEXE(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle = DisposeAfterUse::YES);
|
||||
|
||||
/** Return a list of resources for a given type. */
|
||||
const Array<WinResourceID> getIDList(const WinResourceID &type) const;
|
||||
|
||||
/** Return a stream to the specified resource (or 0 if non-existent). */
|
||||
SeekableReadStream *getResource(const WinResourceID &type, const WinResourceID &id);
|
||||
|
||||
/** Get a string from a string resource. */
|
||||
String loadString(uint32 stringID);
|
||||
|
||||
protected:
|
||||
VersionInfo *parseVersionInfo(SeekableReadStream *stream);
|
||||
|
||||
private:
|
||||
/** A resource. */
|
||||
struct Resource {
|
||||
WinResourceID id;
|
||||
|
||||
WinResourceID type; ///< Type of the resource.
|
||||
|
||||
uint32 offset; ///< Offset within the EXE.
|
||||
uint32 size; ///< Size of the data.
|
||||
|
||||
uint16 flags;
|
||||
uint16 handle;
|
||||
uint16 usage;
|
||||
};
|
||||
|
||||
SeekableReadStream *_exe; ///< Current file.
|
||||
DisposeAfterUse::Flag _disposeFileHandle;
|
||||
|
||||
/** All resources. */
|
||||
List<Resource> _resources;
|
||||
|
||||
typedef HashMap<WinResourceID, Common::String, WinResourceID_Hash, WinResourceID_EqualTo> IDMap;
|
||||
typedef HashMap<WinResourceID, IDMap, WinResourceID_Hash, WinResourceID_EqualTo> TypeMap;
|
||||
TypeMap _nameTable;
|
||||
|
||||
/** Read the offset to the resource table. */
|
||||
uint32 getResourceTableOffset();
|
||||
/** Read the resource table. */
|
||||
bool readResourceTable(uint32 offset);
|
||||
|
||||
/** Read the name table. */
|
||||
bool readNameTable(uint32 offset, uint32 size);
|
||||
|
||||
/** Find a specific resource. */
|
||||
const Resource *findResource(const WinResourceID &type, const WinResourceID &id) const;
|
||||
|
||||
/** Read a resource string. */
|
||||
static String getResourceString(SeekableReadStream &exe, uint32 offset);
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
310
common/formats/winexe_pe.cpp
Normal file
310
common/formats/winexe_pe.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/array.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/endian.h"
|
||||
#include "common/str.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/formats/winexe_pe.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
PEResources::PEResources() {
|
||||
_exe = nullptr;
|
||||
_disposeFileHandle = DisposeAfterUse::YES;
|
||||
}
|
||||
|
||||
PEResources::~PEResources() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void PEResources::clear() {
|
||||
_sections.clear();
|
||||
_resources.clear();
|
||||
if (_exe) {
|
||||
if (_disposeFileHandle == DisposeAfterUse::YES)
|
||||
delete _exe;
|
||||
_exe = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool PEResources::loadFromEXE(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle) {
|
||||
clear();
|
||||
|
||||
_exe = stream;
|
||||
_disposeFileHandle = disposeFileHandle;
|
||||
|
||||
if (!stream)
|
||||
return false;
|
||||
|
||||
if (stream->readUint16BE() != MKTAG16('M', 'Z'))
|
||||
return false;
|
||||
|
||||
stream->skip(58);
|
||||
|
||||
uint32 peOffset = stream->readUint32LE();
|
||||
|
||||
if (!peOffset || peOffset >= (uint32)stream->size())
|
||||
return false;
|
||||
|
||||
stream->seek(peOffset);
|
||||
|
||||
if (stream->readUint32BE() != MKTAG('P','E',0,0))
|
||||
return false;
|
||||
|
||||
stream->skip(2);
|
||||
uint16 sectionCount = stream->readUint16LE();
|
||||
stream->skip(12);
|
||||
uint16 optionalHeaderSize = stream->readUint16LE();
|
||||
stream->skip(optionalHeaderSize + 2);
|
||||
|
||||
// Read in all the sections
|
||||
for (uint16 i = 0; i < sectionCount; i++) {
|
||||
char sectionName[9];
|
||||
stream->read(sectionName, 8);
|
||||
sectionName[8] = 0;
|
||||
|
||||
Section section;
|
||||
stream->skip(4);
|
||||
section.virtualAddress = stream->readUint32LE();
|
||||
section.size = stream->readUint32LE();
|
||||
section.offset = stream->readUint32LE();
|
||||
stream->skip(16);
|
||||
|
||||
_sections[sectionName] = section;
|
||||
}
|
||||
|
||||
// Currently, we require loading a resource section
|
||||
if (!_sections.contains(".rsrc")) {
|
||||
clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
Section &resSection = _sections[".rsrc"];
|
||||
parseResourceLevel(resSection, resSection.offset, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PEResources::parseResourceLevel(Section §ion, uint32 offset, int level) {
|
||||
_exe->seek(offset + 12);
|
||||
|
||||
uint16 namedEntryCount = _exe->readUint16LE();
|
||||
uint16 intEntryCount = _exe->readUint16LE();
|
||||
|
||||
for (uint32 i = 0; i < (uint32)(namedEntryCount + intEntryCount); i++) {
|
||||
uint32 value = _exe->readUint32LE();
|
||||
|
||||
WinResourceID id;
|
||||
|
||||
if (value & 0x80000000) {
|
||||
value &= 0x7fffffff;
|
||||
|
||||
uint32 startPos = _exe->pos();
|
||||
_exe->seek(section.offset + (value & 0x7fffffff));
|
||||
|
||||
// Read in the name, truncating from unicode to ascii
|
||||
String name;
|
||||
uint16 nameLength = _exe->readUint16LE();
|
||||
while (nameLength--)
|
||||
name += (char)(_exe->readUint16LE() & 0xff);
|
||||
|
||||
_exe->seek(startPos);
|
||||
|
||||
id = name;
|
||||
} else {
|
||||
id = value;
|
||||
}
|
||||
|
||||
uint32 nextOffset = _exe->readUint32LE();
|
||||
uint32 lastOffset = _exe->pos();
|
||||
|
||||
if (level == 0)
|
||||
_curType = id;
|
||||
else if (level == 1)
|
||||
_curID = id;
|
||||
else if (level == 2)
|
||||
_curLang = id;
|
||||
|
||||
if (level < 2) {
|
||||
// Time to dive down further
|
||||
parseResourceLevel(section, section.offset + (nextOffset & 0x7fffffff), level + 1);
|
||||
} else {
|
||||
_exe->seek(section.offset + nextOffset);
|
||||
|
||||
Resource resource;
|
||||
resource.offset = _exe->readUint32LE() + section.offset - section.virtualAddress;
|
||||
resource.size = _exe->readUint32LE();
|
||||
|
||||
debug(4, "Found resource '%s' '%s' '%s' at %d of size %d", _curType.toString().c_str(),
|
||||
_curID.toString().c_str(), _curLang.toString().c_str(), resource.offset, resource.size);
|
||||
|
||||
_resources[_curType][_curID][_curLang] = resource;
|
||||
}
|
||||
|
||||
_exe->seek(lastOffset);
|
||||
}
|
||||
}
|
||||
|
||||
const Array<WinResourceID> PEResources::getTypeList() const {
|
||||
Array<WinResourceID> array;
|
||||
|
||||
if (!_exe)
|
||||
return array;
|
||||
|
||||
for (const auto &resource : _resources)
|
||||
array.push_back(resource._key);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
const Array<WinResourceID> PEResources::getIDList(const WinResourceID &type) const {
|
||||
Array<WinResourceID> array;
|
||||
|
||||
if (!_exe || !_resources.contains(type))
|
||||
return array;
|
||||
|
||||
const IDMap &idMap = _resources[type];
|
||||
|
||||
for (const auto &id : idMap)
|
||||
array.push_back(id._key);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
const Array<WinResourceID> PEResources::getLangList(const WinResourceID &type, const WinResourceID &id) const {
|
||||
Array<WinResourceID> array;
|
||||
|
||||
if (!_exe || !_resources.contains(type))
|
||||
return array;
|
||||
|
||||
const IDMap &idMap = _resources[type];
|
||||
|
||||
if (!idMap.contains(id))
|
||||
return array;
|
||||
|
||||
const LangMap &langMap = idMap[id];
|
||||
|
||||
for (const auto &lang : langMap)
|
||||
array.push_back(lang._key);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
SeekableReadStream *PEResources::getResource(const WinResourceID &type, const WinResourceID &id) {
|
||||
Array<WinResourceID> langList = getLangList(type, id);
|
||||
|
||||
if (langList.empty())
|
||||
return nullptr;
|
||||
|
||||
const Resource &resource = _resources[type][id][langList[0]];
|
||||
_exe->seek(resource.offset);
|
||||
return _exe->readStream(resource.size);
|
||||
}
|
||||
|
||||
SeekableReadStream *PEResources::getResource(const WinResourceID &type, const WinResourceID &id, const WinResourceID &lang) {
|
||||
if (!_exe || !_resources.contains(type))
|
||||
return nullptr;
|
||||
|
||||
const IDMap &idMap = _resources[type];
|
||||
|
||||
if (!idMap.contains(id))
|
||||
return nullptr;
|
||||
|
||||
const LangMap &langMap = idMap[id];
|
||||
|
||||
if (!langMap.contains(lang))
|
||||
return nullptr;
|
||||
|
||||
const Resource &resource = langMap[lang];
|
||||
_exe->seek(resource.offset);
|
||||
return _exe->readStream(resource.size);
|
||||
}
|
||||
|
||||
String PEResources::loadString(uint32 stringID) {
|
||||
String string;
|
||||
SeekableReadStream *stream = getResource(kWinString, (stringID >> 4) + 1);
|
||||
|
||||
if (!stream)
|
||||
return string;
|
||||
|
||||
// Skip over strings we don't care about
|
||||
uint32 startString = stringID & ~0xF;
|
||||
|
||||
for (uint32 i = startString; i < stringID; i++)
|
||||
stream->skip(stream->readUint16LE() * 2);
|
||||
|
||||
// HACK: Truncate UTF-16 down to ASCII
|
||||
byte size = stream->readUint16LE();
|
||||
while (size--)
|
||||
string += (char)(stream->readUint16LE() & 0xFF);
|
||||
|
||||
delete stream;
|
||||
return string;
|
||||
}
|
||||
|
||||
WinResources::VersionInfo *PEResources::parseVersionInfo(SeekableReadStream *res) {
|
||||
VersionInfo *info = new VersionInfo;
|
||||
|
||||
while (res->pos() < res->size() && !res->eos()) {
|
||||
while (res->pos() % 4 && !res->eos()) // Pad to 4
|
||||
res->readByte();
|
||||
|
||||
/* uint16 len = */ res->readUint16LE();
|
||||
uint16 valLen = res->readUint16LE();
|
||||
uint16 type = res->readUint16LE();
|
||||
uint16 c;
|
||||
|
||||
if (res->eos())
|
||||
break;
|
||||
|
||||
Common::U32String key;
|
||||
while ((c = res->readUint16LE()) != 0 && !res->eos())
|
||||
key += c;
|
||||
|
||||
while (res->pos() % 4 && !res->eos()) // Pad to 4
|
||||
res->readByte();
|
||||
|
||||
if (res->eos())
|
||||
break;
|
||||
|
||||
if (type != 0) { // text
|
||||
Common::U32String value;
|
||||
for (int j = 0; j < valLen; j++) {
|
||||
uint16 ch = res->readUint16LE();
|
||||
if (ch)
|
||||
value += ch;
|
||||
}
|
||||
|
||||
info->hash.setVal(key.encode(), value);
|
||||
} else {
|
||||
if (key == "VS_VERSION_INFO") {
|
||||
if (!info->readVSVersionInfo(res))
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
114
common/formats/winexe_pe.h
Normal file
114
common/formats/winexe_pe.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COMMON_WINEXE_PE_H
|
||||
#define COMMON_WINEXE_PE_H
|
||||
|
||||
#include "common/hash-str.h"
|
||||
#include "common/hashmap.h"
|
||||
#include "common/str.h"
|
||||
#include "common/formats/winexe.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
/**
|
||||
* @defgroup common_winexe_ne Windows Portable Executable resources
|
||||
* @ingroup common_winexe
|
||||
*
|
||||
* @brief API for managing Windows Portable Executable resources.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
template<class T> class Array;
|
||||
class SeekableReadStream;
|
||||
|
||||
/**
|
||||
* A class able to load resources from a Windows Portable Executable, such
|
||||
* as cursors, bitmaps, and sounds.
|
||||
*/
|
||||
class PEResources : public WinResources {
|
||||
public:
|
||||
PEResources();
|
||||
~PEResources();
|
||||
|
||||
/** Clear all information. */
|
||||
void clear();
|
||||
|
||||
/** Load from an EXE file. */
|
||||
using WinResources::loadFromEXE;
|
||||
|
||||
/** Load from a stream. */
|
||||
bool loadFromEXE(SeekableReadStream *stream, DisposeAfterUse::Flag disposeFileHandle = DisposeAfterUse::YES);
|
||||
|
||||
/** Return a list of resource types. */
|
||||
const Array<WinResourceID> getTypeList() const;
|
||||
|
||||
/** Return a list of IDs for a given type. */
|
||||
const Array<WinResourceID> getIDList(const WinResourceID &type) const;
|
||||
|
||||
/** Return a list of languages for a given type and ID. */
|
||||
const Array<WinResourceID> getLangList(const WinResourceID &type, const WinResourceID &id) const;
|
||||
|
||||
/** Return a stream to the specified resource, taking the first language found (or 0 if non-existent). */
|
||||
SeekableReadStream *getResource(const WinResourceID &type, const WinResourceID &id);
|
||||
|
||||
/** Return a stream to the specified resource (or 0 if non-existent). */
|
||||
SeekableReadStream *getResource(const WinResourceID &type, const WinResourceID &id, const WinResourceID &lang);
|
||||
|
||||
/** Get a string from a string resource. */
|
||||
String loadString(uint32 stringID);
|
||||
|
||||
protected:
|
||||
VersionInfo *parseVersionInfo(SeekableReadStream *stream);
|
||||
|
||||
private:
|
||||
struct Section {
|
||||
uint32 virtualAddress;
|
||||
uint32 size;
|
||||
uint32 offset;
|
||||
};
|
||||
|
||||
HashMap<String, Section, IgnoreCase_Hash, IgnoreCase_EqualTo> _sections;
|
||||
|
||||
SeekableReadStream *_exe;
|
||||
DisposeAfterUse::Flag _disposeFileHandle;
|
||||
|
||||
void parseResourceLevel(Section §ion, uint32 offset, int level);
|
||||
WinResourceID _curType, _curID, _curLang;
|
||||
|
||||
struct Resource {
|
||||
uint32 offset;
|
||||
uint32 size;
|
||||
};
|
||||
|
||||
typedef HashMap<WinResourceID, Resource, WinResourceID_Hash, WinResourceID_EqualTo> LangMap;
|
||||
typedef HashMap<WinResourceID, LangMap, WinResourceID_Hash, WinResourceID_EqualTo> IDMap;
|
||||
typedef HashMap<WinResourceID, IDMap, WinResourceID_Hash, WinResourceID_EqualTo> TypeMap;
|
||||
|
||||
TypeMap _resources;
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
546
common/formats/xmlparser.cpp
Normal file
546
common/formats/xmlparser.cpp
Normal file
@@ -0,0 +1,546 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/formats/xmlparser.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/fs.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/system.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
XMLParser::~XMLParser() {
|
||||
while (!_activeKey.empty())
|
||||
freeNode(_activeKey.pop());
|
||||
|
||||
delete _XMLkeys;
|
||||
delete _stream;
|
||||
|
||||
for (auto *layout : _layoutList)
|
||||
delete layout;
|
||||
|
||||
_layoutList.clear();
|
||||
}
|
||||
|
||||
bool XMLParser::loadFile(const Path &filename) {
|
||||
_stream = SearchMan.createReadStreamForMember(filename);
|
||||
if (!_stream)
|
||||
return false;
|
||||
|
||||
_fileName = filename;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::loadFile(const FSNode &node) {
|
||||
_stream = node.createReadStream();
|
||||
if (!_stream)
|
||||
return false;
|
||||
|
||||
_fileName = node.getName();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::loadBuffer(const byte *buffer, uint32 size, DisposeAfterUse::Flag disposable) {
|
||||
_stream = new MemoryReadStream(buffer, size, disposable);
|
||||
_fileName = "Memory Stream";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::loadStream(SeekableReadStream *stream, const String &name) {
|
||||
_stream = stream;
|
||||
_fileName = name;
|
||||
return _stream != nullptr;
|
||||
}
|
||||
|
||||
void XMLParser::close() {
|
||||
delete _stream;
|
||||
_stream = nullptr;
|
||||
}
|
||||
|
||||
bool XMLParser::parserError(const String &errStr) {
|
||||
_state = kParserError;
|
||||
|
||||
const int startPosition = _stream->pos();
|
||||
int currentPosition = startPosition;
|
||||
int lineCount = 1;
|
||||
char c = 0;
|
||||
|
||||
_stream->seek(0, SEEK_SET);
|
||||
|
||||
while (currentPosition--) {
|
||||
c = _stream->readByte();
|
||||
|
||||
if (c == '\n' || c == '\r')
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
assert(_stream->pos() == startPosition);
|
||||
currentPosition = startPosition;
|
||||
|
||||
Common::String errorMessage = Common::String::format("\n File <%s>, line %d:\n", _fileName.toString().c_str(), lineCount);
|
||||
|
||||
if (startPosition > 1) {
|
||||
int keyOpening = 0;
|
||||
int keyClosing = 0;
|
||||
|
||||
while (currentPosition-- && keyOpening == 0) {
|
||||
_stream->seek(-2, SEEK_CUR);
|
||||
c = _stream->readByte();
|
||||
|
||||
if (c == '<')
|
||||
keyOpening = currentPosition - 1;
|
||||
else if (c == '>')
|
||||
keyClosing = currentPosition;
|
||||
}
|
||||
|
||||
_stream->seek(startPosition, SEEK_SET);
|
||||
currentPosition = startPosition;
|
||||
while (keyClosing == 0 && c && currentPosition++) {
|
||||
c = _stream->readByte();
|
||||
|
||||
if (c == '>')
|
||||
keyClosing = currentPosition;
|
||||
}
|
||||
|
||||
currentPosition = (keyClosing - keyOpening);
|
||||
_stream->seek(keyOpening, SEEK_SET);
|
||||
|
||||
while (currentPosition--)
|
||||
errorMessage += (char)_stream->readByte();
|
||||
}
|
||||
|
||||
errorMessage += "\n\nParser error: ";
|
||||
errorMessage += errStr;
|
||||
errorMessage += "\n\n";
|
||||
|
||||
g_system->logMessage(LogMessageType::kError, errorMessage.c_str());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool XMLParser::parseXMLHeader(ParserNode *node) {
|
||||
assert(node->header);
|
||||
|
||||
if (_activeKey.size() != 1)
|
||||
return parserError("XML Header is expected in the global scope.");
|
||||
|
||||
if (!node->values.contains("version"))
|
||||
return parserError("Missing XML version in XML header.");
|
||||
|
||||
if (node->values["version"] != "1.0")
|
||||
return parserError("Unsupported XML version.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::parseActiveKey(bool closed) {
|
||||
bool ignore = false;
|
||||
assert(_activeKey.empty() == false);
|
||||
|
||||
ParserNode *key = _activeKey.top();
|
||||
|
||||
if (key->name == "xml" && key->header == true) {
|
||||
assert(closed);
|
||||
return parseXMLHeader(key) && closeKey();
|
||||
}
|
||||
|
||||
XMLKeyLayout *layout = (_activeKey.size() == 1) ? _XMLkeys : getParentNode(key)->layout;
|
||||
|
||||
if (layout->children.contains(key->name)) {
|
||||
key->layout = layout->children[key->name];
|
||||
|
||||
StringMap localMap = key->values;
|
||||
int keyCount = localMap.size();
|
||||
|
||||
for (const auto &prop : key->layout->properties) {
|
||||
if (prop.required && !localMap.contains(prop.name))
|
||||
return parserError("Missing required property '" + prop.name + "' inside key '" + key->name + "'");
|
||||
else if (localMap.contains(prop.name)) {
|
||||
keyCount--;
|
||||
localMap.erase(prop.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (keyCount > 0) {
|
||||
Common::String missingKeys;
|
||||
|
||||
for (auto i = localMap.begin(); i != localMap.end(); ++i)
|
||||
missingKeys += i->_key + ' ';
|
||||
|
||||
return parserError(Common::String::format("Unhandled property inside key '%s' (%s, %d items).", key->name.c_str(), missingKeys.c_str(), keyCount));
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!handleUnknownKey(key))
|
||||
return parserError("Unexpected key in the active scope ('" + key->name + "').");
|
||||
ignore = true;
|
||||
}
|
||||
|
||||
// check if any of the parents must be ignored.
|
||||
// if a parent is ignored, all children are too.
|
||||
for (int i = _activeKey.size() - 1; i >= 0; --i) {
|
||||
if (_activeKey[i]->ignore)
|
||||
ignore = true;
|
||||
}
|
||||
|
||||
if (ignore == false && keyCallback(key) == false) {
|
||||
// HACK: People may be stupid and overlook the fact that
|
||||
// when keyCallback() fails, a parserError() must be set.
|
||||
// We set it manually in that case.
|
||||
if (_state != kParserError)
|
||||
parserError("Unhandled exception when parsing '" + key->name + "' key.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (closed)
|
||||
return closeKey();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::parseKeyValue(String keyName) {
|
||||
assert(_activeKey.empty() == false);
|
||||
|
||||
if (_activeKey.top()->values.contains(keyName))
|
||||
return false;
|
||||
|
||||
_token.clear();
|
||||
char stringStart;
|
||||
|
||||
if (_char == '"' || _char == '\'') {
|
||||
stringStart = _char;
|
||||
_char = _stream->readByte();
|
||||
|
||||
while (_char && _char != stringStart) {
|
||||
_token += _char;
|
||||
_char = _stream->readByte();
|
||||
}
|
||||
|
||||
if (_char == 0)
|
||||
return false;
|
||||
|
||||
_char = _stream->readByte();
|
||||
|
||||
} else if (!parseToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_activeKey.top()->values[keyName] = _token;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::parseIntegerKey(const char *key, int count, ...) {
|
||||
bool result;
|
||||
va_list args;
|
||||
va_start(args, count);
|
||||
result = vparseIntegerKey(key, count, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool XMLParser::parseIntegerKey(const String &key, int count, ...) {
|
||||
bool result;
|
||||
va_list args;
|
||||
va_start(args, count);
|
||||
result = vparseIntegerKey(key.c_str(), count, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool XMLParser::vparseIntegerKey(const char *key, int count, va_list args) {
|
||||
char *parseEnd;
|
||||
int *num_ptr;
|
||||
|
||||
while (count--) {
|
||||
while (isSpace(*key))
|
||||
key++;
|
||||
|
||||
num_ptr = va_arg(args, int*);
|
||||
*num_ptr = strtol(key, &parseEnd, 10);
|
||||
|
||||
key = parseEnd;
|
||||
|
||||
while (isSpace(*key))
|
||||
key++;
|
||||
|
||||
if (count && *key++ != ',')
|
||||
return false;
|
||||
}
|
||||
|
||||
return (*key == 0);
|
||||
}
|
||||
|
||||
bool XMLParser::closeKey() {
|
||||
bool ignore = false;
|
||||
bool result = true;
|
||||
|
||||
for (int i = _activeKey.size() - 1; i >= 0; --i) {
|
||||
if (_activeKey[i]->ignore)
|
||||
ignore = true;
|
||||
}
|
||||
|
||||
if (ignore == false)
|
||||
result = closedKeyCallback(_activeKey.top());
|
||||
|
||||
freeNode(_activeKey.pop());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool XMLParser::parse() {
|
||||
if (_stream == nullptr)
|
||||
return false;
|
||||
|
||||
// Make sure we are at the start of the stream.
|
||||
_stream->seek(0, SEEK_SET);
|
||||
|
||||
if (_XMLkeys == nullptr)
|
||||
buildLayout();
|
||||
|
||||
while (!_activeKey.empty())
|
||||
freeNode(_activeKey.pop());
|
||||
|
||||
cleanup();
|
||||
|
||||
bool activeClosure = false;
|
||||
bool activeHeader = false;
|
||||
bool selfClosure;
|
||||
|
||||
_state = kParserNeedHeader;
|
||||
_activeKey.clear();
|
||||
|
||||
_char = _stream->readByte();
|
||||
|
||||
while (_char && _state != kParserError) {
|
||||
if (skipSpaces())
|
||||
continue;
|
||||
|
||||
if (skipComments())
|
||||
continue;
|
||||
|
||||
switch (_state) {
|
||||
case kParserNeedHeader:
|
||||
case kParserNeedKey:
|
||||
if (_char != '<') {
|
||||
if (_allowText) {
|
||||
Common::String text;
|
||||
do {
|
||||
text += _char;
|
||||
_char = _stream->readByte();
|
||||
} while (_char != '<' && _char);
|
||||
if (!_char) {
|
||||
parserError("Unexpected end of file.");
|
||||
break;
|
||||
}
|
||||
if (!textCallback(text)) {
|
||||
parserError("Failed to process text segment.");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
parserError("Parser expecting key start.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((_char = _stream->readByte()) == 0) {
|
||||
parserError("Unexpected end of file.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (_state == kParserNeedHeader) {
|
||||
if (_char != '?') {
|
||||
parserError("Expecting XML header.");
|
||||
break;
|
||||
}
|
||||
|
||||
_char = _stream->readByte();
|
||||
activeHeader = true;
|
||||
} else if (_char == '/') {
|
||||
_char = _stream->readByte();
|
||||
activeClosure = true;
|
||||
} else if (_char == '?') {
|
||||
parserError("Unexpected header. There may only be one XML header per file.");
|
||||
break;
|
||||
}
|
||||
|
||||
_state = kParserNeedKeyName;
|
||||
break;
|
||||
|
||||
case kParserNeedKeyName:
|
||||
if (!parseToken()) {
|
||||
parserError("Invalid key name.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (activeClosure) {
|
||||
if (_activeKey.empty() || _token != _activeKey.top()->name) {
|
||||
parserError("Unexpected closure.");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ParserNode *node = allocNode(); // new ParserNode;
|
||||
node->name = _token;
|
||||
node->ignore = false;
|
||||
node->header = activeHeader;
|
||||
node->depth = _activeKey.size();
|
||||
node->layout = nullptr;
|
||||
_activeKey.push(node);
|
||||
}
|
||||
|
||||
_state = kParserNeedPropertyName;
|
||||
break;
|
||||
|
||||
case kParserNeedPropertyName:
|
||||
if (activeClosure) {
|
||||
if (!closeKey()) {
|
||||
parserError("Missing data when closing key '" + _activeKey.top()->name + "'.");
|
||||
break;
|
||||
}
|
||||
|
||||
activeClosure = false;
|
||||
|
||||
if (_char != '>')
|
||||
parserError("Invalid syntax in key closure.");
|
||||
else
|
||||
_state = kParserNeedKey;
|
||||
|
||||
_char = _stream->readByte();
|
||||
break;
|
||||
}
|
||||
|
||||
selfClosure = false;
|
||||
|
||||
if (_char == '/' || (_char == '?' && activeHeader)) {
|
||||
selfClosure = true;
|
||||
_char = _stream->readByte();
|
||||
}
|
||||
|
||||
if (_char == '>') {
|
||||
if (activeHeader && !selfClosure) {
|
||||
parserError("XML Header must be self-closed.");
|
||||
} else if (parseActiveKey(selfClosure)) {
|
||||
_char = _stream->readByte();
|
||||
_state = kParserNeedKey;
|
||||
}
|
||||
|
||||
activeHeader = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (selfClosure)
|
||||
parserError("Expecting key closure after '/' symbol.");
|
||||
else if (!parseToken())
|
||||
parserError("Error when parsing key value.");
|
||||
else
|
||||
_state = kParserNeedPropertyOperator;
|
||||
|
||||
break;
|
||||
|
||||
case kParserNeedPropertyOperator:
|
||||
if (_char != '=')
|
||||
parserError("Syntax error after key name.");
|
||||
else
|
||||
_state = kParserNeedPropertyValue;
|
||||
|
||||
_char = _stream->readByte();
|
||||
break;
|
||||
|
||||
case kParserNeedPropertyValue:
|
||||
if (!parseKeyValue(_token))
|
||||
parserError("Invalid key value.");
|
||||
else
|
||||
_state = kParserNeedPropertyName;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == kParserError)
|
||||
return false;
|
||||
|
||||
if (_state != kParserNeedKey || !_activeKey.empty())
|
||||
return parserError("Unexpected end of file.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::skipSpaces() {
|
||||
if (!isSpace(_char))
|
||||
return false;
|
||||
|
||||
while (_char && isSpace(_char))
|
||||
_char = _stream->readByte();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool XMLParser::skipComments() {
|
||||
if (_char == '<') {
|
||||
_char = _stream->readByte();
|
||||
|
||||
if (_char != '!') {
|
||||
_stream->seek(-1, SEEK_CUR);
|
||||
_char = '<';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_stream->readByte() != '-' || _stream->readByte() != '-')
|
||||
return parserError("Malformed comment syntax.");
|
||||
|
||||
_char = _stream->readByte();
|
||||
|
||||
while (_char) {
|
||||
if (_char == '-') {
|
||||
if (_stream->readByte() == '-') {
|
||||
|
||||
if (_stream->readByte() != '>')
|
||||
return parserError("Malformed comment (double-hyphen inside comment body).");
|
||||
|
||||
_char = _stream->readByte();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_char = _stream->readByte();
|
||||
}
|
||||
|
||||
return parserError("Comment has no closure.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool XMLParser::parseToken() {
|
||||
_token.clear();
|
||||
|
||||
while (isValidNameChar(_char)) {
|
||||
_token += _char;
|
||||
_char = _stream->readByte();
|
||||
}
|
||||
|
||||
return isSpace(_char) != 0 || _char == '>' || _char == '=' || _char == '/';
|
||||
}
|
||||
|
||||
} // End of namespace Common
|
||||
394
common/formats/xmlparser.h
Normal file
394
common/formats/xmlparser.h
Normal file
@@ -0,0 +1,394 @@
|
||||
/* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef XML_PARSER_H
|
||||
#define XML_PARSER_H
|
||||
|
||||
#include "common/scummsys.h"
|
||||
#include "common/types.h"
|
||||
|
||||
#include "common/fs.h"
|
||||
#include "common/list.h"
|
||||
#include "common/hashmap.h"
|
||||
#include "common/hash-str.h"
|
||||
#include "common/stack.h"
|
||||
#include "common/memorypool.h"
|
||||
|
||||
|
||||
namespace Common {
|
||||
|
||||
/**
|
||||
* @defgroup common_xmlparser XML parser
|
||||
* @ingroup common
|
||||
*
|
||||
* @brief The XML parser allows for parsing XML-like files.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
class SeekableReadStream;
|
||||
|
||||
#define MAX_XML_DEPTH 8
|
||||
|
||||
#define XML_KEY(keyName) {\
|
||||
lay = new CustomXMLKeyLayout;\
|
||||
lay->callback = (&kLocalParserName::parserCallback_##keyName);\
|
||||
layout.top()->children[#keyName] = lay;\
|
||||
layout.push(lay); \
|
||||
_layoutList.push_back(lay);
|
||||
|
||||
#define XML_KEY_RECURSIVE(keyName) {\
|
||||
layout.top()->children[#keyName] = layout.top();\
|
||||
layout.push(layout.top());\
|
||||
}
|
||||
|
||||
#define KEY_END() layout.pop(); }
|
||||
|
||||
#define XML_PROP(propName, req) {\
|
||||
prop.name = #propName; \
|
||||
prop.required = req; \
|
||||
layout.top()->properties.push_back(prop); }
|
||||
|
||||
|
||||
|
||||
#define CUSTOM_XML_PARSER(parserName) \
|
||||
protected: \
|
||||
typedef parserName kLocalParserName; \
|
||||
bool keyCallback(ParserNode *node) override {return node->layout->doCallback(this, node); }\
|
||||
struct CustomXMLKeyLayout : public XMLKeyLayout {\
|
||||
typedef bool (parserName::*ParserCallback)(ParserNode *node);\
|
||||
ParserCallback callback;\
|
||||
bool doCallback(XMLParser *parent, ParserNode *node) {return ((kLocalParserName *)parent->*callback)(node);} };\
|
||||
void buildLayout() override { \
|
||||
Common::Stack<XMLKeyLayout *> layout; \
|
||||
CustomXMLKeyLayout *lay = 0; \
|
||||
XMLKeyLayout::XMLKeyProperty prop; \
|
||||
_XMLkeys = new CustomXMLKeyLayout; \
|
||||
layout.push(_XMLkeys);
|
||||
|
||||
#define PARSER_END() layout.clear(); }
|
||||
|
||||
/**
|
||||
* The base XMLParser class implements generic functionality for parsing
|
||||
* XML-like files.
|
||||
*
|
||||
* In order to use it, it must be inherited with a child class that implements
|
||||
* the XMLParser::keyCallback() function.
|
||||
*
|
||||
* @see XMLParser::keyCallback()
|
||||
*/
|
||||
class XMLParser {
|
||||
public:
|
||||
/**
|
||||
* Parser constructor.
|
||||
*/
|
||||
XMLParser() : _XMLkeys(nullptr), _stream(nullptr), _allowText(false), _char(0) {}
|
||||
|
||||
virtual ~XMLParser();
|
||||
|
||||
/** Active state for the parser */
|
||||
enum ParserState {
|
||||
kParserNeedHeader,
|
||||
kParserNeedKey,
|
||||
kParserNeedKeyName,
|
||||
|
||||
kParserNeedPropertyName,
|
||||
kParserNeedPropertyOperator,
|
||||
kParserNeedPropertyValue,
|
||||
|
||||
kParserError
|
||||
};
|
||||
|
||||
struct XMLKeyLayout;
|
||||
struct ParserNode;
|
||||
|
||||
typedef HashMap<String, XMLParser::XMLKeyLayout*, IgnoreCase_Hash, IgnoreCase_EqualTo> ChildMap;
|
||||
|
||||
/** nested struct representing the layout of the XML file */
|
||||
struct XMLKeyLayout {
|
||||
struct XMLKeyProperty {
|
||||
String name;
|
||||
bool required;
|
||||
};
|
||||
|
||||
List<XMLKeyProperty> properties;
|
||||
ChildMap children;
|
||||
|
||||
virtual bool doCallback(XMLParser *parent, ParserNode *node) = 0;
|
||||
|
||||
virtual ~XMLKeyLayout() {
|
||||
properties.clear();
|
||||
}
|
||||
};
|
||||
|
||||
XMLKeyLayout *_XMLkeys;
|
||||
|
||||
/** Struct representing a parsed node */
|
||||
struct ParserNode {
|
||||
String name;
|
||||
StringMap values;
|
||||
bool ignore;
|
||||
bool header;
|
||||
int depth;
|
||||
XMLKeyLayout *layout;
|
||||
};
|
||||
|
||||
ObjectPool<ParserNode, MAX_XML_DEPTH> _nodePool;
|
||||
|
||||
ParserNode *allocNode() {
|
||||
return new (_nodePool) ParserNode;
|
||||
}
|
||||
|
||||
void freeNode(ParserNode *node) {
|
||||
_nodePool.deleteChunk(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file into the parser.
|
||||
* Used for the loading of Theme Description files
|
||||
* straight from the filesystem.
|
||||
*
|
||||
* @param filename Name of the file to load.
|
||||
*/
|
||||
bool loadFile(const Path &filename);
|
||||
|
||||
bool loadFile(const FSNode &node);
|
||||
|
||||
/**
|
||||
* Loads a memory buffer into the parser.
|
||||
* Used for loading the default theme fallback directly
|
||||
* from memory if no themes can be found.
|
||||
*
|
||||
* @param buffer Pointer to the buffer.
|
||||
* @param size Size of the buffer
|
||||
* @param disposable Sets if the XMLParser owns the buffer,
|
||||
* i.e. if it can be freed safely after it's
|
||||
* no longer needed by the parser.
|
||||
*/
|
||||
bool loadBuffer(const byte *buffer, uint32 size, DisposeAfterUse::Flag disposable = DisposeAfterUse::NO);
|
||||
|
||||
bool loadStream(SeekableReadStream *stream, const String &name = "File Stream");
|
||||
|
||||
void close();
|
||||
|
||||
/**
|
||||
* The actual parsing function.
|
||||
* Parses the loaded data stream, returns true if successful.
|
||||
*/
|
||||
bool parse();
|
||||
|
||||
/**
|
||||
* Returns the active node being parsed (the one on top of
|
||||
* the node stack).
|
||||
*/
|
||||
ParserNode *getActiveNode() {
|
||||
if (!_activeKey.empty())
|
||||
return _activeKey.top();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent of a given node in the stack.
|
||||
*/
|
||||
ParserNode *getParentNode(ParserNode *child) {
|
||||
return child->depth > 0 ? _activeKey[child->depth - 1] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow text nodes (eg <tag>this is a text node</tag>) to appear in the
|
||||
* document.
|
||||
*
|
||||
* By default this parser does not allow text nodes and expects all data
|
||||
* to appear in attributes.
|
||||
*/
|
||||
void setAllowText() {
|
||||
_allowText = true;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
/**
|
||||
* The buildLayout function builds the layout for the parser to use
|
||||
* based on a series of helper macros. This function is automatically
|
||||
* generated by the CUSTOM_XML_PARSER() macro on custom parsers.
|
||||
*
|
||||
* See the documentation regarding XML layouts.
|
||||
*/
|
||||
virtual void buildLayout() = 0;
|
||||
|
||||
/**
|
||||
* The keycallback function is automatically overloaded on custom parsers
|
||||
* when using the CUSTOM_XML_PARSER() macro.
|
||||
*
|
||||
* Its job is to call the corresponding Callback function for the given node.
|
||||
* A function for each key type must be declared separately. See the custom
|
||||
* parser creation instructions.
|
||||
*
|
||||
* When parsing a key in such function, one may chose to skip it, e.g. because it's not needed
|
||||
* on the current configuration. In order to ignore a key, you must set
|
||||
* the "ignore" field of its KeyNode struct to "true": The key and all its children
|
||||
* will then be automatically ignored by the parser.
|
||||
*
|
||||
* The callback function must return true if the key was properly handled (this includes the case when the
|
||||
* key is being ignored). False otherwise. The return of keyCallback() is the same as
|
||||
* the callback function's.
|
||||
* See the sample implementation in GUI::ThemeParser.
|
||||
*/
|
||||
virtual bool keyCallback(ParserNode *node) = 0;
|
||||
|
||||
/**
|
||||
* The closed key callback function MAY be overloaded by inheriting classes to
|
||||
* implement parser-specific functions.
|
||||
*
|
||||
* The closedKeyCallback is issued once a key has been finished parsing, to let
|
||||
* the parser verify that all the required subkeys, etc, were included.
|
||||
*
|
||||
* Unlike the keyCallbacks(), there's just a closedKeyCallback() for all keys.
|
||||
* Use "node->name" to distinguish between each key type.
|
||||
*
|
||||
* Returns true if the key was properly closed, false otherwise.
|
||||
* By default, all keys are properly closed.
|
||||
*/
|
||||
virtual bool closedKeyCallback(ParserNode *node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a text node is found. This will only be called if
|
||||
* setAllowText() has been called, otherwise text nodes are considered
|
||||
* parse errors.
|
||||
*/
|
||||
virtual bool textCallback(const String &val) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a node is closed. Manages its cleanup and calls the
|
||||
* closing callback function if needed.
|
||||
*/
|
||||
bool closeKey();
|
||||
|
||||
/**
|
||||
* Parses the value of a given key. There's no reason to overload this.
|
||||
*/
|
||||
bool parseKeyValue(String keyName);
|
||||
|
||||
/**
|
||||
* Called once a key has been parsed. It handles the closing/cleanup of the
|
||||
* node stack and calls the keyCallback.
|
||||
*/
|
||||
bool parseActiveKey(bool closed);
|
||||
|
||||
/**
|
||||
* Prints an error message when parsing fails and stops the parser.
|
||||
* Parser error always returns "false" so we can pass the return value
|
||||
* directly and break down the parsing.
|
||||
*/
|
||||
bool parserError(const String &errStr);
|
||||
|
||||
/**
|
||||
* Skips spaces/whitelines etc.
|
||||
* @return true if any spaces were skipped.
|
||||
*/
|
||||
bool skipSpaces();
|
||||
|
||||
/**
|
||||
* Skips comment blocks and comment lines.
|
||||
* @return true if any comments were skipped.
|
||||
*/
|
||||
bool skipComments();
|
||||
|
||||
/**
|
||||
* Check if a given character can be part of a KEY or VALUE name.
|
||||
* Overload this if you want to support keys with strange characters
|
||||
* in their name.
|
||||
*/
|
||||
virtual inline bool isValidNameChar(char c) {
|
||||
return isAlnum(c) || c == '_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a the first textual token found.
|
||||
*/
|
||||
bool parseToken();
|
||||
|
||||
/**
|
||||
* Parses the values inside an integer key.
|
||||
* The count parameter specifies the number of values inside
|
||||
* the key, which are expected to be separated with commas.
|
||||
*
|
||||
* Sample usage:
|
||||
* parseIntegerKey("255, 255, 255", 3, &red, &green, &blue);
|
||||
* [will parse each field into its own integer]
|
||||
*
|
||||
* parseIntegerKey("1234", 1, &number);
|
||||
* [will parse the single number into the variable]
|
||||
*
|
||||
* @param key String containing the integers to be parsed.
|
||||
* @param count Number of comma-separated ints in the string.
|
||||
* @param ... Integer variables to store the parsed ints, passed
|
||||
* by reference.
|
||||
* @returns True if the parsing succeeded.
|
||||
*/
|
||||
bool parseIntegerKey(const char *key, int count, ...);
|
||||
bool parseIntegerKey(const String &keyStr, int count, ...);
|
||||
bool vparseIntegerKey(const char *key, int count, va_list args);
|
||||
|
||||
bool parseXMLHeader(ParserNode *node);
|
||||
|
||||
/**
|
||||
* Overload if your parser needs to support parsing the same file
|
||||
* several times, so you can clean up the internal state of the
|
||||
* parser before each parse.
|
||||
*/
|
||||
virtual void cleanup() {}
|
||||
|
||||
/**
|
||||
* Overload if your parser wants to be notified of keys which haven't
|
||||
* been explicitly declared.
|
||||
*
|
||||
* The functions should return true if the key was handled and parsing should
|
||||
* continue, or false (default) to raise a parsing error.
|
||||
*/
|
||||
virtual bool handleUnknownKey(ParserNode *node) { return false; }
|
||||
|
||||
List<XMLKeyLayout *> _layoutList;
|
||||
|
||||
private:
|
||||
char _char;
|
||||
bool _allowText; /** Allow text nodes in the doc (default false) */
|
||||
SeekableReadStream *_stream;
|
||||
Path _fileName;
|
||||
|
||||
ParserState _state; /** Internal state of the parser */
|
||||
|
||||
String _error; /** Current error message */
|
||||
String _token; /** Current text token */
|
||||
|
||||
Stack<ParserNode *> _activeKey; /** Node stack of the parsed keys */
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
} // End of namespace Common
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user