Partially implement warc parsing
This commit is contained in:
parent
b459c60d5b
commit
ae1b6583ca
|
@ -33,7 +33,8 @@ void main(string[] args)
|
||||||
string arcPath;
|
string arcPath;
|
||||||
string mountpoint;
|
string mountpoint;
|
||||||
|
|
||||||
if (args.length < 4 || !(arcPath = args[2]).isValidPath || !(mountpoint = args[3]).isValidPath)
|
if (args.length < 4 || !(arcPath = args[2]).isValidPath
|
||||||
|
|| !(mountpoint = args[3]).isValidPath)
|
||||||
{
|
{
|
||||||
goto case "help";
|
goto case "help";
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@ module cm3d2.arc;
|
||||||
|
|
||||||
import std.file;
|
import std.file;
|
||||||
import std.mmfile;
|
import std.mmfile;
|
||||||
|
import std.conv;
|
||||||
|
import std.stdio;
|
||||||
|
import std.zlib;
|
||||||
|
|
||||||
import dfuse.fuse;
|
import dfuse.fuse;
|
||||||
|
|
||||||
|
@ -9,31 +12,282 @@ import cm3d2;
|
||||||
|
|
||||||
class WarcFile
|
class WarcFile
|
||||||
{
|
{
|
||||||
private MmFile _file;
|
class DirectoryHashNode
|
||||||
|
{
|
||||||
|
private ulong _id;
|
||||||
|
public FileHashEntry[] subdirectoryEntries;
|
||||||
|
public FileHashEntry[] fileEntries;
|
||||||
|
private uint _depth;
|
||||||
|
private ulong[] _parents;
|
||||||
|
|
||||||
this(MmFile file)
|
public DirectoryHashNode[] subdirectories;
|
||||||
|
|
||||||
|
@property
|
||||||
|
ulong id()
|
||||||
|
{
|
||||||
|
return _id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(ubyte[] data)
|
||||||
|
{
|
||||||
|
auto unknown1 = data.readInteger!uint();
|
||||||
|
auto unknown2 = data.readInteger!uint();
|
||||||
|
|
||||||
|
assert(unknown1 == 0x20 && unknown2 == 0x10, "Invalid directory descriptor");
|
||||||
|
|
||||||
|
_id = data.readInteger!ulong();
|
||||||
|
|
||||||
|
auto subdirectoryCount = data.readInteger!uint();
|
||||||
|
auto fileCount = data.readInteger!uint();
|
||||||
|
|
||||||
|
_depth = data.readInteger!uint();
|
||||||
|
|
||||||
|
auto unknown3 = data.readBytes(4);
|
||||||
|
|
||||||
|
subdirectoryEntries = new FileHashEntry[subdirectoryCount];
|
||||||
|
for (auto i = 0; i < subdirectoryCount; i++)
|
||||||
|
{
|
||||||
|
subdirectoryEntries[i] = FileHashEntry(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEntries = new FileHashEntry[fileCount];
|
||||||
|
for (auto i = 0; i < fileCount; i++)
|
||||||
|
{
|
||||||
|
fileEntries[i] = FileHashEntry(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_parents = new ulong[_depth];
|
||||||
|
for (auto i = 0; i < _depth; i++)
|
||||||
|
{
|
||||||
|
_parents[i] = data.readInteger!ulong();
|
||||||
|
}
|
||||||
|
|
||||||
|
subdirectories = new DirectoryHashNode[subdirectoryCount];
|
||||||
|
for (auto i = 0; i < subdirectoryCount; i++)
|
||||||
|
{
|
||||||
|
subdirectories[i] = new DirectoryHashNode(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FileHashEntry
|
||||||
|
{
|
||||||
|
ulong hash;
|
||||||
|
long offset;
|
||||||
|
|
||||||
|
this(ref ubyte[] data)
|
||||||
|
{
|
||||||
|
hash = data.readInteger!ulong();
|
||||||
|
offset = data.readInteger!long();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ArcEntry
|
||||||
|
{
|
||||||
|
public wstring name;
|
||||||
|
public ArcEntry parent;
|
||||||
|
public ArcEntry[] children;
|
||||||
|
|
||||||
|
this(ArcEntry parent)
|
||||||
|
{
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectoryEntry : ArcEntry
|
||||||
|
{
|
||||||
|
this(ArcEntry parent = null)
|
||||||
|
{
|
||||||
|
super(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileEntry : ArcEntry
|
||||||
|
{
|
||||||
|
private bool _deflated;
|
||||||
|
private uint _uncompressedSize;
|
||||||
|
private uint _compressedSize;
|
||||||
|
|
||||||
|
private ubyte[] _fileData;
|
||||||
|
|
||||||
|
this(ref ubyte[] data, ArcEntry parent = null)
|
||||||
|
{
|
||||||
|
super(parent);
|
||||||
|
|
||||||
|
_deflated = data.readInteger!uint() == 1;
|
||||||
|
|
||||||
|
auto unknown = data.readBytes(4);
|
||||||
|
if (unknown != [0, 0, 0, 0])
|
||||||
|
{
|
||||||
|
stderr.writeln("FileEntry.unknown: ", unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
_uncompressedSize = data.readInteger!uint();
|
||||||
|
_compressedSize = data.readInteger!uint();
|
||||||
|
|
||||||
|
assert(_deflated || _compressedSize == _uncompressedSize, "Uncompressed file entry with unequal sizes");
|
||||||
|
|
||||||
|
_fileData = data.readBytes(_compressedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
ubyte[] data()
|
||||||
|
{
|
||||||
|
if (_deflated)
|
||||||
|
{
|
||||||
|
return cast(ubyte[]) uncompress(_fileData[], _uncompressedSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _fileData[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MmFile _file;
|
||||||
|
private string _path;
|
||||||
|
|
||||||
|
private ubyte[] _header;
|
||||||
|
private ubyte[] _fileData;
|
||||||
|
private ubyte[] _footer;
|
||||||
|
|
||||||
|
private DirectoryHashNode _utf8HashTree;
|
||||||
|
private DirectoryHashNode _utf16HashTree;
|
||||||
|
private wstring[ulong] _nameLookupTable;
|
||||||
|
|
||||||
|
public ArcEntry[] entries;
|
||||||
|
|
||||||
|
this(MmFile file, string path)
|
||||||
{
|
{
|
||||||
_file = file;
|
_file = file;
|
||||||
|
_path = path;
|
||||||
|
|
||||||
|
_header = (cast(ubyte[]) _file[])[0 .. 28];
|
||||||
|
|
||||||
readHeader();
|
readHeader();
|
||||||
|
readFooter();
|
||||||
|
|
||||||
|
auto rootName = _nameLookupTable[_utf16HashTree.id];
|
||||||
|
stderr.writeln("root name: " ~ rootName);
|
||||||
|
|
||||||
|
ArcEntry[] getArcEntries(ArcEntry parent, DirectoryHashNode directoryHashNode)
|
||||||
|
{
|
||||||
|
ArcEntry[] entries;
|
||||||
|
|
||||||
|
foreach (fileHashEntry; directoryHashNode.fileEntries)
|
||||||
|
{
|
||||||
|
auto file = _fileData[fileHashEntry.offset .. $];
|
||||||
|
|
||||||
|
auto fileEntry = new FileEntry(file, parent);
|
||||||
|
fileEntry.name = _nameLookupTable[fileHashEntry.hash];
|
||||||
|
entries ~= fileEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (i, directoryHashEntry; directoryHashNode.subdirectoryEntries)
|
||||||
|
{
|
||||||
|
auto directoryEntry = new DirectoryEntry(parent);
|
||||||
|
directoryEntry.name = _nameLookupTable[directoryHashEntry.hash];
|
||||||
|
directoryEntry.children = getArcEntries(directoryEntry, directoryHashNode.subdirectories[i]);
|
||||||
|
entries ~= directoryEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = getArcEntries(null, _utf16HashTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readHeader()
|
private void readHeader()
|
||||||
{
|
{
|
||||||
auto data = cast(ubyte[]) _file[];
|
auto data = _header[];
|
||||||
|
|
||||||
auto type = cast(string) data.readBytes(4);
|
auto type = cast(string) data.readBytes(4);
|
||||||
assert(type == "warc", "Invalid warc file: " ~ type);
|
assert(type == "warc", _path ~ ": invalid warc file");
|
||||||
|
|
||||||
|
auto unknown1 = data.readBytes(4);
|
||||||
|
assert(unknown1 == [0xFF, 0xAA, 0x45, 0xF1], _path ~ ": invalid warc file");
|
||||||
|
|
||||||
|
auto fileVersion = data.readInteger!uint();
|
||||||
|
assert(fileVersion == 1000, _path ~ ": unrecognised version (" ~ fileVersion.to!string
|
||||||
|
~ ")");
|
||||||
|
|
||||||
|
auto unknown2 = data.readInteger!uint();
|
||||||
|
assert(unknown2 == 4, _path ~ ": invalid warc file");
|
||||||
|
|
||||||
|
auto unknown3 = data.readInteger!uint();
|
||||||
|
assert(unknown3 == 2, _path ~ ": invalid warc file");
|
||||||
|
|
||||||
|
auto fileDataLength = data.readInteger!ulong();
|
||||||
|
_footer = (cast(ubyte[]) _file[])[_header.length + fileDataLength .. $];
|
||||||
|
_fileData = (cast(ubyte[]) _file[])[_header.length .. _header.length + fileDataLength];
|
||||||
|
|
||||||
|
assert(data.length == 0, _path ~ ": unexpected data at end of .arc header");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readFooter()
|
||||||
|
{
|
||||||
|
auto data = _footer[];
|
||||||
|
|
||||||
|
enum BlockType : uint
|
||||||
|
{
|
||||||
|
UTF16HashData = 0,
|
||||||
|
UTF8HashData = 1,
|
||||||
|
UTF16NameData = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_utf8HashTree is null || _utf16HashTree is null || _nameLookupTable == null)
|
||||||
|
{
|
||||||
|
auto blockType = data.readInteger!BlockType();
|
||||||
|
auto blockSize = data.readInteger!ulong();
|
||||||
|
auto blockData = data.readBytes(blockSize);
|
||||||
|
|
||||||
|
if (blockType == BlockType.UTF8HashData)
|
||||||
|
{
|
||||||
|
_utf8HashTree = new DirectoryHashNode(blockData);
|
||||||
|
}
|
||||||
|
else if (blockType == BlockType.UTF16HashData)
|
||||||
|
{
|
||||||
|
_utf16HashTree = new DirectoryHashNode(blockData);
|
||||||
|
}
|
||||||
|
else if (blockType == BlockType.UTF16NameData)
|
||||||
|
{
|
||||||
|
readNameLookupTable(new FileEntry(blockData).data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assert(false, _path ~ ": unknown footer block type (" ~ blockType.to!string ~ ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.length == 0, _path ~ ": unexpected data at end of .arc footer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readNameLookupTable(ubyte[] data)
|
||||||
|
{
|
||||||
|
_nameLookupTable = null;
|
||||||
|
|
||||||
|
while (data.length > 0)
|
||||||
|
{
|
||||||
|
auto hash = data.readInteger!ulong();
|
||||||
|
auto nameLength = data.readInteger!uint();
|
||||||
|
auto name = cast(wstring) data.readBytes(nameLength * 2);
|
||||||
|
|
||||||
|
stderr.writeln(hash, " => \"", name, "\"");
|
||||||
|
|
||||||
|
_nameLookupTable[hash] = name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WarpFile
|
class WarpFile
|
||||||
{
|
{
|
||||||
private MmFile _file;
|
private MmFile _file;
|
||||||
|
private string _path;
|
||||||
|
|
||||||
this(MmFile file)
|
this(MmFile file, string path)
|
||||||
{
|
{
|
||||||
_file = file;
|
_file = file;
|
||||||
|
_path = path;
|
||||||
|
|
||||||
readHeader();
|
readHeader();
|
||||||
}
|
}
|
||||||
|
@ -55,8 +309,6 @@ class ArcFileSystem : Operations
|
||||||
|
|
||||||
this(string[] arcPaths)
|
this(string[] arcPaths)
|
||||||
{
|
{
|
||||||
import std.stdio;
|
|
||||||
|
|
||||||
foreach (path; arcPaths)
|
foreach (path; arcPaths)
|
||||||
{
|
{
|
||||||
assert(path.exists);
|
assert(path.exists);
|
||||||
|
@ -67,15 +319,15 @@ class ArcFileSystem : Operations
|
||||||
auto type = cast(string) mmfile[0 .. 4];
|
auto type = cast(string) mmfile[0 .. 4];
|
||||||
if (type == "warc")
|
if (type == "warc")
|
||||||
{
|
{
|
||||||
_warcFiles ~= new WarcFile(mmfile);
|
|
||||||
|
|
||||||
stderr.writeln("warc");
|
stderr.writeln("warc");
|
||||||
|
|
||||||
|
_warcFiles ~= new WarcFile(mmfile, path);
|
||||||
}
|
}
|
||||||
else if (type == "warp")
|
else if (type == "warp")
|
||||||
{
|
{
|
||||||
_warpFiles ~= new WarpFile(mmfile);
|
|
||||||
|
|
||||||
stderr.writeln("warp");
|
stderr.writeln("warp");
|
||||||
|
|
||||||
|
_warpFiles ~= new WarpFile(mmfile, path);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -41,13 +41,13 @@ class Menu
|
||||||
|
|
||||||
assert(data.readString() == "CM3D2_MENU", "Not a valid .menu file");
|
assert(data.readString() == "CM3D2_MENU", "Not a valid .menu file");
|
||||||
|
|
||||||
menu.fileVersion = data.readInt();
|
menu.fileVersion = data.readInteger!uint();
|
||||||
menu.path = data.readString();
|
menu.path = data.readString();
|
||||||
menu.name = data.readString();
|
menu.name = data.readString();
|
||||||
menu.category = data.readString();
|
menu.category = data.readString();
|
||||||
menu.description = data.readString();
|
menu.description = data.readString();
|
||||||
|
|
||||||
assert(data.readInt() == data.length, "Unexpected data at end of file");
|
assert(data.readInteger!uint() == data.length, "Unexpected data at end of file");
|
||||||
|
|
||||||
while (data.length > 0)
|
while (data.length > 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,7 +5,7 @@ import std.range;
|
||||||
import std.traits;
|
import std.traits;
|
||||||
|
|
||||||
ubyte readByte(Range)(ref Range data)
|
ubyte readByte(Range)(ref Range data)
|
||||||
if (isRandomAccessRange!(Unqual!(Range))
|
if (isRandomAccessRange!(Unqual!Range)
|
||||||
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
||||||
{
|
{
|
||||||
auto value = data[0];
|
auto value = data[0];
|
||||||
|
@ -13,22 +13,17 @@ ubyte readByte(Range)(ref Range data)
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
ubyte[] readBytes(Range)(ref Range data, uint count)
|
Range readBytes(Range)(ref Range data, size_t count)
|
||||||
if (isRandomAccessRange!(Unqual!(Range))
|
if (isRandomAccessRange!(Unqual!Range)
|
||||||
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
||||||
{
|
{
|
||||||
ubyte[] bytes = new ubyte[count];
|
auto bytes = data[0 .. count];
|
||||||
|
data = data[count .. $];
|
||||||
for (auto i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
bytes[i] = data.readByte();
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
string readString(Range)(ref Range data)
|
string readString(Range)(ref Range data)
|
||||||
if (isRandomAccessRange!(Unqual!(Range))
|
if (isRandomAccessRange!(Unqual!Range)
|
||||||
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
||||||
{
|
{
|
||||||
auto length = 0;
|
auto length = 0;
|
||||||
|
@ -57,11 +52,10 @@ string readString(Range)(ref Range data)
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint readInt(Range)(ref Range data)
|
Integer readInteger(Integer, Range)(ref Range data)
|
||||||
if (isRandomAccessRange!(Unqual!(Range))
|
if (isRandomAccessRange!(Unqual!Range) && is(ElementType!Range == ubyte) && hasSlicing!Range)
|
||||||
&& is(ElementType!Range == ubyte) && hasSlicing!Range)
|
|
||||||
{
|
{
|
||||||
auto value = littleEndianToNative!uint(data[0 .. 4]);
|
auto value = littleEndianToNative!Integer(data[0 .. Integer.sizeof]);
|
||||||
data = data[4 .. $];
|
data = data[Integer.sizeof .. $];
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue