Bug 1600053 - [remote] Implement IO.read. r=remote-protocol-reviewers,baku,ato,maja_zf

This patch implements the IO.read() method to allow
reading streams for files and blobs.

Therefor all the methods in the IO domain need a registry
for streams. Those have to be stored globally because
they need to be kept existent across different client
connections.

Differential Revision: https://phabricator.services.mozilla.com/D55968

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Henrik Skupin 2019-12-16 16:17:59 +00:00
Родитель 162084495c
Коммит 5a966347fc
8 изменённых файлов: 316 добавлений и 0 удалений

Просмотреть файл

@ -16,6 +16,7 @@ XPCOMUtils.defineLazyModuleGetters(ParentProcessDomains, {
Browser: "chrome://remote/content/domains/parent/Browser.jsm",
Emulation: "chrome://remote/content/domains/parent/Emulation.jsm",
Input: "chrome://remote/content/domains/parent/Input.jsm",
IO: "chrome://remote/content/domains/parent/IO.jsm",
Network: "chrome://remote/content/domains/parent/Network.jsm",
Page: "chrome://remote/content/domains/parent/Page.jsm",
Security: "chrome://remote/content/domains/parent/Security.jsm",

Просмотреть файл

@ -0,0 +1,96 @@
/* 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 http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["IO", "streamRegistry"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
OS: "resource://gre/modules/osfile.jsm",
});
const { Domain } = ChromeUtils.import(
"chrome://remote/content/domains/Domain.jsm"
);
const { StreamRegistry } = ChromeUtils.import(
"chrome://remote/content/StreamRegistry.jsm"
);
const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
// Global singleton for managing open streams
const streamRegistry = new StreamRegistry();
class IO extends Domain {
// commands
/**
* Read a chunk of the stream.
*
* @param {Object} options
* @param {string} options.handle
* Handle of the stream to read.
* @param {number=} options.offset
* Seek to the specified offset before reading. If not specificed,
* proceed with offset following the last read.
* Some types of streams may only support sequential reads.
* @param {number=} options.size
* Maximum number of bytes to read (left upon the agent
* discretion if not specified).
*
* @return {string, boolean, boolean}
* Data that were read, including flags for base64-encoded, and end-of-file reached.
*/
async read(options = {}) {
const { handle, offset, size } = options;
if (typeof handle != "string") {
throw new TypeError(`handle: string value expected`);
}
const stream = streamRegistry.get(handle);
const fileInfo = await stream.stat();
if (typeof offset != "undefined") {
if (typeof offset != "number") {
throw new TypeError(`offset: integer value expected`);
}
// To keep compatibility with Chrome clip invalid offsets
const seekTo = Math.max(0, Math.min(offset, fileInfo.size));
await stream.setPosition(seekTo, OS.File.POS_START);
}
const curPos = await stream.getPosition();
const remainingBytes = fileInfo.size - curPos;
let chunkSize;
if (typeof size != "undefined") {
if (typeof size != "number") {
throw new TypeError(`size: integer value expected`);
}
// Chromium currently crashes for negative sizes (https://bit.ly/2P6h0Fv),
// but might behave similar to the offset and clip invalid values
chunkSize = Math.max(0, Math.min(size, remainingBytes));
} else {
chunkSize = Math.min(DEFAULT_CHUNK_SIZE, remainingBytes);
}
const bytes = await stream.read(chunkSize);
// Each UCS2 character has an upper byte of 0 and a lower byte matching
// the binary data
const data = btoa(String.fromCharCode.apply(null, bytes));
return {
data,
base64Encoded: true,
eof: remainingBytes - bytes.length == 0,
};
}
}

Просмотреть файл

@ -50,6 +50,7 @@ remote.jar:
content/domains/parent/Browser.jsm (domains/parent/Browser.jsm)
content/domains/parent/Emulation.jsm (domains/parent/Emulation.jsm)
content/domains/parent/Input.jsm (domains/parent/Input.jsm)
content/domains/parent/IO.jsm (domains/parent/IO.jsm)
content/domains/parent/Network.jsm (domains/parent/Network.jsm)
content/domains/parent/network/ChannelEventSink.jsm (domains/parent/network/ChannelEventSink.jsm)
content/domains/parent/network/NetworkObserver.jsm (domains/parent/network/NetworkObserver.jsm)

Просмотреть файл

@ -3,6 +3,8 @@
"use strict";
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { RemoteAgentError } = ChromeUtils.import(
"chrome://remote/content/Error.jsm"
);
@ -190,3 +192,52 @@ function timeoutPromise(ms) {
function fail(message) {
ok(false, message);
}
/**
* Create a file with the specified contents.
*
* @param {string} contents
* Contents of the file.
* @param {Object} options
* @param {string=} options.path
* Path of the file. Defaults to the temporary directory.
* @param {boolean=} options.remove
* If true, automatically remove the file after the test. Defaults to true.
*
* @return {Promise}
* @resolves {string}
* Returns the final path of the created file.
*/
async function createFile(contents, options = {}) {
let { path = null, remove = true } = options;
if (!path) {
const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.txt");
const { file, path: tmpPath } = await OS.File.openUnique(basePath, {
humanReadable: true,
});
await file.close();
path = tmpPath;
}
let encoder = new TextEncoder();
let array = encoder.encode(contents);
const count = await OS.File.writeAtomic(path, array, {
encoding: "utf-8",
tmpPath: path + ".tmp",
});
is(count, contents.length, "All data has been written to file");
const file = await OS.File.open(path);
// Automatically remove the file once the test has finished
if (remove) {
registerCleanupFunction(async () => {
await file.close();
await OS.File.remove(path, { ignoreAbsent: true });
});
}
return { file, path };
}

Просмотреть файл

@ -0,0 +1,10 @@
[DEFAULT]
tags = remote
subsuite = remote
prefs = remote.enabled=true
support-files =
head.js
!/remote/test/browser/chrome-remote-interface.js
!/remote/test/browser/head.js
[browser_read.js]

Просмотреть файл

@ -0,0 +1,130 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function seekByOffsets({ IO }) {
const contents = "Lorem ipsum";
const { handle } = await registerFileStream(contents);
for (const offset of [0, 5, 10, 100, 0, -1]) {
const result = await IO.read({ handle, offset });
ok(result.base64Encoded, `Data for offset ${offset} is base64 encoded`);
ok(result.eof, `All data has been read for offset ${offset}`);
is(
atob(result.data),
contents.substring(offset >= 0 ? offset : 0),
`Found expected data for offset ${offset}`
);
}
});
add_task(async function remembersOffsetAfterRead({ IO }) {
const contents = "Lorem ipsum";
const { handle } = await registerFileStream(contents);
let expectedOffset = 0;
const size = 3;
do {
const result = await IO.read({ handle, size });
is(
atob(result.data),
contents.substring(expectedOffset, expectedOffset + size),
`Found expected data for expectedOffset ${expectedOffset}`
);
ok(
result.base64Encoded,
`Data up to expected offset ${expectedOffset} is base64 encoded`
);
is(
result.eof,
expectedOffset + size >= contents.length,
`All data has been read up to expected offset ${expectedOffset}`
);
expectedOffset = Math.min(expectedOffset + size, contents.length);
} while (expectedOffset < contents.length);
});
add_task(async function readBySize({ IO }) {
const contents = "Lorem ipsum";
const { handle } = await registerFileStream(contents);
for (const size of [0, 5, 10, 100, 0, -1]) {
const result = await IO.read({ handle, offset: 0, size });
ok(result.base64Encoded, `Data for size ${size} is base64 encoded`);
is(
result.eof,
size >= contents.length,
`All data has been read for size ${size}`
);
is(
atob(result.data),
contents.substring(0, size),
`Found expected data for size ${size}`
);
}
});
add_task(async function unknownHandle({ IO }) {
const handle = "1000000";
try {
await IO.read({ handle });
ok(false, "Read shouldn't pass");
} catch (e) {
ok(
e.message.startsWith(`Invalid stream handle`),
"Error contains expected message"
);
}
});
add_task(async function invalidHandleTypes({ IO }) {
for (const handle of [null, true, 1, [], {}]) {
try {
await IO.read({ handle });
ok(false, "Read shouldn't pass");
} catch (e) {
ok(
e.message.startsWith(`handle: string value expected`),
"Error contains expected message"
);
}
}
});
add_task(async function invalidOffsetTypes({ IO }) {
const contents = "Lorem ipsum";
const { handle } = await registerFileStream(contents);
for (const offset of [null, true, "1", [], {}]) {
try {
await IO.read({ handle, offset });
ok(false, "Read shouldn't pass");
} catch (e) {
ok(
e.message.startsWith(`offset: integer value expected`),
"Error contains expected message"
);
}
}
});
add_task(async function invalidSizeTypes({ IO }) {
const contents = "Lorem ipsum";
const { handle } = await registerFileStream(contents);
for (const size of [null, true, "1", [], {}]) {
try {
await IO.read({ handle, size });
ok(false, "Read shouldn't pass");
} catch (e) {
ok(
e.message.startsWith(`size: integer value expected`),
"Error contains expected message"
);
}
}
});

Просмотреть файл

@ -0,0 +1,26 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* import-globals-from ../head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/remote/test/browser/head.js",
this
);
const { streamRegistry } = ChromeUtils.import(
"chrome://remote/content/domains/parent/IO.jsm"
);
async function registerFileStream(contents, options = {}) {
// Any file as registered with the stream registry will be automatically
// deleted during the shutdown of Firefox.
options.remove = false;
const { file, path } = await createFile(contents, options);
const handle = streamRegistry.add(file);
return { handle, path };
}

Просмотреть файл

@ -7,6 +7,7 @@ BROWSER_CHROME_MANIFESTS += [
"browser/browser.ini",
"browser/emulation/browser.ini",
"browser/input/browser.ini",
"browser/io/browser.ini",
"browser/network/browser.ini",
"browser/page/browser.ini",
"browser/runtime/browser.ini",