-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathSecombaBase4K.cs
More file actions
265 lines (234 loc) · 9.72 KB
/
SecombaBase4K.cs
File metadata and controls
265 lines (234 loc) · 9.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// Some parts of the following code were used from Secomba/Base4K on the MIT License basis.
// See the associated license file for more information.
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
namespace SecureFolderFS.Core.Cryptography.Cipher
{
public enum Base4KVersion
{
V1,
V2
}
public static class SecombaBase4K
{
// Base addresses for mapping regions
private const int BASE_FLAG_START = 0x04000;
private const int BASE1_START = 0x06000;
private const int BASE1_START_LEGACY = 0x05000;
// Sizes of each mapping region
private const int BASE_FLAG_SIZE = 0x100;
private const int BASE1_SIZE = 0x01000;
private static readonly UTF8Encoding Utf8Encoding = new UTF8Encoding(true, true);
/// <summary>
/// Encodes the specified raw bytes as a Base4K string, mapping each group of bits
/// to Unicode characters in a specific range, suitable for use as file names.
/// </summary>
/// <param name="raw">The raw bytes to encode.</param>
/// <param name="version">The version of Base4K encoding to use. Defaults to <see cref="Base4KVersion.V2"/>.</param>
/// <returns>A Base4K-encoded string representation of the input bytes.</returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="raw"/> is empty or too short to encode.</exception>
[SkipLocalsInit]
public static string Encode(ReadOnlySpan<byte> raw, Base4KVersion version = Base4KVersion.V2)
{
if (raw.Length <= 1)
throw new ArgumentException("Input must be at least 2 bytes long.", nameof(raw));
var maxByteCount = (raw.Length + 1) * 3;
var rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
try
{
var buffer = rentedBuffer.AsSpan();
var bufferPos = 0;
Span<byte> utf8Buffer = stackalloc byte[4];
int offset;
for (var i = 0; i < raw.Length * 2 - 2; i += 3)
{
offset = i % 2 == 0
? ((raw[i / 2] << 4) | ((raw[i / 2 + 1] >> 4) & 0x0f)) & 0x0fff
: ((raw[i / 2] << 8) | (raw[i / 2 + 1] & 0xff)) & 0x0fff;
offset += version == Base4KVersion.V1 ? BASE1_START_LEGACY : BASE1_START;
var written = ToUtf8(offset, utf8Buffer);
utf8Buffer.Slice(0, written).CopyTo(buffer.Slice(bufferPos));
bufferPos += written;
}
if ((raw.Length * 2) % 3 == 2)
{
offset = (raw[^1] & 0xff) + BASE_FLAG_START;
var written = ToUtf8(offset, utf8Buffer);
utf8Buffer.Slice(0, written).CopyTo(buffer.Slice(bufferPos));
bufferPos += written;
}
else if ((raw.Length * 2) % 3 == 1)
{
offset = (raw[^1] & 0x0f) + BASE_FLAG_START;
var written = ToUtf8(offset, utf8Buffer);
utf8Buffer.Slice(0, written).CopyTo(buffer.Slice(bufferPos));
bufferPos += written;
}
return Utf8Encoding.GetString(buffer.Slice(0, bufferPos));
}
finally
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
/// <summary>
/// Decodes a Base4K-encoded string back to the original raw bytes.
/// Attempts decoding with both V2 and V1 (legacy) base addresses automatically.
/// </summary>
/// <param name="encoded">The Base4K-encoded string to decode.</param>
/// <returns>The decoded bytes, or <see langword="null"/> if decoding failed due to invalid or malformed input.</returns>
public static byte[]? Decode(ReadOnlySpan<char> encoded)
{
return DecodeInternal(encoded, BASE1_START) ?? DecodeInternal(encoded, BASE1_START_LEGACY);
}
private static byte[]? DecodeInternal(ReadOnlySpan<char> encoded, int base1Start)
{
var byteCount = Utf8Encoding.GetByteCount(encoded);
var encBytes = new byte[byteCount];
var written = Utf8Encoding.GetBytes(encoded, encBytes);
using var memoryStream = new MemoryStream();
var rentedCollector = ArrayPool<int>.Shared.Rent(written / 3 + 1);
var collectorCount = 0;
try
{
for (var i = 0; i < written;)
{
int nrOfBytes;
if ((encBytes[i] & 0x80) == 0)
{
// 1 byte
nrOfBytes = 1;
}
else if ((encBytes[i] & 0x40) == 0)
{
// Continuation byte — invalid as a leading byte
return null;
}
else if ((encBytes[i] & 0x20) == 0)
{
// 2 bytes
nrOfBytes = 2;
}
else if ((encBytes[i] & 0x10) == 0)
{
// 3 bytes
nrOfBytes = 3;
}
else if ((encBytes[i] & 0x08) == 0)
{
// 4 bytes
nrOfBytes = 4;
}
else
{
// Invalid leading byte
return null;
}
var code = ToCode(encBytes, i, nrOfBytes);
i += nrOfBytes;
if (!(code >= base1Start && code < base1Start + BASE1_SIZE))
{
if (i < written || !(code >= BASE_FLAG_START && code < BASE_FLAG_START + BASE_FLAG_SIZE))
return null;
}
rentedCollector[collectorCount++] = code;
}
for (var i = 0; i < collectorCount; i++)
{
if (rentedCollector[i] >= base1Start)
rentedCollector[i] -= base1Start;
else
{
rentedCollector[i] -= BASE_FLAG_START;
if (i % 2 == 0)
memoryStream.WriteByte((byte)rentedCollector[i]);
else
memoryStream.WriteByte((byte)(((rentedCollector[i - 1] << 4) | ((rentedCollector[i] & 0x0f)) & 0xff)));
break;
}
if (i % 2 == 0)
memoryStream.WriteByte((byte)(rentedCollector[i] >> 4));
else
{
memoryStream.WriteByte((byte)(((rentedCollector[i - 1] << 4) | ((rentedCollector[i] & 0x0f00) >> 8)) & 0xff));
memoryStream.WriteByte((byte)(rentedCollector[i] & 0xff));
}
}
}
finally
{
ArrayPool<int>.Shared.Return(rentedCollector);
}
return memoryStream.ToArray();
}
private static int ToUtf8(int code, Span<byte> destination)
{
switch (code)
{
case > 0xffff:
{
destination[0] = (byte)(0xf0 | ((code >> 18) & 0x07));
destination[1] = (byte)(0x80 | ((code >> 12) & 0x3f));
destination[2] = (byte)(0x80 | ((code >> 6) & 0x3f));
destination[3] = (byte)(0x80 | (code & 0x3f));
return 4;
}
case > 0x7ff:
{
destination[0] = (byte)(0xe0 | ((code >> 12) & 0x0f));
destination[1] = (byte)(0x80 | ((code >> 6) & 0x3f));
destination[2] = (byte)(0x80 | (code & 0x3f));
return 3;
}
case > 0x7f:
{
destination[0] = (byte)(0xc0 | ((code >> 6) & 0x1f));
destination[1] = (byte)(0x80 | (code & 0x3f));
return 2;
}
default:
{
destination[0] = (byte)(code & 0x7f);
return 1;
}
}
}
private static int ToCode(ReadOnlySpan<byte> utf8Char, int offset, int length)
{
var result = 0;
switch (length)
{
case 1:
{
result |= utf8Char[offset];
break;
}
case 2:
{
result |= (utf8Char[offset + 0] & 0x1f) << 6;
result |= (utf8Char[offset + 1] & 0x3f);
break;
}
case 3:
{
result |= (utf8Char[offset + 0] & 0x0f) << 12;
result |= (utf8Char[offset + 1] & 0x3f) << 6;
result |= (utf8Char[offset + 2] & 0x3f);
break;
}
case 4:
{
result |= (utf8Char[offset + 0] & 0x07) << 18;
result |= (utf8Char[offset + 1] & 0x3f) << 12;
result |= (utf8Char[offset + 2] & 0x3f) << 6;
result |= (utf8Char[offset + 3] & 0x3f);
break;
}
}
return result;
}
}
}