Skip to content

Commit

Permalink
Read file directory index, if it exists
Browse files Browse the repository at this point in the history
  • Loading branch information
chances committed Jul 21, 2024
1 parent 5095e81 commit 05c0dc3
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 14 deletions.
80 changes: 66 additions & 14 deletions src/dbpf/package.d
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ struct Version {
static const float sims3 = 2;
/// SimCity (2013)
static const float simCity = 3;

///
static const Version simCity4Index = Version(7, 0);
///
static const Version sc4Index = Version(7, 0);
align(1):
///
uint major;
Expand Down Expand Up @@ -174,15 +179,29 @@ align(1):
uint size;
}

static assert(Entry.alignof == 1);
static assert(Entry.sizeof == 20);
static assert(ResourceEntry.alignof == 1);
static assert(ResourceEntry.sizeof == 24);

///
alias TableEntry = SumType!(Entry, ResourceEntry);
///
alias Table = TableEntry[];

static assert(Entry.alignof == 1);
static assert(Entry.sizeof == 20);
static assert(ResourceEntry.alignof == 1);
static assert(ResourceEntry.sizeof == 24);
///
alias EntryFilter = bool function(TableEntry entry);
import std.conv : asOriginalType;
///
bool entryOf(alias T)(TableEntry entry) if (is(typeof(T) == Tgi) || is(typeof(T.asOriginalType) == Tgi)) {
import std.sumtype : match;

const Tgi tgi = T.asOriginalType;
return entry.match!(
(Entry e) => e.tgi,
(ResourceEntry e) => e.tgi,
) == tgi;
}

/// A Hole Table contains the location and size of all holes in a DBPF file.
/// Remarks:
Expand All @@ -195,7 +214,7 @@ struct HoleTable {
uint size;
}

/// Occurs before `File.contents` only if the `File` is compressed.
/// Occurs before `File.contents`, if and only if the `File` is compressed.
struct FileHeader {
import dbpf.types : int24;
align(1):
Expand All @@ -206,13 +225,14 @@ align(1):
/// See_Also: <a href="https://www.wiki.sc4devotion.com/index.php?title=DBPF_Compression">DBPF Compression</a> (SC4D Encyclopedia)
const ushort compressionId = 0x10FB;
/// Uncompressed size of the file, in bytes.
// FIXME: in Big-Endian format (hi-byte to lo-byte)
int24 uncompressedSize;
}

static assert(FileHeader.alignof == 1);
static assert(FileHeader.sizeof == 9);

import std.typecons : Flag;
import std.typecons : Flag, No, Yes;

/// Files fill the bulk of a DBPF archive.
///
Expand All @@ -221,7 +241,7 @@ import std.typecons : Flag;
/// Each file is either uncompressed or compressed. To check if a file is compressed you first need to read the DIR
/// file, if it exists. If no <a href="https://www.wiki.sc4devotion.com/index.php?title=DIR">DIR</a> entry exists,
/// then no files within the package are compressed.
struct File(bool Compressed = Flag!"compressed" = false) {
struct File(Flag!"compressed" Compressed = false) {
alias contents this;
/// Exists only if this file is compressed.
static if (Compressed) FileHeader header;
Expand All @@ -240,6 +260,11 @@ struct File(bool Compressed = Flag!"compressed" = false) {
}
}

/// ditto
alias UncompressedFile = File!(No.compressed);
/// ditto
alias CompressedFile = File!(Yes.compressed);

/// Thrown when an `Archive` is invalid or corrupt.
class ArchiveException : Exception {
import std.exception : basicExceptionCtors;
Expand Down Expand Up @@ -269,15 +294,20 @@ struct Archive(float DBPF) if (isValidDbpfVersion!DBPF) {
Head metadata;
///
Table entries;
/// If no <a href="https://www.wiki.sc4devotion.com/index.php?title=DIR">DIR</a> entry exists,
/// then no files within the package are compressed.
const UncompressedFile* directory;

/// Open a DBPF archive from the given file `path`.
/// Throws: `FileException` when the archive is not found, or there is some I/O error.
/// Throws: `ArchiveException` when the archive is invalid or corrupt.
this(string path) {
import std.algorithm : equal, map;
import dbpf.tgi : KnownInstance;
import std.algorithm : find, equal, map;
import std.array : array;
import std.conv : text, to;
import std.file : exists, FileException;
import std.range : empty;
import std.string : format;

if (!path.exists) throw new FileException(path, "File does not exist: " ~ path);
Expand All @@ -293,27 +323,49 @@ struct Archive(float DBPF) if (isValidDbpfVersion!DBPF) {
version_.to!float == DBPF,
"Mismatched DBPF version. Expected " ~ DBPF.text ~ ", but saw " ~ version_
);
// Ensure index version matches expectation
enforce(metadata.indexVersion.major == 7, "Archive's index is of an unrecognized version.");

auto filesOffset = this.file.tell;
this.file.seek(metadata.indexOffset);
// FIXME: Use specific type, i.e. Entry or ResourceEntry
auto entries = new Entry[metadata.indexEntryCount];
this.file.rawRead!Entry((entries.ptr)[0..entries.length]);
this.entries = entries.map!((x) {

TableEntry toAbstractEntry(T)(T x) if (is(T : Entry) || is(T : ResourceEntry)) {
TableEntry entry = x;
return entry;
}).array;
}

// Read index of file entries
auto indexFileDirectory(T)() {
auto entries = new T[metadata.indexEntryCount];
this.file.rawRead!T((entries.ptr)[0..entries.length]);
this.entries = entries.map!(toAbstractEntry!T).array;

// Parse index of compressed files, if it exists
const dirEntries = entries.find!(entry => entry.tgi == KnownInstance.dir);
// TODO: Parse the file
if (!dirEntries.empty) return new UncompressedFile(this.read!T(dirEntries[0]));
else return null;
}

if (metadata.indexVersion.minor == 0) this.directory = indexFileDirectory!Entry();
else if (metadata.indexVersion.minor == 1) this.directory = indexFileDirectory!ResourceEntry();

this.file.seek(filesOffset);
}
~this() {
file.close();
if (file.isOpen) file.close();
}

///
void close() {
file.close();
}

ubyte[] read(T)(T entry) if (is(T : Entry) || is(T : ResourceEntry)) {
if (!file.isOpen) file = std.stdio.File(path, "rb");
this.file.seek(entry.offset);
return this.file.rawRead(new ubyte[entry.size]);
}
}

///
Expand Down
4 changes: 4 additions & 0 deletions src/dbpf/tgi.d
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ align(1):
/// A null reference, e.g. indicating that a cohort is a root node when the cohort's parent is equal to this value.
/// See_Also: `KnownInstance.null_`
static Tgi null_ = KnownInstance.null_;

bool opEquals()(auto ref const Tgi s) const {
return typeId == s.typeId && groupId == s.groupId && instanceId == s.instanceId;
}
}

static assert(Tgi.alignof == 1);
Expand Down
43 changes: 43 additions & 0 deletions src/files/dir.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module dbpf.files.dir;

import dbpf : Version;

import std.typecons : Flag, No, Yes;

///
struct Entry(Flag!"isResource" isResource) {
import dbpf.tgi : Tgi;
align(1):
///
Tgi tgi;
/// Resource ID.
static if (isResource) uint resourceId;
/// Size of an uncompressed file, in bytes.
uint size;
}

static assert(Entry!(No.isResource).alignof == 1);
static assert(Entry!(No.isResource).sizeof == 16);
static assert(Entry!(Yes.isResource).alignof == 1);
static assert(Entry!(Yes.isResource).sizeof == 20);

///
struct Directory(Version indexVersion) {
import dbpf : UncompressedFile;

static if (indexVersion == Version.sc4Index) alias DirectoryEntry = Entry!(No.isResource);
static if (indexVersion == Version.sc4Index) alias DirectoryEntry = Entry!(Yes.isResource);

///
DirectoryEntry[] entries;

///
this(ref UncompressedFile file) {
import std.algorithm : copy;
import std.conv : castFrom;

this.entries = new DirectoryEntry[file.size / DirectoryEntry.sizeof];
auto entriesBytes = castFrom!(DirectoryEntry*).to!(ubyte*)(entries.ptr);
assert(file.contents.copy(entriesBytes[0..(entries.length * DirectoryEntry.sizeof)]).length == 0, "");
}
}

0 comments on commit 05c0dc3

Please sign in to comment.