This repository has been archived by the owner on Nov 8, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
GmadReader.cs
211 lines (183 loc) · 7.62 KB
/
GmadReader.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace GmadFileFormat
{
/// <summary>
/// The class that contains the methods for parsing GMAD files
/// </summary>
public static class GmadReader
{
/// <summary>
/// Represents a file that does not have a known offset yet
/// </summary>
/// <remarks>
/// This had to be added to keep .NET Standard < 2.0 support instead of value tuples.
/// </remarks>
private readonly struct PartialFile
{
/// <summary>
/// The path of the file
/// </summary>
public readonly String Path;
/// <summary>
/// The size (in bytes) of the file
/// </summary>
public readonly Int64 Size;
/// <summary>
/// The CRC32 checksum of the file (unused by GMod)
/// </summary>
public readonly UInt32 Crc;
public PartialFile ( String path, Int64 size, UInt32 crc )
{
this.Path = path;
this.Size = size;
this.Crc = crc;
}
/// <inheritdoc/>
public override Boolean Equals ( Object? obj ) =>
obj is PartialFile other && this.Path == other.Path && this.Size == other.Size && this.Crc == other.Crc;
/// <inheritdoc/>
public override Int32 GetHashCode ( )
{
var hashCode = -777822709;
hashCode = hashCode * -1521134295 + EqualityComparer<String>.Default.GetHashCode ( this.Path );
hashCode = hashCode * -1521134295 + this.Size.GetHashCode ( );
hashCode = hashCode * -1521134295 + this.Crc.GetHashCode ( );
return hashCode;
}
public void Deconstruct ( out String path, out Int64 size, out UInt32 crc )
{
path = this.Path;
size = this.Size;
crc = this.Crc;
}
/// <summary>
/// Converts this <see cref="PartialFile"/> into a <see cref="GmadHeader.File"/> with
/// the provided <paramref name="offset"/>
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
public GmadHeader.File WithOffset ( Int64 offset ) =>
new GmadHeader.File ( this.Path, this.Crc, offset, this.Size );
}
/// <summary>
/// The the bytes of the header
/// </summary>
private static ReadOnlySpan<Byte> Header => new Byte[] { 0x47 /* 'G' */, 0x4D /* 'M' */, 0x41 /* 'A' */, 0x44 /* 'D' */ };
/// <summary>
/// Returns wether a byte array has a valid GMAD header
/// </summary>
/// <param name="data">the file content as an array of bytes</param>
/// <returns></returns>
public static Boolean HasValidHeader ( ReadOnlySpan<Byte> data ) =>
Header.SequenceEqual ( data );
/// <summary>
/// Reads 4 bytes from the stream and returns whether they form a valid header.
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public static Boolean HasValidHeader ( Stream stream )
{
var head = ArrayPool<Byte>.Shared.Rent ( 4 );
try
{
stream.Read ( head, 0, 4 );
return HasValidHeader ( head );
}
finally
{
ArrayPool<Byte>.Shared.Return ( head, clearArray: true );
}
}
/// <summary>
/// Parses a GMAD file taking into account possible JSON encoded descriptions
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public static GmadHeader ReadHeader ( Stream stream )
{
if ( !HasValidHeader ( stream ) )
{
throw new Exception ( "Invalid GMAD file." );
}
using var reader = new BinaryReader ( stream, Encoding.UTF8, true );
// We only support up to v3
var formatVersion = ( Int16 ) reader.ReadChar ( );
if ( formatVersion > 3 )
throw new Exception ( "Unsupported GMAD file version." );
// These stuff are almost always wrong (aka SID64 = 0)
var authorSteamId64 = reader.ReadUInt64 ( );
var timestamp = reader.ReadUInt64 ( );
// required content ( not used )
if ( formatVersion > 1 )
{
var content = ReadNullTerminatedString ( reader );
while ( content != "" )
{
content = ReadNullTerminatedString ( reader );
}
}
var name = ReadNullTerminatedString ( reader );
var description = ReadNullTerminatedString ( reader );
var authorName = ReadNullTerminatedString ( reader );
var addonVersion = reader.ReadInt32 ( );
var fileHeaders = new List<PartialFile> ( );
// Retrieve file metadata
while ( reader.ReadUInt32 ( ) != 0 )
{
var path = ReadNullTerminatedString ( reader );
var size = reader.ReadInt64 ( );
var crc = reader.ReadUInt32 ( );
fileHeaders.Add ( new PartialFile ( path, size, crc ) );
}
var filesOffset = stream.Position;
// Files' data is stored after the metadata
var files = new List<GmadHeader.File> ( );
var accumulatedOffset = 0L;
foreach ( PartialFile file in fileHeaders )
{
files.Add ( file.WithOffset ( filesOffset + accumulatedOffset ) );
accumulatedOffset += file.Size;
}
return new GmadHeader ( new GmadHeader.AuthorInfo ( authorName, authorSteamId64 ), description, files, formatVersion, name, timestamp, addonVersion, filesOffset );
static String ReadNullTerminatedString ( BinaryReader reader )
{
var build = new StringBuilder ( );
for ( var ch = reader.ReadChar ( ); ch != 0x00; ch = reader.ReadChar ( ) )
build.Append ( ch );
return build.ToString ( );
}
}
/// <summary>
/// Reads a <see cref="GmadHeader.File"/> from the stream by seeking to its start.
/// </summary>
/// <param name="file"></param>
/// <param name="stream"></param>
/// <returns></returns>
public static Byte[] ReadFile ( GmadHeader.File file, Stream stream )
{
if ( !stream.CanSeek )
throw new NotSupportedException ( "Non-seekable streams are not supported." );
stream.Seek ( file.Offset, SeekOrigin.Begin );
return ReadFileAsNext ( file, stream );
}
/// <summary>
/// Reads a <see cref="GmadHeader.File"/> from the stream WITHOUT SEEKING. If used
/// inappropiately can read different files.
/// </summary>
/// <param name="file"></param>
/// <param name="stream"></param>
/// <returns></returns>
public static Byte[] ReadFileAsNext ( GmadHeader.File file, Stream stream )
{
var buffer = new Byte[file.Size];
var read = stream.Read ( buffer, 0, ( Int32 ) file.Size );
if ( read != file.Size )
throw new InvalidDataException ( "The full file wasn't available in the stream." );
return buffer;
}
}
}