diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e879c7..faef53ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) and is followi ## Unreleased +### :zap: Added + +- [#476](https://github.com/FantasticFiasco/aws-signature-version-4/issues/476) Support for [Unity](https://unity.com/) game engine + ## [2.0.0] - 2021-08-01 ### :zap: Added diff --git a/Directory.Build.props b/Directory.Build.props index bf5f4b98..a46afaad 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,8 @@ - 2.0.0 + 2.1.0 + beta.1 + + - - - - - diff --git a/src/Private/System/Web/HttpUtility.cs b/src/Private/System/Web/HttpUtility.cs new file mode 100644 index 00000000..19608f4e --- /dev/null +++ b/src/Private/System/Web/HttpUtility.cs @@ -0,0 +1,162 @@ +// 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. + +// Authors: +// Patrik Torstensson (Patrik.Torstensson@labs2.com) +// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se) +// Tim Coleman (tim@timcoleman.com) +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// Copyright (C) 2005-2010 Novell, Inc (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#if NET45 +#pragma warning disable IDE0007 // Use implicit type + +using System.Collections.Specialized; +using System.Text; +using System.Web.Util; + +// ReSharper disable once CheckNamespace +namespace System.Web +{ + internal sealed class HttpUtility + { + private sealed class HttpQSCollection : NameValueCollection + { + internal HttpQSCollection() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public override string ToString() + { + int count = Count; + if (count == 0) + { + return ""; + } + + StringBuilder sb = new StringBuilder(); + string?[] keys = AllKeys; + for (int i = 0; i < count; i++) + { + string[]? values = GetValues(keys[i]); + if (values != null) + { + foreach (string value in values) + { + if (string.IsNullOrEmpty(keys[i])) + { + sb.AppendFormat("{0}&", UrlEncode(value)); + } + else + { + sb.AppendFormat("{0}={1}&", keys[i], UrlEncode(value)); + } + } + } + } + + return sb.ToString(0, sb.Length - 1); + } + } + + public static NameValueCollection ParseQueryString(string query) => ParseQueryString(query, Encoding.UTF8); + + public static NameValueCollection ParseQueryString(string query, Encoding encoding) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + HttpQSCollection result = new HttpQSCollection(); + int queryLength = query.Length; + int namePos = queryLength > 0 && query[0] == '?' ? 1 : 0; + if (queryLength == namePos) + { + return result; + } + + while (namePos <= queryLength) + { + int valuePos = -1, valueEnd = -1; + for (int q = namePos; q < queryLength; q++) + { + if (valuePos == -1 && query[q] == '=') + { + valuePos = q + 1; + } + else if (query[q] == '&') + { + valueEnd = q; + break; + } + } + + string? name; + if (valuePos == -1) + { + name = null; + valuePos = namePos; + } + else + { + name = UrlDecode(query.Substring(namePos, valuePos - namePos - 1), encoding); + } + + if (valueEnd < 0) + { + valueEnd = query.Length; + } + + namePos = valueEnd + 1; + string value = UrlDecode(query.Substring(valuePos, valueEnd - valuePos), encoding); + result.Add(name, value); + } + + return result; + } + + public static string UrlEncode(string str) => UrlEncode(str, Encoding.UTF8); + + public static string UrlEncode(string str, Encoding e) => Encoding.ASCII.GetString(UrlEncodeToBytes(str, e)); + + public static byte[]? UrlEncodeToBytes(string str, Encoding e) + { + byte[] bytes = e.GetBytes(str); + return HttpEncoder.UrlEncode(bytes, 0, bytes.Length, alwaysCreateNewReturnValue: false); + } + + public static string UrlDecode(string str, Encoding e) => HttpEncoder.UrlDecode(str, e); + } +} + +#pragma warning restore IDE0007 // Use implicit type +#endif diff --git a/src/Private/System/Web/README.md b/src/Private/System/Web/README.md new file mode 100644 index 00000000..04114ed9 --- /dev/null +++ b/src/Private/System/Web/README.md @@ -0,0 +1,5 @@ +# System.Web + +The following files have been copied from [corefx on GitHub](https://github.com/dotnet/corefx/tree/master/src/System.Web.HttpUtility), because the package `System.Web` is not supported on .NET 4.5 on the Unity game engine. + +For more information, see [#476](https://github.com/FantasticFiasco/aws-signature-version-4/issues/476). diff --git a/src/Private/System/Web/Util/HttpEncoder.cs b/src/Private/System/Web/Util/HttpEncoder.cs new file mode 100644 index 00000000..a183b2c4 --- /dev/null +++ b/src/Private/System/Web/Util/HttpEncoder.cs @@ -0,0 +1,257 @@ +// 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. + +#if NET45 +#pragma warning disable IDE0007 // Use implicit type + +using System.Diagnostics; +using System.Text; + +// ReSharper disable once CheckNamespace +namespace System.Web.Util +{ + internal static class HttpEncoder + { + internal static string UrlDecode(string value, Encoding encoding) + { + int count = value.Length; + UrlDecoder helper = new UrlDecoder(count, encoding); + + // go through the string's chars collapsing %XX and %uXXXX and + // appending each char as char, with exception of %XX constructs + // that are appended as bytes + + for (int pos = 0; pos < count; pos++) + { + char ch = value[pos]; + + if (ch == '+') + { + ch = ' '; + } + else if (ch == '%' && pos < count - 2) + { + if (value[pos + 1] == 'u' && pos < count - 5) + { + int h1 = HttpEncoderUtility.HexToInt(value[pos + 2]); + int h2 = HttpEncoderUtility.HexToInt(value[pos + 3]); + int h3 = HttpEncoderUtility.HexToInt(value[pos + 4]); + int h4 = HttpEncoderUtility.HexToInt(value[pos + 5]); + + if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0) + { // valid 4 hex chars + ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4); + pos += 5; + + // only add as char + helper.AddChar(ch); + continue; + } + } + else + { + int h1 = HttpEncoderUtility.HexToInt(value[pos + 1]); + int h2 = HttpEncoderUtility.HexToInt(value[pos + 2]); + + if (h1 >= 0 && h2 >= 0) + { // valid 2 hex chars + byte b = (byte)((h1 << 4) | h2); + pos += 2; + + // don't add as char + helper.AddByte(b); + continue; + } + } + } + + if ((ch & 0xFF80) == 0) + { + helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode + } + else + { + helper.AddChar(ch); + } + } + + return Utf16StringValidator.ValidateString(helper.GetString()); + } + + internal static byte[]? UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue) + { + byte[]? encoded = UrlEncode(bytes, offset, count); + + return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes)) + ? (byte[])encoded.Clone() + : encoded; + } + + private static byte[]? UrlEncode(byte[] bytes, int offset, int count) + { + if (!ValidateUrlEncodingParameters(bytes, offset, count)) + { + return null; + } + + int cSpaces = 0; + int cUnsafe = 0; + + // count them first + for (int i = 0; i < count; i++) + { + char ch = (char)bytes[offset + i]; + + if (ch == ' ') + { + cSpaces++; + } + else if (!HttpEncoderUtility.IsUrlSafeChar(ch)) + { + cUnsafe++; + } + } + + // nothing to expand? + if (cSpaces == 0 && cUnsafe == 0) + { + // DevDiv 912606: respect "offset" and "count" + if (0 == offset && bytes.Length == count) + { + return bytes; + } + else + { + byte[] subarray = new byte[count]; + Buffer.BlockCopy(bytes, offset, subarray, 0, count); + return subarray; + } + } + + // expand not 'safe' characters into %XX, spaces to +s + byte[] expandedBytes = new byte[count + cUnsafe * 2]; + int pos = 0; + + for (int i = 0; i < count; i++) + { + byte b = bytes[offset + i]; + char ch = (char)b; + + if (HttpEncoderUtility.IsUrlSafeChar(ch)) + { + expandedBytes[pos++] = b; + } + else if (ch == ' ') + { + expandedBytes[pos++] = (byte)'+'; + } + else + { + expandedBytes[pos++] = (byte)'%'; + expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex((b >> 4) & 0xf); + expandedBytes[pos++] = (byte)HttpEncoderUtility.IntToHex(b & 0x0f); + } + } + + return expandedBytes; + } + + private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count) + { + if (bytes == null) + { + throw new ArgumentNullException(nameof(bytes)); + } + if (offset < 0 || offset > bytes.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if (count < 0 || offset + count > bytes.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + return true; + } + + // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes + private class UrlDecoder + { + private readonly int _bufferSize; + + // Accumulate characters in a special array + private int _numChars; + private readonly char[] _charBuffer; + + // Accumulate bytes for decoding into characters in a special array + private int _numBytes; + private byte[]? _byteBuffer; + + // Encoding to convert chars to bytes + private readonly Encoding _encoding; + + private void FlushBytes() + { + if (_numBytes > 0) + { + Debug.Assert(_byteBuffer != null); + _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars); + _numBytes = 0; + } + } + + internal UrlDecoder(int bufferSize, Encoding encoding) + { + _bufferSize = bufferSize; + _encoding = encoding; + + _charBuffer = new char[bufferSize]; + // byte buffer created on demand + } + + internal void AddChar(char ch) + { + if (_numBytes > 0) + { + FlushBytes(); + } + + _charBuffer[_numChars++] = ch; + } + + internal void AddByte(byte b) + { + // if there are no pending bytes treat 7 bit bytes as characters + // this optimization is temp disable as it doesn't work for some encodings + /* + if (_numBytes == 0 && ((b & 0x80) == 0)) { + AddChar((char)b); + } + else + */ + { + if (_byteBuffer == null) + { + _byteBuffer = new byte[_bufferSize]; + } + + _byteBuffer[_numBytes++] = b; + } + } + + internal string GetString() + { + if (_numBytes > 0) + { + FlushBytes(); + } + + return _numChars > 0 ? new string(_charBuffer, 0, _numChars) : ""; + } + } + } +} + +#pragma warning restore IDE0007 // Use implicit type +#endif diff --git a/src/Private/System/Web/Util/HttpEncoderUtility.cs b/src/Private/System/Web/Util/HttpEncoderUtility.cs new file mode 100644 index 00000000..612eeaf3 --- /dev/null +++ b/src/Private/System/Web/Util/HttpEncoderUtility.cs @@ -0,0 +1,57 @@ +// 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. + +#if NET45 +#pragma warning disable IDE0066 // Use 'switch' expression + +using System.Diagnostics; + +// ReSharper disable once CheckNamespace +namespace System.Web.Util +{ + internal static class HttpEncoderUtility + { + public static int HexToInt(char h) => + h >= '0' && h <= '9' + ? h - '0' + : h >= 'a' && h <= 'f' + ? h - 'a' + 10 + : h >= 'A' && h <= 'F' + ? h - 'A' + 10 + : -1; + + public static char IntToHex(int n) + { + Debug.Assert(n < 0x10); + + return n <= 9 ? (char)(n + '0') : (char)(n - 10 + 'a'); + } + + // Set of safe chars, from RFC 1738.4 minus '+' + public static bool IsUrlSafeChar(char ch) + { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) + { + return true; + } + + switch (ch) + { + case '-': + case '_': + case '.': + case '!': + case '*': + case '(': + case ')': + return true; + } + + return false; + } + } +} + +#pragma warning restore IDE0066 // Use 'switch' expression +#endif diff --git a/src/Private/System/Web/Util/Utf16StringValidator.cs b/src/Private/System/Web/Util/Utf16StringValidator.cs new file mode 100644 index 00000000..9d8930a0 --- /dev/null +++ b/src/Private/System/Web/Util/Utf16StringValidator.cs @@ -0,0 +1,78 @@ +// 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. + +#if NET45 +#pragma warning disable IDE0007 // Use implicit type + +// ReSharper disable once CheckNamespace +namespace System.Web.Util +{ + internal static class Utf16StringValidator + { + private const char UnicodeReplacementChar = '\uFFFD'; + + internal static string ValidateString(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + // locate the first surrogate character + int idxOfFirstSurrogate = -1; + for (int i = 0; i < input.Length; i++) + { + if (char.IsSurrogate(input[i])) + { + idxOfFirstSurrogate = i; + break; + } + } + + // fast case: no surrogates = return input string + if (idxOfFirstSurrogate < 0) + { + return input; + } + + // slow case: surrogates exist, so we need to validate them + char[] chars = input.ToCharArray(); + for (int i = idxOfFirstSurrogate; i < chars.Length; i++) + { + char thisChar = chars[i]; + + // If this character is a low surrogate, then it was not preceded by + // a high surrogate, so we'll replace it. + if (char.IsLowSurrogate(thisChar)) + { + chars[i] = UnicodeReplacementChar; + continue; + } + + if (char.IsHighSurrogate(thisChar)) + { + // If this character is a high surrogate and it is followed by a + // low surrogate, allow both to remain. + if (i + 1 < chars.Length && char.IsLowSurrogate(chars[i + 1])) + { + i++; // skip the low surrogate also + continue; + } + + // If this character is a high surrogate and it is not followed + // by a low surrogate, replace it. + chars[i] = UnicodeReplacementChar; + continue; + } + + // Otherwise, this is a non-surrogate character and just move to the + // next character. + } + return new string(chars); + } + } +} + +#pragma warning restore IDE0007 // Use implicit type +#endif