Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions ProtectionScan/Features/MainFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ internal sealed class MainFeature : Feature
#if NETCOREAPP
private const string _jsonName = "json";
internal readonly FlagInput JsonInput = new(_jsonName, ["-j", "--json"], "Output to json file");

private const string _nestedName = "nested";
internal readonly FlagInput NestedInput = new(_nestedName, ["-n", "--nested"], "Output to nested json file if json is already enabled");
#endif

private const string _noArchivesName = "no-archives";
Expand Down Expand Up @@ -63,6 +66,11 @@ internal sealed class MainFeature : Feature
/// Enable JSON output
/// </summary>
public bool Json { get; private set; }

/// <summary>
/// Enable nested JSON output
/// </summary>
public bool Nested { get; private set; }
#endif

public MainFeature()
Expand All @@ -74,6 +82,7 @@ public MainFeature()
Add(FileOnlyInput);
#if NETCOREAPP
Add(JsonInput);
Add(NestedInput);
#endif
Add(NoContentsInput);
Add(NoArchivesInput);
Expand All @@ -93,6 +102,7 @@ public override bool Execute()
FileOnly = GetBoolean(_fileOnlyName);
#if NETCOREAPP
Json = GetBoolean(_jsonName);
Nested = GetBoolean(_nestedName);
#endif

// Create scanner for all paths
Expand Down Expand Up @@ -248,9 +258,51 @@ private void WriteProtectionResultJson(string path, Dictionary<string, List<stri
// Attempt to open a protection file for writing
using var jsw = new StreamWriter(File.OpenWrite($"protection-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.json"));

// Create the output data
var jsonSerializerOptions = new System.Text.Json.JsonSerializerOptions { WriteIndented = true };
string serializedData = System.Text.Json.JsonSerializer.Serialize(protections, jsonSerializerOptions);
string serializedData;
if (Nested)
{
// A nested dictionary is used in order to avoid complex and unnecessary custom serialization.
// A dictionary with an object value is used so that it's not necessary to first parse entries into a
// traditional node system and then bubble up the entire chain creating non-object dictionaries.
var nestedDictionary = new Dictionary<string, object>();
var trimmedPath = path.TrimEnd(['\\', '/']);

// Sort the keys for consistent output
string[] keys = [.. protections.Keys];
Array.Sort(keys);

// Loop over all keys
foreach (string key in keys)
{
// Skip over files with no protection
var value = protections[key];
if (value.Count == 0)
continue;

// Sort the detected protections for consistent output
string[] fileProtections = [.. value];
Array.Sort(fileProtections);
//foreach (var fileProtection in fileProtections)

// Inserts key and protections into nested dictionary, with the key trimmed of the base path.
DeepInsert(nestedDictionary, key.Substring(trimmedPath.Length), fileProtections);
}

// Move nested dictionary into final dictionary with the base path as a key.
var finalDictionary = new Dictionary<string, Dictionary<string, object>>()
{
{trimmedPath, nestedDictionary}
};

// Create the output data
serializedData = System.Text.Json.JsonSerializer.Serialize(finalDictionary, jsonSerializerOptions);
}
else
{
// Create the output data
serializedData = System.Text.Json.JsonSerializer.Serialize(protections, jsonSerializerOptions);
}

// Write the output data
// TODO: this prints plus symbols wrong, probably some other things too
Expand All @@ -263,6 +315,60 @@ private void WriteProtectionResultJson(string path, Dictionary<string, List<stri
Console.WriteLine();
}
}

/// <summary>
/// Inserts file protection dictionary entries into a nested dictionary based on path
/// </summary>
/// <param name="nestedDictionary">File or directory path</param>
/// <param name="path">The "key" for the given protection entry, already trimmed of its base path</param>
/// <param name="protections">The scanned protection(s) for a given file</param>
public static void DeepInsert(Dictionary<string, object> nestedDictionary, string path, string[] protections)
{
var current = nestedDictionary;
path = path.TrimStart(Path.DirectorySeparatorChar);
var pathParts = path.Split(Path.DirectorySeparatorChar);

// Traverses the nested dictionary until the "leaf" dictionary is reached.
for (int i = 0; i < pathParts.Length; i++)
{
var part = pathParts[i];
if (i != (pathParts.Length - 1))
{
if (!current.ContainsKey(part)) // Inserts new subdictionaries if one doesn't already exist
{
var innerObject = new Dictionary<string, object>();
current[part] = innerObject;
current = innerObject;
}
else // Traverses already existing subdictionaries
{
var innerObject = current[part];

// If i.e. a packer has protections detected on it, and then files within it also have
// detections of their own, the later traversal of the files within it will fail, as
// the subdictionary for that packer has already been set to <string, string>. The existing
// value must be pulled, then the new subdictionary can be added, and then the existing value
// can be re-added within the packer with a key of an empty string, in order to indicate it's
// for the packer itself, and to avoid potential future collisions.
if (innerObject is string[])
{
current[part] = new Dictionary<string, object>();
current = (Dictionary<string, object>)current[part];
current.Add("", innerObject);
}
else
{
current[part] = innerObject;
current = (Dictionary<string, object>)innerObject;
}
}
}
else // If the "leaf" dictionary has been reached, add the file and its protections.
{
current.Add(part, protections);
}
}
}
#endif
}
}