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 = @() diff --git a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs index 1c32970..068d268 100644 --- a/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs +++ b/src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Management.Automation; +using PSADTree.Extensions; namespace PSADTree.Commands; @@ -17,150 +19,46 @@ 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); - _truncatedOutput = false; - - try - { - using GroupPrincipal? group = GroupPrincipal.FindByIdentity(_context, Identity); - if (group is null) - { - WriteError(Exceptions.IdentityNotFound(Identity)); - return; - } - - TreeObjectBase[] result = Traverse( - groupPrincipal: group, - source: group.DistinguishedName); + protected override Principal GetFirstPrincipal() => GroupPrincipal.FindByIdentity(Context, Identity); - DisplayWarningIfTruncatedOutput(); - WriteObject(sendToPipeline: result, enumerateCollection: true); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (MultipleMatchesException exception) - { - WriteError(exception.AmbiguousIdentity(Identity)); - } - catch (Exception exception) - { - WriteError(exception.Unspecified(Identity)); - } - } - - private TreeObjectBase[] Traverse( - GroupPrincipal groupPrincipal, - string source) + protected override void HandleFirstPrincipal(Principal principal) { - int depth; - Clear(); - Push(groupPrincipal, new TreeGroup(source, groupPrincipal)); - - while (_stack.Count > 0) + if (principal is GroupPrincipal group && !ShouldExclude(principal)) { - (GroupPrincipal? current, TreeGroup treeGroup) = _stack.Pop(); - - try - { - depth = treeGroup.Depth + 1; - - // if this node has been already processed - if (!_cache.TryAdd(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(); - continue; - } - - using PrincipalSearchResult? search = current?.GetMembers(); - - if (search is not null) - { - EnumerateMembers(treeGroup, search, source, depth); - } - - _index.Add(treeGroup); - _index.TryAddPrincipals(); - current?.Dispose(); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) - { - WriteError(exception.EnumerationFailure(current)); - } + string source = group.DistinguishedName; + PushToStack(new TreeGroup(source, group), group); } - - return _index.GetTree(); } - private void EnumerateMembers( + protected override void BuildFromAD( TreeGroup parent, - PrincipalSearchResult searchResult, + GroupPrincipal groupPrincipal, string source, int depth) { - foreach (Principal member in searchResult.GetSortedEnumerable(_comparer)) + IEnumerable members = groupPrincipal.ToSafeSortedEnumerable( + selector: group => group.GetMembers(), + cmdlet: this, + comparer: Comparer); + + foreach (Principal member in members) { IDisposable? disposable = null; try { - if (member is { DistinguishedName: null }) - { - disposable = member; - continue; - } - - if (member.StructuralObjectClass != "group") + if (member is { DistinguishedName: null } || + member.StructuralObjectClass != "group" && Group.IsPresent || + ShouldExclude(member)) { disposable = member; - if (Group.IsPresent) - { - continue; - } - } - - if (ShouldExclude(member, _exclusionPatterns)) - { continue; } - TreeObjectBase treeObject = ProcessPrincipal( + ProcessPrincipal( principal: member, parent: parent, source: source, depth: depth); - - if (ShowAll.IsPresent) - { - parent.AddChild(treeObject); - } } finally { @@ -169,62 +67,47 @@ private void EnumerateMembers( } } - private TreeObjectBase ProcessPrincipal( + protected override void BuildFromCache(TreeGroup parent, string source, int depth) + { + foreach (TreeObjectBase member in parent.Children) + { + if (member is TreeGroup treeGroup) + { + PushToStack((TreeGroup)treeGroup.Clone(parent, source, depth)); + continue; + } + + if (depth <= Depth) + { + Builder.Add(member.Clone(parent, source, depth)); + } + } + } + + 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)), - GroupPrincipal group => HandleGroup(parent, group, source, depth), + GroupPrincipal group => ProcessGroup(parent, group, source, depth), _ => throw new ArgumentOutOfRangeException(nameof(principal)), }; + parent.AddChild(treeObject); + TreeObjectBase AddTreeObject(TreeObjectBase obj) { if (depth <= Depth) { - _index.AddPrincipal(obj); + Builder.Stage(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) - { - if (member is TreeGroup treeGroup) - { - Push(null, (TreeGroup)treeGroup.Clone(parent, depth)); - continue; - } - - if (depth <= Depth) - { - _index.Add(member.Clone(parent, depth)); - } - } } } diff --git a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs index bf00155..06e65d6 100644 --- a/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs +++ b/src/PSADTree/Commands/GetADTreePrincipalGroupMembershipCommand.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Management.Automation; +using PSADTree.Extensions; namespace PSADTree.Commands; @@ -14,199 +16,92 @@ 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); - _truncatedOutput = false; - Principal? principal; - Clear(); - - try - { - principal = Principal.FindByIdentity(_context, Identity); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (MultipleMatchesException exception) - { - WriteError(exception.AmbiguousIdentity(Identity)); - return; - } - catch (Exception exception) - { - WriteError(exception.Unspecified(Identity)); - return; - } - - if (principal is null) - { - WriteError(Exceptions.IdentityNotFound(Identity)); - return; - } + protected override Principal GetFirstPrincipal() => Principal.FindByIdentity(Context, Identity); + protected override void HandleFirstPrincipal(Principal principal) + { string source = principal.DistinguishedName; switch (principal) { case UserPrincipal user: - _index.Add(new TreeUser(source, user)); + HandleOther(new TreeUser(source, user), principal); break; case ComputerPrincipal computer: - _index.Add(new TreeComputer(source, computer)); + HandleOther(new TreeComputer(source, computer), principal); break; case GroupPrincipal group: - TreeGroup treeGroup = new(source, group); - _index.Add(treeGroup); - _cache.Add(treeGroup); + HandleGroup(new TreeGroup(source, group), group); break; default: throw new ArgumentOutOfRangeException(nameof(principal)); } - try + void HandleGroup(TreeGroup treeGroup, GroupPrincipal groupPrincipal) { - using PrincipalSearchResult search = principal.GetGroups(_context); - foreach (Principal parent in search.GetSortedEnumerable(_comparer)) + if (!ShouldExclude(groupPrincipal)) { - if (ShouldExclude(parent, _exclusionPatterns)) - { - continue; - } - - GroupPrincipal groupPrincipal = (GroupPrincipal)parent; - TreeGroup treeGroup = new(source, null, groupPrincipal, 1); - Push(groupPrincipal, treeGroup); + PushToStack(treeGroup, groupPrincipal); } } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) - { - WriteError(exception.EnumerationFailure(null)); - } - finally - { - principal?.Dispose(); - } - TreeObjectBase[] result = Traverse(source); - DisplayWarningIfTruncatedOutput(); - WriteObject(sendToPipeline: result, enumerateCollection: true); - } - - private TreeObjectBase[] Traverse(string source) - { - int depth; - while (_stack.Count > 0) + void HandleOther(TreeObjectBase treeObject, Principal principal) { - (GroupPrincipal? current, TreeGroup treeGroup) = _stack.Pop(); - - try - { - depth = treeGroup.Depth + 1; - - // if this node has been already processed - if (!_cache.TryAdd(treeGroup)) - { - current?.Dispose(); - treeGroup.Hook(_cache); - _index.Add(treeGroup); - - // if it's a circular reference, go next - if (TreeCache.IsCircular(treeGroup)) - { - treeGroup.SetCircularNested(); - continue; - } + Builder.Add(treeObject); - // else, if we want to show all nodes - if (ShowAll.IsPresent) - { - // reconstruct the output without querying AD again - EnumerateMembership(treeGroup, depth); - continue; - } + IEnumerable principalMembership = principal.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(Context), + cmdlet: this, + comparer: Comparer); - // else, just skip this reference and go next - treeGroup.SetProcessed(); - continue; - } - - using PrincipalSearchResult? search = current?.GetGroups(_context); - - if (search is not null) + foreach (Principal parent in principalMembership) + { + if (!ShouldExclude(parent)) { - EnumerateMembership(treeGroup, search, source, depth); + GroupPrincipal groupPrincipal = (GroupPrincipal)parent; + TreeGroup treeGroup = new(source, null, groupPrincipal, 1); + PushToStack(treeGroup, groupPrincipal); } - - _index.Add(treeGroup); - current?.Dispose(); - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) - { - WriteError(exception.EnumerationFailure(current)); } } - - return _index.GetTree(); } - private void EnumerateMembership( + protected override void BuildFromAD( TreeGroup parent, - PrincipalSearchResult searchResult, + GroupPrincipal groupPrincipal, string source, int depth) { - foreach (Principal group in searchResult.GetSortedEnumerable(_comparer)) - { - if (ShouldExclude(group, _exclusionPatterns)) - { - continue; - } - - TreeGroup treeGroup = ProcessGroup((GroupPrincipal)group); - if (ShowAll.IsPresent) - { - parent.AddChild(treeGroup); - } - } + IEnumerable principalMembership = groupPrincipal.ToSafeSortedEnumerable( + selector: principal => principal.GetGroups(Context), + cmdlet: this, + comparer: Comparer); - TreeGroup ProcessGroup(GroupPrincipal group) + foreach (Principal group in principalMembership) { - if (_cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) + if (ShouldExclude(group)) { - Push(group, (TreeGroup)treeGroup.Clone(parent, depth)); - return treeGroup; + continue; } - treeGroup = new(source, parent, group, depth); - Push(group, treeGroup); - return treeGroup; + TreeGroup treeGroup = ProcessGroup(parent, (GroupPrincipal)group, source, depth); + parent.AddChild(treeGroup); } } - private void EnumerateMembership(TreeGroup parent, int depth) + protected override void BuildFromCache(TreeGroup parent, string source, int depth) { if (depth > 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)); + PushToStack((TreeGroup)group.Clone(parent, source, depth)); } } } diff --git a/src/PSADTree/Exceptions.cs b/src/PSADTree/Extensions/ExceptionExtensions.cs similarity index 53% rename from src/PSADTree/Exceptions.cs rename to src/PSADTree/Extensions/ExceptionExtensions.cs index 8537db3..121dc38 100644 --- a/src/PSADTree/Exceptions.cs +++ b/src/PSADTree/Extensions/ExceptionExtensions.cs @@ -2,26 +2,26 @@ using System.DirectoryServices.AccountManagement; using System.Management.Automation; -namespace PSADTree; +namespace PSADTree.Extensions; -internal static class Exceptions +internal static class ExceptionExtensions { - 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/Extensions/TreeExtensions.cs b/src/PSADTree/Extensions/TreeExtensions.cs new file mode 100644 index 0000000..28686ba --- /dev/null +++ b/src/PSADTree/Extensions/TreeExtensions.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +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.Extensions; + +internal static class TreeExtensions +{ + private static readonly Regex s_reDefaultNamingContext = new( + "(?<=,)DC=.+$", + RegexOptions.Compiled); + +#if !NETCOREAPP + [ThreadStatic] + private static StringBuilder? s_sb; +#endif + internal static string Indent(this string inputString, int indentation) + { + 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(' ', repeatCount) + .Append(corner) + .Append(inputString) + .ToString(); +#endif + } + + internal static TreeObjectBase[] Format( + this TreeObjectBase[] tree) + { + int index; + for (int i = 0; i < tree.Length; i++) + { + TreeObjectBase current = tree[i]; + + if ((index = current.Hierarchy.IndexOf('└')) == -1) + { + continue; + } + + for (int z = i - 1; z >= 0; z--) + { + current = tree[z]; + string hierarchy = current.Hierarchy; + + if (char.IsWhiteSpace(hierarchy[index])) + { + current.Hierarchy = hierarchy.ReplaceAt(index, '│'); + continue; + } + + if (hierarchy[index] == '└') + { + current.Hierarchy = hierarchy.ReplaceAt(index, '├'); + } + + break; + } + } + + return tree; + } + +#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)); + } + + 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 + { + List principals = []; + using PrincipalSearchResult search = selector(principal); + using IEnumerator enumerator = search.GetEnumerator(); + + while (true) + { + try + { + if (!enumerator.MoveNext()) + { + break; + } + + principals.Add(enumerator.Current); + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + throw; + } + catch (Exception exception) + { + cmdlet.WriteError(exception.ToEnumerationFailure(principal)); + } + } + + return principals + .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/PSADTreeCmdletBase.cs b/src/PSADTree/PSADTreeCmdletBase.cs index d7c9bf5..ecd17b3 100644 --- a/src/PSADTree/PSADTreeCmdletBase.cs +++ b/src/PSADTree/PSADTreeCmdletBase.cs @@ -1,36 +1,43 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.DirectoryServices.AccountManagement; using System.Linq; using System.Management.Automation; +using PSADTree.Extensions; namespace PSADTree; +[EditorBrowsable(EditorBrowsableState.Never)] public abstract class PSADTreeCmdletBase : PSCmdlet, IDisposable { - protected const string DepthParameterSet = "Depth"; + private bool _disposed; - protected const string RecursiveParameterSet = "Recursive"; + private bool _truncatedOutput; - protected PrincipalContext? _context; + private bool _canceled; - private bool _disposed; + private readonly HashSet _visited = []; + + private WildcardPattern[]? _exclusionPatterns; + + private const WildcardOptions WildcardPatternOptions = WildcardOptions.Compiled + | WildcardOptions.CultureInvariant + | WildcardOptions.IgnoreCase; - protected bool _truncatedOutput; + protected const string DepthParameterSet = "Depth"; - protected readonly Stack<(GroupPrincipal? group, TreeGroup treeGroup)> _stack = new(); + protected const string RecursiveParameterSet = "Recursive"; - internal readonly TreeCache _cache = new(); + protected PrincipalContext? Context { get; set; } - internal readonly TreeIndex _index = new(); + protected Stack<(GroupPrincipal? group, TreeGroup treeObject)> Stack { get; } = new(); - internal PSADTreeComparer _comparer = new(); + internal TreeCache Cache { get; } = new(); - protected WildcardPattern[]? _exclusionPatterns; + internal TreeBuilder Builder { get; } = new(); - private const WildcardOptions _wpoptions = WildcardOptions.Compiled - | WildcardOptions.CultureInvariant - | WildcardOptions.IgnoreCase; + internal PSADTreeComparer Comparer { get; } = new(); [Parameter( Position = 0, @@ -72,18 +79,16 @@ protected override void BeginProcessing() if (Exclude is not null) { - _exclusionPatterns = Exclude - .Select(e => new WildcardPattern(e, _wpoptions)) - .ToArray(); + _exclusionPatterns = [.. Exclude.Select(e => new WildcardPattern(e, WildcardPatternOptions))]; } 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, @@ -91,75 +96,162 @@ protected override void BeginProcessing() } catch (Exception exception) { - ThrowTerminatingError(exception.SetPrincipalContext()); + ThrowTerminatingError(exception.ToSetPrincipalContext()); } } - protected void Push(GroupPrincipal? groupPrincipal, TreeGroup treeGroup) + protected override void ProcessRecord() { - if (treeGroup.Depth > Depth) + Builder.Clear(); + _visited.Clear(); + _truncatedOutput = false; + + try { - return; + 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)); + } + } - if (treeGroup.Depth == Depth) + protected abstract Principal GetFirstPrincipal(); + + protected abstract void HandleFirstPrincipal(Principal principal); + + private TreeObjectBase[] Traverse(string source) + { + int depth; + while (Stack.Count > 0 && !_canceled) { - _truncatedOutput = true; + (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(); + treeGroup.LinkCachedChildren(Cache); + // 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 || _visited.Add(treeGroup.DistinguishedName)) + { + // reconstruct the output without querying AD again + BuildFromCache(treeGroup, source, 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); + _visited.Add(treeGroup.DistinguishedName); + Builder.CommitStaged(); + current.Dispose(); + } } - _stack.Push((groupPrincipal, treeGroup)); + return Builder.GetTree(); } - protected void DisplayWarningIfTruncatedOutput() + protected abstract void BuildFromAD( + TreeGroup parent, + GroupPrincipal groupPrincipal, + string source, + int depth); + + protected abstract void BuildFromCache( + TreeGroup parent, + string source, + int depth); + + protected void PushToStack( + TreeGroup treeGroup, + GroupPrincipal? groupPrincipal = null) { - if (_truncatedOutput) + if (treeGroup.Depth > Depth) { - WriteWarning($"Result is truncated as enumeration has exceeded the set depth of {Depth}."); + return; } + + if (treeGroup.Depth == Depth) + { + _truncatedOutput = true; + } + + Stack.Push((groupPrincipal, treeGroup)); } - private static bool MatchAny( - Principal principal, - WildcardPattern[] patterns) + protected TreeGroup ProcessGroup( + TreeGroup parent, + GroupPrincipal group, + string source, + int depth) { - foreach (WildcardPattern pattern in patterns) + if (Cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup)) { - if (pattern.IsMatch(principal.SamAccountName)) - { - return true; - } + TreeGroup cloned = (TreeGroup)treeGroup.Clone(parent, source, depth); + PushToStack(cloned, group); + return treeGroup; } - return false; + treeGroup = new TreeGroup(source, parent, group, depth); + PushToStack(treeGroup, group); + return treeGroup; } - protected static bool ShouldExclude( - Principal principal, - WildcardPattern[]? patterns) + protected void DisplayWarningIfTruncatedOutput() { - if (patterns is null) + if (_truncatedOutput) { - return false; + WriteWarning($"Result is truncated as enumeration has exceeded the set depth of {Depth}."); } - - return MatchAny(principal, patterns); } + 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) { - _context?.Dispose(); + Context?.Dispose(); _disposed = true; } } - protected void Clear() - { - _index.Clear(); - _cache.Clear(); - } - public void Dispose() { Dispose(disposing: true); diff --git a/src/PSADTree/TreeBuilder.cs b/src/PSADTree/TreeBuilder.cs new file mode 100644 index 0000000..ffd2c70 --- /dev/null +++ b/src/PSADTree/TreeBuilder.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using PSADTree.Extensions; + +namespace PSADTree; + +internal sealed class TreeBuilder +{ + private readonly List _principals = []; + + private readonly List _output = []; + + internal void Stage(TreeObjectBase principal) => _principals.Add(principal); + + internal void Add(TreeObjectBase principal) => _output.Add(principal); + + internal void CommitStaged() + { + if (_principals.Count > 0) + { + _output.AddRange(_principals); + _principals.Clear(); + } + } + + internal TreeObjectBase[] GetTree() => _output.ToArray().Format(); + + internal void Clear() + { + _output.Clear(); + _principals.Clear(); + } +} diff --git a/src/PSADTree/TreeCache.cs b/src/PSADTree/TreeCache.cs index e7209da..185fd7d 100644 --- a/src/PSADTree/TreeCache.cs +++ b/src/PSADTree/TreeCache.cs @@ -5,18 +5,15 @@ namespace PSADTree; internal sealed class TreeCache { - private readonly Dictionary _cache; + private readonly Dictionary _cache = []; - internal TreeGroup this[string distinguishedName] => - _cache[distinguishedName]; - - internal TreeCache() => _cache = []; - - internal void Add(TreeGroup treeGroup) => - _cache.Add(treeGroup.DistinguishedName, treeGroup); + internal TreeGroup this[string distinguishedName] => _cache[distinguishedName]; internal bool TryAdd(TreeGroup group) { +#if NET6_0_OR_GREATER + return _cache.TryAdd(group.DistinguishedName, group); +#else if (_cache.ContainsKey(group.DistinguishedName)) { return false; @@ -24,33 +21,9 @@ internal bool TryAdd(TreeGroup group) _cache.Add(group.DistinguishedName, group); return true; +#endif } - 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/TreeComputer.cs b/src/PSADTree/TreeComputer.cs index e06906a..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/TreeExtensions.cs b/src/PSADTree/TreeExtensions.cs deleted file mode 100644 index 551edbc..0000000 --- a/src/PSADTree/TreeExtensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.DirectoryServices.AccountManagement; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace PSADTree; - -internal static class TreeExtensions -{ - private static readonly Regex s_reDefaultNamingContext = new( - "(?<=,)DC=.+$", - RegexOptions.Compiled); - - private static readonly StringBuilder s_sb = new(); - - internal static string Indent(this string inputString, int indentation) - { - s_sb.Clear(); - - return s_sb - .Append(' ', (4 * indentation) - 4) - .Append("└── ") - .Append(inputString) - .ToString(); - } - - internal static string GetDefaultNamingContext(this string distinguishedName) => - s_reDefaultNamingContext.Match(distinguishedName).Value; - - internal static TreeObjectBase[] ConvertToTree( - this TreeObjectBase[] inputObject) - { - int index; - TreeObjectBase current; - for (int i = 0; i < inputObject.Length; i++) - { - current = inputObject[i]; - if ((index = current.Hierarchy.IndexOf('└')) == -1) - { - continue; - } - - int z; - char[] replace; - for (z = i - 1; z >= 0; z--) - { - current = inputObject[z]; - if (!char.IsWhiteSpace(current.Hierarchy[index])) - { - UpdateCorner(index, current); - break; - } - - replace = current.Hierarchy.ToCharArray(); - replace[index] = '│'; - current.Hierarchy = new string(replace); - } - } - - return inputObject; - } - - internal static IOrderedEnumerable GetSortedEnumerable( - this PrincipalSearchResult search, PSADTreeComparer comparer) => - search - .OrderBy(static e => e.StructuralObjectClass == "group") - .ThenBy(static e => e, comparer); - - - private static void UpdateCorner(int index, TreeObjectBase current) - { - if (current.Hierarchy[index] == '└') - { - char[] replace = current.Hierarchy.ToCharArray(); - replace[index] = '├'; - current.Hierarchy = new string(replace); - } - } -} diff --git a/src/PSADTree/TreeGroup.cs b/src/PSADTree/TreeGroup.cs index 32a34e0..6aee386 100644 --- a/src/PSADTree/TreeGroup.cs +++ b/src/PSADTree/TreeGroup.cs @@ -1,43 +1,43 @@ -using System; 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 = $" ↔ {VTBrightRed}Circular Reference{VTReset}"; - private const string _isProcessed = " ↔ Processed Group"; + private const string Processed = $" ↔ {VTBrightYellow}Processed Group{VTReset}"; - private const string _vtBrightRed = "\x1B[91m"; + private const string VTBrightRed = "\x1B[91m"; - private const string _vtReset = "\x1B[0m"; + private const string VTBrightYellow = "\x1B[93m"; - private static readonly StringBuilder s_sb = new(); + 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; } private TreeGroup( TreeGroup group, TreeGroup parent, + string source, int depth) - : base(group, parent, depth) + : base(group, parent, source, depth) { - _childs = group._childs; + _children = group._children; + IsCircular = group.IsCircular; } - internal TreeGroup( - string source, - GroupPrincipal group) + internal TreeGroup(string source, GroupPrincipal group) : base(source, group) - { } + { + _children = []; + } internal TreeGroup( string source, @@ -45,30 +45,45 @@ internal TreeGroup( GroupPrincipal group, int depth) : base(source, parent, group, depth) - { } - - internal void SetCircularNested() { - IsCircular = true; - Hierarchy = s_sb - .Append(Hierarchy.Insert(Hierarchy.IndexOf("─ ") + 2, _vtBrightRed)) - .Append(_isCircular) - .Append(_vtReset) - .ToString(); - - s_sb.Clear(); + _children = []; } - internal void SetProcessed() => Hierarchy = string.Concat(Hierarchy, _isProcessed); - - internal void Hook(TreeCache cache) => _childs ??= cache[DistinguishedName]._childs; + private bool IsCircularNested() + { + if (Parent is null) + { + return false; + } + + for (TreeGroup? parent = Parent; parent is not null; parent = parent.Parent) + { + if (DistinguishedName == parent.DistinguishedName) + { + return true; + } + } + + return false; + } - internal void AddChild(TreeObjectBase child) + internal bool SetIfCircularNested() { - _childs ??= []; - _childs.Add(child); + if (IsCircular = IsCircularNested()) + { + Hierarchy = $"{Hierarchy}{Circular}"; + } + + return IsCircular; } - internal override TreeObjectBase Clone(TreeGroup parent, int depth) => - new TreeGroup(this, parent, depth); + internal void SetProcessed() => Hierarchy = $"{Hierarchy}{Processed}"; + + internal void LinkCachedChildren(TreeCache cache) + => _children = cache[DistinguishedName]._children; + + internal void AddChild(TreeObjectBase child) => _children.Add(child); + + internal override TreeObjectBase Clone(TreeGroup parent, string source, int depth) + => new TreeGroup(this, parent, source, depth); } diff --git a/src/PSADTree/TreeIndex.cs b/src/PSADTree/TreeIndex.cs deleted file mode 100644 index 10c66d9..0000000 --- a/src/PSADTree/TreeIndex.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; - -namespace PSADTree; - -internal sealed class TreeIndex -{ - private readonly List _principals; - - private readonly List _output; - - internal TreeIndex() - { - _principals = []; - _output = []; - } - - internal void AddPrincipal(TreeObjectBase principal) => _principals.Add(principal); - - internal void Add(TreeObjectBase principal) => _output.Add(principal); - - internal void TryAddPrincipals() - { - if (_principals.Count > 0) - { - _output.AddRange([.. _principals]); - _principals.Clear(); - } - } - - internal TreeObjectBase[] GetTree() => _output.ToArray().ConvertToTree(); - - internal void Clear() - { - _output.Clear(); - _principals.Clear(); - } -} diff --git a/src/PSADTree/TreeObjectBase.cs b/src/PSADTree/TreeObjectBase.cs index ed515a0..b9d8680 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; } internal string Source { get; } @@ -35,10 +36,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 +54,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 +83,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 2497243..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); } 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 +}