theseus/bundled-addons/pithos/core/pty.js

99 lines
4.5 KiB
JavaScript
Raw Normal View History

// The pseudo-terminal that `s3d login` needs (it reads the recovery phrase
// with term.ReadPassword, which fails on a plain pipe).
//
// Hosts that ship node_modules (web console, desktop) have @lydell/node-pty
// installed. The Theseus add-on leaves it out (the Windows build is 12 MB of
// native code), so on first use it fetches the one platform package it needs
// from the npm registry, checks it against the integrity hash pinned below,
// and unpacks it into the add-on's data folder.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import zlib from 'node:zlib';
import { pathToFileURL } from 'node:url';
const VERSION = '1.2.0-beta.15';
// dist.integrity from the npm registry for each platform package.
const PACKAGES = {
'win32-x64': 'sha512-2f8twEmDVxZ7drchAXjtevpmSPhFok0avAnzXro4t5gmz0xsPNKkoZvymwtuIS3xo7PzQqZOPQ/YzwEMb7oIzQ==',
'win32-arm64': 'sha512-pyAk91w7wnnKrD4mrHXtIXRfmzSWV5bEzvRhurXcMCtCc2TJ424ciUskIgWMhAPP6y3KyUnqElj+U6kY3iOt0A==',
'darwin-x64': 'sha512-yDT2oqPqYMBScyuk1U9Rg5VKcrbMOD9o9jWYYamDADA3NSbUISroPChrqYRQ74Y7BQtNH4gqYAiWOZRi5uQZ0Q==',
'darwin-arm64': 'sha512-6TSBbzdcLiNTHl1mTuzflqXrkmcC36USVGvERoDgvHk2ItEDaMaFZuAJ1CqPmwYj0DyhCS16TVS8OGK9xZnjyQ==',
'linux-x64': 'sha512-+U/5AVvHT6W+8OCYcnJgN0Qgc0ycO3TfD6aaFJHK+WHij797f8gsi5dV1HEO9l6YQmWCD+VL5gaLDhx3mxHwCA==',
'linux-arm64': 'sha512-wkbNF7dYAmtJv+o2+iztVlNwnUB4B0uX0wh/UD+mwMcmE2gNMnW9GChXO7fEE5XJokD0vB5idiHpGegaN+G/sg==',
};
let cached;
// Returns the pty module, or null when none is available and none can be
// installed. installDir: where a downloaded package lives (null = never download).
export async function loadPty({ installDir, onProgress = () => {} } = {}) {
if (cached) return cached;
try {
const m = await import('@lydell/node-pty');
return (cached = m.default || m);
} catch { /* not shipped with this host */ }
if (!installDir) return null;
const plat = `${process.platform}-${process.arch}`;
if (!PACKAGES[plat]) return null;
const pkgDir = path.join(installDir, `node-pty-${plat}-${VERSION}`);
const entry = path.join(pkgDir, 'lib', 'index.js');
if (!fs.existsSync(entry)) await install(plat, pkgDir, onProgress);
const m = await import(pathToFileURL(entry).href);
return (cached = m.default || m);
}
async function install(plat, pkgDir, onProgress) {
const name = `node-pty-${plat}`;
const url = `https://registry.npmjs.org/@lydell/${name}/-/${name}-${VERSION}.tgz`;
onProgress({ phase: 'download', what: 'terminal support', url });
const res = await fetch(url);
if (!res.ok) throw new Error(`could not download terminal support: HTTP ${res.status}`);
const tgz = Buffer.from(await res.arrayBuffer());
const want = PACKAGES[plat];
const got = 'sha512-' + crypto.createHash('sha512').update(tgz).digest('base64');
if (got !== want) throw new Error('terminal support package failed its integrity check');
onProgress({ phase: 'extract', what: 'terminal support' });
const tmp = pkgDir + '.tmp';
fs.rmSync(tmp, { recursive: true, force: true });
for (const { name: file, data } of untar(zlib.gunzipSync(tgz))) {
// npm tarballs put everything under package/.
const rel = file.replace(/^package\//, '');
if (!rel || rel === file) continue;
const out = path.join(tmp, rel);
if (!out.startsWith(tmp + path.sep)) throw new Error('unsafe path in terminal support package');
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, data);
}
fs.rmSync(pkgDir, { recursive: true, force: true });
fs.renameSync(tmp, pkgDir);
onProgress({ phase: 'done', what: 'terminal support' });
}
// Minimal ustar reader: regular files only, with pax "path" overrides.
function* untar(buf) {
let off = 0;
let paxPath = null;
while (off + 512 <= buf.length) {
const h = buf.subarray(off, off + 512);
if (h.every((b) => b === 0)) break;
const str = (a, b) => h.toString('utf8', a, b).replace(/\0.*$/s, '');
let name = str(0, 100);
const prefix = str(345, 500);
if (prefix) name = prefix + '/' + name;
const size = parseInt(str(124, 136).trim() || '0', 8);
const type = String.fromCharCode(h[156] || 48);
const data = buf.subarray(off + 512, off + 512 + size);
off += 512 + Math.ceil(size / 512) * 512;
if (type === 'x') {
const m = /\d+ path=([^\n]+)\n/.exec(data.toString('utf8'));
paxPath = m ? m[1] : null;
continue;
}
if (type === '0' || type === '\0') yield { name: paxPath || name, data: Buffer.from(data) };
paxPath = null;
}
}