extension: (WIP) createWritableDirectory

also break out routeDirectoryForChildren. breaks evals for now

(this is groundwork for writable tab directories; should also be able
to roll evals and watches in under createWritableDirectory)
This commit is contained in:
Omar Rizwan 2021-05-13 16:04:07 -07:00
parent 38a5677dec
commit b442fa6ae5

View file

@ -65,6 +65,9 @@ const utf8ArrayToString = (function() {
return utf8 => decoder.decode(utf8); return utf8 => decoder.decode(utf8);
})(); })();
// global so it can be hot-reloaded
window.Routes = {};
// Helper function: generates a full set of file operations that you // Helper function: generates a full set of file operations that you
// can use as a route handler (so clients can read and write // can use as a route handler (so clients can read and write
// sections of the file, stat it to get its size and see it show up // sections of the file, stat it to get its size and see it show up
@ -109,12 +112,14 @@ const routeWithContents = (function() {
// defined here. // defined here.
async getattr(req) { async getattr(req) {
const data = await getData(req);
if (typeof data === 'undefined') { throw new UnixError(unix.ENOENT); }
return { return {
st_mode: unix.S_IFREG | 0444 | (setData ? 0222 : 0), st_mode: unix.S_IFREG | 0444 | (setData ? 0222 : 0),
st_nlink: 1, st_nlink: 1,
// you'll want to override this if getData() is slow, because // you'll want to override this if getData() is slow, because
// getattr() gets called a lot more cavalierly than open(). // getattr() gets called a lot more cavalierly than open().
st_size: toUtf8Array(await getData(req)).length st_size: toUtf8Array(data).length
}; };
}, },
@ -122,6 +127,7 @@ const routeWithContents = (function() {
// data for all subsequent reads from that application. // data for all subsequent reads from that application.
async open(req) { async open(req) {
const data = await getData(req); const data = await getData(req);
if (typeof data === 'undefined') { throw new UnixError(unix.ENOENT); }
return { fh: Cache.storeObject(req.path, toUtf8Array(data)) }; return { fh: Cache.storeObject(req.path, toUtf8Array(data)) };
}, },
async read({fh, size, offset}) { async read({fh, size, offset}) {
@ -160,8 +166,18 @@ const routeWithContents = (function() {
return routeWithContents; return routeWithContents;
})(); })();
// global so it can be hot-reloaded function routeDirectoryForChildren(path) {
window.Routes = {}; 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]);
entries = [".", "..", ...new Set(entries)];
return { readdir() { return { entries }; }, __isInfill: true };
}
function routeDefer(fn) { return fn; }
Routes["/tabs/create"] = { Routes["/tabs/create"] = {
usage: 'echo "https://www.google.com" > $0', usage: 'echo "https://www.google.com" > $0',
@ -214,6 +230,13 @@ Routes["/tabs/by-id"] = {
} }
}; };
// cannot use this wildcard trick w/o breaking the parent logic
const tabIdDirectory = createWritableDirectory();
Routes["/tabs/by-id/#TAB_ID"] = routeDefer(() => routeDirectoryForChildren("/tabs/by-id/#TAB_ID"));
// Routes["/tabs/by-id/#TAB_ID/:FILENAME"] = routeDirectoryWritable();
// TODO: can I trigger 1. nav to Finder and 2. nav to Terminal from toolbar click?
(function() { (function() {
const routeForTab = (readHandler, writeHandler) => routeWithContents(async ({tabId}) => { const routeForTab = (readHandler, writeHandler) => routeWithContents(async ({tabId}) => {
const tab = await browser.tabs.get(tabId); const tab = await browser.tabs.get(tabId);
@ -246,8 +269,6 @@ Routes["/tabs/by-id"] = {
...routeFromScript(`document.body.innerHTML`) ...routeFromScript(`document.body.innerHTML`)
}; };
// echo true > mnt/tabs/by-id/1644/active
// cat mnt/tabs/by-id/1644/active
Routes["/tabs/by-id/#TAB_ID/active"] = { Routes["/tabs/by-id/#TAB_ID/active"] = {
usage: ['cat $0', usage: ['cat $0',
'echo true > $0'], 'echo true > $0'],
@ -259,58 +280,63 @@ Routes["/tabs/by-id"] = {
) )
}; };
})(); })();
function createWritableDirectory() {
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,
};
},
},
routeForFilename: {
usage: ['echo "2 + 2" > $0',
'cat $0.result'],
async mknod({path, mode}) {
dir[path] = '';
return {};
},
async unlink({path}) {
delete dir[path];
return {};
},
...routeWithContents(
async ({path}) => dir[path],
async ({path}, buf) => { dir[path] = buf; }
)
}
};
}
(function() { (function() {
const evals = {}; // proposed refactor
Routes["/tabs/by-id/#TAB_ID/evals"] = { const evals = createWritableDirectory();
usage: 'ls $0', Routes["/tabs/by-id/#TAB_ID/evals"] = evals.routeForRoot;
async readdir({path, tabId}) { Routes["/tabs/by-id/#TAB_ID/evals/:FILENAME"] = evals.routeForFilename;
return { entries: [".", "..",
...Object.keys(evals[tabId] || {}), // evals[tabId][name].result = JSON.stringify((await browser.tabs.executeScript(tabId, {code: buf}))[0]) + '\n';
...Object.keys(evals[tabId] || {}).map(f => f + '.result')] };
}, // old stuffs
getattr() { // const evals = {};
return { // Routes["/tabs/by-id/#TAB_ID/evals"] = {
st_mode: unix.S_IFDIR | 0777, // writable so you can create/rm evals // };
st_nlink: 3, // Routes["/tabs/by-id/#TAB_ID/evals/:FILENAME"] = {
st_size: 0, // };
};
},
};
Routes["/tabs/by-id/#TAB_ID/evals/:FILENAME"] = {
usage: ['cat $0.result',
'echo "2 + 2" > $0'],
// NOTE: eval runs in extension's content script, not in original page JS context
async mknod({tabId, filename, mode}) {
evals[tabId] = evals[tabId] || {};
evals[tabId][filename] = { code: '' };
return {};
},
async unlink({tabId, filename}) {
delete evals[tabId][filename]; // TODO: also delete evals[tabId] if empty
return {};
},
...routeWithContents(async ({tabId, filename}) => {
const name = filename.replace(/\.result$/, '');
if (!evals[tabId] || !(name in evals[tabId])) { throw new UnixError(unix.ENOENT); }
if (filename.endsWith('.result')) {
return evals[tabId][name].result || '';
} else {
return evals[tabId][name].code;
}
}, async ({tabId, filename}, buf) => {
if (filename.endsWith('.result')) {
// FIXME: case where they try to write to .result file
} else {
const name = filename;
evals[tabId][name].code = buf;
evals[tabId][name].result = JSON.stringify((await browser.tabs.executeScript(tabId, {code: buf}))[0]) + '\n';
}
})
};
})(); })();
(function() { (function() {
const watches = {}; const watches = {};
@ -638,7 +664,7 @@ Routes["/runtime/routes.html"] = routeWithContents(async () => {
<body> <body>
<p>(work in progress)</p> <p>(work in progress)</p>
<dl> <dl>
${Object.entries(Routes).map(([path, {usage, __isInfill}]) => { ` + Object.entries(Routes).map(([path, {usage, __isInfill}]) => {
if (__isInfill) { return ''; } if (__isInfill) { return ''; }
path = path.substring(1); // drop leading / path = path.substring(1); // drop leading /
let usages = usage ? (Array.isArray(usage) ? usage : [usage]) : []; let usages = usage ? (Array.isArray(usage) ? usage : [usage]) : [];
@ -652,7 +678,7 @@ Routes["/runtime/routes.html"] = routeWithContents(async () => {
</ul> </ul>
</dd> </dd>
`; `;
}).join('\n')} }).join('\n') + `
</dl> </dl>
</body> </body>
</html> </html>
@ -721,6 +747,7 @@ Routes["/runtime/background.js.html"] = routeWithContents(async () => {
`; `;
}); });
// Ensure that there are routes for all ancestors. This algorithm is // Ensure that there are routes for all ancestors. This algorithm is
// probably not correct, but whatever. Basically, you need to start at // 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 // the deepest level, fill in all the parents 1 level up that don't
@ -732,17 +759,7 @@ for (let i = 10; i >= 0; i--) {
path = path.substr(0, path.lastIndexOf("/")); path = path.substr(0, path.lastIndexOf("/"));
if (path == '') path = '/'; if (path == '') path = '/';
if (!Routes[path]) { if (!Routes[path]) { Routes[path] = routeDirectoryForChildren(path); }
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]);
entries = [".", "..", ...new Set(entries)];
Routes[path] = { readdir() { return { entries }; }, __isInfill: true };
}
} }
// I also think it would be better to compute this stuff on the fly, // 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 // so you could patch more routes in at runtime, but I need to think
@ -751,6 +768,8 @@ for (let i = 10; i >= 0; i--) {
for (let key in Routes) { for (let key in Routes) {
if (typeof Routes[key] === 'function') Routes[key] = Routes[key]();
// /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[0-9]+)\/url.txt // /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[0-9]+)\/url.txt
Routes[key].__regex = new RegExp( Routes[key].__regex = new RegExp(
'^' + key '^' + key