diff --git a/ProtectionScan/Features/MainFeature.cs b/ProtectionScan/Features/MainFeature.cs index c4c206f4..5276a0e4 100644 --- a/ProtectionScan/Features/MainFeature.cs +++ b/ProtectionScan/Features/MainFeature.cs @@ -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"); #endif private const string _noArchivesName = "no-archives"; @@ -63,6 +66,11 @@ internal sealed class MainFeature : Feature /// Enable JSON output /// public bool Json { get; private set; } + + /// + /// Enable nested JSON output + /// + public bool Nested { get; private set; } #endif public MainFeature() @@ -73,6 +81,7 @@ public MainFeature() Add(DebugInput); Add(FileOnlyInput); #if NETCOREAPP + JsonInput.Add(NestedInput); Add(JsonInput); #endif Add(NoContentsInput); @@ -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 @@ -248,9 +258,48 @@ private void WriteProtectionResultJson(string path, Dictionary(); + 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); + + // 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>() + { + {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 @@ -263,6 +312,55 @@ private void WriteProtectionResultJson(string path, Dictionary + /// Inserts file protection dictionary entries into a nested dictionary based on path + /// + /// File or directory path + /// The "key" for the given protection entry, already trimmed of its base path + /// The scanned protection(s) for a given file + public static void DeepInsert(Dictionary 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(); + current[part] = innerObject; + current = innerObject; + } + else // Traverses already existing subdictionaries + { + var innerObject = current[part]; + + // Handle instances where a protection was already assigned to the current node + if (innerObject is string[]) + { + current[part] = new Dictionary(); + current = (Dictionary)current[part]; + current.Add("", innerObject); + } + else + { + current[part] = innerObject; + current = (Dictionary)innerObject; + } + } + } + else // If the "leaf" dictionary has been reached, add the file and its protections. + { + current.Add(part, protections); + } + } + } #endif } }