mirror of https://github.com/adamdruppe/arsd.git
357 lines
9.5 KiB
D
357 lines
9.5 KiB
D
/++
|
|
A declarative file/stream loader/saver. You define structs with a handful of annotations, this read and writes them to/from files.
|
|
+/
|
|
module arsd.declarativeloader;
|
|
|
|
import std.range;
|
|
|
|
///
|
|
enum BigEndian;
|
|
///
|
|
enum LittleEndian;
|
|
/// @VariableLength indicates the value is saved in a MIDI like format
|
|
enum VariableLength;
|
|
/// @NumBytes!Field or @NumElements!Field controls length of embedded arrays
|
|
struct NumBytes(alias field) {}
|
|
/// ditto
|
|
struct NumElements(alias field) {}
|
|
/// @Tagged!Field indicates a tagged union. Each struct within should have @Tag(X) which is a value of Field
|
|
struct Tagged(alias field) {}
|
|
/// ditto
|
|
auto Tag(T)(T t) {
|
|
return TagStruct!T(t);
|
|
}
|
|
/// For example `@presentIf("version >= 2") int addedInVersion2;`
|
|
struct presentIf { string code; }
|
|
|
|
|
|
struct TagStruct(T) { T t; }
|
|
struct MustBeStruct(T) { T t; }
|
|
/// The marked field is not in the actual file
|
|
enum NotSaved;
|
|
/// Insists the field must be a certain value, like for magic numbers
|
|
auto MustBe(T)(T t) {
|
|
return MustBeStruct!T(t);
|
|
}
|
|
|
|
static bool fieldSaved(alias a)() {
|
|
bool saved;
|
|
static if(is(typeof(a.offsetof))) {
|
|
saved = true;
|
|
static foreach(attr; __traits(getAttributes, a))
|
|
static if(is(attr == NotSaved))
|
|
saved = false;
|
|
}
|
|
return saved;
|
|
}
|
|
|
|
static bool bigEndian(alias a)(bool def) {
|
|
bool be = def;
|
|
static foreach(attr; __traits(getAttributes, a)) {
|
|
static if(is(attr == BigEndian))
|
|
be = true;
|
|
else static if(is(attr == LittleEndian))
|
|
be = false;
|
|
}
|
|
return be;
|
|
}
|
|
|
|
static auto getTag(alias a)() {
|
|
static foreach(attr; __traits(getAttributes, a)) {
|
|
static if(is(typeof(attr) == TagStruct!T, T)) {
|
|
return attr.t;
|
|
}
|
|
}
|
|
assert(0);
|
|
}
|
|
|
|
union N(ty) {
|
|
ty member;
|
|
ubyte[ty.sizeof] bytes;
|
|
}
|
|
|
|
static bool fieldPresent(alias field, T)(T t) {
|
|
bool p = true;
|
|
static foreach(attr; __traits(getAttributes, field)) {
|
|
static if(is(typeof(attr) == presentIf)) {
|
|
bool p2 = false;
|
|
with(t) p2 = mixin(attr.code);
|
|
p = p && p2;
|
|
}
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/// input range of ubytes...
|
|
int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false) {
|
|
int bytesConsumed;
|
|
string currentItem;
|
|
|
|
import std.conv;
|
|
try {
|
|
|
|
ubyte next() {
|
|
if(r.empty)
|
|
throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t));
|
|
auto bfr = r.front;
|
|
r.popFront;
|
|
bytesConsumed++;
|
|
return bfr;
|
|
}
|
|
|
|
bool endianness = bigEndian!T(assumeBigEndian);
|
|
static foreach(memberName; __traits(allMembers, T)) {{
|
|
currentItem = memberName;
|
|
static if(is(typeof(__traits(getMember, T, memberName)))) {
|
|
alias f = __traits(getMember, T, memberName);
|
|
alias ty = typeof(f);
|
|
static if(fieldSaved!f)
|
|
if(fieldPresent!f(t)) {
|
|
endianness = bigEndian!f(endianness);
|
|
// FIXME VariableLength
|
|
static if(is(ty : ulong) || is(ty : double)) {
|
|
N!ty n;
|
|
if(endianness) {
|
|
foreach(i; 0 .. ty.sizeof) {
|
|
version(BigEndian)
|
|
n.bytes[i] = next();
|
|
else
|
|
n.bytes[$ - 1 - i] = next();
|
|
}
|
|
} else {
|
|
foreach(i; 0 .. ty.sizeof) {
|
|
version(BigEndian)
|
|
n.bytes[$ - 1 - i] = next();
|
|
else
|
|
n.bytes[i] = next();
|
|
}
|
|
}
|
|
|
|
// FIXME: MustBe
|
|
|
|
__traits(getMember, t, memberName) = n.member;
|
|
} else static if(is(ty == struct)) {
|
|
bytesConsumed += loadFrom(__traits(getMember, t, memberName), r, endianness);
|
|
} else static if(is(ty == union)) {
|
|
static foreach(attr; __traits(getAttributes, ty))
|
|
static if(is(attr == Tagged!Field, alias Field))
|
|
enum tagField = __traits(identifier, Field);
|
|
static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union");
|
|
|
|
auto tag = __traits(getMember, t, tagField);
|
|
// find the child of the union matching the tag...
|
|
bool found = false;
|
|
static foreach(um; __traits(allMembers, ty)) {
|
|
if(tag == getTag!(__traits(getMember, ty, um))) {
|
|
bytesConsumed += loadFrom(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness);
|
|
found = true;
|
|
}
|
|
}
|
|
if(!found) {
|
|
import std.format;
|
|
throw new Exception(format("found unknown union tag %s at %s", tag, t));
|
|
}
|
|
} else static if(is(ty == E[], E)) {
|
|
static foreach(attr; __traits(getAttributes, f)) {
|
|
static if(is(attr == NumBytes!Field, alias Field))
|
|
ulong numBytesRemaining = __traits(getMember, t, __traits(identifier, Field));
|
|
else static if(is(attr == NumElements!Field, alias Field)) {
|
|
ulong numElementsRemaining = __traits(getMember, t, __traits(identifier, Field));
|
|
}
|
|
}
|
|
|
|
static if(is(typeof(numBytesRemaining))) {
|
|
static if(is(E : const(ubyte)) || is(E : const(char))) {
|
|
while(numBytesRemaining) {
|
|
__traits(getMember, t, memberName) ~= next;
|
|
numBytesRemaining--;
|
|
}
|
|
} else {
|
|
while(numBytesRemaining) {
|
|
E piece;
|
|
auto by = loadFrom(e, r, endianness);
|
|
numBytesRemaining -= by;
|
|
bytesConsumed += by;
|
|
__traits(getMember, t, memberName) ~= piece;
|
|
}
|
|
}
|
|
} else static if(is(typeof(numElementsRemaining))) {
|
|
static if(is(E : const(ubyte)) || is(E : const(char))) {
|
|
while(numElementsRemaining) {
|
|
__traits(getMember, t, memberName) ~= next;
|
|
numElementsRemaining--;
|
|
}
|
|
} else static if(is(E : const(ushort))) {
|
|
while(numElementsRemaining) {
|
|
ushort n;
|
|
n = next << 8;
|
|
n |= next;
|
|
// FIXME all of this filth
|
|
__traits(getMember, t, memberName) ~= n;
|
|
numElementsRemaining--;
|
|
}
|
|
} else {
|
|
while(numElementsRemaining) {
|
|
//import std.stdio; writeln(memberName);
|
|
E piece;
|
|
auto by = loadFrom(piece, r, endianness);
|
|
numElementsRemaining--;
|
|
|
|
// such a filthy hack, needed for Java's mistake though :(
|
|
static if(__traits(compiles, piece.takesTwoSlots())) {
|
|
if(piece.takesTwoSlots()) {
|
|
__traits(getMember, t, memberName) ~= piece;
|
|
numElementsRemaining--;
|
|
}
|
|
}
|
|
|
|
bytesConsumed += by;
|
|
__traits(getMember, t, memberName) ~= piece;
|
|
}
|
|
}
|
|
} else static assert(0, "no way to identify length... " ~ memberName);
|
|
|
|
} else static assert(0, ty.stringof);
|
|
}
|
|
}
|
|
}}
|
|
|
|
} catch(Exception e) {
|
|
throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t), e.file, e.line, e);
|
|
}
|
|
|
|
return bytesConsumed;
|
|
}
|
|
|
|
int saveTo(T, Range)(ref T t, ref Range r, bool assumeBigEndian = false) {
|
|
int bytesWritten;
|
|
string currentItem;
|
|
|
|
import std.conv;
|
|
try {
|
|
|
|
void write(ubyte b) {
|
|
bytesWritten++;
|
|
static if(is(Range == ubyte[]))
|
|
r ~= b;
|
|
else
|
|
r.put(b);
|
|
}
|
|
|
|
bool endianness = bigEndian!T(assumeBigEndian);
|
|
static foreach(memberName; __traits(allMembers, T)) {{
|
|
currentItem = memberName;
|
|
static if(is(typeof(__traits(getMember, T, memberName)))) {
|
|
alias f = __traits(getMember, T, memberName);
|
|
alias ty = typeof(f);
|
|
static if(fieldSaved!f)
|
|
if(fieldPresent!f(t)) {
|
|
endianness = bigEndian!f(endianness);
|
|
// FIXME VariableLength
|
|
static if(is(ty : ulong) || is(ty : double)) {
|
|
N!ty n;
|
|
n.member = __traits(getMember, t, memberName);
|
|
if(endianness) {
|
|
foreach(i; 0 .. ty.sizeof) {
|
|
version(BigEndian)
|
|
write(n.bytes[i]);
|
|
else
|
|
write(n.bytes[$ - 1 - i]);
|
|
}
|
|
} else {
|
|
foreach(i; 0 .. ty.sizeof) {
|
|
version(BigEndian)
|
|
write(n.bytes[$ - 1 - i]);
|
|
else
|
|
write(n.bytes[i]);
|
|
}
|
|
}
|
|
|
|
// FIXME: MustBe
|
|
} else static if(is(ty == struct)) {
|
|
bytesWritten += saveTo(__traits(getMember, t, memberName), r, endianness);
|
|
} else static if(is(ty == union)) {
|
|
static foreach(attr; __traits(getAttributes, ty))
|
|
static if(is(attr == Tagged!Field, alias Field))
|
|
enum tagField = __traits(identifier, Field);
|
|
static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union");
|
|
|
|
auto tag = __traits(getMember, t, tagField);
|
|
// find the child of the union matching the tag...
|
|
bool found = false;
|
|
static foreach(um; __traits(allMembers, ty)) {
|
|
if(tag == getTag!(__traits(getMember, ty, um))) {
|
|
bytesWritten += saveTo(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness);
|
|
found = true;
|
|
}
|
|
}
|
|
if(!found) {
|
|
import std.format;
|
|
throw new Exception(format("found unknown union tag %s at %s", tag, t));
|
|
}
|
|
} else static if(is(ty == E[], E)) {
|
|
|
|
// the numBytesRemaining / numElementsRemaining thing here ASSUMING the
|
|
// arrays are already the correct size. the struct itself could invariant that maybe
|
|
|
|
foreach(item; __traits(getMember, t, memberName)) {
|
|
static if(is(typeof(item) == struct)) {
|
|
bytesWritten += saveTo(item, r, endianness);
|
|
} else {
|
|
static struct dummy {
|
|
typeof(item) i;
|
|
}
|
|
dummy d = dummy(item);
|
|
bytesWritten += saveTo(d, r, endianness);
|
|
}
|
|
}
|
|
|
|
} else static assert(0, ty.stringof);
|
|
}
|
|
}
|
|
}}
|
|
|
|
} catch(Exception e) {
|
|
throw new Exception(T.stringof ~ "." ~ currentItem ~ " save trouble " ~ to!string(t), e.file, e.line, e);
|
|
}
|
|
|
|
return bytesWritten;
|
|
}
|
|
|
|
unittest {
|
|
static struct A {
|
|
int a;
|
|
@presentIf("a > 5") int b;
|
|
int c;
|
|
@NumElements!c ubyte[] d;
|
|
}
|
|
|
|
A a;
|
|
a.loadFrom(cast(ubyte[]) [1, 1, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 6, 7, 8]);
|
|
|
|
assert(a.a == 257);
|
|
assert(a.b == 7);
|
|
assert(a.c == 3);
|
|
assert(a.d == [6,7,8]);
|
|
|
|
a = A.init;
|
|
|
|
a.loadFrom(cast(ubyte[]) [0, 0, 0, 0, 7, 0, 0, 0,1,2,3,4,5,6,7]);
|
|
assert(a.b == 0);
|
|
assert(a.c == 7);
|
|
assert(a.d == [1,2,3,4,5,6,7]);
|
|
|
|
a.a = 44;
|
|
a.c = 3;
|
|
a.d = [5,4,3];
|
|
|
|
ubyte[] saved;
|
|
|
|
a.saveTo(saved);
|
|
|
|
A b;
|
|
b.loadFrom(saved);
|
|
|
|
assert(a == b);
|
|
}
|