modularize playground (prep for persistent/modular file handling)
This commit is contained in:
254
playground/src/emul.ts
Normal file
254
playground/src/emul.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { ZipFile } from "./zipfile";
|
||||
|
||||
class Buffer extends DataView {
|
||||
static alloc(n: number) {
|
||||
return new Buffer(new Uint8Array(n).buffer);
|
||||
}
|
||||
indexOf(n: number) {
|
||||
return new Uint8Array(this.buffer).indexOf(n);
|
||||
}
|
||||
get length() {
|
||||
return this.byteLength;
|
||||
}
|
||||
slice(start: number, end: number) {
|
||||
return new Buffer(this.buffer.slice(start, end));
|
||||
}
|
||||
readUInt8(i: number) {
|
||||
return this.getUint8(i);
|
||||
}
|
||||
writeUInt8(val: number, i: number) {
|
||||
this.setUint8(i, val);
|
||||
}
|
||||
write(value: string, start: number, len: number, enc: string) {
|
||||
// console.log("write", value, start, len, enc);
|
||||
let buf = new TextEncoder().encode(value);
|
||||
let ss = 0;
|
||||
let se = Math.min(len, buf.length);
|
||||
let ts = start;
|
||||
for (; ss < se; ss++, ts++) this.setInt8(ts, buf[ss]);
|
||||
shim.process.__lasterr.errno = 0;
|
||||
return se;
|
||||
}
|
||||
readDoubleLE(i: number) {
|
||||
return this.getFloat64(i, true);
|
||||
}
|
||||
readInt32LE(i: number) {
|
||||
return this.getInt32(i, true);
|
||||
}
|
||||
writeInt32LE(val: number, i: number) {
|
||||
return this.setInt32(i, val, true);
|
||||
}
|
||||
copy(target: Buffer, ts: number, ss: number, se: number) {
|
||||
for (; ss < se; ss++, ts++) target.setInt8(ts, this.getInt8(ss));
|
||||
}
|
||||
static concat(bufs: Buffer[]) {
|
||||
let size = bufs.reduce((a, b) => (a += b.byteLength), 0);
|
||||
let rval = Buffer.alloc(size);
|
||||
let off = 0;
|
||||
for (let buf of bufs) {
|
||||
const view = new Int8Array(rval.buffer);
|
||||
view.set(new Uint8Array(buf.buffer), off);
|
||||
off += buf.byteLength;
|
||||
}
|
||||
return rval;
|
||||
}
|
||||
toString() {
|
||||
return new TextDecoder().decode(this);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Handle {
|
||||
name: string;
|
||||
mode: string;
|
||||
pos: number;
|
||||
buf: Uint8Array;
|
||||
}
|
||||
|
||||
interface Process {
|
||||
platform: string;
|
||||
stdout: {
|
||||
write(s: string): void;
|
||||
};
|
||||
argv: string[];
|
||||
exit(_: number): void;
|
||||
cwd(): string;
|
||||
env: Record<string, string>;
|
||||
__lasterr: { errno: number };
|
||||
}
|
||||
export interface NodeShim {
|
||||
stdout: string;
|
||||
archive?: ZipFile;
|
||||
process: Process;
|
||||
files: Record<string, Uint8Array>;
|
||||
fds: Handle[];
|
||||
tty: {
|
||||
isatty(): number;
|
||||
};
|
||||
os: {
|
||||
platform(): string;
|
||||
};
|
||||
fs: any;
|
||||
}
|
||||
export let shim: NodeShim = {
|
||||
// these three and process are poked at externally
|
||||
archive: undefined,
|
||||
stdout: "",
|
||||
files: {},
|
||||
fds: [],
|
||||
tty: {
|
||||
isatty() {
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
os: {
|
||||
platform() {
|
||||
return "linux";
|
||||
},
|
||||
},
|
||||
fs: {
|
||||
// TODO - Idris is doing readdir, we should implement that
|
||||
opendirSync(name: string) {
|
||||
let fd = shim.fds.findIndex((x) => !x);
|
||||
if (fd < 0) fd = shim.fds.length;
|
||||
console.log("openDir", name);
|
||||
shim.process.__lasterr.errno = 0;
|
||||
return fd;
|
||||
},
|
||||
mkdirSync(name: string) {
|
||||
console.log("mkdir", name);
|
||||
shim.process.__lasterr.errno = 0;
|
||||
return 0;
|
||||
},
|
||||
openSync(name: string, mode: string) {
|
||||
console.log("open", name, mode);
|
||||
let te = new TextEncoder();
|
||||
|
||||
let fd = shim.fds.findIndex((x) => !x);
|
||||
if (fd < 0) fd = shim.fds.length;
|
||||
let buf: Uint8Array;
|
||||
let pos = 0;
|
||||
if (mode == "w") {
|
||||
buf = new Uint8Array(0);
|
||||
} else {
|
||||
// TODO, we need to involve localStorage when the window does multiple files and persists
|
||||
if (shim.files[name]) {
|
||||
buf = shim.files[name];
|
||||
} else if (shim.archive?.entries[name]) {
|
||||
// keep a copy of the uncompressed version for speed
|
||||
buf = shim.files[name] = shim.archive.getData(name);
|
||||
} else {
|
||||
shim.process.__lasterr.errno = 1;
|
||||
throw new Error(`${name} not found`);
|
||||
}
|
||||
}
|
||||
shim.process.__lasterr.errno = 0;
|
||||
shim.fds[fd] = { buf, pos, mode, name };
|
||||
// we'll mutate the pointer as stuff is read
|
||||
return fd;
|
||||
},
|
||||
writeSync(fd: number, line: string | Buffer) {
|
||||
try {
|
||||
let handle = shim.fds[fd];
|
||||
if (!handle) throw new Error(`bad fd ${fd}`);
|
||||
|
||||
let buf2: ArrayBuffer;
|
||||
if (typeof line === "string") {
|
||||
buf2 = new TextEncoder().encode(line);
|
||||
let newbuf = new Uint8Array(handle.buf.byteLength + buf2.byteLength);
|
||||
newbuf.set(new Uint8Array(handle.buf));
|
||||
newbuf.set(new Uint8Array(buf2), handle.buf.byteLength);
|
||||
handle.buf = newbuf;
|
||||
shim.process.__lasterr.errno = 0;
|
||||
} else if (line instanceof Buffer) {
|
||||
let start = arguments[2];
|
||||
let len = arguments[3];
|
||||
buf2 = line.buffer.slice(start, start + len);
|
||||
let newbuf = new Uint8Array(handle.buf.byteLength + buf2.byteLength);
|
||||
newbuf.set(new Uint8Array(handle.buf));
|
||||
newbuf.set(new Uint8Array(buf2), handle.buf.byteLength);
|
||||
handle.buf = newbuf;
|
||||
shim.process.__lasterr.errno = 0;
|
||||
return len;
|
||||
} else {
|
||||
debugger;
|
||||
throw new Error(`write ${typeof line} not implemented`);
|
||||
}
|
||||
} catch (e) {
|
||||
debugger;
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
chmodSync(fn: string, mode: number) {},
|
||||
fstatSync(fd: number) {
|
||||
let hand = shim.fds[fd];
|
||||
return { size: hand.buf.byteLength };
|
||||
},
|
||||
readSync(fd: number, buf: Buffer, start: number, len: number) {
|
||||
let hand = shim.fds[fd];
|
||||
let avail = hand.buf.length - hand.pos;
|
||||
let rd = Math.min(avail, len);
|
||||
let src = hand.buf;
|
||||
let dest = new Uint8Array(buf.buffer);
|
||||
for (let i = 0; i < rd; i++) dest[start + i] = src[hand.pos++];
|
||||
return rd;
|
||||
},
|
||||
closeSync(fd: number) {
|
||||
let handle = shim.fds[fd];
|
||||
// console.log("close", handle.name);
|
||||
if (handle.mode == "w") {
|
||||
shim.files[handle.name] = handle.buf;
|
||||
}
|
||||
delete shim.fds[fd];
|
||||
},
|
||||
},
|
||||
process: {
|
||||
platform: "linux",
|
||||
argv: ["", ""],
|
||||
stdout: {
|
||||
// We'll want to replace this one
|
||||
write(s) {
|
||||
console.log("*", s);
|
||||
shim.stdout += s;
|
||||
},
|
||||
},
|
||||
exit(v: number) {
|
||||
console.log("exit", v);
|
||||
},
|
||||
cwd() {
|
||||
return "";
|
||||
},
|
||||
env: {
|
||||
NO_COLOR: "true",
|
||||
IDRIS2_CG: "javascript",
|
||||
IDRIS2_PREFIX: "",
|
||||
},
|
||||
__lasterr: {
|
||||
errno: 0,
|
||||
},
|
||||
// stdin: { fd: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
// Spy on Idris' calls to see what we need to fill in
|
||||
shim.fs = new Proxy(shim.fs, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop in target) {
|
||||
return (target as any)[prop];
|
||||
}
|
||||
let err = new Error(`IMPLEMENT fs.${String(prop)}`);
|
||||
// idris support eats the exception
|
||||
console.error(err);
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
|
||||
// we intercept require to return our fake node modules
|
||||
declare global {
|
||||
interface Window {
|
||||
require: (x: string) => any;
|
||||
process: Process;
|
||||
}
|
||||
}
|
||||
const requireStub: any = (x: string) => (shim as any)[x];
|
||||
self.require = requireStub;
|
||||
self.process = shim.process;
|
||||
13
playground/src/global.d.ts
vendored
13
playground/src/global.d.ts
vendored
@@ -3,15 +3,8 @@ export {};
|
||||
declare global {
|
||||
// typescript doesn't know worker.ts is a worker
|
||||
function importScripts(...scripts: string[]): void;
|
||||
interface Process {
|
||||
platform: string;
|
||||
stdout: {
|
||||
write(s: string): void;
|
||||
};
|
||||
argv: string[];
|
||||
exit(_: number): void;
|
||||
}
|
||||
let files: Record<string, string>;
|
||||
let process: Process;
|
||||
|
||||
// let files: Record<string, string>;
|
||||
// let process: Process;
|
||||
let newtMain: () => unknown;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,18 @@ import { effect, signal } from "@preact/signals";
|
||||
import { newtConfig, newtTokens } from "./monarch.ts";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { h, render, VNode } from "preact";
|
||||
import { h, render } from "preact";
|
||||
import { ChangeEvent } from "preact/compat";
|
||||
import { archive, preload } from "./preload.ts";
|
||||
import { CompileReq, CompileRes } from "./types.ts";
|
||||
|
||||
// editor.(createModel / setModel / getModels) to switch files
|
||||
|
||||
// TODO - remember files and allow switching?
|
||||
|
||||
// static zip filesystem with user changes overlaid via localStorage
|
||||
// download individual files (we could use the cheap compression from the pdflib or no compression to make zip download)
|
||||
// would need way to reset/delete
|
||||
|
||||
interface FC {
|
||||
file: string;
|
||||
@@ -31,22 +41,27 @@ monaco.languages.registerDefinitionProvider("newt", {
|
||||
if (!topData) return;
|
||||
// HACK - we don't know our filename which was generated from `module` decl, so
|
||||
// assume the last context entry is in our file.
|
||||
let last = topData.context[topData.context.length-1]
|
||||
let file = last.fc.file
|
||||
let last = topData.context[topData.context.length - 1];
|
||||
let file = last.fc.file;
|
||||
|
||||
const info = model.getWordAtPosition(position);
|
||||
if (!info) return;
|
||||
let entry = topData.context.find((entry) => entry.name === info.word);
|
||||
// we can't switch files at the moment
|
||||
if (!entry || entry.fc.file !== file) return
|
||||
let lineNumber = entry.fc.line + 1
|
||||
let column = entry.fc.col + 1
|
||||
let word = model.getWordAtPosition({lineNumber, column})
|
||||
let range = new monaco.Range(lineNumber, column, lineNumber, column)
|
||||
if (!entry || entry.fc.file !== file) return;
|
||||
let lineNumber = entry.fc.line + 1;
|
||||
let column = entry.fc.col + 1;
|
||||
let word = model.getWordAtPosition({ lineNumber, column });
|
||||
let range = new monaco.Range(lineNumber, column, lineNumber, column);
|
||||
if (word) {
|
||||
range = new monaco.Range(lineNumber, word.startColumn, lineNumber, word.endColumn)
|
||||
range = new monaco.Range(
|
||||
lineNumber,
|
||||
word.startColumn,
|
||||
lineNumber,
|
||||
word.endColumn
|
||||
);
|
||||
}
|
||||
return { uri: model.uri,range}
|
||||
return { uri: model.uri, range };
|
||||
},
|
||||
});
|
||||
monaco.languages.registerHoverProvider("newt", {
|
||||
@@ -68,7 +83,7 @@ monaco.languages.registerHoverProvider("newt", {
|
||||
},
|
||||
});
|
||||
const newtWorker = new Worker("worker.js");
|
||||
let postMessage = (msg: any) => newtWorker.postMessage(msg);
|
||||
let postMessage = (msg: CompileReq) => newtWorker.postMessage(msg);
|
||||
|
||||
// Safari/MobileSafari have small stacks in webworkers.
|
||||
if (navigator.vendor.includes("Apple")) {
|
||||
@@ -117,20 +132,29 @@ function setOutput(output: string) {
|
||||
state.output.value = output;
|
||||
}
|
||||
|
||||
window.onmessage = (ev) => {
|
||||
interface ConsoleList {
|
||||
messages: string[]
|
||||
}
|
||||
interface ConsoleItem {
|
||||
message: string
|
||||
}
|
||||
|
||||
type WinMessage = CompileRes | ConsoleList | ConsoleItem
|
||||
|
||||
window.onmessage = (ev: MessageEvent<WinMessage>) => {
|
||||
console.log("window got", ev.data);
|
||||
if (ev.data.messages) state.messages.value = ev.data.messages;
|
||||
if (ev.data.message) {
|
||||
if ('messages' in ev.data) state.messages.value = ev.data.messages;
|
||||
if ('message' in ev.data) {
|
||||
state.messages.value = [...state.messages.value, ev.data.message];
|
||||
}
|
||||
// safari callback
|
||||
if (ev.data.output !== undefined) {
|
||||
if ('output' in ev.data) {
|
||||
setOutput(ev.data.output);
|
||||
state.javascript.value = ev.data.javascript;
|
||||
}
|
||||
};
|
||||
|
||||
newtWorker.onmessage = (ev) => {
|
||||
newtWorker.onmessage = (ev: MessageEvent<CompileRes>) => {
|
||||
setOutput(ev.data.output);
|
||||
state.javascript.value = ev.data.javascript;
|
||||
};
|
||||
@@ -169,9 +193,10 @@ if (window.matchMedia) {
|
||||
}
|
||||
|
||||
async function loadFile(fn: string) {
|
||||
if (fn) {
|
||||
const res = await fetch(fn);
|
||||
const text = await res.text();
|
||||
await preload;
|
||||
let data = archive?.getData(fn);
|
||||
if (data) {
|
||||
let text = new TextDecoder().decode(data);
|
||||
state.editor.value!.setValue(text);
|
||||
} else {
|
||||
state.editor.value!.setValue("module Main\n");
|
||||
|
||||
21
playground/src/preload.ts
Normal file
21
playground/src/preload.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {ZipFile} from './zipfile'
|
||||
export let archive: ZipFile | undefined;
|
||||
export let preload = (async function () {
|
||||
// We pull down an archive of .ttc and support shim.files
|
||||
try {
|
||||
let res = await self.fetch("files.zip");
|
||||
if (res.status === 200) {
|
||||
let data = await res.arrayBuffer();
|
||||
archive = new ZipFile(new Uint8Array(data));
|
||||
let entries = archive.entries;
|
||||
let count = Object.keys(entries).length;
|
||||
console.log(`preloaded ${count} files`);
|
||||
} else {
|
||||
console.error(
|
||||
`fetch of files.zip got status ${res.status}: ${res.statusText}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("preload failed", e);
|
||||
}
|
||||
})();
|
||||
9
playground/src/types.ts
Normal file
9
playground/src/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CompileReq {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export interface CompileRes {
|
||||
output: string
|
||||
javascript: string
|
||||
duration: number
|
||||
}
|
||||
@@ -1,165 +1,39 @@
|
||||
class Buffer extends ArrayBuffer {
|
||||
static alloc(n: number) {
|
||||
return new Buffer(n);
|
||||
}
|
||||
indexOf(n: number) {
|
||||
let view = new Uint8Array(this);
|
||||
return view.indexOf(n);
|
||||
}
|
||||
import { shim } from "./emul";
|
||||
import { archive, preload } from "./preload";
|
||||
import { CompileReq, CompileRes } from "./types";
|
||||
|
||||
static concat(bufs: Buffer[]) {
|
||||
let size = bufs.reduce((a, b) => (a += b.byteLength), 0);
|
||||
let rval = new Buffer(size);
|
||||
let view = new Uint8Array(rval);
|
||||
let off = 0;
|
||||
for (let buf of bufs) {
|
||||
view.set(new Uint8Array(buf), off);
|
||||
off += buf.byteLength;
|
||||
}
|
||||
return rval;
|
||||
}
|
||||
toString() {
|
||||
return new TextDecoder().decode(this);
|
||||
}
|
||||
}
|
||||
|
||||
let files: Record<string, string> = {};
|
||||
interface Handle {
|
||||
name: string;
|
||||
mode: string;
|
||||
pos: number;
|
||||
buf: Buffer;
|
||||
}
|
||||
let fds: Handle[] = [];
|
||||
|
||||
let shim: any = {
|
||||
os: {
|
||||
platform() {
|
||||
return "linux";
|
||||
},
|
||||
},
|
||||
fs: {
|
||||
openSync(name: string, mode: string) {
|
||||
console.log("open", name, arguments);
|
||||
let te = new TextEncoder();
|
||||
|
||||
let fd = fds.findIndex((x) => !x);
|
||||
if (fd < 0) fd = fds.length;
|
||||
let buf;
|
||||
let pos = 0;
|
||||
if (mode == "w") {
|
||||
buf = new Buffer(0);
|
||||
} else {
|
||||
if (!files[name]) throw new Error(`${name} not found`);
|
||||
buf = te.encode(files[name]);
|
||||
}
|
||||
fds[fd] = { buf, pos, mode, name };
|
||||
// we'll mutate the pointer as stuff is read
|
||||
return fd;
|
||||
},
|
||||
writeSync(fd: number, line: string) {
|
||||
if (typeof line !== "string") throw new Error("not implemented");
|
||||
let handle = fds[fd];
|
||||
if (!handle) throw new Error(`bad fd ${fd}`)
|
||||
let buf2 = new TextEncoder().encode(line);
|
||||
handle.buf = Buffer.concat([handle.buf, buf2])
|
||||
},
|
||||
chmodSync(fn: string, mode: number) { },
|
||||
readSync(fd: number, buf: Buffer, start: number, len: number) {
|
||||
let hand = fds[fd];
|
||||
let avail = hand.buf.byteLength - hand.pos;
|
||||
let rd = Math.min(avail, len);
|
||||
let src = new Uint8Array(hand.buf);
|
||||
let dest = new Uint8Array(buf);
|
||||
for (let i = 0; i < rd; i++) dest[start + i] = src[hand.pos++];
|
||||
return rd;
|
||||
},
|
||||
closeSync(fd: number) {
|
||||
let handle = fds[fd];
|
||||
if (handle.mode == "w") {
|
||||
files[handle.name] = new TextDecoder().decode(handle.buf);
|
||||
}
|
||||
delete fds[fd];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Spy on Idris' calls to see what we need to fill in
|
||||
shim.fs = new Proxy(shim.fs, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop in target) {
|
||||
return (target as any)[prop];
|
||||
}
|
||||
throw new Error(`implement fs.${String(prop)}`)
|
||||
},
|
||||
});
|
||||
|
||||
const process: Process = {
|
||||
platform: "linux",
|
||||
argv: ["", ""],
|
||||
stdout: {
|
||||
// We'll want to replace this one
|
||||
write: console.log,
|
||||
},
|
||||
exit(v: number) {
|
||||
console.log("exit", v);
|
||||
},
|
||||
// stdin: { fd: 0 },
|
||||
};
|
||||
|
||||
const require = (x: string) => shim[x];
|
||||
|
||||
// Maybe the shim goes here and we append newt...
|
||||
|
||||
let stdout = ''
|
||||
// We'll want to collect and put info in the monaco
|
||||
process.stdout.write = (s) => {
|
||||
stdout += s
|
||||
};
|
||||
// hack for now
|
||||
const preload = [
|
||||
"Prelude.newt",
|
||||
"Web.newt",
|
||||
"aoc2023/day1/eg.txt",
|
||||
"aoc2023/day1/eg2.txt",
|
||||
]
|
||||
const handleMessage = async function (e) {
|
||||
console.log('message for you sir', e.data)
|
||||
for (let fn of preload) {
|
||||
|
||||
if (!files[fn]) {
|
||||
console.log('preload', fn)
|
||||
let res = await fetch(fn)
|
||||
let text = await res.text()
|
||||
files[fn] = text
|
||||
}
|
||||
}
|
||||
let {src} = e.data
|
||||
let module = 'Main'
|
||||
let m = src.match(/module (\w+)/)
|
||||
if (m) module = m[1]
|
||||
let fn = `${module}.newt`
|
||||
process.argv = ["", "", fn, "-o", "out.js", "--top"];
|
||||
console.log("args", process.argv);
|
||||
files[fn] = src;
|
||||
files['out.js'] = 'No JS output';
|
||||
stdout = ''
|
||||
const start = +new Date()
|
||||
const handleMessage = async function (ev: {data: CompileReq}) {
|
||||
console.log("message", ev.data);
|
||||
await preload;
|
||||
shim.archive = archive;
|
||||
let { src } = ev.data;
|
||||
let module = "Main";
|
||||
let m = src.match(/module (\w+)/);
|
||||
if (m) module = m[1];
|
||||
let fn = `${module}.newt`;
|
||||
const outfile = "out.js";
|
||||
shim.process.argv = ["", "", fn, "-o", outfile, "--top"];
|
||||
console.log("Using args", shim.process.argv);
|
||||
shim.files[fn] = new TextEncoder().encode(src);
|
||||
shim.files[outfile] = new TextEncoder().encode("No JS output");
|
||||
shim.stdout = "";
|
||||
const start = +new Date();
|
||||
try {
|
||||
newtMain();
|
||||
} catch (e) {
|
||||
// make it clickable
|
||||
console.error(e)
|
||||
// make it visable
|
||||
stdout += '\n' + String(e)
|
||||
// make it clickable in console
|
||||
console.error(e);
|
||||
// make it visable on page
|
||||
shim.stdout += "\n" + String(e);
|
||||
}
|
||||
let duration = +new Date() - start
|
||||
console.log(`process ${fn} in ${duration} ms`)
|
||||
let javascript = files['out.js']
|
||||
let output = stdout
|
||||
sendResponse({javascript, output, duration})
|
||||
}
|
||||
let sendResponse = postMessage
|
||||
onmessage = handleMessage
|
||||
let duration = +new Date() - start;
|
||||
console.log(`shim.process ${fn} in ${duration} ms`);
|
||||
let javascript = new TextDecoder().decode(shim.files[outfile]);
|
||||
let output = shim.stdout;
|
||||
sendResponse({ javascript, output, duration });
|
||||
};
|
||||
|
||||
importScripts('newt.js')
|
||||
// hooks for worker.html to override
|
||||
let sendResponse : (_: CompileRes) => void = postMessage;
|
||||
onmessage = handleMessage;
|
||||
importScripts("newt.js");
|
||||
|
||||
305
playground/src/zipfile.ts
Normal file
305
playground/src/zipfile.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
|
||||
// I wrote this inflate years ago, seems to work for zip
|
||||
class BitReader {
|
||||
pos = 0;
|
||||
bits = 0;
|
||||
acc = 0;
|
||||
len: number;
|
||||
data: Uint8Array;
|
||||
constructor(data: Uint8Array) {
|
||||
this.data = data;
|
||||
this.len = data.length;
|
||||
}
|
||||
read(bits: number) {
|
||||
while (this.bits < bits) {
|
||||
if (this.pos >= this.len) throw "EOF";
|
||||
|
||||
this.acc |= this.data[this.pos++] << this.bits;
|
||||
this.bits += 8;
|
||||
}
|
||||
let rval = this.acc & ((1 << bits) - 1);
|
||||
this.acc >>= bits;
|
||||
this.bits -= bits;
|
||||
return rval;
|
||||
}
|
||||
read8() {
|
||||
if (this.pos > this.len) throw "EOF";
|
||||
|
||||
// flush
|
||||
if (this.bits > 0) {
|
||||
this.bits = 0;
|
||||
this.acc = 0;
|
||||
}
|
||||
return this.data[this.pos++];
|
||||
}
|
||||
read16() {
|
||||
let rval = this.read8() * 256 + this.read8();
|
||||
return rval;
|
||||
}
|
||||
}
|
||||
|
||||
class HuffDic {
|
||||
limit: number[];
|
||||
codes: number[];
|
||||
base: number[];
|
||||
constructor(lengths: number[]) {
|
||||
let counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
let min = 0;
|
||||
let max = 0;
|
||||
|
||||
for (let i = 0; i < lengths.length; i++) {
|
||||
let len = lengths[i];
|
||||
if (len != 0) {
|
||||
if (len < min || min == 0) min = len;
|
||||
if (len > max) max = len;
|
||||
counts[len]++;
|
||||
}
|
||||
}
|
||||
|
||||
this.base = [];
|
||||
this.limit = [
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
];
|
||||
|
||||
let code = 0;
|
||||
let seq = 0;
|
||||
let next_code = [];
|
||||
|
||||
for (let i = min; i <= max; i++) {
|
||||
let n = counts[i];
|
||||
next_code[i] = code;
|
||||
this.base[i] = code - seq;
|
||||
code += n;
|
||||
seq += n;
|
||||
this.limit[i] = code - 1;
|
||||
code <<= 1;
|
||||
}
|
||||
|
||||
this.codes = [];
|
||||
for (let i = 0; i < lengths.length; i++) {
|
||||
let n = lengths[i];
|
||||
if (n != 0) {
|
||||
code = next_code[n];
|
||||
next_code[n]++;
|
||||
if (!this.base[n]) this.base[n] = 0;
|
||||
seq = code - this.base[n];
|
||||
this.codes[seq] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readSymbol(r: BitReader) {
|
||||
let v = 0;
|
||||
let l = 0;
|
||||
let offset = 0;
|
||||
for (let i = 1; i < this.limit.length; i++) {
|
||||
v <<= 1;
|
||||
v |= r.read(1);
|
||||
|
||||
let limit = this.limit[i];
|
||||
if (v <= limit) {
|
||||
return this.codes[v - this.base[i]];
|
||||
}
|
||||
}
|
||||
throw "eHUFF";
|
||||
}
|
||||
}
|
||||
let staticHuff: HuffDic;
|
||||
let distHuff: HuffDic;
|
||||
{
|
||||
let tmp = [];
|
||||
|
||||
for (let i = 0; i < 144; i++) tmp[i] = 8;
|
||||
for (let i = 144; i < 256; i++) tmp[i] = 9;
|
||||
for (let i = 256; i < 280; i++) tmp[i] = 7;
|
||||
for (let i = 280; i < 288; i++) tmp[i] = 8;
|
||||
|
||||
staticHuff = new HuffDic(tmp);
|
||||
tmp = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
tmp[i] = 5;
|
||||
}
|
||||
distHuff = new HuffDic(tmp);
|
||||
}
|
||||
|
||||
function inflate(input: Uint8Array) {
|
||||
let r = new BitReader(input);
|
||||
let out = new Uint8Array(65536);
|
||||
let pos = 0;
|
||||
const push = (b: number) => {
|
||||
if (pos + 10 > out.length) {
|
||||
const tmp = new Uint8Array(out.length * 1.5);
|
||||
tmp.set(out);
|
||||
out = tmp;
|
||||
}
|
||||
out[pos++] = b;
|
||||
};
|
||||
|
||||
let fin = 0;
|
||||
while (!fin) {
|
||||
fin = r.read(1);
|
||||
let btype = r.read(2);
|
||||
|
||||
let huff2;
|
||||
let huff3;
|
||||
|
||||
if (btype == 0) {
|
||||
let len = r.read16();
|
||||
let nlen = r.read16();
|
||||
for (let i = 0; i < len; i++) push(r.read8());
|
||||
} else if (btype == 1) {
|
||||
// fixed huffman
|
||||
huff2 = staticHuff;
|
||||
huff3 = distHuff;
|
||||
} else if (btype == 2) {
|
||||
// dynamic huffman
|
||||
let hlit = r.read(5) + 257;
|
||||
let hdist = r.read(5) + 1;
|
||||
let hclen = r.read(4) + 4;
|
||||
let lengths: number[] = [];
|
||||
for (let i = 0; i < 19; i++) lengths[i] = 0;
|
||||
|
||||
let xx = [
|
||||
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
|
||||
];
|
||||
for (let i = 0; i < hclen; i++) {
|
||||
let t = r.read(3);
|
||||
lengths[xx[i]] = t;
|
||||
}
|
||||
|
||||
let huff = new HuffDic(lengths);
|
||||
|
||||
lengths = [];
|
||||
while (true) {
|
||||
let k = huff.readSymbol(r);
|
||||
if (k < 16) {
|
||||
lengths.push(k);
|
||||
} else if (k == 16) {
|
||||
let count = r.read(2) + 3;
|
||||
if (lengths.length == 0) throw new Error("no lengths?");
|
||||
for (; count > 0; count--) lengths.push(lengths[lengths.length - 1]);
|
||||
} else if (k == 17) {
|
||||
let count = r.read(3) + 3;
|
||||
for (; count > 0; count--) lengths.push(0);
|
||||
} else if (k == 18) {
|
||||
let count = r.read(7) + 11;
|
||||
for (; count > 0; count--) lengths.push(0);
|
||||
}
|
||||
if (lengths.length >= hlit + hdist) break;
|
||||
}
|
||||
|
||||
huff2 = new HuffDic(lengths.slice(0, hlit));
|
||||
huff3 = new HuffDic(lengths.slice(hlit));
|
||||
} else {
|
||||
throw new Error("btype " + btype);
|
||||
}
|
||||
|
||||
if (huff2) {
|
||||
while (true) {
|
||||
let k = huff2.readSymbol(r);
|
||||
let len = 0;
|
||||
let n = 0; // extra bits
|
||||
if (k < 256) {
|
||||
push(k);
|
||||
continue;
|
||||
} else if (k == 256) {
|
||||
// End of block
|
||||
break;
|
||||
} else if (k < 265) {
|
||||
len = k - 257 + 3;
|
||||
n = 0;
|
||||
} else if (k < 269) {
|
||||
len = (k - 265) * 2 + 11;
|
||||
n = 1;
|
||||
} else if (k < 273) {
|
||||
len = (k - 269) * 4 + 19;
|
||||
n = 2;
|
||||
} else if (k < 277) {
|
||||
len = (k - 273) * 8 + 35;
|
||||
n = 3;
|
||||
} else if (k < 281) {
|
||||
len = (k - 277) * 16 + 67;
|
||||
n = 4;
|
||||
} else if (k < 285) {
|
||||
len = (k - 281) * 32 + 131;
|
||||
n = 5;
|
||||
} else {
|
||||
len = 258;
|
||||
n = 0;
|
||||
}
|
||||
|
||||
if (n > 0) len += r.read(n);
|
||||
|
||||
// distance
|
||||
|
||||
if (r.pos > r.len) throw new Error("EOF");
|
||||
|
||||
let dist;
|
||||
if (huff3) dist = huff3.readSymbol(r);
|
||||
else dist = r.read(5);
|
||||
|
||||
if (dist < 4) {
|
||||
dist++;
|
||||
} else if (dist < 30) {
|
||||
let db = (dist - 2) >> 1;
|
||||
let extra = (dist & 1) << db;
|
||||
extra |= r.read(db);
|
||||
dist = (1 << (db + 1)) + 1 + extra;
|
||||
} else {
|
||||
throw new Error(`dist ${dist}`);
|
||||
}
|
||||
|
||||
if (dist > pos) throw new Error(`dist ${dist} > pos ${pos}`);
|
||||
|
||||
let s: number = pos - dist;
|
||||
|
||||
for (let i = 0; i < len; i++) push(out[s + i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.slice(0, pos);
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
size: number;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export class ZipFile {
|
||||
data: Uint8Array;
|
||||
entries: Record<string, Entry>;
|
||||
constructor(data: Uint8Array) {
|
||||
this.data = data;
|
||||
this.entries = {};
|
||||
let td = new TextDecoder();
|
||||
let error = (msg: string) => {
|
||||
throw new Error(`${msg} at ${pos}`);
|
||||
};
|
||||
|
||||
let view = new DataView(data.buffer);
|
||||
let pos = 0;
|
||||
while (pos < view.byteLength) {
|
||||
let sig = view.getUint32(pos, true);
|
||||
if (sig == 0x02014b50) break;
|
||||
if (sig != 0x04034b50) error(`bad sig ${sig.toString(16)}`);
|
||||
let method = view.getUint16(pos + 8, true);
|
||||
let csize = view.getUint32(pos + 18, true);
|
||||
let size = view.getUint32(pos + 22, true);
|
||||
let fnlen = view.getUint16(pos + 26, true);
|
||||
let eflen = view.getUint16(pos + 28, true);
|
||||
let fn = td.decode(data.slice(pos + 30, pos + 30 + fnlen));
|
||||
if (size) {
|
||||
let start = pos + 30 + fnlen + eflen;
|
||||
let end = start + csize;
|
||||
this.entries[fn] = { size, start, end };
|
||||
}
|
||||
pos = pos + 30 + fnlen + eflen + csize;
|
||||
}
|
||||
}
|
||||
getData(name: string) {
|
||||
let { start, end, size } = this.entries[name];
|
||||
return inflate(new Uint8Array(this.data.slice(start, end)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user