summaryrefslogtreecommitdiffstats
path: root/dom/system/tests/ioutils/test_ioutils_read_write.html
blob: f52115d26185f328a56ec6e605a5c3562d551fef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <title>Test the IOUtils file I/O API</title>
  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
  <script src="file_ioutils_test_fixtures.js"></script>
  <script>
    "use strict";

    const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
    const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");

    add_task(async function test_read_failure() {
      const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp");
      await Assert.rejects(
        IOUtils.read(doesNotExist),
        /Could not open the file at .*/,
        "IOUtils::read rejects when file does not exist"
      );
    });

    add_task(async function test_write_no_overwrite() {
      // Make a new file, and try to write to it with overwrites disabled.
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_overwrite.tmp");
      const untouchableContents = new TextEncoder().encode("Can't touch this!\n");

      let exists = await IOUtils.exists(tmpFileName);
      ok(!exists, `File ${tmpFileName} should not exist before writing`);

      await IOUtils.write(tmpFileName, untouchableContents);

      exists = await IOUtils.exists(tmpFileName);
      ok(exists, `File ${tmpFileName} should exist after writing`);

      const newContents = new TextEncoder().encode("Nah nah nah!\n");
      await Assert.rejects(
        IOUtils.write(tmpFileName, newContents, {
          mode: "create",
        }),
        /Refusing to overwrite the file at */,
        "IOUtils::write rejects writing to existing file if overwrites are disabled"
      );
      ok(
        await fileHasBinaryContents(tmpFileName, untouchableContents),
        "IOUtils::write doesn't change target file when overwrite is refused"
      );

      const bytesWritten = await IOUtils.write(
        tmpFileName,
        newContents,
        { mode: "overwrite" }
      );
      is(
        bytesWritten,
        newContents.length,
        "IOUtils::write can overwrite files if specified"
      );

      await cleanup(tmpFileName);
    });

    add_task(async function test_write_with_backup() {
      info("Test backup file option with non-existing file");

      let fileContents = new TextEncoder().encode("Original file contents");
      let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_option.tmp");
      let backupFileName = destFileName + ".backup";
      let bytesWritten =
        await IOUtils.write(destFileName, fileContents, {
          backupFile: backupFileName,
        });
      ok(
        await fileHasTextContents(destFileName, "Original file contents"),
        "IOUtils::write creates a new file with the correct contents"
      );
      ok(
        !await fileExists(backupFileName),
        "IOUtils::write does not create a backup if the target file does not exist"
      );
      is(
        bytesWritten,
        fileContents.length,
        "IOUtils::write correctly writes to a new file without performing a backup"
      );

      info("Test backup file option with existing destination");
      let newFileContents = new TextEncoder().encode("New file contents");
      ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
      bytesWritten =
        await IOUtils.write(destFileName, newFileContents, {
          backupFile: backupFileName,
        });
      ok(
        await fileHasTextContents(backupFileName, "Original file contents"),
        "IOUtils::write can backup an existing file before writing"
      );
      ok(
        await fileHasTextContents(destFileName, "New file contents"),
        "IOUtils::write can create the target with the correct contents"
      );
      is(
        bytesWritten,
        newFileContents.length,
        "IOUtils::write correctly writes to the target after taking a backup"
      );

      await cleanup(destFileName, backupFileName);
    });

    add_task(async function test_write_with_backup_and_tmp() {
      info("Test backup with tmp and backup file options, non-existing destination");

      let fileContents = new TextEncoder().encode("Original file contents");
      let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_with_backup_and_tmp_options.tmp");
      let backupFileName = destFileName + ".backup";
      let tmpFileName = PathUtils.join(PathUtils.tempDir, "temp_file.tmp");
      let bytesWritten =
        await IOUtils.write(destFileName, fileContents, {
          backupFile: backupFileName,
          tmpPath: tmpFileName,
        });
      ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
      ok(
        !await fileExists(backupFileName),
        "IOUtils::write does not create a backup if the target file does not exist"
      );
      ok(
        await fileHasTextContents(destFileName, "Original file contents"),
        "IOUtils::write can write to the destination when a temporary file is used"
      );
      is(
        bytesWritten,
        fileContents.length,
        "IOUtils::write can copy tmp file to destination without performing a backup"
      );

      info("Test backup with tmp and backup file options, existing destination");
      let newFileContents = new TextEncoder().encode("New file contents");
      ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
      bytesWritten =
        await IOUtils.write(destFileName, newFileContents, {
          backupFile: backupFileName,
          tmpPath: tmpFileName,
        });

      ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
      ok(
        await fileHasTextContents(backupFileName, "Original file contents"),
        "IOUtils::write can create a backup if the target file exists"
      );
      ok(
        await fileHasTextContents(destFileName, "New file contents"),
        "IOUtils::write can write to the destination when a temporary file is used"
      );
      is(
        bytesWritten,
        newFileContents.length,
        "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup"
      );

      info("Test backup with tmp and backup file options, existing destination and backup");
      newFileContents = new TextEncoder().encode("Updated new file contents");
      ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
      ok(await fileExists(backupFileName), `Expected ${backupFileName} to exist`);
      bytesWritten =
        await IOUtils.write(destFileName, newFileContents, {
          backupFile: backupFileName,
          tmpPath: tmpFileName,
        });

      ok(!await fileExists(tmpFileName), "IOUtils::write cleans up the tmpFile");
      ok(
        await fileHasTextContents(backupFileName, "New file contents"),
        "IOUtils::write can create a backup if the target file exists"
      );
      ok(
        await fileHasTextContents(destFileName, "Updated new file contents"),
        "IOUtils::write can write to the destination when a temporary file is used"
      );
      is(
        bytesWritten,
        newFileContents.length,
        "IOUtils::write IOUtils::write can move tmp file to destination after performing a backup"
      );

      await cleanup(destFileName, backupFileName);
    });

    add_task(async function test_partial_read() {
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_partial_read.tmp");
      const bytes = Uint8Array.of(...new Array(50).keys());
      const bytesWritten = await IOUtils.write(tmpFileName, bytes);
      is(
        bytesWritten,
        50,
        "IOUtils::write can write entire byte array to file"
      );

      // Read just the first 10 bytes.
      const first10 = bytes.slice(0, 10);
      const bytes10 = await IOUtils.read(tmpFileName, { maxBytes: 10 });
      ok(
        ObjectUtils.deepEqual(bytes10, first10),
        "IOUtils::read can read part of a file, up to specified max bytes"
      );

      // Trying to explicitly read nothing isn't useful, but it should still
      // succeed.
      const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 });
      is(bytes0.length, 0, "IOUtils::read can read 0 bytes");

      await cleanup(tmpFileName);
    });

    add_task(async function test_empty_read_and_write() {
      // Trying to write an empty file isn't very useful, but it should still
      // succeed.
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_empty.tmp");
      const emptyByteArray = new Uint8Array(0);
      const bytesWritten = await IOUtils.write(
        tmpFileName,
        emptyByteArray
      );
      is(bytesWritten, 0, "IOUtils::write can create an empty file");

      // Trying to explicitly read nothing isn't useful, but it should still
      // succeed.
      const bytes0 = await IOUtils.read(tmpFileName, { maxBytes: 0 });
      is(bytes0.length, 0, "IOUtils::read can read 0 bytes");

      // Implicitly try to read nothing.
      const nothing = await IOUtils.read(tmpFileName);
      is(nothing.length, 0, "IOUtils:: read can read empty files");

      await cleanup(tmpFileName);
    });

    add_task(async function test_full_read_and_write() {
      // Write a file.

      info("Test writing to a new binary file");
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_numbers.tmp");
      const bytes = Uint8Array.of(...new Array(50).keys());
      const bytesWritten = await IOUtils.write(tmpFileName, bytes);
      is(
        bytesWritten,
        50,
        "IOUtils::write can write entire byte array to file"
      );

      // Read it back.
      info("Test reading a binary file");
      let fileContents = await IOUtils.read(tmpFileName);
      ok(
        ObjectUtils.deepEqual(bytes, fileContents) &&
        bytes.length == fileContents.length,
        "IOUtils::read can read back entire file"
      );

      const tooManyBytes = bytes.length + 1;
      fileContents = await IOUtils.read(tmpFileName, { maxBytes: tooManyBytes });
      ok(
        ObjectUtils.deepEqual(bytes, fileContents) &&
        fileContents.length == bytes.length,
        "IOUtils::read can read entire file when requested maxBytes is too large"
      );

      // Clean up.
      await cleanup(tmpFileName);
    });

    add_task(async function test_write_relative_path() {
      const tmpFileName = "test_ioutils_write_relative_path.tmp";
      const bytes = Uint8Array.of(...new Array(50).keys());

      info("Test writing a file at a relative destination");
      await Assert.rejects(
        IOUtils.write(tmpFileName, bytes),
        /Could not parse path/,
        "IOUtils::write only works with absolute paths"
      );
    });

    add_task(async function test_read_relative_path() {
      const tmpFileName = "test_ioutils_read_relative_path.tmp";

      info("Test reading a file at a relative destination");
      await Assert.rejects(
        IOUtils.read(tmpFileName),
        /Could not parse path/,
        "IOUtils::write only works with absolute paths"
      );
    });

    add_task(async function test_lz4() {
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4.tmp");

      info("Test writing lz4 encoded data");
      const varyingBytes = Uint8Array.of(...new Array(50).keys());
      let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true });
      is(bytesWritten, 64, "Expected to write 64 bytes");

      info("Test reading lz4 encoded data");
      let readData = await IOUtils.read(tmpFileName, { decompress: true });
      ok(readData.equals(varyingBytes), "IOUtils can write and read back LZ4 encoded data");

      info("Test writing lz4 compressed data");
      const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
      bytesWritten = await IOUtils.write(tmpFileName, repeatedBytes, { compress: true });
      is(bytesWritten, 23, "Expected 50 bytes to compress to 23 bytes");

      info("Test reading lz4 encoded data");
      readData = await IOUtils.read(tmpFileName, { decompress: true });
      ok(readData.equals(repeatedBytes), "IOUtils can write and read back LZ4 compressed data");

      info("Test writing empty lz4 compressed data")
      const empty = new Uint8Array();
      bytesWritten = await IOUtils.write(tmpFileName, empty, { compress: true });
      is(bytesWritten, 12, "Expected to write just the LZ4 header, with a content length of 0");


      info("Test reading empty lz4 compressed data")
      const readEmpty = await IOUtils.read(tmpFileName, { decompress: true });
      ok(readEmpty.equals(empty), "IOUtils can write and read back empty buffers with LZ4");
      const readEmptyRaw = await IOUtils.read(tmpFileName, { decompress: false });
      is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header");
      const expectedHeader = Uint8Array.of(109, 111, 122, 76, 122, 52, 48, 0, 0, 0, 0, 0); // "mozLz40\0\0\0\0"
      ok(readEmptyRaw.equals(expectedHeader), "Expected to read header with content length of 0");

      await cleanup(tmpFileName);
    });

    add_task(async function test_lz4_bad_call() {
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_bad_call.tmp");

      info("Test decompression with invalid options");
      const varyingBytes = Uint8Array.of(...new Array(50).keys());
      let bytesWritten = await IOUtils.write(tmpFileName, varyingBytes, { compress: true });
      is(bytesWritten, 64, "Expected to write 64 bytes");
      await Assert.rejects(
        IOUtils.read(tmpFileName, { maxBytes: 4, decompress: true }),
        /The `maxBytes` and `decompress` options are not compatible/,
        "IOUtils::read rejects when maxBytes and decompress options are both used"
      );

      await cleanup(tmpFileName)
    });

    add_task(async function test_lz4_failure() {
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_lz4_fail.tmp");

      info("Test decompression of non-lz4 data");
      const repeatedBytes = Uint8Array.of(...new Array(50).fill(1));
      await IOUtils.write(tmpFileName, repeatedBytes, { compress: false });

      await Assert.rejects(
        IOUtils.read(tmpFileName, { decompress: true }),
        (actual) => {
          is(actual.constructor, DOMException,
             "rejection reason constructor for decompress with bad header");
          is(actual.name, "NotReadableError",
             "rejection error name for decompress with bad header");
          ok(/Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/
             .test(actual.message),
             "rejection error message for decompress with bad header. Got "
             + actual.message);
          return true;
        },
        "IOUtils::read fails to decompress LZ4 data with a bad header"
      );

      info("Test decompression of short byte buffer");
      const elevenBytes = Uint8Array.of(...new Array(11).fill(1));
      await IOUtils.write(tmpFileName, elevenBytes, { compress: false });

      await Assert.rejects(
        IOUtils.read(tmpFileName, { decompress: true }),
        /Could not decompress file because the buffer is too short/,
        "IOUtils::read fails to decompress LZ4 data with missing header"
      );

      info("Test decompression of valid header, but corrupt contents");
      const headerFor10bytes = [109, 111, 122, 76, 122, 52, 48, 0, 10, 0, 0, 0] // "mozlz40\0" + 4 byte length
      const badContents = new Array(11).fill(255); // Bad leading byte, followed by uncompressed stream.
      const goodHeaderBadContents = Uint8Array.of(...headerFor10bytes, ...badContents);
      await IOUtils.write(tmpFileName, goodHeaderBadContents, { compress: false });

      await Assert.rejects(
        IOUtils.read(tmpFileName, { decompress: true }),
        /Could not decompress file contents, the file may be corrupt/,
        "IOUtils::read fails to read corrupt LZ4 contents with a correct header"
      );

      await cleanup(tmpFileName);
    });

    add_task(async function test_write_directory() {
      const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_directory.tmp");
      const tmpPath = `${fileName}.tmp`;
      const bytes = Uint8Array.of(1, 2, 3, 4);

      await IOUtils.makeDirectory(fileName);
      await Assert.rejects(
        IOUtils.write(fileName, bytes),
        /NotAllowedError: Could not open the file at .+ for writing/);

      await Assert.rejects(
        IOUtils.write(fileName, bytes, { tmpPath }),
        /NotAllowedError: Could not open the file at .+ for writing/);

      ok(!await IOUtils.exists(PathUtils.join(fileName, PathUtils.filename(tmpPath))));
    });

    add_task(async function test_read_offset() {
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_read_offset.tmp");

      const bytes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
      const byteArray = Uint8Array.of(...bytes);

      await IOUtils.write(tmpFileName, byteArray);

      for (const offset of [0, 5]) {
        info(`Reading bytes from offset ${offset}`);

        const readBytes = await IOUtils.read(tmpFileName, { offset });
        Assert.deepEqual(
          Array.from(readBytes),
          bytes.slice(offset),
          `should have read bytes from offset ${offset}`
        );
      }

      for (const offset of [0, 5]) {
        info(`Reading up to 5 bytes from offset ${offset}`);

        const readBytes = await IOUtils.read(tmpFileName, {offset, maxBytes: 5});
        Assert.deepEqual(
          Array.from(readBytes),
          bytes.slice(offset, offset + 5),
          `should have read 5 bytes from offset ${offset}`
        );
      }

      {
        info(`Reading bytes from offset 10`);
        const readBytes = await IOUtils.read(tmpFileName, {offset: 10});
        is(readBytes.length, 0, "should have read 0 bytes");
      }

      {
        info(`Reading up to 10 bytes from offset 5`);
        const readBytes = await IOUtils.read(tmpFileName, {offset: 5, maxBytes: 10});
        is(readBytes.length, 5, "should have read 5 bytes");
        Assert.deepEqual(
          Array.from(readBytes),
          bytes.slice(5, 10),
          "should have read last 5 bytes"
        );
      }
    });

    add_task(async function test_write_appendOrCreate() {
      const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_appendOrCreate.tmp");

      await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4), { mode: "appendOrCreate" });

      {
        const contents = await IOUtils.read(fileName);
        Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4], "read bytes should be equal");
      }

      await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "appendOrCreate" });

      {
        const contents = await IOUtils.read(fileName);
        Assert.deepEqual(Array.from(contents), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appendOrCreateing");
      }

      await cleanup(fileName);
    });

    add_task(async function test_write_append() {
      const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append.tmp");

      await IOUtils.write(fileName, Uint8Array.of(0, 1, 2, 3, 4));

      const beforeAppend = await IOUtils.read(fileName);
      Assert.deepEqual(Array.from(beforeAppend), [0, 1, 2, 3, 4], "read bytes should be equal");

      await IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" });

      const afterAppend = await IOUtils.read(fileName);
      Assert.deepEqual(Array.from(afterAppend), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "read bytes should be equal after appending");

      await cleanup(fileName);
    });

    add_task(async function test_write_append_no_create() {
      const fileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_write_append_no_create.tmp");

      await Assert.rejects(
        IOUtils.write(fileName, Uint8Array.of(5, 6, 7, 8, 9), { mode: "append" }),
        /NotFoundError: Could not open the file at .*/
      );
    });
  </script>
</head>

<body>
  <p id="display"></p>
  <div id="content" style="display: none"></div>
  <pre id="test"></pre>
</body>

</html>