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