Skip to content

Commit bda0c6c

Browse files
NainetenNaineten
authored andcommitted
feat: finalize OFPA support with memory-safe decoding and UI refinements
Complete the Unreal Engine OFPA (One File Per Actor) support with safety guards, updated localization, and performance fixes. Changes: - Memory Safety: Implemented MaxOFPASampleSize (256KB) limit for binary reads in QueryFileContent.RunBatchAsync to prevent OOM when processing large UE assets. - Robust Parsing: Increased MaxHeaderScanSize in OFPAParser and improved heuristic detection for ActorLabel/FolderLabel in complex uasset files. - UI/UX: Added OFPA-specific icons and updated Repository Configuration view to support the new toggle. - Localization: Added translations for English, Russian, and Chinese (Simplified). - Stability: Added unit tests for PathToDisplayNameConverter and Repository-level Unreal Engine support logic. - Cleanup: Removed legacy SourceGit.sln (migrated to .slnx) and internal PerformanceLogger.
1 parent 68a20b5 commit bda0c6c

19 files changed

Lines changed: 240 additions & 210 deletions

SourceGit.sln

Lines changed: 0 additions & 56 deletions
This file was deleted.

src/Commands/QueryFileContent.cs

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ private static async Task<Stream> RunObjectSpecAsync(string repo, string objectS
4949
{
5050
App.RaiseException(repo, $"Failed to query file content: {e}");
5151
}
52-
sw.Stop();
53-
Utilities.PerformanceLogger.Log($"[GitShow] {objectSpec} : {sw.ElapsedMilliseconds}ms");
5452

5553
stream.Position = 0;
5654
return stream;
@@ -76,19 +74,9 @@ public static async Task<Dictionary<string, byte[]>> RunBatchAsync(string repo,
7674
RedirectStandardOutput = true,
7775
};
7876

79-
var swTotal = Stopwatch.StartNew();
80-
var swStart = Stopwatch.StartNew();
81-
long startMs = 0, firstReadMs = 0, dataReadMs = 0, waitExitMs = 0;
82-
int missingCount = 0;
83-
long totalBytesRead = 0;
84-
long totalBytesInObjects = 0;
85-
int minSize = int.MaxValue, maxSize = 0;
86-
8777
try
8878
{
8979
using var proc = Process.Start(starter)!;
90-
swStart.Stop();
91-
startMs = swStart.ElapsedMilliseconds;
9280

9381
// Write requests in background to avoid deadlock (pipe buffer full)
9482
var writeTask = Task.Run(async () =>
@@ -101,89 +89,50 @@ public static async Task<Dictionary<string, byte[]>> RunBatchAsync(string repo,
10189
});
10290

10391
await using var output = proc.StandardOutput.BaseStream;
104-
105-
var swFirstRead = Stopwatch.StartNew();
106-
var swDataRead = new Stopwatch();
107-
bool firstReadDone = false;
108-
10992
for (int i = 0; i < objectSpecs.Count; i++)
11093
{
11194
var header = await ReadBatchHeaderLineAsync(output).ConfigureAwait(false);
112-
113-
if (!firstReadDone)
114-
{
115-
swFirstRead.Stop();
116-
firstReadMs = swFirstRead.ElapsedMilliseconds;
117-
firstReadDone = true;
118-
}
119-
12095
if (header == null)
12196
break;
12297

12398
if (header.EndsWith(" missing", StringComparison.Ordinal))
124-
{
125-
missingCount++;
12699
continue;
127-
}
128100

129101
var size = ParseBatchObjectSize(header);
130102
if (size > 0)
131103
{
132-
totalBytesInObjects += size;
133-
if (size < minSize) minSize = size;
134-
if (size > maxSize) maxSize = size;
135-
136104
// If maxBytesPerObject is set, read only that many bytes and skip the rest.
137105
var bytesToRead = (maxBytesPerObject > 0 && size > maxBytesPerObject)
138106
? maxBytesPerObject
139107
: size;
140108
var bytesToSkip = size - bytesToRead;
141109

142-
swDataRead.Start();
143110
var data = await ReadExactBytesAsync(output, bytesToRead).ConfigureAwait(false);
144-
swDataRead.Stop();
145-
146111
if (data != null)
147112
{
148113
results[objectSpecs[i]] = data;
149-
totalBytesRead += data.Length;
150114
}
151115

152116
// Skip remaining bytes if we limited the read.
153117
if (bytesToSkip > 0)
154118
{
155-
swDataRead.Start();
156119
await SkipBytesAsync(output, bytesToSkip).ConfigureAwait(false);
157-
swDataRead.Stop();
158120
}
159121
}
160122

161123
// Consume trailing newline after object content (even for size 0).
162124
_ = await ReadSingleByteAsync(output).ConfigureAwait(false);
163125
}
164126

165-
dataReadMs = swDataRead.ElapsedMilliseconds;
166-
167127
// Ensure writing is finished (should be, or implies error)
168128
await writeTask.ConfigureAwait(false);
169-
170-
var swWait = Stopwatch.StartNew();
171129
await proc.WaitForExitAsync().ConfigureAwait(false);
172-
swWait.Stop();
173-
waitExitMs = swWait.ElapsedMilliseconds;
174130
}
175131
catch (Exception e)
176132
{
177133
App.RaiseException(repo, $"Failed to query batch file content: {e}");
178134
}
179135

180-
swTotal.Stop();
181-
var avgSize = results.Count > 0 ? totalBytesInObjects / results.Count : 0;
182-
Utilities.PerformanceLogger.Log(
183-
$"[GitBatch] {objectSpecs.Count} specs, {results.Count} found, {missingCount} missing | " +
184-
$"Data:{totalBytesRead / 1024}KB (min:{minSize / 1024}KB avg:{avgSize / 1024}KB max:{maxSize / 1024}KB) | " +
185-
$"Start:{startMs}ms FirstRead:{firstReadMs}ms DataRead:{dataReadMs}ms Exit:{waitExitMs}ms Total:{swTotal.ElapsedMilliseconds}ms");
186-
187136
return results;
188137
}
189138

src/Converters/OFPAConverters.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ public object Convert(IList<object?> values, Type targetType, object? parameter,
1717
string path = values[0] as string ?? string.Empty;
1818
var decodedPaths = values[1] as IReadOnlyDictionary<string, string>;
1919

20-
if (decodedPaths != null && decodedPaths.TryGetValue(path, out var decoded))
20+
if (decodedPaths != null &&
21+
decodedPaths.TryGetValue(path, out var decoded) &&
22+
!string.IsNullOrEmpty(decoded))
23+
{
2124
return decoded;
25+
}
2226

2327
if (parameter as string == "PureFileName")
2428
return Path.GetFileName(path);

src/Models/RepositorySettings.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public bool EnableOFPADecoding
5959
set;
6060
} = false;
6161

62+
public bool EnableUnrealEngineSupport
63+
{
64+
get;
65+
set;
66+
} = false;
67+
6268
public bool EnableForceOnFetch
6369
{
6470
get;

src/Resources/Icons.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,5 @@
158158
<StreamGeometry x:Key="Icons.Worktree">M853 267H514c-4 0-6-2-9-4l-38-66c-13-21-38-36-64-36H171c-41 0-75 34-75 75v555c0 41 34 75 75 75h683c41 0 75-34 75-75V341c0-41-34-75-75-75zm-683-43h233c4 0 6 2 9 4l38 66c13 21 38 36 64 36H853c6 0 11 4 11 11v75h-704V235c0-6 4-11 11-11zm683 576H171c-6 0-11-4-11-11V480h704V789c0 6-4 11-11 11z</StreamGeometry>
159159
<StreamGeometry x:Key="Icons.Worktree.Add">M896 96 614 96c-58 0-128-19-179-51C422 38 390 19 358 19L262 19 128 19c-70 0-128 58-128 128l0 736c0 70 58 128 128 128l768 0c70 0 128-58 128-128L1024 224C1024 154 966 96 896 96zM704 685 544 685l0 160c0 19-13 32-32 32s-32-13-32-32l0-160L320 685c-19 0-32-13-32-32 0-19 13-32 32-32l160 0L480 461c0-19 13-32 32-32s32 13 32 32l0 160L704 621c19 0 32 13 32 32C736 666 723 685 704 685zM890 326 102 326 102 250c0-32 32-64 64-64l659 0c38 0 64 32 64 64L890 326z</StreamGeometry>
160160
<StreamGeometry x:Key="Icons.Worktrees">M1182 527a91 91 0 00-88-117H92a91 91 0 00-88 117l137 441A80 80 0 00217 1024h752a80 80 0 0076-56zM133 295a31 31 0 0031 31h858a31 31 0 0031-31A93 93 0 00959 203H226a93 93 0 00-94 92zM359 123h467a31 31 0 0031-31A92 92 0 00765 0H421a92 92 0 00-92 92 31 31 0 0031 31z</StreamGeometry>
161+
<StreamGeometry x:Key="Icons.Unreal">M803.7,995.81c156.5-73.92,205.56-210.43,216.6-263.61c-57.22,58.6-120.53,118-163.11,76.88c0,0-2.33-219.45-2.33-309.43c0-121,114.75-211.18,114.75-211.18c-63.11,11.24-138.89,33.71-219.33,112.65c-7.26,7.2-14.14,14.76-20.62,22.67c-34.47-26.39-79.14-18.48-79.14-18.48c24.14,13.26,48.23,51.88,48.23,83.85v314.26c0,0-52.63,46.3-93.19,46.3c-9.14,0.07-18.17-2.05-26.33-6.18c-8.16-4.13-15.21-10.15-20.56-17.56c-3.21-4.19-5.87-8.78-7.91-13.65V424.07c-11.99,9.89-52.51,18.04-52.51-49.22c0-41.79,30.11-91.6,83.73-122.15c-73.63,11.23-142.59,43.04-198.92,91.76c-42.8,36.98-77.03,82.85-100.31,134.4c-23.28,51.55-35.06,107.55-34.51,164.12c0,0,39.21-122.51,88.32-133.83c7.15-1.88,14.65-2.07,21.89-0.54c7.24,1.53,14.02,4.72,19.81,9.34c5.79,4.61,10.41,10.51,13.51,17.23c3.1,6.72,4.59,14.07,4.34,21.46V844.3c0,29.16-18.8,35.53-36.17,35.22c-11.77-0.83-23.4-3.02-34.66-6.53c35.86,48.53,82.46,88.12,136.15,115.66c53.69,27.54,113.03,42.29,173.37,43.1l106.05-106.6L803.7,995.81z</StreamGeometry>
161162
</ResourceDictionary>

src/Resources/Locales/en_US.axaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,6 @@
923923
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">USE MINE</x:String>
924924
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">USE THEIRS</x:String>
925925
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">INCLUDE UNTRACKED FILES</x:String>
926-
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">DECODE UNREAL ENGINE OFPA FILENAMES</x:String>
927926
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">NO RECENT INPUT MESSAGES</x:String>
928927
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">NO COMMIT TEMPLATES</x:String>
929928
<x:String x:Key="Text.WorkingCopy.NoVerify" xml:space="preserve">No-Verify</x:String>
@@ -946,4 +945,8 @@
946945
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
947946
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
948947
<x:String x:Key="Text.Yes" xml:space="preserve">YES</x:String>
948+
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Unreal Engine: show OFPA actor names</x:String>
949+
<x:String x:Key="Text.Configure.UnrealEngine" xml:space="preserve">Unreal Engine</x:String>
950+
<x:String x:Key="Text.Configure.UnrealEngine.Support" xml:space="preserve">Enable Unreal Engine support</x:String>
951+
<x:String x:Key="Text.Configure.UnrealEngine.Support.Tip" xml:space="preserve">Enables Unreal Engine-specific features such as OFPA file name decoding.</x:String>
949952
</ResourceDictionary>

src/Resources/Locales/ru_RU.axaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,6 @@
925925
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">ИСПОЛЬЗОВАТЬ МОИ</x:String>
926926
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">ИСПОЛЬЗОВАТЬ ИХ</x:String>
927927
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">Показывать неотслеживаемые файлы</x:String>
928-
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Декодировать имена файлов Unreal Engine OFPA</x:String>
929928
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">Нет недавних сообщений коммитов</x:String>
930929
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">НЕТ ШАБЛОНОВ РЕВИЗИИ</x:String>
931930
<x:String x:Key="Text.WorkingCopy.NoVerify" xml:space="preserve">Не проверять</x:String>
@@ -948,4 +947,8 @@
948947
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Удалить</x:String>
949948
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Разблокировать</x:String>
950949
<x:String x:Key="Text.Yes" xml:space="preserve">Да</x:String>
950+
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Unreal Engine: show OFPA actor names</x:String>
951+
<x:String x:Key="Text.Configure.UnrealEngine" xml:space="preserve">Unreal Engine</x:String>
952+
<x:String x:Key="Text.Configure.UnrealEngine.Support" xml:space="preserve">Enable Unreal Engine support</x:String>
953+
<x:String x:Key="Text.Configure.UnrealEngine.Support.Tip" xml:space="preserve">Enables Unreal Engine-specific features such as OFPA file name decoding.</x:String>
951954
</ResourceDictionary>

src/Resources/Locales/zh_CN.axaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,6 @@
927927
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">使用 MINE</x:String>
928928
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">使用 THEIRS</x:String>
929929
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">显示未跟踪文件</x:String>
930-
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">解码 Unreal Engine OFPA 文件名</x:String>
931930
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">没有提交信息记录</x:String>
932931
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">没有可应用的提交信息模板</x:String>
933932
<x:String x:Key="Text.WorkingCopy.NoVerify" xml:space="preserve">跳过GIT钩子</x:String>
@@ -950,4 +949,8 @@
950949
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">移除工作树</x:String>
951950
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">解除工作树锁定</x:String>
952951
<x:String x:Key="Text.Yes" xml:space="preserve">好的</x:String>
952+
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Unreal Engine: show OFPA actor names</x:String>
953+
<x:String x:Key="Text.Configure.UnrealEngine" xml:space="preserve">Unreal Engine</x:String>
954+
<x:String x:Key="Text.Configure.UnrealEngine.Support" xml:space="preserve">Enable Unreal Engine support</x:String>
955+
<x:String x:Key="Text.Configure.UnrealEngine.Support.Tip" xml:space="preserve">Enables Unreal Engine-specific features such as OFPA file name decoding.</x:String>
953956
</ResourceDictionary>

src/SourceGit.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
4-
<TargetFramework>net9.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ApplicationManifest>App.manifest</ApplicationManifest>
66
<ApplicationIcon>App.ico</ApplicationIcon>
77
<Version>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)\\..\\VERSION"))</Version>
@@ -36,6 +36,10 @@
3636
<AssemblyMetadata Include="BuildDate" Value="$([System.DateTime]::Now.ToString('o'))"/>
3737
</ItemGroup>
3838

39+
<ItemGroup>
40+
<InternalsVisibleTo Include="SourceGit.Tests" />
41+
</ItemGroup>
42+
3943
<ItemGroup>
4044
<AvaloniaResource Include="App.ico" />
4145
<AvaloniaResource Include="Resources/Fonts/*" />

src/Utilities/OFPAParser.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static class OFPAParser
2525
// Unreal Engine asset magic number (little-endian: 0x9E2A83C1)
2626
private static readonly byte[] UnrealMagic = { 0xC1, 0x83, 0x2A, 0x9E };
2727

28-
private const int HeaderScanLimit = 1024;
28+
private const int MaxHeaderScanSize = 256 * 1024;
2929
private const int MaxStringLength = 256;
3030
private const int PropertyTagWindow = 150;
3131
private const int MinimumHeaderSize = 20;
@@ -119,7 +119,7 @@ public static bool IsOFPAFile(string path)
119119

120120
// Fast path: find '/' to locate FolderName string
121121
int searchStart = MinimumHeaderSize;
122-
int slashOffset = FindByte(buffer, (byte)'/', MinimumHeaderSize, Math.Min(fileSize, HeaderScanLimit));
122+
int slashOffset = FindByte(buffer, (byte)'/', MinimumHeaderSize, Math.Min(fileSize, MaxHeaderScanSize));
123123
if (slashOffset >= 24)
124124
{
125125
int length = ReadInt32(buffer, slashOffset - 4);
@@ -128,7 +128,7 @@ public static bool IsOFPAFile(string path)
128128
}
129129

130130
// Scan for NameMap count and offset
131-
int headerLength = Math.Min(fileSize, HeaderScanLimit);
131+
int headerLength = Math.Min(fileSize, MaxHeaderScanSize);
132132
int scanLimit = headerLength - MinimumHeaderSize;
133133
int nameCount = 0;
134134
int nameOffset = 0;

0 commit comments

Comments
 (0)