using System.Globalization;
using System.Text;
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
namespace XFESystem.Browser;
public sealed partial class DomBridge
{
private sealed class DecoderState(Decoder decoder, bool ignoreBom, string name) { public Decoder Decoder = decoder; public bool IgnoreBom = ignoreBom; public string Name = name; public bool First = true; }
private readonly Dictionary<int, DecoderState> decoders = [];
private int nextDecoderId;
private void InstallEncodingCallbacks()
{
var encoding = new BrowserHostObject(engine);
Function(encoding, "btoa", args =>
{
string text = Argument(args, 0);
if ((text.Length + 2L) / 3 * 4 > limits.MaximumHtmlCharacters) throw new BrowserLimitException("Base64 output exceeds its character budget.");
var bytes = new byte[text.Length];
for (int index = 0; index < text.Length; index++)
{
if (text[index] > 255) throw AdjacentError("InvalidCharacterError", "Base64 input must contain only byte characters.", 5);
bytes[index] = (byte)text[index];
}
return Convert.ToBase64String(bytes);
});
Function(encoding, "atob", args =>
{
string input = Argument(args, 0);
var text = new StringBuilder(input.Length + 3);
foreach (char value in input) if (value is not ('\t' or '\n' or '\f' or '\r' or ' ')) text.Append(value);
if (text.Length % 4 == 0)
{
if (text.Length > 0 && text[^1] == '=') text.Length--;
if (text.Length > 0 && text[^1] == '=') text.Length--;
}
if (text.Length % 4 == 1) throw AdjacentError("InvalidCharacterError", "Invalid Base64 input.", 5);
for (int index = 0; index < text.Length; index++)
if (!(char.IsAsciiLetterOrDigit(text[index]) || text[index] is '+' or '/'))
throw AdjacentError("InvalidCharacterError", "Invalid Base64 input.", 5);
while (text.Length % 4 != 0) text.Append('=');
byte[] bytes = Convert.FromBase64String(text.ToString());
var characters = new char[bytes.Length];
for (int index = 0; index < bytes.Length; index++) characters[index] = (char)bytes[index];
return new string(characters);
});
Function(encoding, "encode", args =>
{
var text = Value(args, 0).IsUndefined() ? string.Empty : Argument(args, 0);
var bytes = Encoding.UTF8.GetBytes(text);
if (bytes.LongLength > limits.MaximumScriptMemoryBytes / 2) throw new BrowserLimitException("UTF-8 output exceeds its byte budget.");
var array = CallInternal(bootstrap.Get("byteArray"), [bytes.Length]).AsObject();
for (var index = 0; index < bytes.Length; ++index) array.Set(index.ToString(CultureInfo.InvariantCulture), (int)bytes[index], array);
return array;
});
if (host.FillRandom is not null) Function(encoding, "random", args =>
{
var view = Value(args, 0).AsObject(); int length = NumberIndex(view.Get("length"));
if (length > 65536) throw new BrowserLimitException("The cryptographic random request exceeds 65536 bytes.");
var bytes = new byte[length]; host.FillRandom(bytes);
for (int index = 0; index < length; index++) view.Set(index.ToString(CultureInfo.InvariantCulture), (int)bytes[index], view);
return JsValue.Undefined;
});
Function(encoding, "decoder", args =>
{
if (decoders.Count >= limits.MaximumPendingTasks) throw new BrowserLimitException("The UTF-8 decoder limit was reached.");
var name = Argument(args, 2); bool fatal = Bool(Value(args, 0));
Encoding codec = name switch
{
"utf-8" => new UTF8Encoding(false, fatal),
"utf-16le" => new UnicodeEncoding(false, false, fatal),
"utf-16be" => new UnicodeEncoding(true, false, fatal),
_ => throw new ArgumentException("Unsupported text decoding label.")
};
var id = ++nextDecoderId; decoders.Add(id, new(codec.GetDecoder(), Bool(Value(args, 1)), name)); return id;
});
Function(encoding, "decode", args =>
{
if (!decoders.TryGetValue(NumberIndex(Value(args, 0)), out var state)) throw new ArgumentException("Unknown text decoder.");
var view = CallInternal(bootstrap.Get("bytesOf"), [Value(args, 1)]).AsObject(); var length = NumberIndex(view.Get("length"));
if (length > limits.MaximumHtmlCharacters * 4L || length > limits.MaximumScriptMemoryBytes / 2) throw new BrowserLimitException("UTF-8 input exceeds its byte budget.");
var bytes = new byte[length]; for (var index = 0; index < length; ++index) bytes[index] = (byte)Math.Clamp(Number(view.Get(index.ToString(CultureInfo.InvariantCulture))), 0, 255);
bool flush = !Bool(Value(args, 2)); var characters = new char[length + 2];
int count;
try { count = state.Decoder.GetChars(bytes, 0, bytes.Length, characters, 0, flush); }
catch (DecoderFallbackException) { state.Decoder.Reset(); state.First = true; throw new ArgumentException("The encoded data is not valid " + state.Name + "."); }
var result = new string(characters, 0, count);
if (state.First && result.Length > 0) { state.First = false; if (!state.IgnoreBom && result[0] == '\uFEFF') result = result[1..]; }
if (flush) { state.Decoder.Reset(); state.First = true; }
if (result.Length > limits.MaximumHtmlCharacters) throw new BrowserLimitException("Decoded text exceeds its character budget.");
return result;
});
engine.SetValue("__xfeEncoding", encoding);
}
}
using System.Globalization;
using System.Text;
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
namespace XFESystem.Browser;
public sealed partial class DomBridge
{
private sealed class DecoderState(Decoder decoder, bool ignoreBom, string name) { public Decoder Decoder = decoder; public bool IgnoreBom = ignoreBom; public string Name = name; public bool First = true; }
private readonly Dictionary<int, DecoderState> decoders = [];
private int nextDecoderId;
private void InstallEncodingCallbacks()
{
var encoding = new BrowserHostObject(engine);
Function(encoding, "btoa", args =>
{
string text = Argument(args, 0);
if ((text.Length + 2L) / 3 * 4 > limits.MaximumHtmlCharacters) throw new BrowserLimitException("Base64 output exceeds its character budget.");
var bytes = new byte[text.Length];
for (int index = 0; index < text.Length; index++)
{
if (text[index] > 255) throw AdjacentError("InvalidCharacterError", "Base64 input must contain only byte characters.", 5);
bytes[index] = (byte)text[index];
}
return Convert.ToBase64String(bytes);
});
Function(encoding, "atob", args =>
{
string input = Argument(args, 0);
var text = new StringBuilder(input.Length + 3);
foreach (char value in input) if (value is not ('\t' or '\n' or '\f' or '\r' or ' ')) text.Append(value);
if (text.Length % 4 == 0)
{
if (text.Length > 0 && text[^1] == '=') text.Length--;
if (text.Length > 0 && text[^1] == '=') text.Length--;
}
if (text.Length % 4 == 1) throw AdjacentError("InvalidCharacterError", "Invalid Base64 input.", 5);
for (int index = 0; index < text.Length; index++)
if (!(char.IsAsciiLetterOrDigit(text[index]) || text[index] is '+' or '/'))
throw AdjacentError("InvalidCharacterError", "Invalid Base64 input.", 5);
while (text.Length % 4 != 0) text.Append('=');
byte[] bytes = Convert.FromBase64String(text.ToString());
var characters = new char[bytes.Length];
for (int index = 0; index < bytes.Length; index++) characters[index] = (char)bytes[index];
return new string(characters);
});
Function(encoding, "encode", args =>
{
var text = Value(args, 0).IsUndefined() ? string.Empty : Argument(args, 0);
var bytes = Encoding.UTF8.GetBytes(text);
if (bytes.LongLength > limits.MaximumScriptMemoryBytes / 2) throw new BrowserLimitException("UTF-8 output exceeds its byte budget.");
var array = CallInternal(bootstrap.Get("byteArray"), [bytes.Length]).AsObject();
for (var index = 0; index < bytes.Length; ++index) array.Set(index.ToString(CultureInfo.InvariantCulture), (int)bytes[index], array);
return array;
});
if (host.FillRandom is not null) Function(encoding, "random", args =>
{
var view = Value(args, 0).AsObject(); int length = NumberIndex(view.Get("length"));
if (length > 65536) throw new BrowserLimitException("The cryptographic random request exceeds 65536 bytes.");
var bytes = new byte[length]; host.FillRandom(bytes);
for (int index = 0; index < length; index++) view.Set(index.ToString(CultureInfo.InvariantCulture), (int)bytes[index], view);
return JsValue.Undefined;
});
Function(encoding, "decoder", args =>
{
if (decoders.Count >= limits.MaximumPendingTasks) throw new BrowserLimitException("The UTF-8 decoder limit was reached.");
var name = Argument(args, 2); bool fatal = Bool(Value(args, 0));
Encoding codec = name switch
{
"utf-8" => new UTF8Encoding(false, fatal),
"utf-16le" => new UnicodeEncoding(false, false, fatal),
"utf-16be" => new UnicodeEncoding(true, false, fatal),
_ => throw new ArgumentException("Unsupported text decoding label.")
};
var id = ++nextDecoderId; decoders.Add(id, new(codec.GetDecoder(), Bool(Value(args, 1)), name)); return id;
});
Function(encoding, "decode", args =>
{
if (!decoders.TryGetValue(NumberIndex(Value(args, 0)), out var state)) throw new ArgumentException("Unknown text decoder.");
var view = CallInternal(bootstrap.Get("bytesOf"), [Value(args, 1)]).AsObject(); var length = NumberIndex(view.Get("length"));
if (length > limits.MaximumHtmlCharacters * 4L || length > limits.MaximumScriptMemoryBytes / 2) throw new BrowserLimitException("UTF-8 input exceeds its byte budget.");
var bytes = new byte[length]; for (var index = 0; index < length; ++index) bytes[index] = (byte)Math.Clamp(Number(view.Get(index.ToString(CultureInfo.InvariantCulture))), 0, 255);
bool flush = !Bool(Value(args, 2)); var characters = new char[length + 2];
int count;
try { count = state.Decoder.GetChars(bytes, 0, bytes.Length, characters, 0, flush); }
catch (DecoderFallbackException) { state.Decoder.Reset(); state.First = true; throw new ArgumentException("The encoded data is not valid " + state.Name + "."); }
var result = new string(characters, 0, count);
if (state.First && result.Length > 0) { state.First = false; if (!state.IgnoreBom && result[0] == '\uFEFF') result = result[1..]; }
if (flush) { state.Decoder.Reset(); state.First = true; }
if (result.Length > limits.MaximumHtmlCharacters) throw new BrowserLimitException("Decoded text exceeds its character budget.");
return result;
});
engine.SetValue("__xfeEncoding", encoding);
}
}