< Summary

Information
Class: DirectSight.Common.Glob
Assembly DirectSight
File(s): /home/runner/work/DirectSight/DirectSight/DirectSight/Common/Glob.cs
Line coverage
41%
Covered lines: 102
Uncovered lines: 142
Coverable lines: 244
Total lines: 439
Line coverage: 41.8%
Branch coverage
38%
Covered branches: 39
Total branches: 102
Branch coverage: 38.2%
Method coverage

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%11100%
.ctor()100%110%
.ctor(...)100%11100%
ToString()100%110%
GetHashCode()100%110%
Equals(...)0%440%
ExpandNames()100%11100%
Expand()100%110%
CreateRegexOrString(...)100%22100%
Expand()54.76%424248.43%
GlobToRegex(...)71.42%141468.75%
Ungroup()3.57%28288.06%
GetDirectories()0%440%
.ctor(...)50%4471.42%
IsMatch(...)25%4483.33%

File(s)

/home/runner/work/DirectSight/DirectSight/DirectSight/Common/Glob.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Text;
 6using System.Text.RegularExpressions;
 7
 8namespace DirectSight.Common;
 9
 10/// <summary>
 11/// Finds files and directories by matching their path names against a pattern.
 12/// Implementation based on https://github.com/mganss/Glob.cs.
 13/// </summary>
 14internal class Glob
 15{
 116    private static readonly char[] GlobCharacters = "*?[]{}".ToCharArray();
 17
 118    private static readonly Dictionary<string, RegexOrString> RegexOrStringCache = [];
 19
 120    private static readonly HashSet<char> RegexSpecialChars = new HashSet<char>(['[', '\\', '^', '$', '.', '|', '?', '*'
 21
 22    /// <summary>
 23    /// Initializes a new instance of the <see cref="Glob"/> class.
 24    /// </summary>
 025    public Glob()
 026    {
 027    }
 28
 29    /// <summary>
 30    /// Initializes a new instance of the <see cref="Glob"/> class.
 31    /// </summary>
 32    /// <param name="pattern">The pattern to be matched. See <see cref="Pattern"/> for syntax.</param>
 1833    public Glob(string pattern)
 1834    {
 1835        this.Pattern = pattern;
 1836    }
 37
 38    /// <summary>
 39    /// Gets or sets a value indicating the pattern to match file and directory names against.
 40    /// The pattern can contain the following special characters:
 41    /// <list type="table">
 42    /// <item>
 43    /// <term>?</term>
 44    /// <description>Matches any single character in a file or directory name.</description>
 45    /// </item>
 46    /// <item>
 47    /// <term>*</term>
 48    /// <description>Matches zero or more characters in a file or directory name.</description>
 49    /// </item>
 50    /// <item>
 51    /// <term>**</term>
 52    /// <description>Matches zero or more recursve directories.</description>
 53    /// </item>
 54    /// <item>
 55    /// <term>[...]</term>
 56    /// <description>Matches a set of characters in a name. Syntax is equivalent to character groups in <see cref="Syste
 57    /// </item>
 58    /// <item>
 59    /// <term>{group1,group2,...}</term>
 60    /// <description>Matches any of the pattern groups. Groups can contain groups and patterns.</description>
 61    /// </item>
 62    /// </list>
 63    /// </summary>
 3664    public string Pattern { get; set; }
 65
 66    /// <summary>
 67    /// Gets or sets a value indicating whether case should be ignored in file and directory names. Default is true.
 68    /// </summary>
 6269    public bool IgnoreCase { get; set; } = true;
 70
 71    /// <summary>
 72    /// Returns a <see cref="string" /> that represents this instance.
 73    /// </summary>
 74    /// <returns>
 75    /// A <see cref="string" /> that represents this instance.
 76    /// </returns>
 77    public override string ToString()
 078    {
 079        return this.Pattern;
 080    }
 81
 82    /// <summary>
 83    /// Returns a hash code for this instance.
 84    /// </summary>
 85    /// <returns>
 86    /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
 87    /// </returns>
 88    public override int GetHashCode()
 089    {
 090        return this.Pattern.GetHashCode();
 091    }
 92
 93    /// <summary>
 94    /// Determines whether the specified <see cref="object" />, is equal to this instance.
 95    /// </summary>
 96    /// <param name="obj">The <see cref="object" /> to compare with this instance.</param>
 97    /// <returns>
 98    ///   <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.
 99    /// </returns>
 100    public override bool Equals(object obj)
 0101    {
 102        // Check for null and compare run-time types.
 0103        if (obj == null || this.GetType() != obj.GetType())
 0104        {
 0105            return false;
 106        }
 107
 0108        var g = (Glob)obj;
 0109        return this.Pattern == g.Pattern;
 0110    }
 111
 112    /// <summary>
 113    /// Performs a pattern match.
 114    /// </summary>
 115    /// <returns>The matched path names.</returns>
 116    public IEnumerable<string> ExpandNames()
 18117    {
 153118        return this.Expand(this.Pattern, false).Select(f => f.FullName);
 18119    }
 120
 121    /// <summary>
 122    /// Performs a pattern match.
 123    /// </summary>
 124    /// <returns>The matched <see cref="FileSystemInfo"/> objects.</returns>
 125    public IEnumerable<FileSystemInfo> Expand()
 0126    {
 0127        return this.Expand(this.Pattern, false);
 0128    }
 129
 130    private RegexOrString CreateRegexOrString(string pattern)
 11131    {
 11132        if (!RegexOrStringCache.TryGetValue(pattern, out RegexOrString regexOrString))
 6133        {
 6134            regexOrString = new RegexOrString(GlobToRegex(pattern), pattern, this.IgnoreCase, compileRegex: true);
 6135            RegexOrStringCache[pattern] = regexOrString;
 6136        }
 137
 11138        return regexOrString;
 11139    }
 140
 141    private IEnumerable<FileSystemInfo> Expand(string path, bool dirOnly)
 38142    {
 38143        if (string.IsNullOrEmpty(path))
 0144        {
 0145            yield break;
 146        }
 147
 148        // stop looking if there are no more glob characters in the path.
 149        // but only if ignoring case because FileSystemInfo.Exists always ignores case.
 38150        if (this.IgnoreCase && path.IndexOfAny(GlobCharacters) < 0)
 27151        {
 27152            FileSystemInfo fsi = null;
 27153            bool exists = false;
 154
 27155            fsi = dirOnly ? (FileSystemInfo)new DirectoryInfo(path) : new FileInfo(path);
 27156            exists = fsi.Exists;
 157
 27158            if (exists)
 24159            {
 24160                yield return fsi;
 15161            }
 162
 18163            yield break;
 164        }
 165
 11166        string parent = Path.GetDirectoryName(path);
 167
 11168        if (parent == null)
 0169        {
 0170            DirectoryInfo dir = new DirectoryInfo(path);
 171
 0172            if (dir != null)
 0173            {
 0174                yield return dir;
 0175            }
 176
 0177            yield break;
 178        }
 179
 11180        if (parent == string.Empty)
 1181        {
 1182            parent = Directory.GetCurrentDirectory();
 1183        }
 184
 11185        var child = Path.GetFileName(path);
 186
 187        // handle groups that contain folders
 188        // child will contain unmatched closing brace
 95189        if (child.Count(c => c == '}') > child.Count(c => c == '{'))
 0190        {
 0191            foreach (var group in Ungroup(path))
 0192            {
 0193                foreach (var item in this.Expand(group, dirOnly))
 0194                {
 0195                    yield return item;
 0196                }
 0197            }
 198
 0199            yield break;
 200        }
 201
 11202        if (child == "**")
 0203        {
 0204            foreach (DirectoryInfo dir in this.Expand(parent, true).DistinctBy(d => d.FullName).Cast<DirectoryInfo>())
 0205            {
 0206                yield return dir;
 207
 0208                foreach (var subDir in GetDirectories(dir))
 0209                {
 0210                    yield return subDir;
 0211                }
 0212            }
 213
 0214            yield break;
 215        }
 216
 22217        var childRegexes = Ungroup(child).Select(s => this.CreateRegexOrString(s)).ToList();
 218
 84219        foreach (DirectoryInfo parentDir in this.Expand(parent, true).DistinctBy(d => d.FullName).Cast<DirectoryInfo>())
 17220        {
 17221            IEnumerable<FileSystemInfo> fileSystemEntries = dirOnly ? parentDir.EnumerateDirectories() : parentDir.Enume
 222
 593223            foreach (var fileSystemEntry in fileSystemEntries)
 271224            {
 542225                if (childRegexes.Any(r => r.IsMatch(fileSystemEntry.Name)))
 128226                {
 128227                    yield return fileSystemEntry;
 128228                }
 271229            }
 230
 34231            if (childRegexes.Any(r => r.Pattern == @"^\.\.$"))
 0232            {
 0233                yield return parentDir.Parent ?? parentDir;
 0234            }
 235
 34236            if (childRegexes.Any(r => r.Pattern == @"^\.$"))
 0237            {
 0238                yield return parentDir;
 0239            }
 17240        }
 11241    }
 242
 243    private static string GlobToRegex(string glob)
 6244    {
 6245        var regex = new StringBuilder();
 6246        var characterClass = false;
 247
 6248        regex.Append("^");
 249
 84250        foreach (var c in glob)
 33251        {
 33252            if (characterClass)
 0253            {
 0254                if (c == ']')
 0255                {
 0256                    characterClass = false;
 0257                }
 258
 0259                regex.Append(c);
 0260                continue;
 261            }
 262
 33263            switch (c)
 264            {
 265                case '*':
 7266                    regex.Append(".*");
 7267                    break;
 268                case '?':
 1269                    regex.Append(".");
 1270                    break;
 271                case '[':
 0272                    characterClass = true;
 0273                    regex.Append(c);
 0274                    break;
 275                default:
 25276                    if (RegexSpecialChars.Contains(c))
 3277                    {
 3278                        regex.Append('\\');
 3279                    }
 280
 25281                    regex.Append(c);
 25282                    break;
 283            }
 33284        }
 285
 6286        regex.Append("$");
 287
 6288        return regex.ToString();
 6289    }
 290
 291    private static IEnumerable<string> Ungroup(string path)
 11292    {
 11293        if (!path.Contains('{'))
 11294        {
 11295            yield return path;
 11296            yield break;
 297        }
 298
 0299        var level = 0;
 0300        var option = new StringBuilder();
 0301        var prefix = string.Empty;
 0302        var postfix = string.Empty;
 0303        var options = new List<string>();
 304
 0305        for (int i = 0; i < path.Length; i++)
 0306        {
 0307            var c = path[i];
 308
 0309            switch (c)
 310            {
 311                case '{':
 0312                    level++;
 0313                    if (level == 1)
 0314                    {
 0315                        prefix = option.ToString();
 0316                        option.Clear();
 0317                    }
 318                    else
 0319                    {
 0320                        option.Append(c);
 0321                    }
 322
 0323                    break;
 324                case ',':
 0325                    if (level == 1)
 0326                    {
 0327                        options.Add(option.ToString());
 0328                        option.Clear();
 0329                    }
 330                    else
 0331                    {
 0332                        option.Append(c);
 0333                    }
 334
 0335                    break;
 336                case '}':
 0337                    level--;
 0338                    if (level == 0)
 0339                    {
 0340                        options.Add(option.ToString());
 0341                        break;
 342                    }
 343                    else
 0344                    {
 0345                        option.Append(c);
 0346                    }
 347
 0348                    break;
 349                default:
 0350                    option.Append(c);
 0351                    break;
 352            }
 353
 0354            if (level == 0 && c == '}' && (i + 1) < path.Length)
 0355            {
 0356                postfix = path.Substring(i + 1);
 0357                break;
 358            }
 0359        }
 360
 361        // invalid grouping
 0362        if (level > 0)
 0363        {
 0364            yield return path;
 0365            yield break;
 366        }
 367
 0368        var postGroups = Ungroup(postfix);
 369
 0370        foreach (var opt in options.SelectMany(o => Ungroup(o)))
 0371        {
 0372            foreach (var postGroup in postGroups)
 0373            {
 0374                var s = prefix + opt + postGroup;
 0375                yield return s;
 0376            }
 0377        }
 0378    }
 379
 380    private static IEnumerable<DirectoryInfo> GetDirectories(DirectoryInfo root)
 0381    {
 382        IEnumerable<DirectoryInfo> subDirs;
 383
 384        try
 0385        {
 0386            subDirs = root.EnumerateDirectories();
 0387        }
 0388        catch (Exception)
 0389        {
 0390            yield break;
 391        }
 392
 0393        foreach (DirectoryInfo dirInfo in subDirs)
 0394        {
 0395            yield return dirInfo;
 396
 0397            foreach (var recursiveDir in GetDirectories(dirInfo))
 0398            {
 0399                yield return recursiveDir;
 0400            }
 0401        }
 0402    }
 403
 404    private class RegexOrString
 405    {
 6406        public RegexOrString(string pattern, string rawString, bool ignoreCase, bool compileRegex)
 6407        {
 6408            this.IgnoreCase = ignoreCase;
 409
 410            try
 6411            {
 6412                this.Regex = new Regex(
 6413                    pattern,
 6414                    RegexOptions.CultureInvariant | (ignoreCase ? RegexOptions.IgnoreCase : 0) | (compileRegex ? RegexOp
 6415                this.Pattern = pattern;
 6416            }
 0417            catch
 0418            {
 0419                this.Pattern = rawString;
 0420            }
 6421        }
 422
 548423        public Regex Regex { get; set; }
 424
 40425        public string Pattern { get; set; }
 426
 6427        public bool IgnoreCase { get; set; }
 428
 429        public bool IsMatch(string input)
 271430        {
 271431            if (this.Regex != null)
 271432            {
 271433                return this.Regex.IsMatch(input);
 434            }
 435
 0436            return this.Pattern.Equals(input, this.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Or
 271437        }
 438    }
 439}