2021-04-23 03:31:43 +02:00
|
|
|
// This file is the heart of TabFS. Each route (synthetic file) is
|
2021-08-17 00:47:26 +02:00
|
|
|
// defined by an entry in the Routes object.
|
2021-04-23 03:31:43 +02:00
|
|
|
|
2018-11-12 01:18:21 +01:00
|
|
|
const unix = {
|
|
|
|
EPERM: 1,
|
|
|
|
ENOENT: 2,
|
|
|
|
ESRCH: 3,
|
|
|
|
EINTR: 4,
|
|
|
|
EIO: 5,
|
|
|
|
ENXIO: 6,
|
2019-02-28 08:38:12 +01:00
|
|
|
ENOTSUP: 45,
|
2020-12-25 23:55:35 +01:00
|
|
|
ETIMEDOUT: 110, // FIXME: not on macOS (?)
|
2018-11-12 01:18:21 +01:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2020-12-02 20:58:37 +01:00
|
|
|
class UnixError extends Error {
|
2020-12-02 22:45:20 +01:00
|
|
|
constructor(error) { super(); this.name = "UnixError"; this.error = error; }
|
2018-11-22 11:52:54 +01:00
|
|
|
}
|
|
|
|
|
2021-03-14 23:50:49 +01:00
|
|
|
const sanitize = (function() {
|
|
|
|
// from https://github.com/parshap/node-sanitize-filename/blob/209c39b914c8eb48ee27bcbde64b2c7822fdf3de/index.js
|
|
|
|
|
2021-03-14 23:53:04 +01:00
|
|
|
// I've added ' ' to the list of illegal characters. it's a
|
|
|
|
// decision whether we want to allow spaces in filenames... I think
|
|
|
|
// they're annoying, so I'm sanitizing them out for now.
|
|
|
|
var illegalRe = /[\/\?<>\\:\*\|" ]/g;
|
2021-03-14 23:50:49 +01:00
|
|
|
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
|
|
|
var reservedRe = /^\.+$/;
|
|
|
|
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
|
|
|
var windowsTrailingRe = /[\. ]+$/;
|
|
|
|
|
|
|
|
function sanitize(input, replacement) {
|
|
|
|
if (typeof input !== 'string') {
|
|
|
|
throw new Error('Input must be string');
|
|
|
|
}
|
|
|
|
var sanitized = input
|
|
|
|
.replace(illegalRe, replacement)
|
|
|
|
.replace(controlRe, replacement)
|
|
|
|
.replace(reservedRe, replacement)
|
|
|
|
.replace(windowsReservedRe, replacement)
|
|
|
|
.replace(windowsTrailingRe, replacement);
|
|
|
|
return sanitized.slice(0, 200);
|
|
|
|
}
|
|
|
|
return input => sanitize(input, '_');
|
|
|
|
})();
|
|
|
|
|
2020-12-20 00:22:44 +01:00
|
|
|
const stringToUtf8Array = (function() {
|
|
|
|
const encoder = new TextEncoder("utf-8");
|
|
|
|
return str => encoder.encode(str);
|
|
|
|
})();
|
|
|
|
const utf8ArrayToString = (function() {
|
|
|
|
const decoder = new TextDecoder("utf-8");
|
|
|
|
return utf8 => decoder.decode(utf8);
|
|
|
|
})();
|
2018-11-24 07:15:29 +01:00
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
// global so it can be hot-reloaded
|
|
|
|
window.Routes = {};
|
|
|
|
|
2021-07-27 00:15:21 +02:00
|
|
|
// Helper function: you provide getData and setData functions that define
|
|
|
|
// the contents of an entire file => it returns a proper route handler
|
|
|
|
// object with a full set of file operations that you can put in
|
|
|
|
// `Routes` (so clients can read and write sections of the file, stat
|
|
|
|
// it to get its size and see it show up in ls, etc),
|
2021-07-27 00:03:34 +02:00
|
|
|
const makeRouteWithContents = (function() {
|
2021-03-26 10:21:14 +01:00
|
|
|
const Cache = {
|
|
|
|
// used when you open a file to cache the content we got from the
|
|
|
|
// browser until you close that file. (so we can respond to
|
|
|
|
// individual chunk read() and write() requests without doing a
|
|
|
|
// whole new conversation with the browser and regenerating the
|
|
|
|
// content -- important for taking a screenshot, for instance)
|
|
|
|
store: {}, nextHandle: 0,
|
2021-04-06 22:48:48 +02:00
|
|
|
storeObject(path, object) {
|
2021-03-26 10:21:14 +01:00
|
|
|
const handle = ++this.nextHandle;
|
2021-04-06 22:48:48 +02:00
|
|
|
this.store[handle] = {path, object};
|
2021-03-26 10:21:14 +01:00
|
|
|
return handle;
|
|
|
|
},
|
2021-04-06 22:48:48 +02:00
|
|
|
getObjectForHandle(handle) { return this.store[handle].object; },
|
|
|
|
setObjectForHandle(handle, object) { this.store[handle].object = object; },
|
|
|
|
removeObjectForHandle(handle) { delete this.store[handle]; },
|
|
|
|
setObjectForPath(path, object) {
|
|
|
|
for (let storedObject of Object.values(this.store)) {
|
|
|
|
if (storedObject.path === path) {
|
|
|
|
storedObject.object = object;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-26 10:21:14 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
function toUtf8Array(stringOrArray) {
|
|
|
|
if (typeof stringOrArray == 'string') { return stringToUtf8Array(stringOrArray); }
|
|
|
|
else { return stringOrArray; }
|
|
|
|
}
|
2020-12-19 08:54:36 +01:00
|
|
|
|
2021-07-27 00:03:34 +02:00
|
|
|
const makeRouteWithContents = (getData, setData) => ({
|
2021-03-26 10:21:14 +01:00
|
|
|
// getData: (req: Request U Vars) -> Promise<contentsOfFile: String|Uint8Array>
|
|
|
|
// setData [optional]: (req: Request U Vars, newContentsOfFile: String) -> Promise<>
|
|
|
|
|
|
|
|
// You can override file operations (like `truncate` or `getattr`)
|
|
|
|
// in the returned set if you want different behavior from what's
|
|
|
|
// defined here.
|
|
|
|
|
|
|
|
async getattr(req) {
|
2021-07-23 02:53:17 +02:00
|
|
|
const data = await getData(req);
|
|
|
|
if (typeof data === 'undefined') { throw new UnixError(unix.ENOENT); }
|
2021-03-26 10:21:14 +01:00
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFREG | 0444 | (setData ? 0222 : 0),
|
|
|
|
st_nlink: 1,
|
|
|
|
// you'll want to override this if getData() is slow, because
|
|
|
|
// getattr() gets called a lot more cavalierly than open().
|
2021-07-23 02:53:17 +02:00
|
|
|
st_size: toUtf8Array(data).length
|
2021-03-26 10:21:14 +01:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
// We call getData() once when the file is opened, then cache that
|
|
|
|
// data for all subsequent reads from that application.
|
|
|
|
async open(req) {
|
2021-04-06 22:48:48 +02:00
|
|
|
const data = await getData(req);
|
2021-07-23 02:53:17 +02:00
|
|
|
if (typeof data === 'undefined') { throw new UnixError(unix.ENOENT); }
|
2021-04-06 22:48:48 +02:00
|
|
|
return { fh: Cache.storeObject(req.path, toUtf8Array(data)) };
|
2021-03-26 10:21:14 +01:00
|
|
|
},
|
|
|
|
async read({fh, size, offset}) {
|
2021-04-06 22:48:48 +02:00
|
|
|
return { buf: String.fromCharCode(...Cache.getObjectForHandle(fh).slice(offset, offset + size)) };
|
2021-03-26 10:21:14 +01:00
|
|
|
},
|
|
|
|
async write(req) {
|
|
|
|
const {fh, offset, buf} = req;
|
|
|
|
let arr = Cache.getObjectForHandle(fh);
|
|
|
|
const bufarr = stringToUtf8Array(buf);
|
|
|
|
if (offset + bufarr.length > arr.length) {
|
|
|
|
const newArr = new Uint8Array(offset + bufarr.length);
|
|
|
|
newArr.set(arr.slice(0, Math.min(offset, arr.length)));
|
|
|
|
arr = newArr;
|
|
|
|
Cache.setObjectForHandle(fh, arr);
|
|
|
|
}
|
|
|
|
arr.set(bufarr, offset);
|
|
|
|
// I guess caller should override write() if they want to actually
|
|
|
|
// patch and not just re-set the whole string (for example,
|
|
|
|
// if they want to hot-reload just one function the user modified)
|
|
|
|
await setData(req, utf8ArrayToString(arr)); return { size: bufarr.length };
|
|
|
|
},
|
|
|
|
async release({fh}) { Cache.removeObjectForHandle(fh); return {}; },
|
|
|
|
|
|
|
|
async truncate(req) {
|
|
|
|
let arr = toUtf8Array(await getData(req));
|
|
|
|
if (req.size !== arr.length) {
|
|
|
|
const newArr = new Uint8Array(req.size);
|
|
|
|
newArr.set(arr.slice(0, Math.min(req.size, arr.length)));
|
|
|
|
arr = newArr;
|
|
|
|
}
|
2021-04-06 22:48:48 +02:00
|
|
|
Cache.setObjectForPath(req.path, arr);
|
2021-03-26 10:21:14 +01:00
|
|
|
await setData(req, utf8ArrayToString(arr)); return {};
|
2020-12-23 13:18:26 +01:00
|
|
|
}
|
2021-03-26 10:21:14 +01:00
|
|
|
});
|
2021-07-27 00:03:34 +02:00
|
|
|
makeRouteWithContents.Cache = Cache;
|
|
|
|
return makeRouteWithContents;
|
2021-03-26 10:21:14 +01:00
|
|
|
})();
|
2020-12-12 00:45:19 +01:00
|
|
|
|
2021-07-27 00:15:21 +02:00
|
|
|
// Helper function: returns a route handler for `path` based on all
|
|
|
|
// the children of `path` that already exist in Routes.
|
|
|
|
//
|
|
|
|
// e.g., if `Routes['/tabs/create']` and `Routes['/tabs/by-id']` and
|
|
|
|
// `Routes['/tabs/last-focused']` are all already defined, then
|
|
|
|
// `makeDefaultRouteForDirectory('/tabs')` will return a route that
|
|
|
|
// defines a directory with entries 'create', 'by-id', and
|
|
|
|
// 'last-focused'.
|
2021-07-26 23:56:48 +02:00
|
|
|
function makeDefaultRouteForDirectory(path) {
|
2021-07-23 02:53:17 +02:00
|
|
|
function depth(p) { return p === '/' ? 0 : (p.match(/\//g) || []).length; }
|
|
|
|
|
|
|
|
// find all direct children
|
|
|
|
let entries = Object.keys(Routes)
|
|
|
|
.filter(k => k.startsWith(path) && depth(k) === depth(path) + 1)
|
|
|
|
.map(k => k.substr((path === '/' ? 0 : path.length) + 1).split('/')[0])
|
|
|
|
// exclude entries with variables like :FILENAME in them
|
|
|
|
.filter(k => !k.includes("#") && !k.includes(":"));
|
|
|
|
|
|
|
|
entries = [".", "..", ...new Set(entries)];
|
|
|
|
return { readdir() { return { entries }; }, __isInfill: true };
|
|
|
|
}
|
2021-03-23 07:25:38 +01:00
|
|
|
|
|
|
|
Routes["/tabs/create"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: 'Create a new tab.',
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: 'echo "https://www.google.com" > $0',
|
2021-03-22 03:42:44 +01:00
|
|
|
async write({buf}) {
|
2020-12-20 00:22:44 +01:00
|
|
|
const url = buf.trim();
|
|
|
|
await browser.tabs.create({url});
|
|
|
|
return {size: stringToUtf8Array(buf).length};
|
|
|
|
},
|
2021-03-22 03:42:44 +01:00
|
|
|
async truncate() { return {}; }
|
|
|
|
};
|
2020-12-20 00:22:44 +01:00
|
|
|
|
2021-04-23 03:53:30 +02:00
|
|
|
Routes["/tabs/by-title"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: 'Open tabs, organized by title; each subfolder represents an open tab.',
|
2021-04-23 03:53:30 +02:00
|
|
|
usage: 'ls $0',
|
|
|
|
getattr() {
|
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFDIR | 0777, // writable so you can delete tabs
|
|
|
|
st_nlink: 3,
|
|
|
|
st_size: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
async readdir() {
|
|
|
|
const tabs = await browser.tabs.query({});
|
|
|
|
return { entries: [".", "..", ...tabs.map(tab => sanitize(String(tab.title)) + "." + String(tab.id))] };
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Routes["/tabs/by-title/:TAB_TITLE.#TAB_ID"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Represents one open tab.
|
|
|
|
It's a symbolic link to /tabs/by-id/#TAB_ID.`,
|
2021-04-23 03:53:30 +02:00
|
|
|
// TODO: date
|
|
|
|
usage: ['rm $0'],
|
2021-08-17 00:47:26 +02:00
|
|
|
async readlink({tabId}) {
|
2021-04-23 03:53:30 +02:00
|
|
|
return { buf: "../by-id/" + tabId };
|
|
|
|
},
|
|
|
|
async unlink({tabId}) {
|
|
|
|
await browser.tabs.remove(tabId);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Routes["/tabs/last-focused"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Represents the most recently focused tab.
|
|
|
|
It's a symbolic link to /tabs/by-id/[ID of most recently focused tab].`,
|
2021-04-23 03:53:30 +02:00
|
|
|
async readlink() {
|
|
|
|
const id = (await browser.tabs.query({ active: true, lastFocusedWindow: true }))[0].id;
|
|
|
|
return { buf: "by-id/" + id };
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-04-20 07:05:05 +02:00
|
|
|
Routes["/tabs/by-id"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Open tabs, organized by ID; each subfolder represents an open tab.`,
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: 'ls $0',
|
2020-12-12 00:45:19 +01:00
|
|
|
async readdir() {
|
|
|
|
const tabs = await browser.tabs.query({});
|
2020-12-14 07:02:29 +01:00
|
|
|
return { entries: [".", "..", ...tabs.map(tab => String(tab.id))] };
|
2020-12-12 00:45:19 +01:00
|
|
|
}
|
|
|
|
};
|
2020-12-12 11:36:37 +01:00
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
// TODO: temporarily disabled: make tab directory writable
|
|
|
|
|
|
|
|
// const tabIdDirectory = createWritableDirectory();
|
|
|
|
// Routes["/tabs/by-id/#TAB_ID"] = routeDefer(() => {
|
2021-07-26 23:56:48 +02:00
|
|
|
// const childrenRoute = makeDefaultRouteForDirectory("/tabs/by-id/#TAB_ID");
|
2021-07-23 02:53:17 +02:00
|
|
|
// return {
|
|
|
|
// ...tabIdDirectory.routeForRoot, // so getattr is inherited
|
|
|
|
// async readdir(req) {
|
|
|
|
// const entries =
|
|
|
|
// [...(await tabIdDirectory.routeForRoot.readdir(req)).entries,
|
|
|
|
// ...(await childrenRoute.readdir(req)).entries];
|
|
|
|
// return {entries: [...new Set(entries)]};
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
// });
|
|
|
|
// Routes["/tabs/by-id/#TAB_ID/:FILENAME"] = tabIdDirectory.routeForFilename;
|
|
|
|
|
|
|
|
// TODO: can I trigger 1. nav to Finder and 2. nav to Terminal from toolbar click?
|
|
|
|
|
2020-12-12 00:45:19 +01:00
|
|
|
(function() {
|
2021-07-27 00:03:34 +02:00
|
|
|
const routeForTab = (readHandler, writeHandler) => makeRouteWithContents(async ({tabId}) => {
|
2020-12-12 05:01:11 +01:00
|
|
|
const tab = await browser.tabs.get(tabId);
|
2020-12-15 21:11:41 +01:00
|
|
|
return readHandler(tab);
|
|
|
|
|
2021-03-22 03:42:44 +01:00
|
|
|
}, writeHandler ? async ({tabId}, buf) => {
|
2020-12-15 21:11:41 +01:00
|
|
|
await browser.tabs.update(tabId, writeHandler(buf));
|
|
|
|
} : undefined);
|
2021-03-22 03:42:44 +01:00
|
|
|
|
2021-07-27 00:03:34 +02:00
|
|
|
const routeFromScript = code => makeRouteWithContents(async ({tabId}) => {
|
2020-12-12 05:01:11 +01:00
|
|
|
return (await browser.tabs.executeScript(tabId, {code}))[0];
|
|
|
|
});
|
|
|
|
|
2021-04-20 07:05:05 +02:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/url.txt"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Text file containing the current URL of this tab.`,
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: ['cat $0',
|
|
|
|
'echo "https://www.google.com" > $0'],
|
|
|
|
...routeForTab(tab => tab.url + "\n",
|
|
|
|
buf => ({ url: buf }))
|
|
|
|
};
|
|
|
|
Routes["/tabs/by-id/#TAB_ID/title.txt"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Text file containing the current title of this tab.`,
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: 'cat $0',
|
|
|
|
...routeForTab(tab => tab.title + "\n")
|
|
|
|
};
|
|
|
|
Routes["/tabs/by-id/#TAB_ID/text.txt"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Text file containing the current body text of this tab.`,
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: 'cat $0',
|
|
|
|
...routeFromScript(`document.body.innerText`)
|
|
|
|
};
|
|
|
|
Routes["/tabs/by-id/#TAB_ID/body.html"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: `Text file containing the current body HTML of this tab.`,
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: 'cat $0',
|
|
|
|
...routeFromScript(`document.body.innerHTML`)
|
|
|
|
};
|
2021-02-19 19:02:39 +01:00
|
|
|
|
2021-04-20 07:05:05 +02:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/active"] = {
|
2021-08-17 00:47:26 +02:00
|
|
|
description: 'Text file with `true` or `false` depending on whether this tab is active in its window.',
|
2021-04-20 07:05:05 +02:00
|
|
|
usage: ['cat $0',
|
|
|
|
'echo true > $0'],
|
|
|
|
...routeForTab(
|
|
|
|
tab => JSON.stringify(tab.active) + '\n',
|
|
|
|
// WEIRD: we do startsWith because you might end up with buf
|
|
|
|
// being "truee" (if it was "false", then someone wrote "true")
|
|
|
|
buf => ({ active: buf.startsWith("true") })
|
|
|
|
)
|
|
|
|
};
|
2020-12-12 00:45:19 +01:00
|
|
|
})();
|
2021-07-23 02:53:17 +02:00
|
|
|
function createWritableDirectory() {
|
2021-07-26 23:56:48 +02:00
|
|
|
// Returns a 'writable directory' object, which represents a
|
|
|
|
// writable directory that users can put arbitrary stuff into. It's
|
|
|
|
// not itself a route, but it has .routeForRoot and
|
|
|
|
// .routeForFilename properties that are routes.
|
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
const dir = {};
|
|
|
|
return {
|
|
|
|
directory: dir,
|
|
|
|
routeForRoot: {
|
|
|
|
usage: 'ls $0',
|
|
|
|
async readdir({path}) {
|
|
|
|
// get just last component of keys (filename)
|
|
|
|
return { entries: [".", "..",
|
|
|
|
...Object.keys(dir).map(
|
|
|
|
key => key.substr(key.lastIndexOf("/") + 1)
|
|
|
|
)] };
|
|
|
|
},
|
|
|
|
getattr() {
|
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFDIR | 0777, // writable so you can create/rm evals
|
|
|
|
st_nlink: 3,
|
|
|
|
st_size: 0,
|
|
|
|
};
|
|
|
|
},
|
2021-02-23 00:33:31 +01:00
|
|
|
},
|
2021-07-23 02:53:17 +02:00
|
|
|
routeForFilename: {
|
|
|
|
usage: ['echo "2 + 2" > $0',
|
|
|
|
'cat $0.result'],
|
2021-04-20 07:05:05 +02:00
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
async mknod({path, mode}) {
|
|
|
|
dir[path] = '';
|
|
|
|
return {};
|
|
|
|
},
|
|
|
|
async unlink({path}) {
|
|
|
|
delete dir[path];
|
|
|
|
return {};
|
|
|
|
},
|
|
|
|
|
2021-07-27 00:03:34 +02:00
|
|
|
...makeRouteWithContents(
|
2021-07-23 02:53:17 +02:00
|
|
|
async ({path}) => dir[path],
|
|
|
|
async ({path}, buf) => { dir[path] = buf; }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2021-02-26 00:41:47 +01:00
|
|
|
|
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
(function() {
|
|
|
|
const evals = createWritableDirectory();
|
|
|
|
Routes["/tabs/by-id/#TAB_ID/evals"] = evals.routeForRoot;
|
|
|
|
Routes["/tabs/by-id/#TAB_ID/evals/:FILENAME"] = {
|
|
|
|
...evals.routeForFilename,
|
|
|
|
async write(req) {
|
|
|
|
const ret = await evals.routeForFilename.write(req);
|
|
|
|
const code = evals.directory[req.path];
|
|
|
|
evals.directory[req.path + '.result'] = JSON.stringify((await browser.tabs.executeScript(req.tabId, {code}))[0]) + '\n';
|
|
|
|
return ret;
|
|
|
|
}
|
2021-02-26 00:41:47 +01:00
|
|
|
};
|
|
|
|
})();
|
|
|
|
(function() {
|
|
|
|
const watches = {};
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/watches"] = {
|
2021-03-22 03:42:44 +01:00
|
|
|
async readdir({tabId}) {
|
2021-02-26 00:41:47 +01:00
|
|
|
return { entries: [".", "..", ...Object.keys(watches[tabId] || [])] };
|
|
|
|
},
|
|
|
|
getattr() {
|
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFDIR | 0777, // writable so you can create/rm watches
|
|
|
|
st_nlink: 3,
|
|
|
|
st_size: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
};
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/watches/:EXPR"] = {
|
2021-02-26 00:41:47 +01:00
|
|
|
// NOTE: eval runs in extension's content script, not in original page JS context
|
2021-03-22 03:42:44 +01:00
|
|
|
async mknod({tabId, expr, mode}) {
|
2021-02-26 00:41:47 +01:00
|
|
|
watches[tabId] = watches[tabId] || {};
|
|
|
|
watches[tabId][expr] = async function() {
|
2021-02-23 00:33:31 +01:00
|
|
|
return (await browser.tabs.executeScript(tabId, {code: expr}))[0];
|
|
|
|
};
|
|
|
|
return {};
|
|
|
|
},
|
2021-03-22 03:42:44 +01:00
|
|
|
async unlink({tabId, expr}) {
|
2021-02-26 00:41:47 +01:00
|
|
|
delete watches[tabId][expr]; // TODO: also delete watches[tabId] if empty
|
2021-02-23 01:18:07 +01:00
|
|
|
return {};
|
|
|
|
},
|
2021-02-23 00:33:31 +01:00
|
|
|
|
2021-07-27 00:03:34 +02:00
|
|
|
...makeRouteWithContents(async ({tabId, expr}) => {
|
2021-02-26 00:41:47 +01:00
|
|
|
if (!watches[tabId] || !(expr in watches[tabId])) { throw new UnixError(unix.ENOENT); }
|
|
|
|
return JSON.stringify(await watches[tabId][expr]()) + '\n';
|
2021-03-22 03:42:44 +01:00
|
|
|
|
2021-02-23 01:18:07 +01:00
|
|
|
}, () => {
|
|
|
|
// setData handler -- only providing this so that getattr reports
|
|
|
|
// that the file is writable, so it can be deleted without annoying prompt.
|
|
|
|
throw new UnixError(unix.EPERM);
|
2021-02-23 00:33:31 +01:00
|
|
|
})
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/window"] = {
|
2020-12-26 00:54:53 +01:00
|
|
|
// a symbolic link to /windows/[id for this window]
|
2021-03-22 03:42:44 +01:00
|
|
|
async readlink({tabId}) {
|
|
|
|
const tab = await browser.tabs.get(tabId);
|
2020-12-26 00:54:53 +01:00
|
|
|
return { buf: "../../../windows/" + tab.windowId };
|
|
|
|
}
|
|
|
|
};
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/control"] = {
|
2021-04-23 03:53:30 +02:00
|
|
|
// see https://developer.chrome.com/extensions/tabs
|
|
|
|
usage: ['echo remove > $0',
|
|
|
|
'echo reload > $0',
|
|
|
|
'echo goForward > $0',
|
|
|
|
'echo goBack > $0',
|
|
|
|
'echo discard > $0'],
|
2021-03-22 03:42:44 +01:00
|
|
|
async write({tabId, buf}) {
|
2020-11-06 04:09:44 +01:00
|
|
|
const command = buf.trim();
|
2020-12-15 21:11:41 +01:00
|
|
|
await browser.tabs[command](tabId);
|
2020-12-20 00:22:44 +01:00
|
|
|
return {size: stringToUtf8Array(buf).length};
|
2020-12-20 02:11:59 +01:00
|
|
|
},
|
2021-03-22 03:42:44 +01:00
|
|
|
async truncate({size}) { return {}; }
|
2020-10-28 04:53:39 +01:00
|
|
|
};
|
2020-12-25 22:59:35 +01:00
|
|
|
// debugger/ : debugger-API-dependent (Chrome-only)
|
2020-12-25 22:40:18 +01:00
|
|
|
(function() {
|
2021-01-01 03:00:25 +01:00
|
|
|
if (!chrome.debugger) return;
|
2021-03-22 03:42:44 +01:00
|
|
|
|
|
|
|
async function attachDebugger(tabId) {
|
|
|
|
return new Promise((resolve, reject) => chrome.debugger.attach({tabId}, "1.3", () => {
|
|
|
|
if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); }
|
|
|
|
else { resolve(); }
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
async function detachDebugger(tabId) {
|
|
|
|
return new Promise((resolve, reject) => chrome.debugger.detach({tabId}, () => {
|
|
|
|
if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); }
|
|
|
|
else { resolve(); }
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
const TabManager = (function() {
|
|
|
|
if (chrome.debugger) chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
|
console.log(source, method, params);
|
|
|
|
if (method === "Page.frameStartedLoading") {
|
|
|
|
// we're gonna assume we're always plugged into both Page and Debugger.
|
|
|
|
TabManager.scriptsForTab[source.tabId] = {};
|
|
|
|
|
|
|
|
} else if (method === "Debugger.scriptParsed") {
|
|
|
|
TabManager.scriptsForTab[source.tabId] = TabManager.scriptsForTab[source.tabId] || {};
|
|
|
|
TabManager.scriptsForTab[source.tabId][params.scriptId] = params;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
scriptsForTab: {},
|
|
|
|
debugTab: async function(tabId) {
|
|
|
|
// meant to be higher-level wrapper for raw attach/detach
|
|
|
|
// TODO: could we remember if we're already attached? idk if it's worth it
|
|
|
|
try { await attachDebugger(tabId); }
|
|
|
|
catch (e) {
|
|
|
|
if (e.message.indexOf('Another debugger is already attached') !== -1) {
|
|
|
|
await detachDebugger(tabId);
|
|
|
|
await attachDebugger(tabId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: detach automatically? some kind of reference counting thing?
|
|
|
|
},
|
|
|
|
enableDomainForTab: async function(tabId, domain) {
|
|
|
|
// TODO: could we remember if we're already enabled? idk if it's worth it
|
|
|
|
if (domain === 'Debugger') { TabManager.scriptsForTab[tabId] = {}; }
|
|
|
|
await sendDebuggerCommand(tabId, `${domain}.enable`, {});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
function sendDebuggerCommand(tabId, method, commandParams) {
|
|
|
|
return new Promise((resolve, reject) =>
|
|
|
|
chrome.debugger.sendCommand({tabId}, method, commandParams, result => {
|
|
|
|
if (result) { resolve(result); } else { reject(chrome.runtime.lastError); }
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-01-01 02:33:11 +01:00
|
|
|
// possible idea: console (using Log API instead of monkey-patching)
|
2020-12-25 22:59:35 +01:00
|
|
|
// resources/
|
|
|
|
// TODO: scripts/ TODO: allow creation, eval immediately
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/debugger/resources"] = {
|
2021-03-22 03:42:44 +01:00
|
|
|
async readdir({tabId}) {
|
2020-12-25 22:40:18 +01:00
|
|
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
|
|
|
|
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
|
2021-03-14 23:50:49 +01:00
|
|
|
return { entries: [".", "..", ...frameTree.resources.map(r => sanitize(String(r.url)))] };
|
2020-12-25 22:40:18 +01:00
|
|
|
}
|
|
|
|
};
|
2021-07-27 00:03:34 +02:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/debugger/resources/:SUFFIX"] = makeRouteWithContents(async ({path, tabId, suffix}) => {
|
2020-12-25 22:40:18 +01:00
|
|
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
|
|
|
|
|
|
|
|
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
|
|
|
|
for (let resource of frameTree.resources) {
|
2021-03-14 23:50:49 +01:00
|
|
|
const resourceSuffix = sanitize(String(resource.url));
|
2020-12-25 22:40:18 +01:00
|
|
|
if (resourceSuffix === suffix) {
|
|
|
|
let {base64Encoded, content} = await sendDebuggerCommand(tabId, "Page.getResourceContent", {
|
|
|
|
frameId: frameTree.frame.id,
|
|
|
|
url: resource.url
|
|
|
|
});
|
|
|
|
if (base64Encoded) { return Uint8Array.from(atob(content), c => c.charCodeAt(0)); }
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new UnixError(unix.ENOENT);
|
|
|
|
});
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/debugger/scripts"] = {
|
2021-03-22 03:42:44 +01:00
|
|
|
async opendir({tabId}) {
|
2020-12-25 22:40:18 +01:00
|
|
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
|
|
|
|
return { fh: 0 };
|
|
|
|
},
|
2021-03-22 03:42:44 +01:00
|
|
|
async readdir({tabId}) {
|
|
|
|
// it's useful to put the ID first in the script filenames, so
|
|
|
|
// the .js extension stays on the end
|
2020-12-29 13:27:12 +01:00
|
|
|
const scriptFileNames = Object.values(TabManager.scriptsForTab[tabId])
|
2021-03-14 23:50:49 +01:00
|
|
|
.map(params => params.scriptId + "_" + sanitize(params.url));
|
2020-12-29 13:27:12 +01:00
|
|
|
return { entries: [".", "..", ...scriptFileNames] };
|
2020-12-25 22:40:18 +01:00
|
|
|
}
|
|
|
|
};
|
2021-03-22 03:51:31 +01:00
|
|
|
function pathScriptInfo(tabId, filename) {
|
|
|
|
const [scriptId, ...rest] = filename.split("_");
|
2020-12-29 23:16:25 +01:00
|
|
|
const scriptInfo = TabManager.scriptsForTab[tabId][scriptId];
|
2021-03-14 23:50:49 +01:00
|
|
|
if (!scriptInfo || sanitize(scriptInfo.url) !== rest.join("_")) {
|
2020-12-29 23:16:25 +01:00
|
|
|
throw new UnixError(unix.ENOENT);
|
|
|
|
}
|
|
|
|
return scriptInfo;
|
|
|
|
}
|
2021-07-27 00:03:34 +02:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/debugger/scripts/:FILENAME"] = makeRouteWithContents(async ({tabId, filename}) => {
|
2020-12-25 22:40:18 +01:00
|
|
|
await TabManager.debugTab(tabId);
|
|
|
|
await TabManager.enableDomainForTab(tabId, "Page");
|
|
|
|
await TabManager.enableDomainForTab(tabId, "Debugger");
|
|
|
|
|
2021-03-22 03:51:31 +01:00
|
|
|
const {scriptId} = pathScriptInfo(tabId, filename);
|
2020-12-25 22:40:18 +01:00
|
|
|
const {scriptSource} = await sendDebuggerCommand(tabId, "Debugger.getScriptSource", {scriptId});
|
|
|
|
return scriptSource;
|
|
|
|
|
2021-03-22 03:51:31 +01:00
|
|
|
}, async ({tabId, filename}, buf) => {
|
2020-12-25 22:40:18 +01:00
|
|
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
|
|
|
|
|
2021-03-22 03:51:31 +01:00
|
|
|
const {scriptId} = pathScriptInfo(tabId, filename);
|
2020-12-25 22:40:18 +01:00
|
|
|
await sendDebuggerCommand(tabId, "Debugger.setScriptSource", {scriptId, scriptSource: buf});
|
|
|
|
});
|
|
|
|
})();
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/inputs"] = {
|
2021-03-22 03:51:31 +01:00
|
|
|
async readdir({tabId}) {
|
2021-02-19 20:26:33 +01:00
|
|
|
// TODO: assign new IDs to inputs without them?
|
2021-03-21 23:35:01 +01:00
|
|
|
const code = `Array.from(document.querySelectorAll('textarea, input[type=text]'))
|
|
|
|
.map(e => e.id).filter(id => id)`;
|
2021-01-11 07:55:34 +01:00
|
|
|
const ids = (await browser.tabs.executeScript(tabId, {code}))[0];
|
|
|
|
return { entries: [".", "..", ...ids.map(id => `${id}.txt`)] };
|
|
|
|
}
|
|
|
|
};
|
2021-07-27 00:03:34 +02:00
|
|
|
Routes["/tabs/by-id/#TAB_ID/inputs/:INPUT_ID.txt"] = makeRouteWithContents(async ({tabId, inputId}) => {
|
2021-02-22 05:43:31 +01:00
|
|
|
const code = `document.getElementById('${inputId}').value`;
|
|
|
|
const inputValue = (await browser.tabs.executeScript(tabId, {code}))[0];
|
|
|
|
if (inputValue === null) { throw new UnixError(unix.ENOENT); } /* FIXME: hack to deal with if inputId isn't valid */
|
|
|
|
return inputValue;
|
2021-03-22 03:42:44 +01:00
|
|
|
|
2021-03-22 03:51:31 +01:00
|
|
|
}, async ({tabId, inputId}, buf) => {
|
2021-02-22 05:43:31 +01:00
|
|
|
const code = `document.getElementById('${inputId}').value = unescape('${escape(buf)}')`;
|
2021-01-11 07:55:34 +01:00
|
|
|
await browser.tabs.executeScript(tabId, {code});
|
|
|
|
});
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/windows"] = {
|
2020-12-25 23:55:35 +01:00
|
|
|
async readdir() {
|
|
|
|
const windows = await browser.windows.getAll();
|
|
|
|
return { entries: [".", "..", ...windows.map(window => String(window.id))] };
|
|
|
|
}
|
|
|
|
};
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/windows/last-focused"] = {
|
2021-02-22 05:43:31 +01:00
|
|
|
// a symbolic link to /windows/[id for this window]
|
2021-03-22 03:42:44 +01:00
|
|
|
async readlink() {
|
2021-02-22 05:43:31 +01:00
|
|
|
const windowId = (await browser.windows.getLastFocused()).id;
|
|
|
|
return { buf: windowId };
|
|
|
|
}
|
|
|
|
};
|
2021-02-19 19:32:28 +01:00
|
|
|
(function() {
|
2021-07-27 00:03:34 +02:00
|
|
|
const withWindow = (readHandler, writeHandler) => makeRouteWithContents(async ({windowId}) => {
|
2021-02-19 19:32:28 +01:00
|
|
|
const window = await browser.windows.get(windowId);
|
|
|
|
return readHandler(window);
|
|
|
|
|
2021-03-22 03:42:44 +01:00
|
|
|
}, writeHandler ? async ({windowId}, buf) => {
|
2021-02-19 19:32:28 +01:00
|
|
|
await browser.windows.update(windowId, writeHandler(buf));
|
|
|
|
} : undefined);
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/windows/#WINDOW_ID/focused"] =
|
2021-03-22 03:42:44 +01:00
|
|
|
withWindow(window => JSON.stringify(window.focused) + '\n',
|
|
|
|
buf => ({ focused: buf.startsWith('true') }));
|
2021-02-19 19:32:28 +01:00
|
|
|
})();
|
2021-07-27 00:03:34 +02:00
|
|
|
Routes["/windows/#WINDOW_ID/visible-tab.png"] = { ...makeRouteWithContents(async ({windowId}) => {
|
2021-02-22 05:43:31 +01:00
|
|
|
// screen capture is a window thing and not a tab thing because you
|
2020-12-28 19:41:15 +01:00
|
|
|
// can only capture the visible tab for each window anyway; you
|
|
|
|
// can't take a screenshot of just any arbitrary tab
|
2020-12-25 23:55:35 +01:00
|
|
|
const dataUrl = await browser.tabs.captureVisibleTab(windowId, {format: 'png'});
|
2020-12-26 00:13:04 +01:00
|
|
|
return Uint8Array.from(atob(dataUrl.substr(("data:image/png;base64,").length)),
|
|
|
|
c => c.charCodeAt(0));
|
2020-12-25 23:55:35 +01:00
|
|
|
|
2021-03-22 03:42:44 +01:00
|
|
|
}), async getattr() {
|
2020-12-25 23:55:35 +01:00
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFREG | 0444,
|
|
|
|
st_nlink: 1,
|
|
|
|
st_size: 10000000 // hard-code to 10MB for now
|
|
|
|
};
|
|
|
|
} };
|
|
|
|
|
2020-11-22 20:52:08 +01:00
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/extensions"] = {
|
2020-12-23 01:36:09 +01:00
|
|
|
async readdir() {
|
|
|
|
const infos = await browser.management.getAll();
|
2021-03-22 03:42:44 +01:00
|
|
|
return { entries: [".", "..", ...infos.map(info => `${sanitize(info.name)}.${info.id}`)] };
|
2020-12-23 01:36:09 +01:00
|
|
|
}
|
|
|
|
};
|
2021-07-27 00:03:34 +02:00
|
|
|
Routes["/extensions/:EXTENSION_TITLE.:EXTENSION_ID/enabled"] = { ...makeRouteWithContents(async ({extensionId}) => {
|
2020-12-23 05:34:01 +01:00
|
|
|
const info = await browser.management.get(extensionId);
|
2020-12-23 13:18:26 +01:00
|
|
|
return String(info.enabled) + '\n';
|
2020-12-23 05:34:01 +01:00
|
|
|
|
2021-03-22 03:42:44 +01:00
|
|
|
}, async ({extensionId}, buf) => {
|
2020-12-23 13:18:26 +01:00
|
|
|
await browser.management.setEnabled(extensionId, buf.trim() === "true");
|
2020-12-25 08:46:48 +01:00
|
|
|
|
|
|
|
// suppress truncate so it doesn't accidentally flip the state when you do, e.g., `echo true >`
|
|
|
|
}), truncate() { return {}; } };
|
2020-12-23 01:36:09 +01:00
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes["/runtime/reload"] = {
|
2021-03-22 03:42:44 +01:00
|
|
|
async write({buf}) {
|
2020-12-24 22:49:38 +01:00
|
|
|
await browser.runtime.reload();
|
|
|
|
return {size: stringToUtf8Array(buf).length};
|
|
|
|
},
|
2020-12-25 23:55:35 +01:00
|
|
|
truncate() { return {}; }
|
2020-12-24 22:49:38 +01:00
|
|
|
};
|
|
|
|
|
2021-04-23 01:17:42 +02:00
|
|
|
(function() {
|
2021-04-23 03:53:30 +02:00
|
|
|
// window.__backgroundJS needs to be a global because we want its
|
|
|
|
// value (the changed JS text) to survive even as this whole file
|
|
|
|
// gets re-evaluated.
|
2021-04-23 01:48:01 +02:00
|
|
|
window.__backgroundJS = window.__backgroundJS || false;
|
2021-04-23 01:17:42 +02:00
|
|
|
Object.defineProperty(window, 'backgroundJS', {
|
|
|
|
async get() {
|
2021-04-23 01:48:01 +02:00
|
|
|
if (!window.__backgroundJS) {
|
|
|
|
window.__backgroundJS = await window.fetch(chrome.runtime.getURL('background.js'))
|
2021-04-23 01:39:15 +02:00
|
|
|
.then(r => r.text());
|
|
|
|
}
|
2021-04-23 01:48:01 +02:00
|
|
|
return window.__backgroundJS;
|
2021-04-23 01:17:42 +02:00
|
|
|
},
|
2021-04-23 01:48:01 +02:00
|
|
|
set(js) { window.__backgroundJS = js; },
|
2021-04-23 01:39:15 +02:00
|
|
|
configurable: true // so we can rerun this on hot reload
|
2021-04-23 01:17:42 +02:00
|
|
|
});
|
|
|
|
})();
|
|
|
|
|
2021-07-27 00:03:34 +02:00
|
|
|
Routes["/runtime/routes.html"] = makeRouteWithContents(async () => {
|
2021-04-23 03:53:30 +02:00
|
|
|
// WIP
|
2021-04-23 03:31:43 +02:00
|
|
|
const jsLines = (await window.backgroundJS).split('\n');
|
2021-08-17 01:43:34 +02:00
|
|
|
function findRouteLineRange(path) {
|
|
|
|
console.log('frlr', path);
|
2021-04-23 03:31:43 +02:00
|
|
|
for (let i = 0; i < jsLines.length; i++) {
|
2021-08-17 01:43:34 +02:00
|
|
|
if (jsLines[i].includes(`Routes["${path}"] = `)) { return [
|
|
|
|
i + 1, // + 1 because GitHub line numbers are 1-indexed
|
|
|
|
(function() {
|
|
|
|
// TODO:
|
|
|
|
// find the first bracket on that line after the =
|
|
|
|
// walk forward until we find the corresponding match for it
|
|
|
|
// that's the last line?
|
|
|
|
return i + 2;
|
|
|
|
})()
|
|
|
|
]; }
|
2021-04-23 03:31:43 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-23 02:23:05 +02:00
|
|
|
return `
|
|
|
|
<html>
|
|
|
|
<body>
|
2021-04-23 03:53:30 +02:00
|
|
|
<p>(work in progress)</p>
|
2021-04-23 02:23:05 +02:00
|
|
|
<dl>
|
2021-08-17 00:47:26 +02:00
|
|
|
` + Object.entries(Routes).map(([path, {usage, description, __isInfill}]) => {
|
2021-04-23 03:53:30 +02:00
|
|
|
if (__isInfill) { return ''; }
|
2021-04-23 03:31:43 +02:00
|
|
|
let usages = usage ? (Array.isArray(usage) ? usage : [usage]) : [];
|
2021-08-17 01:43:34 +02:00
|
|
|
usages = usages.map(u => u.replace('\$0', path.substring(1) /* drop leading / */));
|
|
|
|
const lineRange = findRouteLineRange(path);
|
2021-04-23 02:23:05 +02:00
|
|
|
return `
|
2021-08-17 01:43:34 +02:00
|
|
|
<dt>${path.substring(1)}</dt>
|
2021-08-17 00:47:26 +02:00
|
|
|
${description ? `<dd>Description: ${description}</dd>` :
|
2021-08-17 01:43:34 +02:00
|
|
|
'<dd style="background-color: #f99">No description found!</dd>'}
|
2021-08-17 00:47:26 +02:00
|
|
|
${usages.length > 0 ? `<dd>Usage:
|
2021-04-23 02:23:05 +02:00
|
|
|
<ul>
|
|
|
|
${usages.map(u => `<li>${u}</li>`).join('\n')}
|
|
|
|
</ul>
|
2021-08-17 01:43:34 +02:00
|
|
|
</dd>` : '<dd style="background-color: #f99">No usage found!</dd>'}
|
|
|
|
${lineRange ?
|
2021-08-17 01:51:39 +02:00
|
|
|
`<dd><details>
|
|
|
|
<summary>Source code (<a href="https://github.com/osnr/TabFS/blob/master/extension/background.js#L${lineRange[0]}-L${lineRange[1]}">on GitHub</a>)</summary>
|
|
|
|
<pre><code>${
|
|
|
|
jsLines[lineRange[0] - 1]
|
|
|
|
// FIXME: get entire range; escape for HTML
|
|
|
|
}</code></pre>
|
|
|
|
</details></dd>` : '<dd style="background-color: #f99">No source code found!</dd>'}
|
2021-08-17 01:43:34 +02:00
|
|
|
`;
|
2021-07-23 02:53:17 +02:00
|
|
|
}).join('\n') + `
|
2021-04-23 02:23:05 +02:00
|
|
|
</dl>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`;
|
|
|
|
});
|
|
|
|
|
2020-10-29 23:56:57 +01:00
|
|
|
// Ensure that there are routes for all ancestors. This algorithm is
|
2020-12-25 22:40:18 +01:00
|
|
|
// probably not correct, but whatever. Basically, you need to start at
|
|
|
|
// the deepest level, fill in all the parents 1 level up that don't
|
|
|
|
// exist yet, then walk up one level at a time. It's important to go
|
|
|
|
// one level at a time so you know (for each parent) what all the
|
|
|
|
// children will be.
|
2020-12-24 22:49:38 +01:00
|
|
|
for (let i = 10; i >= 0; i--) {
|
2021-03-23 07:25:38 +01:00
|
|
|
for (let path of Object.keys(Routes).filter(key => key.split("/").length === i)) {
|
2020-10-28 04:53:39 +01:00
|
|
|
path = path.substr(0, path.lastIndexOf("/"));
|
2020-10-28 22:14:57 +01:00
|
|
|
if (path == '') path = '/';
|
2020-10-28 04:53:39 +01:00
|
|
|
|
2021-07-26 23:56:48 +02:00
|
|
|
if (!Routes[path]) { Routes[path] = makeDefaultRouteForDirectory(path); }
|
2018-11-15 08:52:26 +01:00
|
|
|
}
|
2020-12-25 22:40:18 +01:00
|
|
|
// I also think it would be better to compute this stuff on the fly,
|
|
|
|
// so you could patch more routes in at runtime, but I need to think
|
|
|
|
// a bit about how to make that work with wildcards.
|
2020-10-28 04:53:39 +01:00
|
|
|
}
|
2020-12-02 22:45:20 +01:00
|
|
|
|
2021-03-21 23:35:01 +01:00
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
for (let key in Routes) {
|
2021-03-22 03:42:44 +01:00
|
|
|
// /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[0-9]+)\/url.txt
|
2021-07-23 02:53:17 +02:00
|
|
|
Routes[key].__matchVarCount = 0;
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes[key].__regex = new RegExp(
|
2021-03-21 23:35:01 +01:00
|
|
|
'^' + key
|
|
|
|
.split('/')
|
|
|
|
.map(keySegment => keySegment
|
|
|
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
.replace(/([#:])([A-Z_]+)/g, (_, sigil, varName) => {
|
2021-07-23 02:53:17 +02:00
|
|
|
Routes[key].__matchVarCount++;
|
2021-03-22 03:42:44 +01:00
|
|
|
return `(?<${sigil === '#' ? 'int$' : 'string$'}${varName}>` +
|
|
|
|
(sigil === '#' ? '[0-9]+' : '[^/]+') + `)`;
|
2021-03-21 23:35:01 +01:00
|
|
|
}))
|
|
|
|
.join('/') + '$');
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
Routes[key].__match = function(path) {
|
|
|
|
const result = Routes[key].__regex.exec(path);
|
2021-03-21 23:35:01 +01:00
|
|
|
if (!result) { return; }
|
|
|
|
|
|
|
|
const vars = {};
|
2021-03-22 03:42:44 +01:00
|
|
|
for (let [typeAndVarName, value] of Object.entries(result.groups || {})) {
|
2021-03-21 23:35:01 +01:00
|
|
|
let [type_, varName] = typeAndVarName.split('$');
|
|
|
|
// TAB_ID -> tabId
|
|
|
|
varName = varName.toLowerCase();
|
|
|
|
varName = varName.replace(/_([a-z])/g, c => c[1].toUpperCase());
|
|
|
|
vars[varName] = type_ === 'int' ? parseInt(value) : value;
|
|
|
|
}
|
|
|
|
return vars;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Fill in default implementations of fs ops:
|
|
|
|
|
2020-12-02 22:45:20 +01:00
|
|
|
// if readdir -> directory -> add getattr, opendir, releasedir
|
2021-03-23 07:25:38 +01:00
|
|
|
if (Routes[key].readdir) {
|
|
|
|
Routes[key] = {
|
2020-12-02 22:45:20 +01:00
|
|
|
getattr() {
|
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFDIR | 0755,
|
|
|
|
st_nlink: 3,
|
|
|
|
st_size: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
opendir({path}) { return { fh: 0 }; },
|
|
|
|
releasedir({path}) { return {}; },
|
2021-03-23 07:25:38 +01:00
|
|
|
...Routes[key]
|
2020-12-02 22:45:20 +01:00
|
|
|
};
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
} else if (Routes[key].readlink) {
|
|
|
|
Routes[key] = {
|
2021-03-22 03:42:44 +01:00
|
|
|
async getattr(req) {
|
|
|
|
const st_size = (await this.readlink(req)).buf.length + 1;
|
2020-12-02 22:45:20 +01:00
|
|
|
return {
|
|
|
|
st_mode: unix.S_IFLNK | 0444,
|
|
|
|
st_nlink: 1,
|
|
|
|
// You _must_ return correct linkee path length from getattr!
|
|
|
|
st_size
|
|
|
|
};
|
|
|
|
},
|
2021-03-23 07:25:38 +01:00
|
|
|
...Routes[key]
|
2020-12-02 22:45:20 +01:00
|
|
|
};
|
|
|
|
|
2021-03-23 07:25:38 +01:00
|
|
|
} else if (Routes[key].read || Routes[key].write) {
|
|
|
|
Routes[key] = {
|
2020-12-02 22:45:20 +01:00
|
|
|
async getattr() {
|
|
|
|
return {
|
2021-03-23 07:25:38 +01:00
|
|
|
st_mode: unix.S_IFREG | ((Routes[key].read && 0444) | (Routes[key].write && 0222)),
|
2020-12-02 22:45:20 +01:00
|
|
|
st_nlink: 1,
|
|
|
|
st_size: 100 // FIXME
|
|
|
|
};
|
|
|
|
},
|
2021-03-21 23:35:01 +01:00
|
|
|
open() { return { fh: 0 }; },
|
|
|
|
release() { return {}; },
|
2021-03-23 07:25:38 +01:00
|
|
|
...Routes[key]
|
2020-12-02 22:45:20 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
// most specific (lowest matchVarCount) routes should match first
|
|
|
|
const sortedRoutes = Object.values(Routes).sort((a, b) =>
|
|
|
|
a.__matchVarCount - b.__matchVarCount
|
|
|
|
);
|
2021-03-22 03:42:44 +01:00
|
|
|
function tryMatchRoute(path) {
|
2021-03-21 23:35:01 +01:00
|
|
|
if (path.match(/\/\._[^\/]+$/)) {
|
2021-03-22 03:42:44 +01:00
|
|
|
// Apple Double ._whatever file for xattrs
|
|
|
|
throw new UnixError(unix.ENOTSUP);
|
2019-03-02 10:48:56 +01:00
|
|
|
}
|
2018-11-12 01:31:02 +01:00
|
|
|
|
2021-07-23 02:53:17 +02:00
|
|
|
for (let route of sortedRoutes) {
|
2021-03-22 03:42:44 +01:00
|
|
|
const vars = route.__match(path);
|
|
|
|
if (vars) { return [route, vars]; }
|
2018-11-12 01:31:02 +01:00
|
|
|
}
|
2021-03-21 23:35:01 +01:00
|
|
|
throw new UnixError(unix.ENOENT);
|
2018-11-22 11:52:54 +01:00
|
|
|
}
|
|
|
|
|
2020-10-28 04:53:39 +01:00
|
|
|
let port;
|
|
|
|
async function onMessage(req) {
|
2020-12-03 01:57:06 +01:00
|
|
|
if (req.buf) req.buf = atob(req.buf);
|
2020-12-12 11:36:37 +01:00
|
|
|
console.log('req', req);
|
2018-11-29 18:53:03 +01:00
|
|
|
|
2020-10-28 04:53:39 +01:00
|
|
|
let response = { op: req.op, error: unix.EIO };
|
2020-12-25 08:46:48 +01:00
|
|
|
let didTimeout = false, timeout = setTimeout(() => {
|
|
|
|
// timeout is very useful because some operations just hang
|
|
|
|
// (like trying to take a screenshot, until the tab is focused)
|
|
|
|
didTimeout = true; console.error('timeout');
|
2021-01-03 11:23:20 +01:00
|
|
|
port.postMessage({ id: req.id, op: req.op, error: unix.ETIMEDOUT });
|
2020-12-25 08:46:48 +01:00
|
|
|
}, 1000);
|
|
|
|
|
2020-10-28 04:53:39 +01:00
|
|
|
/* console.time(req.op + ':' + req.path);*/
|
|
|
|
try {
|
2021-03-22 03:42:44 +01:00
|
|
|
const [route, vars] = tryMatchRoute(req.path);
|
|
|
|
response = await route[req.op]({...req, ...vars});
|
2020-10-28 04:53:39 +01:00
|
|
|
response.op = req.op;
|
2020-12-04 18:58:09 +01:00
|
|
|
if (response.buf) { response.buf = btoa(response.buf); }
|
2018-11-29 18:53:03 +01:00
|
|
|
|
2018-11-22 11:52:54 +01:00
|
|
|
} catch (e) {
|
2019-02-26 08:08:52 +01:00
|
|
|
console.error(e);
|
2018-11-11 20:44:36 +01:00
|
|
|
response = {
|
2018-11-22 11:52:54 +01:00
|
|
|
op: req.op,
|
|
|
|
error: e instanceof UnixError ? e.error : unix.EIO
|
2021-03-21 23:35:01 +01:00
|
|
|
};
|
2018-11-12 01:18:21 +01:00
|
|
|
}
|
2018-11-24 19:58:33 +01:00
|
|
|
/* console.timeEnd(req.op + ':' + req.path);*/
|
2018-11-11 14:32:44 +01:00
|
|
|
|
2020-12-25 08:46:48 +01:00
|
|
|
if (!didTimeout) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
|
|
|
console.log('resp', response);
|
2021-01-03 11:23:20 +01:00
|
|
|
response.id = req.id;
|
2020-12-25 08:46:48 +01:00
|
|
|
port.postMessage(response);
|
|
|
|
}
|
2018-11-11 14:32:44 +01:00
|
|
|
};
|
2018-11-29 18:53:03 +01:00
|
|
|
|
|
|
|
function tryConnect() {
|
2021-04-20 14:07:22 +02:00
|
|
|
// so we don't try to reconnect if we're hot-swapping background.js.
|
|
|
|
if (window.isConnected) return;
|
|
|
|
|
2021-02-03 08:07:24 +01:00
|
|
|
// Safari is very weird -- it has this native app that we have to talk to,
|
2021-02-08 11:32:21 +01:00
|
|
|
// so we poke that app to wake it up, get it to start the TabFS process
|
|
|
|
// and boot a WebSocket, then connect to it.
|
2021-02-03 08:07:24 +01:00
|
|
|
// Is there a better way to do this?
|
|
|
|
if (chrome.runtime.getURL('/').startsWith('safari-web-extension://')) { // Safari-only
|
2021-02-08 11:32:21 +01:00
|
|
|
chrome.runtime.sendNativeMessage('com.rsnous.tabfs', {op: 'safari_did_connect'}, resp => {
|
2021-02-03 08:07:24 +01:00
|
|
|
console.log(resp);
|
2021-02-08 11:32:21 +01:00
|
|
|
|
2021-02-08 22:45:26 +01:00
|
|
|
let socket;
|
|
|
|
function connectSocket(checkAfterTime) {
|
|
|
|
socket = new WebSocket('ws://localhost:9991');
|
|
|
|
socket.addEventListener('message', event => {
|
|
|
|
onMessage(JSON.parse(event.data));
|
|
|
|
});
|
|
|
|
|
|
|
|
port = { postMessage(message) {
|
|
|
|
socket.send(JSON.stringify(message));
|
|
|
|
} };
|
2021-02-08 11:32:21 +01:00
|
|
|
|
2021-02-08 22:45:26 +01:00
|
|
|
setTimeout(() => {
|
2021-04-20 14:07:22 +02:00
|
|
|
if (socket.readyState === 1) {
|
|
|
|
window.isConnected = true;
|
|
|
|
|
|
|
|
} else {
|
2021-02-08 22:45:26 +01:00
|
|
|
console.log('ws connection failed, retrying in', checkAfterTime);
|
|
|
|
connectSocket(checkAfterTime * 2);
|
|
|
|
}
|
|
|
|
}, checkAfterTime);
|
|
|
|
}
|
|
|
|
connectSocket(200);
|
2021-02-03 08:07:24 +01:00
|
|
|
});
|
2021-02-08 11:32:21 +01:00
|
|
|
return;
|
2021-02-03 08:07:24 +01:00
|
|
|
}
|
2021-02-08 11:32:21 +01:00
|
|
|
|
|
|
|
port = chrome.runtime.connectNative('com.rsnous.tabfs');
|
2021-04-23 03:31:43 +02:00
|
|
|
window.isConnected = true;
|
2021-02-08 11:32:21 +01:00
|
|
|
port.onMessage.addListener(onMessage);
|
2021-04-20 14:07:22 +02:00
|
|
|
port.onDisconnect.addListener(p => {
|
|
|
|
window.isConnected = false;
|
|
|
|
console.log('disconnect', p);
|
|
|
|
});
|
2018-11-29 18:53:03 +01:00
|
|
|
}
|
|
|
|
|
2021-03-15 05:33:23 +01:00
|
|
|
|
|
|
|
if (typeof process === 'object') {
|
|
|
|
// we're running in node (as part of a test)
|
|
|
|
// return everything they might want to test
|
2021-03-23 07:25:38 +01:00
|
|
|
module.exports = {Routes, tryMatchRoute};
|
2021-03-15 05:33:23 +01:00
|
|
|
|
|
|
|
} else {
|
2018-11-29 18:53:03 +01:00
|
|
|
tryConnect();
|
2020-10-28 04:53:39 +01:00
|
|
|
}
|
2021-04-23 01:17:42 +02:00
|
|
|
|