Skip to content

Commit 6aad495

Browse files
NainetenNaineten
authored andcommitted
feat: enhance OFPA support and support all view modes
This update polishes the OFPA naming implementation and extends support to List and Grid views. Improvements: - Refactored binary parser into 'SourceGit.Utilities.OFPAParser' with improved naming and safety. - Created 'PathToDisplayNameConverter' to enable decoded names in List and Grid views. - Fixed UI refresh issue when toggling OFPA mode by ensuring dictionary reference swapping. - Added race condition protection in async decoding task. - Updated all view modes in 'ChangeCollectionView' to support 'DecodedPaths'. - Fixed nullable warnings for .NET 10.0 compatibility.
1 parent a10bc73 commit 6aad495

6 files changed

Lines changed: 173 additions & 105 deletions

File tree

src/Converters/OFPAConverters.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.IO;
6+
using Avalonia.Data.Converters;
7+
8+
namespace SourceGit.Converters
9+
{
10+
public class PathToDisplayNameConverter : IMultiValueConverter
11+
{
12+
public object Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
13+
{
14+
if (values.Count < 2)
15+
return "";
16+
17+
string path = values[0] as string ?? string.Empty;
18+
var decodedPaths = values[1] as IReadOnlyDictionary<string, string>;
19+
20+
if (decodedPaths != null && decodedPaths.TryGetValue(path, out var decoded))
21+
return decoded;
22+
23+
if (parameter as string == "PureFileName")
24+
return Path.GetFileName(path);
25+
26+
return path;
27+
}
28+
}
29+
30+
public static class OFPAConverters
31+
{
32+
public static readonly PathToDisplayNameConverter PathToDisplayName = new();
33+
}
34+
}
Lines changed: 84 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using System.IO;
66
using System.Text;
77

8-
namespace SourceGit.Commands
8+
namespace SourceGit.Utilities
99
{
1010
/// <summary>
1111
/// Decodes human-readable names from Unreal Engine OFPA (One File Per Actor) .uasset files.
@@ -20,14 +20,20 @@ namespace SourceGit.Commands
2020
/// Compatibility: UE 4.26 - 5.7+
2121
/// Performance: ~0.1 ms/file
2222
/// </summary>
23-
public static class DecodeOFPAPath
23+
/// <summary>
24+
/// Decodes human-readable names from Unreal Engine OFPA (One File Per Actor) .uasset files.
25+
/// These files have hashed names like "KCBX0GWLTFQT9RJ8M1LY8.uasset" in __ExternalActors__ folders.
26+
/// </summary>
27+
public static class OFPAParser
2428
{
2529
// Unreal Engine asset magic number (little-endian: 0x9E2A83C1)
2630
private static readonly byte[] UnrealMagic = { 0xC1, 0x83, 0x2A, 0x9E };
2731

2832
private const int HeaderScanLimit = 1024;
2933
private const int MaxStringLength = 256;
3034
private const int PropertyTagWindow = 150;
35+
private const int MinimumHeaderSize = 20;
36+
private const int PatternLength = 16;
3137

3238
/// <summary>
3339
/// Result of decoding an OFPA file.
@@ -77,6 +83,9 @@ public static bool IsOFPAFile(string path)
7783
{
7884
try
7985
{
86+
if (!File.Exists(filePath))
87+
return null;
88+
8089
var data = File.ReadAllBytes(filePath);
8190
return DecodeFromData(data);
8291
}
@@ -93,7 +102,7 @@ public static bool IsOFPAFile(string path)
93102
/// <returns>Decoded label or null if data is invalid</returns>
94103
public static DecodeResult? DecodeFromData(byte[] data)
95104
{
96-
if (data == null || data.Length < 20)
105+
if (data == null || data.Length < MinimumHeaderSize)
97106
return null;
98107

99108
// Check magic number
@@ -104,48 +113,47 @@ public static bool IsOFPAFile(string path)
104113
return ParseUAsset(data);
105114
}
106115

107-
private static DecodeResult? ParseUAsset(byte[] data)
116+
private static DecodeResult? ParseUAsset(byte[] buffer)
108117
{
109-
int size = data.Length;
118+
int fileSize = buffer.Length;
110119

111120
// Fast path: find '/' to locate FolderName string
112-
int start = 20;
113-
int slashOff = FindByte(data, (byte)'/', 20, Math.Min(size, HeaderScanLimit));
114-
if (slashOff >= 24)
121+
int searchStart = MinimumHeaderSize;
122+
int slashOffset = FindByte(buffer, (byte)'/', MinimumHeaderSize, Math.Min(fileSize, HeaderScanLimit));
123+
if (slashOffset >= 24)
115124
{
116-
int pLen = ReadInt32(data, slashOff - 4);
117-
if (pLen > 0 && pLen < MaxStringLength)
118-
start = slashOff - 4;
125+
int length = ReadInt32(buffer, slashOffset - 4);
126+
if (length > 0 && length < MaxStringLength)
127+
searchStart = slashOffset - 4;
119128
}
120129

121-
// Scan for name_count and name_offset
122-
int headerLen = Math.Min(size, HeaderScanLimit);
123-
int limit = headerLen - 20;
130+
// Scan for NameMap count and offset
131+
int headerLength = Math.Min(fileSize, HeaderScanLimit);
132+
int scanLimit = headerLength - MinimumHeaderSize;
124133
int nameCount = 0;
125134
int nameOffset = 0;
126135

127-
for (int off = start; off < limit; off++)
136+
for (int currentPos = searchStart; currentPos < scanLimit; currentPos++)
128137
{
129-
int pLen = ReadInt32(data, off);
130-
if (pLen > 0 && pLen < MaxStringLength)
138+
int stringLength = ReadInt32(buffer, currentPos);
139+
if (stringLength > 0 && stringLength < MaxStringLength)
131140
{
132-
int strEnd = off + 4 + pLen;
133-
if (strEnd > limit)
141+
int stringEnd = currentPos + 4 + stringLength;
142+
if (stringEnd > scanLimit)
134143
break;
135144

136-
byte ch = data[off + 4];
145+
byte firstChar = buffer[currentPos + 4];
137146
// Check for '/' or "None"
138-
if (ch == 47 || (ch == 78 && MatchBytes(data, off + 4, "None")))
147+
if (firstChar == 47 || (firstChar == 78 && MatchBytes(buffer, currentPos + 4, "None")))
139148
{
140-
int baseOff = strEnd;
141-
if (baseOff + 12 <= headerLen)
149+
if (stringEnd + 12 <= headerLength)
142150
{
143-
int nc = ReadInt32(data, baseOff + 4);
144-
int no = ReadInt32(data, baseOff + 8);
145-
if (nc > 0 && nc < 100000 && no > 0 && no < size)
151+
int count = ReadInt32(buffer, stringEnd + 4);
152+
int offset = ReadInt32(buffer, stringEnd + 8);
153+
if (count > 0 && count < 100000 && offset > 0 && offset < fileSize)
146154
{
147-
nameCount = nc;
148-
nameOffset = no;
155+
nameCount = count;
156+
nameOffset = offset;
149157
break;
150158
}
151159
}
@@ -157,118 +165,118 @@ public static bool IsOFPAFile(string path)
157165
return null;
158166

159167
// Parse Name Map - find target indices
160-
int labelIdx = -1;
161-
int strIdx = -1;
168+
int labelIndex = -1;
169+
int propertyIndex = -1;
162170
string? labelType = null;
163171

164-
int pos = nameOffset;
165-
for (int i = 0; i < nameCount && pos + 4 <= size; i++)
172+
int position = nameOffset;
173+
for (int i = 0; i < nameCount && position + 4 <= fileSize; i++)
166174
{
167-
int sLen = ReadInt32(data, pos);
168-
pos += 4;
175+
int stringLength = ReadInt32(buffer, position);
176+
position += 4;
169177

170-
if (sLen > 0)
178+
if (stringLength > 0)
171179
{
172-
int end = pos + sLen;
173-
if (end > size)
180+
int end = position + stringLength;
181+
if (end > fileSize)
174182
break;
175183

176184
// Check for target strings
177-
if (sLen == 11 && labelIdx < 0 && MatchBytes(data, pos, "ActorLabel"))
185+
if (stringLength == 11 && labelIndex < 0 && MatchBytes(buffer, position, "ActorLabel"))
178186
{
179-
labelIdx = i;
187+
labelIndex = i;
180188
labelType = "ActorLabel";
181189
}
182-
else if (sLen == 12)
190+
else if (stringLength == 12)
183191
{
184-
if (MatchBytes(data, pos, "StrProperty"))
192+
if (MatchBytes(buffer, position, "StrProperty"))
185193
{
186-
strIdx = i;
194+
propertyIndex = i;
187195
}
188-
else if (labelIdx < 0 && MatchBytes(data, pos, "FolderLabel"))
196+
else if (labelIndex < 0 && MatchBytes(buffer, position, "FolderLabel"))
189197
{
190-
labelIdx = i;
198+
labelIndex = i;
191199
labelType = "FolderLabel";
192200
}
193201
}
194-
else if (sLen == 6 && labelIdx < 0 && MatchBytes(data, pos, "Label"))
202+
else if (stringLength == 6 && labelIndex < 0 && MatchBytes(buffer, position, "Label"))
195203
{
196-
labelIdx = i;
204+
labelIndex = i;
197205
labelType = "Label";
198206
}
199207

200-
pos = end;
208+
position = end;
201209

202-
if (labelIdx >= 0 && strIdx >= 0)
210+
if (labelIndex >= 0 && propertyIndex >= 0)
203211
break;
204212
}
205-
else if (sLen < 0)
213+
else if (stringLength < 0)
206214
{
207215
// UTF-16 string
208-
pos += (-sLen) * 2;
216+
position += (-stringLength) * 2;
209217
}
210218

211219
// Skip hash value if present
212-
if (pos + 4 <= size)
220+
if (position + 4 <= fileSize)
213221
{
214-
int nv = ReadInt32(data, pos);
215-
if (nv == 0 || nv < -512 || nv > 512)
216-
pos += 4;
222+
int hash = ReadInt32(buffer, position);
223+
if (hash == 0 || hash < -512 || hash > 512)
224+
position += 4;
217225
}
218226
}
219227

220-
if (labelIdx < 0 || strIdx < 0 || labelType == null)
228+
if (labelIndex < 0 || propertyIndex < 0 || labelType == null)
221229
return null;
222230

223-
// Find property tag pattern: [labelIdx, 0, strIdx, 0]
224-
byte[] pattern = new byte[16];
225-
BinaryPrimitives.WriteInt32LittleEndian(pattern.AsSpan(0), labelIdx);
231+
// Find property tag pattern: [labelIndex, 0, propertyIndex, 0]
232+
byte[] pattern = new byte[PatternLength];
233+
BinaryPrimitives.WriteInt32LittleEndian(pattern.AsSpan(0), labelIndex);
226234
BinaryPrimitives.WriteInt32LittleEndian(pattern.AsSpan(4), 0);
227-
BinaryPrimitives.WriteInt32LittleEndian(pattern.AsSpan(8), strIdx);
235+
BinaryPrimitives.WriteInt32LittleEndian(pattern.AsSpan(8), propertyIndex);
228236
BinaryPrimitives.WriteInt32LittleEndian(pattern.AsSpan(12), 0);
229237

230-
int tagOff = FindPattern(data, pattern);
231-
if (tagOff == -1)
238+
int tagOffset = FindPattern(buffer, pattern);
239+
if (tagOffset == -1)
232240
return null;
233241

234242
// Extract string value
235-
int searchStart = tagOff + 16;
236-
int searchEnd = Math.Min(searchStart + PropertyTagWindow, size);
243+
int valueSearchStart = tagOffset + PatternLength;
244+
int valueSearchEnd = Math.Min(valueSearchStart + PropertyTagWindow, fileSize);
237245

238-
for (int i = searchStart; i < searchEnd - 4; i++)
246+
for (int i = valueSearchStart; i < valueSearchEnd - 4; i++)
239247
{
240-
int pLen = ReadInt32(data, i);
248+
int stringLength = ReadInt32(buffer, i);
241249

242-
if (pLen > 0 && pLen < 128)
250+
if (stringLength > 0 && stringLength < 128)
243251
{
244-
int strEnd = i + 4 + pLen - 1; // -1 for null terminator
245-
if (strEnd <= searchEnd)
252+
int stringEnd = i + 4 + stringLength - 1; // -1 for null terminator
253+
if (stringEnd <= valueSearchEnd)
246254
{
247255
// Check if it's printable ASCII
248256
bool valid = true;
249-
for (int j = i + 4; j < strEnd && valid; j++)
257+
for (int j = i + 4; j < stringEnd && valid; j++)
250258
{
251-
byte b = data[j];
259+
byte b = buffer[j];
252260
if (b < 32 || b > 126)
253261
valid = false;
254262
}
255263

256-
if (valid && strEnd > i + 4)
264+
if (valid && stringEnd > i + 4)
257265
{
258-
string value = Encoding.ASCII.GetString(data, i + 4, strEnd - i - 4);
266+
string value = Encoding.ASCII.GetString(buffer, i + 4, stringEnd - i - 4);
259267
return new DecodeResult(labelType, value);
260268
}
261269
}
262270
}
263-
else if (pLen < 0 && pLen > -128)
271+
else if (stringLength < 0 && stringLength > -128)
264272
{
265273
// UTF-16 string
266-
int strEnd = i + 4 + ((-pLen) * 2) - 2; // -2 for null terminator
267-
if (strEnd <= searchEnd && strEnd > i + 4)
274+
int stringEnd = i + 4 + ((-stringLength) * 2) - 2; // -2 for null terminator
275+
if (stringEnd <= valueSearchEnd && stringEnd > i + 4)
268276
{
269277
try
270278
{
271-
string value = Encoding.Unicode.GetString(data, i + 4, strEnd - i - 4);
279+
string value = Encoding.Unicode.GetString(buffer, i + 4, stringEnd - i - 4);
272280
return new DecodeResult(labelType, value);
273281
}
274282
catch

0 commit comments

Comments
 (0)