text: /*!
   * send
   * Copyright(c) 2012 TJ Holowaychuk
   * Copyright(c) 2014-2016 Douglas Christopher Wilson
   * MIT Licensed
   */
  
  'use strict'
  
  /**
   * Module dependencies.
   * @private
   */
  
  var createError = require('http-errors')
  var debug = require('debug')('send')
  var deprecate = require('depd')('send')
  var destroy = require('destroy')
  var encodeUrl = require('encodeurl')
  var escapeHtml = require('escape-html')
  var etag = require('etag')
  var fresh = require('fresh')
  var fs = require('fs')
  var mime = require('mime')
  var ms = require('ms')
  var onFinished = require('on-finished')
  var parseRange = require('range-parser')
  var path = require('path')
  var statuses = require('statuses')
  var Stream = require('stream')
  var util = require('util')
  
  /**
   * Path function references.
   * @private
   */
  
  var extname = path.extname
  var join = path.join
  var normalize = path.normalize
  var resolve = path.resolve
  var sep = path.sep
  
  /**
   * Regular expression for identifying a bytes Range header.
   * @private
   */
  
  var BYTES_RANGE_REGEXP = /^ *bytes=/
  
  /**
   * Maximum value allowed for the max age.
   * @private
   */
  
  var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000 // 1 year
  
  /**
   * Regular expression to match a path with a directory up component.
   * @private
   */
  
  var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/
  
  /**
   * Module exports.
   * @public
   */
  
  module.exports = send
  module.exports.mime = mime
  
  /**
   * Return a `SendStream` for `req` and `path`.
   *
   * @param {object} req
   * @param {string} path
   * @param {object} [options]
   * @return {SendStream}
   * @public
   */
  
  function send (req, path, options) {
    return new SendStream(req, path, options)
  }
  
  /**
   * Initialize a `SendStream` with the given `path`.
   *
   * @param {Request} req
   * @param {String} path
   * @param {object} [options]
   * @private
   */
  
  function SendStream (req, path, options) {
    Stream.call(this)
  
    var opts = options || {}
  
    this.options = opts
    this.path = path
    this.req = req
  
    this._acceptRanges = opts.acceptRanges !== undefined
      ? Boolean(opts.acceptRanges)
      : true
  
    this._cacheControl = opts.cacheControl !== undefined
      ? Boolean(opts.cacheControl)
      : true
  
    this._etag = opts.etag !== undefined
      ? Boolean(opts.etag)
      : true
  
    this._dotfiles = opts.dotfiles !== undefined
      ? opts.dotfiles
      : 'ignore'
  
    if (this._dotfiles !== 'ignore' && this._dotfiles !== 'allow' && this._dotfiles !== 'deny') {
      throw new TypeError('dotfiles option must be "allow", "deny", or "ignore"')
    }
  
    this._hidden = Boolean(opts.hidden)
  
    if (opts.hidden !== undefined) {
      deprecate('hidden: use dotfiles: \'' + (this._hidden ? 'allow' : 'ignore') + '\' instead')
    }
  
    // legacy support
    if (opts.dotfiles === undefined) {
      this._dotfiles = undefined
    }
  
    this._extensions = opts.extensions !== undefined
      ? normalizeList(opts.extensions, 'extensions option')
      : []
  
    this._immutable = opts.immutable !== undefined
      ? Boolean(opts.immutable)
      : false
  
    this._index = opts.index !== undefined
      ? normalizeList(opts.index, 'index option')
      : ['index.html']
  
    this._lastModified = opts.lastModified !== undefined
      ? Boolean(opts.lastModified)
      : true
  
    this._maxage = opts.maxAge || opts.maxage
    this._maxage = typeof this._maxage === 'string'
      ? ms(this._maxage)
      : Number(this._maxage)
    this._maxage = !isNaN(this._maxage)
      ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
      : 0
  
    this._root = opts.root
      ? resolve(opts.root)
      : null
  
    if (!this._root && opts.from) {
      this.from(opts.from)
    }
  }
  
  /**
   * Inherits from `Stream`.
   */
  
  util.inherits(SendStream, Stream)
  
  /**
   * Enable or disable etag generation.
   *
   * @param {Boolean} val
   * @return {SendStream}
   * @api public
   */
  
  SendStream.prototype.etag = deprecate.function(function etag (val) {
    this._etag = Boolean(val)
    debug('etag %s', this._etag)
    return this
  }, 'send.etag: pass etag as option')
  
  /**
   * Enable or disable "hidden" (dot) files.
   *
   * @param {Boolean} path
   * @return {SendStream}
   * @api public
   */
  
  SendStream.prototype.hidden = deprecate.function(function hidden (val) {
    this._hidden = Boolean(val)
    this._dotfiles = undefined
    debug('hidden %s', this._hidden)
    return this
  }, 'send.hidden: use dotfiles option')
  
  /**
   * Set index `paths`, set to a falsy
   * value to disable index support.
   *
   * @param {String|Boolean|Array} paths
   * @return {SendStream}
   * @api public
   */
  
  SendStream.prototype.index = deprecate.function(function index (paths) {
    var index = !paths ? [] : normalizeList(paths, 'paths argument')
    debug('index %o', paths)
    this._index = index
    return this
  }, 'send.index: pass index as option')
  
  /**
   * Set root `path`.
   *
   * @param {String} path
   * @return {SendStream}
   * @api public
   */
  
  SendStream.prototype.root = function root (path) {
    this._root = resolve(String(path))
    debug('root %s', this._root)
    return this
  }
  
  SendStream.prototype.from = deprecate.function(SendStream.prototype.root,
    'send.from: pass root as option')
  
  SendStream.prototype.root = deprecate.function(SendStream.prototype.root,
    'send.root: pass root as option')
  
  /**
   * Set max-age to `maxAge`.
   *
   * @param {Number} maxAge
   * @return {SendStream}
   * @api public
   */
  
  SendStream.prototype.maxage = deprecate.function(function maxage (maxAge) {
    this._maxage = typeof maxAge === 'string'
      ? ms(maxAge)
      : Number(maxAge)
    this._maxage = !isNaN(this._maxage)
      ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE)
      : 0
    debug('max-age %d', this._maxage)
    return this
  }, 'send.maxage: pass maxAge as option')
  
  /**
   * Emit error with `status`.
   *
   * @param {number} status
   * @param {Error} [err]
   * @private
   */
  
  SendStream.prototype.error = function error (status, err) {
    // emit if listeners instead of responding
    if (hasListeners(this, 'error')) {
      return this.emit('error', createError(status, err, {
        expose: false
      }))
    }
  
    var res = this.res
    var msg = statuses[status] || String(status)
    var doc = createHtmlDocument('Error', escapeHtml(msg))
  
    // clear existing headers
    clearHeaders(res)
  
    // add error headers
    if (err && err.headers) {
      setHeaders(res, err.headers)
    }
  
    // send basic response
    res.statusCode = status
    res.setHeader('Content-Type', 'text/html; charset=UTF-8')
    res.setHeader('Content-Length', Buffer.byteLength(doc))
    res.setHeader('Content-Security-Policy', "default-src 'none'")
    res.setHeader('X-Content-Type-Options', 'nosniff')
    res.end(doc)
  }
  
  /**
   * Check if the pathname ends with "/".
   *
   * @return {boolean}
   * @private
   */
  
  SendStream.prototype.hasTrailingSlash = function hasTrailingSlash () {
    return this.path[this.path.length - 1] === '/'
  }
  
  /**
   * Check if this is a conditional GET request.
   *
   * @return {Boolean}
   * @api private
   */
  
  SendStream.prototype.isConditionalGET = function isConditionalGET () {
    return this.req.headers['if-match'] ||
      this.req.headers['if-unmodified-since'] ||
      this.req.headers['if-none-match'] ||
      this.req.headers['if-modified-since']
  }
  
  /**
   * Check if the request preconditions failed.
   *
   * @return {boolean}
   * @private
   */
  
  SendStream.prototype.isPreconditionFailure = function isPreconditionFailure () {
    var req = this.req
    var res = this.res
  
    // if-match
    var match = req.headers['if-match']
    if (match) {
      var etag = res.getHeader('ETag')
      return !etag || (match !== '*' && parseTokenList(match).every(function (match) {
        return match !== etag && match !== 'W/' + etag && 'W/' + match !== etag
      }))
    }
  
    // if-unmodified-since
    var unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since'])
    if (!isNaN(unmodifiedSince)) {
      var lastModified = parseHttpDate(res.getHeader('Last-Modified'))
      return isNaN(lastModified) || lastModified > unmodifiedSince
    }
  
    return false
  }
  
  /**
   * Strip content-* header fields.
   *
   * @private
   */
  
  SendStream.prototype.removeContentHeaderFields = function removeContentHeaderFields () {
    var res = this.res
    var headers = getHeaderNames(res)
  
    for (var i = 0; i < headers.length; i++) {
      var header = headers[i]
      if (header.substr(0, 8) === 'content-' && header !== 'content-location') {
        res.removeHeader(header)
      }
    }
  }
  
  /**
   * Respond with 304 not modified.
   *
   * @api private
   */
  
  SendStream.prototype.notModified = function notModified () {
    var res = this.res
    debug('not modified')
    this.removeContentHeaderFields()
    res.statusCode = 304
    res.end()
  }
  
  /**
   * Raise error that headers already sent.
   *
   * @api private
   */
  
  SendStream.prototype.headersAlreadySent = function headersAlreadySent () {
    var err = new Error('Can\'t set headers after they are sent.')
    debug('headers already sent')
    this.error(500, err)
  }
  
  /**
   * Check if the request is cacheable, aka
   * responded with 2xx or 304 (see RFC 2616 section 14.2{5,6}).
   *
   * @return {Boolean}
   * @api private
   */
  
  SendStream.prototype.isCachable = function isCachable () {
    var statusCode = this.res.statusCode
    return (statusCode >= 200 && statusCode < 300) ||
      statusCode === 304
  }
  
  /**
   * Handle stat() error.
   *
   * @param {Error} error
   * @private
   */
  
  SendStream.prototype.onStatError = function onStatError (error) {
    switch (error.code) {
      case 'ENAMETOOLONG':
      case 'ENOENT':
      case 'ENOTDIR':
        this.error(404, error)
        break
      default:
        this.error(500, error)
        break
    }
  }
  
  /**
   * Check if the cache is fresh.
   *
   * @return {Boolean}
   * @api private
   */
  
  SendStream.prototype.isFresh = function isFresh () {
    return fresh(this.req.headers, {
      'etag': this.res.getHeader('ETag'),
      'last-modified': this.res.getHeader('Last-Modified')
    })
  }
  
  /**
   * Check if the range is fresh.
   *
   * @return {Boolean}
   * @api private
   */
  
  SendStream.prototype.isRangeFresh = function isRangeFresh () {
    var ifRange = this.req.headers['if-range']
  
    if (!ifRange) {
      return true
    }
  
    // if-range as etag
    if (ifRange.indexOf('"') !== -1) {
      var etag = this.res.getHeader('ETag')
      return Boolean(etag && ifRange.indexOf(etag) !== -1)
    }
  
    // if-range as modified date
    var lastModified = this.res.getHeader('Last-Modified')
    return parseHttpDate(lastModified) <= parseHttpDate(ifRange)
  }
  
  /**
   * Redirect to path.
   *
   * @param {string} path
   * @private
   */
  
  SendStream.prototype.redirect = function redirect (path) {
    var res = this.res
  
    if (hasListeners(this, 'directory')) {
      this.emit('directory', res, path)
      return
    }
  
    if (this.hasTrailingSlash()) {
      this.error(403)
      return
    }
  
    var loc = encodeUrl(collapseLeadingSlashes(this.path + '/'))
    var doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
      escapeHtml(loc) + '</a>')
  
    // redirect
    res.statusCode = 301
    res.setHeader('Content-Type', 'text/html; charset=UTF-8')
    res.setHeader('Content-Length', Buffer.byteLength(doc))
    res.setHeader('Content-Security-Policy', "default-src 'none'")
    res.setHeader('X-Content-Type-Options', 'nosniff')
    res.setHeader('Location', loc)
    res.end(doc)
  }
  
  /**
   * Pipe to `res.
   *
   * @param {Stream} res
   * @return {Stream} res
   * @api public
   */
  
  SendStream.prototype.pipe = function pipe (res) {
    // root path
    var root = this._root
  
    // references
    this.res = res
  
    // decode the path
    var path = decode(this.path)
    if (path === -1) {
      this.error(400)
      return res
    }
  
    // null byte(s)
    if (~path.indexOf('\0')) {
      this.error(400)
      return res
    }
  
    var parts
    if (root !== null) {
      // normalize
      if (path) {
        path = normalize('.' + sep + path)
      }
  
      // malicious path
      if (UP_PATH_REGEXP.test(path)) {
        debug('malicious path "%s"', path)
        this.error(403)
        return res
      }
  
      // explode path parts
      parts = path.split(sep)
  
      // join / normalize from optional root dir
      path = normalize(join(root, path))
    } else {
      // ".." is malicious without "root"
      if (UP_PATH_REGEXP.test(path)) {
        debug('malicious path "%s"', path)
        this.error(403)
        return res
      }
  
      // explode path parts
      parts = normalize(path).split(sep)
  
      // resolve the path
      path = resolve(path)
    }
  
    // dotfile handling
    if (containsDotFile(parts)) {
      var access = this._dotfiles
  
      // legacy support
      if (access === undefined) {
        access = parts[parts.length - 1][0] === '.'
          ? (this._hidden ? 'allow' : 'ignore')
          : 'allow'
      }
  
      debug('%s dotfile "%s"', access, path)
      switch (access) {
        case 'allow':
          break
        case 'deny':
          this.error(403)
          return res
        case 'ignore':
        default:
          this.error(404)
          return res
      }
    }
  
    // index file support
    if (this._index.length && this.hasTrailingSlash()) {
      this.sendIndex(path)
      return res
    }
  
    this.sendFile(path)
    return res
  }
  
  /**
   * Transfer `path`.
   *
   * @param {String} path
   * @api public
   */
  
  SendStream.prototype.send = function send (path, stat) {
    var len = stat.size
    var options = this.options
    var opts = {}
    var res = this.res
    var req = this.req
    var ranges = req.headers.range
    var offset = options.start || 0
  
    if (headersSent(res)) {
      // impossible to send now
      this.headersAlreadySent()
      return
    }
  
    debug('pipe "%s"', path)
  
    // set header fields
    this.setHeader(path, stat)
  
    // set content-type
    this.type(path)
  
    // conditional GET support
    if (this.isConditionalGET()) {
      if (this.isPreconditionFailure()) {
        this.error(412)
        return
      }
  
      if (this.isCachable() && this.isFresh()) {
        this.notModified()
        return
      }
    }
  
    // adjust len to start/end options
    len = Math.max(0, len - offset)
    if (options.end !== undefined) {
      var bytes = options.end - offset + 1
      if (len > bytes) len = bytes
    }
  
    // Range support
    if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
      // parse
      ranges = parseRange(len, ranges, {
        combine: true
      })
  
      // If-Range support
      if (!this.isRangeFresh()) {
        debug('range stale')
        ranges = -2
      }
  
      // unsatisfiable
      if (ranges === -1) {
        debug('range unsatisfiable')
  
        // Content-Range
        res.setHeader('Content-Range', contentRange('bytes', len))
  
        // 416 Requested Range Not Satisfiable
        return this.error(416, {
          headers: { 'Content-Range': res.getHeader('Content-Range') }
        })
      }
  
      // valid (syntactically invalid/multiple ranges are treated as a regular response)
      if (ranges !== -2 && ranges.length === 1) {
        debug('range %j', ranges)
  
        // Content-Range
        res.statusCode = 206
        res.setHeader('Content-Range', contentRange('bytes', len, ranges[0]))
  
        // adjust for requested range
        offset += ranges[0].start
        len = ranges[0].end - ranges[0].start + 1
      }
    }
  
    // clone options
    for (var prop in options) {
      opts[prop] = options[prop]
    }
  
    // set read options
    opts.start = offset
    opts.end = Math.max(offset, offset + len - 1)
  
    // content-length
    res.setHeader('Content-Length', len)
  
    // HEAD support
    if (req.method === 'HEAD') {
      res.end()
      return
    }
  
    this.stream(path, opts)
  }
  
  /**
   * Transfer file for `path`.
   *
   * @param {String} path
   * @api private
   */
  SendStream.prototype.sendFile = function sendFile (path) {
    var i = 0
    var self = this
  
    debug('stat "%s"', path)
    fs.stat(path, function onstat (err, stat) {
      if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
        // not found, check extensions
        return next(err)
      }
      if (err) return self.onStatError(err)
      if (stat.isDirectory()) return self.redirect(path)
      self.emit('file', path, stat)
      self.send(path, stat)
    })
  
    function next (err) {
      if (self._extensions.length <= i) {
        return err
          ? self.onStatError(err)
          : self.error(404)
      }
  
      var p = path + '.' + self._extensions[i++]
  
      debug('stat "%s"', p)
      fs.stat(p, function (err, stat) {
        if (err) return next(err)
        if (stat.isDirectory()) return next()
        self.emit('file', p, stat)
        self.send(p, stat)
      })
    }
  }
  
  /**
   * Transfer index for `path`.
   *
   * @param {String} path
   * @api private
   */
  SendStream.prototype.sendIndex = function sendIndex (path) {
    var i = -1
    var self = this
  
    function next (err) {
      if (++i >= self._index.length) {
        if (err) return self.onStatError(err)
        return self.error(404)
      }
  
      var p = join(path, self._index[i])
  
      debug('stat "%s"', p)
      fs.stat(p, function (err, stat) {
        if (err) return next(err)
        if (stat.isDirectory()) return next()
        self.emit('file', p, stat)
        self.send(p, stat)
      })
    }
  
    next()
  }
  
  /**
   * Stream `path` to the response.
   *
   * @param {String} path
   * @param {Object} options
   * @api private
   */
  
  SendStream.prototype.stream = function stream (path, options) {
    // TODO: this is all lame, refactor meeee
    var finished = false
    var self = this
    var res = this.res
  
    // pipe
    var stream = fs.createReadStream(path, options)
    this.emit('stream', stream)
    stream.pipe(res)
  
    // response finished, done with the fd
    onFinished(res, function onfinished () {
      finished = true
      destroy(stream)
    })
  
    // error handling code-smell
    stream.on('error', function onerror (err) {
      // request already finished
      if (finished) return
  
      // clean up stream
      finished = true
      destroy(stream)
  
      // error
      self.onStatError(err)
    })
  
    // end
    stream.on('end', function onend () {
      self.emit('end')
    })
  }
  
  /**
   * Set content-type based on `path`
   * if it hasn't been explicitly set.
   *
   * @param {String} path
   * @api private
   */
  
  SendStream.prototype.type = function type (path) {
    var res = this.res
  
    if (res.getHeader('Content-Type')) return
  
    var type = mime.lookup(path)
  
    if (!type) {
      debug('no content-type')
      return
    }
  
    var charset = mime.charsets.lookup(type)
  
    debug('content-type %s', type)
    res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
  }
  
  /**
   * Set response header fields, most
   * fields may be pre-defined.
   *
   * @param {String} path
   * @param {Object} stat
   * @api private
   */
  
  SendStream.prototype.setHeader = function setHeader (path, stat) {
    var res = this.res
  
    this.emit('headers', res, path, stat)
  
    if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
      debug('accept ranges')
      res.setHeader('Accept-Ranges', 'bytes')
    }
  
    if (this._cacheControl && !res.getHeader('Cache-Control')) {
      var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000)
  
      if (this._immutable) {
        cacheControl += ', immutable'
      }
  
      debug('cache-control %s', cacheControl)
      res.setHeader('Cache-Control', cacheControl)
    }
  
    if (this._lastModified && !res.getHeader('Last-Modified')) {
      var modified = stat.mtime.toUTCString()
      debug('modified %s', modified)
      res.setHeader('Last-Modified', modified)
    }
  
    if (this._etag && !res.getHeader('ETag')) {
      var val = etag(stat)
      debug('etag %s', val)
      res.setHeader('ETag', val)
    }
  }
  
  /**
   * Clear all headers from a response.
   *
   * @param {object} res
   * @private
   */
  
  function clearHeaders (res) {
    var headers = getHeaderNames(res)
  
    for (var i = 0; i < headers.length; i++) {
      res.removeHeader(headers[i])
    }
  }
  
  /**
   * Collapse all leading slashes into a single slash
   *
   * @param {string} str
   * @private
   */
  function collapseLeadingSlashes (str) {
    for (var i = 0; i < str.length; i++) {
      if (str[i] !== '/') {
        break
      }
    }
  
    return i > 1
      ? '/' + str.substr(i)
      : str
  }
  
  /**
   * Determine if path parts contain a dotfile.
   *
   * @api private
   */
  
  function containsDotFile (parts) {
    for (var i = 0; i < parts.length; i++) {
      var part = parts[i]
      if (part.length > 1 && part[0] === '.') {
        return true
      }
    }
  
    return false
  }
  
  /**
   * Create a Content-Range header.
   *
   * @param {string} type
   * @param {number} size
   * @param {array} [range]
   */
  
  function contentRange (type, size, range) {
    return type + ' ' + (range ? range.start + '-' + range.end : '*') + '/' + size
  }
  
  /**
   * Create a minimal HTML document.
   *
   * @param {string} title
   * @param {string} body
   * @private
   */
  
  function createHtmlDocument (title, body) {
    return '<!DOCTYPE html>\n' +
      '<html lang="en">\n' +
      '<head>\n' +
      '<meta charset="utf-8">\n' +
      '<title>' + title + '</title>\n' +
      '</head>\n' +
      '<body>\n' +
      '<pre>' + body + '</pre>\n' +
      '</body>\n' +
      '</html>\n'
  }
  
  /**
   * decodeURIComponent.
   *
   * Allows V8 to only deoptimize this fn instead of all
   * of send().
   *
   * @param {String} path
   * @api private
   */
  
  function decode (path) {
    try {
      return decodeURIComponent(path)
    } catch (err) {
      return -1
    }
  }
  
  /**
   * Get the header names on a response.
   *
   * @param {object} res
   * @returns {array[string]}
   * @private
   */
  
  function getHeaderNames (res) {
    return typeof res.getHeaderNames !== 'function'
      ? Object.keys(typeof res.getHeaders === 'function' ? res.getHeaders : res._headers || {})
      : res.getHeaderNames()
  }
  
  /**
   * Determine if emitter has listeners of a given type.
   *
   * The way to do this check is done three different ways in Node.js >= 0.8
   * so this consolidates them into a minimal set using instance methods.
   *
   * @param {EventEmitter} emitter
   * @param {string} type
   * @returns {boolean}
   * @private
   */
  
  function hasListeners (emitter, type) {
    var count = typeof emitter.listenerCount !== 'function'
      ? emitter.listeners(type).length
      : emitter.listenerCount(type)
  
    return count > 0
  }
  
  /**
   * Determine if the response headers have been sent.
   *
   * @param {object} res
   * @returns {boolean}
   * @private
   */
  
  function headersSent (res) {
    return typeof res.headersSent !== 'boolean'
      ? Boolean(res._header)
      : res.headersSent
  }
  
  /**
   * Normalize the index option into an array.
   *
   * @param {boolean|string|array} val
   * @param {string} name
   * @private
   */
  
  function normalizeList (val, name) {
    var list = [].concat(val || [])
  
    for (var i = 0; i < list.length; i++) {
      if (typeof list[i] !== 'string') {
        throw new TypeError(name + ' must be array of strings or false')
      }
    }
  
    return list
  }
  
  /**
   * Parse an HTTP Date into a number.
   *
   * @param {string} date
   * @private
   */
  
  function parseHttpDate (date) {
    var timestamp = date && Date.parse(date)
  
    return typeof timestamp === 'number'
      ? timestamp
      : NaN
  }
  
  /**
   * Parse a HTTP token list.
   *
   * @param {string} str
   * @private
   */
  
  function parseTokenList (str) {
    var end = 0
    var list = []
    var start = 0
  
    // gather tokens
    for (var i = 0, len = str.length; i < len; i++) {
      switch (str.charCodeAt(i)) {
        case 0x20: /*   */
          if (start === end) {
            start = end = i + 1
          }
          break
        case 0x2c: /* , */
          list.push(str.substring(start, end))
          start = end = i + 1
          break
        default:
          end = i + 1
          break
      }
    }
  
    // final token
    list.push(str.substring(start, end))
  
    return list
  }
  
  /**
   * Set an object of headers on a response.
   *
   * @param {object} res
   * @param {object} headers
   * @private
   */
  
  function setHeaders (res, headers) {
    var keys = Object.keys(headers)
  
    for (var i = 0; i < keys.length; i++) {
      var key = keys[i]
      res.setHeader(key, headers[key])
    }
  }
  