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
}
}