forked from LittleBigRefresh/Refresh
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathImporter.cs
More file actions
229 lines (192 loc) · 9.57 KB
/
Copy pathImporter.cs
File metadata and controls
229 lines (192 loc) · 9.57 KB
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Bunkum.Core;
using JetBrains.Annotations;
using NotEnoughLogs;
using Refresh.Common.Helpers;
using Refresh.Database.Models.Assets;
using Refresh.Database.Models.Authentication;
namespace Refresh.Core.Importing;
public abstract class Importer
{
private readonly Logger _logger;
protected readonly Stopwatch Stopwatch;
public static Lazy<byte[]?> PSPKey = null!;
protected Importer(Logger? logger = null)
{
if (logger == null)
{
logger = new Logger();
}
this._logger = logger;
this.Stopwatch = new Stopwatch();
PSPKey = new(() =>
{
try
{
//Read the key
byte[] key = File.ReadAllBytes("keys/psp");
//If the hash matches, return the read key
if (SHA1.HashData(key).AsSpan().SequenceEqual(new byte[] { 0x12, 0xb5, 0xa8, 0xb5, 0x91, 0x55, 0x24, 0x96, 0x00, 0xdf, 0x0e, 0x33, 0xf9, 0xc5, 0xa8, 0x76, 0xc1, 0x85, 0x43, 0xfe }))
return key;
//If the hash does not match, log an error and return null
this._logger.LogError(BunkumCategory.Digest, "PSP key failed to validate! Correct hash is 12b5a8b59155249600df0e33f9c5a876c18543fe");
return null;
}
catch(Exception e)
{
if (e.GetType() == typeof(FileNotFoundException) || e.GetType() == typeof(DirectoryNotFoundException))
{
this._logger.LogWarning(BunkumCategory.Digest, "PSP key file not found, encrypting/decrypting of PSP images will not work.");
}
else
{
this._logger.LogError(BunkumCategory.Digest, "Unknown error while loading PSP key! err: {0}", e.ToString());
}
return null;
}
}, LazyThreadSafetyMode.ExecutionAndPublication);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void Info(string message)
{
this._logger.LogInfo(BunkumCategory.UserContent, $"[{this.Stopwatch.ElapsedMilliseconds}ms] {message}");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void Warn(string message)
{
this._logger.LogWarning(BunkumCategory.UserContent, $"[{this.Stopwatch.ElapsedMilliseconds}ms] {message}");
}
protected void Debug(string message)
{
this._logger.LogDebug(BunkumCategory.UserContent, $"[{this.Stopwatch.ElapsedMilliseconds}ms] {message}");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool MatchesMagic(ReadOnlySpan<byte> data, ReadOnlySpan<byte> magic)
{
if (magic.Length > data.Length) return false;
return data[..magic.Length].SequenceEqual(magic);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool MatchesMagic(ReadOnlySpan<byte> data, uint magic)
{
Span<byte> magicSpan = stackalloc byte[sizeof(uint)];
BitConverter.TryWriteBytes(magicSpan, BinaryPrimitives.ReverseEndianness(magic));
return MatchesMagic(data, magicSpan);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool MatchesMagic(ReadOnlySpan<byte> data, ulong magic)
{
Span<byte> magicSpan = stackalloc byte[sizeof(ulong)];
BitConverter.TryWriteBytes(magicSpan, BinaryPrimitives.ReverseEndianness(magic));
return MatchesMagic(data, magicSpan);
}
/// <summary>
/// Tries to detect TGA files sent from the PSP
/// </summary>
/// <param name="data">The data to check</param>
/// <returns>Whether the file is likely of TGA format</returns>
private static bool IsPspTga(ReadOnlySpan<byte> data)
{
try
{
byte imageIdLength = data[0];
byte colorMapType = data[1];
byte imageType = data[2];
ReadOnlySpan<byte> colorMapSpecification = data[3..8];
ReadOnlySpan<byte> imageSpecification = data[8..18];
short xOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[..2]);
short yOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[2..4]);
ushort width = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[4..6]);
ushort height = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[6..8]);
byte depth = imageSpecification[8];
byte descriptor = imageSpecification[9];
//PSP does not seem to fill out this information
if (imageIdLength != 0) return false;
if (xOrigin != 0) return false;
if (yOrigin != 0) return false;
//These are the fields set by PSP, that shouldn't change from image to image
if (colorMapType != 1) return false;
if (descriptor != 0) return false;
if (imageType != 1) return false;
if (depth != 8) return false;
//Reasonable validation checks (PSP seems to only send images of max size 480x272)
if (width > 500) return false;
if (height > 300) return false;
return true;
}
catch
{
//If the data couldn't be read, it's not a TGA
return false;
}
}
private bool IsMip(Span<byte> rawData)
{
//If we dont have a key, then we cant determine the data type
if (PSPKey.Value == null) return false;
//Data less than this size isn't encrypted(?) and all Mip files uploaded to the server will be encrypted
//See https://github.com/ennuo/lbparc/blob/16ad36aa7f4eae2f7b406829e604082750f16fe1/tools/toggle.js#L33
if (rawData.Length < 0x19) return false;
try
{
//Decrypt the data
ReadOnlySpan<byte> data = ResourceHelper.PspDecrypt(rawData.ToArray(), PSPKey.Value);
uint clutOffset = BinaryPrimitives.ReadUInt32LittleEndian(data[..4]);
uint width = BinaryPrimitives.ReadUInt32LittleEndian(data[4..8]);
uint height = BinaryPrimitives.ReadUInt32LittleEndian(data[8..12]);
byte bpp = data[12];
byte numBlocks = data[13];
byte texMode = data[14];
byte alpha = data[15];
uint dataOffset = BinaryPrimitives.ReadUInt32LittleEndian(data[16..20]);
//Its unlikely that any mip textures are ever gonna be this big
if (width > 512 || height > 512) return false;
//We only support MIP files which have a bpp of 4 and 8
if (bpp != 8 && bpp != 4) return false;
//Alpha can only be 0 or 1
if (alpha > 1) return false;
//If the data offset is past the end of the file, its not a MIP
if (dataOffset > data.Length || clutOffset > data.Length) return false;
//If the size of the image is too big for the data passed in, its not a MIP
if (width * height * bpp / 8 > data.Length - dataOffset) return false;
}
catch
{
//If the data is invalid an invalid encrypted file, its not a MIP
return false;
}
return true;
}
[Pure]
protected (GameAssetType, GameAssetFormat) DetermineAssetType(Span<byte> data, TokenPlatform? tokenPlatform)
{
// MATT is a special format constructed by the game when making a grief report. It does not follow standard serialization rules, so it's not caught by the below lines
if (MatchesMagic(data, "MATT"u8))
return (GameAssetType.GriefSongState, GameAssetFormat.Unknown);
GameAssetType? lbpAssetType = GameAssetTypeExtensions.LbpMagicToGameAssetType(data.Slice(0, 3));
if (lbpAssetType != null)
{
// If the asset format is a valid asset format, then return the asset type and format
if (Enum.IsDefined(typeof(GameAssetFormat), data[3]))
return (lbpAssetType.Value, (GameAssetFormat)data[3]);
// If the asset format is not a valid format, then we don't know what this asset is, all real serialized LBP assets will have a valid asset format
this.Warn($"Unknown asset format for game asset [0x{Convert.ToHexString(data[..4])}] [str: {Encoding.ASCII.GetString(data[..4])}]");
return (GameAssetType.Unknown, GameAssetFormat.Unknown);
}
// Traditional files
// Good reference for magics: https://en.wikipedia.org/wiki/List_of_file_signatures
if (MatchesMagic(data, 0xFFD8FFE0)) return (GameAssetType.Jpeg, GameAssetFormat.Unknown);
if (MatchesMagic(data, 0x89504E470D0A1A0A)) return (GameAssetType.Png, GameAssetFormat.Unknown);
// ReSharper disable once ConvertIfStatementToSwitchStatement
if (tokenPlatform is null or TokenPlatform.PSP && IsPspTga(data)) return (GameAssetType.Tga, GameAssetFormat.Unknown);
if (tokenPlatform is null or TokenPlatform.PSP && this.IsMip(data)) return (GameAssetType.Mip, GameAssetFormat.Unknown);
// Incase the asset is ghost data sent by LBP Hub
if (MatchesMagic(data, "<ghost>"u8)) return (GameAssetType.ChallengeGhost, GameAssetFormat.Unknown);
this.Warn($"Unknown asset header [0x{Convert.ToHexString(data[..4])}] [str: {Encoding.ASCII.GetString(data[..4])}]");
return (GameAssetType.Unknown, GameAssetFormat.Unknown);
}
}