extension,test: Start on simplifying routing.

Introduces a 'named variable' syntax so we can remove all the manual
indexing into path components / converting to int in individual fs
ops.

Also rewrites the route finding to use regex: all routes are compiled
to regexes, then the route finder just walks through them all until it
hits a match.

Doesn't fully work yet; JS test passes.
This commit is contained in:
Omar Rizwan 2021-03-21 15:35:01 -07:00
parent 57726b4354
commit be519afe5f
2 changed files with 63 additions and 70 deletions

View File

@ -223,26 +223,6 @@ router["/tabs/by-id"] = {
return { entries: [".", "..", ...tabs.map(tab => String(tab.id))] };
}
};
// title.txt
// url.txt
// text.txt
// TODO: document.html
// eval-in
// eval-out
// TODO: mem (?)
// TODO: cpu (?)
// TODO: dom/ ?
// TODO: globals/ ?
// TODO: archive.mhtml ?
// TODO: printed.pdf
// control
// there's a question about whether to do stuff through injected
// JavaScript or through the devtools API.
(function() {
const withTab = (readHandler, writeHandler) => defineFile(async path => {
@ -259,23 +239,22 @@ router["/tabs/by-id"] = {
return (await browser.tabs.executeScript(tabId, {code}))[0];
});
router["/tabs/by-id/*/url.txt"] = withTab(tab => tab.url + "\n", buf => ({ url: buf }));
router["/tabs/by-id/*/title.txt"] = withTab(tab => tab.title + "\n");
router["/tabs/by-id/*/text.txt"] = fromScript(`document.body.innerText`);
router["/tabs/by-id/*/body.html"] = fromScript(`document.body.innerHTML`);
router["/tabs/by-id/#TAB_ID/url.txt"] = withTab(tab => tab.url + "\n", buf => ({ url: buf }));
router["/tabs/by-id/#TAB_ID/title.txt"] = withTab(tab => tab.title + "\n");
router["/tabs/by-id/#TAB_ID/text.txt"] = fromScript(`document.body.innerText`);
router["/tabs/by-id/#TAB_ID/body.html"] = fromScript(`document.body.innerHTML`);
// echo true > mnt/tabs/by-id/1644/active
// cat mnt/tabs/by-id/1644/active
router["/tabs/by-id/*/active"] = withTab(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") }));
router["/tabs/by-id/#TAB_ID/active"] = withTab(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") }));
})();
(function() {
const evals = {};
router["/tabs/by-id/*/evals"] = {
async readdir({path}) {
const tabId = parseInt(pathComponent(path, -2));
router["/tabs/by-id/#TAB_ID/evals"] = {
async readdir({path, tabId}) {
return { entries: [".", "..",
...Object.keys(evals[tabId] || {}),
...Object.keys(evals[tabId] || {}).map(f => f + '.result')] };
@ -288,7 +267,7 @@ router["/tabs/by-id"] = {
};
},
};
router["/tabs/by-id/*/evals/*"] = {
router["/tabs/by-id/#TAB_ID/evals/*"] = {
// NOTE: eval runs in extension's content script, not in original page JS context
async mknod({path, mode}) {
const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
@ -326,7 +305,7 @@ router["/tabs/by-id"] = {
})();
(function() {
const watches = {};
router["/tabs/by-id/*/watches"] = {
router["/tabs/by-id/#TAB_ID/watches"] = {
async readdir({path}) {
const tabId = parseInt(pathComponent(path, -2));
return { entries: [".", "..", ...Object.keys(watches[tabId] || [])] };
@ -339,7 +318,7 @@ router["/tabs/by-id"] = {
};
},
};
router["/tabs/by-id/*/watches/*"] = {
router["/tabs/by-id/#TAB_ID/watches/*"] = {
// NOTE: eval runs in extension's content script, not in original page JS context
async mknod({path, mode}) {
const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
@ -367,14 +346,14 @@ router["/tabs/by-id"] = {
};
})();
router["/tabs/by-id/*/window"] = {
router["/tabs/by-id/#TAB_ID/window"] = {
// a symbolic link to /windows/[id for this window]
async readlink({path}) {
const tabId = parseInt(pathComponent(path, -2)); const tab = await browser.tabs.get(tabId);
return { buf: "../../../windows/" + tab.windowId };
}
};
router["/tabs/by-id/*/control"] = {
router["/tabs/by-id/#TAB_ID/control"] = {
// echo remove > mnt/tabs/by-id/1644/control
async write({path, buf}) {
const tabId = parseInt(pathComponent(path, -2));
@ -393,7 +372,7 @@ router["/tabs/by-id/*/control"] = {
// resources/
// TODO: scripts/ TODO: allow creation, eval immediately
router["/tabs/by-id/*/debugger/resources"] = {
router["/tabs/by-id/#TAB_ID/debugger/resources"] = {
async readdir({path}) {
const tabId = parseInt(pathComponent(path, -3));
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
@ -401,7 +380,7 @@ router["/tabs/by-id/*/control"] = {
return { entries: [".", "..", ...frameTree.resources.map(r => sanitize(String(r.url)))] };
}
};
router["/tabs/by-id/*/debugger/resources/*"] = defineFile(async path => {
router["/tabs/by-id/#TAB_ID/debugger/resources/*"] = defineFile(async path => {
const [tabId, suffix] = [parseInt(pathComponent(path, -4)), pathComponent(path, -1)];
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
@ -419,7 +398,7 @@ router["/tabs/by-id/*/control"] = {
}
throw new UnixError(unix.ENOENT);
});
router["/tabs/by-id/*/debugger/scripts"] = {
router["/tabs/by-id/#TAB_ID/debugger/scripts"] = {
async opendir({path}) {
const tabId = parseInt(pathComponent(path, -3));
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
@ -442,7 +421,7 @@ router["/tabs/by-id/*/control"] = {
}
return scriptInfo;
}
router["/tabs/by-id/*/debugger/scripts/*"] = defineFile(async path => {
router["/tabs/by-id/#TAB_ID/debugger/scripts/*"] = defineFile(async path => {
const [tabId, suffix] = [parseInt(pathComponent(path, -4)), pathComponent(path, -1)];
await TabManager.debugTab(tabId);
await TabManager.enableDomainForTab(tabId, "Page");
@ -461,16 +440,17 @@ router["/tabs/by-id/*/control"] = {
});
})();
router["/tabs/by-id/*/inputs"] = {
router["/tabs/by-id/#TAB_ID/inputs"] = {
async readdir({path}) {
const tabId = parseInt(pathComponent(path, -2));
// TODO: assign new IDs to inputs without them?
const code = `Array.from(document.querySelectorAll('textarea, input[type=text]')).map(e => e.id).filter(id => id)`
const code = `Array.from(document.querySelectorAll('textarea, input[type=text]'))
.map(e => e.id).filter(id => id)`;
const ids = (await browser.tabs.executeScript(tabId, {code}))[0];
return { entries: [".", "..", ...ids.map(id => `${id}.txt`)] };
}
};
router["/tabs/by-id/*/inputs/*"] = defineFile(async path => {
router["/tabs/by-id/#TAB_ID/inputs/*"] = defineFile(async path => {
const [tabId, inputId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
const code = `document.getElementById('${inputId}').value`;
const inputValue = (await browser.tabs.executeScript(tabId, {code}))[0];
@ -631,8 +611,36 @@ for (let i = 10; i >= 0; i--) {
// a bit about how to make that work with wildcards.
}
// fill in default implementations of fs ops
for (let key in router) {
// /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[^/]+)\/url.txt
router[key].__regex = new RegExp(
'^' + key
.split('/')
.map(keySegment => keySegment
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/([#:])([A-Z_]+)/g, (_, sigil, varName) => {
return `(?<${sigil === '#' ? 'int$' : 'string$'}${varName}>[^/]+)`;
}))
.join('/') + '$');
router[key].__match = function(path) {
const result = router[key].__regex.exec(path);
if (!result) { return; }
const vars = {};
for (let [typeAndVarName, value] of Object.entries(result.groups)) {
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:
// if readdir -> directory -> add getattr, opendir, releasedir
if (router[key].readdir) {
router[key] = {
@ -671,37 +679,22 @@ for (let key in router) {
st_size: 100 // FIXME
};
},
open() {
return { fh: 0 };
},
release() {
return {};
},
open() { return { fh: 0 }; },
release() { return {}; },
...router[key]
};
}
}
function findRoute(path) {
let pathSegments = path.split("/");
if (pathSegments[pathSegments.length - 1].startsWith("._")) {
if (path.match(/\/\._[^\/]+$/)) {
throw new UnixError(unix.ENOTSUP); // Apple Double file for xattrs
}
let routingPath = "";
for (let segment of pathSegments) {
if (routingPath === "/") { routingPath = ""; }
if (router[routingPath + "/*"]) {
routingPath += "/*";
} else if (router[routingPath + "/" + segment]) {
routingPath += "/" + segment;
} else {
throw new UnixError(unix.ENOENT);
}
for (let route of Object.values(router)) {
if (route.__match(path)) { return route; }
}
return router[routingPath];
throw new UnixError(unix.ENOENT);
}
let port;
@ -728,7 +721,7 @@ async function onMessage(req) {
response = {
op: req.op,
error: e instanceof UnixError ? e.error : unix.EIO
}
};
}
/* console.timeEnd(req.op + ':' + req.path);*/
@ -775,7 +768,7 @@ function tryConnect() {
port = chrome.runtime.connectNative('com.rsnous.tabfs');
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(p => {console.log('disconnect', p)});
port.onDisconnect.addListener(p => { console.log('disconnect', p); });
}

View File

@ -6,7 +6,7 @@ global.chrome = {};
const {router, findRoute} = require('../extension/background');
(async () => {
const tabRoute = await router['/tabs/by-id/*'].readdir();
const tabRoute = await router['/tabs/by-id/#TAB_ID'].readdir();
assert(['.', '..', 'url.txt', 'title.txt', 'text.txt']
.every(file => tabRoute.entries.includes(file)));
@ -16,6 +16,6 @@ const {router, findRoute} = require('../extension/background');
{ entries: ['.', '..', 'create',
'by-id', 'by-title', 'last-focused'] });
assert.deepEqual(findRoute('/tabs/by-id/TABID/url.txt'),
router['/tabs/by-id/*/url.txt']);
assert.deepEqual(findRoute('/tabs/by-id/10/url.txt'),
router['/tabs/by-id/#TAB_ID/url.txt']);
})();