Skip to content

Commit f8b2787

Browse files
committed
Implemented util class for base64url, start of JohnnyCrazy#490
1 parent d8e9386 commit f8b2787

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Text;
2+
using NUnit.Framework;
3+
using SpotifyAPI.Web;
4+
5+
namespace SpotifyAPI.Web.Tests
6+
{
7+
[TestFixture]
8+
public class Base64UtilTest
9+
{
10+
[Test]
11+
public void Base64UrlDecode_Works()
12+
{
13+
var encoded = "SGVsbG9Xb3JsZA==";
14+
15+
Assert.AreEqual("HelloWorld", Encoding.UTF8.GetString(Base64Util.UrlDecode(encoded)));
16+
}
17+
18+
[Test]
19+
public void Base64UrlEncode_Works()
20+
{
21+
var decoded = "HelloWorld";
22+
23+
Assert.AreEqual("SGVsbG9Xb3JsZA==", Base64Util.UrlEncode(Encoding.UTF8.GetBytes(decoded)));
24+
}
25+
26+
[Test]
27+
public void Base64UrlEncode_WorksSpecialChars()
28+
{
29+
var bytes = new byte[] { 0x04, 0x9f, 0x9c, 0xff, 0x3f, 0x0a };
30+
31+
// normal base64: BJ+c/z8K
32+
Assert.AreEqual("BJ-c_z8K", Base64Util.UrlEncode(bytes));
33+
}
34+
35+
[Test]
36+
public void Base64UrlDecode_WorksSpecialChars()
37+
{
38+
var bytes = new byte[] { 0x04, 0x9f, 0x9c, 0xff, 0x3f, 0x0a };
39+
40+
// normal base64: BJ+c/z8K
41+
Assert.AreEqual(bytes, Base64Util.UrlDecode("BJ-c_z8K"));
42+
}
43+
}
44+
}

‎SpotifyAPI.Web/Assembly.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("SpotifyAPI.Web.Tests")]

‎SpotifyAPI.Web/Util/Base64Util.cs‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
2+
using System;
3+
using System.Globalization;
4+
5+
namespace SpotifyAPI.Web
6+
{
7+
internal class Base64Util
8+
{
9+
internal const string WebEncoders_InvalidCountOffsetOrLength = "Invalid {0}, {1} or {2} length.";
10+
internal const string WebEncoders_MalformedInput = "Malformed input: {0} is an invalid input length.";
11+
12+
public static string UrlEncode(byte[] input)
13+
{
14+
if (input == null)
15+
{
16+
throw new ArgumentNullException(nameof(input));
17+
}
18+
19+
// Special-case empty input
20+
if (input.Length == 0)
21+
{
22+
return string.Empty;
23+
}
24+
25+
var buffer = new char[GetArraySizeRequiredToEncode(input.Length)];
26+
var numBase64Chars = Convert.ToBase64CharArray(input, 0, input.Length, buffer, 0);
27+
28+
return new string(buffer, startIndex: 0, length: numBase64Chars);
29+
}
30+
31+
public static byte[] UrlDecode(string input)
32+
{
33+
var buffer = new char[GetArraySizeRequiredToDecode(input.Length)];
34+
if (input == null)
35+
{
36+
throw new ArgumentNullException(nameof(input));
37+
}
38+
39+
if (count == 0)
40+
{
41+
return Array.Empty<byte>();
42+
}
43+
44+
// Assumption: input is base64url encoded without padding and contains no whitespace.
45+
46+
var paddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
47+
var arraySizeRequired = checked(count + paddingCharsToAdd);
48+
49+
// Copy input into buffer, fixing up '-' -> '+' and '_' -> '/'.
50+
var i = 0;
51+
for (var j = 0; i < count; i++, j++)
52+
{
53+
var ch = input[j];
54+
if (ch == '-')
55+
{
56+
buffer[i] = '+';
57+
}
58+
else if (ch == '_')
59+
{
60+
buffer[i] = '/';
61+
}
62+
else
63+
{
64+
buffer[i] = ch;
65+
}
66+
}
67+
68+
// Add the padding characters back.
69+
for (; paddingCharsToAdd > 0; i++, paddingCharsToAdd--)
70+
{
71+
buffer[i] = '=';
72+
}
73+
74+
// Decode.
75+
// If the caller provided invalid base64 chars, they'll be caught here.
76+
return Convert.FromBase64CharArray(buffer, 0, arraySizeRequired);
77+
}
78+
79+
private static int GetArraySizeRequiredToEncode(int count)
80+
{
81+
var numWholeOrPartialInputBlocks = checked(count + 2) / 3;
82+
return checked(numWholeOrPartialInputBlocks * 4);
83+
}
84+
85+
private static int GetArraySizeRequiredToDecode(int count)
86+
{
87+
if (count < 0)
88+
{
89+
throw new ArgumentOutOfRangeException(nameof(count));
90+
}
91+
92+
if (count == 0)
93+
{
94+
return 0;
95+
}
96+
97+
var numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
98+
99+
return checked(count + numPaddingCharsToAdd);
100+
}
101+
102+
private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
103+
{
104+
switch (inputLength % 4)
105+
{
106+
case 0:
107+
return 0;
108+
case 2:
109+
return 2;
110+
case 3:
111+
return 1;
112+
default:
113+
throw new FormatException(
114+
string.Format(
115+
CultureInfo.CurrentCulture,
116+
WebEncoders_MalformedInput,
117+
inputLength));
118+
}
119+
}
120+
}
121+
122+
123+
}

0 commit comments

Comments
 (0)