about summary refs log tree commit diff stats
path: root/src/loader
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-08-13 22:48:12 +0200
committerbptato <nincsnevem662@gmail.com>2024-08-13 23:03:41 +0200
commit885a3493b6cad4b4247a200928fe61e41883aaba (patch)
tree2b823ef18043c775f21b8ad723c826ffdc6b2663 /src/loader
parent968de41082280dde47bac7c2bb59522284b4c672 (diff)
downloadchawan-885a3493b6cad4b4247a200928fe61e41883aaba.tar.gz
xhr: progress
* fix header case sensitivity issues
	-> probably still wrong as it discards the original
	  casing. better than nothing, anyway
* fix fulfill on generic promises
* support standard open() async parameter weirdness
* refactor loader response body reading (so bodyRead is no longer
  mandatory)
* actually read response body

still missing: response body getters
Diffstat (limited to 'src/loader')
-rw-r--r--src/loader/headers.nim28
-rw-r--r--src/loader/loader.nim16
-rw-r--r--src/loader/response.nim90
3 files changed, 90 insertions, 44 deletions
diff --git a/src/loader/headers.nim b/src/loader/headers.nim
index 6b598cc2..e09d2267 100644
--- a/src/loader/headers.nim
+++ b/src/loader/headers.nim
@@ -5,6 +5,7 @@ import monoucha/fromjs
 import monoucha/javascript
 import monoucha/jserror
 import monoucha/quickjs
+import monoucha/tojs
 import types/opt
 import utils/twtstr
 
@@ -99,7 +100,8 @@ func isForbiddenRequestHeader*(name, value: string): bool =
   return false
 
 func isForbiddenResponseHeaderName*(name: string): bool =
-  return name in ["Set-Cookie", "Set-Cookie2"]
+  return name.equalsIgnoreCase("Set-Cookie") or
+    name.equalsIgnoreCase("Set-Cookie2")
 
 proc validate(this: Headers; name, value: string): JSResult[bool] =
   if not name.isValidHeaderName() or not value.isValidHeaderValue():
@@ -118,7 +120,6 @@ func isNoCorsSafelistedName(name: string): bool =
     name.equalsIgnoreCase("Content-Language") or
     name.equalsIgnoreCase("Content-Type")
 
-
 const CorsUnsafeRequestByte = {
   char(0x00)..char(0x08), char(0x10)..char(0x1F), '"', '(', ')', ':', '<', '>',
   '?', '@', '[', '\\', ']', '{', '}', '\e'
@@ -145,12 +146,14 @@ func isNoCorsSafelisted(name, value: string): bool =
 func get0(this: Headers; name: string): string =
   return this.table[name].join(", ")
 
-proc get(this: Headers; name: string): JSResult[Option[string]] {.jsfunc.} =
+proc get*(ctx: JSContext; this: Headers; name: string): JSValue {.jsfunc.} =
   if not name.isValidHeaderName():
-    return errTypeError("Invalid header name")
+    JS_ThrowTypeError(ctx, "Invalid header name")
+    return JS_EXCEPTION
+  let name = name.toHeaderCase()
   if name notin this.table:
-    return ok(none(string))
-  return ok(some(this.get0(name)))
+    return JS_NULL
+  return ctx.toJS(this.get0(name))
 
 proc removeRange(this: Headers) =
   if this.guard == hgRequestNoCors:
@@ -162,18 +165,21 @@ proc append(this: Headers; name, value: string): JSResult[void] {.jsfunc.} =
   let value = value.strip(chars = HTTPWhitespace)
   if not ?this.validate(name, value):
     return ok()
+  let name = name.toHeaderCase()
   if this.guard == hgRequestNoCors:
     if name in this.table:
       let tmp = this.get0(name) & ", " & value
       if not name.isNoCorsSafelisted(tmp):
         return ok()
-      this.table[name].add(value)
-    else:
-      this.table[name] = @[value]
+  if name in this.table:
+    this.table[name].add(value)
+  else:
+    this.table[name] = @[value]
   this.removeRange()
   ok()
 
 proc delete(this: Headers; name: string): JSResult[void] {.jsfunc.} =
+  let name = name.toHeaderCase()
   if not ?this.validate(name, "") or name notin this.table:
     return ok()
   if not name.isNoCorsSafelistedName() and not name.equalsIgnoreCase("Range"):
@@ -185,6 +191,7 @@ proc delete(this: Headers; name: string): JSResult[void] {.jsfunc.} =
 proc has(this: Headers; name: string): JSResult[bool] {.jsfunc.} =
   if not name.isValidHeaderName():
     return errTypeError("Invalid header name")
+  let name = name.toHeaderCase()
   return ok(name in this.table)
 
 proc set(this: Headers; name, value: string): JSResult[void] {.jsfunc.} =
@@ -193,8 +200,7 @@ proc set(this: Headers; name, value: string): JSResult[void] {.jsfunc.} =
     return
   if this.guard == hgRequestNoCors and not name.isNoCorsSafelisted(value):
     return
-  #TODO do this case insensitively
-  this.table[name] = @[value]
+  this.table[name.toHeaderCase()] = @[value]
   this.removeRange()
 
 proc fill(headers: Headers; s: seq[(string, string)]): JSResult[void] =
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 91212e24..dfc95b8b 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -1140,21 +1140,17 @@ proc onRead*(loader: FileLoader; fd: int) =
   if response != nil:
     response.onRead(response)
     if response.body.isend:
-      response.bodyRead.resolve()
-      response.bodyRead = nil
+      if response.onFinish != nil:
+        response.onFinish(response, true)
+      response.onFinish = nil
       response.unregisterFun()
 
 proc onError*(loader: FileLoader; fd: int) =
   let response = loader.ongoing.getOrDefault(fd)
   if response != nil:
-    when defined(debug):
-      var lbuf {.noinit.}: array[BufferSize, char]
-      if not response.body.isend:
-        let n = response.body.recvData(lbuf)
-        assert n == 0
-      assert response.body.isend
-    response.bodyRead.resolve()
-    response.bodyRead = nil
+    if response.onFinish != nil:
+      response.onFinish(response, false)
+    response.onFinish = nil
     response.unregisterFun()
 
 # Note: this blocks until headers are received.
diff --git a/src/loader/response.nim b/src/loader/response.nim
index a3b7e3e4..8143ccbf 100644
--- a/src/loader/response.nim
+++ b/src/loader/response.nim
@@ -11,6 +11,7 @@ import loader/request
 import monoucha/javascript
 import monoucha/jserror
 import monoucha/quickjs
+import monoucha/tojs
 import types/blob
 import types/color
 import types/opt
@@ -40,10 +41,10 @@ type
     url*: URL #TODO should be urllist?
     unregisterFun*: proc()
     resumeFun*: proc(outputId: int)
-    bodyRead*: EmptyPromise
     internalMessage*: string # should NOT be exposed to JS!
     outputId*: int
     onRead*: proc(response: Response) {.nimcall.}
+    onFinish*: proc(response: Response; success: bool) {.nimcall.}
     opaque*: RootRef
     flags*: set[ResponseFlag]
 
@@ -55,7 +56,6 @@ proc newResponse*(res: int; request: Request; stream: SocketStream;
     res: res,
     url: request.url,
     body: stream,
-    bodyRead: EmptyPromise(),
     outputId: outputId,
     status: status
   )
@@ -83,8 +83,10 @@ proc close*(response: Response) {.jsfunc.} =
   response.bodyUsed = true
   if response.unregisterFun != nil:
     response.unregisterFun()
+    response.unregisterFun = nil
   if response.body != nil:
     response.body.sclose()
+    response.body = nil
 
 func getCharset*(this: Response; fallback: Charset): Charset =
   if "Content-Type" notin this.headers.table:
@@ -103,8 +105,17 @@ func getContentType*(this: Response; fallback = "application/octet-stream"):
   # override buffer mime.types
   return DefaultGuess.guessContentType(this.url.pathname, fallback)
 
+func getContentLength*(this: Response): int64 =
+  this.headers.table.withValue("Content-Length", p):
+    for x in p[]:
+      let u = parseUInt64(x.strip(), allowSign = false)
+      if u.isSome and u.get <= uint64(int64.high):
+        return int64(u.get)
+  return -1
+
 type TextOpaque = ref object of RootObj
   buf: string
+  bodyRead: Promise[JSResult[string]]
 
 const BufferSize = 4096
 
@@ -122,6 +133,16 @@ proc onReadText(response: Response) =
       opaque.buf.setLen(olen)
       break
 
+proc onFinishText(response: Response; success: bool) =
+  let opaque = TextOpaque(response.opaque)
+  let bodyRead = opaque.bodyRead
+  if success:
+    let charset = response.getCharset(CHARSET_UTF_8)
+    bodyRead.resolve(JSResult[string].ok(opaque.buf.decodeAll(charset)))
+  else:
+    let err = newTypeError("NetworkError when attempting to fetch resource")
+    bodyRead.resolve(JSResult[string].err(err))
+
 proc resume*(response: Response) =
   response.resumeFun(response.outputId)
   response.resumeFun = nil
@@ -137,20 +158,20 @@ proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
       .err(newTypeError("Body has already been consumed"))
     p.resolve(err)
     return p
-  let opaque = TextOpaque()
+  let opaque = TextOpaque(bodyRead: newPromise[JSResult[string]]())
   response.opaque = opaque
   response.onRead = onReadText
+  response.onFinish = onFinishText
   response.bodyUsed = true
   response.resume()
-  return response.bodyRead.then(proc(): JSResult[string] =
-    let charset = response.getCharset(CHARSET_UTF_8)
-    ok(opaque.buf.decodeAll(charset))
-  )
+  return opaque.bodyRead
 
 type BlobOpaque = ref object of RootObj
   p: pointer
   len: int
   size: int
+  bodyRead: Promise[JSResult[Blob]]
+  contentType: string
 
 proc onReadBlob(response: Response) =
   let opaque = BlobOpaque(response.opaque)
@@ -168,29 +189,45 @@ proc onReadBlob(response: Response) =
     except ErrorAgain:
       break
 
+proc onFinishBlob(response: Response; success: bool) =
+  let opaque = BlobOpaque(response.opaque)
+  let bodyRead = opaque.bodyRead
+  if success:
+    let p = realloc(opaque.p, opaque.len)
+    opaque.p = nil
+    let blob = if p == nil:
+      newBlob(nil, 0, opaque.contentType, nil)
+    else:
+      newBlob(p, opaque.len, opaque.contentType, deallocBlob)
+    bodyRead.resolve(JSResult[Blob].ok(blob))
+  else:
+    if opaque.p != nil:
+      dealloc(opaque.p)
+      opaque.p = nil
+    let res = newTypeError("Error reading response")
+    bodyRead.resolve(JSResult[Blob].err(res))
+
 proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
   if response.bodyUsed:
     let p = newPromise[JSResult[Blob]]()
     let err = JSResult[Blob].err(newTypeError("Body has already been consumed"))
     p.resolve(err)
     return p
-  let opaque = BlobOpaque()
+  let opaque = BlobOpaque(
+    bodyRead: newPromise[JSResult[Blob]](),
+    contentType: response.getContentType()
+  )
   response.opaque = opaque
   response.onRead = onReadBlob
+  response.onFinish = onFinishBlob
   response.bodyUsed = true
   response.resume()
-  let contentType = response.getContentType()
-  return response.bodyRead.then(proc(): JSResult[Blob] =
-    let p = realloc(opaque.p, opaque.len)
-    opaque.p = nil
-    if p == nil:
-      return ok(newBlob(nil, 0, contentType, nil))
-    ok(newBlob(p, opaque.len, contentType, deallocBlob))
-  )
+  return opaque.bodyRead
 
 type BitmapOpaque = ref object of RootObj
   bmp: Bitmap
   idx: int
+  bodyRead: EmptyPromise
 
 proc onReadBitmap(response: Response) =
   let opaque = BitmapOpaque(response.opaque)
@@ -206,26 +243,33 @@ proc onReadBitmap(response: Response) =
     except ErrorAgain:
       break
 
+proc onFinishBitmap(response: Response; success: bool) =
+  let opaque = BitmapOpaque(response.opaque)
+  opaque.bodyRead.resolve()
+
 proc saveToBitmap*(response: Response; bmp: Bitmap): EmptyPromise =
   assert not response.bodyUsed
-  let opaque = BitmapOpaque(bmp: bmp, idx: 0)
+  let opaque = BitmapOpaque(bmp: bmp, idx: 0, bodyRead: EmptyPromise())
   let size = bmp.width * bmp.height
   bmp.px = cast[seq[RGBAColorBE]](newSeqUninitialized[uint32](size))
   response.opaque = opaque
   if size > 0:
     response.onRead = onReadBitmap
+    response.onFinish = onFinishBitmap
   else:
     response.unregisterFun()
     response.body.sclose()
+    opaque.bodyRead.resolve()
   response.bodyUsed = true
   response.resume()
-  return response.bodyRead
+  return opaque.bodyRead
 
-proc json(ctx: JSContext; this: Response): Promise[JSResult[JSValue]]
-    {.jsfunc.} =
-  return this.text().then(proc(s: JSResult[string]): JSResult[JSValue] =
-    let s = ?s
-    return ok(JS_ParseJSON(ctx, cstring(s), csize_t(s.len), cstring"<input>"))
+proc json(ctx: JSContext; this: Response): Promise[JSValue] {.jsfunc.} =
+  return this.text().then(proc(s: JSResult[string]): JSValue =
+    if s.isNone:
+      return ctx.toJS(s.error)
+    return JS_ParseJSON(ctx, cstring(s.get), csize_t(s.get.len),
+      cstring"<input>")
   )
 
 proc addResponseModule*(ctx: JSContext) =