• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const { Blob, File: NativeFile } = require('buffer')
4const { types } = require('util')
5const { kState } = require('./symbols')
6const { isBlobLike } = require('./util')
7const { webidl } = require('./webidl')
8const { parseMIMEType, serializeAMimeType } = require('./dataURL')
9const { kEnumerableProperty } = require('../core/util')
10const encoder = new TextEncoder()
11
12class File extends Blob {
13  constructor (fileBits, fileName, options = {}) {
14    // The File constructor is invoked with two or three parameters, depending
15    // on whether the optional dictionary parameter is used. When the File()
16    // constructor is invoked, user agents must run the following steps:
17    webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' })
18
19    fileBits = webidl.converters['sequence<BlobPart>'](fileBits)
20    fileName = webidl.converters.USVString(fileName)
21    options = webidl.converters.FilePropertyBag(options)
22
23    // 1. Let bytes be the result of processing blob parts given fileBits and
24    // options.
25    // Note: Blob handles this for us
26
27    // 2. Let n be the fileName argument to the constructor.
28    const n = fileName
29
30    // 3. Process FilePropertyBag dictionary argument by running the following
31    // substeps:
32
33    //    1. If the type member is provided and is not the empty string, let t
34    //    be set to the type dictionary member. If t contains any characters
35    //    outside the range U+0020 to U+007E, then set t to the empty string
36    //    and return from these substeps.
37    //    2. Convert every character in t to ASCII lowercase.
38    let t = options.type
39    let d
40
41    // eslint-disable-next-line no-labels
42    substep: {
43      if (t) {
44        t = parseMIMEType(t)
45
46        if (t === 'failure') {
47          t = ''
48          // eslint-disable-next-line no-labels
49          break substep
50        }
51
52        t = serializeAMimeType(t).toLowerCase()
53      }
54
55      //    3. If the lastModified member is provided, let d be set to the
56      //    lastModified dictionary member. If it is not provided, set d to the
57      //    current date and time represented as the number of milliseconds since
58      //    the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
59      d = options.lastModified
60    }
61
62    // 4. Return a new File object F such that:
63    // F refers to the bytes byte sequence.
64    // F.size is set to the number of total bytes in bytes.
65    // F.name is set to n.
66    // F.type is set to t.
67    // F.lastModified is set to d.
68
69    super(processBlobParts(fileBits, options), { type: t })
70    this[kState] = {
71      name: n,
72      lastModified: d,
73      type: t
74    }
75  }
76
77  get name () {
78    webidl.brandCheck(this, File)
79
80    return this[kState].name
81  }
82
83  get lastModified () {
84    webidl.brandCheck(this, File)
85
86    return this[kState].lastModified
87  }
88
89  get type () {
90    webidl.brandCheck(this, File)
91
92    return this[kState].type
93  }
94}
95
96class FileLike {
97  constructor (blobLike, fileName, options = {}) {
98    // TODO: argument idl type check
99
100    // The File constructor is invoked with two or three parameters, depending
101    // on whether the optional dictionary parameter is used. When the File()
102    // constructor is invoked, user agents must run the following steps:
103
104    // 1. Let bytes be the result of processing blob parts given fileBits and
105    // options.
106
107    // 2. Let n be the fileName argument to the constructor.
108    const n = fileName
109
110    // 3. Process FilePropertyBag dictionary argument by running the following
111    // substeps:
112
113    //    1. If the type member is provided and is not the empty string, let t
114    //    be set to the type dictionary member. If t contains any characters
115    //    outside the range U+0020 to U+007E, then set t to the empty string
116    //    and return from these substeps.
117    //    TODO
118    const t = options.type
119
120    //    2. Convert every character in t to ASCII lowercase.
121    //    TODO
122
123    //    3. If the lastModified member is provided, let d be set to the
124    //    lastModified dictionary member. If it is not provided, set d to the
125    //    current date and time represented as the number of milliseconds since
126    //    the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
127    const d = options.lastModified ?? Date.now()
128
129    // 4. Return a new File object F such that:
130    // F refers to the bytes byte sequence.
131    // F.size is set to the number of total bytes in bytes.
132    // F.name is set to n.
133    // F.type is set to t.
134    // F.lastModified is set to d.
135
136    this[kState] = {
137      blobLike,
138      name: n,
139      type: t,
140      lastModified: d
141    }
142  }
143
144  stream (...args) {
145    webidl.brandCheck(this, FileLike)
146
147    return this[kState].blobLike.stream(...args)
148  }
149
150  arrayBuffer (...args) {
151    webidl.brandCheck(this, FileLike)
152
153    return this[kState].blobLike.arrayBuffer(...args)
154  }
155
156  slice (...args) {
157    webidl.brandCheck(this, FileLike)
158
159    return this[kState].blobLike.slice(...args)
160  }
161
162  text (...args) {
163    webidl.brandCheck(this, FileLike)
164
165    return this[kState].blobLike.text(...args)
166  }
167
168  get size () {
169    webidl.brandCheck(this, FileLike)
170
171    return this[kState].blobLike.size
172  }
173
174  get type () {
175    webidl.brandCheck(this, FileLike)
176
177    return this[kState].blobLike.type
178  }
179
180  get name () {
181    webidl.brandCheck(this, FileLike)
182
183    return this[kState].name
184  }
185
186  get lastModified () {
187    webidl.brandCheck(this, FileLike)
188
189    return this[kState].lastModified
190  }
191
192  get [Symbol.toStringTag] () {
193    return 'File'
194  }
195}
196
197Object.defineProperties(File.prototype, {
198  [Symbol.toStringTag]: {
199    value: 'File',
200    configurable: true
201  },
202  name: kEnumerableProperty,
203  lastModified: kEnumerableProperty
204})
205
206webidl.converters.Blob = webidl.interfaceConverter(Blob)
207
208webidl.converters.BlobPart = function (V, opts) {
209  if (webidl.util.Type(V) === 'Object') {
210    if (isBlobLike(V)) {
211      return webidl.converters.Blob(V, { strict: false })
212    }
213
214    if (
215      ArrayBuffer.isView(V) ||
216      types.isAnyArrayBuffer(V)
217    ) {
218      return webidl.converters.BufferSource(V, opts)
219    }
220  }
221
222  return webidl.converters.USVString(V, opts)
223}
224
225webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(
226  webidl.converters.BlobPart
227)
228
229// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag
230webidl.converters.FilePropertyBag = webidl.dictionaryConverter([
231  {
232    key: 'lastModified',
233    converter: webidl.converters['long long'],
234    get defaultValue () {
235      return Date.now()
236    }
237  },
238  {
239    key: 'type',
240    converter: webidl.converters.DOMString,
241    defaultValue: ''
242  },
243  {
244    key: 'endings',
245    converter: (value) => {
246      value = webidl.converters.DOMString(value)
247      value = value.toLowerCase()
248
249      if (value !== 'native') {
250        value = 'transparent'
251      }
252
253      return value
254    },
255    defaultValue: 'transparent'
256  }
257])
258
259/**
260 * @see https://www.w3.org/TR/FileAPI/#process-blob-parts
261 * @param {(NodeJS.TypedArray|Blob|string)[]} parts
262 * @param {{ type: string, endings: string }} options
263 */
264function processBlobParts (parts, options) {
265  // 1. Let bytes be an empty sequence of bytes.
266  /** @type {NodeJS.TypedArray[]} */
267  const bytes = []
268
269  // 2. For each element in parts:
270  for (const element of parts) {
271    // 1. If element is a USVString, run the following substeps:
272    if (typeof element === 'string') {
273      // 1. Let s be element.
274      let s = element
275
276      // 2. If the endings member of options is "native", set s
277      //    to the result of converting line endings to native
278      //    of element.
279      if (options.endings === 'native') {
280        s = convertLineEndingsNative(s)
281      }
282
283      // 3. Append the result of UTF-8 encoding s to bytes.
284      bytes.push(encoder.encode(s))
285    } else if (
286      types.isAnyArrayBuffer(element) ||
287      types.isTypedArray(element)
288    ) {
289      // 2. If element is a BufferSource, get a copy of the
290      //    bytes held by the buffer source, and append those
291      //    bytes to bytes.
292      if (!element.buffer) { // ArrayBuffer
293        bytes.push(new Uint8Array(element))
294      } else {
295        bytes.push(
296          new Uint8Array(element.buffer, element.byteOffset, element.byteLength)
297        )
298      }
299    } else if (isBlobLike(element)) {
300      // 3. If element is a Blob, append the bytes it represents
301      //    to bytes.
302      bytes.push(element)
303    }
304  }
305
306  // 3. Return bytes.
307  return bytes
308}
309
310/**
311 * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native
312 * @param {string} s
313 */
314function convertLineEndingsNative (s) {
315  // 1. Let native line ending be be the code point U+000A LF.
316  let nativeLineEnding = '\n'
317
318  // 2. If the underlying platform’s conventions are to
319  //    represent newlines as a carriage return and line feed
320  //    sequence, set native line ending to the code point
321  //    U+000D CR followed by the code point U+000A LF.
322  if (process.platform === 'win32') {
323    nativeLineEnding = '\r\n'
324  }
325
326  return s.replace(/\r?\n/g, nativeLineEnding)
327}
328
329// If this function is moved to ./util.js, some tools (such as
330// rollup) will warn about circular dependencies. See:
331// https://github.com/nodejs/undici/issues/1629
332function isFileLike (object) {
333  return (
334    (NativeFile && object instanceof NativeFile) ||
335    object instanceof File || (
336      object &&
337      (typeof object.stream === 'function' ||
338      typeof object.arrayBuffer === 'function') &&
339      object[Symbol.toStringTag] === 'File'
340    )
341  )
342}
343
344module.exports = { File, FileLike, isFileLike }
345