Skip to content

Commit

Permalink
Add fingerprint command, to both assembly and appbundle branches
Browse files Browse the repository at this point in the history
... to uniquely identify assemblies and appbundles using `Info.plist`
  • Loading branch information
spouliot committed Oct 20, 2022
1 parent 18b2883 commit 6fb183a
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 1 deletion.
3 changes: 2 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# ChangeLog

## 1.2.0 (unreleased)
## 1.2.0 (20 Oct 2022)

### Features
- Add `appbundle` branch and [`is-trimmable`](https://github.com/spouliot/cilout/wiki/AppBundleIsTrimmable) command to detect any trimmable assembly inside the app bundle
- Add `has-entrypoint` command, to both `assembly` and `appbundle` branches, to detect if the assembly has an entry point
- Add `fingerprint` command, to both `assembly` and `appbundle` branches, to uniquely identify assemblies and appbundle using `Info.plist`

### Updates
- Updated Spectre.Console to 0.45.0
Expand Down
93 changes: 93 additions & 0 deletions Commands/AppBundleFingerprintCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Spectre.Console.Cli;
using Claunia.PropertyList;

namespace CilOut.Commands;

public sealed class AppBundleFingerprintCommand : Command<AppBundleFingerprintCommand.Settings> {

public sealed class Settings : AppBundleSettings {

[Description ("Output using a diff-friendly, tab seperated values, format")]
[CommandOption ("-d|--diff-friendly")]
[DefaultValue (false)]
public bool DiffFriendly { get; init; }
}

public override int Execute ([NotNull] CommandContext context, [NotNull] Settings settings)
{
var result = ReturnCodes.Success;
var appbundle = settings.AppBundle!;
SortedDictionary<string, SortedDictionary<string, string>> fingerprints = new ();
foreach (var file in Directory.EnumerateFiles (appbundle, "*", SearchOption.AllDirectories)) {
switch (Path.GetExtension (file).ToLowerInvariant ()) {
case ".dll":
case ".exe":
var assembly_result = AssemblyFingerprintCommand.Fingerprint (file, out var minutiae);
if (assembly_result == ReturnCodes.Success)
fingerprints.Add (file, minutiae);

if (assembly_result != ReturnCodes.Success)
result = assembly_result;
break;
case ".plist":
try {
fingerprints.Add (file, ParseInfoPlist (file));
}
catch {
result = ReturnCodes.Failure;
}
break;
}
}

foreach (var (file, minutiae) in fingerprints) {
var n = appbundle.Length;
if (file [n] == '/')
n++;
var assembly = file [n..];
if (settings.DiffFriendly) {
AssemblyFingerprintCommand.TabSeparatedValues (assembly, minutiae);
} else {
AssemblyFingerprintCommand.ShowResult (assembly, result, minutiae);
}
}

return (int) result;
}

static SortedDictionary<string,string> ParseInfoPlist (string filename)
{
SortedDictionary<string,string> minutiae = new ();
NSDictionary? info = (NSDictionary) PropertyListParser.Parse (filename);
if (info is not null) {
foreach (var element in info) {
switch (element.Key) {
case "DTCompiler":
case "DTPlatformBuild":
case "DTPlatformName":
case "DTPlatformVersion":
case "DTSDKBuild":
case "DTSDKName":
case "DTXcode":
case "DTXcodeBuild":
minutiae.Add (element.Key, element.Value.ToString ()!);
break;
case "com.microsoft.ios":
case "com.microsoft.macos":
case "com.microsoft.maccatalyst":
case "com.microsoft.tvos":
case "com.microsoft.watchos":
NSDictionary? dict = (NSDictionary) element.Value;
if (dict is not null) {
foreach (var subelement in dict)
minutiae.Add (element.Key + "/" + subelement.Key, subelement.Value.ToString ()!);
}
break;
}
}
}
return minutiae;
}
}
107 changes: 107 additions & 0 deletions Commands/AssemblyFingerprintCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Mono.Cecil;
using Spectre.Console;
using Spectre.Console.Cli;

namespace CilOut.Commands;

public sealed class AssemblyFingerprintCommand : Command<AssemblyFingerprintCommand.Settings> {

public sealed class Settings : AssemblySettings {

[Description ("Output using a diff-friendly, tab seperated values, format")]
[CommandOption ("-d|--diff-friendly")]
[DefaultValue (false)]
public bool DiffFriendly { get; init; }
}

public override int Execute ([NotNull] CommandContext context, [NotNull] Settings settings)
{
var assembly = settings.Assembly!;
var result = Fingerprint (assembly, out var minutiae);

if (settings.DiffFriendly) {
TabSeparatedValues (Path.GetFileName (assembly), minutiae);
} else {
ShowResult (assembly, result, minutiae);
}

return (int) result;
}

public static void TabSeparatedValues (string assemblyName, SortedDictionary<string,string> minutiae)
{
foreach (var (key, value) in minutiae) {
Console.Write (assemblyName);
Console.Write ('\t');
Console.Write (key);
Console.Write ('\t');
Console.WriteLine (value);
}
}

public static void ShowResult (string assemblyName, ReturnCodes result, SortedDictionary<string,string> minutiae)
{
var asm = assemblyName.EscapeMarkup ();
switch (result) {
case ReturnCodes.Success:
AnsiConsole.WriteLine ($"Assembly `{asm}` fingerprint is composed of");
foreach (var (key, value) in minutiae) {
AnsiConsole.Write (key);
AnsiConsole.Write (" : '");
AnsiConsole.Write (value);
AnsiConsole.WriteLine ('\'');
}
break;
case ReturnCodes.Failure:
AnsiConsole.MarkupLine ($"Assembly `{asm}` has [bold]no[/] minutiae.");
break;
case ReturnCodes.CouldNotReadAssembly:
AnsiConsole.MarkupLine ($"[red]Error: [/] Could not read assembly `{asm}`.");
break;
}
}

public static ReturnCodes Fingerprint (string assembly, out SortedDictionary<string,string> minutiae)
{
minutiae = new ();
try {
AssemblyDefinition ad = AssemblyDefinition.ReadAssembly (assembly);
// FullName includes [Assembly]Version, Culture and PublicKeyToken
minutiae.Add ("FullName", ad.FullName);
if (ad.HasCustomAttributes) {
foreach (var ca in ad.CustomAttributes) {
switch (ca.AttributeType.FullName) {
case "System.Reflection.AssemblyCompanyAttribute":
case "System.Reflection.AssemblyConfigurationAttribute":
case "System.Reflection.AssemblyFileVersionAttribute": // can differ from AssemblyVersion
case "System.Reflection.AssemblyInformationalVersionAttribute":
case "System.Runtime.Versioning.TargetFrameworkAttribute":
case "System.Runtime.Versioning.TargetPlatformAttribute":
case "System.Runtime.Versioning.SupportedOSPlatform":
minutiae.Add (ca.AttributeType.Name, GetConstructorArgument (ca, 0));
break;
case "System.Reflection.AssemblyMetadataAttribute":
minutiae.Add (ca.AttributeType.Name + "." + GetConstructorArgument (ca, 0), GetConstructorArgument (ca, 1));
break;
}
}
}
return minutiae.Count > 0 ? ReturnCodes.Success : ReturnCodes.Failure;
} catch (Exception e) {
if (Environment.GetEnvironmentVariable ("V") is not null)
AnsiConsole.WriteException (e);
return ReturnCodes.CouldNotReadAssembly;
}
}

static string GetConstructorArgument (CustomAttribute ca, int n)
{
if (!ca.HasConstructorArguments)
return "";
if (n >= ca.ConstructorArguments.Count)
return "";
return ca.ConstructorArguments [0].Value.ToString () ?? "";
}
}
4 changes: 4 additions & 0 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
config.Settings.ApplicationName = "cilout";
config.AddBranch<AssemblySettings> ("assembly", add => {
add.SetDescription ("Assembly to analyze");
add.AddCommand<AssemblyFingerprintCommand> ("fingerprint")
.WithDescription ("Generate a fingerprint of the assembly to detect future changes.");
add.AddCommand<AssemblyIsTrimmableCommand> ("is-trimmable")
.WithDescription ("Check for the presence of [[assembly: AssemblyMetadata (\"Trimmable\", \"true\")]] inside the specified assembly.");
add.AddCommand<AssemblyHasEntryPointCommand> ("has-entrypoint")
Expand All @@ -18,6 +20,8 @@
});
config.AddBranch<AppBundleSettings> ("appbundle", add => {
add.SetDescription ("Application Bundle to analyze");
add.AddCommand<AppBundleFingerprintCommand> ("fingerprint")
.WithDescription ("Generate a fingerprint of the appbundle to detect future changes.");
add.AddCommand<AppBundleIsTrimmableCommand> ("is-trimmable")
.WithDescription ("Check for the presence of [[assembly: AssemblyMetadata (\"Trimmable\", \"true\")]] inside any assemblies of the appbundle.");
add.AddCommand<AppBundleHasEntryPointCommand> ("has-entrypoint")
Expand Down
1 change: 1 addition & 0 deletions cilout.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="plist-cil" Version="2.2.0" />
<PackageReference Include="Spectre.Console" Version="0.45.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.45.0" />
</ItemGroup>
Expand Down

0 comments on commit 6fb183a

Please sign in to comment.