TabFS/extension/background.js

397 lines
11 KiB
JavaScript

const unix = {
EPERM: 1,
ENOENT: 2,
ESRCH: 3,
EINTR: 4,
EIO: 5,
ENXIO: 6,
ENOTSUP: 45,
// Unix file types
S_IFMT: 0170000, // type of file mask
S_IFIFO: 010000, // named pipe (fifo)
S_IFCHR: 020000, // character special
S_IFDIR: 040000, // directory
S_IFBLK: 060000, // block special
S_IFREG: 0100000, // regular
S_IFLNK: 0120000, // symbolic link
S_IFSOCK: 0140000, // socket
}
function UnixError(error) {
this.name = "UnixError";
this.error = error;
}
UnixError.prototype = Error.prototype;
async function debugTab(tabId) {
if (!debugged[tabId]) {
await new Promise(resolve => chrome.debugger.attach({tabId}, "1.3", resolve));
debugged[tabId] = 0;
}
debugged[tabId] += 1;
}
function sendDebuggerCommand(tabId, method, commandParams) {
return new Promise((resolve, reject) =>
chrome.debugger.sendCommand({tabId}, method, commandParams, result => {
console.log(method, result);
if (result) {
resolve(result);
} else {
reject(chrome.runtime.lastError);
}
})
);
}
// tabs/by-id/ID/title
// tabs/by-id/ID/url
// tabs/by-id/ID/console
// tabs/by-id/ID/mem (?)
// tabs/by-id/ID/cpu (?)
// tabs/by-id/ID/screenshot.png
// tabs/by-id/ID/text.txt
// tabs/by-id/ID/printed.pdf
// tabs/by-id/ID/control
// tabs/by-id/ID/sources/
function pathComponent(path, i) {
const components = path.split('/');
return components[i >= 0 ? i : components.length + i];
}
function sanitize(s) {
return s.replace(/[^A-Za-z0-9_\-\.]/gm, '_');
}
const debugged = {};
const router = {
"tabs": {
/* "last-focused": {
* // FIXME: symlink to tab by id.
* async readlink() {
* return "../windows/last-focused/selected-tab"
* }
* },
*/
"by-title": {
async readdir() {
const tabs = await browser.tabs.query({});
return tabs.map(tab => sanitize(String(tab.title).slice(0, 200)) + "_" + String(tab.id));
},
"*": {
async getattr(path) {
const st_size = (await this.readlink(path)).length + 1;
return {
st_mode: unix.S_IFLNK | 0444,
st_nlink: 1,
// You _must_ return correct linkee path length from getattr!
st_size
};
},
async readlink(path) {
const parts = path.split("_");
const id = parts[parts.length - 1];
return "../by-id/" + id;
}
}
},
"by-id": {
async readdir() {
const tabs = await browser.tabs.query({});
return tabs.map(tab => String(tab.id));
},
"*": {
"url": {
async read(path, fh, size, offset) {
const tab = await browser.tabs.get(parseInt(pathComponent(path, -2)));
return (tab.url + "\n").substr(offset, size);
}
},
"title": {
async read(path, fh, size, offset) {
const tab = await browser.tabs.get(parseInt(pathComponent(path, -2)));
return (tab.title + "\n").substr(offset, size);
}
},
"text": {
async read(path, fh, size, offset) {
const tabId = parseInt(pathComponent(path, -2));
const {result} = await chrome.tabs.executeScript(tabId, {code: "console.log('hi'); document.body.innerText;"});
/* await debugTab(tabId);
* await sendDebuggerCommand(tabId, "Runtime.enable", {});
* const {result} = await sendDebuggerCommand(tabId, "Runtime.evaluate", {expression: "document.body.innerText", returnByValue: true});*/
return result.value.substr(offset, size)
}
},
"snapshot.mhtml": {
async read(path, fh, size, offset) {
const tabId = parseInt(pathComponent(path, -2));
await debugTab(tabId);
await sendDebuggerCommand(tabId, "Page.enable", {});
const {data} = await sendDebuggerCommand(tabId, "Page.captureSnapshot");
return data.substr(offset, size)
}
},
"screenshot.png": {
// Broken. Filesystem hangs (? in JS?) and needs to be killed if you read this.
async read(path, fh, size, offset) {
const tabId = parseInt(pathComponent(path, -2));
await debugTab(tabId);
await sendDebuggerCommand(tabId, "Page.enable", {});
const {data} = await sendDebuggerCommand(tabId, "Page.captureScreenshot");
const buf = btoa(atob(data).substr(offset, size));
return { buf, base64Encoded: true };
}
},
"resources": {
async opendir(path) {
const tabId = parseInt(pathComponent(path, -2));
await debugTab(tabId);
return 0;
},
async readdir(path) {
const tabId = parseInt(pathComponent(path, -2));
if (!debugged[tabId]) throw new UnixError(unix.EIO);
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
return frameTree.resources.map(r => sanitize(String(r.url).slice(0, 200)));
},
async releasedir(path) {
return 0;
},
"*": {
async read(path, fh, size, offset) {
const tabId = parseInt(pathComponent(path, -3));
const suffix = pathComponent(path, -1);
if (!debugged[tabId]) throw new UnixError(unix.EIO);
await sendDebuggerCommand(tabId, "Page.enable", {});
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
for (let resource of frameTree.resources) {
const resourceSuffix = sanitize(String(resource.url).slice(0, 200));
if (resourceSuffix === suffix) {
let {base64Encoded, content} = await sendDebuggerCommand(tabId, "Page.getResourceContent", {
frameId: frameTree.frame.id,
url: resource.url
});
if (base64Encoded) {
const buf = btoa(atob(content).substr(offset, size));
return { buf, base64Encoded: true };
}
return content.substr(offset, size);
}
}
throw new UnixError(unix.ENOENT);
}
}
},
"control": {
async write(path, buf) {
const tabId = parseInt(pathComponent(path, -2));
if (buf.trim() === 'close') {
await new Promise(resolve => chrome.tabs.remove(tabId, resolve));
} else {
throw new UnixError(unix.EIO);
}
}
}
}
}
}
};
function findRoute(path) {
let route = router;
let pathSegments = path.split("/");
if (pathSegments[pathSegments.length - 1].startsWith("._")) {
throw new UnixError(unix.ENOTSUP); // Apple Double file for xattrs
}
for (let segment of pathSegments) {
if (segment === "") continue;
route = route[segment] || route["*"];
if (!route) throw new UnixError(unix.ENOENT);
}
return route;
}
async function getattr(path) {
let route = findRoute(path);
if (route.getattr) {
return route.getattr(path);
} else if (route.read || route.write) {
// default file attrs
return {
st_mode: unix.S_IFREG | ((route.read && 0444) || (route.write && 0222)),
st_nlink: 1,
st_size: 100 // FIXME
};
} else {
// default dir attrs
return {
st_mode: unix.S_IFDIR | 0755,
st_nlink: 3
};
}
}
async function open(path) {
let route = findRoute(path);
if (route.open) return route.open(path);
else return 0; // empty fh
}
async function read(path, fh, size, offset) {
let route = findRoute(path);
if (route.read) return route.read(path, fh, size, offset);
}
async function write(path, buf, offset) {
let route = findRoute(path);
if (route.write) return route.write(path, buf, offset);
}
async function release(path, fh) {
let route = findRoute(path);
if (route.release) return route.release(path, fh);
}
async function readlink(path) {
let route = findRoute(path);
if (route.readlink) return route.readlink(path);
}
async function opendir(path) {
let route = findRoute(path);
if (route.opendir) return route.opendir(path);
else return 0; // empty fh
}
async function readdir(path) {
let route = findRoute(path);
if (route.readdir) return route.readdir(path);
return Object.keys(route);
}
async function releasedir(path) {
let route = findRoute(path);
if (route.releasedir) return route.releasedir(path);
}
function log(...ss) {
console.log(...ss);
}
let port;
async function onMessage(req) {
log('req', req);
let response = { op: req.op, error: unix.EIO };
/* console.time(req.op + ':' + req.path);*/
try {
if (req.op === 'getattr') {
response = {
op: 'getattr',
st_mode: 0,
st_nlink: 0,
st_size: 0,
...(await getattr(req.path))
};
} else if (req.op === 'open') {
response = {
op: 'open',
fh: await open(req.path)
};
} else if (req.op === 'read') {
const ret = await read(req.path, req.fh, req.size, req.offset)
const buf = typeof ret === 'string' ? ret : ret.buf;
response = {
op: 'read',
buf
};
if (ret.base64Encoded) response.base64Encoded = ret.base64Encoded;
} else if (req.op === 'write') {
// FIXME: decide whether base64 should be handled here
// or in a higher layer?
const ret = await write(req.path, atob(req.buf), req.offset)
response = {
op: 'write'
};
} else if (req.op === 'release') {
await release(req.path, req.fh);
response = {
op: 'release'
};
} else if (req.op === 'readlink') {
const buf = await readlink(req.path)
response = {
op: 'readlink',
buf
};
} else if (req.op === 'opendir') {
response = {
op: 'opendir',
fh: await opendir(req.path)
};
} else if (req.op === 'readdir') {
response = {
op: 'readdir',
entries: [".", "..", ...(await readdir(req.path))]
};
} else if (req.op === 'releasedir') {
await releasedir(req.path, req.fh);
response = { op: 'releasedir' };
}
} catch (e) {
console.error(e);
response = {
op: req.op,
error: e instanceof UnixError ? e.error : unix.EIO
}
}
/* console.timeEnd(req.op + ':' + req.path);*/
log('resp', response);
port.postMessage(response);
};
function tryConnect() {
port = chrome.runtime.connectNative('com.rsnous.TabFS');
/* console.log('hello', port);*/
/* updateToolbarIcon();*/
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(p => {log('disconnect', p)});
/* ws = new WebSocket("ws://localhost:8888");
* updateToolbarIcon();
* ws.onopen = ws.onclose = updateToolbarIcon;
* ws.onmessage = onmessage;*/
}
function updateToolbarIcon() {
if (port && port.onMessage) { // OPEN
chrome.browserAction.setBadgeBackgroundColor({color: 'blue'});
chrome.browserAction.setBadgeText({text: 'f'});
} else {
chrome.browserAction.setBadgeBackgroundColor({color: 'red'});
chrome.browserAction.setBadgeText({text: '!'});
}
}
tryConnect();
chrome.browserAction.onClicked.addListener(function() {
tryConnect();
});