OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking / node_modules / bare-form-data / index.js

262 lines · javascript ref:
const { ReadableStream } = require('bare-stream/web')
const { isBuffer } = require('bare-buffer')
const errors = require('./lib/errors')

class FormData {
  constructor() {
    this._entries = []
  }

  append(name, value, filename) {
    if (typeof value !== 'string') {
      if (!isFile(value) || filename) {
        value = new File([value], filename || 'blob', { type: value.type })
      }
    }

    this._entries.push([name, value])
  }

  delete(name) {
    this._entries = this._entries.filter((entry) => entry[0] !== name)
  }

  get(name) {
    const entry = this._entries.find((entry) => entry[0] === name)

    return entry ? entry[1] : null
  }

  getAll(name) {
    const entries = []

    for (const entry of this._entries) {
      if (entry[0] === name) entries.push(entry[1])
    }

    return entries
  }

  has(name) {
    return this._entries.findIndex((entry) => entry[0] === name) !== -1
  }

  set(name, value, filename) {
    this.delete(name)
    this.append(name, value, filename)
  }

  [Symbol.iterator]() {
    return this._entries[Symbol.iterator]()
  }

  [Symbol.for('bare.inspect')]() {
    return {
      __proto__: { constructor: FormData },

      entries: this._entries
    }
  }
}

module.exports = exports = FormData

exports.FormData = FormData

function isFormData(value) {
  return value instanceof FormData
}

exports.isFormData = isFormData

class Blob {
  // https://w3c.github.io/FileAPI/#dom-blob-blob
  constructor(parts, options = {}) {
    const { type = '' } = options

    this._bytes = processBlobParts(parts)
    this._type = type
  }

  // https://w3c.github.io/FileAPI/#dfn-size
  get size() {
    return this._bytes.byteLength
  }

  // https://w3c.github.io/FileAPI/#dfn-type
  get type() {
    return this._type
  }

  // https://w3c.github.io/FileAPI/#stream-method-algo
  stream() {
    const bytes = this._bytes

    return new ReadableStream({
      start(controller) {
        controller.enqueue(bytes)
        controller.close()
      }
    })
  }

  async buffer() {
    return Buffer.from(this._bytes)
  }

  // https://w3c.github.io/FileAPI/#bytes-method-algo
  async bytes() {
    return this.buffer()
  }

  // https://w3c.github.io/FileAPI/#arraybuffer-method-algo
  async arrayBuffer() {
    const buffer = new ArrayBuffer(this._bytes.byteLength)
    buffer.set(this._bytes)
    return buffer
  }

  // https://w3c.github.io/FileAPI/#text-method-algo
  async text() {
    return this._bytes.toString()
  }

  [Symbol.for('bare.inspect')]() {
    return {
      __proto__: { constructor: Blob },

      size: this.size,
      type: this.type
    }
  }
}

exports.Blob = Blob

function isBlob(value) {
  return value instanceof Blob
}

exports.isBlob = Blob.isBlob = isBlob

class File extends Blob {
  // https://w3c.github.io/FileAPI/#dom-file-file
  constructor(parts, name, options = {}) {
    const { lastModified = Date.now() } = options

    super(parts, options)

    this._name = name
    this._lastModified = lastModified
  }

  // https://w3c.github.io/FileAPI/#dfn-name
  get name() {
    return this._name
  }

  // https://w3c.github.io/FileAPI/#dfn-lastModified
  get lastModified() {
    return this._lastModified
  }

  [Symbol.for('bare.inspect')]() {
    return {
      __proto__: { constructor: File },

      size: this.size,
      type: this.type,
      name: this.name,
      lastModified: this.lastModified
    }
  }
}

exports.File = File

function isFile(value) {
  return value instanceof File
}

exports.isFile = File.isFile = isFile

// https://w3c.github.io/FileAPI/#process-blob-parts
function processBlobParts(parts) {
  const chunks = []

  for (const part of parts) {
    if (typeof part === 'string') {
      const buffer = Buffer.from(part)
      if (parts.length === 1) return buffer
      chunks.push(buffer)
    } else if (isBlob(part)) {
      chunks.push(part._bytes)
    } else if (isBuffer(part)) {
      chunks.push(part)
    } else if (ArrayBuffer.isView(part)) {
      chunks.push(Buffer.from(part.buffer, part.byteOffset, part.byteLength))
    } else {
      chunks.push(Buffer.from(part))
    }
  }

  return Buffer.concat(chunks)
}

function toBlob(formData, mimeType = 'multipart/form-data') {
  switch (mimeType) {
    case 'multipart/form-data':
      return toMultipartBlob(formData)
    default:
      throw errors.INVALID_MIME_TYPE(`Invalid MIME type '${mimeType}'`)
  }
}

exports.toBlob = toBlob

function escape(value) {
  return value.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
}

function normalizeLinefeeds(value) {
  return value.replace(/\r?\n|\r/g, '\r\n')
}

const linefeed = Buffer.from('\r\n')

// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm
function toMultipartBlob(formData) {
  const boundary = Math.random().toString(16).slice(2, 18).padStart(16, '0')

  const prefix = `--${boundary}\r\nContent-Disposition: form-data`

  const parts = []

  for (const [name, value] of formData) {
    if (typeof value === 'string') {
      const chunk = Buffer.from(
        prefix +
          `; name="${escape(normalizeLinefeeds(name))}"` +
          `\r\n\r\n${normalizeLinefeeds(value)}\r\n`
      )

      parts.push(chunk)
    } else {
      const chunk = Buffer.from(
        prefix +
          `; name="${escape(normalizeLinefeeds(name))}"` +
          `; filename="${escape(value.name)}"` +
          `\r\nContent-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`
      )

      parts.push(chunk, value, linefeed)
    }
  }

  parts.push(Buffer.from(`--${boundary}--\r\n`))

  return new Blob(parts, {
    type: 'multipart/form-data; boundary=' + boundary
  })
}