Skip to content

Commit 2107813

Browse files
committed
v1.1.0
+ Added support for DxO PhotoLab + Added directory permission check * Refactored code Signed-off-by: Still Hsu <dev@stillu.cc>
1 parent baed9b5 commit 2107813

File tree

7 files changed

+257
-73
lines changed

7 files changed

+257
-73
lines changed

.idea/.idea.AffinityPatcher/.idea/vcs.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AffinityPatcher/AffinityPatcher.csproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
<SelfContained>true</SelfContained>
1212
<PublishSingleFile>true</PublishSingleFile>
1313
<PublishTrimmed>true</PublishTrimmed>
14+
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
15+
<Deterministic>true</Deterministic>
16+
<Version>1.1.0</Version>
1417
</PropertyGroup>
1518

1619
<ItemGroup>
17-
<PackageReference Include="dnlib" Version="4.4.0" />
18-
<PackageReference Include="Humanizer.Core" Version="3.0.0-beta.13" />
19-
<PackageReference Include="Spectre.Console" Version="0.49.1-preview.0.2" />
20+
<PackageReference Include="dnlib" Version="4.5.0" />
21+
<PackageReference Include="Humanizer.Core" Version="3.0.0-beta.96" />
22+
<PackageReference Include="Spectre.Console" Version="0.50.0" />
2023
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
2124
</ItemGroup>
2225

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace AffinityPatcher.Enums
2+
{
3+
public enum CrashReportUploadPolicy
4+
{
5+
// Token: 0x04009C1F RID: 39967
6+
User,
7+
8+
// Token: 0x04009C20 RID: 39968
9+
Always,
10+
11+
// Token: 0x04009C21 RID: 39969
12+
Never
13+
}
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace AffinityPatcher.Enums
2+
{
3+
public enum PatcherMode
4+
{
5+
Affinity,
6+
DxO
7+
}
8+
}

AffinityPatcher/Program.cs

Lines changed: 172 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
using System.CommandLine;
2-
using System.CommandLine.Invocation;
3-
using System.Security.Cryptography;
2+
using System.Runtime.InteropServices;
43
using System.Text;
4+
using AffinityPatcher.Enums;
5+
using AffinityPatcher.Utils;
56
using dnlib.DotNet;
67
using dnlib.DotNet.Emit;
78
using dnlib.DotNet.Writer;
@@ -12,81 +13,109 @@ namespace AffinityPatcher
1213
{
1314
internal class Program
1415
{
15-
// Token: 0x02000502 RID: 1282
16-
public enum CrashReportUploadPolicy
17-
{
18-
// Token: 0x04009C1F RID: 39967
19-
User,
20-
21-
// Token: 0x04009C20 RID: 39968
22-
Always,
23-
24-
// Token: 0x04009C21 RID: 39969
25-
Never
26-
}
27-
2816
static async Task<int> Main(string[] args)
2917
{
3018
var rootCommand =
31-
new RootCommand("Simple application for patching license activation amongst Affinity v2.x/v1.x products.");
19+
new RootCommand("Universal application patcher for Affinity v2.x/v1.x products and DxO PhotoLab.");
3220

3321
var inputOptions = new Option<DirectoryInfo?>("--input",
34-
description: "Target Affinity directory (i.e., path containing Photo/Designer.exe).")
35-
{ IsRequired = true };
22+
description: "Target application directory (i.e., path containing the main executable).")
23+
{ IsRequired = true };
24+
25+
var modeOptions = new Option<PatcherMode>("--mode",
26+
description: "Select which application to patch.")
27+
{ IsRequired = false };
28+
modeOptions.SetDefaultValue(PatcherMode.Affinity);
29+
3630
var verboseOptions = new Option<bool>("--verbose", description: "Enable verbose logging.");
3731
var backupOptions = new Option<bool>("--keep", description: "Backup original assembly.");
3832

3933
rootCommand.AddOption(inputOptions);
34+
rootCommand.AddOption(modeOptions);
4035
rootCommand.AddOption(verboseOptions);
4136
rootCommand.AddOption(backupOptions);
4237

43-
rootCommand.SetHandler((di, shouldVerbose, shouldBackup) =>
38+
rootCommand.SetHandler((di, mode, shouldVerbose, shouldBackup) =>
4439
{
4540
if (di is not { Exists: true })
46-
{
47-
throw new DirectoryNotFoundException("Cannot find the target Affinity directory.");
48-
}
41+
throw new DirectoryNotFoundException($"Cannot find the target {mode} directory.");
42+
43+
bool hasWriteAccess = DirectoryAccessGeneric.HasWriteAccess(di.FullName);
44+
if (shouldVerbose)
45+
AnsiConsole.MarkupLine($"[grey]Checking write access to {di.FullName}: {hasWriteAccess}[/]");
46+
if (!hasWriteAccess)
47+
throw new AccessViolationException("Target directory does not have write access. You may need to re-run this application as administrator.");
4948

50-
var personaAssembly = FindPersonaAssembly(di);
51-
if (personaAssembly == null)
49+
AnsiConsole.MarkupLine(
50+
$"[grey]Patching product \"{mode}\" - if this is incorrect, make sure you have selected the desired product via the \"--mode\" switch.[/]");
51+
switch (mode)
5252
{
53-
throw new FileNotFoundException("Cannot find the required assembly.");
53+
case PatcherMode.Affinity:
54+
PatchAffinity(di, shouldVerbose, shouldBackup);
55+
break;
56+
case PatcherMode.DxO:
57+
PatchDxO(di, shouldVerbose, shouldBackup);
58+
break;
59+
default:
60+
throw new ArgumentException("Invalid patcher mode specified.");
5461
}
62+
}, inputOptions, modeOptions, verboseOptions, backupOptions);
5563

56-
PatchPersonaAssembly(personaAssembly.FullName, verbose: shouldVerbose, keepOriginal: shouldBackup);
57-
}, inputOptions, verboseOptions, backupOptions);
64+
return await rootCommand.InvokeAsync(args);
65+
}
5866

67+
private static void PatchAffinity(DirectoryInfo directoryInfo, bool verbose, bool keepOriginal)
68+
{
69+
AnsiConsole.MarkupLine("[blue]Starting Affinity patching process...[/]");
5970

60-
return await rootCommand.InvokeAsync(args);
71+
var personaAssembly = FindAssembly("Serif.Interop.Persona.dll",directoryInfo);
72+
if (personaAssembly == null)
73+
throw new FileNotFoundException("Cannot find the required Affinity assembly (Serif.Interop.Persona.dll).");
74+
75+
PatchAffinityAssembly(personaAssembly.FullName, verbose: verbose, keepOriginal: keepOriginal);
76+
}
77+
78+
private static void PatchDxO(DirectoryInfo directoryInfo, bool verbose, bool keepOriginal)
79+
{
80+
AnsiConsole.MarkupLine("[blue]Starting DxO PhotoLab patching process...[/]");
81+
82+
var activationAssembly = FindAssembly("DxO.PhotoLab.Activation.dll", directoryInfo);
83+
var activationInteropAssembly = FindAssembly("DxO.PhotoLab.Activation.Interop.dll", directoryInfo);
84+
85+
if (activationAssembly == null || activationInteropAssembly == null)
86+
throw new FileNotFoundException("Cannot find the required DxO assemblies.");
87+
88+
PatchDxOAssembly(activationAssembly.FullName, verbose: verbose, keepOriginal: keepOriginal);
89+
PatchDxOAssembly(activationInteropAssembly.FullName, verbose: verbose, keepOriginal: keepOriginal);
6190
}
6291

63-
static FileInfo? FindPersonaAssembly(DirectoryInfo? directoryInfo)
92+
private static FileInfo? FindAssembly(string dllName, DirectoryInfo? directoryInfo)
6493
{
65-
var targetPath = Path.Join(directoryInfo?.FullName, "Serif.Interop.Persona.dll");
66-
var fi = new FileInfo(targetPath);
67-
if (fi.Exists) return fi;
6894
// use the backup file if one exists
69-
targetPath = Path.Join(directoryInfo?.FullName, "Serif.Interop.Persona.dll.bak");
70-
fi = new FileInfo(targetPath);
71-
return fi.Exists ? fi : null;
95+
var expectedPath = Path.Join(directoryInfo?.FullName, dllName);
96+
var expectedFi = new FileInfo(expectedPath);
97+
return expectedFi.Exists ? expectedFi : null;
7298
}
7399

74-
static void PatchPersonaAssembly(string targetFile, bool verbose, bool keepOriginal)
100+
private static void PatchAffinityAssembly(string targetFile, bool verbose, bool keepOriginal)
75101
{
76102
if (keepOriginal)
77103
{
78104
File.Copy(targetFile, targetFile + ".bak", overwrite: true);
79-
AnsiConsole.MarkupLine("[green]Backed up original assembly.[/]");
105+
AnsiConsole.MarkupLine("[green]Backed up original Affinity assembly.[/]");
80106
}
81107

82108
var moduleContext = ModuleDef.CreateModuleContext();
83109
var tempOutput = Path.GetTempFileName();
110+
84111
using (var module = ModuleDefMD.Load(targetFile, moduleContext))
85112
{
86113
var patchedList = new List<string>();
87114
var application = module.Types.FirstOrDefault(x => x.FullName == "Serif.Interop.Persona.Application");
115+
88116
var methodsToPatchToTrue = application?.Methods.Where(x =>
89117
x.Name == "HasEntitlementToRun" || x.Name == "CheckEula" || x.Name == "CheckAnalytics");
118+
90119
if (methodsToPatchToTrue != null)
91120
{
92121
foreach (var method in methodsToPatchToTrue)
@@ -97,15 +126,14 @@ static void PatchPersonaAssembly(string targetFile, bool verbose, bool keepOrigi
97126
$"Located [grey]{method.FullName}[/], patching with [grey]\"return true\"[/].");
98127
}
99128

100-
PatchWithLdcRet(method.Body, 1);
129+
Patcher.PatchWithLdcRet(method.Body, 1);
101130
patchedList.Add(method.FullName);
102131
}
103132
}
104133

105134
var methodsToPatchToFalse = application?.Methods.Where(x => x.Name == "get_AllowsOptInAnalytics");
106135
if (methodsToPatchToFalse != null)
107136
{
108-
109137
foreach (var method in methodsToPatchToFalse)
110138
{
111139
if (verbose)
@@ -114,7 +142,7 @@ static void PatchPersonaAssembly(string targetFile, bool verbose, bool keepOrigi
114142
$"Located [grey]{method.FullName}[/], patching with [grey]\"return false\"[/].");
115143
}
116144

117-
PatchWithLdcRet(method.Body, 0);
145+
Patcher.PatchWithLdcRet(method.Body, 0);
118146
patchedList.Add(method.FullName);
119147
}
120148
}
@@ -128,53 +156,128 @@ static void PatchPersonaAssembly(string targetFile, bool verbose, bool keepOrigi
128156
$"Located [grey]{crashPolicy.FullName}[/], patching as [grey]{CrashReportUploadPolicy.Never.Humanize()}.[/]");
129157
}
130158

131-
PatchWithLdcRet(crashPolicy.Body, (int)CrashReportUploadPolicy.Never);
159+
Patcher.PatchWithLdcRet(crashPolicy.Body, (int)CrashReportUploadPolicy.Never);
132160
patchedList.Add(crashPolicy.FullName);
133161
}
134162

135-
AnsiConsole.Status().Spinner(Spinner.Known.Aesthetic)
136-
.Start("Saving assembly...", x =>
163+
SaveAssembly(module, tempOutput, patchedList, "Affinity");
164+
}
165+
166+
FinalizeAssembly(targetFile, tempOutput);
167+
}
168+
169+
private static void PatchDxOAssembly(string targetFile, bool verbose, bool keepOriginal)
170+
{
171+
if (keepOriginal)
172+
{
173+
var backupPath = targetFile + ".bak";
174+
File.Copy(targetFile, backupPath, overwrite: true);
175+
AnsiConsole.MarkupLine($"[green]Backed up original DxO assembly as \"{backupPath}\".[/]");
176+
}
177+
178+
var moduleContext = ModuleDef.CreateModuleContext();
179+
var tempOutput = Path.GetTempFileName();
180+
181+
using (var module = ModuleDefMD.Load(targetFile, moduleContext))
182+
{
183+
var patchedList = new List<string>();
184+
var features = module.Types.Where(x => x.FullName.Contains("DxO.PhotoLab.Activation.Feature") || x.FullName.Contains("DxOActivation"));
185+
186+
foreach (var feature in features)
187+
{
188+
var methodsToPatchToTrue = feature?.Methods.Where(x => x.HasBody).Where(x =>
189+
x.Name.EndsWith("IsValid") || x.Name.EndsWith("HasAnyLicense") ||
190+
x.Name.EndsWith("IsActivated") || x.Name.EndsWith("IsElite"));
191+
192+
if (methodsToPatchToTrue != null)
137193
{
138-
if (module.IsILOnly)
194+
foreach (var method in methodsToPatchToTrue)
139195
{
140-
module.Write(tempOutput);
196+
Patcher.PatchWithLdcRetVerbose(method.FullName, method.Body, 1, verbose);
197+
patchedList.Add(method.FullName);
141198
}
142-
else
199+
}
200+
201+
var methodsToPatchToFalse = feature?.Methods.Where(x => x.HasBody).Where(x =>
202+
x.Name.EndsWith("IsExpired") || x.Name.EndsWith("IsDemo") ||
203+
x.Name.EndsWith("IsTemporary") || x.Name == "Check");
204+
205+
if (methodsToPatchToFalse != null)
206+
{
207+
foreach (var method in methodsToPatchToFalse)
143208
{
144-
var writerOptions = new NativeModuleWriterOptions(module, false);
145-
module.NativeWrite(tempOutput, writerOptions);
209+
Patcher.PatchWithLdcRetVerbose(method.FullName, method.Body, 0, verbose);
210+
patchedList.Add(method.FullName);
146211
}
147-
});
212+
}
148213

149-
var sb = new StringBuilder();
150-
foreach (var patched in patchedList)
151-
{
152-
sb.AppendLine($"- [green]{patched}[/]");
214+
var methodsToPatchToTwo = feature?.Methods.Where(x => x.HasBody).Where(x => x.Name.EndsWith("DemoType"));
215+
if (methodsToPatchToTwo != null)
216+
{
217+
foreach (var method in methodsToPatchToTwo)
218+
{
219+
Patcher.PatchWithLdcRetVerbose(method.FullName, method.Body, 2, verbose);
220+
patchedList.Add(method.FullName);
221+
}
222+
}
223+
224+
var methodsToPatchToSpecifiedAmount = feature?.Methods.Where(x => x.HasBody).Where(x =>
225+
x.Name.EndsWith("RemainingDays") || x.Name == "RemainingOfflineDays");
226+
227+
if (methodsToPatchToSpecifiedAmount != null)
228+
{
229+
foreach (var method in methodsToPatchToSpecifiedAmount)
230+
{
231+
Patcher.PatchWithLdcRetVerbose(method.FullName, method.Body, 99, verbose);
232+
patchedList.Add(method.FullName);
233+
}
234+
}
153235
}
154236

155-
var panel = new Panel(sb.ToString());
156-
panel.Padding = new Padding(1, 1);
157-
panel.Header("Patched");
158-
AnsiConsole.Write(panel);
237+
SaveAssembly(module, tempOutput, patchedList, "DxO");
159238
}
160239

161-
if (targetFile.EndsWith(".bak"))
240+
FinalizeAssembly(targetFile, tempOutput);
241+
}
242+
243+
private static void SaveAssembly(ModuleDefMD module, string tempOutput, List<string> patchedList, string appName)
244+
{
245+
AnsiConsole.Status().Spinner(Spinner.Known.Aesthetic)
246+
.Start($"Saving {appName} assembly...", _ =>
247+
{
248+
if (module.IsILOnly)
249+
{
250+
module.Write(tempOutput);
251+
}
252+
else
253+
{
254+
var writerOptions = new NativeModuleWriterOptions(module, false);
255+
module.NativeWrite(tempOutput, writerOptions);
256+
}
257+
});
258+
259+
var sb = new StringBuilder();
260+
foreach (var patched in patchedList)
162261
{
163-
targetFile = targetFile.Replace(".bak", "");
262+
sb.AppendLine($"- [green]{patched}[/]");
164263
}
165-
File.Move(tempOutput, targetFile, overwrite: true);
166264

167-
AnsiConsole.MarkupLine(
168-
$"[green]Assembly saved as {targetFile}[/] with file size [bold]{new FileInfo(targetFile).Length.Bytes().Humanize()}[/].");
265+
var panel = new Panel(sb.ToString())
266+
{
267+
Padding = new Padding(1, 1)
268+
};
269+
panel.Header($"Patched ({appName})");
270+
AnsiConsole.Write(panel);
169271
}
170272

171-
static void PatchWithLdcRet(CilBody cilBody, int ldcValue)
273+
private static void FinalizeAssembly(string targetFile, string tempOutput)
172274
{
173-
cilBody.Instructions.Clear();
174-
cilBody.ExceptionHandlers.Clear();
175-
cilBody.Variables.Clear();
176-
cilBody.Instructions.Add(Instruction.CreateLdcI4(ldcValue));
177-
cilBody.Instructions.Add(Instruction.Create(OpCodes.Ret));
275+
if (targetFile.EndsWith(".bak")) targetFile = targetFile.Replace(".bak", "");
276+
277+
File.Move(tempOutput, targetFile, overwrite: true);
278+
279+
AnsiConsole.MarkupLine(
280+
$"[green]Assembly saved as {targetFile}[/] with file size [bold]{new FileInfo(targetFile).Length.Bytes().Humanize()}[/].");
178281
}
179282
}
180-
}
283+
}

0 commit comments

Comments
 (0)