Skip to content

Commit 82954ba

Browse files
committed
Merge remote-tracking branch 'upstream/master' into cn-7.4
2 parents 58b69af + 7eea7d6 commit 82954ba

File tree

22 files changed

+315
-106
lines changed

22 files changed

+315
-106
lines changed

Dalamud/Configuration/PluginConfigurations.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
1111
/// <summary>
1212
/// Configuration to store settings for a dalamud plugin.
1313
/// </summary>
14-
[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
14+
[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
1515
public sealed class PluginConfigurations
1616
{
1717
private readonly DirectoryInfo configDirectory;

Dalamud/Dalamud.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Dalamud.Common;
1010
using Dalamud.Configuration.Internal;
1111
using Dalamud.Game;
12+
using Dalamud.Hooking.Internal.Verification;
1213
using Dalamud.Plugin.Internal;
1314
using Dalamud.Storage;
1415
using Dalamud.Utility;
@@ -75,6 +76,11 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati
7576
scanner,
7677
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
7778

79+
using (Timings.Start("HookVerifier Init"))
80+
{
81+
HookVerifier.Initialize(scanner);
82+
}
83+
7884
// Set up FFXIVClientStructs
7985
this.SetupClientStructsResolver(cacheDir);
8086

Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ public IEnumerable<AtkValuePtr> AtkValueEnumerable
5555
AtkValuePtr ptr;
5656
unsafe
5757
{
58+
#pragma warning disable CS0618 // Type or member is obsolete
5859
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
60+
#pragma warning restore CS0618 // Type or member is obsolete
5961
}
6062

6163
yield return ptr;

Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ public IEnumerable<AtkValuePtr> AtkValueEnumerable
5555
AtkValuePtr ptr;
5656
unsafe
5757
{
58+
#pragma warning disable CS0618 // Type or member is obsolete
5859
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
60+
#pragma warning restore CS0618 // Type or member is obsolete
5961
}
6062

6163
yield return ptr;

Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Diagnostics;
3+
using System.Linq;
34
using System.Runtime.CompilerServices;
45

56
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@@ -132,6 +133,19 @@ internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [Calle
132133
}
133134
}
134135

136+
/// <summary>
137+
/// Resolves a virtual table address to the original virtual table address.
138+
/// </summary>
139+
/// <param name="tableAddress">The modified address to resolve.</param>
140+
/// <returns>The original address.</returns>
141+
internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
142+
{
143+
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
144+
if (matchedTable == null) return null;
145+
146+
return matchedTable.OriginalVirtualTable;
147+
}
148+
135149
private void OnAddonInitialize(AtkUnitBase* addon)
136150
{
137151
try
@@ -246,4 +260,8 @@ public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handl
246260
});
247261
}
248262
}
263+
264+
/// <inheritdoc/>
265+
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
266+
=> (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
249267
}

Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ internal unsafe class AddonVirtualTable : IDisposable
4545

4646
private readonly AtkUnitBase* atkUnitBase;
4747

48-
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable;
49-
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable;
50-
5148
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
5249
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
5350
private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
@@ -78,16 +75,16 @@ internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
7875
this.lifecycleService = lifecycleService;
7976

8077
// Save original virtual table
81-
this.originalVirtualTable = addon->VirtualTable;
78+
this.OriginalVirtualTable = addon->VirtualTable;
8279

8380
// Create copy of original table
8481
// Note this will copy any derived/overriden functions that this specific addon has.
8582
// Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
86-
this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
87-
NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
83+
this.ModifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
84+
NativeMemory.Copy(addon->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
8885

8986
// Overwrite the addons existing virtual table with our own
90-
addon->VirtualTable = this.modifiedVirtualTable;
87+
addon->VirtualTable = this.ModifiedVirtualTable;
9188

9289
// Pin each of our listener functions
9390
this.destructorFunction = this.OnAddonDestructor;
@@ -108,30 +105,40 @@ internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
108105
this.focusFunction = this.OnAddonFocus;
109106

110107
// Overwrite specific virtual table entries
111-
this.modifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
112-
this.modifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
113-
this.modifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
114-
this.modifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
115-
this.modifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
116-
this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
117-
this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
118-
this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
119-
this.modifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
120-
this.modifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
121-
this.modifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
122-
this.modifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
123-
this.modifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
124-
this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
125-
this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
126-
this.modifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
108+
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
109+
this.ModifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
110+
this.ModifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
111+
this.ModifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
112+
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
113+
this.ModifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
114+
this.ModifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
115+
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
116+
this.ModifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
117+
this.ModifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
118+
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
119+
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
120+
this.ModifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
121+
this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
122+
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
123+
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
127124
}
128125

126+
/// <summary>
127+
/// Gets the original virtual table address for this addon.
128+
/// </summary>
129+
internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; }
130+
131+
/// <summary>
132+
/// Gets the modified virtual address for this addon.
133+
/// </summary>
134+
internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; }
135+
129136
/// <inheritdoc/>
130137
public void Dispose()
131138
{
132139
// Ensure restoration is done atomically.
133-
Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable);
134-
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
140+
Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.OriginalVirtualTable);
141+
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
135142
}
136143

137144
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
@@ -144,7 +151,7 @@ public void Dispose()
144151

145152
try
146153
{
147-
result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
154+
result = this.OriginalVirtualTable->Dtor(thisPtr, freeFlags);
148155
}
149156
catch (Exception e)
150157
{
@@ -153,7 +160,7 @@ public void Dispose()
153160

154161
if ((freeFlags & 1) == 1)
155162
{
156-
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
163+
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
157164
AddonLifecycle.AllocatedTables.Remove(this);
158165
}
159166
}
@@ -182,7 +189,7 @@ private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
182189

183190
try
184191
{
185-
this.originalVirtualTable->OnSetup(addon, valueCount, values);
192+
this.OriginalVirtualTable->OnSetup(addon, valueCount, values);
186193
}
187194
catch (Exception e)
188195
{
@@ -209,7 +216,7 @@ private void OnAddonFinalize(AtkUnitBase* thisPtr)
209216

210217
try
211218
{
212-
this.originalVirtualTable->Finalizer(thisPtr);
219+
this.OriginalVirtualTable->Finalizer(thisPtr);
213220
}
214221
catch (Exception e)
215222
{
@@ -234,7 +241,7 @@ private void OnAddonDraw(AtkUnitBase* addon)
234241

235242
try
236243
{
237-
this.originalVirtualTable->Draw(addon);
244+
this.OriginalVirtualTable->Draw(addon);
238245
}
239246
catch (Exception e)
240247
{
@@ -265,7 +272,7 @@ private void OnAddonUpdate(AtkUnitBase* addon, float delta)
265272

266273
try
267274
{
268-
this.originalVirtualTable->Update(addon, delta);
275+
this.OriginalVirtualTable->Update(addon, delta);
269276
}
270277
catch (Exception e)
271278
{
@@ -299,7 +306,7 @@ private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* value
299306

300307
try
301308
{
302-
result = this.originalVirtualTable->OnRefresh(addon, valueCount, values);
309+
result = this.OriginalVirtualTable->OnRefresh(addon, valueCount, values);
303310
}
304311
catch (Exception e)
305312
{
@@ -333,7 +340,7 @@ private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArray
333340

334341
try
335342
{
336-
this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
343+
this.OriginalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
337344
}
338345
catch (Exception e)
339346
{
@@ -369,7 +376,7 @@ private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int
369376

370377
try
371378
{
372-
this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
379+
this.OriginalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
373380
}
374381
catch (Exception e)
375382
{
@@ -398,7 +405,7 @@ private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
398405

399406
try
400407
{
401-
result = this.originalVirtualTable->Open(thisPtr, depthLayer);
408+
result = this.OriginalVirtualTable->Open(thisPtr, depthLayer);
402409
}
403410
catch (Exception e)
404411
{
@@ -432,7 +439,7 @@ private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
432439

433440
try
434441
{
435-
result = this.originalVirtualTable->Close(thisPtr, fireCallback);
442+
result = this.OriginalVirtualTable->Close(thisPtr, fireCallback);
436443
}
437444
catch (Exception e)
438445
{
@@ -466,7 +473,7 @@ private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint
466473

467474
try
468475
{
469-
this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
476+
this.OriginalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
470477
}
471478
catch (Exception e)
472479
{
@@ -500,7 +507,7 @@ private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallba
500507

501508
try
502509
{
503-
this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
510+
this.OriginalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
504511
}
505512
catch (Exception e)
506513
{
@@ -527,7 +534,7 @@ private void OnAddonMove(AtkUnitBase* thisPtr)
527534

528535
try
529536
{
530-
this.originalVirtualTable->OnMove(thisPtr);
537+
this.OriginalVirtualTable->OnMove(thisPtr);
531538
}
532539
catch (Exception e)
533540
{
@@ -554,7 +561,7 @@ private void OnAddonMouseOver(AtkUnitBase* thisPtr)
554561

555562
try
556563
{
557-
this.originalVirtualTable->OnMouseOver(thisPtr);
564+
this.OriginalVirtualTable->OnMouseOver(thisPtr);
558565
}
559566
catch (Exception e)
560567
{
@@ -581,7 +588,7 @@ private void OnAddonMouseOut(AtkUnitBase* thisPtr)
581588

582589
try
583590
{
584-
this.originalVirtualTable->OnMouseOut(thisPtr);
591+
this.OriginalVirtualTable->OnMouseOut(thisPtr);
585592
}
586593
catch (Exception e)
587594
{
@@ -608,7 +615,7 @@ private void OnAddonFocus(AtkUnitBase* thisPtr)
608615

609616
try
610617
{
611-
this.originalVirtualTable->Focus(thisPtr);
618+
this.OriginalVirtualTable->Focus(thisPtr);
612619
}
613620
catch (Exception e)
614621
{

Dalamud/Game/Gui/Dtr/DtrBarEntry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public bool Shown
150150
}
151151

152152
/// <inheritdoc/>
153-
[Api14ToDo("Maybe make this config scoped to internal name?")]
153+
[Api15ToDo("Maybe make this config scoped to internal name?")]
154154
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
155155

156156
/// <inheritdoc/>

Dalamud/Hooking/Hook.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using Dalamud.Configuration.Internal;
66
using Dalamud.Hooking.Internal;
7+
using Dalamud.Hooking.Internal.Verification;
78

89
namespace Dalamud.Hooking;
910

@@ -230,6 +231,8 @@ internal static Hook<T> FromAddress(IntPtr procAddress, T detour, bool useMinHoo
230231
if (EnvironmentConfiguration.DalamudForceMinHook)
231232
useMinHook = true;
232233

234+
HookVerifier.Verify<T>(procAddress);
235+
233236
procAddress = HookManager.FollowJmp(procAddress);
234237
if (useMinHook)
235238
return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly());
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Linq;
2+
3+
namespace Dalamud.Hooking.Internal.Verification;
4+
5+
/// <summary>
6+
/// Exception thrown when a provided delegate for a hook does not match a known delegate.
7+
/// </summary>
8+
public class HookVerificationException : Exception
9+
{
10+
private HookVerificationException(string message)
11+
: base(message)
12+
{
13+
}
14+
15+
/// <summary>
16+
/// Create a new <see cref="HookVerificationException"/> exception.
17+
/// </summary>
18+
/// <param name="address">The address of the function that is being hooked.</param>
19+
/// <param name="passed">The delegate passed by the user.</param>
20+
/// <param name="enforced">The delegate we think is correct.</param>
21+
/// <param name="message">Additional context to show to the user.</param>
22+
/// <returns>The created exception.</returns>
23+
internal static HookVerificationException Create(IntPtr address, Type passed, Type enforced, string message)
24+
{
25+
return new HookVerificationException(
26+
$"Hook verification failed for address 0x{address.ToInt64():X}\n\n" +
27+
$"Why: {message}\n" +
28+
$"Passed Delegate: {GetSignature(passed)}\n" +
29+
$"Correct Delegate: {GetSignature(enforced)}\n\n" +
30+
"The hook delegate must exactly match the provided signature to prevent memory corruption and wrong data passed to originals.");
31+
}
32+
33+
private static string GetSignature(Type delegateType)
34+
{
35+
var method = delegateType.GetMethod("Invoke");
36+
if (method == null) return delegateType.Name;
37+
38+
var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name));
39+
return $"{method.ReturnType.Name} ({parameters})";
40+
}
41+
}

0 commit comments

Comments
 (0)