• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SamboyCoding / Cpp2IL / 28842073904

07 Jul 2026 04:42AM UTC coverage: 36.304% (+0.2%) from 36.132%
28842073904

Pull #577

github

web-flow
Merge 4257b9187 into 5ef1a1957
Pull Request #577: Diffable CS improvements

2734 of 8570 branches covered (31.9%)

Branch coverage included in aggregate %.

249 of 267 new or added lines in 6 files covered. (93.26%)

6 existing lines in 1 file now uncovered.

5078 of 12948 relevant lines covered (39.22%)

170756.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.95
/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs
1
using System;
2
using System.Buffers.Binary;
3
using System.CodeDom.Compiler;
4
using System.Collections.Generic;
5
using System.Diagnostics.CodeAnalysis;
6
using System.Globalization;
7
using System.IO;
8
using System.Linq;
9
using System.Reflection;
10
using Cpp2IL.Core.Api;
11
using Cpp2IL.Core.Extensions;
12
using Cpp2IL.Core.Logging;
13
using Cpp2IL.Core.Model.Contexts;
14
using Cpp2IL.Core.Utils;
15
using LibCpp2IL;
16

17
namespace Cpp2IL.Core.OutputFormats;
18

19
public class DiffableCsOutputFormat : Cpp2IlOutputFormat
20
{
21
    public static bool IncludeMethodLength = false;
22

23
    public override string OutputFormatId => "diffable-cs";
1✔
24
    public override string OutputFormatName => "Diffable C#";
×
25

26
    public override void DoOutput(ApplicationAnalysisContext context, string outputRoot)
27
    {
28
        //General principle of diffable CS:
29
        //- Same-line method bodies ({ })
30
        //- Attributes in alphabetical order
31
        //- Members in alphabetical order and in nested type-field-event-prop-method member order
32
        //- No info on addresses or tokens as these change with every rebuild
33

34
        //The idea is to make it as easy as possible for software like WinMerge, github, etc, to diff the two versions of the code and show the user exactly what changed.
35

36
        outputRoot = Path.Combine(outputRoot, "DiffableCs");
3✔
37

38
        if (Directory.Exists(outputRoot))
3!
39
        {
40
            Logger.InfoNewline("Removing old DiffableCs output directory...", "DiffableCsOutputFormat");
×
41
            Directory.Delete(outputRoot, true);
×
42
        }
43

44
        Logger.InfoNewline("Building C# files and directory structure...", "DiffableCsOutputFormat");
3✔
45
        var files = BuildOutput(context, outputRoot);
3✔
46

47
        Logger.InfoNewline("Writing C# files...", "DiffableCsOutputFormat");
3✔
48
        foreach (var (filePath, fileContent) in files)
11,340✔
49
        {
50
            File.WriteAllText(filePath, fileContent.ToString());
5,667✔
51
        }
52
    }
3✔
53

54
    private static Dictionary<string, StringWriter> BuildOutput(ApplicationAnalysisContext context, string outputRoot)
55
    {
56
        var ret = new Dictionary<string, StringWriter>();
3✔
57

58
        foreach (var assembly in context.Assemblies)
222✔
59
        {
60
            var asmPath = Path.Combine(outputRoot, assembly.CleanAssemblyName);
108✔
61
            Directory.CreateDirectory(asmPath);
108✔
62

63
            foreach (var type in assembly.TopLevelTypes)
11,550✔
64
            {
65
                if (type is InjectedTypeAnalysisContext)
5,667✔
66
                    continue;
67

68
                var path = Path.Combine(asmPath, type.NamespaceAsSubdirs, MiscUtils.CleanPathElement(type.Name + ".cs"));
5,667✔
69
                Directory.CreateDirectory(Path.GetDirectoryName(path)!);
5,667✔
70

71
                var stringWriter = new StringWriter();
5,667✔
72
                var writer = new IndentedTextWriter(stringWriter, "\t");
5,667✔
73

74
                //Namespace at top of file
75
                if (!string.IsNullOrEmpty(type.Namespace))
5,667✔
76
                {
77
                    writer.WriteLine($"namespace {type.Namespace};");
5,541✔
78
                    writer.WriteLineNoTabs(string.Empty);
5,541✔
79
                }
80
                else
81
                {
82
                    writer.WriteLine("//Type is in global namespace");
126✔
83
                    writer.WriteLineNoTabs(string.Empty);
126✔
84
                }
85

86
                AppendType(writer, type);
5,667✔
87

88
                ret[path] = stringWriter;
5,667✔
89
            }
90
        }
91

92
        return ret;
3✔
93
    }
94

95
    private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext type)
96
    {
97
        // if (type.IsCompilerGeneratedBasedOnCustomAttributes)
98
        //Do not output compiler-generated types
99
        // return;
100

101
        //Custom attributes for type. Includes a trailing newline
102
        AppendCustomAttributes(writer, type);
7,404✔
103

104
        //Type declaration line
105
        writer.Write(CsFileUtils.GetKeyWordsForType(type));
7,404✔
106
        writer.Write(' ');
7,404✔
107
        writer.Write(CsFileUtils.GetTypeName(type));
7,404✔
108
        CsFileUtils.WriteInheritanceInfo(type, writer);
7,404✔
109
        writer.WriteLine();
7,404✔
110
        writer.WriteLine('{');
7,404✔
111

112
        //Type declaration done, increase indent
113
        writer.Indent++;
7,404✔
114

115
        if (type.IsEnumType)
7,404✔
116
        {
117
            var enumValues = type.Fields.Where(f => f.IsStatic).ToList();
11,655✔
118
            enumValues.SortByExtractedKey(e => e.Token); //Not as good as sorting by value but it'll do
58,059✔
119
            foreach (var enumValue in enumValues)
21,612✔
120
            {
121
                writer.Write(enumValue.Name);
9,957✔
122
                writer.Write(" = ");
9,957✔
123
                writer.Write(InvariantValue(enumValue.BackingData!.DefaultValue));
9,957✔
124
                writer.WriteLine(',');
9,957✔
125
            }
126
        }
127
        else
128
        {
129
            //Nested classes, alphabetical order
130
            var nestedTypes = type.NestedTypes.Clone();
6,555✔
131
            nestedTypes.SortByExtractedKey(t => t.Name);
13,683✔
132
            foreach (var nested in nestedTypes)
16,584✔
133
                AppendType(writer, nested);
1,737✔
134

135
            //Fields, offset order, static first
136
            var fields = type.Fields.Clone();
6,555✔
137
            fields.SortByExtractedKey(f => f.IsStatic ? f.Offset : f.Offset + 0x1000);
54,105✔
138
            foreach (var field in fields)
44,460✔
139
                AppendField(writer, field);
15,675✔
140

141
            writer.WriteLineNoTabs(string.Empty);
6,555✔
142

143
            //Events, alphabetical order
144
            var events = type.Events.Clone();
6,555✔
145
            events.SortByExtractedKey(e => e.Name);
6,561✔
146
            foreach (var evt in events)
13,140✔
147
                AppendEvent(writer, evt);
15✔
148

149
            //Properties, alphabetical order
150
            var properties = type.Properties.Clone();
6,555✔
151
            properties.SortByExtractedKey(p => p.Name);
28,365✔
152
            foreach (var prop in properties)
24,570✔
153
                AppendProperty(writer, prop);
5,730✔
154

155
            //Methods, alphabetical order
156
            var methods = type.Methods.Clone();
6,555✔
157
            methods.SortByExtractedKey(m => m.Name);
264,573✔
158
            foreach (var method in methods)
87,564✔
159
                AppendMethod(writer, method);
37,227✔
160
        }
161

162
        //Decrease indent, close brace
163
        writer.Indent--;
7,404✔
164
        writer.WriteLine('}');
7,404✔
165
        writer.WriteLineNoTabs(string.Empty);
7,404✔
166
    }
7,404✔
167

168
    private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext field)
169
    {
170
        if (field is InjectedFieldAnalysisContext)
15,675!
171
            return;
×
172

173
        //Custom attributes for field. Includes a trailing newline
174
        AppendCustomAttributes(writer, field);
15,675✔
175

176
        //Field declaration line
177
        writer.Write(CsFileUtils.GetKeyWordsForField(field));
15,675✔
178
        writer.Write(' ');
15,675✔
179
        writer.Write(CsFileUtils.GetTypeName(field.FieldType));
15,675✔
180
        writer.Write(' ');
15,675✔
181
        writer.Write(field.Name);
15,675✔
182

183
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,675✔
184
        {
185
            var fieldRva = field.StaticArrayInitialValue;
297✔
186
            if (fieldRva.Length > 0)
297✔
187
            {
188
                AppendFieldRvaInitializer(writer, field, fieldRva);
297✔
189
                return;
297✔
190
            }
191
        }
192

193
        if (field.BackingData?.DefaultValue is { } defaultValue)
15,378!
194
        {
195
            writer.Write(" = ");
549✔
196

197
            if (defaultValue is string stringDefaultValue)
549✔
198
            {
199
                writer.Write('"');
186✔
200
                writer.Write(stringDefaultValue);
186✔
201
                writer.Write('"');
186✔
202
            }
203
            else if (defaultValue is char charDefaultValue)
363✔
204
            {
205
                writer.Write("'\\u");
6✔
206
                writer.Write(((int)charDefaultValue).ToString("X"));
6✔
207
                writer.Write("'");
6✔
208
            }
209
            else
210
                writer.Write(InvariantValue(defaultValue));
357✔
211
        }
212

213
        writer.Write("; //Field offset: 0x");
15,378✔
214
        writer.Write(field.Offset.ToString("X"));
15,378✔
215

216
        if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0)
15,378!
NEW
217
            writer.Write(" || Has Field RVA (address hidden for diffability)");
×
218

219
        writer.WriteLine();
15,378✔
220
    }
15,378✔
221

222
    private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data)
223
    {
224
        var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)";
297✔
225

226
        if (TryAscendingInt32Array(data, out var ints))
297✔
227
        {
228
            writer.Write(" = new int[]");
15✔
229
            writer.Write(tail);
15✔
230
            writer.WriteLine();
15✔
231
            writer.WriteLine('{');
15✔
232
            writer.Indent++;
15✔
233
            for (var i = 0; i < ints.Length; i += 12)
78✔
234
            {
235
                var n = Math.Min(12, ints.Length - i);
24✔
236
                for (var j = 0; j < n; j++)
402✔
237
                {
238
                    if (j > 0) writer.Write(", ");
330✔
239
                    writer.Write(ints[i + j]);
177✔
240
                }
241
                if (i + n < ints.Length) writer.Write(',');
33✔
242
                writer.WriteLine();
24✔
243
            }
244
            writer.Indent--;
15✔
245
            writer.WriteLine("};");
15✔
246
            return;
15✔
247
        }
248

249
        writer.Write(" = new byte[]");
282✔
250
        writer.Write(tail);
282✔
251
        writer.WriteLine();
282✔
252
        writer.WriteLine('{');
282✔
253
        writer.Indent++;
282✔
254
        for (var i = 0; i < data.Length; i += 16)
16,578✔
255
        {
256
            var n = Math.Min(16, data.Length - i);
8,007✔
257
            for (var j = 0; j < n; j++)
268,896✔
258
            {
259
                if (j > 0) writer.Write(", ");
244,875✔
260
                writer.Write("0x");
126,441✔
261
                writer.Write(data[i + j].ToString("X2"));
126,441✔
262
            }
263
            if (i + n < data.Length) writer.Write(',');
15,732✔
264
            writer.WriteLine();
8,007✔
265
        }
266
        writer.Indent--;
282✔
267
        writer.WriteLine("};");
282✔
268
    }
282✔
269

270
    //blobs that decode as 0 followed by strictly ascending little-endian int32s are (probably) offset tables,
271
    //so show them as int[] rather than a hex dump
272
    private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int[]? ints)
273
    {
274
        ints = null;
297✔
275

276
        const int minElements = 8; //short blobs can pass the ascending check by pure coincidence
277
        if (b.Length < sizeof(int) * minElements || b.Length % sizeof(int) != 0)
297✔
278
            return false;
105✔
279

280
        var values = new int[b.Length / sizeof(int)];
192✔
281
        var prev = -1;
192✔
282

283
        for (var i = 0; i < values.Length; i++)
810✔
284
        {
285
            var v = BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(i * sizeof(int), sizeof(int)));
390✔
286

287
            if (i == 0 && v != 0)
390✔
288
                return false;
156✔
289

290
            if (v <= prev)
234✔
291
                return false;
21✔
292

293
            prev = v;
213✔
294
            values[i] = v;
213✔
295
        }
296

297
        ints = values;
15✔
298
        return true;
15✔
299
    }
300

301
    private static void AppendEvent(IndentedTextWriter writer, EventAnalysisContext evt)
302
    {
303
        //Custom attributes for event. Includes a trailing newline
304
        AppendCustomAttributes(writer, evt);
15✔
305

306
        //Event declaration line
307
        writer.Write(CsFileUtils.GetKeyWordsForEvent(evt));
15✔
308
        writer.Write(' ');
15✔
309
        writer.Write(CsFileUtils.GetTypeName(evt.EventType));
15✔
310
        writer.Write(' ');
15✔
311
        writer.Write(evt.Name);
15✔
312
        writer.WriteLine();
15✔
313
        writer.WriteLine('{');
15✔
314

315
        //Add/Remove/Invoke
316
        writer.Indent++;
15✔
317
        if (evt.Adder != null)
15!
318
            AppendAccessor(writer, evt.Adder, "add", evt.Visibility);
15✔
319
        if (evt.Remover != null)
15!
320
            AppendAccessor(writer, evt.Remover, "remove", evt.Visibility);
15✔
321
        if (evt.Invoker != null)
15!
NEW
322
            AppendAccessor(writer, evt.Invoker, "fire", evt.Visibility);
×
323
        writer.Indent--;
15✔
324

325
        writer.WriteLine('}');
15✔
326
        writer.WriteLineNoTabs(string.Empty);
15✔
327
    }
15✔
328

329
    private static void AppendProperty(IndentedTextWriter writer, PropertyAnalysisContext prop)
330
    {
331
        //Custom attributes for property. Includes a trailing newline
332
        AppendCustomAttributes(writer, prop);
5,730✔
333

334
        //Property declaration line
335
        writer.Write(CsFileUtils.GetKeyWordsForProperty(prop));
5,730✔
336
        writer.Write(' ');
5,730✔
337
        writer.Write(CsFileUtils.GetTypeName(prop.PropertyType));
5,730✔
338
        writer.Write(' ');
5,730✔
339
        writer.Write(prop.Name);
5,730✔
340
        writer.WriteLine();
5,730✔
341
        writer.WriteLine('{');
5,730✔
342

343
        //Get/Set
344
        writer.Indent++;
5,730✔
345
        if (prop.Getter != null)
5,730✔
346
            AppendAccessor(writer, prop.Getter, "get", prop.Visibility);
5,433✔
347
        if (prop.Setter != null)
5,730✔
348
            AppendAccessor(writer, prop.Setter, "set", prop.Visibility);
801✔
349
        writer.Indent--;
5,730✔
350

351
        writer.WriteLine('}');
5,730✔
352
        writer.WriteLineNoTabs(string.Empty);
5,730✔
353
    }
5,730✔
354

355
    private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContext method)
356
    {
357
        if (method is InjectedMethodAnalysisContext)
37,227!
358
            return;
×
359

360
        //Custom attributes for method. Includes a trailing newline
361
        AppendCustomAttributes(writer, method);
37,227✔
362

363
        //Method declaration line
364
        writer.Write(CsFileUtils.GetKeyWordsForMethod(method));
37,227✔
365
        writer.Write(' ');
37,227✔
366
        if (method.Name is not ".ctor" and not ".cctor")
37,227✔
367
        {
368
            writer.Write(CsFileUtils.GetTypeName(method.ReturnType));
31,050✔
369
            writer.Write(' ');
31,050✔
370
            writer.Write(method.Name);
31,050✔
371
        }
372
        else
373
        {
374
            //Constructor
375
            writer.Write(CsFileUtils.GetTypeName(method.DeclaringType!));
6,177✔
376
        }
377

378
        writer.Write('(');
37,227✔
379
        writer.Write(CsFileUtils.GetMethodParameterString(method));
37,227✔
380
        writer.Write(") { }");
37,227✔
381

382
        if (IncludeMethodLength)
37,227!
383
        {
NEW
384
            writer.Write(" //Length: ");
×
NEW
385
            writer.Write(method.RawBytes.Length);
×
386
        }
387

388
        writer.WriteLine();
37,227✔
389
        writer.WriteLineNoTabs(string.Empty);
37,227✔
390
    }
37,227✔
391

392
    //get/set/add/remove/raise
393
    private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, MethodAttributes parentVisibility)
394
    {
395
        //Custom attributes for accessor. Includes a trailing newline
396
        AppendCustomAttributes(writer, accessor);
6,264✔
397

398
        writer.Write(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility));
6,264✔
399
        writer.Write(' ');
6,264✔
400
        writer.Write(accessorType);
6,264✔
401
        writer.Write(" { } //Length: ");
6,264✔
402
        writer.Write(accessor.RawBytes.Length);
6,264✔
403
        writer.WriteLine();
6,264✔
404
    }
6,264✔
405

406
    private static void AppendCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner)
407
        => CsFileUtils.WriteCustomAttributeStrings(owner, writer, true, true);
72,315✔
408

409
    private static string InvariantValue(object? value)
410
        => value is null ? "" : value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString() ?? "";
10,314!
411
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc