From 828c1f5b54f5102a68b7c25e6e849ce98cdc996b Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 10 Jan 2019 20:48:42 -0600 Subject: [PATCH] Add a pattern option to EnumerateEntries --- src/LibHac/Compatibility/Env.cs | 13 + src/LibHac/Compatibility/FileSystemName.cs | 367 ++++++++++++++++++ .../{Compat.cs => Compatibility/Rsa.cs} | 6 +- src/LibHac/Crypto.cs | 4 +- src/LibHac/IO/FileSystemExtensions.cs | 46 ++- 5 files changed, 427 insertions(+), 9 deletions(-) create mode 100644 src/LibHac/Compatibility/Env.cs create mode 100644 src/LibHac/Compatibility/FileSystemName.cs rename src/LibHac/{Compat.cs => Compatibility/Rsa.cs} (93%) diff --git a/src/LibHac/Compatibility/Env.cs b/src/LibHac/Compatibility/Env.cs new file mode 100644 index 00000000..d80193ae --- /dev/null +++ b/src/LibHac/Compatibility/Env.cs @@ -0,0 +1,13 @@ +using System; + +namespace LibHac.Compatibility +{ + /// + /// Contains variables describing runtime environment info + /// needed for compatibility code. + /// + internal static class Env + { + public static bool IsMono { get; } = Type.GetType("Mono.Runtime") != null; + } +} diff --git a/src/LibHac/Compatibility/FileSystemName.cs b/src/LibHac/Compatibility/FileSystemName.cs new file mode 100644 index 00000000..040962b3 --- /dev/null +++ b/src/LibHac/Compatibility/FileSystemName.cs @@ -0,0 +1,367 @@ + +#if NETFRAMEWORK + +// This code was introduced in .NET Core 2.1 + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace LibHac.Compatibility +{ + /// + /// Provides methods for matching file system names. + /// + internal static class FileSystemName + { + private static readonly char[] WildcardChars = + { + '\"', '<', '>', '*', '?' + }; + + private static readonly char[] SimpleWildcardChars = + { + '*', '?' + }; + + /// + /// Return true if the given expression matches the given name. '*' and '?' are wildcards, '\' escapes. + /// + public static bool MatchesSimpleExpression(ReadOnlySpan expression, ReadOnlySpan name, bool ignoreCase = true) + { + return MatchPattern(expression, name, ignoreCase, useExtendedWildcards: false); + } + + // Matching routine description + // ============================ + // (copied from native impl) + // + // This routine compares a Dbcs name and an expression and tells the caller + // if the name is in the language defined by the expression. The input name + // cannot contain wildcards, while the expression may contain wildcards. + // + // Expression wild cards are evaluated as shown in the nondeterministic + // finite automatons below. Note that ~* and ~? are DOS_STAR and DOS_QM. + // + // ~* is DOS_STAR, ~? is DOS_QM, and ~. is DOS_DOT + // + // S + // <-----< + // X | | e Y + // X * Y == (0)----->-(1)->-----(2)-----(3) + // + // S-. + // <-----< + // X | | e Y + // X ~* Y == (0)----->-(1)->-----(2)-----(3) + // + // X S S Y + // X ?? Y == (0)---(1)---(2)---(3)---(4) + // + // X . . Y + // X ~.~. Y == (0)---(1)----(2)------(3)---(4) + // | |________| + // | ^ | + // |_______________| + // ^EOF or .^ + // + // X S-. S-. Y + // X ~?~? Y == (0)---(1)-----(2)-----(3)---(4) + // | |________| + // | ^ | + // |_______________| + // ^EOF or .^ + // + // where S is any single character + // S-. is any single character except the final . + // e is a null character transition + // EOF is the end of the name string + // + // In words: + // + // * matches 0 or more characters. + // ? matches exactly 1 character. + // DOS_STAR matches 0 or more characters until encountering and matching + // the final . in the name. + // DOS_QM matches any single character, or upon encountering a period or + // end of name string, advances the expression to the end of the + // set of contiguous DOS_QMs. + // DOS_DOT matches either a . or zero characters beyond name string. + + private static bool MatchPattern(ReadOnlySpan expression, ReadOnlySpan name, bool ignoreCase, bool useExtendedWildcards) + { + // The idea behind the algorithm is pretty simple. We keep track of all possible locations + // in the regular expression that are matching the name. When the name has been exhausted, + // if one of the locations in the expression is also just exhausted, the name is in the + // language defined by the regular expression. + + if (expression.Length == 0 || name.Length == 0) + return false; + + if (expression[0] == '*') + { + // Just * matches everything + if (expression.Length == 1) + return true; + + ReadOnlySpan expressionEnd = expression.Slice(1); + if (expressionEnd.IndexOfAny(useExtendedWildcards ? WildcardChars : SimpleWildcardChars) == -1) + { + // Handle the special case of a single starting *, which essentially means "ends with" + + // If the name doesn't have enough characters to match the remaining expression, it can't be a match. + if (name.Length < expressionEnd.Length) + return false; + + // See if we end with the expression + return name.EndsWith(expressionEnd, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + } + + int nameOffset = 0; + int expressionOffset; + + int priorMatch; + int currentMatch; + int priorMatchCount; + int matchCount = 1; + + char nameChar = '\0'; + char expressionChar; + + // ReSharper disable once RedundantAssignment + Span temp = stackalloc int[0]; + Span currentMatches = stackalloc int[16]; + Span priorMatches = stackalloc int[16]; + priorMatches[0] = 0; + + int maxState = expression.Length * 2; + int currentState; + bool nameFinished = false; + + // Walk through the name string, picking off characters. We go one + // character beyond the end because some wild cards are able to match + // zero characters beyond the end of the string. + // + // With each new name character we determine a new set of states that + // match the name so far. We use two arrays that we swap back and forth + // for this purpose. One array lists the possible expression states for + // all name characters up to but not including the current one, and other + // array is used to build up the list of states considering the current + // name character as well. The arrays are then switched and the process + // repeated. + // + // There is not a one-to-one correspondence between state number and + // offset into the expression. State numbering is not continuous. + // This allows a simple conversion between state number and expression + // offset. Each character in the expression can represent one or two + // states. * and DOS_STAR generate two states: expressionOffset * 2 and + // expressionOffset * 2 + 1. All other expression characters can produce + // only a single state. Thus expressionOffset = currentState / 2. + + while (!nameFinished) + { + if (nameOffset < name.Length) + { + // Not at the end of the name. Grab the current character and move the offset forward. + nameChar = name[nameOffset++]; + } + else + { + // At the end of the name. If the expression is exhausted, exit. + if (priorMatches[matchCount - 1] == maxState) + break; + + nameFinished = true; + } + + // Now, for each of the previous stored expression matches, see what + // we can do with this name character. + priorMatch = 0; + currentMatch = 0; + priorMatchCount = 0; + + while (priorMatch < matchCount) + { + // We have to carry on our expression analysis as far as possible for each + // character of name, so we loop here until the expression stops matching. + + expressionOffset = (priorMatches[priorMatch++] + 1) / 2; + + while (expressionOffset < expression.Length) + { + currentState = expressionOffset * 2; + expressionChar = expression[expressionOffset]; + + // We may be about to exhaust the local space for matches, + // so we have to reallocate if this is the case. + if (currentMatch >= currentMatches.Length - 2) + { + int newSize = currentMatches.Length * 2; + temp = new int[newSize]; + currentMatches.CopyTo(temp); + currentMatches = temp; + + temp = new int[newSize]; + priorMatches.CopyTo(temp); + priorMatches = temp; + } + + if (expressionChar == '*') + { + // '*' matches any character zero or more times. + // ReSharper disable once RedundantJumpStatement + goto MatchZeroOrMore; + } + else if (useExtendedWildcards && expressionChar == '<') + { + // '<' (DOS_STAR) matches any character except '.' zero or more times. + + // If we are at a period, determine if we are allowed to + // consume it, i.e. make sure it is not the last one. + + bool notLastPeriod = false; + if (!nameFinished && nameChar == '.') + { + for (int offset = nameOffset; offset < name.Length; offset++) + { + if (name[offset] == '.') + { + notLastPeriod = true; + break; + } + } + } + + if (nameFinished || nameChar != '.' || notLastPeriod) + { + // ReSharper disable once RedundantJumpStatement + goto MatchZeroOrMore; + } + else + { + // We are at a period. We can only match zero + // characters (i.e. the epsilon transition). + goto MatchZero; + } + } + else + { + // The remaining expression characters all match by consuming a character, + // so we need to force the expression and state forward. + currentState += 2; + + if (useExtendedWildcards && expressionChar == '>') + { + // '>' (DOS_QM) is the most complicated. If the name is finished, + // we can match zero characters. If this name is a '.', we + // don't match, but look at the next expression. Otherwise + // we match a single character. + if (nameFinished || nameChar == '.') + goto NextExpressionCharacter; + + currentMatches[currentMatch++] = currentState; + goto ExpressionFinished; + } + else if (useExtendedWildcards && expressionChar == '"') + { + // A '"' (DOS_DOT) can match either a period, or zero characters + // beyond the end of name. + if (nameFinished) + { + goto NextExpressionCharacter; + } + else if (nameChar == '.') + { + currentMatches[currentMatch++] = currentState; + } + goto ExpressionFinished; + } + else + { + if (expressionChar == '\\') + { + // Escape character, try to move the expression forward again and match literally. + if (++expressionOffset == expression.Length) + { + currentMatches[currentMatch++] = maxState; + goto ExpressionFinished; + } + + currentState = expressionOffset * 2 + 2; + expressionChar = expression[expressionOffset]; + } + + // From this point on a name character is required to even + // continue, let alone make a match. + if (nameFinished) goto ExpressionFinished; + + if (expressionChar == '?') + { + // If this expression was a '?' we can match it once. + currentMatches[currentMatch++] = currentState; + } + else if (ignoreCase + ? char.ToUpperInvariant(expressionChar) == char.ToUpperInvariant(nameChar) + : expressionChar == nameChar) + { + // Matched a non-wildcard character + currentMatches[currentMatch++] = currentState; + } + + goto ExpressionFinished; + } + } + + MatchZeroOrMore: + currentMatches[currentMatch++] = currentState; + MatchZero: + currentMatches[currentMatch++] = currentState + 1; + NextExpressionCharacter: + if (++expressionOffset == expression.Length) + currentMatches[currentMatch++] = maxState; + } // while (expressionOffset < expression.Length) + + ExpressionFinished: + + // Prevent duplication in the destination array. + // + // Each of the arrays is monotonically increasing and non-duplicating, thus we skip + // over any source element in the source array if we just added the same element to + // the destination array. This guarantees non-duplication in the destination array. + + if ((priorMatch < matchCount) && (priorMatchCount < currentMatch)) + { + while (priorMatchCount < currentMatch) + { + int previousLength = priorMatches.Length; + while ((priorMatch < previousLength) && (priorMatches[priorMatch] < currentMatches[priorMatchCount])) + { + priorMatch++; + } + priorMatchCount++; + } + } + } // while (sourceCount < matchesCount) + + // If we found no matches in the just finished iteration it's time to bail. + if (currentMatch == 0) + return false; + + // Swap the meaning the two arrays + temp = priorMatches; + priorMatches = currentMatches; + currentMatches = temp; + + matchCount = currentMatch; + } // while (!nameFinished) + + currentState = priorMatches[matchCount - 1]; + + return currentState == maxState; + } + } +} +#endif diff --git a/src/LibHac/Compat.cs b/src/LibHac/Compatibility/Rsa.cs similarity index 93% rename from src/LibHac/Compat.cs rename to src/LibHac/Compatibility/Rsa.cs index 13b6b263..b95b1075 100644 --- a/src/LibHac/Compat.cs +++ b/src/LibHac/Compatibility/Rsa.cs @@ -4,12 +4,10 @@ using System; using System.Numerics; using System.Security.Cryptography; -namespace LibHac +namespace LibHac.Compatibility { - internal class Compat + internal static class Rsa { - public static bool IsMono { get; } = Type.GetType("Mono.Runtime") != null; - public static bool Rsa2048PssVerifyMono(byte[] data, byte[] signature, byte[] modulus) { const int rsaLen = 0x100; diff --git a/src/LibHac/Crypto.cs b/src/LibHac/Crypto.cs index 622d0ecf..c0f5201c 100644 --- a/src/LibHac/Crypto.cs +++ b/src/LibHac/Crypto.cs @@ -157,9 +157,9 @@ namespace LibHac public static Validity Rsa2048PssVerify(byte[] data, byte[] signature, byte[] modulus) { #if NETFRAMEWORK - if (Compat.IsMono) + if (Compatibility.Env.IsMono) { - return Compat.Rsa2048PssVerifyMono(data, signature, modulus) + return Compatibility.Rsa.Rsa2048PssVerifyMono(data, signature, modulus) ? Validity.Valid : Validity.Invalid; } diff --git a/src/LibHac/IO/FileSystemExtensions.cs b/src/LibHac/IO/FileSystemExtensions.cs index c84b097e..65beb76b 100644 --- a/src/LibHac/IO/FileSystemExtensions.cs +++ b/src/LibHac/IO/FileSystemExtensions.cs @@ -3,6 +3,10 @@ using System.Buffers; using System.Collections.Generic; using System.IO; +#if !NETFRAMEWORK +using System.IO.Enumeration; +#endif + namespace LibHac.IO { public static class FileSystemExtensions @@ -55,18 +59,35 @@ namespace LibHac.IO source.CopyFileSystem(destFs, logger); } + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem) + { + return fileSystem.OpenDirectory("/", OpenDirectoryMode.All).EnumerateEntries(); + } + public static IEnumerable EnumerateEntries(this IDirectory directory) { + return directory.EnumerateEntries("*", SearchOptions.Default); + } + + public static IEnumerable EnumerateEntries(this IDirectory directory, string searchPattern, SearchOptions searchOptions) + { + bool ignoreCase = searchOptions.HasFlag(SearchOptions.CaseInsensitive); + bool recurse = searchOptions.HasFlag(SearchOptions.RecurseSubdirectories); + IFileSystem fs = directory.ParentFileSystem; foreach (DirectoryEntry entry in directory.Read()) { - yield return entry; - if (entry.Type != DirectoryEntryType.Directory) continue; + if (MatchesPattern(searchPattern, entry.Name, ignoreCase)) + { + yield return entry; + } + + if (entry.Type != DirectoryEntryType.Directory || !recurse) continue; IDirectory subDir = fs.OpenDirectory(directory.FullPath + '/' + entry.Name, OpenDirectoryMode.All); - foreach (DirectoryEntry subEntry in subDir.EnumerateEntries()) + foreach (DirectoryEntry subEntry in subDir.EnumerateEntries(searchPattern, searchOptions)) { yield return subEntry; } @@ -123,5 +144,24 @@ namespace LibHac.IO return count; } + + public static bool MatchesPattern(string searchPattern, string name, bool ignoreCase) + { +#if NETFRAMEWORK + return Compatibility.FileSystemName.MatchesSimpleExpression(searchPattern.AsSpan(), + name.AsSpan(), ignoreCase); +#else + return FileSystemName.MatchesSimpleExpression(searchPattern.AsSpan(), + name.AsSpan(), ignoreCase); +#endif + } + } + + [Flags] + public enum SearchOptions + { + Default = 0, + RecurseSubdirectories = 1 << 0, + CaseInsensitive = 1 << 1 } }