mirror of
https://github.com/SabreTools/SabreTools.IO.git
synced 2026-02-12 21:32:16 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8924a50432 | ||
|
|
97f00a2565 | ||
|
|
f35231d95b | ||
|
|
96c6bba93e | ||
|
|
b0d81f225b | ||
|
|
ef699ee1fb | ||
|
|
0910b716ba | ||
|
|
584feb33e6 | ||
|
|
b99c80390e | ||
|
|
3a79646650 | ||
|
|
f16f05beb9 | ||
|
|
e901b52143 | ||
|
|
a638b146b4 | ||
|
|
6bfc961a87 | ||
|
|
a825bae039 | ||
|
|
e0eba8e5bb | ||
|
|
fb4b533dfb | ||
|
|
6162af2216 | ||
|
|
3b5fd128f0 | ||
|
|
9beb2177aa | ||
|
|
f0033af712 | ||
|
|
3de5b2378d | ||
|
|
ee7ce59627 | ||
|
|
39bf9c19ad | ||
|
|
32cab49bae | ||
|
|
5d71957841 | ||
|
|
7ea182c7d8 | ||
|
|
b97ec13661 | ||
|
|
8caeea053f | ||
|
|
a7476b6ac9 | ||
|
|
f0095f9e41 | ||
|
|
a0b5ea1368 | ||
|
|
a94d2c8c64 | ||
|
|
8c19ad712a | ||
|
|
0317f751b9 | ||
|
|
b8d431b06b | ||
|
|
3fcf10e2f7 | ||
|
|
40e439b18c | ||
|
|
bf707b1c11 | ||
|
|
d074a6a7ee | ||
|
|
0c736c2491 | ||
|
|
964506057d | ||
|
|
cd08925411 | ||
|
|
6ea8aab7c7 | ||
|
|
561dbdcc9a | ||
|
|
b4bad28823 |
@@ -397,39 +397,190 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void ReadTypeExplicitTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(bytesWithString);
|
||||
var br = new BinaryReader(stream);
|
||||
var expected = new TestStructExplicit
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0504,
|
||||
FourthValue = 0x0706,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
var read = br.ReadType<TestStructExplicit>();
|
||||
Assert.Equal(expected.FirstValue, read.FirstValue);
|
||||
Assert.Equal(expected.SecondValue, read.SecondValue);
|
||||
Assert.Equal(expected.ThirdValue, read.ThirdValue);
|
||||
Assert.Equal(expected.FourthValue, read.FourthValue);
|
||||
Assert.Equal(expected.FifthValue, read.FifthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeSequentialTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(bytesWithString);
|
||||
var br = new BinaryReader(stream);
|
||||
var expected = new TestStructSequential
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0908,
|
||||
FourthValue = 0x0B0A,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
var read = br.ReadType<TestStructSequential>();
|
||||
Assert.Equal(expected.FirstValue, read.FirstValue);
|
||||
Assert.Equal(expected.SecondValue, read.SecondValue);
|
||||
Assert.Equal(expected.ThirdValue, read.ThirdValue);
|
||||
Assert.Equal(expected.FourthValue, read.FourthValue);
|
||||
Assert.Equal(expected.FifthValue, read.FifthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeStringsTest()
|
||||
{
|
||||
byte[] structBytes =
|
||||
[
|
||||
0x03, 0x41, 0x42, 0x43, // AnsiBStr
|
||||
0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, // BStr
|
||||
0x41, 0x42, 0x43, // ByValTStr
|
||||
0x41, 0x42, 0x43, 0x00, // LPStr
|
||||
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, // LPWStr
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(structBytes);
|
||||
var br = new BinaryReader(stream);
|
||||
var expected = new TestStructStrings
|
||||
{
|
||||
AnsiBStr = "ABC",
|
||||
BStr = "ABC",
|
||||
ByValTStr = "ABC",
|
||||
LPStr = "ABC",
|
||||
LPWStr = "ABC",
|
||||
};
|
||||
var read = br.ReadType<TestStructStrings>();
|
||||
Assert.Equal(expected.AnsiBStr, read.AnsiBStr);
|
||||
Assert.Equal(expected.BStr, read.BStr);
|
||||
Assert.Equal(expected.ByValTStr, read.ByValTStr);
|
||||
Assert.Equal(expected.LPStr, read.LPStr);
|
||||
Assert.Equal(expected.LPWStr, read.LPWStr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeArraysTest()
|
||||
{
|
||||
byte[] structBytes =
|
||||
[
|
||||
// Byte Array
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
|
||||
// Int Array
|
||||
0x03, 0x02, 0x01, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
0x05, 0x04, 0x03, 0x02,
|
||||
0x06, 0x05, 0x04, 0x03,
|
||||
|
||||
// Struct Array (X, Y)
|
||||
0xFF, 0x00, 0x00, 0xFF,
|
||||
0x00, 0xFF, 0xFF, 0x00,
|
||||
0xAA, 0x55, 0x55, 0xAA,
|
||||
0x55, 0xAA, 0xAA, 0x55,
|
||||
|
||||
// LPArray
|
||||
0x04, 0x00,
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(structBytes);
|
||||
var br = new BinaryReader(stream);
|
||||
var expected = new TestStructArrays
|
||||
{
|
||||
ByteArray = [0x00, 0x01, 0x02, 0x03],
|
||||
IntArray = [0x00010203, 0x01020304, 0x02030405, 0x03040506],
|
||||
StructArray =
|
||||
[
|
||||
new TestStructPoint { X = 0x00FF, Y = 0xFF00 },
|
||||
new TestStructPoint { X = 0xFF00, Y = 0x00FF },
|
||||
new TestStructPoint { X = 0x55AA, Y = 0xAA55 },
|
||||
new TestStructPoint { X = 0xAA55, Y = 0x55AA },
|
||||
],
|
||||
LPByteArrayLength = 0x0004,
|
||||
LPByteArray = [0x00, 0x01, 0x02, 0x03],
|
||||
};
|
||||
var read = br.ReadType<TestStructArrays>();
|
||||
Assert.NotNull(read.ByteArray);
|
||||
Assert.True(expected.ByteArray.SequenceEqual(read.ByteArray));
|
||||
Assert.NotNull(read.IntArray);
|
||||
Assert.True(expected.IntArray.SequenceEqual(read.IntArray));
|
||||
Assert.NotNull(read.StructArray);
|
||||
Assert.True(expected.StructArray.SequenceEqual(read.StructArray));
|
||||
Assert.Equal(expected.LPByteArrayLength, read.LPByteArrayLength);
|
||||
Assert.NotNull(read.LPByteArray);
|
||||
Assert.True(expected.LPByteArray.SequenceEqual(read.LPByteArray));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeInheritanceTest()
|
||||
{
|
||||
byte[] structBytes1 =
|
||||
[
|
||||
0x41, 0x42, 0x43, 0x44, // Signature
|
||||
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
|
||||
0xAA, 0x55, 0xAA, 0x55, // FieldA
|
||||
0x55, 0xAA, 0x55, 0xAA, // FieldB
|
||||
];
|
||||
|
||||
var stream1 = new MemoryStream(structBytes1);
|
||||
var br1 = new BinaryReader(stream1);
|
||||
var expected1 = new TestStructInheritanceChild1
|
||||
{
|
||||
Signature = [0x41, 0x42, 0x43, 0x44],
|
||||
IdentifierType = 0xFF00FF00,
|
||||
FieldA = 0x55AA55AA,
|
||||
FieldB = 0xAA55AA55,
|
||||
};
|
||||
var read1 = br1.ReadType<TestStructInheritanceChild1>();
|
||||
Assert.NotNull(read1?.Signature);
|
||||
Assert.Equal(expected1.Signature, read1.Signature);
|
||||
Assert.Equal(expected1.IdentifierType, read1.IdentifierType);
|
||||
Assert.Equal(expected1.FieldA, read1.FieldA);
|
||||
Assert.Equal(expected1.FieldB, read1.FieldB);
|
||||
|
||||
byte[] structBytes2 =
|
||||
[
|
||||
0x41, 0x42, 0x43, 0x44, // Signature
|
||||
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
|
||||
0xAA, 0x55, // FieldA
|
||||
0x55, 0xAA, // FieldB
|
||||
];
|
||||
|
||||
var stream2 = new MemoryStream(structBytes2);
|
||||
var br2 = new BinaryReader(stream2);
|
||||
var expected2 = new TestStructInheritanceChild2
|
||||
{
|
||||
Signature = [0x41, 0x42, 0x43, 0x44],
|
||||
IdentifierType = 0xFF00FF00,
|
||||
FieldA = 0x55AA,
|
||||
FieldB = 0xAA55,
|
||||
};
|
||||
var read2 = br2.ReadType<TestStructInheritanceChild2>();
|
||||
Assert.NotNull(read2?.Signature);
|
||||
Assert.Equal(expected2.Signature, read2.Signature);
|
||||
Assert.Equal(expected2.IdentifierType, read2.IdentifierType);
|
||||
Assert.Equal(expected2.FieldA, read2.FieldA);
|
||||
Assert.Equal(expected2.FieldB, read2.FieldB);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,14 +449,21 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void WriteTypeExplicitTest()
|
||||
{
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
|
||||
var bw = new BinaryWriter(stream);
|
||||
var obj = new TestStructExplicit
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
byte[] expected = _bytes.Take(8).ToArray();
|
||||
byte[] expected = bytesWithString.Take(12).ToArray();
|
||||
bool write = bw.WriteType(obj);
|
||||
Assert.True(write);
|
||||
ValidateBytes(expected, stream.GetBuffer());
|
||||
@@ -465,16 +472,23 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void WriteTypeSequentialTest()
|
||||
{
|
||||
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(new byte[24], 0, count: 24, true, true);
|
||||
var bw = new BinaryWriter(stream);
|
||||
var obj = new TestStructSequential
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0908,
|
||||
FourthValue = 0x0B0A,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
byte[] expected = _bytes.Take(12).ToArray();
|
||||
byte[] expected = bytesWithString.Take(16).ToArray();
|
||||
bool write = bw.WriteType(obj);
|
||||
Assert.True(write);
|
||||
ValidateBytes(expected, stream.GetBuffer());
|
||||
|
||||
@@ -365,15 +365,22 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void ReadTypeExplicitTest()
|
||||
{
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
int offset = 0;
|
||||
var expected = new TestStructExplicit
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0504,
|
||||
FourthValue = 0x0706,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
var read = _bytes.ReadType<TestStructExplicit>(ref offset);
|
||||
var read = bytesWithString.ReadType<TestStructExplicit>(ref offset);
|
||||
Assert.Equal(expected.FirstValue, read.FirstValue);
|
||||
Assert.Equal(expected.SecondValue, read.SecondValue);
|
||||
Assert.Equal(expected.ThirdValue, read.ThirdValue);
|
||||
@@ -383,19 +390,158 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void ReadTypeSequentialTest()
|
||||
{
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
int offset = 0;
|
||||
var expected = new TestStructSequential
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0908,
|
||||
FourthValue = 0x0B0A,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
var read = _bytes.ReadType<TestStructSequential>(ref offset);
|
||||
var read = bytesWithString.ReadType<TestStructSequential>(ref offset);
|
||||
Assert.Equal(expected.FirstValue, read.FirstValue);
|
||||
Assert.Equal(expected.SecondValue, read.SecondValue);
|
||||
Assert.Equal(expected.ThirdValue, read.ThirdValue);
|
||||
Assert.Equal(expected.FourthValue, read.FourthValue);
|
||||
Assert.Equal(expected.FifthValue, read.FifthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeStringsTest()
|
||||
{
|
||||
byte[] structBytes =
|
||||
[
|
||||
0x03, 0x41, 0x42, 0x43, // AnsiBStr
|
||||
0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, // BStr
|
||||
0x41, 0x42, 0x43, // ByValTStr
|
||||
0x41, 0x42, 0x43, 0x00, // LPStr
|
||||
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, // LPWStr
|
||||
];
|
||||
|
||||
int offset = 0;
|
||||
var expected = new TestStructStrings
|
||||
{
|
||||
AnsiBStr = "ABC",
|
||||
BStr = "ABC",
|
||||
ByValTStr = "ABC",
|
||||
LPStr = "ABC",
|
||||
LPWStr = "ABC",
|
||||
};
|
||||
var read = structBytes.ReadType<TestStructStrings>(ref offset);
|
||||
Assert.Equal(expected.AnsiBStr, read.AnsiBStr);
|
||||
Assert.Equal(expected.BStr, read.BStr);
|
||||
Assert.Equal(expected.ByValTStr, read.ByValTStr);
|
||||
Assert.Equal(expected.LPStr, read.LPStr);
|
||||
Assert.Equal(expected.LPWStr, read.LPWStr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeArraysTest()
|
||||
{
|
||||
byte[] structBytes =
|
||||
[
|
||||
// Byte Array
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
|
||||
// Int Array
|
||||
0x03, 0x02, 0x01, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
0x05, 0x04, 0x03, 0x02,
|
||||
0x06, 0x05, 0x04, 0x03,
|
||||
|
||||
// Struct Array (X, Y)
|
||||
0xFF, 0x00, 0x00, 0xFF,
|
||||
0x00, 0xFF, 0xFF, 0x00,
|
||||
0xAA, 0x55, 0x55, 0xAA,
|
||||
0x55, 0xAA, 0xAA, 0x55,
|
||||
|
||||
// LPArray
|
||||
0x04, 0x00,
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
];
|
||||
|
||||
int offset = 0;
|
||||
var expected = new TestStructArrays
|
||||
{
|
||||
ByteArray = [0x00, 0x01, 0x02, 0x03],
|
||||
IntArray = [0x00010203, 0x01020304, 0x02030405, 0x03040506],
|
||||
StructArray =
|
||||
[
|
||||
new TestStructPoint { X = 0x00FF, Y = 0xFF00 },
|
||||
new TestStructPoint { X = 0xFF00, Y = 0x00FF },
|
||||
new TestStructPoint { X = 0x55AA, Y = 0xAA55 },
|
||||
new TestStructPoint { X = 0xAA55, Y = 0x55AA },
|
||||
],
|
||||
LPByteArrayLength = 0x0004,
|
||||
LPByteArray = [0x00, 0x01, 0x02, 0x03],
|
||||
};
|
||||
var read = structBytes.ReadType<TestStructArrays>(ref offset);
|
||||
Assert.NotNull(read.ByteArray);
|
||||
Assert.True(expected.ByteArray.SequenceEqual(read.ByteArray));
|
||||
Assert.NotNull(read.IntArray);
|
||||
Assert.True(expected.IntArray.SequenceEqual(read.IntArray));
|
||||
Assert.NotNull(read.StructArray);
|
||||
Assert.True(expected.StructArray.SequenceEqual(read.StructArray));
|
||||
Assert.Equal(expected.LPByteArrayLength, read.LPByteArrayLength);
|
||||
Assert.NotNull(read.LPByteArray);
|
||||
Assert.True(expected.LPByteArray.SequenceEqual(read.LPByteArray));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeInheritanceTest()
|
||||
{
|
||||
byte[] structBytes1 =
|
||||
[
|
||||
0x41, 0x42, 0x43, 0x44, // Signature
|
||||
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
|
||||
0xAA, 0x55, 0xAA, 0x55, // FieldA
|
||||
0x55, 0xAA, 0x55, 0xAA, // FieldB
|
||||
];
|
||||
|
||||
int offset1 = 0;
|
||||
var expected1 = new TestStructInheritanceChild1
|
||||
{
|
||||
Signature = [0x41, 0x42, 0x43, 0x44],
|
||||
IdentifierType = 0xFF00FF00,
|
||||
FieldA = 0x55AA55AA,
|
||||
FieldB = 0xAA55AA55,
|
||||
};
|
||||
var read1 = structBytes1.ReadType<TestStructInheritanceChild1>(ref offset1);
|
||||
Assert.NotNull(read1?.Signature);
|
||||
Assert.Equal(expected1.Signature, read1.Signature);
|
||||
Assert.Equal(expected1.IdentifierType, read1.IdentifierType);
|
||||
Assert.Equal(expected1.FieldA, read1.FieldA);
|
||||
Assert.Equal(expected1.FieldB, read1.FieldB);
|
||||
|
||||
byte[] structBytes2 =
|
||||
[
|
||||
0x41, 0x42, 0x43, 0x44, // Signature
|
||||
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
|
||||
0xAA, 0x55, // FieldA
|
||||
0x55, 0xAA, // FieldB
|
||||
];
|
||||
|
||||
int offset2 = 0;
|
||||
var expected2 = new TestStructInheritanceChild2
|
||||
{
|
||||
Signature = [0x41, 0x42, 0x43, 0x44],
|
||||
IdentifierType = 0xFF00FF00,
|
||||
FieldA = 0x55AA,
|
||||
FieldB = 0xAA55,
|
||||
};
|
||||
var read2 = structBytes2.ReadType<TestStructInheritanceChild2>(ref offset2);
|
||||
Assert.NotNull(read2?.Signature);
|
||||
Assert.Equal(expected2.Signature, read2.Signature);
|
||||
Assert.Equal(expected2.IdentifierType, read2.IdentifierType);
|
||||
Assert.Equal(expected2.FieldA, read2.FieldA);
|
||||
Assert.Equal(expected2.FieldB, read2.FieldB);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,14 +432,21 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void WriteTypeExplicitTest()
|
||||
{
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
byte[] buffer = new byte[16];
|
||||
int offset = 0;
|
||||
var obj = new TestStructExplicit
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
byte[] expected = _bytes.Take(8).ToArray();
|
||||
byte[] expected = bytesWithString.Take(12).ToArray();
|
||||
bool write = buffer.WriteType(ref offset, obj);
|
||||
Assert.True(write);
|
||||
ValidateBytes(expected, buffer);
|
||||
@@ -448,16 +455,23 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void WriteTypeSequentialTest()
|
||||
{
|
||||
byte[] buffer = new byte[16];
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
byte[] buffer = new byte[24];
|
||||
int offset = 0;
|
||||
var obj = new TestStructSequential
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0908,
|
||||
FourthValue = 0x0B0A,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
byte[] expected = _bytes.Take(12).ToArray();
|
||||
byte[] expected = bytesWithString.Take(16).ToArray();
|
||||
bool write = buffer.WriteType(ref offset, obj);
|
||||
Assert.True(write);
|
||||
ValidateBytes(expected, buffer);
|
||||
|
||||
@@ -359,13 +359,20 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void ReadTypeExplicitTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(bytesWithString);
|
||||
var expected = new TestStructExplicit
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0504,
|
||||
FourthValue = 0x0706,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
var read = stream.ReadType<TestStructExplicit>();
|
||||
Assert.Equal(expected.FirstValue, read.FirstValue);
|
||||
@@ -377,19 +384,158 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void ReadTypeSequentialTest()
|
||||
{
|
||||
var stream = new MemoryStream(_bytes);
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(bytesWithString);
|
||||
var expected = new TestStructSequential
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0908,
|
||||
FourthValue = 0x0B0A,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
var read = stream.ReadType<TestStructSequential>();
|
||||
Assert.Equal(expected.FirstValue, read.FirstValue);
|
||||
Assert.Equal(expected.SecondValue, read.SecondValue);
|
||||
Assert.Equal(expected.ThirdValue, read.ThirdValue);
|
||||
Assert.Equal(expected.FourthValue, read.FourthValue);
|
||||
Assert.Equal(expected.FifthValue, read.FifthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeStringsTest()
|
||||
{
|
||||
byte[] structBytes =
|
||||
[
|
||||
0x03, 0x41, 0x42, 0x43, // AnsiBStr
|
||||
0x03, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, // BStr
|
||||
0x41, 0x42, 0x43, // ByValTStr
|
||||
0x41, 0x42, 0x43, 0x00, // LPStr
|
||||
0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, // LPWStr
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(structBytes);
|
||||
var expected = new TestStructStrings
|
||||
{
|
||||
AnsiBStr = "ABC",
|
||||
BStr = "ABC",
|
||||
ByValTStr = "ABC",
|
||||
LPStr = "ABC",
|
||||
LPWStr = "ABC",
|
||||
};
|
||||
var read = stream.ReadType<TestStructStrings>();
|
||||
Assert.Equal(expected.AnsiBStr, read.AnsiBStr);
|
||||
Assert.Equal(expected.BStr, read.BStr);
|
||||
Assert.Equal(expected.ByValTStr, read.ByValTStr);
|
||||
Assert.Equal(expected.LPStr, read.LPStr);
|
||||
Assert.Equal(expected.LPWStr, read.LPWStr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeArraysTest()
|
||||
{
|
||||
byte[] structBytes =
|
||||
[
|
||||
// Byte Array
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
|
||||
// Int Array
|
||||
0x03, 0x02, 0x01, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
0x05, 0x04, 0x03, 0x02,
|
||||
0x06, 0x05, 0x04, 0x03,
|
||||
|
||||
// Struct Array (X, Y)
|
||||
0xFF, 0x00, 0x00, 0xFF,
|
||||
0x00, 0xFF, 0xFF, 0x00,
|
||||
0xAA, 0x55, 0x55, 0xAA,
|
||||
0x55, 0xAA, 0xAA, 0x55,
|
||||
|
||||
// LPArray
|
||||
0x04, 0x00,
|
||||
0x00, 0x01, 0x02, 0x03,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(structBytes);
|
||||
var expected = new TestStructArrays
|
||||
{
|
||||
ByteArray = [0x00, 0x01, 0x02, 0x03],
|
||||
IntArray = [0x00010203, 0x01020304, 0x02030405, 0x03040506],
|
||||
StructArray =
|
||||
[
|
||||
new TestStructPoint { X = 0x00FF, Y = 0xFF00 },
|
||||
new TestStructPoint { X = 0xFF00, Y = 0x00FF },
|
||||
new TestStructPoint { X = 0x55AA, Y = 0xAA55 },
|
||||
new TestStructPoint { X = 0xAA55, Y = 0x55AA },
|
||||
],
|
||||
LPByteArrayLength = 0x0004,
|
||||
LPByteArray = [0x00, 0x01, 0x02, 0x03],
|
||||
};
|
||||
var read = stream.ReadType<TestStructArrays>();
|
||||
Assert.NotNull(read.ByteArray);
|
||||
Assert.True(expected.ByteArray.SequenceEqual(read.ByteArray));
|
||||
Assert.NotNull(read.IntArray);
|
||||
Assert.True(expected.IntArray.SequenceEqual(read.IntArray));
|
||||
Assert.NotNull(read.StructArray);
|
||||
Assert.True(expected.StructArray.SequenceEqual(read.StructArray));
|
||||
Assert.Equal(expected.LPByteArrayLength, read.LPByteArrayLength);
|
||||
Assert.NotNull(read.LPByteArray);
|
||||
Assert.True(expected.LPByteArray.SequenceEqual(read.LPByteArray));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTypeInheritanceTest()
|
||||
{
|
||||
byte[] structBytes1 =
|
||||
[
|
||||
0x41, 0x42, 0x43, 0x44, // Signature
|
||||
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
|
||||
0xAA, 0x55, 0xAA, 0x55, // FieldA
|
||||
0x55, 0xAA, 0x55, 0xAA, // FieldB
|
||||
];
|
||||
|
||||
var stream1 = new MemoryStream(structBytes1);
|
||||
var expected1 = new TestStructInheritanceChild1
|
||||
{
|
||||
Signature = [0x41, 0x42, 0x43, 0x44],
|
||||
IdentifierType = 0xFF00FF00,
|
||||
FieldA = 0x55AA55AA,
|
||||
FieldB = 0xAA55AA55,
|
||||
};
|
||||
var read1 = stream1.ReadType<TestStructInheritanceChild1>();
|
||||
Assert.NotNull(read1?.Signature);
|
||||
Assert.Equal(expected1.Signature, read1.Signature);
|
||||
Assert.Equal(expected1.IdentifierType, read1.IdentifierType);
|
||||
Assert.Equal(expected1.FieldA, read1.FieldA);
|
||||
Assert.Equal(expected1.FieldB, read1.FieldB);
|
||||
|
||||
byte[] structBytes2 =
|
||||
[
|
||||
0x41, 0x42, 0x43, 0x44, // Signature
|
||||
0x00, 0xFF, 0x00, 0xFF, // IdentifierType
|
||||
0xAA, 0x55, // FieldA
|
||||
0x55, 0xAA, // FieldB
|
||||
];
|
||||
|
||||
var stream2 = new MemoryStream(structBytes2);
|
||||
var expected2 = new TestStructInheritanceChild2
|
||||
{
|
||||
Signature = [0x41, 0x42, 0x43, 0x44],
|
||||
IdentifierType = 0xFF00FF00,
|
||||
FieldA = 0x55AA,
|
||||
FieldB = 0xAA55,
|
||||
};
|
||||
var read2 = stream2.ReadType<TestStructInheritanceChild2>();
|
||||
Assert.NotNull(read2?.Signature);
|
||||
Assert.Equal(expected2.Signature, read2.Signature);
|
||||
Assert.Equal(expected2.IdentifierType, read2.IdentifierType);
|
||||
Assert.Equal(expected2.FieldA, read2.FieldA);
|
||||
Assert.Equal(expected2.FieldB, read2.FieldB);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,13 +397,20 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void WriteTypeExplicitTest()
|
||||
{
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
|
||||
var obj = new TestStructExplicit
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
byte[] expected = _bytes.Take(8).ToArray();
|
||||
byte[] expected = bytesWithString.Take(12).ToArray();
|
||||
bool write = stream.WriteType(obj);
|
||||
Assert.True(write);
|
||||
ValidateBytes(expected, stream.GetBuffer());
|
||||
@@ -412,15 +419,22 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[Fact]
|
||||
public void WriteTypeSequentialTest()
|
||||
{
|
||||
var stream = new MemoryStream(new byte[16], 0, 16, true, true);
|
||||
byte[] bytesWithString =
|
||||
[
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x41, 0x42, 0x43, 0x00,
|
||||
];
|
||||
|
||||
var stream = new MemoryStream(new byte[24], 0, 24, true, true);
|
||||
var obj = new TestStructSequential
|
||||
{
|
||||
FirstValue = 0x03020100,
|
||||
FirstValue = TestEnum.RecognizedTestValue,
|
||||
SecondValue = 0x07060504,
|
||||
ThirdValue = 0x0908,
|
||||
FourthValue = 0x0B0A,
|
||||
FifthValue = "ABC",
|
||||
};
|
||||
byte[] expected = _bytes.Take(12).ToArray();
|
||||
byte[] expected = bytesWithString.Take(16).ToArray();
|
||||
bool write = stream.WriteType(obj);
|
||||
Assert.True(write);
|
||||
ValidateBytes(expected, stream.GetBuffer());
|
||||
|
||||
9
SabreTools.IO.Test/Extensions/TestEnum.cs
Normal file
9
SabreTools.IO.Test/Extensions/TestEnum.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace SabreTools.IO.Test.Extensions
|
||||
{
|
||||
internal enum TestEnum : uint
|
||||
{
|
||||
None = 0x00000000,
|
||||
RecognizedTestValue = 0x03020100,
|
||||
UpperBoundaryValue = 0xFFFFFFFF,
|
||||
}
|
||||
}
|
||||
53
SabreTools.IO.Test/Extensions/TestStructArrays.cs
Normal file
53
SabreTools.IO.Test/Extensions/TestStructArrays.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SabreTools.IO.Test.Extensions
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
internal struct TestStructArrays
|
||||
{
|
||||
/// <summary>
|
||||
/// 4 entry byte array
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
public byte[]? ByteArray;
|
||||
|
||||
/// <summary>
|
||||
/// 4 entry int array
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
public int[]? IntArray;
|
||||
|
||||
/// <summary>
|
||||
/// 4 entry struct array
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
public TestStructPoint[]? StructArray;
|
||||
|
||||
/// <summary>
|
||||
/// Length of <see cref="LPByteArray"/>
|
||||
/// </summary>
|
||||
public ushort LPByteArrayLength;
|
||||
|
||||
/// <summary>
|
||||
/// 4 entry byte array whose length is defined by <see cref="LPByteArrayLength"/>
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)]
|
||||
public byte[]? LPByteArray;
|
||||
|
||||
// /// <summary>
|
||||
// /// 4 entry nested byte array
|
||||
// /// </summary>
|
||||
// /// <remarks>This will likely fail</remarks>
|
||||
// [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
// public byte[][]? NestedByteArray;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct for nested tests
|
||||
/// </summary>
|
||||
internal struct TestStructPoint
|
||||
{
|
||||
public ushort X;
|
||||
public ushort Y;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace SabreTools.IO.Test.Extensions
|
||||
internal struct TestStructExplicit
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public int FirstValue;
|
||||
public TestEnum FirstValue;
|
||||
|
||||
[FieldOffset(4)]
|
||||
public int SecondValue;
|
||||
@@ -16,5 +16,8 @@ namespace SabreTools.IO.Test.Extensions
|
||||
|
||||
[FieldOffset(6)]
|
||||
public short FourthValue;
|
||||
|
||||
[FieldOffset(8), MarshalAs(UnmanagedType.LPStr)]
|
||||
public string? FifthValue;
|
||||
}
|
||||
}
|
||||
|
||||
29
SabreTools.IO.Test/Extensions/TestStructInheritance.cs
Normal file
29
SabreTools.IO.Test/Extensions/TestStructInheritance.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SabreTools.IO.Test.Extensions
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
internal class TestStructInheritanceParent
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
|
||||
public byte[]? Signature;
|
||||
|
||||
public uint IdentifierType;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
internal class TestStructInheritanceChild1 : TestStructInheritanceParent
|
||||
{
|
||||
public uint FieldA;
|
||||
|
||||
public uint FieldB;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
internal class TestStructInheritanceChild2 : TestStructInheritanceParent
|
||||
{
|
||||
public ushort FieldA;
|
||||
|
||||
public ushort FieldB;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,15 @@ namespace SabreTools.IO.Test.Extensions
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct TestStructSequential
|
||||
{
|
||||
public int FirstValue;
|
||||
public TestEnum FirstValue;
|
||||
|
||||
public int SecondValue;
|
||||
|
||||
public ushort ThirdValue;
|
||||
|
||||
public short FourthValue;
|
||||
|
||||
[MarshalAs(UnmanagedType.LPStr)]
|
||||
public string? FifthValue;
|
||||
}
|
||||
}
|
||||
|
||||
38
SabreTools.IO.Test/Extensions/TestStructStrings.cs
Normal file
38
SabreTools.IO.Test/Extensions/TestStructStrings.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SabreTools.IO.Test.Extensions
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
internal struct TestStructStrings
|
||||
{
|
||||
/// <summary>
|
||||
/// ASCII-encoded, byte-length-prefixed string
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.AnsiBStr)]
|
||||
public string? AnsiBStr;
|
||||
|
||||
/// <summary>
|
||||
/// Unicode-encoded, WORD-length-prefixed string
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.BStr)]
|
||||
public string? BStr;
|
||||
|
||||
/// <summary>
|
||||
/// Fixed length string
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 3)]
|
||||
public string? ByValTStr;
|
||||
|
||||
/// <summary>
|
||||
/// ASCII-encoded, null-terminated string
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.LPStr)]
|
||||
public string? LPStr;
|
||||
|
||||
/// <summary>
|
||||
/// Unicode-encoded, null-terminated string
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string? LPWStr;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
|
||||
@@ -6,16 +6,17 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -324,8 +328,12 @@ namespace SabreTools.IO.Extensions
|
||||
// Short-circuit to explicit implementations
|
||||
if (encoding.Equals(Encoding.ASCII))
|
||||
return reader.ReadNullTerminatedAnsiString();
|
||||
else if (encoding.Equals(Encoding.UTF8))
|
||||
return reader.ReadNullTerminatedUTF8String();
|
||||
else if (encoding.Equals(Encoding.Unicode))
|
||||
return reader.ReadNullTerminatedUnicodeString();
|
||||
else if (encoding.Equals(Encoding.UTF32))
|
||||
return reader.ReadNullTerminatedUTF32String();
|
||||
|
||||
if (reader.BaseStream.Position >= reader.BaseStream.Length)
|
||||
return null;
|
||||
@@ -350,36 +358,44 @@ namespace SabreTools.IO.Extensions
|
||||
if (reader.BaseStream.Position >= reader.BaseStream.Length)
|
||||
return null;
|
||||
|
||||
List<byte> buffer = [];
|
||||
while (reader.BaseStream.Position < reader.BaseStream.Length)
|
||||
{
|
||||
byte ch = reader.ReadByte();
|
||||
buffer.Add(ch);
|
||||
if (ch == '\0')
|
||||
break;
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString([.. buffer]);
|
||||
byte[] buffer = ReadUntilNull1Byte(reader);
|
||||
return Encoding.ASCII.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated Unicode string from the underlying stream
|
||||
/// Read a null-terminated UTF-8 string from the underlying stream
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUTF8String(this BinaryReader reader)
|
||||
{
|
||||
if (reader.BaseStream.Position >= reader.BaseStream.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = ReadUntilNull1Byte(reader);
|
||||
return Encoding.ASCII.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated UTF-16 (Unicode) string from the underlying stream
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUnicodeString(this BinaryReader reader)
|
||||
{
|
||||
if (reader.BaseStream.Position >= reader.BaseStream.Length)
|
||||
return null;
|
||||
|
||||
List<byte> buffer = [];
|
||||
while (reader.BaseStream.Position < reader.BaseStream.Length)
|
||||
{
|
||||
byte[] ch = reader.ReadBytes(2);
|
||||
buffer.AddRange(ch);
|
||||
if (ch[0] == '\0' && ch[1] == '\0')
|
||||
break;
|
||||
}
|
||||
byte[] buffer = ReadUntilNull2Byte(reader);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
return Encoding.Unicode.GetString([.. buffer]);
|
||||
/// <summary>
|
||||
/// Read a null-terminated UTF-32 string from the underlying stream
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUTF32String(this BinaryReader reader)
|
||||
{
|
||||
if (reader.BaseStream.Position >= reader.BaseStream.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = ReadUntilNull4Byte(reader);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -407,23 +423,23 @@ namespace SabreTools.IO.Extensions
|
||||
return null;
|
||||
|
||||
ushort size = reader.ReadUInt16();
|
||||
if (reader.BaseStream.Position + size >= reader.BaseStream.Length)
|
||||
if (reader.BaseStream.Position + (size * 2) >= reader.BaseStream.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = reader.ReadBytes(size);
|
||||
byte[] buffer = reader.ReadBytes(size * 2);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a string that is terminated by a newline but contains a quoted portion that
|
||||
/// may also contain a newline from the stream
|
||||
/// may also contain a newline from the underlying stream
|
||||
/// </summary>
|
||||
public static string? ReadQuotedString(this BinaryReader reader)
|
||||
=> reader.ReadQuotedString(Encoding.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Read a string that is terminated by a newline but contains a quoted portion that
|
||||
/// may also contain a newline from the stream
|
||||
/// may also contain a newline from the underlying stream
|
||||
/// </summary>
|
||||
public static string? ReadQuotedString(this BinaryReader reader, Encoding encoding)
|
||||
{
|
||||
@@ -456,16 +472,272 @@ namespace SabreTools.IO.Extensions
|
||||
/// <summary>
|
||||
/// Read a <typeparamref name="T"/> from the underlying stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are read by value, not by reference
|
||||
/// - Complex objects are read by value, not by reference
|
||||
/// - Enumeration values are read by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are deserialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static T? ReadType<T>(this BinaryReader reader)
|
||||
=> (T?)reader.ReadType(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the underlying stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are read by value, not by reference
|
||||
/// - Complex objects are read by value, not by reference
|
||||
/// - Enumeration values are read by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are deserialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static object? ReadType(this BinaryReader reader, Type type)
|
||||
{
|
||||
int typeSize = Marshal.SizeOf(typeof(T));
|
||||
byte[] buffer = reader.ReadBytes(typeSize);
|
||||
// Handle special struct cases
|
||||
if (type == typeof(Guid))
|
||||
return reader.ReadGuid();
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (type == typeof(Half))
|
||||
return reader.ReadHalf();
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
else if (type == typeof(Int128))
|
||||
return reader.ReadInt128();
|
||||
else if (type == typeof(UInt128))
|
||||
return reader.ReadUInt128();
|
||||
#endif
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
var data = (T?)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
|
||||
handle.Free();
|
||||
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
|
||||
return ReadComplexType(reader, type);
|
||||
else if (type.IsValueType && type.IsEnum)
|
||||
return ReadNormalType(reader, Enum.GetUnderlyingType(type));
|
||||
else
|
||||
return ReadNormalType(reader, type);
|
||||
}
|
||||
|
||||
return data;
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the underlying stream
|
||||
/// </summary>
|
||||
private static object? ReadNormalType(BinaryReader reader, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
int typeSize = Marshal.SizeOf(type);
|
||||
byte[] buffer = reader.ReadBytes(typeSize); ;
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
var data = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), type);
|
||||
handle.Free();
|
||||
|
||||
return data;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the underlying stream
|
||||
/// </summary>
|
||||
private static object? ReadComplexType(BinaryReader reader, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to create an instance of the type
|
||||
var instance = Activator.CreateInstance(type);
|
||||
if (instance == null)
|
||||
return null;
|
||||
|
||||
// Get the layout information
|
||||
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
|
||||
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
|
||||
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = reader.BaseStream.Position;
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fields = MarshalHelpers.GetFields(type);
|
||||
|
||||
// Loop through the fields and set them
|
||||
foreach (var fi in fields)
|
||||
{
|
||||
// If we have an explicit layout, move accordingly
|
||||
if (layoutKind == LayoutKind.Explicit)
|
||||
{
|
||||
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
|
||||
reader.BaseStream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
SetField(reader, encoding, fields, instance, fi);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a single field on an object
|
||||
/// </summary>
|
||||
private static void SetField(BinaryReader reader, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
if (fi.FieldType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
var value = ReadStringType(reader, encoding, fi);
|
||||
fi.SetValue(instance, value);
|
||||
}
|
||||
else if (fi.FieldType.IsArray)
|
||||
{
|
||||
var value = ReadArrayType(reader, fields, instance, fi);
|
||||
fi.SetValue(instance, Convert.ChangeType(value, fi.FieldType));
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = reader.ReadType(fi.FieldType);
|
||||
fi.SetValue(instance, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an array type field for an object
|
||||
/// </summary>
|
||||
private static Array ReadArrayType(BinaryReader reader, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
if (marshalAsAttr == null)
|
||||
return new object[0];
|
||||
|
||||
// Get the number of elements expected
|
||||
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
|
||||
if (elementCount < 0)
|
||||
return new object[0];
|
||||
|
||||
// Get the item type for the array
|
||||
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
|
||||
|
||||
// Loop through and build the array
|
||||
Array arr = Array.CreateInstance(elementType, elementCount);
|
||||
for (int i = 0; i < elementCount; i++)
|
||||
{
|
||||
var value = ReadType(reader, elementType);
|
||||
arr.SetValue(value, i);
|
||||
}
|
||||
|
||||
// Return the built array
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a string type field for an object
|
||||
/// </summary>
|
||||
private static string? ReadStringType(BinaryReader reader, Encoding encoding, FieldInfo? fi)
|
||||
{
|
||||
#if NET20 || NET35
|
||||
var attributes = fi?.GetCustomAttributes(typeof(MarshalAsAttribute), true);
|
||||
MarshalAsAttribute? marshalAsAttr;
|
||||
if (attributes == null || attributes.Length == 0)
|
||||
marshalAsAttr = default;
|
||||
else
|
||||
marshalAsAttr = attributes[0] as MarshalAsAttribute;
|
||||
#else
|
||||
var marshalAsAttr = fi?
|
||||
.GetCustomAttributes(typeof(MarshalAsAttribute), true)?
|
||||
.FirstOrDefault() as MarshalAsAttribute;
|
||||
#endif
|
||||
|
||||
switch (marshalAsAttr?.Value)
|
||||
{
|
||||
case UnmanagedType.AnsiBStr:
|
||||
return reader.ReadPrefixedAnsiString();
|
||||
|
||||
case UnmanagedType.BStr:
|
||||
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
|
||||
return reader.ReadPrefixedUnicodeString();
|
||||
|
||||
case UnmanagedType.ByValTStr:
|
||||
int byvalLength = marshalAsAttr!.SizeConst;
|
||||
byte[] byvalBytes = reader.ReadBytes(byvalLength);
|
||||
return encoding.GetString(byvalBytes);
|
||||
|
||||
case UnmanagedType.LPStr:
|
||||
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
|
||||
case null:
|
||||
return reader.ReadNullTerminatedAnsiString();
|
||||
|
||||
#if NET472_OR_GREATER || NETCOREAPP
|
||||
case UnmanagedType.LPUTF8Str:
|
||||
return reader.ReadNullTerminatedUTF8String();
|
||||
#endif
|
||||
|
||||
case UnmanagedType.LPWStr:
|
||||
return reader.ReadNullTerminatedUnicodeString();
|
||||
|
||||
// No other string types are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 1-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull1Byte(BinaryReader reader)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (reader.BaseStream.Position < reader.BaseStream.Length)
|
||||
{
|
||||
byte next = reader.ReadByte();
|
||||
if (next == 0x00)
|
||||
break;
|
||||
|
||||
bytes.Add(next);
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 2-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull2Byte(BinaryReader reader)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (reader.BaseStream.Position < reader.BaseStream.Length)
|
||||
{
|
||||
ushort next = reader.ReadUInt16();
|
||||
if (next == 0x0000)
|
||||
break;
|
||||
|
||||
bytes.AddRange(BitConverter.GetBytes(next));
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 4-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull4Byte(BinaryReader reader)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (reader.BaseStream.Position < reader.BaseStream.Length)
|
||||
{
|
||||
uint next = reader.ReadUInt32();
|
||||
if (next == 0x00000000)
|
||||
break;
|
||||
|
||||
bytes.AddRange(BitConverter.GetBytes(next));
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
#if NET7_0_OR_GREATER
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -15,8 +16,6 @@ namespace SabreTools.IO.Extensions
|
||||
/// TODO: Handle proper negative values for Int24 and Int48
|
||||
public static class BinaryWriterExtensions
|
||||
{
|
||||
#region Write
|
||||
|
||||
/// <inheritdoc cref="BinaryWriter.Write(byte[])"/>
|
||||
/// <remarks>Writes in big-endian format</remarks>
|
||||
public static bool WriteBigEndian(this BinaryWriter writer, byte[] value)
|
||||
@@ -345,11 +344,23 @@ namespace SabreTools.IO.Extensions
|
||||
=> writer.WriteNullTerminatedString(value, Encoding.ASCII);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated Unicode string to the underlying stream
|
||||
/// Write a null-terminated UTF-8 string to the underlying stream
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUTF8String(this BinaryWriter writer, string? value)
|
||||
=> writer.WriteNullTerminatedString(value, Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated UTF-16 (Unicode) string to the underlying stream
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUnicodeString(this BinaryWriter writer, string? value)
|
||||
=> writer.WriteNullTerminatedString(value, Encoding.Unicode);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated UTF-32 string to the underlying stream
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUTF32String(this BinaryWriter writer, string? value)
|
||||
=> writer.WriteNullTerminatedString(value, Encoding.UTF32);
|
||||
|
||||
/// <summary>
|
||||
/// Write a byte-prefixed ASCII string to the underlying stream
|
||||
/// </summary>
|
||||
@@ -413,20 +424,228 @@ namespace SabreTools.IO.Extensions
|
||||
/// <summary>
|
||||
/// Write a <typeparamref name="T"/> to the underlying stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are written by value, not by reference
|
||||
/// - Complex objects are written by value, not by reference
|
||||
/// - Enumeration values are written by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are serialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static bool WriteType<T>(this BinaryWriter writer, T? value)
|
||||
=> writer.WriteType(value, typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Write a <typeparamref name="T"/> to the underlying stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are written by value, not by reference
|
||||
/// - Complex objects are written by value, not by reference
|
||||
/// - Enumeration values are written by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are serialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static bool WriteType(this BinaryWriter writer, object? value, Type type)
|
||||
{
|
||||
// Handle the null case
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
// Handle special struct cases
|
||||
if (type == typeof(Guid))
|
||||
return writer.Write((Guid)value);
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (type == typeof(Half))
|
||||
{
|
||||
writer.Write((Half)value);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
else if (type == typeof(Int128))
|
||||
return writer.Write((Int128)value);
|
||||
else if (type == typeof(UInt128))
|
||||
return writer.Write((UInt128)value);
|
||||
#endif
|
||||
|
||||
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
|
||||
return WriteComplexType(writer, value, type);
|
||||
else if (type.IsValueType && type.IsEnum)
|
||||
return WriteNormalType(writer, value, Enum.GetUnderlyingType(type));
|
||||
else
|
||||
return WriteNormalType(writer, value, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static bool WriteNormalType(BinaryWriter writer, object? value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
int typeSize = Marshal.SizeOf(type);
|
||||
if (value.GetType() != type)
|
||||
value = Convert.ChangeType(value, type);
|
||||
|
||||
var buffer = new byte[typeSize];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
|
||||
handle.Free();
|
||||
|
||||
return WriteFromBuffer(writer, buffer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static bool WriteComplexType(BinaryWriter writer, object? value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
// Get the layout information
|
||||
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
|
||||
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
|
||||
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = writer.BaseStream.Position;
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fields = MarshalHelpers.GetFields(type);
|
||||
|
||||
// Loop through the fields and set them
|
||||
foreach (var fi in fields)
|
||||
{
|
||||
// If we have an explicit layout, move accordingly
|
||||
if (layoutKind == LayoutKind.Explicit)
|
||||
{
|
||||
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
|
||||
writer.BaseStream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (!GetField(writer, encoding, fields, value, fi))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a single field from an object
|
||||
/// </summary>
|
||||
private static bool GetField(BinaryWriter writer, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
if (fi.FieldType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
return WriteStringType(writer, encoding, instance, fi);
|
||||
}
|
||||
else if (fi.FieldType.IsArray)
|
||||
{
|
||||
return WriteArrayType(writer, fields, instance, fi);
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = fi.GetValue(instance);
|
||||
return writer.WriteType(value, fi.FieldType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an array type field from an object
|
||||
/// </summary>
|
||||
private static bool WriteArrayType(BinaryWriter writer, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
if (marshalAsAttr == null)
|
||||
return false;
|
||||
|
||||
int typeSize = Marshal.SizeOf(typeof(T));
|
||||
// Get the array
|
||||
Array? arr = fi.GetValue(instance) as Array;
|
||||
if (arr == null)
|
||||
return false;
|
||||
|
||||
var buffer = new byte[typeSize];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
|
||||
handle.Free();
|
||||
// Get the number of elements expected
|
||||
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
|
||||
if (elementCount < 0)
|
||||
return false;
|
||||
|
||||
return WriteFromBuffer(writer, buffer);
|
||||
// Get the item type for the array
|
||||
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
|
||||
|
||||
// Loop through and write the array
|
||||
for (int i = 0; i < elementCount; i++)
|
||||
{
|
||||
var value = arr.GetValue(i);
|
||||
if (!WriteType(writer, value, elementType))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string type field from an object
|
||||
/// </summary>
|
||||
private static bool WriteStringType(BinaryWriter writer, Encoding encoding, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
string? fieldValue = fi.GetValue(instance) as string;
|
||||
if (fieldValue == null)
|
||||
return true;
|
||||
|
||||
switch (marshalAsAttr?.Value)
|
||||
{
|
||||
case UnmanagedType.AnsiBStr:
|
||||
return writer.WritePrefixedAnsiString(fieldValue);
|
||||
|
||||
case UnmanagedType.BStr:
|
||||
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
|
||||
return writer.WritePrefixedUnicodeString(fieldValue);
|
||||
|
||||
case UnmanagedType.ByValTStr:
|
||||
int byvalLength = marshalAsAttr!.SizeConst;
|
||||
byte[] byvalBytes = encoding.GetBytes(fieldValue);
|
||||
byte[] byvalSizedBytes = new byte[byvalLength];
|
||||
Array.Copy(byvalBytes, byvalSizedBytes, Math.Min(byvalBytes.Length, byvalSizedBytes.Length));
|
||||
writer.Write(byvalSizedBytes);
|
||||
return true;
|
||||
|
||||
case UnmanagedType.LPStr:
|
||||
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
|
||||
case null:
|
||||
return writer.WriteNullTerminatedAnsiString(fieldValue);
|
||||
|
||||
#if NET472_OR_GREATER || NETCOREAPP
|
||||
case UnmanagedType.LPUTF8Str:
|
||||
return writer.WriteNullTerminatedUTF8String(fieldValue);
|
||||
#endif
|
||||
|
||||
case UnmanagedType.LPWStr:
|
||||
return writer.WriteNullTerminatedUnicodeString(fieldValue);
|
||||
|
||||
// No other string types are recognized
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -446,7 +665,5 @@ namespace SabreTools.IO.Extensions
|
||||
writer.Write(value, 0, value.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
#if NET7_0_OR_GREATER
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -453,8 +454,12 @@ namespace SabreTools.IO.Extensions
|
||||
// Short-circuit to explicit implementations
|
||||
if (encoding.Equals(Encoding.ASCII))
|
||||
return content.ReadNullTerminatedAnsiString(ref offset);
|
||||
else if (encoding.Equals(Encoding.UTF8))
|
||||
return content.ReadNullTerminatedUTF8String(ref offset);
|
||||
else if (encoding.Equals(Encoding.Unicode))
|
||||
return content.ReadNullTerminatedUnicodeString(ref offset);
|
||||
else if (encoding.Equals(Encoding.UTF32))
|
||||
return content.ReadNullTerminatedUTF32String(ref offset);
|
||||
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
@@ -479,36 +484,44 @@ namespace SabreTools.IO.Extensions
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
|
||||
List<byte> buffer = [];
|
||||
while (offset < content.Length)
|
||||
{
|
||||
byte ch = content.ReadByteValue(ref offset);
|
||||
buffer.Add(ch);
|
||||
if (ch == '\0')
|
||||
break;
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString([.. buffer]);
|
||||
byte[] buffer = ReadUntilNull1Byte(content, ref offset);
|
||||
return Encoding.ASCII.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated Unicode string from the byte array
|
||||
/// Read a null-terminated UTF-8 string from the byte array
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUTF8String(this byte[] content, ref int offset)
|
||||
{
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = ReadUntilNull1Byte(content, ref offset);
|
||||
return Encoding.UTF8.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated UTF-16 (Unicode) string from the byte array
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUnicodeString(this byte[] content, ref int offset)
|
||||
{
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
|
||||
List<byte> buffer = [];
|
||||
while (offset < content.Length)
|
||||
{
|
||||
byte[] ch = content.ReadBytes(ref offset, 2);
|
||||
buffer.AddRange(ch);
|
||||
if (ch[0] == '\0' && ch[1] == '\0')
|
||||
break;
|
||||
}
|
||||
byte[] buffer = ReadUntilNull2Byte(content, ref offset);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
return Encoding.Unicode.GetString([.. buffer]);
|
||||
/// <summary>
|
||||
/// Read a null-terminated UTF-32 string from the byte array
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUTF32String(this byte[] content, ref int offset)
|
||||
{
|
||||
if (offset >= content.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = ReadUntilNull4Byte(content, ref offset);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -536,10 +549,10 @@ namespace SabreTools.IO.Extensions
|
||||
return null;
|
||||
|
||||
ushort size = content.ReadUInt16(ref offset);
|
||||
if (offset + size >= content.Length)
|
||||
if (offset + (size * 2) >= content.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = content.ReadBytes(ref offset, size);
|
||||
byte[] buffer = content.ReadBytes(ref offset, size * 2);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
@@ -583,18 +596,263 @@ namespace SabreTools.IO.Extensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <typeparamref name="T"/> from the byte array
|
||||
/// Read a <typeparamref name="T"/> from the stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are read by value, not by reference
|
||||
/// - Complex objects are read by value, not by reference
|
||||
/// - Enumeration values are read by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are deserialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static T? ReadType<T>(this byte[] content, ref int offset)
|
||||
=> (T?)content.ReadType(ref offset, typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are read by value, not by reference
|
||||
/// - Complex objects are read by value, not by reference
|
||||
/// - Enumeration values are read by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are deserialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static object? ReadType(this byte[] content, ref int offset, Type type)
|
||||
{
|
||||
int typeSize = Marshal.SizeOf(typeof(T));
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, typeSize);
|
||||
// Handle special struct cases
|
||||
if (type == typeof(Guid))
|
||||
return content.ReadGuid(ref offset);
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (type == typeof(Half))
|
||||
return content.ReadHalf(ref offset);
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
else if (type == typeof(Int128))
|
||||
return content.ReadInt128(ref offset);
|
||||
else if (type == typeof(UInt128))
|
||||
return content.ReadUInt128(ref offset);
|
||||
#endif
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
var data = (T?)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
|
||||
handle.Free();
|
||||
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
|
||||
return ReadComplexType(content, ref offset, type);
|
||||
else if (type.IsValueType && type.IsEnum)
|
||||
return ReadNormalType(content, ref offset, Enum.GetUnderlyingType(type));
|
||||
else
|
||||
return ReadNormalType(content, ref offset, type);
|
||||
}
|
||||
|
||||
return data;
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static object? ReadNormalType(byte[] content, ref int offset, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
int typeSize = Marshal.SizeOf(type);
|
||||
byte[] buffer = ReadToBuffer(content, ref offset, typeSize);
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
var data = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), type);
|
||||
handle.Free();
|
||||
|
||||
return data;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static object? ReadComplexType(byte[] content, ref int offset, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to create an instance of the type
|
||||
var instance = Activator.CreateInstance(type);
|
||||
if (instance == null)
|
||||
return null;
|
||||
|
||||
// Get the layout information
|
||||
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
|
||||
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
|
||||
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
|
||||
|
||||
// Cache the current offset
|
||||
int currentOffset = offset;
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fields = MarshalHelpers.GetFields(type);
|
||||
|
||||
// Loop through the fields and set them
|
||||
foreach (var fi in fields)
|
||||
{
|
||||
// If we have an explicit layout, move accordingly
|
||||
if (layoutKind == LayoutKind.Explicit)
|
||||
{
|
||||
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
|
||||
offset = currentOffset + fieldOffset?.Value ?? 0;
|
||||
}
|
||||
|
||||
SetField(content, ref offset, encoding, fields, instance, fi);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a single field on an object
|
||||
/// </summary>
|
||||
private static void SetField(byte[] content, ref int offset, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
if (fi.FieldType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
var value = ReadStringType(content, ref offset, encoding, instance, fi);
|
||||
fi.SetValue(instance, value);
|
||||
}
|
||||
else if (fi.FieldType.IsArray)
|
||||
{
|
||||
var value = ReadArrayType(content, ref offset, fields, instance, fi);
|
||||
fi.SetValue(instance, Convert.ChangeType(value, fi.FieldType));
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = content.ReadType(ref offset, fi.FieldType);
|
||||
fi.SetValue(instance, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an array type field for an object
|
||||
/// </summary>
|
||||
private static Array ReadArrayType(byte[] content, ref int offset, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
if (marshalAsAttr == null)
|
||||
return new object[0];
|
||||
|
||||
// Get the number of elements expected
|
||||
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
|
||||
if (elementCount < 0)
|
||||
return new object[0];
|
||||
|
||||
// Get the item type for the array
|
||||
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
|
||||
|
||||
// Loop through and build the array
|
||||
Array arr = Array.CreateInstance(elementType, elementCount);
|
||||
for (int i = 0; i < elementCount; i++)
|
||||
{
|
||||
var value = ReadType(content, ref offset, elementType);
|
||||
arr.SetValue(value, i);
|
||||
}
|
||||
|
||||
// Return the built array
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a string type field for an object
|
||||
/// </summary>
|
||||
private static string? ReadStringType(byte[] content, ref int offset, Encoding encoding, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
|
||||
switch (marshalAsAttr?.Value)
|
||||
{
|
||||
case UnmanagedType.AnsiBStr:
|
||||
return content.ReadPrefixedAnsiString(ref offset);
|
||||
|
||||
case UnmanagedType.BStr:
|
||||
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
|
||||
return content.ReadPrefixedUnicodeString(ref offset);
|
||||
|
||||
case UnmanagedType.ByValTStr:
|
||||
int byvalLength = marshalAsAttr!.SizeConst;
|
||||
byte[] byvalBytes = content.ReadBytes(ref offset, byvalLength);
|
||||
return encoding.GetString(byvalBytes);
|
||||
|
||||
case UnmanagedType.LPStr:
|
||||
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
|
||||
case null:
|
||||
return content.ReadNullTerminatedAnsiString(ref offset);
|
||||
|
||||
#if NET472_OR_GREATER || NETCOREAPP
|
||||
case UnmanagedType.LPUTF8Str:
|
||||
return content.ReadNullTerminatedUTF8String(ref offset);
|
||||
#endif
|
||||
|
||||
case UnmanagedType.LPWStr:
|
||||
return content.ReadNullTerminatedUnicodeString(ref offset);
|
||||
|
||||
// No other string types are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 1-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull1Byte(byte[] content, ref int offset)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (offset < content.Length)
|
||||
{
|
||||
byte next = content.ReadByte(ref offset);
|
||||
if (next == 0x00)
|
||||
break;
|
||||
|
||||
bytes.Add(next);
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 2-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull2Byte(byte[] content, ref int offset)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (offset < content.Length)
|
||||
{
|
||||
ushort next = content.ReadUInt16(ref offset);
|
||||
if (next == 0x0000)
|
||||
break;
|
||||
|
||||
bytes.AddRange(BitConverter.GetBytes(next));
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 4-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull4Byte(byte[] content, ref int offset)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (offset < content.Length)
|
||||
{
|
||||
uint next = content.ReadUInt32(ref offset);
|
||||
if (next == 0x00000000)
|
||||
break;
|
||||
|
||||
bytes.AddRange(BitConverter.GetBytes(next));
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -604,7 +862,7 @@ namespace SabreTools.IO.Extensions
|
||||
{
|
||||
// If we have an invalid length
|
||||
if (length < 0)
|
||||
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value");
|
||||
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value, {length} requested");
|
||||
|
||||
// Handle the 0-byte case
|
||||
if (length == 0)
|
||||
@@ -612,7 +870,7 @@ namespace SabreTools.IO.Extensions
|
||||
|
||||
// If there are not enough bytes
|
||||
if (offset + length > content.Length)
|
||||
throw new System.IO.EndOfStreamException(nameof(content));
|
||||
throw new System.IO.EndOfStreamException($"Requested to read {length} bytes from {nameof(content)}, {content.Length - offset} returned");
|
||||
|
||||
// Handle the general case, forcing a read of the correct length
|
||||
byte[] buffer = new byte[length];
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
#if NET7_0_OR_GREATER
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -494,11 +495,23 @@ namespace SabreTools.IO.Extensions
|
||||
=> content.WriteNullTerminatedString(ref offset, value, Encoding.ASCII);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated Unicode string to the byte array
|
||||
/// Write a null-terminated UTF-8 string to the byte array
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUTF8String(this byte[] content, ref int offset, string? value)
|
||||
=> content.WriteNullTerminatedString(ref offset, value, Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated UTF-16 (Unicode) string to the byte array
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUnicodeString(this byte[] content, ref int offset, string? value)
|
||||
=> content.WriteNullTerminatedString(ref offset, value, Encoding.Unicode);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated UTF-32 string to the byte array
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUTF32String(this byte[] content, ref int offset, string? value)
|
||||
=> content.WriteNullTerminatedString(ref offset, value, Encoding.UTF32);
|
||||
|
||||
/// <summary>
|
||||
/// Write a byte-prefixed ASCII string to the byte array
|
||||
/// </summary>
|
||||
@@ -564,20 +577,224 @@ namespace SabreTools.IO.Extensions
|
||||
/// <summary>
|
||||
/// Write a <typeparamref name="T"/> to the byte array
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are written by value, not by reference
|
||||
/// - Complex objects are written by value, not by reference
|
||||
/// - Enumeration values are written by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are serialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static bool WriteType<T>(this byte[] content, ref int offset, T? value)
|
||||
=> content.WriteType(ref offset, value, typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Write a <typeparamref name="T"/> to the byte array
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are written by value, not by reference
|
||||
/// - Complex objects are written by value, not by reference
|
||||
/// - Enumeration values are written by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are serialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static bool WriteType(this byte[] content, ref int offset, object? value, Type type)
|
||||
{
|
||||
// Handle the null case
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
// Handle special struct cases
|
||||
if (type == typeof(Guid))
|
||||
return content.Write(ref offset, (Guid)value);
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (type == typeof(Half))
|
||||
return content.Write(ref offset, (Half)value);
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
else if (type == typeof(Int128))
|
||||
return content.Write(ref offset, (Int128)value);
|
||||
else if (type == typeof(UInt128))
|
||||
return content.Write(ref offset, (UInt128)value);
|
||||
#endif
|
||||
|
||||
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
|
||||
return WriteComplexType(content, ref offset, value, type);
|
||||
else if (type.IsValueType && type.IsEnum)
|
||||
return WriteNormalType(content, ref offset, value, Enum.GetUnderlyingType(type));
|
||||
else
|
||||
return WriteNormalType(content, ref offset, value, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static bool WriteNormalType(byte[] content, ref int offset, object? value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
int typeSize = Marshal.SizeOf(type);
|
||||
if (value.GetType() != type)
|
||||
value = Convert.ChangeType(value, type);
|
||||
|
||||
var buffer = new byte[typeSize];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
|
||||
handle.Free();
|
||||
|
||||
return WriteFromBuffer(content, ref offset, buffer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static bool WriteComplexType(byte[] content, ref int offset, object? value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
// Get the layout information
|
||||
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
|
||||
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
|
||||
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
|
||||
|
||||
// Cache the current offset
|
||||
int currentOffset = offset;
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fields = MarshalHelpers.GetFields(type);
|
||||
|
||||
// Loop through the fields and set them
|
||||
foreach (var fi in fields)
|
||||
{
|
||||
// If we have an explicit layout, move accordingly
|
||||
if (layoutKind == LayoutKind.Explicit)
|
||||
{
|
||||
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
|
||||
offset = currentOffset + fieldOffset?.Value ?? 0;
|
||||
}
|
||||
|
||||
if (!GetField(content, ref offset, encoding, fields, value, fi))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a single field from an object
|
||||
/// </summary>
|
||||
private static bool GetField(byte[] content, ref int offset, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
if (fi.FieldType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
return WriteStringType(content, ref offset, encoding, instance, fi);
|
||||
}
|
||||
else if (fi.FieldType.IsArray)
|
||||
{
|
||||
return WriteArrayType(content, ref offset, fields, instance, fi);
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = fi.GetValue(instance);
|
||||
return content.WriteType(ref offset, value, fi.FieldType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an array type field from an object
|
||||
/// </summary>
|
||||
private static bool WriteArrayType(byte[] content, ref int offset, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
if (marshalAsAttr == null)
|
||||
return false;
|
||||
|
||||
int typeSize = Marshal.SizeOf(typeof(T));
|
||||
// Get the array
|
||||
Array? arr = fi.GetValue(instance) as Array;
|
||||
if (arr == null)
|
||||
return false;
|
||||
|
||||
var buffer = new byte[typeSize];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
|
||||
handle.Free();
|
||||
// Get the number of elements expected
|
||||
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
|
||||
if (elementCount < 0)
|
||||
return false;
|
||||
|
||||
return WriteFromBuffer(content, ref offset, buffer);
|
||||
// Get the item type for the array
|
||||
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
|
||||
|
||||
// Loop through and write the array
|
||||
for (int i = 0; i < elementCount; i++)
|
||||
{
|
||||
var value = arr.GetValue(i);
|
||||
if (!WriteType(content, ref offset, value, elementType))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string type field from an object
|
||||
/// </summary>
|
||||
private static bool WriteStringType(byte[] content, ref int offset, Encoding encoding, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
string? fieldValue = fi.GetValue(instance) as string;
|
||||
if (fieldValue == null)
|
||||
return true;
|
||||
|
||||
switch (marshalAsAttr?.Value)
|
||||
{
|
||||
case UnmanagedType.AnsiBStr:
|
||||
return content.WritePrefixedAnsiString(ref offset, fieldValue);
|
||||
|
||||
case UnmanagedType.BStr:
|
||||
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
|
||||
return content.WritePrefixedUnicodeString(ref offset, fieldValue);
|
||||
|
||||
case UnmanagedType.ByValTStr:
|
||||
int byvalLength = marshalAsAttr!.SizeConst;
|
||||
byte[] byvalBytes = encoding.GetBytes(fieldValue);
|
||||
byte[] byvalSizedBytes = new byte[byvalLength];
|
||||
Array.Copy(byvalBytes, byvalSizedBytes, Math.Min(byvalBytes.Length, byvalSizedBytes.Length));
|
||||
return content.Write(ref offset, byvalSizedBytes);
|
||||
|
||||
case UnmanagedType.LPStr:
|
||||
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
|
||||
case null:
|
||||
return content.WriteNullTerminatedAnsiString(ref offset, fieldValue);
|
||||
|
||||
#if NET472_OR_GREATER || NETCOREAPP
|
||||
case UnmanagedType.LPUTF8Str:
|
||||
return content.WriteNullTerminatedUTF8String(ref offset, fieldValue);
|
||||
#endif
|
||||
|
||||
case UnmanagedType.LPWStr:
|
||||
return content.WriteNullTerminatedUnicodeString(ref offset, fieldValue);
|
||||
|
||||
// No other string types are recognized
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.IO.Extensions
|
||||
@@ -10,7 +11,15 @@ namespace SabreTools.IO.Extensions
|
||||
public static IEnumerable<T> SafeEnumerate<T>(this IEnumerable<T> enumerable)
|
||||
{
|
||||
// Get the enumerator for the enumerable
|
||||
var enumerator = enumerable.GetEnumerator();
|
||||
IEnumerator<T> enumerator;
|
||||
try
|
||||
{
|
||||
enumerator = enumerable.GetEnumerator();
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Iterate through and absorb any errors
|
||||
while (true)
|
||||
@@ -21,6 +30,16 @@ namespace SabreTools.IO.Extensions
|
||||
{
|
||||
moved = enumerator.MoveNext();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Specific case for collections that were modified
|
||||
yield break;
|
||||
}
|
||||
catch (System.IO.IOException ex) when (ex.Message.Contains("The file or directory is corrupted and unreadable."))
|
||||
{
|
||||
// Specific case we can't circumvent
|
||||
yield break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
using System;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Extensions
|
||||
@@ -118,13 +123,28 @@ namespace SabreTools.IO.Extensions
|
||||
return null;
|
||||
|
||||
// If it does and it is empty, return a blank enumerable
|
||||
#if NET20 || NET35
|
||||
if (new List<string>(root!.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories)).Count == 0)
|
||||
#else
|
||||
if (!root!.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories).Any())
|
||||
#endif
|
||||
return [];
|
||||
|
||||
// Otherwise, get the complete list
|
||||
#if NET20 || NET35
|
||||
var empty = new List<string>();
|
||||
foreach (var dir in root!.SafeEnumerateDirectories("*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (new List<string>(dir!.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories)).Count == 0)
|
||||
empty.Add(dir);
|
||||
}
|
||||
|
||||
return empty;
|
||||
#else
|
||||
return root!.SafeEnumerateDirectories("*", SearchOption.AllDirectories)
|
||||
.Where(dir => !dir.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories).Any())
|
||||
.ToList();
|
||||
#endif
|
||||
}
|
||||
|
||||
#region Safe Directory Enumeration
|
||||
@@ -309,68 +329,296 @@ namespace SabreTools.IO.Extensions
|
||||
/// <remarks>Calls <see cref="SafeGetFileSystemEntries(string, string, SearchOption)"/> implementation</remarks>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
|
||||
=> path.SafeGetFileSystemEntries(searchPattern, searchOption);
|
||||
#else
|
||||
#elif NET40_OR_GREATER
|
||||
/// <inheritdoc cref="Directory.EnumerateDirectories(string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
|
||||
{
|
||||
var enumerable = Directory.EnumerateDirectories(path);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateDirectories(path);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
|
||||
{
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string, SearchOption)"/>
|
||||
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern, searchOption);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern, searchOption);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFiles(string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFiles(this string path)
|
||||
{
|
||||
var enumerable = Directory.EnumerateFiles(path);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateFiles(path);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFiles(string, string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
|
||||
{
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFiles(string, string, SearchOption)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern, searchOption);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern, searchOption);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
|
||||
{
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
|
||||
{
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string, SearchOption)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, searchOption);
|
||||
return enumerable.SafeEnumerate();
|
||||
try
|
||||
{
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, searchOption);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// <inheritdoc cref="Directory.EnumerateDirectories(string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateDirectories(this string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
string searchPattern = "*";
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly;
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly;
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateDirectories(string, string, SearchOption)"/>
|
||||
public static IEnumerable<string> SafeEnumerateDirectories(this string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
try
|
||||
{
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateDirectories(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFiles(string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFiles(this string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
string searchPattern = "*";
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly;
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFiles(string, string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly;
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFiles(string, string, SearchOption)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFiles(this string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
try
|
||||
{
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateFiles(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
string searchPattern = "*";
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly;
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly;
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Directory.EnumerateFileSystemEntries(string, string, SearchOption)"/>
|
||||
public static IEnumerable<string> SafeEnumerateFileSystemEntries(this string path, string searchPattern, SearchOption searchOption)
|
||||
{
|
||||
try
|
||||
{
|
||||
var enumerationOptions = FromSearchOption(searchOption);
|
||||
enumerationOptions.IgnoreInaccessible = true;
|
||||
var enumerable = Directory.EnumerateFileSystemEntries(path, searchPattern, enumerationOptions);
|
||||
return enumerable.SafeEnumerate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="EnumerationOptions" /> class with the recommended default options.</summary>
|
||||
/// <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/EnumerationOptions.cs#L42"</remarks>
|
||||
private static EnumerationOptions FromSearchOption(SearchOption searchOption)
|
||||
{
|
||||
if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories))
|
||||
throw new ArgumentOutOfRangeException(nameof(searchOption));
|
||||
|
||||
return searchOption == SearchOption.AllDirectories
|
||||
? new EnumerationOptions { RecurseSubdirectories = true, MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false }
|
||||
: new EnumerationOptions { MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = false };
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
192
SabreTools.IO/Extensions/MarshalHelpers.cs
Normal file
192
SabreTools.IO/Extensions/MarshalHelpers.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Common methods for use during marshalling
|
||||
/// </summary>
|
||||
internal static class MarshalHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Get an attribute of the requested type
|
||||
/// </summary>
|
||||
public static T? GetAttribute<T>(FieldInfo? fi) where T : Attribute
|
||||
{
|
||||
// If the field info is invalid
|
||||
if (fi == null)
|
||||
return null;
|
||||
|
||||
// Get all matching attributes
|
||||
var attributes = fi.GetCustomAttributes(typeof(T), true);
|
||||
if (attributes == null || attributes.Length == 0)
|
||||
return null;
|
||||
|
||||
// Get the first attribute that matches
|
||||
return attributes[0] as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an attribute of the requested type
|
||||
/// </summary>
|
||||
public static T? GetAttribute<T>(Type? type) where T : Attribute
|
||||
{
|
||||
// If the field info is invalid
|
||||
if (type == null)
|
||||
return null;
|
||||
|
||||
// Get all matching attributes
|
||||
var attributes = type.GetCustomAttributes(typeof(T), true);
|
||||
if (attributes == null || attributes.Length == 0)
|
||||
return null;
|
||||
|
||||
// Get the first attribute that matches
|
||||
return attributes[0] as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the layout kind for a type
|
||||
/// </summary>
|
||||
public static LayoutKind DetermineLayoutKind(StructLayoutAttribute? layoutAttr, Type type)
|
||||
{
|
||||
LayoutKind layoutKind = LayoutKind.Auto;
|
||||
|
||||
if (layoutAttr != null)
|
||||
layoutKind = layoutAttr.Value;
|
||||
else if (type.IsAutoLayout)
|
||||
layoutKind = LayoutKind.Auto;
|
||||
else if (type.IsExplicitLayout)
|
||||
layoutKind = LayoutKind.Explicit;
|
||||
else if (type.IsLayoutSequential)
|
||||
layoutKind = LayoutKind.Sequential;
|
||||
|
||||
return layoutKind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the encoding for a type
|
||||
/// </summary>
|
||||
public static Encoding DetermineEncoding(StructLayoutAttribute? layoutAttr)
|
||||
{
|
||||
return layoutAttr?.CharSet switch
|
||||
{
|
||||
CharSet.None => Encoding.ASCII,
|
||||
CharSet.Ansi => Encoding.ASCII,
|
||||
CharSet.Unicode => Encoding.Unicode,
|
||||
CharSet.Auto => Encoding.ASCII, // UTF-8 on Unix
|
||||
_ => Encoding.ASCII,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the parent hierarchy for a given type
|
||||
/// </summary>
|
||||
/// <remarks>Returns the highest parent as the first element</remarks>
|
||||
public static IEnumerable<Type> DetermineTypeLineage(Type type)
|
||||
{
|
||||
var lineage = new List<Type>();
|
||||
while (type != typeof(object) && type != typeof(ValueType))
|
||||
{
|
||||
lineage.Add(type);
|
||||
type = type.BaseType ?? typeof(object);
|
||||
}
|
||||
|
||||
lineage.Reverse();
|
||||
return lineage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an ordered set of fields from a type
|
||||
/// </summary>
|
||||
/// <remarks>Returns fields from the parents before fields from the type</remarks>
|
||||
public static FieldInfo[] GetFields(Type type)
|
||||
{
|
||||
// Get the type hierarchy for ensuring serialization order
|
||||
var lineage = DetermineTypeLineage(type);
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fieldsList = new List<FieldInfo>();
|
||||
foreach (var nextType in lineage)
|
||||
{
|
||||
var nextFields = nextType.GetFields();
|
||||
foreach (var field in nextFields)
|
||||
{
|
||||
#if NET20 || NET35
|
||||
bool any = false;
|
||||
foreach (var f in fieldsList)
|
||||
{
|
||||
if (f.Name == field.Name && f.FieldType == field.FieldType)
|
||||
{
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!any)
|
||||
#else
|
||||
if (!fieldsList.Any(f => f.Name == field.Name && f.FieldType == field.FieldType))
|
||||
#endif
|
||||
fieldsList.Add(field);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. fieldsList];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the expected array size for a field
|
||||
/// </summary>
|
||||
/// <returns>Array size on success, -1 on failure</returns>
|
||||
public static int GetArrayElementCount(MarshalAsAttribute marshalAsAttr, FieldInfo[] fields, object instance)
|
||||
{
|
||||
int elementCount = -1;
|
||||
if (marshalAsAttr.Value == UnmanagedType.ByValArray)
|
||||
{
|
||||
elementCount = marshalAsAttr.SizeConst;
|
||||
}
|
||||
else if (marshalAsAttr.Value == UnmanagedType.LPArray)
|
||||
{
|
||||
elementCount = marshalAsAttr.SizeConst;
|
||||
if (marshalAsAttr.SizeParamIndex >= 0)
|
||||
elementCount = GetLPArraySizeFromField(marshalAsAttr, fields, instance);
|
||||
}
|
||||
|
||||
return elementCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the expected LPArray size from a field
|
||||
/// </summary>
|
||||
public static int GetLPArraySizeFromField(MarshalAsAttribute marshalAsAttr, FieldInfo[] fields, object instance)
|
||||
{
|
||||
// If the index is invalid
|
||||
if (marshalAsAttr.SizeParamIndex < 0)
|
||||
return -1;
|
||||
|
||||
// Get the size field
|
||||
var sizeField = fields[marshalAsAttr.SizeParamIndex];
|
||||
if (sizeField == null)
|
||||
return -1;
|
||||
|
||||
// Cast based on the field type
|
||||
return sizeField.GetValue(instance) switch
|
||||
{
|
||||
sbyte val => val,
|
||||
byte val => val,
|
||||
short val => val,
|
||||
ushort val => val,
|
||||
int val => val,
|
||||
uint val => (int)val,
|
||||
long val => (int)val,
|
||||
ulong val => (int)val,
|
||||
_ => -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
#if NET7_0_OR_GREATER
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -437,8 +438,12 @@ namespace SabreTools.IO.Extensions
|
||||
// Short-circuit to explicit implementations
|
||||
if (encoding.Equals(Encoding.ASCII))
|
||||
return stream.ReadNullTerminatedAnsiString();
|
||||
else if (encoding.Equals(Encoding.UTF8))
|
||||
return stream.ReadNullTerminatedUTF8String();
|
||||
else if (encoding.Equals(Encoding.Unicode))
|
||||
return stream.ReadNullTerminatedUnicodeString();
|
||||
else if (encoding.Equals(Encoding.UTF32))
|
||||
return stream.ReadNullTerminatedUTF32String();
|
||||
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
@@ -463,36 +468,44 @@ namespace SabreTools.IO.Extensions
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
List<byte> buffer = [];
|
||||
while (stream.Position < stream.Length)
|
||||
{
|
||||
byte ch = stream.ReadByteValue();
|
||||
buffer.Add(ch);
|
||||
if (ch == '\0')
|
||||
break;
|
||||
}
|
||||
|
||||
return Encoding.ASCII.GetString([.. buffer]);
|
||||
byte[] buffer = ReadUntilNull1Byte(stream);
|
||||
return Encoding.ASCII.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated Unicode string from the stream
|
||||
/// Read a null-terminated UTF-8 string from the stream
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUTF8String(this Stream stream)
|
||||
{
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = ReadUntilNull1Byte(stream);
|
||||
return Encoding.UTF8.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a null-terminated UTF-16 (Unicode) string from the stream
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUnicodeString(this Stream stream)
|
||||
{
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
List<byte> buffer = [];
|
||||
while (stream.Position < stream.Length)
|
||||
{
|
||||
byte[] ch = stream.ReadBytes(2);
|
||||
buffer.AddRange(ch);
|
||||
if (ch[0] == '\0' && ch[1] == '\0')
|
||||
break;
|
||||
}
|
||||
byte[] buffer = ReadUntilNull2Byte(stream);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
return Encoding.Unicode.GetString([.. buffer]);
|
||||
/// <summary>
|
||||
/// Read a null-terminated UTF-32 string from the stream
|
||||
/// </summary>
|
||||
public static string? ReadNullTerminatedUTF32String(this Stream stream)
|
||||
{
|
||||
if (stream.Position >= stream.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = ReadUntilNull4Byte(stream);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -520,10 +533,10 @@ namespace SabreTools.IO.Extensions
|
||||
return null;
|
||||
|
||||
ushort size = stream.ReadUInt16();
|
||||
if (stream.Position + size >= stream.Length)
|
||||
if (stream.Position + (size * 2) >= stream.Length)
|
||||
return null;
|
||||
|
||||
byte[] buffer = stream.ReadBytes(size);
|
||||
byte[] buffer = stream.ReadBytes(size * 2);
|
||||
return Encoding.Unicode.GetString(buffer);
|
||||
}
|
||||
|
||||
@@ -569,16 +582,261 @@ namespace SabreTools.IO.Extensions
|
||||
/// <summary>
|
||||
/// Read a <typeparamref name="T"/> from the stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are read by value, not by reference
|
||||
/// - Complex objects are read by value, not by reference
|
||||
/// - Enumeration values are read by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are deserialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static T? ReadType<T>(this Stream stream)
|
||||
=> (T?)stream.ReadType(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are read by value, not by reference
|
||||
/// - Complex objects are read by value, not by reference
|
||||
/// - Enumeration values are read by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are deserialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static object? ReadType(this Stream stream, Type type)
|
||||
{
|
||||
int typeSize = Marshal.SizeOf(typeof(T));
|
||||
byte[] buffer = ReadToBuffer(stream, typeSize);
|
||||
// Handle special struct cases
|
||||
if (type == typeof(Guid))
|
||||
return stream.ReadGuid();
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (type == typeof(Half))
|
||||
return stream.ReadHalf();
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
else if (type == typeof(Int128))
|
||||
return stream.ReadInt128();
|
||||
else if (type == typeof(UInt128))
|
||||
return stream.ReadUInt128();
|
||||
#endif
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
var data = (T?)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
|
||||
handle.Free();
|
||||
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
|
||||
return ReadComplexType(stream, type);
|
||||
else if (type.IsValueType && type.IsEnum)
|
||||
return ReadNormalType(stream, Enum.GetUnderlyingType(type));
|
||||
else
|
||||
return ReadNormalType(stream, type);
|
||||
}
|
||||
|
||||
return data;
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static object? ReadNormalType(Stream stream, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
int typeSize = Marshal.SizeOf(type);
|
||||
byte[] buffer = ReadToBuffer(stream, typeSize);
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
var data = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), type);
|
||||
handle.Free();
|
||||
|
||||
return data;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static object? ReadComplexType(Stream stream, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to create an instance of the type
|
||||
var instance = Activator.CreateInstance(type);
|
||||
if (instance == null)
|
||||
return null;
|
||||
|
||||
// Get the layout information
|
||||
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
|
||||
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
|
||||
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = stream.Position;
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fields = MarshalHelpers.GetFields(type);
|
||||
|
||||
// Loop through the fields and set them
|
||||
foreach (var fi in fields)
|
||||
{
|
||||
// If we have an explicit layout, move accordingly
|
||||
if (layoutKind == LayoutKind.Explicit)
|
||||
{
|
||||
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
|
||||
stream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
SetField(stream, encoding, fields, instance, fi);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a single field on an object
|
||||
/// </summary>
|
||||
private static void SetField(Stream stream, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
if (fi.FieldType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
var value = ReadStringType(stream, encoding, instance, fi);
|
||||
fi.SetValue(instance, value);
|
||||
}
|
||||
else if (fi.FieldType.IsArray)
|
||||
{
|
||||
var value = ReadArrayType(stream, fields, instance, fi);
|
||||
fi.SetValue(instance, Convert.ChangeType(value, fi.FieldType));
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = stream.ReadType(fi.FieldType);
|
||||
fi.SetValue(instance, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an array type field for an object
|
||||
/// </summary>
|
||||
private static Array ReadArrayType(Stream stream, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
if (marshalAsAttr == null)
|
||||
return new object[0];
|
||||
|
||||
// Get the number of elements expected
|
||||
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
|
||||
if (elementCount < 0)
|
||||
return new object[0];
|
||||
|
||||
// Get the item type for the array
|
||||
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
|
||||
|
||||
// Loop through and build the array
|
||||
Array arr = Array.CreateInstance(elementType, elementCount);
|
||||
for (int i = 0; i < elementCount; i++)
|
||||
{
|
||||
var value = ReadType(stream, elementType);
|
||||
arr.SetValue(value, i);
|
||||
}
|
||||
|
||||
// Return the built array
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a string type field for an object
|
||||
/// </summary>
|
||||
private static string? ReadStringType(Stream stream, Encoding encoding, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
|
||||
switch (marshalAsAttr?.Value)
|
||||
{
|
||||
case UnmanagedType.AnsiBStr:
|
||||
return stream.ReadPrefixedAnsiString();
|
||||
|
||||
case UnmanagedType.BStr:
|
||||
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
|
||||
return stream.ReadPrefixedUnicodeString();
|
||||
|
||||
case UnmanagedType.ByValTStr:
|
||||
int byvalLength = marshalAsAttr!.SizeConst;
|
||||
byte[] byvalBytes = stream.ReadBytes(byvalLength);
|
||||
return encoding.GetString(byvalBytes);
|
||||
|
||||
case UnmanagedType.LPStr:
|
||||
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
|
||||
case null:
|
||||
return stream.ReadNullTerminatedAnsiString();
|
||||
|
||||
#if NET472_OR_GREATER || NETCOREAPP
|
||||
case UnmanagedType.LPUTF8Str:
|
||||
return stream.ReadNullTerminatedUTF8String();
|
||||
#endif
|
||||
|
||||
case UnmanagedType.LPWStr:
|
||||
return stream.ReadNullTerminatedUnicodeString();
|
||||
|
||||
// No other string types are recognized
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 1-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull1Byte(Stream stream)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (stream.Position < stream.Length)
|
||||
{
|
||||
byte next = stream.ReadByteValue();
|
||||
if (next == 0x00)
|
||||
break;
|
||||
|
||||
bytes.Add(next);
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 2-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull2Byte(Stream stream)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (stream.Position < stream.Length)
|
||||
{
|
||||
ushort next = stream.ReadUInt16();
|
||||
if (next == 0x0000)
|
||||
break;
|
||||
|
||||
bytes.AddRange(BitConverter.GetBytes(next));
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read bytes until a 4-byte null terminator is found
|
||||
/// </summary>
|
||||
private static byte[] ReadUntilNull4Byte(Stream stream)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
while (stream.Position < stream.Length)
|
||||
{
|
||||
uint next = stream.ReadUInt32();
|
||||
if (next == 0x00000000)
|
||||
break;
|
||||
|
||||
bytes.AddRange(BitConverter.GetBytes(next));
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -588,7 +846,7 @@ namespace SabreTools.IO.Extensions
|
||||
{
|
||||
// If we have an invalid length
|
||||
if (length < 0)
|
||||
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value");
|
||||
throw new ArgumentOutOfRangeException($"{nameof(length)} must be 0 or a positive value, {length} requested");
|
||||
|
||||
// Handle the 0-byte case
|
||||
if (length == 0)
|
||||
@@ -598,7 +856,7 @@ namespace SabreTools.IO.Extensions
|
||||
byte[] buffer = new byte[length];
|
||||
int read = stream.Read(buffer, 0, length);
|
||||
if (read < length)
|
||||
throw new EndOfStreamException(nameof(stream));
|
||||
throw new EndOfStreamException($"Requested to read {length} bytes from {nameof(stream)}, {read} returned");
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
#if NET7_0_OR_GREATER
|
||||
using System.Numerics;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -495,12 +496,24 @@ namespace SabreTools.IO.Extensions
|
||||
=> stream.WriteNullTerminatedString(value, Encoding.ASCII);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated Unicode string to the stream
|
||||
/// Write a null-terminated UTF-8 string to the stream
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUTF8String(this Stream stream, string? value)
|
||||
=> stream.WriteNullTerminatedString(value, Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated UTF-16 (Unicode) string to the stream
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUnicodeString(this Stream stream, string? value)
|
||||
=> stream.WriteNullTerminatedString(value, Encoding.Unicode);
|
||||
|
||||
/// <summary>
|
||||
/// Write a null-terminated UTF-32 string to the stream
|
||||
/// </summary>
|
||||
public static bool WriteNullTerminatedUTF32String(this Stream stream, string? value)
|
||||
=> stream.WriteNullTerminatedString(value, Encoding.UTF32);
|
||||
|
||||
//// <summary>
|
||||
/// Write a byte-prefixed ASCII string to the stream
|
||||
/// </summary>
|
||||
public static bool WritePrefixedAnsiString(this Stream stream, string? value)
|
||||
@@ -565,20 +578,225 @@ namespace SabreTools.IO.Extensions
|
||||
/// <summary>
|
||||
/// Write a <typeparamref name="T"/> to the stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are written by value, not by reference
|
||||
/// - Complex objects are written by value, not by reference
|
||||
/// - Enumeration values are written by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are serialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static bool WriteType<T>(this Stream stream, T? value)
|
||||
=> stream.WriteType(value, typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Write a <typeparamref name="T"/> to the stream
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is different than standard marshalling in a few notable ways:
|
||||
/// - Strings are written by value, not by reference
|
||||
/// - Complex objects are written by value, not by reference
|
||||
/// - Enumeration values are written by the underlying value type
|
||||
/// - Arrays of the above are handled sequentially as above
|
||||
/// - Inherited fields from parents are serialized BEFORE fields in the child
|
||||
/// </remarks>
|
||||
public static bool WriteType(this Stream stream, object? value, Type type)
|
||||
{
|
||||
// Handle the null case
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
// Handle special struct cases
|
||||
if (type == typeof(Guid))
|
||||
return stream.Write((Guid)value);
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (type == typeof(Half))
|
||||
return stream.Write((Half)value);
|
||||
#endif
|
||||
#if NET7_0_OR_GREATER
|
||||
else if (type == typeof(Int128))
|
||||
return stream.Write((Int128)value);
|
||||
else if (type == typeof(UInt128))
|
||||
return stream.Write((UInt128)value);
|
||||
#endif
|
||||
|
||||
if (type.IsClass || (type.IsValueType && !type.IsEnum && !type.IsPrimitive))
|
||||
return WriteComplexType(stream, value, type);
|
||||
else if (type.IsValueType && type.IsEnum)
|
||||
return WriteNormalType(stream, value, Enum.GetUnderlyingType(type));
|
||||
else
|
||||
return WriteNormalType(stream, value, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static bool WriteNormalType(Stream stream, object? value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
int typeSize = Marshal.SizeOf(type);
|
||||
if (value.GetType() != type)
|
||||
value = Convert.ChangeType(value, type);
|
||||
|
||||
var buffer = new byte[typeSize];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
|
||||
handle.Free();
|
||||
|
||||
return WriteFromBuffer(stream, buffer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <paramref name="type"/> from the stream
|
||||
/// </summary>
|
||||
private static bool WriteComplexType(Stream stream, object? value, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Null values cannot be written
|
||||
if (value == null)
|
||||
return true;
|
||||
|
||||
// Get the layout information
|
||||
var layoutAttr = MarshalHelpers.GetAttribute<StructLayoutAttribute>(type);
|
||||
LayoutKind layoutKind = MarshalHelpers.DetermineLayoutKind(layoutAttr, type);
|
||||
Encoding encoding = MarshalHelpers.DetermineEncoding(layoutAttr);
|
||||
|
||||
// Cache the current offset
|
||||
long currentOffset = stream.Position;
|
||||
|
||||
// Generate the fields by parent first
|
||||
var fields = MarshalHelpers.GetFields(type);
|
||||
|
||||
// Loop through the fields and set them
|
||||
foreach (var fi in fields)
|
||||
{
|
||||
// If we have an explicit layout, move accordingly
|
||||
if (layoutKind == LayoutKind.Explicit)
|
||||
{
|
||||
var fieldOffset = MarshalHelpers.GetAttribute<FieldOffsetAttribute>(fi);
|
||||
stream.Seek(currentOffset + fieldOffset?.Value ?? 0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
if (!GetField(stream, encoding, fields, value, fi))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a single field from an object
|
||||
/// </summary>
|
||||
private static bool GetField(Stream stream, Encoding encoding, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
if (fi.FieldType.IsAssignableFrom(typeof(string)))
|
||||
{
|
||||
return WriteStringType(stream, encoding, instance, fi);
|
||||
}
|
||||
else if (fi.FieldType.IsArray)
|
||||
{
|
||||
return WriteArrayType(stream, fields, instance, fi);
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = fi.GetValue(instance);
|
||||
return stream.WriteType(value, fi.FieldType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an array type field from an object
|
||||
/// </summary>
|
||||
private static bool WriteArrayType(Stream stream, FieldInfo[] fields, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
if (marshalAsAttr == null)
|
||||
return false;
|
||||
|
||||
int typeSize = Marshal.SizeOf(typeof(T));
|
||||
// Get the array
|
||||
Array? arr = fi.GetValue(instance) as Array;
|
||||
if (arr == null)
|
||||
return false;
|
||||
|
||||
var buffer = new byte[typeSize];
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
Marshal.StructureToPtr(value, handle.AddrOfPinnedObject(), false);
|
||||
handle.Free();
|
||||
// Get the number of elements expected
|
||||
int elementCount = MarshalHelpers.GetArrayElementCount(marshalAsAttr, fields, instance);
|
||||
if (elementCount < 0)
|
||||
return false;
|
||||
|
||||
return WriteFromBuffer(stream, buffer);
|
||||
// Get the item type for the array
|
||||
Type elementType = fi.FieldType.GetElementType() ?? typeof(object);
|
||||
|
||||
// Loop through and write the array
|
||||
for (int i = 0; i < elementCount; i++)
|
||||
{
|
||||
var value = arr.GetValue(i);
|
||||
if (!WriteType(stream, value, elementType))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the built array
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string type field from an object
|
||||
/// </summary>
|
||||
private static bool WriteStringType(Stream stream, Encoding encoding, object instance, FieldInfo fi)
|
||||
{
|
||||
var marshalAsAttr = MarshalHelpers.GetAttribute<MarshalAsAttribute>(fi);
|
||||
string? fieldValue = fi.GetValue(instance) as string;
|
||||
if (fieldValue == null)
|
||||
return true;
|
||||
|
||||
switch (marshalAsAttr?.Value)
|
||||
{
|
||||
case UnmanagedType.AnsiBStr:
|
||||
return stream.WritePrefixedAnsiString(fieldValue);
|
||||
|
||||
case UnmanagedType.BStr:
|
||||
case UnmanagedType.TBStr: // Technically distinct; returns char[] instead
|
||||
return stream.WritePrefixedUnicodeString(fieldValue);
|
||||
|
||||
case UnmanagedType.ByValTStr:
|
||||
int byvalLength = marshalAsAttr!.SizeConst;
|
||||
byte[] byvalBytes = encoding.GetBytes(fieldValue);
|
||||
byte[] byvalSizedBytes = new byte[byvalLength];
|
||||
Array.Copy(byvalBytes, byvalSizedBytes, Math.Min(byvalBytes.Length, byvalSizedBytes.Length));
|
||||
return Write(stream, byvalSizedBytes);
|
||||
|
||||
case UnmanagedType.LPStr:
|
||||
case UnmanagedType.LPTStr: // Technically distinct; possibly not null-terminated
|
||||
case null:
|
||||
return stream.WriteNullTerminatedAnsiString(fieldValue);
|
||||
|
||||
#if NET472_OR_GREATER || NETCOREAPP
|
||||
case UnmanagedType.LPUTF8Str:
|
||||
return stream.WriteNullTerminatedUTF8String(fieldValue);
|
||||
#endif
|
||||
|
||||
case UnmanagedType.LPWStr:
|
||||
return stream.WriteNullTerminatedUnicodeString(fieldValue);
|
||||
|
||||
// No other string types are recognized
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Text;
|
||||
using SabreTools.IO.Readers;
|
||||
using SabreTools.IO.Writers;
|
||||
@@ -179,7 +181,17 @@ namespace SabreTools.IO
|
||||
using IniWriter writer = new(stream, Encoding.UTF8);
|
||||
|
||||
// Order the dictionary by keys to link sections together
|
||||
#if NET20 || NET35
|
||||
var orderedKeyValuePairs = new List<KeyValuePair<string, string?>>();
|
||||
foreach (var kvp in _keyValuePairs)
|
||||
{
|
||||
orderedKeyValuePairs.Add(kvp);
|
||||
}
|
||||
|
||||
orderedKeyValuePairs.Sort((x, y) => x.Key.CompareTo(y.Key));
|
||||
#else
|
||||
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
|
||||
#endif
|
||||
|
||||
string section = string.Empty;
|
||||
foreach (var keyValuePair in orderedKeyValuePairs)
|
||||
@@ -196,7 +208,13 @@ namespace SabreTools.IO
|
||||
|
||||
// If the key contains an '.', we need to put them back in
|
||||
string newSection = data[0].Trim();
|
||||
#if NET20 || NET35
|
||||
string[] dataKey = new string[data.Length - 1];
|
||||
Array.Copy(data, 1, dataKey, 0, dataKey.Length);
|
||||
key = string.Join(".", dataKey).Trim();
|
||||
#else
|
||||
key = string.Join(".", data.Skip(1).ToArray()).Trim();
|
||||
#endif
|
||||
|
||||
// If we have a new section, write it out
|
||||
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -221,9 +239,39 @@ namespace SabreTools.IO
|
||||
|
||||
#region IDictionary Impelementations
|
||||
|
||||
#if NET20 || NET35
|
||||
public ICollection<string> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
var keys = _keyValuePairs?.Keys;
|
||||
if (keys == null || keys.Count == 0)
|
||||
return [];
|
||||
|
||||
var keyArr = new string[keys.Count];
|
||||
keys.CopyTo(keyArr, 0);
|
||||
return keyArr;
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<string?> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
var values = _keyValuePairs?.Values;
|
||||
if (values == null || values.Count == 0)
|
||||
return [];
|
||||
|
||||
var valueArr = new string[values.Count];
|
||||
values.CopyTo(valueArr, 0);
|
||||
return valueArr;
|
||||
}
|
||||
}
|
||||
#else
|
||||
public ICollection<string> Keys => _keyValuePairs?.Keys?.ToArray() ?? [];
|
||||
|
||||
public ICollection<string?> Values => _keyValuePairs?.Values?.ToArray() ?? [];
|
||||
#endif
|
||||
|
||||
public int Count => (_keyValuePairs as ICollection<KeyValuePair<string, string>>)?.Count ?? 0;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.IO.Extensions;
|
||||
using SabreTools.Matching;
|
||||
using SabreTools.Matching.Compare;
|
||||
|
||||
namespace SabreTools.IO
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -295,11 +297,22 @@ namespace SabreTools.IO.Readers
|
||||
s = s.Trim();
|
||||
|
||||
// Now we get each string, divided up as cleanly as possible
|
||||
#if NET20 || NET35
|
||||
var matchList = Regex.Matches(s, InternalPatternAttributesCMP);
|
||||
var matchStrings = new List<string>();
|
||||
foreach (Match m in matchList)
|
||||
{
|
||||
matchStrings.Add(m.Groups[0].Value);
|
||||
}
|
||||
|
||||
string[] matches = matchStrings.ToArray();
|
||||
#else
|
||||
string[] matches = Regex
|
||||
.Matches(s, InternalPatternAttributesCMP)
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[0].Value)
|
||||
.ToArray();
|
||||
#endif
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.IO.Readers
|
||||
@@ -118,7 +120,13 @@ namespace SabreTools.IO.Readers
|
||||
|
||||
// If the value field contains an '=', we need to put them back in
|
||||
string key = data[0].Trim();
|
||||
#if NET20 || NET35
|
||||
var valueArr = new string[data.Length - 1];
|
||||
Array.Copy(data, 1, valueArr, 0, valueArr.Length);
|
||||
string value = string.Join("=", valueArr).Trim();
|
||||
#else
|
||||
string value = string.Join("=", data.Skip(1).ToArray()).Trim();
|
||||
#endif
|
||||
|
||||
KeyValuePair = new KeyValuePair<string, string>(key, value);
|
||||
RowType = IniRowType.KeyValue;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -124,7 +126,11 @@ namespace SabreTools.IO.Readers
|
||||
// https://stackoverflow.com/questions/3776458/split-a-comma-separated-string-with-both-quoted-and-unquoted-strings
|
||||
var lineSplitRegex = new Regex($"(?:^|{Separator})(\"(?:[^\"]+|\"\")*\"|[^{Separator}]*)");
|
||||
var temp = new List<string>();
|
||||
#if NET20 || NET35
|
||||
foreach (Match? match in lineSplitRegex.Matches(fullLine))
|
||||
#else
|
||||
foreach (Match? match in lineSplitRegex.Matches(fullLine).Cast<Match?>())
|
||||
#endif
|
||||
{
|
||||
string? curr = match?.Value;
|
||||
if (curr == null)
|
||||
@@ -143,7 +149,17 @@ namespace SabreTools.IO.Readers
|
||||
// Otherwise, just split on the delimiter
|
||||
else
|
||||
{
|
||||
Line = fullLine.Split(Separator).Select(f => f.Trim()).ToList();
|
||||
#if NET20 || NET35
|
||||
Line = new List<string>();
|
||||
foreach (string f in fullLine.Split(Separator))
|
||||
{
|
||||
Line.Add(f.Trim());
|
||||
}
|
||||
#else
|
||||
Line = fullLine.Split(Separator)
|
||||
.Select(f => f.Trim())
|
||||
.ToList();
|
||||
#endif
|
||||
}
|
||||
|
||||
// If we don't have a header yet and are expecting one, read this as the header
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Assembly Properties -->
|
||||
@@ -7,7 +7,8 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.4.5</Version>
|
||||
<Version>1.4.12</Version>
|
||||
<WarningsNotAsErrors>CS0618</WarningsNotAsErrors>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
@@ -22,16 +23,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../README.md" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Support for old .NET versions -->
|
||||
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
|
||||
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
|
||||
<None Include="../README.md" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SabreTools.Matching" Version="1.3.1" />
|
||||
<PackageReference Include="SabreTools.Matching" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
#if NET40_OR_GREATER || NETCOREAPP
|
||||
using System.Linq;
|
||||
#endif
|
||||
|
||||
namespace SabreTools.IO.Streams
|
||||
{
|
||||
@@ -110,7 +112,11 @@ namespace SabreTools.IO.Streams
|
||||
/// </summary>
|
||||
public ReadOnlyCompositeStream(IEnumerable<Stream> streams)
|
||||
{
|
||||
#if NET20 || NET35
|
||||
_streams = new List<Stream>(streams);
|
||||
#else
|
||||
_streams = streams.ToList();
|
||||
#endif
|
||||
_length = 0;
|
||||
_position = 0;
|
||||
|
||||
@@ -148,7 +154,7 @@ namespace SabreTools.IO.Streams
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// Determine which stream we start reading from
|
||||
(int streamIndex, long streamOffset) = DetermineStreamIndex(_position);
|
||||
int streamIndex = DetermineStreamIndex(_position, out long streamOffset);
|
||||
if (streamIndex == -1)
|
||||
return 0;
|
||||
|
||||
@@ -227,12 +233,16 @@ namespace SabreTools.IO.Streams
|
||||
/// <summary>
|
||||
/// Determine the index of the stream that contains a particular offset
|
||||
/// </summary>
|
||||
/// <returns>Index of the stream containing the offset and the real offset in the stream, (-1, -1) on error</returns>
|
||||
private (int index, long realOffset) DetermineStreamIndex(long offset)
|
||||
/// <param name="realOffset">Output parameter representing the real offset in the stream, -1 on error</param>
|
||||
/// <returns>Index of the stream containing the offset, -1 on error</returns>
|
||||
private int DetermineStreamIndex(long offset, out long realOffset)
|
||||
{
|
||||
// If the offset is out of bounds
|
||||
if (offset < 0 || offset >= _length)
|
||||
return (-1, -1);
|
||||
{
|
||||
realOffset = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Seek through until we hit the correct offset
|
||||
long currentLength = 0;
|
||||
@@ -241,13 +251,14 @@ namespace SabreTools.IO.Streams
|
||||
currentLength += _streams[i].Length;
|
||||
if (currentLength > offset)
|
||||
{
|
||||
long realOffset = offset - (currentLength - _streams[i].Length);
|
||||
return (i, realOffset);
|
||||
realOffset = offset - (currentLength - _streams[i].Length);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// Should never happen
|
||||
return (-1, -1);
|
||||
realOffset = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user