summaryrefslogtreecommitdiffstats
path: root/dom/system/tests/ioutils/test_ioutils_read_write_utf8.html
blob: 196f5d4862cd3de9bafad67d51cb6f84bd6c2472 (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
<!-- 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");

    // This is an impossible sequence of bytes in an UTF-8 encoded file.
    // See section 3.5.3 of this text:
    // https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
    const invalidUTF8 = Uint8Array.of(0xfe, 0xfe, 0xff, 0xff);

    add_task(async function test_read_utf8_failure() {
      info("Test attempt to read non-existent file (UTF8)");
      const doesNotExist = PathUtils.join(PathUtils.tempDir, "does_not_exist.tmp");
      await Assert.rejects(
        IOUtils.readUTF8(doesNotExist),
        /Could not open the file at .*/,
        "IOUtils::readUTF8 rejects when file does not exist"
      );

      info("Test attempt to read invalid UTF-8");
      const invalidUTF8File = PathUtils.join(PathUtils.tempDir, "invalid_utf8.tmp");

      // Deliberately write the invalid byte sequence to file.
      await IOUtils.write(invalidUTF8File, invalidUTF8);

      await Assert.rejects(
        IOUtils.readUTF8(invalidUTF8File),
        /Could not read file\(.*\) because it is not UTF-8 encoded/,
        "IOUtils::readUTF8 will reject when reading a file that is not valid UTF-8"
      );

      await cleanup(invalidUTF8File);
    });

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

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

      const bytesWritten = await IOUtils.writeUTF8(
        tmpFileName,
        newContents,
        { mode: "overwrite" }
      );
      is(
        bytesWritten,
        newContents.length,
        "IOUtils::writeUTF8 can overwrite files if specified"
      );
      ok(
        await fileHasTextContents(tmpFileName, newContents),
        "IOUtils::writeUTF8 overwrites with the expected contents"
      );

      await cleanup(tmpFileName);
    });

    add_task(async function test_write_with_backup() {
      info("Test backup file option with non-existing file");
      let fileContents = "Original file contents";
      let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_option.tmp");
      let backupFileName = destFileName + ".backup";
      let bytesWritten =
        await IOUtils.writeUTF8(destFileName, fileContents, {
          backupFile: backupFileName,
        });
      ok(
        await fileHasTextContents(destFileName, "Original file contents"),
        "IOUtils::writeUTF8 creates a new file with the correct contents"
      );
      ok(
        !await fileExists(backupFileName),
        "IOUtils::writeUTF8 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 file contents";
      ok(await fileExists(destFileName), `Expected ${destFileName} to exist`);
      bytesWritten =
        await IOUtils.writeUTF8(destFileName, newFileContents, {
          backupFile: backupFileName,
        });
      ok(
        await fileHasTextContents(backupFileName, "Original file contents"),
        "IOUtils::writeUTF8 can backup an existing file before writing"
      );
      ok(
        await fileHasTextContents(destFileName, "New file contents"),
        "IOUtils::writeUTF8 can create the target with the correct contents"
      );
      is(
        bytesWritten,
        newFileContents.length,
        "IOUtils::writeUTF8 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 = "Original file contents";
      let destFileName = PathUtils.join(PathUtils.tempDir, "test_write_utf8_with_backup_and_tmp_options.tmp");
      let backupFileName = destFileName + ".backup";
      let tmpFileName = PathUtils.join(PathUtils.tempDir, "temp_file.tmp");
      let bytesWritten =
        await IOUtils.writeUTF8(destFileName, fileContents, {
          backupFile: backupFileName,
          tmpPath: tmpFileName,
        });
      ok(!await fileExists(tmpFileName), "IOUtils::writeUTF8 cleans up the tmpFile");
      ok(
        !await fileExists(backupFileName),
        "IOUtils::writeUTF8 does not create a backup if the target file does not exist"
      );
      ok(
        await fileHasTextContents(destFileName, "Original file contents"),
        "IOUtils::writeUTF8 can write to the destination when a temporary file is used"
      );
      is(
        bytesWritten,
        fileContents.length,
        "IOUtils::writeUTF8 can copy tmp file to destination without performing a backup"
      );

      info("Test backup with tmp and backup file options, existing destination");
      let newFileContents = "New file contents";
      bytesWritten =
        await IOUtils.writeUTF8(destFileName, newFileContents, {
          backupFile: backupFileName,
          tmpPath: tmpFileName,
        });

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

      await cleanup(destFileName, backupFileName);
    });

    add_task(async function test_empty_read_and_write_utf8() {
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_empty_utf8.tmp");
      const emptyString = ""
      const bytesWritten = await IOUtils.writeUTF8(
        tmpFileName,
        emptyString
      );
      is(bytesWritten, 0, "IOUtils::writeUTF8 can create an empty file");

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

      await cleanup(tmpFileName);
    });

    add_task(async function test_full_read_and_write_utf8() {
      // Write a file.
      info("Test writing emoji file");
      const tmpFileName = PathUtils.join(PathUtils.tempDir, "test_ioutils_emoji.tmp");

      // Make sure non-ASCII text is supported for writing and reading back.
      // For fun, a sampling of space-separated emoji characters from different
      // Unicode versions, including multi-byte glyphs that are rendered using
      // ZWJ sequences.
      const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
      const expectedBytes = 71;
      const bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji);
      is(
        bytesWritten,
        expectedBytes,
        "IOUtils::writeUTF8 can write emoji to file"
      );

      // Read it back.
      info("Test reading emoji from file");
      let fileContents = await IOUtils.readUTF8(tmpFileName);
      ok(
        emoji == fileContents &&
        emoji.length == fileContents.length,
        "IOUtils::readUTF8 can read back entire file"
      );

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

    add_task(async function test_write_utf8_relative_path() {
      const tmpFileName = "test_ioutils_write_utf8_relative_path.tmp";

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

    add_task(async function test_read_utf8_relative_path() {
      const tmpFileName = "test_ioutils_read_utf8_relative_path.tmp";

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


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

      info("Test writing lz4 encoded UTF-8 string");
      const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
      let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true });
      is(bytesWritten, 83, "Expected to write 64 bytes");

      info("Test reading lz4 encoded UTF-8 string");
      let readData = await IOUtils.readUTF8(tmpFileName, { decompress: true });
      is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data");

      info("Test writing lz4 compressed UTF-8 string");
      const lotsOfCoffee = new Array(24).fill("☕️").join(""); // ☕️ is 3 bytes in UTF-8: \0xe2 \0x98 \0x95
      bytesWritten = await IOUtils.writeUTF8(tmpFileName, lotsOfCoffee, { compress: true });
      console.log(bytesWritten);
      is(bytesWritten, 28, "Expected 72 bytes to compress to 28 bytes");

      info("Test reading lz4 encoded UTF-8 string");
      readData = await IOUtils.readUTF8(tmpFileName, { decompress: true });
      is(readData, lotsOfCoffee, "IOUtils can write and read back UTF-8 LZ4 compressed data");

      info("Test writing empty lz4 compressed UTF-8 string")
      const empty = "";
      bytesWritten = await IOUtils.writeUTF8(tmpFileName, empty, { compress: true });
      is(bytesWritten, 12, "Expected to write just the LZ4 header");

      info("Test reading empty lz4 compressed UTF-8 string")
      const readEmpty = await IOUtils.readUTF8(tmpFileName, { decompress: true });
      is(readEmpty, empty, "IOUtils can write and read back empty buffers with LZ4");
      const readEmptyRaw = await IOUtils.readUTF8(tmpFileName, { decompress: false });
      is(readEmptyRaw.length, 12, "Expected to read back just the LZ4 header");

      await cleanup(tmpFileName);
    });

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

      info("readUTF8 ignores the maxBytes option if provided");
      const emoji = "☕️ ⚧️ 😀 🖖🏿 🤠 🏳️‍🌈 🥠 🏴‍☠️ 🪐";
      let bytesWritten = await IOUtils.writeUTF8(tmpFileName, emoji, { compress: true });
      is(bytesWritten, 83, "Expected to write 83 bytes");

      let readData = await IOUtils.readUTF8(tmpFileName, { maxBytes: 4, decompress: true });
      is(readData, emoji, "IOUtils can write and read back UTF-8 LZ4 encoded data");

      await cleanup(tmpFileName)
    });

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

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

      await Assert.rejects(
        IOUtils.readUTF8(tmpFileName, { decompress: true }),
        /Could not decompress file because it has an invalid LZ4 header \(wrong magic number: .*\)/,
        "IOUtils::readUTF8 fails to decompress LZ4 data with a bad header"
      );

      info("Test UTF-8 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.readUTF8(tmpFileName, { decompress: true }),
        /Could not decompress file because the buffer is too short/,
        "IOUtils::readUTF8 fails to decompress LZ4 data with missing header"
      );

      info("Test UTF-8 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.readUTF8(tmpFileName, { decompress: true }),
        /Could not decompress file contents, the file may be corrupt/,
        "IOUtils::readUTF8 fails to read corrupt LZ4 contents with a correct header"
      );

      info("Testing decompression of an empty file (no header)");
      {
        const n = await IOUtils.writeUTF8(tmpFileName, "");
        ok(n === 0, "Overwrote with empty file");
      }
      await Assert.rejects(
        IOUtils.readUTF8(tmpFileName, { decompress: true }),
        /Could not decompress file because the buffer is too short/,
        "IOUtils::readUTF8 fails to decompress empty files"
      );

      await cleanup(tmpFileName);
    });
  </script>
</head>

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

</html>