import std/streams import bindings/quickjs import js/error import js/javascript import js/jstypes import chagashi/charset import chagashi/decoder import chagashi/decodercore import chagashi/validator import chagashi/validatorcore type JSTextEncoder = ref object JSTextDecoder = ref object encoding: Charset ignoreBOM {.jsget.}: bool fatal {.jsget.}: bool doNotFlush: bool bomSeen: bool td: TextDecoder tv: ref TextValidatorUTF8 validateBuf: seq[uint8] jsDestructor(JSTextDecoder) jsDestructor(JSTextEncoder) type TextDecoderOptions = object of JSDict fatal: bool ignoreBOM: bool func newJSTextDecoder(label = "utf-8", options = TextDecoderOptions()): JSResult[JSTextDecoder] {.jsctor.} = let encoding = getCharset(label) if encoding in {CHARSET_UNKNOWN, CHARSET_REPLACEMENT}: return err(newRangeError("Invalid encoding label")) return ok(JSTextDecoder( ignoreBOM: options.ignoreBOM, fatal: options.fatal, td: if encoding != CHARSET_UTF_8: newTextDecoder(encoding) else: nil, tv: if encoding == CHARSET_UTF_8: (ref TextValidatorUTF8)() else: nil, encoding: encoding )) type Growbuf = object p: ptr UncheckedArray[uint8] cap: int len: int {.warning[Deprecated]: off.}: proc `=destroy`(growbuf: var Growbuf) = if growbuf.p != nil: dealloc(growbuf.p) growbuf.p = nil const BufferSize = 128 proc grow(buf: var Growbuf) = if buf.cap == 0: buf.cap = BufferSize else: buf.cap *= 2 buf.p = cast[ptr UncheckedArray[uint8]](buf.p.realloc(buf.cap)) proc write(buf: var Growbuf, s: openArray[uint8]) = if buf.len + s.len > buf.cap: buf.grow() if s.len > 0: copyMem(addr buf.p[buf.len], unsafeAddr s[0], s.len) buf.len += s.len proc write(buf: var Growbuf, s: string) = if buf.len + s.len > buf.cap: buf.grow() if s.len > 0: copyMem(addr buf.p[buf.len], unsafeAddr s[0], s.len) buf.len += s.len proc decode0(this: JSTextDecoder, ctx: JSContext, input: JSArrayBufferView, stream: bool): JSResult[JSValue] = var oq = Growbuf( p: cast[ptr UncheckedArray[uint8]](alloc(BufferSize)), len: 0, cap: BufferSize ) var i = 0 let H = int(input.abuf.len) - 1 template handle_error = if this.fatal: return errTypeError("Failed to decode string") oq.write("\uFFFD") i = this.td.i while true: case this.td.decode(input.abuf.p.toOpenArray(i, H), oq.p.toOpenArray(0, oq.cap - 1), oq.len) of tdrDone: if not stream: case this.td.finish() of tdfrDone: discard of tdfrError: handle_error break of tdrError: handle_error of tdrReqOutput: oq.grow() return ok(JS_NewStringLen(ctx, cast[cstring](oq.p), csize_t(oq.len))) proc validate0(this: JSTextDecoder, ctx: JSContext, input: JSArrayBufferView, stream: bool): JSResult[JSValue] = # assume input is valid; do not allocate yet var oq = Growbuf(p: nil, len: 0, cap: 0) var i = 0 let H = int(input.abuf.len) - 1 var n = 0 template handle_error = if this.fatal: return errTypeError("Failed to decode string") # write from previous error (or beginning) to the last valid char oq.write(input.abuf.p.toOpenArray(i, n)) oq.write("\uFFFD") this.validateBuf.setLen(0) i = this.tv.i while true: case this.tv[].validate(input.abuf.p.toOpenArray(i, H), n) of tvrDone: break of tvrError: handle_error if not stream: case this.tv[].finish() of tvrDone: discard of tvrError: handle_error if this.validateBuf.len > 0 and n > -1: oq.write(this.validateBuf) oq.write(input.abuf.p.toOpenArray(i, n)) this.validateBuf.setLen(0) this.validateBuf.add(input.abuf.p.toOpenArray(n + 1, input.abuf.high)) if oq.len > 0: assert oq.p != nil return ok(JS_NewStringLen(ctx, cast[cstring](oq.p), csize_t(oq.len))) assert oq.p == nil return ok(JS_NewStringLen(ctx, cast[cstring](input.abuf.p), csize_t(n + 1))) type TextDecodeOptions = object of JSDict stream: bool #TODO AllowSharedBufferSource proc decode(ctx: JSContext, this: JSTextDecoder, input = opt(JSArrayBufferView), options = TextDecodeOptions()): JSResult[JSValue] {.jsfunc.} = if not this.doNotFlush: if this.td != nil: this.td = newTextDecoder(this.encoding) else: assert this.tv != nil this.tv = (ref TextValidatorUTF8)() this.bomSeen = false if this.doNotFlush != options.stream: this.doNotFlush = options.stream if input.isSome: if this.td != nil: return this.decode0(ctx, input.get, options.stream) else: assert this.encoding == CHARSET_UTF_8 # just validate return this.validate0(ctx, input.get, options.stream) return ok(JS_NewString(ctx, "")) func jencoding(this: JSTextDecoder): string {.jsfget: "encoding".} = return $this.encoding func newTextEncoder(): JSTextEncoder {.jsctor.} = return JSTextEncoder() func jencoding(this: JSTextEncoder): string {.jsfget: "encoding".} = return "utf-8" proc dealloc_wrap(rt: JSRuntime, opaque, p: pointer) {.cdecl.} = dealloc(p) proc encode(this: JSTextEncoder, input = ""): JSUint8Array {.jsfunc.} = # we have to validate input first :/ #TODO it is possible to do less copies here... var input = input.toValidUTF8() let buf = cast[ptr UncheckedArray[uint8]](alloc(input.len)) copyMem(buf, addr input[0], input.len) let abuf = JSArrayBuffer( p: buf, len: csize_t(input.len), dealloc: dealloc_wrap ) return JSUint8Array( abuf: abuf, offset: 0, nmemb: csize_t(input.len) ) #TODO encodeInto proc addEncodingModule*(ctx: JSContext) = ctx.registerType(JSTextDecoder, name = "TextDecoder") ctx.registerType(JSTextEncoder, name = "TextEncoder")