@@ -32,6 +32,9 @@ internal sealed class MainFeature : Feature
3232#if NETCOREAPP
3333 private const string _jsonName = "json" ;
3434 internal readonly FlagInput JsonInput = new ( _jsonName , [ "-j" , "--json" ] , "Output to json file" ) ;
35+
36+ private const string _nestedName = "nested" ;
37+ internal readonly FlagInput NestedInput = new ( _nestedName , [ "-n" , "--nested" ] , "Output to nested json file" ) ;
3538#endif
3639
3740 private const string _noArchivesName = "no-archives" ;
@@ -63,6 +66,11 @@ internal sealed class MainFeature : Feature
6366 /// Enable JSON output
6467 /// </summary>
6568 public bool Json { get ; private set ; }
69+
70+ /// <summary>
71+ /// Enable nested JSON output
72+ /// </summary>
73+ public bool Nested { get ; private set ; }
6674#endif
6775
6876 public MainFeature ( )
@@ -73,6 +81,7 @@ public MainFeature()
7381 Add ( DebugInput ) ;
7482 Add ( FileOnlyInput ) ;
7583#if NETCOREAPP
84+ JsonInput . Add ( NestedInput ) ;
7685 Add ( JsonInput ) ;
7786#endif
7887 Add ( NoContentsInput ) ;
@@ -93,6 +102,7 @@ public override bool Execute()
93102 FileOnly = GetBoolean ( _fileOnlyName ) ;
94103#if NETCOREAPP
95104 Json = GetBoolean ( _jsonName ) ;
105+ Nested = GetBoolean ( _nestedName ) ;
96106#endif
97107
98108 // Create scanner for all paths
@@ -248,9 +258,62 @@ private void WriteProtectionResultJson(string path, Dictionary<string, List<stri
248258 // Attempt to open a protection file for writing
249259 using var jsw = new StreamWriter ( File . OpenWrite ( $ "protection-{ DateTime . Now : yyyy-MM-dd_HHmmss.ffff} .json") ) ;
250260
251- // Create the output data
252261 var jsonSerializerOptions = new System . Text . Json . JsonSerializerOptions { WriteIndented = true } ;
253- string serializedData = System . Text . Json . JsonSerializer . Serialize ( protections , jsonSerializerOptions ) ;
262+ string serializedData ;
263+ if ( Nested )
264+ {
265+ // A nested dictionary is used to achieve proper serialization.
266+ var nestedDictionary = new Dictionary < string , object > ( ) ;
267+ var trimmedPath = path . TrimEnd ( [ '\\ ' , '/' ] ) ;
268+
269+ // Sort the keys for consistent output
270+ string [ ] keys = [ .. protections . Keys ] ;
271+ Array . Sort ( keys ) ;
272+
273+ var modifyNodeList = new List < ( Dictionary < string , object > , string , string [ ] ) > ( ) ;
274+
275+ // Loop over all keys
276+ foreach ( string key in keys )
277+ {
278+ // Skip over files with no protection
279+ var value = protections [ key ] ;
280+ if ( value . Count == 0 )
281+ continue ;
282+
283+ // Sort the detected protections for consistent output
284+ string [ ] fileProtections = [ .. value ] ;
285+ Array . Sort ( fileProtections ) ;
286+
287+ // Inserts key and protections into nested dictionary, with the key trimmed of the base path.
288+ InsertNode ( nestedDictionary , key . Substring ( trimmedPath . Length ) , fileProtections , modifyNodeList ) ;
289+ }
290+
291+ // Adds the non-leaf-node protections back in
292+ for ( int i = 0 ; i < modifyNodeList . Count ; i ++ )
293+ {
294+ var copyDictionary = modifyNodeList [ i ] . Item1 [ modifyNodeList [ i ] . Item2 ] ;
295+
296+ var modifyNode = new List < object > ( ) ;
297+ modifyNode . Add ( modifyNodeList [ i ] . Item3 ) ;
298+ modifyNode . Add ( copyDictionary ) ;
299+
300+ modifyNodeList [ i ] . Item1 [ modifyNodeList [ i ] . Item2 ] = modifyNode ;
301+ }
302+
303+ // Move nested dictionary into final dictionary with the base path as a key.
304+ var finalDictionary = new Dictionary < string , Dictionary < string , object > > ( )
305+ {
306+ { trimmedPath , nestedDictionary }
307+ } ;
308+
309+ // Create the output data
310+ serializedData = System . Text . Json . JsonSerializer . Serialize ( finalDictionary , jsonSerializerOptions ) ;
311+ }
312+ else
313+ {
314+ // Create the output data
315+ serializedData = System . Text . Json . JsonSerializer . Serialize ( protections , jsonSerializerOptions ) ;
316+ }
254317
255318 // Write the output data
256319 // TODO: this prints plus symbols wrong, probably some other things too
@@ -263,6 +326,48 @@ private void WriteProtectionResultJson(string path, Dictionary<string, List<stri
263326 Console . WriteLine ( ) ;
264327 }
265328 }
329+
330+ /// <summary>
331+ /// Inserts file protection dictionary entries into a nested dictionary based on path
332+ /// </summary>
333+ /// <param name="nestedDictionary">File or directory path</param>
334+ /// <param name="path">The "key" for the given protection entry, already trimmed of its base path</param>
335+ /// <param name="protections">The scanned protection(s) for a given file</param>
336+ public static void InsertNode ( Dictionary < string , object > nestedDictionary , string path , string [ ] protections , List < ( Dictionary < string , object > , string , string [ ] ) > modifyNodeList )
337+ {
338+ var current = nestedDictionary ;
339+ var pathParts = path . Split ( Path . DirectorySeparatorChar , StringSplitOptions . RemoveEmptyEntries ) ;
340+
341+ // Traverses the nested dictionary until the "leaf" dictionary is reached.
342+ for ( int i = 0 ; i < pathParts . Length - 1 ; i ++ )
343+ {
344+ var part = pathParts [ i ] ;
345+
346+ // Inserts new subdictionaries if one doesn't already exist
347+ if ( ! current . ContainsKey ( part ) )
348+ {
349+ var innerDictionary = new Dictionary < string , object > ( ) ;
350+ current [ part ] = innerDictionary ;
351+ current = innerDictionary ;
352+ continue ;
353+ }
354+
355+ var innerObject = current [ part ] ;
356+
357+ // Handle instances where a protection was already assigned to the current node
358+ if ( innerObject is string [ ] existingProtections )
359+ {
360+ modifyNodeList . Add ( ( current , part , existingProtections ) ) ;
361+ innerObject = new Dictionary < string , object > ( ) ;
362+ }
363+
364+ current [ part ] = innerObject ;
365+ current = ( Dictionary < string , object > ) current [ part ] ;
366+ }
367+
368+ // If the "leaf" dictionary has been reached, add the file and its protections.
369+ current . Add ( pathParts [ ^ 1 ] , protections ) ;
370+ }
266371#endif
267372 }
268373}
0 commit comments