From 1f1902d7d00fe3b10754414923ce4f9ae2a9b289 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 10 Sep 2025 17:49:04 -0300 Subject: [PATCH 01/23] should fix issue... who knows --- .../Commands/GetADTreeGroupMemberCommand.cs | 24 +-- ...etADTreePrincipalGroupMembershipCommand.cs | 34 ++-- src/PSADTree/Exceptions.cs | 12 +- src/PSADTree/PSADTreeCmdletBase.cs | 4 +- src/PSADTree/TreeExtensions.cs | 153 ++++++++++++++---- src/PSADTree/TreeGroup.cs | 1 - src/PSADTree/TreeIndex.cs | 2 +- 7 files changed, 163 insertions(+), 67 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 1c32970..0547d95 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Management.Automation; @@ -28,7 +29,7 @@ protected override void ProcessRecord() using GroupPrincipal? group = GroupPrincipal.FindByIdentity(_context, Identity); if (group is null) { - WriteError(Exceptions.IdentityNotFound(Identity)); + WriteError(Identity.ToIdentityNotFound()); return; } @@ -45,11 +46,11 @@ protected override void ProcessRecord() } catch (MultipleMatchesException exception) { - WriteError(exception.AmbiguousIdentity(Identity)); + WriteError(exception.ToAmbiguousIdentity(Identity)); } catch (Exception exception) { - WriteError(exception.Unspecified(Identity)); + WriteError(exception.ToUnspecified(Identity)); } } @@ -96,11 +97,9 @@ private TreeObjectBase[] Traverse( continue; } - using PrincipalSearchResult? search = current?.GetMembers(); - - if (search is not null) + if (current is not null) { - EnumerateMembers(treeGroup, search, source, depth); + EnumerateMembers(treeGroup, current, source, depth); } _index.Add(treeGroup); @@ -113,7 +112,7 @@ private TreeObjectBase[] Traverse( } catch (Exception exception) { - WriteError(exception.EnumerationFailure(current)); + WriteError(exception.ToEnumerationFailure(current)); } } @@ -122,11 +121,16 @@ private TreeObjectBase[] Traverse( private void EnumerateMembers( TreeGroup parent, - PrincipalSearchResult searchResult, + GroupPrincipal group, string source, int depth) { - foreach (Principal member in searchResult.GetSortedEnumerable(_comparer)) + IEnumerable members = group.ToSafeSortedEnumerable( + selector: group => group.GetMembers(), + cmdlet: this, + comparer: Comparer); + + foreach (Principal member in members) { IDisposable? disposable = null; try diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index bf00155..944607d 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Management.Automation; @@ -32,18 +33,18 @@ protected override void ProcessRecord() } catch (MultipleMatchesException exception) { - WriteError(exception.AmbiguousIdentity(Identity)); + WriteError(exception.ToAmbiguousIdentity(Identity)); return; } catch (Exception exception) { - WriteError(exception.Unspecified(Identity)); + WriteError(exception.ToUnspecified(Identity)); return; } if (principal is null) { - WriteError(Exceptions.IdentityNotFound(Identity)); + WriteError(Identity.ToIdentityNotFound()); return; } @@ -70,8 +71,12 @@ protected override void ProcessRecord() try { - using PrincipalSearchResult search = principal.GetGroups(_context); - foreach (Principal parent in search.GetSortedEnumerable(_comparer)) + IEnumerable groups = principal.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(_context), + cmdlet: this, + comparer: Comparer); + + foreach (Principal parent in groups) { if (ShouldExclude(parent, _exclusionPatterns)) { @@ -89,7 +94,7 @@ protected override void ProcessRecord() } catch (Exception exception) { - WriteError(exception.EnumerationFailure(null)); + WriteError(exception.ToEnumerationFailure(null)); } finally { @@ -139,11 +144,14 @@ private TreeObjectBase[] Traverse(string source) continue; } - using PrincipalSearchResult? search = current?.GetGroups(_context); - - if (search is not null) + if (current is not null) { - EnumerateMembership(treeGroup, search, source, depth); + IEnumerable groups = current.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(_context), + cmdlet: this, + comparer: Comparer); + + EnumerateMembership(treeGroup, groups, source, depth); } _index.Add(treeGroup); @@ -155,7 +163,7 @@ private TreeObjectBase[] Traverse(string source) } catch (Exception exception) { - WriteError(exception.EnumerationFailure(current)); + WriteError(exception.ToEnumerationFailure(current)); } } @@ -164,11 +172,11 @@ private TreeObjectBase[] Traverse(string source) private void EnumerateMembership( TreeGroup parent, - PrincipalSearchResult searchResult, + IEnumerable groups, string source, int depth) { - foreach (Principal group in searchResult.GetSortedEnumerable(_comparer)) + foreach (Principal group in groups) { if (ShouldExclude(group, _exclusionPatterns)) { diff --git a/src/PSADTree/Exceptions.cs b/src/PSADTree/Exceptions.cs index 8537db3..723100f 100644 --- a/src/PSADTree/Exceptions.cs +++ b/src/PSADTree/Exceptions.cs @@ -6,22 +6,22 @@ namespace PSADTree; internal static class Exceptions { - internal static ErrorRecord IdentityNotFound(string? identity) => + internal static ErrorRecord ToIdentityNotFound(this string? identity) => new( new NoMatchingPrincipalException($"Cannot find an object with identity: '{identity}'."), "IdentityNotFound", ErrorCategory.ObjectNotFound, identity); - internal static ErrorRecord AmbiguousIdentity(this Exception exception, string? identity) => + internal static ErrorRecord ToAmbiguousIdentity(this Exception exception, string? identity) => new(exception, "AmbiguousIdentity", ErrorCategory.InvalidResult, identity); - internal static ErrorRecord Unspecified(this Exception exception, string? identity) => + internal static ErrorRecord ToUnspecified(this Exception exception, string? identity) => new(exception, "Unspecified", ErrorCategory.NotSpecified, identity); - internal static ErrorRecord EnumerationFailure(this Exception exception, GroupPrincipal? groupPrincipal) => - new(exception, "EnumerationFailure", ErrorCategory.NotSpecified, groupPrincipal); + internal static ErrorRecord ToEnumerationFailure(this Exception exception, Principal? principal) => + new(exception, "EnumerationFailure", ErrorCategory.NotSpecified, principal); - internal static ErrorRecord SetPrincipalContext(this Exception exception) => + internal static ErrorRecord ToSetPrincipalContext(this Exception exception) => new(exception, "SetPrincipalContext", ErrorCategory.ConnectionError, null); } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index d7c9bf5..d47103f 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -24,7 +24,7 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable internal readonly TreeIndex _index = new(); - internal PSADTreeComparer _comparer = new(); + internal PSADTreeComparer Comparer { get; } = new(); protected WildcardPattern[]? _exclusionPatterns; @@ -91,7 +91,7 @@ protected override void BeginProcessing() } catch (Exception exception) { - ThrowTerminatingError(exception.SetPrincipalContext()); + ThrowTerminatingError(exception.ToSetPrincipalContext()); } } diff --git a/src/PSADTree/TreeExtensions.cs b/src/PSADTree/TreeExtensions.cs index 551edbc..bccd6b6 100644 --- a/src/PSADTree/TreeExtensions.cs +++ b/src/PSADTree/TreeExtensions.cs @@ -1,5 +1,9 @@ +using System; +using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Linq; +using System.Management.Automation; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; @@ -11,69 +15,150 @@ internal static class TreeExtensions "(?<=,)DC=.+$", RegexOptions.Compiled); - private static readonly StringBuilder s_sb = new(); - +#if !NETCOREAPP + [ThreadStatic] + private static StringBuilder? s_sb; +#endif internal static string Indent(this string inputString, int indentation) { - s_sb.Clear(); + const string corner = "└── "; + int repeatCount = (4 * indentation) - 4; + int capacity = repeatCount + 4 + inputString.Length; + +#if NETCOREAPP + return string.Create( + capacity, (repeatCount, corner, inputString), + static (buffer, state) => + { + int count = state.repeatCount; + buffer[..count].Fill(' '); + state.corner.AsSpan().CopyTo(buffer[count..]); + state.inputString.AsSpan().CopyTo(buffer[(count + 4)..]); + }); +#else + s_sb ??= new StringBuilder(64); + s_sb.Clear().EnsureCapacity(capacity); return s_sb - .Append(' ', (4 * indentation) - 4) - .Append("└── ") + .Append(' ', repeatCount) + .Append(corner) .Append(inputString) .ToString(); +#endif } - internal static string GetDefaultNamingContext(this string distinguishedName) => - s_reDefaultNamingContext.Match(distinguishedName).Value; - - internal static TreeObjectBase[] ConvertToTree( - this TreeObjectBase[] inputObject) + internal static TreeObjectBase[] Format( + this TreeObjectBase[] tree) { int index; - TreeObjectBase current; - for (int i = 0; i < inputObject.Length; i++) + for (int i = 0; i < tree.Length; i++) { - current = inputObject[i]; + TreeObjectBase current = tree[i]; + if ((index = current.Hierarchy.IndexOf('└')) == -1) { continue; } - int z; - char[] replace; - for (z = i - 1; z >= 0; z--) + for (int z = i - 1; z >= 0; z--) { - current = inputObject[z]; - if (!char.IsWhiteSpace(current.Hierarchy[index])) + current = tree[z]; + string hierarchy = current.Hierarchy; + + if (char.IsWhiteSpace(hierarchy[index])) { - UpdateCorner(index, current); - break; + current.Hierarchy = hierarchy.ReplaceAt(index, '│'); + continue; + } + + if (hierarchy[index] == '└') + { + current.Hierarchy = hierarchy.ReplaceAt(index, '├'); } - replace = current.Hierarchy.ToCharArray(); - replace[index] = '│'; - current.Hierarchy = new string(replace); + break; } } - return inputObject; + return tree; } - internal static IOrderedEnumerable GetSortedEnumerable( - this PrincipalSearchResult search, PSADTreeComparer comparer) => - search - .OrderBy(static e => e.StructuralObjectClass == "group") - .ThenBy(static e => e, comparer); +#if NETCOREAPP + [SkipLocalsInit] +#endif + private static unsafe string ReplaceAt(this string input, int index, char newChar) + { +#if NETCOREAPP + return string.Create( + input.Length, (input, index, newChar), + static (buffer, state) => + { + state.input.AsSpan().CopyTo(buffer); + buffer[state.index] = state.newChar; + }); +#else + if (input.Length > 0x200) + { + char[] chars = input.ToCharArray(); + chars[index] = newChar; + return new string(chars); + } + char* pChars = stackalloc char[0x200]; + fixed (char* source = input) + { + Buffer.MemoryCopy( + source, + pChars, + 0x200 * sizeof(char), + input.Length * sizeof(char)); + } - private static void UpdateCorner(int index, TreeObjectBase current) + pChars[index] = newChar; + return new string(pChars, 0, input.Length); +#endif + } + + internal static IEnumerable ToSafeSortedEnumerable( + this TPrincipal principal, + Func> selector, + PSCmdlet cmdlet, + PSADTreeComparer comparer) + where TPrincipal : Principal { - if (current.Hierarchy[index] == '└') + List principals = []; + using PrincipalSearchResult search = selector(principal); + using IEnumerator enumerator = search.GetEnumerator(); + + while (true) { - char[] replace = current.Hierarchy.ToCharArray(); - replace[index] = '├'; - current.Hierarchy = new string(replace); + try + { + if (!enumerator.MoveNext()) + { + break; + } + + principals.Add(enumerator.Current); + } + catch (Exception exception) + { + cmdlet.WriteError(exception.ToEnumerationFailure(principal)); + continue; + } } + + return principals + .OrderBy(static e => e.StructuralObjectClass == "group") + .ThenBy(static e => e, comparer); } + + // internal static IOrderedEnumerable GetSortedEnumerable( + // this PrincipalSearchResult search, PSADTreeComparer comparer) => + // search + // .OrderBy(static e => e.StructuralObjectClass == "group") + // .ThenBy(static e => e, comparer); + + internal static string GetDefaultNamingContext(this string distinguishedName) => + s_reDefaultNamingContext.Match(distinguishedName).Value; } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 32a34e0..bbf0b56 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.DirectoryServices.AccountManagement; diff --git a/src/PSADTree/TreeIndex.cs b/src/PSADTree/TreeIndex.cs index 10c66d9..6e33dae 100644 --- a/src/PSADTree/TreeIndex.cs +++ b/src/PSADTree/TreeIndex.cs @@ -27,7 +27,7 @@ internal void TryAddPrincipals() } } - internal TreeObjectBase[] GetTree() => _output.ToArray().ConvertToTree(); + internal TreeObjectBase[] GetTree() => _output.ToArray().Format(); internal void Clear() { From f1766507f564612d459d2c9ff5353f2e74130d55 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 10 Sep 2025 18:04:39 -0300 Subject: [PATCH 02/23] bump module ver --- module/PSADTree.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/PSADTree.psd1 b/module/PSADTree.psd1 index c8448c4..0e1db7f 100644 --- a/module/PSADTree.psd1 +++ b/module/PSADTree.psd1 @@ -16,7 +16,7 @@ } # Version number of this module. - ModuleVersion = '1.1.5' + ModuleVersion = '1.1.6' # Supported PSEditions # CompatiblePSEditions = @() From 548c87cbc7a90877883c6f3fd153d5f0c829b545 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 10 Sep 2025 18:06:52 -0300 Subject: [PATCH 03/23] remove commented method, no longer needed --- src/PSADTree/TreeExtensions.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/PSADTree/TreeExtensions.cs b/src/PSADTree/TreeExtensions.cs index bccd6b6..cd5a9e4 100644 --- a/src/PSADTree/TreeExtensions.cs +++ b/src/PSADTree/TreeExtensions.cs @@ -153,12 +153,6 @@ internal static IEnumerable ToSafeSortedEnumerable( .ThenBy(static e => e, comparer); } - // internal static IOrderedEnumerable GetSortedEnumerable( - // this PrincipalSearchResult search, PSADTreeComparer comparer) => - // search - // .OrderBy(static e => e.StructuralObjectClass == "group") - // .ThenBy(static e => e, comparer); - internal static string GetDefaultNamingContext(this string distinguishedName) => s_reDefaultNamingContext.Match(distinguishedName).Value; } From 09ecb5705f6b94ace2d8033532ca3c0b899dfa8c Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 10 Sep 2025 18:44:48 -0300 Subject: [PATCH 04/23] remove unnecessary `continue` --- src/PSADTree/TreeExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PSADTree/TreeExtensions.cs b/src/PSADTree/TreeExtensions.cs index cd5a9e4..7d4b5d3 100644 --- a/src/PSADTree/TreeExtensions.cs +++ b/src/PSADTree/TreeExtensions.cs @@ -144,7 +144,6 @@ internal static IEnumerable ToSafeSortedEnumerable( catch (Exception exception) { cmdlet.WriteError(exception.ToEnumerationFailure(principal)); - continue; } } From 97be6e3e8833064858ce504a1525c8fd4cc8ce8d Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 10 Sep 2025 18:48:06 -0300 Subject: [PATCH 05/23] add preprocessor directives for using statements --- src/PSADTree/TreeExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PSADTree/TreeExtensions.cs b/src/PSADTree/TreeExtensions.cs index 7d4b5d3..8f3d317 100644 --- a/src/PSADTree/TreeExtensions.cs +++ b/src/PSADTree/TreeExtensions.cs @@ -3,8 +3,11 @@ using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; +#if NETCOREAPP using System.Runtime.CompilerServices; +#else using System.Text; +#endif using System.Text.RegularExpressions; namespace PSADTree; From 90c8db45f7d1703017554e77131e6bb0b75c9908 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 11 Sep 2025 11:42:35 -0300 Subject: [PATCH 06/23] simplifies method. removes the need for SB. --- src/PSADTree/PSADTreeCmdletBase.cs | 4 ++-- src/PSADTree/TreeGroup.cs | 21 ++++++--------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index d47103f..347d6e3 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -28,7 +28,7 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable protected WildcardPattern[]? _exclusionPatterns; - private const WildcardOptions _wpoptions = WildcardOptions.Compiled + private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled | WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; @@ -73,7 +73,7 @@ protected override void BeginProcessing() if (Exclude is not null) { _exclusionPatterns = Exclude - .Select(e => new WildcardPattern(e, _wpoptions)) + .Select(e => new WildcardPattern(e, WildcardPatternOptions)) .ToArray(); } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index bbf0b56..64e83f6 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -1,21 +1,18 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.DirectoryServices.AccountManagement; -using System.Text; namespace PSADTree; public sealed class TreeGroup : TreeObjectBase { - private const string _isCircular = " ↔ Circular Reference"; + private const string Circular = " ↔ Circular Reference"; - private const string _isProcessed = " ↔ Processed Group"; + private const string Processed = " ↔ Processed Group"; - private const string _vtBrightRed = "\x1B[91m"; + private const string VTBrightRed = "\x1B[91m"; - private const string _vtReset = "\x1B[0m"; - - private static readonly StringBuilder s_sb = new(); + private const string VTReset = "\x1B[0m"; private List? _childs; @@ -49,16 +46,10 @@ internal TreeGroup( internal void SetCircularNested() { IsCircular = true; - Hierarchy = s_sb - .Append(Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, _vtBrightRed)) - .Append(_isCircular) - .Append(_vtReset) - .ToString(); - - s_sb.Clear(); + Hierarchy = $"{Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, VTBrightRed)}{Circular}{VTReset}"; } - internal void SetProcessed() => Hierarchy = string.Concat(Hierarchy, _isProcessed); + internal void SetProcessed() => Hierarchy = string.Concat(Hierarchy, Processed); internal void Hook(TreeCache cache) => _childs ??= cache[DistinguishedName]._childs; From 19e9f773b524efb08200c1f5e2321cfce81ec3f2 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 12 Sep 2025 10:18:12 -0300 Subject: [PATCH 07/23] this commit introduces logic for re-using the TreeCache per input object (it was cleared per each before). Needs testing. --- .../Commands/GetADTreeGroupMemberCommand.cs | 169 ++++++++---------- ...etADTreePrincipalGroupMembershipCommand.cs | 34 ++-- src/PSADTree/PSADTreeCmdletBase.cs | 30 ++-- src/PSADTree/TreeComputer.cs | 4 +- src/PSADTree/TreeExtensions.cs | 4 + src/PSADTree/TreeGroup.cs | 18 +- src/PSADTree/TreeUser.cs | 4 +- 7 files changed, 124 insertions(+), 139 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 0547d95..1858fb1 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -22,7 +22,7 @@ protected override void ProcessRecord() { Dbg.Assert(Identity is not null); Dbg.Assert(_context is not null); - _truncatedOutput = false; + TruncatedOutput = false; try { @@ -59,72 +59,104 @@ private TreeObjectBase[] Traverse( string source) { int depth; - Clear(); + Index.Clear(); Push(groupPrincipal, new TreeGroup(source, groupPrincipal)); + HashSet visited = []; - while (_stack.Count > 0) + while (Stack.Count > 0) { - (GroupPrincipal? current, TreeGroup treeGroup) = _stack.Pop(); + (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); + depth = treeGroup.Depth + 1; + Index.Add(treeGroup); - try + // if this node has been already processed + if (!Cache.TryAdd(treeGroup)) { - depth = treeGroup.Depth + 1; - - // if this node has been already processed - if (!_cache.TryAdd(treeGroup)) + // if it's a circular reference, go next + if (TreeCache.IsCircular(treeGroup)) { - current?.Dispose(); - treeGroup.Hook(_cache); - _index.Add(treeGroup); - - // if it's a circular reference, go next - if (TreeCache.IsCircular(treeGroup)) - { - treeGroup.SetCircularNested(); - continue; - } - - // else, if we want to show all nodes - if (ShowAll.IsPresent) - { - // reconstruct the output without querying AD again - EnumerateMembers(treeGroup, depth); - continue; - } - - // else, just skip this reference and go next - treeGroup.SetProcessed(); + treeGroup.SetCircularNested(); continue; } - if (current is not null) + // else, if we want to show all nodes and this node was not yet visited + if (ShowAll && !visited.Add(treeGroup.DistinguishedName)) { - EnumerateMembers(treeGroup, current, source, depth); + // reconstruct the output without querying AD again + EnumerateMembers(treeGroup, depth); + continue; } - _index.Add(treeGroup); - _index.TryAddPrincipals(); - current?.Dispose(); + // else, just skip this reference and go next + treeGroup.SetProcessed(); + continue; } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + + // else, group isn't cached query AD + EnumerateMembers(treeGroup, current, source, depth); + Index.TryAddPrincipals(); + current?.Dispose(); + } + + return Index.GetTree(); + } + + private TreeObjectBase ProcessPrincipal( + Principal principal, + TreeGroup parent, + string source, + int depth) + { + return principal switch + { + UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)), + ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)), + GroupPrincipal group => HandleGroup(parent, group, source, depth), + _ => throw new ArgumentOutOfRangeException(nameof(principal)), + }; + + TreeObjectBase AddTreeObject(TreeObjectBase obj) + { + if (depth <= Depth) { - throw; + Index.AddPrincipal(obj); } - catch (Exception exception) + + return obj; + } + + TreeObjectBase HandleGroup( + TreeGroup parent, + GroupPrincipal group, + string source, + int depth) + { + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { - WriteError(exception.ToEnumerationFailure(current)); + TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); + cloned.Hook(Cache); + Push(null, cloned); + group.Dispose(); + return treeGroup; } - } - return _index.GetTree(); + treeGroup = new TreeGroup(source, parent, group, depth); + Push(group, treeGroup); + return treeGroup; + } } private void EnumerateMembers( TreeGroup parent, - GroupPrincipal group, + GroupPrincipal? group, string source, int depth) { + if (group is null) + { + return; + } + IEnumerable members = group.ToSafeSortedEnumerable( selector: group => group.GetMembers(), cmdlet: this, @@ -150,7 +182,7 @@ private void EnumerateMembers( } } - if (ShouldExclude(member, _exclusionPatterns)) + if (ShouldExclude(member, ExclusionPatterns)) { continue; } @@ -161,10 +193,7 @@ private void EnumerateMembers( source: source, depth: depth); - if (ShowAll.IsPresent) - { - parent.AddChild(treeObject); - } + parent.AddChild(treeObject); } finally { @@ -173,51 +202,9 @@ private void EnumerateMembers( } } - private TreeObjectBase ProcessPrincipal( - Principal principal, - TreeGroup parent, - string source, - int depth) - { - return principal switch - { - UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)), - ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)), - GroupPrincipal group => HandleGroup(parent, group, source, depth), - _ => throw new ArgumentOutOfRangeException(nameof(principal)), - }; - - TreeObjectBase AddTreeObject(TreeObjectBase obj) - { - if (depth <= Depth) - { - _index.AddPrincipal(obj); - } - - return obj; - } - - TreeObjectBase HandleGroup( - TreeGroup parent, - GroupPrincipal group, - string source, - int depth) - { - if (_cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) - { - Push(group, (TreeGroup)treeGroup.Clone(parent, depth)); - return treeGroup; - } - - treeGroup = new TreeGroup(source, parent, group, depth); - Push(group, treeGroup); - return treeGroup; - } - } - private void EnumerateMembers(TreeGroup parent, int depth) { - foreach (TreeObjectBase member in parent.Childs) + foreach (TreeObjectBase member in parent.Children) { if (member is TreeGroup treeGroup) { @@ -227,7 +214,7 @@ private void EnumerateMembers(TreeGroup parent, int depth) if (depth <= Depth) { - _index.Add(member.Clone(parent, depth)); + Index.Add(member.Clone(parent, depth)); } } } diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 944607d..e7e3339 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -19,9 +19,9 @@ protected override void ProcessRecord() { Dbg.Assert(Identity is not null); Dbg.Assert(_context is not null); - _truncatedOutput = false; + TruncatedOutput = false; Principal? principal; - Clear(); + Index.Clear(); try { @@ -52,17 +52,17 @@ protected override void ProcessRecord() switch (principal) { case UserPrincipal user: - _index.Add(new TreeUser(source, user)); + Index.Add(new TreeUser(source, user)); break; case ComputerPrincipal computer: - _index.Add(new TreeComputer(source, computer)); + Index.Add(new TreeComputer(source, computer)); break; case GroupPrincipal group: TreeGroup treeGroup = new(source, group); - _index.Add(treeGroup); - _cache.Add(treeGroup); + Index.Add(treeGroup); + Cache.Add(treeGroup); break; default: @@ -78,7 +78,7 @@ protected override void ProcessRecord() foreach (Principal parent in groups) { - if (ShouldExclude(parent, _exclusionPatterns)) + if (ShouldExclude(parent, ExclusionPatterns)) { continue; } @@ -109,20 +109,20 @@ protected override void ProcessRecord() private TreeObjectBase[] Traverse(string source) { int depth; - while (_stack.Count > 0) + while (Stack.Count > 0) { - (GroupPrincipal? current, TreeGroup treeGroup) = _stack.Pop(); + (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); try { depth = treeGroup.Depth + 1; // if this node has been already processed - if (!_cache.TryAdd(treeGroup)) + if (!Cache.TryAdd(treeGroup)) { current?.Dispose(); - treeGroup.Hook(_cache); - _index.Add(treeGroup); + treeGroup.Hook(Cache); + Index.Add(treeGroup); // if it's a circular reference, go next if (TreeCache.IsCircular(treeGroup)) @@ -154,7 +154,7 @@ private TreeObjectBase[] Traverse(string source) EnumerateMembership(treeGroup, groups, source, depth); } - _index.Add(treeGroup); + Index.Add(treeGroup); current?.Dispose(); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) @@ -167,7 +167,7 @@ private TreeObjectBase[] Traverse(string source) } } - return _index.GetTree(); + return Index.GetTree(); } private void EnumerateMembership( @@ -178,7 +178,7 @@ private void EnumerateMembership( { foreach (Principal group in groups) { - if (ShouldExclude(group, _exclusionPatterns)) + if (ShouldExclude(group, ExclusionPatterns)) { continue; } @@ -192,7 +192,7 @@ private void EnumerateMembership( TreeGroup ProcessGroup(GroupPrincipal group) { - if (_cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { Push(group, (TreeGroup)treeGroup.Clone(parent, depth)); return treeGroup; @@ -211,7 +211,7 @@ private void EnumerateMembership(TreeGroup parent, int depth) return; } - foreach (TreeObjectBase child in parent.Childs) + foreach (TreeObjectBase child in parent.Children) { TreeGroup group = (TreeGroup)child; Push(null, (TreeGroup)group.Clone(parent, depth)); diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 347d6e3..95b78b3 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -16,17 +16,17 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable private bool _disposed; - protected bool _truncatedOutput; + protected bool TruncatedOutput { get; set; } - protected readonly Stack<(GroupPrincipal? group, TreeGroup treeGroup)> _stack = new(); + protected Stack<(GroupPrincipal? group, TreeGroup treeGroup)> Stack { get; } = new(); - internal readonly TreeCache _cache = new(); + internal TreeCache Cache { get; } = new(); - internal readonly TreeIndex _index = new(); + internal TreeIndex Index { get; } = new(); internal PSADTreeComparer Comparer { get; } = new(); - protected WildcardPattern[]? _exclusionPatterns; + protected WildcardPattern[]? ExclusionPatterns { get; set; } private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled | WildcardOptions.CultureInvariant @@ -72,9 +72,7 @@ protected override void BeginProcessing() if (Exclude is not null) { - _exclusionPatterns = Exclude - .Select(e => new WildcardPattern(e, WildcardPatternOptions)) - .ToArray(); + ExclusionPatterns = [.. Exclude.Select(e => new WildcardPattern(e, WildcardPatternOptions))]; } if (Credential is null) @@ -104,15 +102,15 @@ protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) if (treeGroup.Depth == Depth) { - _truncatedOutput = true; + TruncatedOutput = true; } - _stack.Push((groupPrincipal, treeGroup)); + Stack.Push((groupPrincipal, treeGroup)); } protected void DisplayWarningIfTruncatedOutput() { - if (_truncatedOutput) + if (TruncatedOutput) { WriteWarning($"Result is truncated as enumeration has exceeded the set depth of {Depth}."); } @@ -154,11 +152,11 @@ protected virtual void Dispose(bool disposing) } } - protected void Clear() - { - _index.Clear(); - _cache.Clear(); - } + // protected void Clear() + // { + // Index.Clear(); + // Cache.Clear(); + // } public void Dispose() { diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index e06906a..0ce5324 100644 --- a/src/PSADTree/TreeComputer.cs +++ b/src/PSADTree/TreeComputer.cs @@ -25,6 +25,6 @@ internal TreeComputer( : base(source, computer) { } - internal override TreeObjectBase Clone(TreeGroup parent, int depth) => - new TreeComputer(this, parent, depth); + internal override TreeObjectBase Clone(TreeGroup parent, int depth) + => new TreeComputer(this, parent, depth); } diff --git a/src/PSADTree/TreeExtensions.cs b/src/PSADTree/TreeExtensions.cs index 8f3d317..191c234 100644 --- a/src/PSADTree/TreeExtensions.cs +++ b/src/PSADTree/TreeExtensions.cs @@ -144,6 +144,10 @@ internal static IEnumerable ToSafeSortedEnumerable( principals.Add(enumerator.Current); } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + throw; + } catch (Exception exception) { cmdlet.WriteError(exception.ToEnumerationFailure(principal)); diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 64e83f6..f53d4ca 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -14,9 +14,9 @@ public sealed class TreeGroup : TreeObjectBase private const string VTReset = "\x1B[0m"; - private List? _childs; + private List _children = []; - public ReadOnlyCollection Childs => new(_childs ??= []); + public ReadOnlyCollection Children => new(_children); public bool IsCircular { get; private set; } @@ -26,7 +26,7 @@ private TreeGroup( int depth) : base(group, parent, depth) { - _childs = group._childs; + _children = group._children; } internal TreeGroup( @@ -51,14 +51,10 @@ internal void SetCircularNested() internal void SetProcessed() => Hierarchy = string.Concat(Hierarchy, Processed); - internal void Hook(TreeCache cache) => _childs ??= cache[DistinguishedName]._childs; + internal void Hook(TreeCache cache) => _children = cache[DistinguishedName]._children; - internal void AddChild(TreeObjectBase child) - { - _childs ??= []; - _childs.Add(child); - } + internal void AddChild(TreeObjectBase child) => _children.Add(child); - internal override TreeObjectBase Clone(TreeGroup parent, int depth) => - new TreeGroup(this, parent, depth); + internal override TreeObjectBase Clone(TreeGroup parent, int depth) + => new TreeGroup(this, parent, depth); } diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs index 2497243..50c0600 100644 --- a/src/PSADTree/TreeUser.cs +++ b/src/PSADTree/TreeUser.cs @@ -25,6 +25,6 @@ internal TreeUser( : base(source, user) { } - internal override TreeObjectBase Clone(TreeGroup parent, int depth) => - new TreeUser(this, parent, depth); + internal override TreeObjectBase Clone(TreeGroup parent, int depth) + => new TreeUser(this, parent, depth); } From dd09d8e57302152791a94d2d08680dac1d8ce28b Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Fri, 12 Sep 2025 16:25:56 -0300 Subject: [PATCH 08/23] .... --- .../Commands/GetADTreeGroupMemberCommand.cs | 34 ++++--- ...etADTreePrincipalGroupMembershipCommand.cs | 95 ++++++++----------- src/PSADTree/PSADTreeCmdletBase.cs | 8 +- src/PSADTree/TreeGroup.cs | 4 +- 4 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 1858fb1..a8b99fb 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -21,22 +21,19 @@ public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase protected override void ProcessRecord() { Dbg.Assert(Identity is not null); - Dbg.Assert(_context is not null); + Dbg.Assert(Context is not null); TruncatedOutput = false; try { - using GroupPrincipal? group = GroupPrincipal.FindByIdentity(_context, Identity); + using GroupPrincipal? group = GroupPrincipal.FindByIdentity(Context, Identity); if (group is null) { WriteError(Identity.ToIdentityNotFound()); return; } - TreeObjectBase[] result = Traverse( - groupPrincipal: group, - source: group.DistinguishedName); - + TreeObjectBase[] result = Traverse(group); DisplayWarningIfTruncatedOutput(); WriteObject(sendToPipeline: result, enumerateCollection: true); } @@ -54,14 +51,25 @@ protected override void ProcessRecord() } } - private TreeObjectBase[] Traverse( - GroupPrincipal groupPrincipal, - string source) + private TreeGroup GetFirstTreeGroup(GroupPrincipal group) + { + if (!Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + { + return new(group.DistinguishedName, group); + } + + treeGroup = (TreeGroup)treeGroup.Clone(); + treeGroup.Hook(Cache); + return treeGroup; + } + + private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) { - int depth; Index.Clear(); - Push(groupPrincipal, new TreeGroup(source, groupPrincipal)); + int depth; + string source = groupPrincipal.DistinguishedName; HashSet visited = []; + Push(groupPrincipal, GetFirstTreeGroup(groupPrincipal)); while (Stack.Count > 0) { @@ -79,8 +87,8 @@ private TreeObjectBase[] Traverse( continue; } - // else, if we want to show all nodes and this node was not yet visited - if (ShowAll && !visited.Add(treeGroup.DistinguishedName)) + // else, if we want to show all nodes OR this node was not yet visited + if (ShowAll || !visited.Add(treeGroup.DistinguishedName)) { // reconstruct the output without querying AD again EnumerateMembers(treeGroup, depth); diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index e7e3339..035fc0b 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -18,14 +18,14 @@ public sealed class GetADTreePrincipalGroupMembershipCommand : PSADTreeCmdletBas protected override void ProcessRecord() { Dbg.Assert(Identity is not null); - Dbg.Assert(_context is not null); + Dbg.Assert(Context is not null); TruncatedOutput = false; Principal? principal; Index.Clear(); try { - principal = Principal.FindByIdentity(_context, Identity); + principal = Principal.FindByIdentity(Context, Identity); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -62,7 +62,7 @@ protected override void ProcessRecord() case GroupPrincipal group: TreeGroup treeGroup = new(source, group); Index.Add(treeGroup); - Cache.Add(treeGroup); + Cache.TryAdd(treeGroup); break; default: @@ -72,7 +72,7 @@ protected override void ProcessRecord() try { IEnumerable groups = principal.ToSafeSortedEnumerable( - selector: principal => principal.GetGroups(_context), + selector: principal => principal.GetGroups(Context), cmdlet: this, comparer: Comparer); @@ -109,73 +109,62 @@ protected override void ProcessRecord() private TreeObjectBase[] Traverse(string source) { int depth; + HashSet visited = []; + while (Stack.Count > 0) { (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); - try - { - depth = treeGroup.Depth + 1; + depth = treeGroup.Depth + 1; + Index.Add(treeGroup); - // if this node has been already processed - if (!Cache.TryAdd(treeGroup)) + // if this node has been already processed + if (!Cache.TryAdd(treeGroup)) + { + // if it's a circular reference, go next + if (TreeCache.IsCircular(treeGroup)) { - current?.Dispose(); - treeGroup.Hook(Cache); - Index.Add(treeGroup); - - // if it's a circular reference, go next - if (TreeCache.IsCircular(treeGroup)) - { - treeGroup.SetCircularNested(); - continue; - } - - // else, if we want to show all nodes - if (ShowAll.IsPresent) - { - // reconstruct the output without querying AD again - EnumerateMembership(treeGroup, depth); - continue; - } - - // else, just skip this reference and go next - treeGroup.SetProcessed(); + treeGroup.SetCircularNested(); continue; } - if (current is not null) + // else, if we want to show all nodes and this node was not yet visited + if (ShowAll && !visited.Add(treeGroup.DistinguishedName)) { - IEnumerable groups = current.ToSafeSortedEnumerable( - selector: principal => principal.GetGroups(_context), - cmdlet: this, - comparer: Comparer); - - EnumerateMembership(treeGroup, groups, source, depth); + // reconstruct the output without querying AD again + EnumerateMembership(treeGroup, depth); + continue; } - Index.Add(treeGroup); - current?.Dispose(); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) - { - WriteError(exception.ToEnumerationFailure(current)); + // else, just skip this reference and go next + treeGroup.SetProcessed(); + continue; } + + // else, get membership from AD Query + EnumerateMembership(current, treeGroup, source, depth); + current?.Dispose(); } return Index.GetTree(); } private void EnumerateMembership( + Principal? principal, TreeGroup parent, - IEnumerable groups, string source, int depth) { + if (principal is null) + { + return; + } + + IEnumerable groups = principal.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(Context), + cmdlet: this, + comparer: Comparer); + foreach (Principal group in groups) { if (ShouldExclude(group, ExclusionPatterns)) @@ -184,17 +173,17 @@ private void EnumerateMembership( } TreeGroup treeGroup = ProcessGroup((GroupPrincipal)group); - if (ShowAll.IsPresent) - { - parent.AddChild(treeGroup); - } + parent.AddChild(treeGroup); } TreeGroup ProcessGroup(GroupPrincipal group) { if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { - Push(group, (TreeGroup)treeGroup.Clone(parent, depth)); + TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); + cloned.Hook(Cache); + Push(group, cloned); + group.Dispose(); return treeGroup; } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 95b78b3..b1233a4 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -12,7 +12,7 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable protected const string RecursiveParameterSet = "Recursive"; - protected PrincipalContext? _context; + protected PrincipalContext? Context { get; set; } private bool _disposed; @@ -77,11 +77,11 @@ protected override void BeginProcessing() if (Credential is null) { - _context = new PrincipalContext(ContextType.Domain, Server); + Context = new PrincipalContext(ContextType.Domain, Server); return; } - _context = new PrincipalContext( + Context = new PrincipalContext( ContextType.Domain, Server, Credential.UserName, @@ -147,7 +147,7 @@ protected virtual void Dispose(bool disposing) { if (disposing && !_disposed) { - _context?.Dispose(); + Context?.Dispose(); _disposed = true; } } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index f53d4ca..2788fc7 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -22,7 +22,7 @@ public sealed class TreeGroup : TreeObjectBase private TreeGroup( TreeGroup group, - TreeGroup parent, + TreeGroup? parent, int depth) : base(group, parent, depth) { @@ -55,6 +55,6 @@ internal void SetCircularNested() internal void AddChild(TreeObjectBase child) => _children.Add(child); - internal override TreeObjectBase Clone(TreeGroup parent, int depth) + internal override TreeObjectBase Clone(TreeGroup? parent = null, int depth = 0) => new TreeGroup(this, parent, depth); } From 9eac3d24163f4ffa6e0ed6a43efd4a45ef5a8adc Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 13 Sep 2025 16:09:04 -0300 Subject: [PATCH 09/23] refactoring... simplifying --- .../Commands/GetADTreeGroupMemberCommand.cs | 106 +++++++++--------- ...etADTreePrincipalGroupMembershipCommand.cs | 7 +- src/PSADTree/PSADTreeCmdletBase.cs | 31 ++--- src/PSADTree/TreeCache.cs | 39 +------ src/PSADTree/TreeGroup.cs | 50 +++++++-- src/PSADTree/TreeIndex.cs | 14 +-- 6 files changed, 120 insertions(+), 127 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index a8b99fb..fc01563 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -22,7 +22,7 @@ protected override void ProcessRecord() { Dbg.Assert(Identity is not null); Dbg.Assert(Context is not null); - TruncatedOutput = false; + base.ProcessRecord(); try { @@ -53,22 +53,19 @@ protected override void ProcessRecord() private TreeGroup GetFirstTreeGroup(GroupPrincipal group) { - if (!Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + TreeGroup treeGroup = new(group.DistinguishedName, group); + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? _)) { - return new(group.DistinguishedName, group); + treeGroup.LinkCachedChildren(Cache); } - treeGroup = (TreeGroup)treeGroup.Clone(); - treeGroup.Hook(Cache); return treeGroup; } private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) { - Index.Clear(); int depth; string source = groupPrincipal.DistinguishedName; - HashSet visited = []; Push(groupPrincipal, GetFirstTreeGroup(groupPrincipal)); while (Stack.Count > 0) @@ -81,14 +78,13 @@ private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) if (!Cache.TryAdd(treeGroup)) { // if it's a circular reference, go next - if (TreeCache.IsCircular(treeGroup)) + if (treeGroup.SetIfCircularNested()) { - treeGroup.SetCircularNested(); continue; } // else, if we want to show all nodes OR this node was not yet visited - if (ShowAll || !visited.Add(treeGroup.DistinguishedName)) + if (ShowAll || IsNotVisited(treeGroup)) { // reconstruct the output without querying AD again EnumerateMembers(treeGroup, depth); @@ -109,51 +105,6 @@ private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) return Index.GetTree(); } - private TreeObjectBase ProcessPrincipal( - Principal principal, - TreeGroup parent, - string source, - int depth) - { - return principal switch - { - UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)), - ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)), - GroupPrincipal group => HandleGroup(parent, group, source, depth), - _ => throw new ArgumentOutOfRangeException(nameof(principal)), - }; - - TreeObjectBase AddTreeObject(TreeObjectBase obj) - { - if (depth <= Depth) - { - Index.AddPrincipal(obj); - } - - return obj; - } - - TreeObjectBase HandleGroup( - TreeGroup parent, - GroupPrincipal group, - string source, - int depth) - { - if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) - { - TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); - cloned.Hook(Cache); - Push(null, cloned); - group.Dispose(); - return treeGroup; - } - - treeGroup = new TreeGroup(source, parent, group, depth); - Push(group, treeGroup); - return treeGroup; - } - } - private void EnumerateMembers( TreeGroup parent, GroupPrincipal? group, @@ -226,4 +177,49 @@ private void EnumerateMembers(TreeGroup parent, int depth) } } } + + private TreeObjectBase ProcessPrincipal( + Principal principal, + TreeGroup parent, + string source, + int depth) + { + return principal switch + { + UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)), + ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)), + GroupPrincipal group => ProcessGroup(parent, group, source, depth), + _ => throw new ArgumentOutOfRangeException(nameof(principal)), + }; + + TreeObjectBase AddTreeObject(TreeObjectBase obj) + { + if (depth <= Depth) + { + Index.AddPrincipal(obj); + } + + return obj; + } + + TreeObjectBase ProcessGroup( + TreeGroup parent, + GroupPrincipal group, + string source, + int depth) + { + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + { + TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); + cloned.LinkCachedChildren(Cache); + Push(null, cloned); + group.Dispose(); + return treeGroup; + } + + treeGroup = new TreeGroup(source, parent, group, depth); + Push(group, treeGroup); + return treeGroup; + } + } } diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 035fc0b..45d2780 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -19,9 +19,7 @@ protected override void ProcessRecord() { Dbg.Assert(Identity is not null); Dbg.Assert(Context is not null); - TruncatedOutput = false; Principal? principal; - Index.Clear(); try { @@ -122,9 +120,8 @@ private TreeObjectBase[] Traverse(string source) if (!Cache.TryAdd(treeGroup)) { // if it's a circular reference, go next - if (TreeCache.IsCircular(treeGroup)) + if (treeGroup.SetIfCircularNested()) { - treeGroup.SetCircularNested(); continue; } @@ -181,7 +178,7 @@ TreeGroup ProcessGroup(GroupPrincipal group) if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); - cloned.Hook(Cache); + cloned.LinkCachedChildren(Cache); Push(group, cloned); group.Dispose(); return treeGroup; diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index b1233a4..201f2be 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -8,21 +8,23 @@ namespace PSADTree; public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable { + private bool _disposed; + + private bool _truncatedOutput; + + private readonly HashSet _visited = []; + protected const string DepthParameterSet = "Depth"; protected const string RecursiveParameterSet = "Recursive"; protected PrincipalContext? Context { get; set; } - private bool _disposed; - - protected bool TruncatedOutput { get; set; } - protected Stack<(GroupPrincipal? group, TreeGroup treeGroup)> Stack { get; } = new(); internal TreeCache Cache { get; } = new(); - internal TreeIndex Index { get; } = new(); + internal TreeBuilder Index { get; } = new(); internal PSADTreeComparer Comparer { get; } = new(); @@ -93,6 +95,15 @@ protected override void BeginProcessing() } } + protected bool IsNotVisited(TreeGroup group) => _visited.Add(group.DistinguishedName); + + protected override void ProcessRecord() + { + Index.Clear(); + _visited.Clear(); + _truncatedOutput = false; + } + protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) { if (treeGroup.Depth > Depth) @@ -102,7 +113,7 @@ protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) if (treeGroup.Depth == Depth) { - TruncatedOutput = true; + _truncatedOutput = true; } Stack.Push((groupPrincipal, treeGroup)); @@ -110,7 +121,7 @@ protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) protected void DisplayWarningIfTruncatedOutput() { - if (TruncatedOutput) + if (_truncatedOutput) { WriteWarning($"Result is truncated as enumeration has exceeded the set depth of {Depth}."); } @@ -152,12 +163,6 @@ protected virtual void Dispose(bool disposing) } } - // protected void Clear() - // { - // Index.Clear(); - // Cache.Clear(); - // } - public void Dispose() { Dispose(disposing: true); diff --git a/src/PSADTree/TreeCache.cs b/src/PSADTree/TreeCache.cs index e7209da..4abe6e3 100644 --- a/src/PSADTree/TreeCache.cs +++ b/src/PSADTree/TreeCache.cs @@ -5,15 +5,11 @@ namespace PSADTree; internal sealed class TreeCache { - private readonly Dictionary _cache; + private readonly Dictionary _cache = []; - internal TreeGroup this[string distinguishedName] => - _cache[distinguishedName]; + internal TreeGroup this[string distinguishedName] => _cache[distinguishedName]; - internal TreeCache() => _cache = []; - - internal void Add(TreeGroup treeGroup) => - _cache.Add(treeGroup.DistinguishedName, treeGroup); + internal void Add(TreeGroup group) => _cache.Add(group.DistinguishedName, group); internal bool TryAdd(TreeGroup group) { @@ -26,31 +22,6 @@ internal bool TryAdd(TreeGroup group) return true; } - internal bool TryGet( - string distinguishedName, - [NotNullWhen(true)] out TreeGroup? principal) => - _cache.TryGetValue(distinguishedName, out principal); - - internal static bool IsCircular(TreeGroup node) - { - if (node.Parent is null) - { - return false; - } - - TreeGroup? current = node.Parent; - while (current is not null) - { - if (node.DistinguishedName == current.DistinguishedName) - { - return true; - } - - current = current.Parent; - } - - return false; - } - - internal void Clear() => _cache.Clear(); + internal bool TryGet(string distinguishedName, [NotNullWhen(true)] out TreeGroup? group) + => _cache.TryGetValue(distinguishedName, out group); } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 2788fc7..c77eaa9 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -14,7 +14,7 @@ public sealed class TreeGroup : TreeObjectBase private const string VTReset = "\x1B[0m"; - private List _children = []; + private List _children; public ReadOnlyCollection Children => new(_children); @@ -22,7 +22,7 @@ public sealed class TreeGroup : TreeObjectBase private TreeGroup( TreeGroup group, - TreeGroup? parent, + TreeGroup parent, int depth) : base(group, parent, depth) { @@ -33,7 +33,9 @@ internal TreeGroup( string source, GroupPrincipal group) : base(source, group) - { } + { + _children = []; + } internal TreeGroup( string source, @@ -41,20 +43,48 @@ internal TreeGroup( GroupPrincipal group, int depth) : base(source, parent, group, depth) - { } + { + _children = []; + } - internal void SetCircularNested() + private bool IsCircularNested() { - IsCircular = true; - Hierarchy = $"{Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, VTBrightRed)}{Circular}{VTReset}"; + if (Parent is null) + { + return false; + } + + TreeGroup? current = Parent; + while (current is not null) + { + if (DistinguishedName == current.DistinguishedName) + { + return true; + } + + current = current.Parent; + } + + return false; + } + + internal bool SetIfCircularNested() + { + IsCircular = IsCircularNested(); + if (IsCircular) + { + Hierarchy = $"{Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, VTBrightRed)}{Circular}{VTReset}"; + } + + return IsCircular; } - internal void SetProcessed() => Hierarchy = string.Concat(Hierarchy, Processed); + internal void SetProcessed() => Hierarchy = $"{Hierarchy}{Processed}"; - internal void Hook(TreeCache cache) => _children = cache[DistinguishedName]._children; + internal void LinkCachedChildren(TreeCache cache) => _children = cache[DistinguishedName]._children; internal void AddChild(TreeObjectBase child) => _children.Add(child); - internal override TreeObjectBase Clone(TreeGroup? parent = null, int depth = 0) + internal override TreeObjectBase Clone(TreeGroup parent, int depth) => new TreeGroup(this, parent, depth); } diff --git a/src/PSADTree/TreeIndex.cs b/src/PSADTree/TreeIndex.cs index 6e33dae..d8d3163 100644 --- a/src/PSADTree/TreeIndex.cs +++ b/src/PSADTree/TreeIndex.cs @@ -2,17 +2,11 @@ namespace PSADTree; -internal sealed class TreeIndex +internal sealed class TreeBuilder { - private readonly List _principals; + private readonly List _principals = []; - private readonly List _output; - - internal TreeIndex() - { - _principals = []; - _output = []; - } + private readonly List _output = []; internal void AddPrincipal(TreeObjectBase principal) => _principals.Add(principal); @@ -22,7 +16,7 @@ internal void TryAddPrincipals() { if (_principals.Count > 0) { - _output.AddRange([.. _principals]); + _output.AddRange(_principals); _principals.Clear(); } } From aad4f00f7e46d14123280a193d0f3f717e4afa00 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sat, 13 Sep 2025 19:26:57 -0300 Subject: [PATCH 10/23] refactoring... simplifying --- .../Commands/GetADTreeGroupMemberCommand.cs | 6 +-- ...etADTreePrincipalGroupMembershipCommand.cs | 4 +- src/PSADTree/PSADTreeCmdletBase.cs | 42 ++++--------------- src/PSADTree/TreeGroup.cs | 9 ++++ src/PSADTree/TreeIndex.cs | 4 +- 5 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index fc01563..24a952e 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -98,7 +98,7 @@ private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) // else, group isn't cached query AD EnumerateMembers(treeGroup, current, source, depth); - Index.TryAddPrincipals(); + Index.CommitStaged(); current?.Dispose(); } @@ -141,7 +141,7 @@ private void EnumerateMembers( } } - if (ShouldExclude(member, ExclusionPatterns)) + if (ShouldExclude(member)) { continue; } @@ -196,7 +196,7 @@ TreeObjectBase AddTreeObject(TreeObjectBase obj) { if (depth <= Depth) { - Index.AddPrincipal(obj); + Index.Stage(obj); } return obj; diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 45d2780..fe05022 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -76,7 +76,7 @@ protected override void ProcessRecord() foreach (Principal parent in groups) { - if (ShouldExclude(parent, ExclusionPatterns)) + if (ShouldExclude(parent)) { continue; } @@ -164,7 +164,7 @@ private void EnumerateMembership( foreach (Principal group in groups) { - if (ShouldExclude(group, ExclusionPatterns)) + if (ShouldExclude(group)) { continue; } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 201f2be..0b69972 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -14,6 +14,12 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable private readonly HashSet _visited = []; + private WildcardPattern[]? _exclusionPatterns; + + private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled + | WildcardOptions.CultureInvariant + | WildcardOptions.IgnoreCase; + protected const string DepthParameterSet = "Depth"; protected const string RecursiveParameterSet = "Recursive"; @@ -28,12 +34,6 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable internal PSADTreeComparer Comparer { get; } = new(); - protected WildcardPattern[]? ExclusionPatterns { get; set; } - - private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled - | WildcardOptions.CultureInvariant - | WildcardOptions.IgnoreCase; - [Parameter( Position = 0, Mandatory = true, @@ -74,7 +74,7 @@ protected override void BeginProcessing() if (Exclude is not null) { - ExclusionPatterns = [.. Exclude.Select(e => new WildcardPattern(e, WildcardPatternOptions))]; + _exclusionPatterns = [.. Exclude.Select(e => new WildcardPattern(e, WildcardPatternOptions))]; } if (Credential is null) @@ -127,32 +127,8 @@ protected void DisplayWarningIfTruncatedOutput() } } - private static bool MatchAny( - Principal principal, - WildcardPattern[] patterns) - { - foreach (WildcardPattern pattern in patterns) - { - if (pattern.IsMatch(principal.SamAccountName)) - { - return true; - } - } - - return false; - } - - protected static bool ShouldExclude( - Principal principal, - WildcardPattern[]? patterns) - { - if (patterns is null) - { - return false; - } - - return MatchAny(principal, patterns); - } + protected bool ShouldExclude(Principal principal) => + _exclusionPatterns?.Any(pattern => pattern.IsMatch(principal.SamAccountName)) ?? false; protected virtual void Dispose(bool disposing) { diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index c77eaa9..ecce8e3 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -14,6 +14,8 @@ public sealed class TreeGroup : TreeObjectBase private const string VTReset = "\x1B[0m"; + private readonly bool _isCloned; + private List _children; public ReadOnlyCollection Children => new(_children); @@ -26,7 +28,9 @@ private TreeGroup( int depth) : base(group, parent, depth) { + _isCloned = true; _children = group._children; + IsCircular = group.IsCircular; } internal TreeGroup( @@ -49,6 +53,11 @@ internal TreeGroup( private bool IsCircularNested() { + if (_isCloned) + { + return IsCircular; + } + if (Parent is null) { return false; diff --git a/src/PSADTree/TreeIndex.cs b/src/PSADTree/TreeIndex.cs index d8d3163..245bf89 100644 --- a/src/PSADTree/TreeIndex.cs +++ b/src/PSADTree/TreeIndex.cs @@ -8,11 +8,11 @@ internal sealed class TreeBuilder private readonly List _output = []; - internal void AddPrincipal(TreeObjectBase principal) => _principals.Add(principal); + internal void Stage(TreeObjectBase principal) => _principals.Add(principal); internal void Add(TreeObjectBase principal) => _output.Add(principal); - internal void TryAddPrincipals() + internal void CommitStaged() { if (_principals.Count > 0) { From 7d2f1d3815281c7ae2840465d21d28bf3d64dc16 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 15 Sep 2025 12:48:07 -0300 Subject: [PATCH 11/23] refactoring... simplifying... almost there, next same for PrincipalGroupMembership cmdlet. --- .../Commands/GetADTreeGroupMemberCommand.cs | 100 +++++++----------- ...etADTreePrincipalGroupMembershipCommand.cs | 22 +--- src/PSADTree/PSADTreeCmdletBase.cs | 21 +++- src/PSADTree/TreeGroup.cs | 9 +- 4 files changed, 66 insertions(+), 86 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 24a952e..994f6a7 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -66,7 +66,7 @@ private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) { int depth; string source = groupPrincipal.DistinguishedName; - Push(groupPrincipal, GetFirstTreeGroup(groupPrincipal)); + PushToStack(groupPrincipal, GetFirstTreeGroup(groupPrincipal)); while (Stack.Count > 0) { @@ -74,30 +74,16 @@ private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) depth = treeGroup.Depth + 1; Index.Add(treeGroup); - // if this node has been already processed + // if this group is already processed if (!Cache.TryAdd(treeGroup)) { - // if it's a circular reference, go next - if (treeGroup.SetIfCircularNested()) - { - continue; - } - - // else, if we want to show all nodes OR this node was not yet visited - if (ShowAll || IsNotVisited(treeGroup)) - { - // reconstruct the output without querying AD again - EnumerateMembers(treeGroup, depth); - continue; - } - - // else, just skip this reference and go next - treeGroup.SetProcessed(); + HandleCachedGroup(treeGroup, depth); + current?.Dispose(); continue; } - // else, group isn't cached query AD - EnumerateMembers(treeGroup, current, source, depth); + // else, group isn't cached so query AD + BuildMembersFromAD(treeGroup, current, source, depth); Index.CommitStaged(); current?.Dispose(); } @@ -105,7 +91,27 @@ private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) return Index.GetTree(); } - private void EnumerateMembers( + private void HandleCachedGroup(TreeGroup treeGroup, int depth) + { + // if it's a circular reference, nothing to do here + if (treeGroup.SetIfCircularNested()) + { + return; + } + + // else, if we want to show all nodes OR this node was not yet visited + if (ShowAll || IsNotVisited(treeGroup)) + { + // reconstruct the output without querying AD again + BuildMembersFromCache(treeGroup, depth); + return; + } + + // else, just skip this reference and go next + treeGroup.SetProcessed(); + } + + private void BuildMembersFromAD( TreeGroup parent, GroupPrincipal? group, string source, @@ -126,33 +132,19 @@ private void EnumerateMembers( IDisposable? disposable = null; try { - if (member is { DistinguishedName: null }) + if (member is { DistinguishedName: null } || + member.StructuralObjectClass != "group" && Group.IsPresent || + ShouldExclude(member)) { disposable = member; continue; } - if (member.StructuralObjectClass != "group") - { - disposable = member; - if (Group.IsPresent) - { - continue; - } - } - - if (ShouldExclude(member)) - { - continue; - } - - TreeObjectBase treeObject = ProcessPrincipal( + ProcessPrincipal( principal: member, parent: parent, source: source, depth: depth); - - parent.AddChild(treeObject); } finally { @@ -161,13 +153,13 @@ private void EnumerateMembers( } } - private void EnumerateMembers(TreeGroup parent, int depth) + private void BuildMembersFromCache(TreeGroup parent, int depth) { foreach (TreeObjectBase member in parent.Children) { if (member is TreeGroup treeGroup) { - Push(null, (TreeGroup)treeGroup.Clone(parent, depth)); + PushToStack(null, (TreeGroup)treeGroup.Clone(parent, depth)); continue; } @@ -178,13 +170,13 @@ private void EnumerateMembers(TreeGroup parent, int depth) } } - private TreeObjectBase ProcessPrincipal( + private void ProcessPrincipal( Principal principal, TreeGroup parent, string source, int depth) { - return principal switch + TreeObjectBase treeObject = principal switch { UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)), ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)), @@ -192,6 +184,8 @@ private TreeObjectBase ProcessPrincipal( _ => throw new ArgumentOutOfRangeException(nameof(principal)), }; + parent.AddChild(treeObject); + TreeObjectBase AddTreeObject(TreeObjectBase obj) { if (depth <= Depth) @@ -201,25 +195,5 @@ TreeObjectBase AddTreeObject(TreeObjectBase obj) return obj; } - - TreeObjectBase ProcessGroup( - TreeGroup parent, - GroupPrincipal group, - string source, - int depth) - { - if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) - { - TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); - cloned.LinkCachedChildren(Cache); - Push(null, cloned); - group.Dispose(); - return treeGroup; - } - - treeGroup = new TreeGroup(source, parent, group, depth); - Push(group, treeGroup); - return treeGroup; - } } } diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index fe05022..927c7ac 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -83,7 +83,7 @@ protected override void ProcessRecord() GroupPrincipal groupPrincipal = (GroupPrincipal)parent; TreeGroup treeGroup = new(source, null, groupPrincipal, 1); - Push(groupPrincipal, treeGroup); + PushToStack(groupPrincipal, treeGroup); } } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) @@ -169,25 +169,9 @@ private void EnumerateMembership( continue; } - TreeGroup treeGroup = ProcessGroup((GroupPrincipal)group); + TreeGroup treeGroup = ProcessGroup(parent, (GroupPrincipal)group, source, depth); parent.AddChild(treeGroup); } - - TreeGroup ProcessGroup(GroupPrincipal group) - { - if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) - { - TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); - cloned.LinkCachedChildren(Cache); - Push(group, cloned); - group.Dispose(); - return treeGroup; - } - - treeGroup = new(source, parent, group, depth); - Push(group, treeGroup); - return treeGroup; - } } private void EnumerateMembership(TreeGroup parent, int depth) @@ -200,7 +184,7 @@ private void EnumerateMembership(TreeGroup parent, int depth) foreach (TreeObjectBase child in parent.Children) { TreeGroup group = (TreeGroup)child; - Push(null, (TreeGroup)group.Clone(parent, depth)); + PushToStack(null, (TreeGroup)group.Clone(parent, depth)); } } } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 0b69972..1b657b2 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -104,7 +104,7 @@ protected override void ProcessRecord() _truncatedOutput = false; } - protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) + protected void PushToStack(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) { if (treeGroup.Depth > Depth) { @@ -119,6 +119,25 @@ protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) Stack.Push((groupPrincipal, treeGroup)); } + protected TreeGroup ProcessGroup( + TreeGroup parent, + GroupPrincipal group, + string source, + int depth) + { + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + { + TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); + cloned.LinkCachedChildren(Cache); + PushToStack(group, cloned); + return treeGroup; + } + + treeGroup = new TreeGroup(source, parent, group, depth); + PushToStack(group, treeGroup); + return treeGroup; + } + protected void DisplayWarningIfTruncatedOutput() { if (_truncatedOutput) diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index ecce8e3..27346ed 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -53,6 +53,7 @@ internal TreeGroup( private bool IsCircularNested() { + // there is no need to check again if the object is cloned if (_isCloned) { return IsCircular; @@ -79,8 +80,7 @@ private bool IsCircularNested() internal bool SetIfCircularNested() { - IsCircular = IsCircularNested(); - if (IsCircular) + if (IsCircular = IsCircularNested()) { Hierarchy = $"{Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, VTBrightRed)}{Circular}{VTReset}"; } @@ -90,7 +90,10 @@ internal bool SetIfCircularNested() internal void SetProcessed() => Hierarchy = $"{Hierarchy}{Processed}"; - internal void LinkCachedChildren(TreeCache cache) => _children = cache[DistinguishedName]._children; + internal void LinkCachedChildren(TreeCache cache) + { + _children = cache[DistinguishedName]._children; + } internal void AddChild(TreeObjectBase child) => _children.Add(child); From ac1c14f8eafccceb50598ca6e3babe57345979e5 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 15 Sep 2025 12:55:03 -0300 Subject: [PATCH 12/23] refactoring... simplifying... almost there, next same for PrincipalGroupMembership cmdlet. --- src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 994f6a7..6036203 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -54,8 +54,11 @@ protected override void ProcessRecord() private TreeGroup GetFirstTreeGroup(GroupPrincipal group) { TreeGroup treeGroup = new(group.DistinguishedName, group); + // if the first group is cached if (Cache.TryGet(group.DistinguishedName, out TreeGroup? _)) { + // link the children and build from cached path, + // no need to query AD at all! treeGroup.LinkCachedChildren(Cache); } From f281c0bd2d6e3a16bd07218e3ad18b0213466811 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 17 Sep 2025 11:42:59 -0300 Subject: [PATCH 13/23] abstracted most of the logic to base cmdlet --- .../Commands/GetADTreeGroupMemberCommand.cs | 104 ++---------- ...etADTreePrincipalGroupMembershipCommand.cs | 156 ++++-------------- src/PSADTree/PSADTreeCmdletBase.cs | 94 ++++++++++- src/PSADTree/{TreeIndex.cs => TreeBuilder.cs} | 0 4 files changed, 132 insertions(+), 222 deletions(-) rename src/PSADTree/{TreeIndex.cs => TreeBuilder.cs} (100%) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 6036203..5d1b5e0 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -18,41 +18,11 @@ public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase [Parameter] public SwitchParameter Group { get; set; } - protected override void ProcessRecord() - { - Dbg.Assert(Identity is not null); - Dbg.Assert(Context is not null); - base.ProcessRecord(); - - try - { - using GroupPrincipal? group = GroupPrincipal.FindByIdentity(Context, Identity); - if (group is null) - { - WriteError(Identity.ToIdentityNotFound()); - return; - } + protected override Principal GetFirstPrincipal() => GroupPrincipal.FindByIdentity(Context, Identity); - TreeObjectBase[] result = Traverse(group); - DisplayWarningIfTruncatedOutput(); - WriteObject(sendToPipeline: result, enumerateCollection: true); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (MultipleMatchesException exception) - { - WriteError(exception.ToAmbiguousIdentity(Identity)); - } - catch (Exception exception) - { - WriteError(exception.ToUnspecified(Identity)); - } - } - - private TreeGroup GetFirstTreeGroup(GroupPrincipal group) + protected override void HandleFirstPrincipal(Principal principal) { + GroupPrincipal group = (GroupPrincipal)principal; TreeGroup treeGroup = new(group.DistinguishedName, group); // if the first group is cached if (Cache.TryGet(group.DistinguishedName, out TreeGroup? _)) @@ -62,70 +32,16 @@ private TreeGroup GetFirstTreeGroup(GroupPrincipal group) treeGroup.LinkCachedChildren(Cache); } - return treeGroup; - } - - private TreeObjectBase[] Traverse(GroupPrincipal groupPrincipal) - { - int depth; - string source = groupPrincipal.DistinguishedName; - PushToStack(groupPrincipal, GetFirstTreeGroup(groupPrincipal)); - - while (Stack.Count > 0) - { - (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); - depth = treeGroup.Depth + 1; - Index.Add(treeGroup); - - // if this group is already processed - if (!Cache.TryAdd(treeGroup)) - { - HandleCachedGroup(treeGroup, depth); - current?.Dispose(); - continue; - } - - // else, group isn't cached so query AD - BuildMembersFromAD(treeGroup, current, source, depth); - Index.CommitStaged(); - current?.Dispose(); - } - - return Index.GetTree(); + PushToStack(group, treeGroup); } - private void HandleCachedGroup(TreeGroup treeGroup, int depth) - { - // if it's a circular reference, nothing to do here - if (treeGroup.SetIfCircularNested()) - { - return; - } - - // else, if we want to show all nodes OR this node was not yet visited - if (ShowAll || IsNotVisited(treeGroup)) - { - // reconstruct the output without querying AD again - BuildMembersFromCache(treeGroup, depth); - return; - } - - // else, just skip this reference and go next - treeGroup.SetProcessed(); - } - - private void BuildMembersFromAD( + protected override void BuildFromAD( TreeGroup parent, - GroupPrincipal? group, + GroupPrincipal groupPrincipal, string source, int depth) { - if (group is null) - { - return; - } - - IEnumerable members = group.ToSafeSortedEnumerable( + IEnumerable members = groupPrincipal.ToSafeSortedEnumerable( selector: group => group.GetMembers(), cmdlet: this, comparer: Comparer); @@ -156,7 +72,7 @@ private void BuildMembersFromAD( } } - private void BuildMembersFromCache(TreeGroup parent, int depth) + protected override void BuildFromCache(TreeGroup parent, int depth) { foreach (TreeObjectBase member in parent.Children) { @@ -168,7 +84,7 @@ private void BuildMembersFromCache(TreeGroup parent, int depth) if (depth <= Depth) { - Index.Add(member.Clone(parent, depth)); + Builder.Add(member.Clone(parent, depth)); } } } @@ -193,7 +109,7 @@ TreeObjectBase AddTreeObject(TreeObjectBase obj) { if (depth <= Depth) { - Index.Stage(obj); + Builder.Stage(obj); } return obj; diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 927c7ac..8342a39 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -15,154 +15,64 @@ namespace PSADTree.Commands; typeof(TreeComputer))] public sealed class GetADTreePrincipalGroupMembershipCommand : PSADTreeCmdletBase { - protected override void ProcessRecord() - { - Dbg.Assert(Identity is not null); - Dbg.Assert(Context is not null); - Principal? principal; + protected override Principal GetFirstPrincipal() => Principal.FindByIdentity(Context, Identity); - try - { - principal = Principal.FindByIdentity(Context, Identity); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (MultipleMatchesException exception) - { - WriteError(exception.ToAmbiguousIdentity(Identity)); - return; - } - catch (Exception exception) + protected override void HandleFirstPrincipal(Principal principal) + { + TreeGroup LinkIfCached(TreeGroup treeGroup) { - WriteError(exception.ToUnspecified(Identity)); - return; - } + // if the first group is cached + if (Cache.TryGet(treeGroup.DistinguishedName, out TreeGroup? _)) + { + // link the children and build from cached path, + // no need to query AD at all! + treeGroup.LinkCachedChildren(Cache); + } - if (principal is null) - { - WriteError(Identity.ToIdentityNotFound()); - return; + return treeGroup; } string source = principal.DistinguishedName; - switch (principal) - { - case UserPrincipal user: - Index.Add(new TreeUser(source, user)); - break; - - case ComputerPrincipal computer: - Index.Add(new TreeComputer(source, computer)); - break; - - case GroupPrincipal group: - TreeGroup treeGroup = new(source, group); - Index.Add(treeGroup); - Cache.TryAdd(treeGroup); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(principal)); - } - - try - { - IEnumerable groups = principal.ToSafeSortedEnumerable( - selector: principal => principal.GetGroups(Context), - cmdlet: this, - comparer: Comparer); - - foreach (Principal parent in groups) - { - if (ShouldExclude(parent)) - { - continue; - } - - GroupPrincipal groupPrincipal = (GroupPrincipal)parent; - TreeGroup treeGroup = new(source, null, groupPrincipal, 1); - PushToStack(groupPrincipal, treeGroup); - } - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) + TreeObjectBase treeObject = principal switch { - WriteError(exception.ToEnumerationFailure(null)); - } - finally - { - principal?.Dispose(); - } + UserPrincipal user => new TreeUser(source, user), + ComputerPrincipal computer => new TreeComputer(source, computer), + GroupPrincipal group => LinkIfCached(new TreeGroup(source, group)), + _ => throw new ArgumentOutOfRangeException(nameof(principal)) + }; - TreeObjectBase[] result = Traverse(source); - DisplayWarningIfTruncatedOutput(); - WriteObject(sendToPipeline: result, enumerateCollection: true); - } + Builder.Add(treeObject); - private TreeObjectBase[] Traverse(string source) - { - int depth; - HashSet visited = []; + IEnumerable principalMembership = principal.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(Context), + cmdlet: this, + comparer: Comparer); - while (Stack.Count > 0) + foreach (Principal parent in principalMembership) { - (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); - - depth = treeGroup.Depth + 1; - Index.Add(treeGroup); - - // if this node has been already processed - if (!Cache.TryAdd(treeGroup)) + if (ShouldExclude(parent)) { - // if it's a circular reference, go next - if (treeGroup.SetIfCircularNested()) - { - continue; - } - - // else, if we want to show all nodes and this node was not yet visited - if (ShowAll && !visited.Add(treeGroup.DistinguishedName)) - { - // reconstruct the output without querying AD again - EnumerateMembership(treeGroup, depth); - continue; - } - - // else, just skip this reference and go next - treeGroup.SetProcessed(); continue; } - // else, get membership from AD Query - EnumerateMembership(current, treeGroup, source, depth); - current?.Dispose(); + GroupPrincipal groupPrincipal = (GroupPrincipal)parent; + TreeGroup treeGroup = new(source, null, groupPrincipal, 1); + PushToStack(groupPrincipal, treeGroup); } - - return Index.GetTree(); } - private void EnumerateMembership( - Principal? principal, + protected override void BuildFromAD( TreeGroup parent, + GroupPrincipal groupPrincipal, string source, int depth) { - if (principal is null) - { - return; - } - - IEnumerable groups = principal.ToSafeSortedEnumerable( + IEnumerable principalMembership = groupPrincipal.ToSafeSortedEnumerable( selector: principal => principal.GetGroups(Context), cmdlet: this, comparer: Comparer); - foreach (Principal group in groups) + foreach (Principal group in principalMembership) { if (ShouldExclude(group)) { @@ -174,7 +84,7 @@ private void EnumerateMembership( } } - private void EnumerateMembership(TreeGroup parent, int depth) + protected override void BuildFromCache(TreeGroup parent, int depth) { if (depth > Depth) { diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 1b657b2..284ad78 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -26,11 +26,11 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable protected PrincipalContext? Context { get; set; } - protected Stack<(GroupPrincipal? group, TreeGroup treeGroup)> Stack { get; } = new(); + protected Stack<(GroupPrincipal? group, TreeGroup treeObject)> Stack { get; } = new(); internal TreeCache Cache { get; } = new(); - internal TreeBuilder Index { get; } = new(); + internal TreeBuilder Builder { get; } = new(); internal PSADTreeComparer Comparer { get; } = new(); @@ -95,15 +95,99 @@ protected override void BeginProcessing() } } - protected bool IsNotVisited(TreeGroup group) => _visited.Add(group.DistinguishedName); - protected override void ProcessRecord() { - Index.Clear(); + Builder.Clear(); _visited.Clear(); _truncatedOutput = false; + + try + { + using Principal? principal = GetFirstPrincipal(); + if (principal is null) + { + WriteError(Identity.ToIdentityNotFound()); + return; + } + + HandleFirstPrincipal(principal); + TreeObjectBase[] result = Traverse(principal.DistinguishedName); + DisplayWarningIfTruncatedOutput(); + WriteObject(sendToPipeline: result, enumerateCollection: true); + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + throw; + } + catch (MultipleMatchesException exception) + { + WriteError(exception.ToAmbiguousIdentity(Identity)); + } + catch (Exception exception) + { + WriteError(exception.ToUnspecified(Identity)); + } + } + + protected abstract Principal GetFirstPrincipal(); + + protected abstract void HandleFirstPrincipal(Principal principal); + + private bool IsNotVisited(TreeGroup group) => _visited.Add(group.DistinguishedName); + + private TreeObjectBase[] Traverse(string source) + { + int depth; + while (Stack.Count > 0) + { + (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); + depth = treeGroup.Depth + 1; + Builder.Add(treeGroup); + + // if this group is already cached + if (!Cache.TryAdd(treeGroup)) + { + current?.Dispose(); + // if it's a circular reference, nothing to do here + if (treeGroup.SetIfCircularNested()) + { + continue; + } + + // else, if we want to show all nodes OR this node was not yet visited + if (ShowAll || IsNotVisited(treeGroup)) + { + // reconstruct the output without querying AD again + BuildFromCache(treeGroup, depth); + continue; + } + + // else, just skip this reference and go next + treeGroup.SetProcessed(); + continue; + } + + if (current is not null) + { + // else, group isn't cached so query AD + BuildFromAD(treeGroup, current, source, depth); + } + + Builder.CommitStaged(); + current?.Dispose(); + } + + return Builder.GetTree(); } + protected abstract void BuildFromAD( + TreeGroup parent, + GroupPrincipal groupPrincipal, + string source, + int depth); + + protected abstract void BuildFromCache(TreeGroup parent, int depth); + protected void PushToStack(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) { if (treeGroup.Depth > Depth) diff --git a/src/PSADTree/TreeIndex.cs b/src/PSADTree/TreeBuilder.cs similarity index 100% rename from src/PSADTree/TreeIndex.cs rename to src/PSADTree/TreeBuilder.cs From 30530a908f53a4743b40564ecffd723c2203ff03 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 17 Sep 2025 15:32:40 -0300 Subject: [PATCH 14/23] improves caching mechanism --- src/PSADTree/TreeGroup.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 27346ed..c93c22d 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -14,7 +14,7 @@ public sealed class TreeGroup : TreeObjectBase private const string VTReset = "\x1B[0m"; - private readonly bool _isCloned; + private bool _isLinked; private List _children; @@ -28,7 +28,7 @@ private TreeGroup( int depth) : base(group, parent, depth) { - _isCloned = true; + _isLinked = true; _children = group._children; IsCircular = group.IsCircular; } @@ -54,7 +54,7 @@ internal TreeGroup( private bool IsCircularNested() { // there is no need to check again if the object is cloned - if (_isCloned) + if (_isLinked) { return IsCircular; } @@ -93,6 +93,7 @@ internal bool SetIfCircularNested() internal void LinkCachedChildren(TreeCache cache) { _children = cache[DistinguishedName]._children; + _isLinked = true; } internal void AddChild(TreeObjectBase child) => _children.Add(child); From a1a6b6bf3aeb3d9fffa228aa0ce1f4bd4b6c27c7 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Wed, 17 Sep 2025 19:28:23 -0300 Subject: [PATCH 15/23] improves caching mechanism --- src/PSADTree/PSADTreeCmdletBase.cs | 13 ++++++------- src/PSADTree/TreeCache.cs | 6 ++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 284ad78..ed0148c 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -171,10 +171,9 @@ private TreeObjectBase[] Traverse(string source) { // else, group isn't cached so query AD BuildFromAD(treeGroup, current, source, depth); + Builder.CommitStaged(); + current.Dispose(); } - - Builder.CommitStaged(); - current?.Dispose(); } return Builder.GetTree(); @@ -204,10 +203,10 @@ protected void PushToStack(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) } protected TreeGroup ProcessGroup( - TreeGroup parent, - GroupPrincipal group, - string source, - int depth) + TreeGroup parent, + GroupPrincipal group, + string source, + int depth) { if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { diff --git a/src/PSADTree/TreeCache.cs b/src/PSADTree/TreeCache.cs index 4abe6e3..185fd7d 100644 --- a/src/PSADTree/TreeCache.cs +++ b/src/PSADTree/TreeCache.cs @@ -9,10 +9,11 @@ internal sealed class TreeCache internal TreeGroup this[string distinguishedName] => _cache[distinguishedName]; - internal void Add(TreeGroup group) => _cache.Add(group.DistinguishedName, group); - internal bool TryAdd(TreeGroup group) { +#if NET6_0_OR_GREATER + return _cache.TryAdd(group.DistinguishedName, group); +#else if (_cache.ContainsKey(group.DistinguishedName)) { return false; @@ -20,6 +21,7 @@ internal bool TryAdd(TreeGroup group) _cache.Add(group.DistinguishedName, group); return true; +#endif } internal bool TryGet(string distinguishedName, [NotNullWhen(true)] out TreeGroup? group) From 12b4c1c4d3ae57b3437e9799f51a5d2755f84cc1 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Sep 2025 10:18:28 -0300 Subject: [PATCH 16/23] improves caching mechanism --- .../Commands/GetADTreeGroupMemberCommand.cs | 11 +---------- .../GetADTreePrincipalGroupMembershipCommand.cs | 15 +-------------- src/PSADTree/PSADTreeCmdletBase.cs | 10 +++++----- src/PSADTree/TreeGroup.cs | 11 ++++++++--- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 5d1b5e0..18e5de7 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -23,16 +23,7 @@ public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase protected override void HandleFirstPrincipal(Principal principal) { GroupPrincipal group = (GroupPrincipal)principal; - TreeGroup treeGroup = new(group.DistinguishedName, group); - // if the first group is cached - if (Cache.TryGet(group.DistinguishedName, out TreeGroup? _)) - { - // link the children and build from cached path, - // no need to query AD at all! - treeGroup.LinkCachedChildren(Cache); - } - - PushToStack(group, treeGroup); + PushToStack(group, new(group.DistinguishedName, group)); } protected override void BuildFromAD( diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 8342a39..0f1fd84 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -19,25 +19,12 @@ public sealed class GetADTreePrincipalGroupMembershipCommand : PSADTreeCmdletBas protected override void HandleFirstPrincipal(Principal principal) { - TreeGroup LinkIfCached(TreeGroup treeGroup) - { - // if the first group is cached - if (Cache.TryGet(treeGroup.DistinguishedName, out TreeGroup? _)) - { - // link the children and build from cached path, - // no need to query AD at all! - treeGroup.LinkCachedChildren(Cache); - } - - return treeGroup; - } - string source = principal.DistinguishedName; TreeObjectBase treeObject = principal switch { UserPrincipal user => new TreeUser(source, user), ComputerPrincipal computer => new TreeComputer(source, computer), - GroupPrincipal group => LinkIfCached(new TreeGroup(source, group)), + GroupPrincipal group => new TreeGroup(source, group), _ => throw new ArgumentOutOfRangeException(nameof(principal)) }; diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index ed0148c..9898e81 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -16,6 +16,8 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable private WildcardPattern[]? _exclusionPatterns; + private readonly TreeCache _cache = new(); + private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled | WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; @@ -28,8 +30,6 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable protected Stack<(GroupPrincipal? group, TreeGroup treeObject)> Stack { get; } = new(); - internal TreeCache Cache { get; } = new(); - internal TreeBuilder Builder { get; } = new(); internal PSADTreeComparer Comparer { get; } = new(); @@ -145,9 +145,10 @@ private TreeObjectBase[] Traverse(string source) Builder.Add(treeGroup); // if this group is already cached - if (!Cache.TryAdd(treeGroup)) + if (!_cache.TryAdd(treeGroup)) { current?.Dispose(); + treeGroup.LinkCachedChildren(_cache); // if it's a circular reference, nothing to do here if (treeGroup.SetIfCircularNested()) { @@ -208,10 +209,9 @@ protected TreeGroup ProcessGroup( string source, int depth) { - if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + if (_cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); - cloned.LinkCachedChildren(Cache); PushToStack(group, cloned); return treeGroup; } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index c93c22d..d8e0935 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -53,7 +53,7 @@ internal TreeGroup( private bool IsCircularNested() { - // there is no need to check again if the object is cloned + // there is no need to check again if the object is linked if (_isLinked) { return IsCircular; @@ -92,8 +92,13 @@ internal bool SetIfCircularNested() internal void LinkCachedChildren(TreeCache cache) { - _children = cache[DistinguishedName]._children; - _isLinked = true; + if (!_isLinked) + { + TreeGroup cached = cache[DistinguishedName]; + _children = cached._children; + _isLinked = true; + IsCircular = cached.IsCircular; + } } internal void AddChild(TreeObjectBase child) => _children.Add(child); From 42169fa677de74fcc65eb03dc2e744b4db3b4ac3 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Sep 2025 11:05:36 -0300 Subject: [PATCH 17/23] some changes to see if it works now :( --- .../Commands/GetADTreeGroupMemberCommand.cs | 6 ++- ...etADTreePrincipalGroupMembershipCommand.cs | 53 +++++++++++++------ src/PSADTree/PSADTreeCmdletBase.cs | 10 ++-- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 18e5de7..ad6eaaa 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -22,8 +22,10 @@ public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase protected override void HandleFirstPrincipal(Principal principal) { - GroupPrincipal group = (GroupPrincipal)principal; - PushToStack(group, new(group.DistinguishedName, group)); + if (principal is GroupPrincipal group && !ShouldExclude(principal)) + { + PushToStack(group, new(group.DistinguishedName, group)); + } } protected override void BuildFromAD( diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 0f1fd84..7a59faf 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -20,31 +20,50 @@ public sealed class GetADTreePrincipalGroupMembershipCommand : PSADTreeCmdletBas protected override void HandleFirstPrincipal(Principal principal) { string source = principal.DistinguishedName; - TreeObjectBase treeObject = principal switch + switch (principal) { - UserPrincipal user => new TreeUser(source, user), - ComputerPrincipal computer => new TreeComputer(source, computer), - GroupPrincipal group => new TreeGroup(source, group), - _ => throw new ArgumentOutOfRangeException(nameof(principal)) - }; + case UserPrincipal user: + HandleOther(new TreeUser(source, user), principal); + break; - Builder.Add(treeObject); + case ComputerPrincipal computer: + HandleOther(new TreeComputer(source, computer), principal); + break; - IEnumerable principalMembership = principal.ToSafeSortedEnumerable( - selector: principal => principal.GetGroups(Context), - cmdlet: this, - comparer: Comparer); + case GroupPrincipal group: + HandleGroup(new TreeGroup(source, group), group); + break; - foreach (Principal parent in principalMembership) + default: + throw new ArgumentOutOfRangeException(nameof(principal)); + } + + void HandleGroup(TreeGroup treeGroup, GroupPrincipal groupPrincipal) { - if (ShouldExclude(parent)) + if (!ShouldExclude(groupPrincipal)) { - continue; + PushToStack(groupPrincipal, treeGroup); } + } + + void HandleOther(TreeObjectBase treeObject, Principal principal) + { + Builder.Add(treeObject); - GroupPrincipal groupPrincipal = (GroupPrincipal)parent; - TreeGroup treeGroup = new(source, null, groupPrincipal, 1); - PushToStack(groupPrincipal, treeGroup); + IEnumerable principalMembership = principal.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(Context), + cmdlet: this, + comparer: Comparer); + + foreach (Principal parent in principalMembership) + { + if (!ShouldExclude(parent)) + { + GroupPrincipal groupPrincipal = (GroupPrincipal)parent; + TreeGroup treeGroup = new(source, null, groupPrincipal, 1); + PushToStack(groupPrincipal, treeGroup); + } + } } } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 9898e81..4ee01e3 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -16,8 +16,6 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable private WildcardPattern[]? _exclusionPatterns; - private readonly TreeCache _cache = new(); - private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled | WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; @@ -30,6 +28,8 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable protected Stack<(GroupPrincipal? group, TreeGroup treeObject)> Stack { get; } = new(); + internal TreeCache Cache { get; } = new(); + internal TreeBuilder Builder { get; } = new(); internal PSADTreeComparer Comparer { get; } = new(); @@ -145,10 +145,10 @@ private TreeObjectBase[] Traverse(string source) Builder.Add(treeGroup); // if this group is already cached - if (!_cache.TryAdd(treeGroup)) + if (!Cache.TryAdd(treeGroup)) { + treeGroup.LinkCachedChildren(Cache); current?.Dispose(); - treeGroup.LinkCachedChildren(_cache); // if it's a circular reference, nothing to do here if (treeGroup.SetIfCircularNested()) { @@ -209,7 +209,7 @@ protected TreeGroup ProcessGroup( string source, int depth) { - if (_cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); PushToStack(group, cloned); From 0c7146e29434a186e1e86d0df0ace9e72e942704 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 18 Sep 2025 13:46:07 -0700 Subject: [PATCH 18/23] for now member cmdlet seem to be working --- .../Commands/GetADTreeGroupMemberCommand.cs | 6 +-- ...etADTreePrincipalGroupMembershipCommand.cs | 4 +- src/PSADTree/PSADTreeCmdletBase.cs | 22 +++++--- src/PSADTree/TreeComputer.cs | 11 ++-- src/PSADTree/TreeGroup.cs | 51 +++++++++---------- src/PSADTree/TreeObjectBase.cs | 9 ++-- src/PSADTree/TreeUser.cs | 11 ++-- 7 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index ad6eaaa..cbb1c4d 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -65,19 +65,19 @@ protected override void BuildFromAD( } } - protected override void BuildFromCache(TreeGroup parent, int depth) + protected override void BuildFromCache(TreeGroup parent, string source, int depth) { foreach (TreeObjectBase member in parent.Children) { if (member is TreeGroup treeGroup) { - PushToStack(null, (TreeGroup)treeGroup.Clone(parent, depth)); + PushToStack(null, (TreeGroup)treeGroup.Clone(parent, source, depth)); continue; } if (depth <= Depth) { - Builder.Add(member.Clone(parent, depth)); + Builder.Add(member.Clone(parent, source, depth)); } } } diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 7a59faf..1892ac3 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -90,7 +90,7 @@ protected override void BuildFromAD( } } - protected override void BuildFromCache(TreeGroup parent, int depth) + protected override void BuildFromCache(TreeGroup parent, string source, int depth) { if (depth > Depth) { @@ -100,7 +100,7 @@ protected override void BuildFromCache(TreeGroup parent, int depth) foreach (TreeObjectBase child in parent.Children) { TreeGroup group = (TreeGroup)child; - PushToStack(null, (TreeGroup)group.Clone(parent, depth)); + PushToStack(null, (TreeGroup)group.Clone(parent, source, depth)); } } } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 4ee01e3..6973f49 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -12,6 +12,8 @@ public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable private bool _truncatedOutput; + private bool _canceled; + private readonly HashSet _visited = []; private WildcardPattern[]? _exclusionPatterns; @@ -133,12 +135,10 @@ protected override void ProcessRecord() protected abstract void HandleFirstPrincipal(Principal principal); - private bool IsNotVisited(TreeGroup group) => _visited.Add(group.DistinguishedName); - private TreeObjectBase[] Traverse(string source) { int depth; - while (Stack.Count > 0) + while (Stack.Count > 0 && !_canceled) { (GroupPrincipal? current, TreeGroup treeGroup) = Stack.Pop(); depth = treeGroup.Depth + 1; @@ -147,8 +147,8 @@ private TreeObjectBase[] Traverse(string source) // if this group is already cached if (!Cache.TryAdd(treeGroup)) { - treeGroup.LinkCachedChildren(Cache); current?.Dispose(); + treeGroup.LinkCachedChildren(Cache); // if it's a circular reference, nothing to do here if (treeGroup.SetIfCircularNested()) { @@ -156,10 +156,10 @@ private TreeObjectBase[] Traverse(string source) } // else, if we want to show all nodes OR this node was not yet visited - if (ShowAll || IsNotVisited(treeGroup)) + if (ShowAll || _visited.Add(treeGroup.DistinguishedName)) { // reconstruct the output without querying AD again - BuildFromCache(treeGroup, depth); + BuildFromCache(treeGroup, source, depth); continue; } @@ -172,6 +172,7 @@ private TreeObjectBase[] Traverse(string source) { // else, group isn't cached so query AD BuildFromAD(treeGroup, current, source, depth); + _visited.Add(treeGroup.DistinguishedName); Builder.CommitStaged(); current.Dispose(); } @@ -186,7 +187,10 @@ protected abstract void BuildFromAD( string source, int depth); - protected abstract void BuildFromCache(TreeGroup parent, int depth); + protected abstract void BuildFromCache( + TreeGroup parent, + string source, + int depth); protected void PushToStack(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) { @@ -211,7 +215,7 @@ protected TreeGroup ProcessGroup( { if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { - TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, depth); + TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, source, depth); PushToStack(group, cloned); return treeGroup; } @@ -232,6 +236,8 @@ protected void DisplayWarningIfTruncatedOutput() protected bool ShouldExclude(Principal principal) => _exclusionPatterns?.Any(pattern => pattern.IsMatch(principal.SamAccountName)) ?? false; + protected override void StopProcessing() => _canceled = true; + protected virtual void Dispose(bool disposing) { if (disposing && !_disposed) diff --git a/src/PSADTree/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index 0ce5324..ae7590d 100644 --- a/src/PSADTree/TreeComputer.cs +++ b/src/PSADTree/TreeComputer.cs @@ -7,8 +7,9 @@ public sealed class TreeComputer : TreeObjectBase private TreeComputer( TreeComputer computer, TreeGroup parent, + string source, int depth) - : base(computer, parent, depth) + : base(computer, parent, source, depth) { } internal TreeComputer( @@ -19,12 +20,10 @@ internal TreeComputer( : base(source, parent, computer, depth) { } - internal TreeComputer( - string source, - ComputerPrincipal computer) + internal TreeComputer(string source, ComputerPrincipal computer) : base(source, computer) { } - internal override TreeObjectBase Clone(TreeGroup parent, int depth) - => new TreeComputer(this, parent, depth); + internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) + => new TreeComputer(this, parent, source, depth); } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index d8e0935..b7f4932 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -6,15 +6,17 @@ namespace PSADTree; public sealed class TreeGroup : TreeObjectBase { - private const string Circular = " ↔ Circular Reference"; + private const string Circular = $" ↔ {VTBrightRed}Circular Reference{VTReset}"; - private const string Processed = " ↔ Processed Group"; + private const string Processed = $" ↔ {VTBrightYellow}Processed Group{VTReset}"; private const string VTBrightRed = "\x1B[91m"; + private const string VTBrightYellow = "\x1B[93m"; + private const string VTReset = "\x1B[0m"; - private bool _isLinked; + public bool IsLinked { get; private set; } private List _children; @@ -25,17 +27,16 @@ public sealed class TreeGroup : TreeObjectBase private TreeGroup( TreeGroup group, TreeGroup parent, + string source, int depth) - : base(group, parent, depth) + : base(group, parent, source, depth) { - _isLinked = true; + IsLinked = true; _children = group._children; IsCircular = group.IsCircular; } - internal TreeGroup( - string source, - GroupPrincipal group) + internal TreeGroup(string source, GroupPrincipal group) : base(source, group) { _children = []; @@ -53,26 +54,23 @@ internal TreeGroup( private bool IsCircularNested() { - // there is no need to check again if the object is linked - if (_isLinked) - { - return IsCircular; - } + // // there is no need to check again if the object is linked + // if (IsLinked) + // { + // return IsCircular; + // } if (Parent is null) { return false; } - TreeGroup? current = Parent; - while (current is not null) + for (TreeGroup? parent = Parent; parent is not null; parent = parent.Parent) { - if (DistinguishedName == current.DistinguishedName) + if (DistinguishedName == parent.DistinguishedName) { return true; } - - current = current.Parent; } return false; @@ -82,7 +80,7 @@ internal bool SetIfCircularNested() { if (IsCircular = IsCircularNested()) { - Hierarchy = $"{Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, VTBrightRed)}{Circular}{VTReset}"; + Hierarchy = $"{Hierarchy}{Circular}"; } return IsCircular; @@ -92,17 +90,14 @@ internal bool SetIfCircularNested() internal void LinkCachedChildren(TreeCache cache) { - if (!_isLinked) - { - TreeGroup cached = cache[DistinguishedName]; - _children = cached._children; - _isLinked = true; - IsCircular = cached.IsCircular; - } + TreeGroup cached = cache[DistinguishedName]; + _children = cached._children; + IsLinked = true; + IsCircular = cached.IsCircular; } internal void AddChild(TreeObjectBase child) => _children.Add(child); - internal override TreeObjectBase Clone(TreeGroup parent, int depth) - => new TreeGroup(this, parent, depth); + internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) + => new TreeGroup(this, parent, source, depth); } diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index ed515a0..8cae161 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -35,10 +35,11 @@ public abstract class TreeObjectBase protected TreeObjectBase( TreeObjectBase treeObject, TreeGroup? parent, + string source, int depth) { Depth = depth; - Source = treeObject.Source; + Source = source; SamAccountName = treeObject.SamAccountName; Domain = treeObject.Domain; ObjectClass = treeObject.ObjectClass; @@ -52,9 +53,7 @@ protected TreeObjectBase( DisplayName = treeObject.DisplayName; } - protected TreeObjectBase( - string source, - Principal principal) + protected TreeObjectBase(string source, Principal principal) { Source = source; SamAccountName = principal.SamAccountName; @@ -83,5 +82,5 @@ protected TreeObjectBase( public override string ToString() => DistinguishedName; - internal abstract TreeObjectBase Clone(TreeGroup parent, int depth); + internal abstract TreeObjectBase Clone(TreeGroup parent, string source, int depth); } diff --git a/src/PSADTree/TreeUser.cs b/src/PSADTree/TreeUser.cs index 50c0600..9abf626 100644 --- a/src/PSADTree/TreeUser.cs +++ b/src/PSADTree/TreeUser.cs @@ -7,8 +7,9 @@ public sealed class TreeUser : TreeObjectBase private TreeUser( TreeUser user, TreeGroup parent, + string source, int depth) - : base(user, parent, depth) + : base(user, parent, source, depth) { } internal TreeUser( @@ -19,12 +20,10 @@ internal TreeUser( : base(source, parent, user, depth) { } - internal TreeUser( - string source, - UserPrincipal user) + internal TreeUser(string source, UserPrincipal user) : base(source, user) { } - internal override TreeObjectBase Clone(TreeGroup parent, int depth) - => new TreeUser(this, parent, depth); + internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) + => new TreeUser(this, parent, source, depth); } From 8c827c2d270cec4724606c341892ea0aa52d7b58 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Sun, 21 Sep 2025 11:11:58 -0300 Subject: [PATCH 19/23] depth to get only --- src/PSADTree/TreeObjectBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index 8cae161..1db12b7 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -6,7 +6,7 @@ namespace PSADTree; public abstract class TreeObjectBase { - internal int Depth { get; set; } + internal int Depth { get; } internal string Source { get; } From 09618e3d5d4e76a198403bfebfadbc8474af9836 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 25 Sep 2025 07:44:02 -0700 Subject: [PATCH 20/23] renames Exception.cs. Moves extensions to their own folder. Makes Depth a public property --- src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs | 1 + .../GetADTreePrincipalGroupMembershipCommand.cs | 1 + .../ExceptionExtensions.cs} | 4 ++-- src/PSADTree/{ => Extensions}/TreeExtensions.cs | 2 +- src/PSADTree/PSADTreeCmdletBase.cs | 1 + src/PSADTree/TreeBuilder.cs | 1 + src/PSADTree/TreeGroup.cs | 11 ----------- src/PSADTree/TreeObjectBase.cs | 3 ++- 8 files changed, 9 insertions(+), 15 deletions(-) rename src/PSADTree/{Exceptions.cs => Extensions/ExceptionExtensions.cs} (93%) rename src/PSADTree/{ => Extensions}/TreeExtensions.cs (99%) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index cbb1c4d..7a23c91 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Management.Automation; +using PSADTree.Extensions; namespace PSADTree.Commands; diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index 1892ac3..c3086fb 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Management.Automation; +using PSADTree.Extensions; namespace PSADTree.Commands; diff --git a/src/PSADTree/Exceptions.cs b/src/PSADTree/Extensions/ExceptionExtensions.cs similarity index 93% rename from src/PSADTree/Exceptions.cs rename to src/PSADTree/Extensions/ExceptionExtensions.cs index 723100f..121dc38 100644 --- a/src/PSADTree/Exceptions.cs +++ b/src/PSADTree/Extensions/ExceptionExtensions.cs @@ -2,9 +2,9 @@ using System.DirectoryServices.AccountManagement; using System.Management.Automation; -namespace PSADTree; +namespace PSADTree.Extensions; -internal static class Exceptions +internal static class ExceptionExtensions { internal static ErrorRecord ToIdentityNotFound(this string? identity) => new( diff --git a/src/PSADTree/TreeExtensions.cs b/src/PSADTree/Extensions/TreeExtensions.cs similarity index 99% rename from src/PSADTree/TreeExtensions.cs rename to src/PSADTree/Extensions/TreeExtensions.cs index 191c234..28686ba 100644 --- a/src/PSADTree/TreeExtensions.cs +++ b/src/PSADTree/Extensions/TreeExtensions.cs @@ -10,7 +10,7 @@ #endif using System.Text.RegularExpressions; -namespace PSADTree; +namespace PSADTree.Extensions; internal static class TreeExtensions { diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index 6973f49..ae23bf3 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -3,6 +3,7 @@ using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; +using PSADTree.Extensions; namespace PSADTree; diff --git a/src/PSADTree/TreeBuilder.cs b/src/PSADTree/TreeBuilder.cs index 245bf89..ffd2c70 100644 --- a/src/PSADTree/TreeBuilder.cs +++ b/src/PSADTree/TreeBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using PSADTree.Extensions; namespace PSADTree; diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index b7f4932..8168f12 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -16,8 +16,6 @@ public sealed class TreeGroup : TreeObjectBase private const string VTReset = "\x1B[0m"; - public bool IsLinked { get; private set; } - private List _children; public ReadOnlyCollection Children => new(_children); @@ -31,7 +29,6 @@ private TreeGroup( int depth) : base(group, parent, source, depth) { - IsLinked = true; _children = group._children; IsCircular = group.IsCircular; } @@ -54,12 +51,6 @@ internal TreeGroup( private bool IsCircularNested() { - // // there is no need to check again if the object is linked - // if (IsLinked) - // { - // return IsCircular; - // } - if (Parent is null) { return false; @@ -92,8 +83,6 @@ internal void LinkCachedChildren(TreeCache cache) { TreeGroup cached = cache[DistinguishedName]; _children = cached._children; - IsLinked = true; - IsCircular = cached.IsCircular; } internal void AddChild(TreeObjectBase child) => _children.Add(child); diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index 8cae161..135d1cd 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -1,12 +1,13 @@ using System; using System.DirectoryServices.AccountManagement; using System.Security.Principal; +using PSADTree.Extensions; namespace PSADTree; public abstract class TreeObjectBase { - internal int Depth { get; set; } + public int Depth { get; set; } internal string Source { get; } From cd5d310d88a08e19f1555b85373e67a11b217b95 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Thu, 25 Sep 2025 07:44:57 -0700 Subject: [PATCH 21/23] renames Exception.cs. Moves extensions to their own folder. Makes Depth a public property --- src/PSADTree/TreeObjectBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index b5e5970..b9d8680 100644 --- a/src/PSADTree/TreeObjectBase.cs +++ b/src/PSADTree/TreeObjectBase.cs @@ -7,8 +7,7 @@ namespace PSADTree; public abstract class TreeObjectBase { - public int Depth { get; set; } - internal int Depth { get; } + public int Depth { get; } internal string Source { get; } From cd6dac60f985928d21347986a43bbcd1c45dc7e6 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 29 Sep 2025 12:09:14 -0700 Subject: [PATCH 22/23] looks like final version. both cmdlets looking good --- src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs | 5 +++-- .../GetADTreePrincipalGroupMembershipCommand.cs | 6 +++--- src/PSADTree/PSADTreeCmdletBase.cs | 10 +++++++--- src/PSADTree/TreeGroup.cs | 5 +---- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 7a23c91..068d268 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -25,7 +25,8 @@ protected override void HandleFirstPrincipal(Principal principal) { if (principal is GroupPrincipal group && !ShouldExclude(principal)) { - PushToStack(group, new(group.DistinguishedName, group)); + string source = group.DistinguishedName; + PushToStack(new TreeGroup(source, group), group); } } @@ -72,7 +73,7 @@ protected override void BuildFromCache(TreeGroup parent, string source, int dept { if (member is TreeGroup treeGroup) { - PushToStack(null, (TreeGroup)treeGroup.Clone(parent, source, depth)); + PushToStack((TreeGroup)treeGroup.Clone(parent, source, depth)); continue; } diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index c3086fb..06e65d6 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -43,7 +43,7 @@ void HandleGroup(TreeGroup treeGroup, GroupPrincipal groupPrincipal) { if (!ShouldExclude(groupPrincipal)) { - PushToStack(groupPrincipal, treeGroup); + PushToStack(treeGroup, groupPrincipal); } } @@ -62,7 +62,7 @@ void HandleOther(TreeObjectBase treeObject, Principal principal) { GroupPrincipal groupPrincipal = (GroupPrincipal)parent; TreeGroup treeGroup = new(source, null, groupPrincipal, 1); - PushToStack(groupPrincipal, treeGroup); + PushToStack(treeGroup, groupPrincipal); } } } @@ -101,7 +101,7 @@ protected override void BuildFromCache(TreeGroup parent, string source, int dept foreach (TreeObjectBase child in parent.Children) { TreeGroup group = (TreeGroup)child; - PushToStack(null, (TreeGroup)group.Clone(parent, source, depth)); + PushToStack((TreeGroup)group.Clone(parent, source, depth)); } } } diff --git a/src/PSADTree/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index ae23bf3..ecd17b3 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; @@ -7,6 +8,7 @@ namespace PSADTree; +[EditorBrowsable(EditorBrowsableState.Never)] public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable { private bool _disposed; @@ -193,7 +195,9 @@ protected abstract void BuildFromCache( string source, int depth); - protected void PushToStack(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) + protected void PushToStack( + TreeGroup treeGroup, + GroupPrincipal? groupPrincipal = null) { if (treeGroup.Depth > Depth) { @@ -217,12 +221,12 @@ protected TreeGroup ProcessGroup( if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, source, depth); - PushToStack(group, cloned); + PushToStack(cloned, group); return treeGroup; } treeGroup = new TreeGroup(source, parent, group, depth); - PushToStack(group, treeGroup); + PushToStack(treeGroup, group); return treeGroup; } diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 8168f12..6aee386 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -80,10 +80,7 @@ internal bool SetIfCircularNested() internal void SetProcessed() => Hierarchy = $"{Hierarchy}{Processed}"; internal void LinkCachedChildren(TreeCache cache) - { - TreeGroup cached = cache[DistinguishedName]; - _children = cached._children; - } + => _children = cache[DistinguishedName]._children; internal void AddChild(TreeObjectBase child) => _children.Add(child); From 38198921d57695847ff1c688aa67f47a582c0a59 Mon Sep 17 00:00:00 2001 From: Santiago Squarzon Date: Mon, 29 Sep 2025 13:07:47 -0700 Subject: [PATCH 23/23] adds perf tests... --- tests/perf-1.1.6.ps1 | 11 +++++++++++ tests/perfCode.ps1 | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/perf-1.1.6.ps1 create mode 100644 tests/perfCode.ps1 diff --git a/tests/perf-1.1.6.ps1 b/tests/perf-1.1.6.ps1 new file mode 100644 index 0000000..c0dca92 --- /dev/null +++ b/tests/perf-1.1.6.ps1 @@ -0,0 +1,11 @@ +$func = 'https://gist.githubusercontent.com/santisq/bd3d1d47c89f030be1b4e57b92baaddd/raw/aa78870a9674e9e4769b05e333586bf405c1362c/Measure-Expression.ps1' +Invoke-RestMethod $func | Invoke-Expression + +$modulePath = Convert-Path $PSScriptRoot\..\output\PSADTree + +Measure-Expression @{ + 'v1.1.6-pwsh-7' = { pwsh -File $PSScriptRoot\perfCode.ps1 $modulePath } + 'v1.1.6-pwsh-5.1' = { powershell -File $PSScriptRoot\perfCode.ps1 $modulePath } + 'v1.1.5-pwsh-7' = { pwsh -File $PSScriptRoot\perfCode.ps1 PSADTree } + 'v1.1.5-pwsh-5.1' = { powershell -File $PSScriptRoot\perfCode.ps1 PSADTree } +} diff --git a/tests/perfCode.ps1 b/tests/perfCode.ps1 new file mode 100644 index 0000000..0c219fe --- /dev/null +++ b/tests/perfCode.ps1 @@ -0,0 +1,11 @@ +param($pathOrName) + +Import-Module $pathOrName + +Write-Host "Running PSADTree v$((Get-Module PSADTree).Version)" + +$objects = Get-ADObject -LDAPFilter '(|(objectClass=group)(objectClass=user))' +0..3 | ForEach-Object { + $objects | Where-Object ObjectClass -EQ group | Get-ADTreeGroupMember -Recursive -ShowAll + $objects | Get-ADTreePrincipalGroupMembership -Recursive -ShowAll +}