about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/config/mailcap.nim59
-rw-r--r--src/html/dom.nim4
-rw-r--r--src/loader/loader.nim42
-rw-r--r--src/loader/response.nim38
-rw-r--r--src/local/client.nim5
-rw-r--r--src/local/container.nim19
-rw-r--r--src/local/pager.nim37
-rw-r--r--src/types/blob.nim2
-rw-r--r--src/utils/mimeguess.nim19
-rw-r--r--src/utils/twtstr.nim21
10 files changed, 112 insertions, 134 deletions
diff --git a/src/config/mailcap.nim b/src/config/mailcap.nim
index 90815249..b108ca20 100644
--- a/src/config/mailcap.nim
+++ b/src/config/mailcap.nim
@@ -8,8 +8,6 @@ import types/url
 import types/opt
 import utils/twtstr
 
-import chagashi/charset
-
 type
   MailcapParser = object
     stream: Stream
@@ -51,11 +49,11 @@ proc consume(state: var MailcapParser): char =
     inc state.line
   return c
 
-proc reconsume(state: var MailcapParser, c: char) =
+proc reconsume(state: var MailcapParser; c: char) =
   state.buf = c
   state.hasbuf = true
 
-proc skipBlanks(state: var MailcapParser, c: var char): bool =
+proc skipBlanks(state: var MailcapParser; c: var char): bool =
   while state.has():
     c = state.consume()
     if c notin AsciiWhitespace - {'\n'}:
@@ -127,7 +125,7 @@ proc consumeCommand(state: var MailcapParser): Result[string, string] =
 type NamedField = enum
   NO_NAMED_FIELD, NAMED_FIELD_TEST, NAMED_FIELD_NAMETEMPLATE, NAMED_FIELD_EDIT
 
-proc parseFieldKey(entry: var MailcapEntry, k: string): NamedField =
+proc parseFieldKey(entry: var MailcapEntry; k: string): NamedField =
   case k
   of "needsterminal":
     entry.flags.incl(NEEDSTERMINAL)
@@ -145,7 +143,7 @@ proc parseFieldKey(entry: var MailcapEntry, k: string): NamedField =
     return NAMED_FIELD_EDIT
   return NO_NAMED_FIELD
 
-proc consumeField(state: var MailcapParser, entry: var MailcapEntry):
+proc consumeField(state: var MailcapParser; entry: var MailcapEntry):
     Result[bool, string] =
   state.skipBlanks()
   if not state.has():
@@ -218,7 +216,7 @@ type UnquoteResult* = object
 type QuoteState = enum
   QS_NORMAL, QS_DQUOTED, QS_SQUOTED
 
-proc quoteFile(file: string, qs: QuoteState): string =
+proc quoteFile(file: string; qs: QuoteState): string =
   var s = ""
   for c in file:
     case c
@@ -238,8 +236,8 @@ proc quoteFile(file: string, qs: QuoteState): string =
     s &= c
   return s
 
-proc unquoteCommand*(ecmd, contentType, outpath: string, url: URL,
-    charset: Charset, canpipe: var bool): string =
+proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL;
+    canpipe: var bool): string =
   var cmd = ""
   var attrname = ""
   var state: UnquoteState
@@ -307,24 +305,7 @@ proc unquoteCommand*(ecmd, contentType, outpath: string, url: URL,
       state = STATE_NORMAL
     of STATE_ATTR:
       if c == '}':
-        if attrname == "charset":
-          cmd &= quoteFile($charset, qs)
-          continue
-        #TODO this is broken, because content-type is stripped of ; fields
-        let kvs = contentType.after(';').toLowerAscii()
-        var i = kvs.find(attrname)
-        var s = ""
-        if i != -1 and kvs.len > i + attrname.len and
-            kvs[i + attrname.len] == '=':
-          i = skipBlanks(kvs, i + attrname.len + 1)
-          var q = false
-          for j in i ..< kvs.len:
-            if not q and kvs[j] == '\\':
-              q = true
-            elif not q and (kvs[j] == ';' or kvs[j] in AsciiWhitespace):
-              break
-            else:
-              s &= kvs[j]
+        let s = contentType.getContentTypeAttr(attrname)
         cmd &= quoteFile(s, qs)
         attrname = ""
       elif c == '\\':
@@ -333,28 +314,24 @@ proc unquoteCommand*(ecmd, contentType, outpath: string, url: URL,
         attrname &= c
   return cmd
 
-proc unquoteCommand*(ecmd, contentType, outpath: string, url: URL,
-    charset: Charset): string =
+proc unquoteCommand*(ecmd, contentType, outpath: string; url: URL): string =
   var canpipe: bool
-  return unquoteCommand(ecmd, contentType, outpath, url, charset, canpipe)
+  return unquoteCommand(ecmd, contentType, outpath, url, canpipe)
 
-proc getMailcapEntry*(mailcap: Mailcap; mimeType, outpath: string;
-    url: URL; charset: Charset): ptr MailcapEntry =
-  let mt = mimeType.until('/')
-  if mt.len + 1 >= mimeType.len:
+proc getMailcapEntry*(mailcap: Mailcap; contentType, outpath: string; url: URL):
+    ptr MailcapEntry =
+  let mt = contentType.until('/')
+  if mt.len + 1 >= contentType.len:
     return nil
-  let st = mimeType[mt.len + 1 .. ^1]
+  let st = contentType.until(AsciiWhitespace + {';'}, mt.len + 1)
   for entry in mailcap:
-    if not (entry.mt.len == 1 and entry.mt[0] == '*') and
-        entry.mt != mt:
+    if not (entry.mt.len == 1 and entry.mt[0] == '*') and entry.mt != mt:
       continue
-    if not (entry.subt.len == 1 and entry.subt[0] == '*') and
-        entry.subt != st:
+    if not (entry.subt.len == 1 and entry.subt[0] == '*') and entry.subt != st:
       continue
     if entry.test != "":
       var canpipe = true
-      let cmd = unquoteCommand(entry.test, mimeType, outpath, url, charset,
-        canpipe)
+      let cmd = unquoteCommand(entry.test, contentType, outpath, url, canpipe)
       if not canpipe:
         continue
       if execCmd(cmd) != 0:
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 23dd49f2..81504d56 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -2776,7 +2776,7 @@ proc loadResource(window: Window, link: HTMLLinkElement) =
         let res = res.get
         #TODO we should use ReadableStreams for this (which would allow us to
         # parse CSS asynchronously)
-        if res.contentType == "text/css":
+        if res.getContentType() == "text/css":
           return res.text()
         res.unregisterFun()
     ).then(proc(s: JSResult[string]) =
@@ -2803,7 +2803,7 @@ proc loadResource(window: Window, image: HTMLImageElement) =
         if res.isErr:
           return
         let res = res.get
-        if res.contentType == "image/png":
+        if res.getContentType() == "image/png":
           return res.blob()
       ).then(proc(pngData: JSResult[Blob]) =
         if pngData.isErr:
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 70f84043..904069a6 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -43,11 +43,8 @@ import types/cookie
 import types/referrer
 import types/urimethodmap
 import types/url
-import utils/mimeguess
 import utils/twtstr
 
-import chagashi/charset
-
 export request
 export response
 
@@ -760,38 +757,7 @@ proc runFileLoader*(fd: cint; config: LoaderConfig) =
     ctx.finishCycle(unregRead, unregWrite)
   ctx.exitLoader()
 
-proc getAttribute(contentType, attrname: string): string =
-  let kvs = contentType.after(';')
-  var i = kvs.find(attrname)
-  var s = ""
-  if i != -1 and kvs.len > i + attrname.len and
-      kvs[i + attrname.len] == '=':
-    i += attrname.len + 1
-    while i < kvs.len and kvs[i] in AsciiWhitespace:
-      inc i
-    var q = false
-    for j, c in kvs.toOpenArray(i, kvs.high):
-      if q:
-        s &= c
-      elif c == '\\':
-        q = true
-      elif c == ';' or c in AsciiWhitespace:
-        break
-      else:
-        s &= c
-  return s
-
-proc applyHeaders(response: Response; request: Request) =
-  if "Content-Type" in response.headers.table:
-    #TODO this is inefficient and broken on several levels. (In particular,
-    # it breaks mailcap named attributes other than charset.)
-    # Ideally, contentType would be a separate object type.
-    let header = response.headers.table["Content-Type"][0].toLowerAscii()
-    response.contentType = header.until(';').strip().toLowerAscii()
-    response.charset = getCharset(header.getAttribute("charset"))
-  else:
-    response.contentType = guessContentType($response.url.path,
-      "application/octet-stream", DefaultGuess)
+proc getRedirect*(response: Response; request: Request): Request =
   if "Location" in response.headers.table:
     if response.status in 301u16..303u16 or response.status in 307u16..308u16:
       let location = response.headers.table["Location"][0]
@@ -801,14 +767,15 @@ proc applyHeaders(response: Response; request: Request) =
             request.httpMethod notin {HTTP_GET, HTTP_HEAD}) or
             (response.status == 301 or response.status == 302 and
             request.httpMethod == HTTP_POST):
-          response.redirect = newRequest(url.get, HTTP_GET,
+          return newRequest(url.get, HTTP_GET,
             mode = request.mode, credentialsMode = request.credentialsMode,
             destination = request.destination)
         else:
-          response.redirect = newRequest(url.get, request.httpMethod,
+          return newRequest(url.get, request.httpMethod,
             body = request.body, multipart = request.multipart,
             mode = request.mode, credentialsMode = request.credentialsMode,
             destination = request.destination)
+  return nil
 
 proc connect(loader: FileLoader; buffered = true): SocketStream =
   let stream = connectSocketStream(loader.process, buffered, blocking = true)
@@ -899,7 +866,6 @@ proc handleHeaders(response: Response; request: Request; stream: SocketStream) =
   stream.sread(response.outputId)
   stream.sread(response.status)
   stream.sread(response.headers)
-  response.applyHeaders(request)
   # Only a stream of the response body may arrive after this point.
   response.body = stream
 
diff --git a/src/loader/response.nim b/src/loader/response.nim
index 6add4928..f419c432 100644
--- a/src/loader/response.nim
+++ b/src/loader/response.nim
@@ -1,4 +1,6 @@
 import std/streams
+import std/strutils
+import std/tables
 
 import bindings/quickjs
 import io/promise
@@ -10,6 +12,8 @@ import loader/headers
 import loader/request
 import types/blob
 import types/url
+import utils/mimeguess
+import utils/twtstr
 
 import chagashi/charset
 import chagashi/decoder
@@ -37,15 +41,12 @@ type
     res*: int
     body*: SocketStream
     bodyUsed* {.jsget.}: bool
-    contentType*: string
     status* {.jsget.}: uint16
     headers* {.jsget.}: Headers
     headersGuard: HeadersGuard
-    redirect*: Request
     url*: URL #TODO should be urllist?
     unregisterFun*: proc()
     bodyRead*: Promise[string]
-    charset*: Charset
     internalMessage*: string # should NOT be exposed to JS!
     outputId*: int
 
@@ -87,6 +88,23 @@ proc close*(response: Response) {.jsfunc.} =
   if response.body != nil:
     response.body.close()
 
+func getCharset*(this: Response; fallback: Charset): Charset =
+  if "Content-Type" notin this.headers.table:
+    return fallback
+  let header = this.headers.table["Content-Type"][0].toLowerAscii()
+  let cs = header.getContentTypeAttr("charset").getCharset()
+  if cs == CHARSET_UNKNOWN:
+    return fallback
+  return cs
+
+func getContentType*(this: Response): string =
+  if "Content-Type" in this.headers.table:
+    let header = this.headers.table["Content-Type"][0].toLowerAscii()
+    return header.until(';').strip()
+  # also use DefaultGuess for container, so that local mime.types cannot
+  # override buffer mime.types
+  return DefaultGuess.guessContentType(this.url.pathname)
+
 proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
   if response.body == nil:
     let p = newPromise[JSResult[string]]()
@@ -101,16 +119,13 @@ proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
   let bodyRead = response.bodyRead
   response.bodyRead = nil
   return bodyRead.then(proc(s: string): JSResult[string] =
-    let cs = if response.charset == CHARSET_UNKNOWN:
-      CHARSET_UTF_8
-    else:
-      response.charset
+    let charset = response.getCharset(CHARSET_UTF_8)
     #TODO this is inefficient
     # maybe add a JS type that turns a seq[char] into JS strings
-    if cs in {CHARSET_UTF_8, CHARSET_UNKNOWN}:
+    if charset == CHARSET_UTF_8:
       ok(s.toValidUTF8())
     else:
-      ok(newTextDecoder(cs).decodeAll(s))
+      ok(newTextDecoder(charset).decodeAll(s))
   )
 
 proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
@@ -122,13 +137,14 @@ proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
     return p
   let bodyRead = response.bodyRead
   response.bodyRead = nil
+  let contentType = response.getContentType()
   return bodyRead.then(proc(s: string): JSResult[Blob] =
     if s.len == 0:
-      return ok(newBlob(nil, 0, response.contentType, nil))
+      return ok(newBlob(nil, 0, contentType, nil))
     GC_ref(s)
     let deallocFun = proc() =
       GC_unref(s)
-    let blob = newBlob(unsafeAddr s[0], s.len, response.contentType, deallocFun)
+    let blob = newBlob(unsafeAddr s[0], s.len, contentType, deallocFun)
     ok(blob))
 
 proc json(ctx: JSContext, this: Response): Promise[JSResult[JSValue]]
diff --git a/src/local/client.nim b/src/local/client.nim
index 0d1bf063..6b605311 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -518,8 +518,9 @@ proc handleRead(client: Client, fd: int) =
     let response = stream.readResponse(container.request)
     if response.body == nil:
       client.pager.fail(container, response.getErrorMessage())
-    elif response.redirect != nil:
-      client.pager.redirect(container, response)
+    elif (let redirect = response.getRedirect(container.request);
+        redirect != nil):
+      client.pager.redirect(container, response, redirect)
       response.body.close()
     else:
       client.pager.connected(container, response)
diff --git a/src/local/container.nim b/src/local/container.nim
index 49d8885e..fc3384d9 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -1402,15 +1402,11 @@ proc applyResponse*(container: Container; response: Response) =
   container.setLoadInfo("Connected to " & $response.url & ". Downloading...")
   # setup content type; note that isSome means an override so we skip it
   if container.contentType.isNone:
-    if response.contentType == "application/octet-stream":
-      let contentType = guessContentType(container.url.pathname,
-        "application/octet-stream", container.config.mimeTypes)
-      if contentType != "application/octet-stream":
-        container.contentType = some(contentType)
-      else:
-        container.contentType = some(response.contentType)
-    else:
-      container.contentType = some(response.contentType)
+    var contentType = response.getContentType()
+    if contentType == "application/octet-stream":
+      contentType = container.config.mimeTypes
+        .guessContentType(container.url.pathname)
+    container.contentType = some(contentType)
   # setup charsets:
   # * override charset
   # * network charset
@@ -1418,8 +1414,9 @@ proc applyResponse*(container: Container; response: Response) =
   # HTML may override the last two (but not the override charset).
   if container.config.charsetOverride != CHARSET_UNKNOWN:
     container.charsetStack = @[container.config.charsetOverride]
-  elif response.charset != CHARSET_UNKNOWN:
-    container.charsetStack = @[response.charset]
+  elif (let charset = response.getCharset(CHARSET_UNKNOWN);
+      charset != CHARSET_UNKNOWN):
+    container.charsetStack = @[charset]
   else:
     container.charsetStack = @[]
     for i in countdown(container.config.charsets.high, 0):
diff --git a/src/local/pager.nim b/src/local/pager.nim
index cba5a5d3..a0f63aca 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -1087,11 +1087,10 @@ type CheckMailcapResult = object
   ishtml: bool
 
 # Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler.
-proc ansiDecode(pager: Pager; url: URL; charset: Charset; ishtml: var bool;
-    fdin: cint): cint =
-  let entry = pager.mailcap.getMailcapEntry("text/x-ansi", "", url, charset)
+proc ansiDecode(pager: Pager; url: URL; ishtml: var bool; fdin: cint): cint =
+  let entry = pager.mailcap.getMailcapEntry("text/x-ansi", "", url)
   var canpipe = true
-  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, charset, canpipe)
+  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, canpipe)
   if not canpipe:
     pager.alert("Error: could not pipe to text/x-ansi, decoding as text/plain")
     return -1
@@ -1283,35 +1282,34 @@ proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string;
 # pager is suspended until the command exits.
 #TODO add support for edit/compose, better error handling
 proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
-    istreamOutputId: int): CheckMailcapResult =
+    istreamOutputId: int; contentType: string): CheckMailcapResult =
   if container.filter != nil:
     return pager.filterBuffer(stream, container.filter.cmd, container.ishtml)
   # contentType must exist, because we set it in applyResponse
-  let contentType = container.contentType.get
-  if contentType == "text/html":
+  let shortContentType = container.contentType.get
+  if shortContentType == "text/html":
     # We support text/html natively, so it would make little sense to execute
     # mailcap filters for it.
     return CheckMailcapResult(connect: true, fdout: stream.fd, ishtml: true)
-  if contentType == "text/plain":
+  if shortContentType == "text/plain":
     # text/plain could potentially be useful. Unfortunately, many mailcaps
     # include a text/plain entry with less by default, so it's probably better
     # to ignore this.
     return CheckMailcapResult(connect: true, fdout: stream.fd)
   #TODO callback for outpath or something
   let url = container.url
-  let cs = container.charset
-  let entry = pager.mailcap.getMailcapEntry(contentType, "", url, cs)
+  let entry = pager.mailcap.getMailcapEntry(contentType, "", url)
   if entry == nil:
     return CheckMailcapResult(connect: true, fdout: stream.fd)
   let tmpdir = pager.tmpdir
   let ext = url.pathname.afterLast('.')
   let tempfile = getTempFile(tmpdir, ext)
   let outpath = if entry.nametemplate != "":
-    unquoteCommand(entry.nametemplate, contentType, tempfile, url, cs)
+    unquoteCommand(entry.nametemplate, contentType, tempfile, url)
   else:
     tempfile
   var canpipe = true
-  let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, cs, canpipe)
+  let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, canpipe)
   var ishtml = HTMLOUTPUT in entry.flags
   let needsterminal = NEEDSTERMINAL in entry.flags
   putEnv("MAILCAP_URL", $url)
@@ -1336,7 +1334,7 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
       pager.runMailcapReadFile(stream, cmd, outpath, pipefdOut)
     discard close(pipefdOut[1]) # close write
     let fdout = if not ishtml and ANSIOUTPUT in entry.flags:
-      pager.ansiDecode(url, cs, ishtml, pipefdOut[0])
+      pager.ansiDecode(url, ishtml, pipefdOut[0])
     else:
       pipefdOut[0]
     delEnv("MAILCAP_URL")
@@ -1368,10 +1366,10 @@ proc fail*(pager: Pager; container: Container; errorMessage: string) =
   else:
     pager.alert("Can't load " & $container.url & " (" & errorMessage & ")")
 
-proc redirect*(pager: Pager; container: Container; response: Response) =
+proc redirect*(pager: Pager; container: Container; response: Response;
+    request: Request) =
   # still need to apply response, or we lose cookie jars.
   container.applyResponse(response)
-  let request = response.redirect
   if container.redirectdepth < pager.config.network.max_redirect:
     if container.url.scheme == request.url.scheme or
         container.url.scheme == "cgi-bin" or
@@ -1395,10 +1393,15 @@ proc connected*(pager: Pager; container: Container; response: Response) =
     pager.authorize()
     istream.close()
     return
-  let mailcapRes = pager.checkMailcap(container, istream, response.outputId)
+  let realContentType = if "Content-Type" in response.headers:
+    response.headers["Content-Type"]
+  else:
+    # both contentType and charset must be set by applyResponse.
+    container.contentType.get & ";charset=" & $container.charset
+  let mailcapRes = pager.checkMailcap(container, istream, response.outputId,
+    realContentType)
   if mailcapRes.connect:
     container.ishtml = mailcapRes.ishtml
-    container.applyResponse(response)
     # buffer now actually exists; create a process for it
     container.process = pager.forkserver.forkBuffer(
       container.config,
diff --git a/src/types/blob.nim b/src/types/blob.nim
index 63fb5b84..5b1efa65 100644
--- a/src/types/blob.nim
+++ b/src/types/blob.nim
@@ -53,7 +53,7 @@ proc newWebFile*(path: string, webkitRelativePath = ""): WebFile =
     isfile: true,
     path: path,
     file: file,
-    ctype: guessContentType(path),
+    ctype: DefaultGuess.guessContentType(path),
     webkitRelativePath: webkitRelativePath
   )
 
diff --git a/src/utils/mimeguess.nim b/src/utils/mimeguess.nim
index 0a65c909..4b8df086 100644
--- a/src/utils/mimeguess.nim
+++ b/src/utils/mimeguess.nim
@@ -8,22 +8,19 @@ const DefaultGuess* = block:
   let ss = newStringStream(staticRead"res/mime.types")
   parseMimeTypes(ss)
 
-proc guessContentType*(path: string, fallback = "text/plain",
-    guess = DefaultGuess): string =
-  var i = path.len - 1
+func guessContentType*(mimeTypes: MimeTypes; path: string): string =
   var n = 0
-  while i > 0:
+  for i in countdown(path.high, 0):
     if path[i] == '/':
-      return fallback
+      break
     if path[i] == '.':
       n = i
       break
-    dec i
   if n > 0:
     let ext = path.substr(n + 1)
-    if ext in guess:
-      return guess[ext]
-  return fallback
+    if ext in mimeTypes:
+      return mimeTypes[ext]
+  return "application/octet-stream"
 
 const JavaScriptTypes = [
   "application/ecmascript",
@@ -44,5 +41,5 @@ const JavaScriptTypes = [
   "text/x-javascript"
 ]
 
-proc isJavaScriptType*(s: string): bool =
-  return binarySearch(JavaScriptTypes, s) != -1
+func isJavaScriptType*(s: string): bool =
+  return JavaScriptTypes.binarySearch(s) != -1
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index 4d6b48ea..fe6ba236 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -693,3 +693,24 @@ func strictParseEnum*[T: enum](s: string): Opt[T] =
     if s in tab:
       return ok(tab[s])
   return err()
+
+proc getContentTypeAttr*(contentType, attrname: string): string =
+  let kvs = contentType.after(';')
+  var i = kvs.find(attrname)
+  var s = ""
+  if i != -1 and kvs.len > i + attrname.len and
+      kvs[i + attrname.len] == '=':
+    i += attrname.len + 1
+    while i < kvs.len and kvs[i] in AsciiWhitespace:
+      inc i
+    var q = false
+    for j, c in kvs.toOpenArray(i, kvs.high):
+      if q:
+        s &= c
+      elif c == '\\':
+        q = true
+      elif c == ';' or c in AsciiWhitespace:
+        break
+      else:
+        s &= c
+  return s