1'use strict'
2const invalidTokenRegex = /[^^_`a-zA-Z\-0-9!#$%&'*+.|~]/
3const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/
4
5const validateName = name => {
6  name = `${name}`
7  if (invalidTokenRegex.test(name) || name === '') {
8    throw new TypeError(`${name} is not a legal HTTP header name`)
9  }
10}
11
12const validateValue = value => {
13  value = `${value}`
14  if (invalidHeaderCharRegex.test(value)) {
15    throw new TypeError(`${value} is not a legal HTTP header value`)
16  }
17}
18
19const find = (map, name) => {
20  name = name.toLowerCase()
21  for (const key in map) {
22    if (key.toLowerCase() === name) {
23      return key
24    }
25  }
26  return undefined
27}
28
29const MAP = Symbol('map')
30class Headers {
31  constructor (init = undefined) {
32    this[MAP] = Object.create(null)
33    if (init instanceof Headers) {
34      const rawHeaders = init.raw()
35      const headerNames = Object.keys(rawHeaders)
36      for (const headerName of headerNames) {
37        for (const value of rawHeaders[headerName]) {
38          this.append(headerName, value)
39        }
40      }
41      return
42    }
43
44    // no-op
45    if (init === undefined || init === null) {
46      return
47    }
48
49    if (typeof init === 'object') {
50      const method = init[Symbol.iterator]
51      if (method !== null && method !== undefined) {
52        if (typeof method !== 'function') {
53          throw new TypeError('Header pairs must be iterable')
54        }
55
56        // sequence<sequence<ByteString>>
57        // Note: per spec we have to first exhaust the lists then process them
58        const pairs = []
59        for (const pair of init) {
60          if (typeof pair !== 'object' ||
61              typeof pair[Symbol.iterator] !== 'function') {
62            throw new TypeError('Each header pair must be iterable')
63          }
64          const arrPair = Array.from(pair)
65          if (arrPair.length !== 2) {
66            throw new TypeError('Each header pair must be a name/value tuple')
67          }
68          pairs.push(arrPair)
69        }
70
71        for (const pair of pairs) {
72          this.append(pair[0], pair[1])
73        }
74      } else {
75        // record<ByteString, ByteString>
76        for (const key of Object.keys(init)) {
77          this.append(key, init[key])
78        }
79      }
80    } else {
81      throw new TypeError('Provided initializer must be an object')
82    }
83  }
84
85  get (name) {
86    name = `${name}`
87    validateName(name)
88    const key = find(this[MAP], name)
89    if (key === undefined) {
90      return null
91    }
92
93    return this[MAP][key].join(', ')
94  }
95
96  forEach (callback, thisArg = undefined) {
97    let pairs = getHeaders(this)
98    for (let i = 0; i < pairs.length; i++) {
99      const [name, value] = pairs[i]
100      callback.call(thisArg, value, name, this)
101      // refresh in case the callback added more headers
102      pairs = getHeaders(this)
103    }
104  }
105
106  set (name, value) {
107    name = `${name}`
108    value = `${value}`
109    validateName(name)
110    validateValue(value)
111    const key = find(this[MAP], name)
112    this[MAP][key !== undefined ? key : name] = [value]
113  }
114
115  append (name, value) {
116    name = `${name}`
117    value = `${value}`
118    validateName(name)
119    validateValue(value)
120    const key = find(this[MAP], name)
121    if (key !== undefined) {
122      this[MAP][key].push(value)
123    } else {
124      this[MAP][name] = [value]
125    }
126  }
127
128  has (name) {
129    name = `${name}`
130    validateName(name)
131    return find(this[MAP], name) !== undefined
132  }
133
134  delete (name) {
135    name = `${name}`
136    validateName(name)
137    const key = find(this[MAP], name)
138    if (key !== undefined) {
139      delete this[MAP][key]
140    }
141  }
142
143  raw () {
144    return this[MAP]
145  }
146
147  keys () {
148    return new HeadersIterator(this, 'key')
149  }
150
151  values () {
152    return new HeadersIterator(this, 'value')
153  }
154
155  [Symbol.iterator] () {
156    return new HeadersIterator(this, 'key+value')
157  }
158
159  entries () {
160    return new HeadersIterator(this, 'key+value')
161  }
162
163  get [Symbol.toStringTag] () {
164    return 'Headers'
165  }
166
167  static exportNodeCompatibleHeaders (headers) {
168    const obj = Object.assign(Object.create(null), headers[MAP])
169
170    // http.request() only supports string as Host header. This hack makes
171    // specifying custom Host header possible.
172    const hostHeaderKey = find(headers[MAP], 'Host')
173    if (hostHeaderKey !== undefined) {
174      obj[hostHeaderKey] = obj[hostHeaderKey][0]
175    }
176
177    return obj
178  }
179
180  static createHeadersLenient (obj) {
181    const headers = new Headers()
182    for (const name of Object.keys(obj)) {
183      if (invalidTokenRegex.test(name)) {
184        continue
185      }
186
187      if (Array.isArray(obj[name])) {
188        for (const val of obj[name]) {
189          if (invalidHeaderCharRegex.test(val)) {
190            continue
191          }
192
193          if (headers[MAP][name] === undefined) {
194            headers[MAP][name] = [val]
195          } else {
196            headers[MAP][name].push(val)
197          }
198        }
199      } else if (!invalidHeaderCharRegex.test(obj[name])) {
200        headers[MAP][name] = [obj[name]]
201      }
202    }
203    return headers
204  }
205}
206
207Object.defineProperties(Headers.prototype, {
208  get: { enumerable: true },
209  forEach: { enumerable: true },
210  set: { enumerable: true },
211  append: { enumerable: true },
212  has: { enumerable: true },
213  delete: { enumerable: true },
214  keys: { enumerable: true },
215  values: { enumerable: true },
216  entries: { enumerable: true },
217})
218
219const getHeaders = (headers, kind = 'key+value') =>
220  Object.keys(headers[MAP]).sort().map(
221    kind === 'key' ? k => k.toLowerCase()
222    : kind === 'value' ? k => headers[MAP][k].join(', ')
223    : k => [k.toLowerCase(), headers[MAP][k].join(', ')]
224  )
225
226const INTERNAL = Symbol('internal')
227
228class HeadersIterator {
229  constructor (target, kind) {
230    this[INTERNAL] = {
231      target,
232      kind,
233      index: 0,
234    }
235  }
236
237  get [Symbol.toStringTag] () {
238    return 'HeadersIterator'
239  }
240
241  next () {
242    /* istanbul ignore if: should be impossible */
243    if (!this || Object.getPrototypeOf(this) !== HeadersIterator.prototype) {
244      throw new TypeError('Value of `this` is not a HeadersIterator')
245    }
246
247    const { target, kind, index } = this[INTERNAL]
248    const values = getHeaders(target, kind)
249    const len = values.length
250    if (index >= len) {
251      return {
252        value: undefined,
253        done: true,
254      }
255    }
256
257    this[INTERNAL].index++
258
259    return { value: values[index], done: false }
260  }
261}
262
263// manually extend because 'extends' requires a ctor
264Object.setPrototypeOf(HeadersIterator.prototype,
265  Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())))
266
267module.exports = Headers
268