Files
tubestation/browser/components/backup/tests/xpcshell/head.js
Mike Conley 660509b6b4 Bug 1897498 - Add a mechanism for exporting backup snapshots to a single file archive. r=Gijs,valentin,backup-reviewers,frontend-codestyle-reviewers,Standard8,kpatenio
There are a number of interesting things going on this patch that I think are worth highlighting
here for my reviewers:

1. The single-file archive format is an HTML file that uses an inlined multipart/mixed MIME
   message within a HTML document comment in order to embed the backup data into the archive.
2. We use the multipart/mixed nsIStreamConverter to extract the JSON and binary data from
   the MIME block.
3. We use a Archive Worker to do the archive creation, allowing us to do the work of construction
   off of the main thread.
4. The Archive Worker is only parsing the header and getting the byte offset of the MIME block.
   Extraction is happening in the parent process. This is mainly for simplicity for now, since
   the Archive Worker cannot invoke an nsIStreamConverter. Down the line, if we determine that
   we'd prefer the Archive Worker do the base64 decoding off of the main thread, we may need
   to use a Message Channel to send the byte sfrom the nsIStreamConverter to it, and add
   stream-writing support to IOUtils so that the Archive Worker can take care of sending the
   decoded bytes to disk.
5. The patch doesn't expose the extraction mechanism in any way except through the debug
   interface right now. That will come down the line. In the meantime, this mechanism
   can be manually tested in the debug interface by creating a backup, which should also
   create an "archive.html" file in the backups folder. Using the "Extract from archive"
   button in the debug tool will let you select that HTML file and extract the ZIP as
   a file in the backups folder called "extraction.zip".
6. The test template contains Unicode characters because certain locales might involve
   us writing Unicode characters in the HTML template when generating the archive. The
   fun part about that is calculating where the byte offset is for the MIME block! See
   the comment in the Archive.worker.mjs script for how that works.

Differential Revision: https://phabricator.services.mozilla.com/D211588
2024-06-11 13:05:58 +00:00

218 lines
6.1 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
"use strict";
const { BackupService } = ChromeUtils.importESModule(
"resource:///modules/backup/BackupService.sys.mjs"
);
const { BackupResource } = ChromeUtils.importESModule(
"resource:///modules/backup/BackupResource.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const { Sqlite } = ChromeUtils.importESModule(
"resource://gre/modules/Sqlite.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
const BYTES_IN_KB = 1000;
do_get_profile();
/**
* Some fake backup resource classes to test with.
*/
class FakeBackupResource1 extends BackupResource {
static get key() {
return "fake1";
}
static get requiresEncryption() {
return false;
}
}
/**
* Another fake backup resource class to test with.
*/
class FakeBackupResource2 extends BackupResource {
static get key() {
return "fake2";
}
static get requiresEncryption() {
return false;
}
static get priority() {
return 1;
}
}
/**
* Yet another fake backup resource class to test with.
*/
class FakeBackupResource3 extends BackupResource {
static get key() {
return "fake3";
}
static get requiresEncryption() {
return false;
}
static get priority() {
return 2;
}
}
/**
* Create a file of a given size in kilobytes.
*
* @param {string} path the path where the file will be created.
* @param {number} sizeInKB size file in Kilobytes.
* @returns {Promise<undefined>}
*/
async function createKilobyteSizedFile(path, sizeInKB) {
let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB);
await IOUtils.write(path, bytes);
}
/**
* @typedef {object} TestFileObject
* @property {(string|Array.<string>)} path
* The relative path of the file. It can be a string or an array of strings
* in the event that directories need to be created. For example, this is
* an array of valid TestFileObjects.
*
* [
* { path: "file1.txt" },
* { path: ["dir1", "file2.txt"] },
* { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 },
* { path: "file4.txt" },
* ]
*
* @property {number} [sizeInKB=10]
* The size of the created file in kilobytes. Defaults to 10.
*/
/**
* Easily creates a series of test files and directories under parentPath.
*
* @param {string} parentPath
* The path to the parent directory where the files will be created.
* @param {TestFileObject[]} testFilesArray
* An array of TestFileObjects describing what test files to create within
* the parentPath.
* @see TestFileObject
* @returns {Promise<undefined>}
*/
async function createTestFiles(parentPath, testFilesArray) {
for (let { path, sizeInKB } of testFilesArray) {
if (Array.isArray(path)) {
// Make a copy of the array of path elements, chopping off the last one.
// We'll assume the unchopped items are directories, and make sure they
// exist first.
let folders = path.slice(0, -1);
await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders));
}
if (sizeInKB === undefined) {
sizeInKB = 10;
}
// This little piece of cleverness coerces a string into an array of one
// if path is a string, or just leaves it alone if it's already an array.
let filePath = PathUtils.join(parentPath, ...[].concat(path));
await createKilobyteSizedFile(filePath, sizeInKB);
}
}
/**
* Checks that files exist within a particular folder. The filesize is not
* checked.
*
* @param {string} parentPath
* The path to the parent directory where the files should exist.
* @param {TestFileObject[]} testFilesArray
* An array of TestFileObjects describing what test files to search for within
* parentPath.
* @see TestFileObject
* @returns {Promise<undefined>}
*/
async function assertFilesExist(parentPath, testFilesArray) {
for (let { path } of testFilesArray) {
let copiedFileName = PathUtils.join(parentPath, ...[].concat(path));
Assert.ok(
await IOUtils.exists(copiedFileName),
`${copiedFileName} should exist in the staging folder`
);
}
}
/**
* Remove a file or directory at a path if it exists and files are unlocked.
*
* @param {string} path path to remove.
*/
async function maybeRemovePath(path) {
try {
await IOUtils.remove(path, { ignoreAbsent: true, recursive: true });
} catch (error) {
// Sometimes remove() throws when the file is not unlocked soon
// enough.
if (error.name != "NS_ERROR_FILE_IS_LOCKED") {
// Ignoring any errors, as the temp folder will be cleaned up.
console.error(error);
}
}
}
/**
* A generator function for deterministically generating a sequence of
* pseudo-random numbers between 0 and 255 with a fixed seed. This means we can
* generate an arbitrary amount of nonsense information, but that generation
* will be consistent between test runs. It's definitely not a cryptographically
* secure random number generator! Please don't use it for that!
*
* @yields {number}
* The next number in the sequence.
*/
function* seededRandomNumberGenerator() {
// This is a verbatim copy of the public domain-licensed code in
// https://github.com/bryc/code/blob/master/jshash/PRNGs.md for the sfc32
// PRNG (see https://pracrand.sourceforge.net/RNG_engines.txt)
let sfc32 = function (a, b, c, d) {
return function () {
a |= 0;
b |= 0;
c |= 0;
d |= 0;
var t = (((a + b) | 0) + d) | 0;
d = (d + 1) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
};
// The seeds don't need to make sense, they just need to be the same from
// test run to test run to give us a consistent stream of nonsense.
const SEED1 = 123;
const SEED2 = 456;
const SEED3 = 789;
const SEED4 = 101;
let generator = sfc32(SEED1, SEED2, SEED3, SEED4);
while (true) {
yield Math.round(generator() * 1000) % 255;
}
}