diff --git a/config/mime.json b/config/mime.json index d0b0ceea..514c68f3 100644 --- a/config/mime.json +++ b/config/mime.json @@ -4,6 +4,7 @@ "3gp": "video/3gpp", "3gpp": "video/3gpp", "7z": "application/x-7z-compressed", + "a": "application/x-archive", "ai": "application/pdf", "aif": "audio/x-aiff", "aiff": "audio/x-aiff", @@ -169,6 +170,7 @@ "war": "application/java-archive", "wav": "audio/wave", "wave": "audio/wave", + "wasm": "application/wasm", "wbmp": "image/vnd.wap.wbmp", "webm": "video/webm", "webp": "image/webp", diff --git a/public/assets/helpers/loader_wasm.js b/public/assets/helpers/loader_wasm.js new file mode 100644 index 00000000..768f2712 --- /dev/null +++ b/public/assets/helpers/loader_wasm.js @@ -0,0 +1,172 @@ +export default async function(baseURL, path) { + const wasi = new Wasi(); + const wasm = await WebAssembly.instantiateStreaming( + fetch(new URL(path, baseURL)), { + wasi_snapshot_preview1: { + ...wasi, + }, + env: { + ...wasi, + ...syscalls, + }, + }, + ); + wasi.instance = wasm.instance; + return wasm; +} + +const FS = {}; +let nextFd = 0; +writeFS(new Uint8Array(), 0); // stdin +writeFS(new Uint8Array(), 1); // stdout +writeFS(new Uint8Array(), 2); // stderr +if (nextFd !== 3) throw new Error("Unexpected next fd"); + +export function writeFS(buffer, fd) { + if (fd === undefined) fd = nextFd; + else if (!(buffer instanceof Uint8Array)) throw new Error("can only write Uint8Array"); + + FS[fd] = { + buffer, + position: 0, + }; + nextFd += 1; + return nextFd - 1; +} + +export function readFS(fd) { + if (fd < 3) throw new Error("cannot read from stdin, stdout or stderr"); + const file = FS[fd]; + if (!file) throw new Error("file does not exist"); + return file.buffer; +} + +export const syscalls = { + __syscall_fcntl64: (fd, cmd, varargs) => { + console.log(`Stubbed __syscall_fcntl64 called with fd=${fd}, cmd=${cmd}, varargs=${varargs}`); + return -1; + }, + __syscall_ioctl: (fd, op, varargs) => { + switch (op) { + case 21523: + break; + default: + console.log(`Stubbed __syscall_ioctl called with fd=${fd}, request=${op}, varargs=${varargs}`); + } + return 0; + }, +}; + +export class Wasi { + #instance; + + constructor() { + this.fd_read = this.fd_read.bind(this); + this.fd_write = this.fd_write.bind(this); + this.fd_seek = this.fd_seek.bind(this); + this.fd_close = this.fd_close.bind(this); + } + + set instance(val) { + this.#instance = val; + } + + fd_write(fd, iovs, iovs_len, nwritten) { + if (!FS[fd]) { + console.error(`Invalid fd: ${fd}`); + return -1; + } + let output = FS[fd].buffer; + const ioVecArray = new Uint32Array(this.#instance.exports.memory.buffer, iovs, iovs_len * 2); + const memory = new Uint8Array(this.#instance.exports.memory.buffer); + let totalBytesWritten = 0; + for (let i = 0; i < iovs_len * 2; i += 2) { + const sub = memory.subarray( + (ioVecArray[i] || 0), + (ioVecArray[i] || 0) + (ioVecArray[i+1] || 0), + ); + const tmp = new Uint8Array(output.byteLength + sub.byteLength); + tmp.set(output, 0); + tmp.set(sub, output.byteLength); + output = tmp; + totalBytesWritten += ioVecArray[i+1] || 0; + } + const dataView = new DataView(this.#instance.exports.memory.buffer); + dataView.setUint32(nwritten, totalBytesWritten, true); + + FS[fd].buffer = output; + if (fd < 3 && fd >= 0) { + const msg = fd === 1 ? "stdout" : fd === 2 ? "stderr" : "stdxx"; + console.log(msg + ": " + (new TextDecoder()).decode(output)); + FS[fd].buffer = new ArrayBuffer(0); + } + return 0; + } + + fd_read(fd, iovs, iovs_len, nread) { + const file = FS[fd]; + if (!file) { + console.error(`Invalid fd: ${fd}`); + return -1; + } + + const ioVecArray = new Uint32Array(this.#instance.exports.memory.buffer, iovs, iovs_len * 2); + const memory = new Uint8Array(this.#instance.exports.memory.buffer); + let totalBytesRead = 0; + for (let i = 0; i < iovs_len * 2; i += 2) { + const offset = ioVecArray[i]; + const length = ioVecArray[i + 1] || 0; + const bytesToRead = Math.min( + length, + file.buffer.length - file.position, + ); + if (bytesToRead <= 0) { + break; + } + memory.set( + file.buffer.subarray(file.position, file.position + bytesToRead), + offset, + ); + file.position += bytesToRead; + totalBytesRead += bytesToRead; + } + + const dataView = new DataView(this.#instance.exports.memory.buffer); + dataView.setUint32(nread, totalBytesRead, true); + return 0; + } + + fd_seek(fd, offsetBigInt, whence) { + const offset = Number(offsetBigInt); + const file = FS[fd]; + if (!file) { + console.error(`Invalid FD: ${fd}`); + return -1; + } + switch (whence) { + case 0: // SEEK_SET + file.position = offset; + break; + case 1: // SEEK_CUR + file.position += offset; + break; + case 2: // SEEK_END + file.position = file.buffer.length + offset; + break; + default: + console.log(`fd_seek called with fd=${fd}, offset=${offset}, position=${file.position} whence=${whence}`); + const error = new Error("fd_seek trace"); + console.log("Invalid whence", error.stack); + return -1; + } + return 0; + } + + fd_close(fd) { + if (!FS[fd]) { + console.error(`Invalid FD: ${fd}`); + return -1; + } + return 0; + } +} diff --git a/public/assets/pages/filespage/ctrl_filesystem.js b/public/assets/pages/filespage/ctrl_filesystem.js index cfa131f3..b098a623 100644 --- a/public/assets/pages/filespage/ctrl_filesystem.js +++ b/public/assets/pages/filespage/ctrl_filesystem.js @@ -181,9 +181,9 @@ export default async function(render) { $list, virtual, }) => ( - virtual ? - rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true }) : - rxjs.EMPTY + virtual + ? rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true }) + : rxjs.EMPTY ).pipe( rxjs.map((e) => { // 0-------------0-----------1-----------2-----------3 .... diff --git a/public/assets/pages/viewerpage/application_table.js b/public/assets/pages/viewerpage/application_table.js index 0d89b07b..7b772dc4 100644 --- a/public/assets/pages/viewerpage/application_table.js +++ b/public/assets/pages/viewerpage/application_table.js @@ -3,11 +3,14 @@ import rxjs, { effect } from "../../lib/rx.js"; import { qs, qsa } from "../../lib/dom.js"; import ajax from "../../lib/ajax.js"; import { loadCSS } from "../../helpers/loader.js"; +import t from "../../locales/index.js"; import ctrlError from "../ctrl_error.js"; import { renderMenubar, buttonDownload } from "./component_menubar.js"; +import { getLoader } from "./application_table/loader.js"; +import { transition } from "./common.js"; -export default async function(render, { getDownloadUrl = nop, getFilename = nop, hasMenubar = true }) { +export default async function(render, { mime, getDownloadUrl = nop, getFilename = nop, hasMenubar = true }) { const $page = createElement(`
@@ -33,29 +36,21 @@ export default async function(render, { getDownloadUrl = nop, getFilename = nop, // feature: initial render const init$ = ajax({ url: getDownloadUrl(), responseType: "arraybuffer" }).pipe( rxjs.mergeMap(async({ response }) => { - const module = await import("../../lib/vendor/shp-to-geojson.browser.js"); - const data = new module.DBase(module.Buffer.from(response)); - - const dummy = []; - // const dummy = new Array(50).fill({}).map((_, i) => ({ - // fieldName: "dummy"+i, - // fieldLength: 4, - // })); - data.properties = data.properties.concat(dummy); + const table = new (await getLoader(mime))(response); // build head const $tr = createElement(`
`); - data.properties.forEach(({ fieldName, fieldLength }) => { + table.getHeader().forEach(({ name, size }) => { const $th = createElement(` -
- ${fieldName} +
+ ${name}
`); let ascending = null; qs($th, "img").onclick = (e) => { ascending = !ascending; - sortBy(qsa($dom.tbody, `.tr [data-column="${fieldName}"]`), ascending); + sortBy(qsa($dom.tbody, `.tr [data-column="${name}"]`), ascending); qsa(e.target.closest(".tr"), "img").forEach(($img) => { $img.style.transform = "rotate(0deg)"; }); @@ -66,18 +61,24 @@ export default async function(render, { getDownloadUrl = nop, getFilename = nop, $dom.thead.appendChild($tr); // build body - for (let i =0; i { const $tr = createElement(`
`); - data.properties.forEach(({ fieldName, fieldLength }) => { + table.getHeader().forEach(({ name, size }) => { $tr.appendChild(createElement(` -
- ${row[fieldName] || "-"} +
+ ${obj[name] || "-"}
`)); }); $dom.tbody.appendChild($tr); - } + }); + if (body.length === 0) $dom.tbody.appendChild(createElement(` +

+ ${t("Empty")} +

+ `)); + transition($dom.tbody.parentElement); }), rxjs.share(), rxjs.catchError(ctrlError()), diff --git a/public/assets/pages/viewerpage/application_table/Makefile b/public/assets/pages/viewerpage/application_table/Makefile new file mode 100644 index 00000000..9a7ddbc9 --- /dev/null +++ b/public/assets/pages/viewerpage/application_table/Makefile @@ -0,0 +1,2 @@ +all: + emcc loader_symbol.c -o loader_symbol.wasm -O2 --no-entry diff --git a/public/assets/pages/viewerpage/application_table/loader.js b/public/assets/pages/viewerpage/application_table/loader.js new file mode 100644 index 00000000..4a638d3e --- /dev/null +++ b/public/assets/pages/viewerpage/application_table/loader.js @@ -0,0 +1,24 @@ +// import loaderDBase from "./loader_dbase.js"; +// import loaderSymbol from "./loader_symbol.js"; + +class ITable { + contructor() {} + getHeader() { throw new Error("NOT_IMPLEMENTED"); } + getBody() { throw new Error("NOT_IMPLEMENTED"); } +} + +export async function getLoader(mime) { + let module = null; + switch (mime) { + case "application/dbf": + module = await import("./loader_dbase.js"); + break; + case "application/x-archive": + module = await import("./loader_symbol.js"); + break; + default: + throw new TypeError(`unsupported mimetype '${mime}'`); + } + + return module.default(ITable); +} diff --git a/public/assets/pages/viewerpage/application_table/loader_dbase.js b/public/assets/pages/viewerpage/application_table/loader_dbase.js new file mode 100644 index 00000000..d112b19e --- /dev/null +++ b/public/assets/pages/viewerpage/application_table/loader_dbase.js @@ -0,0 +1,26 @@ +export default async function(ITable) { + const module = await import("../../../lib/vendor/shp-to-geojson.browser.js"); + + return class TableImpl extends ITable { + constructor(response) { + super(); + this.data = new module.DBase(module.Buffer.from(response)); + } + + getHeader() { + return this.data.properties.map(({ fieldName, fieldLength }) => ({ + name: fieldName, + size: fieldLength, + })); + } + + getBody() { + const body = []; + for (let i =0; i +#include +#include +#include + +#define ARMAG "!\n" +#define SARMAG sizeof(ARMAG) - 1 +#define AR_HDR_SIZE 60 + +struct ar_hdr { + char name[16]; + char timestamp[12]; + char owner[6]; + char group[6]; + char mode[8]; + char size[10]; + char fmag[2]; +}; + +EMSCRIPTEN_KEEPALIVE int execute(int fdinput, int fdoutput) { + if (fdinput == 0) { + fprintf(stderr, "ERROR - missing input %d\n", fdinput); + return 1; + } + if (fdoutput == 0) { + fprintf(stderr, "ERROR - missing input %d\n", fdoutput); + return 1; + } + + FILE* finput = fdopen(fdinput, "rb"); + if (!finput) { + fprintf(stderr, "ERROR - cannot open input file\n"); + return 1; + } + FILE* foutput = fdopen(fdoutput, "wb"); + if (!foutput) { + fprintf(stderr, "ERROR - cannot open output file\n"); + fclose(finput); + return 1; + } + + char magic[SARMAG]; + size_t c = fread(magic, 1, SARMAG, finput); + if (c == 0) { + fprintf(stderr, "ERROR count=%zu error=%d\n", c, ferror(finput)); + fclose(finput); + fclose(foutput); + return 1; + } + if (strncmp(magic, ARMAG, SARMAG) != 0) { + fprintf(stderr, "ERROR file is not of the expected shape"); + fclose(finput); + fclose(foutput); + return 1; + } + + struct ar_hdr header; + while (fread(&header, 1, AR_HDR_SIZE, finput) == AR_HDR_SIZE) { + if (strncmp(header.fmag, "`\n", 2) != 0) { + fprintf(stderr, "Invalid header format.\n"); + break; + } + long size = strtol(header.size, NULL, 10); + char filename[17] = {0}; + strncpy(filename, header.name, 16); + for (int i = strlen(filename) - 1; i >= 0; i--) { + if (filename[i] == ' ' || filename[i] == '/') { + filename[i] = '\0'; + } + } + if (strlen(filename) > 0) fprintf( + foutput, + "%.16s,%ld,%ld,%ld,%ld,%ld\n", + filename, + strtol(header.timestamp, NULL, 10), + strtol(header.owner, NULL, 10), + strtol(header.group, NULL, 10), + strtol(header.mode, NULL, 10), + size, + ); + fseek(finput, (size + 1) & ~1, SEEK_CUR); + } + + fflush(foutput); + fclose(foutput); + fclose(finput); + return 0; +} diff --git a/public/assets/pages/viewerpage/application_table/loader_symbol.js b/public/assets/pages/viewerpage/application_table/loader_symbol.js new file mode 100644 index 00000000..2c0c9d3e --- /dev/null +++ b/public/assets/pages/viewerpage/application_table/loader_symbol.js @@ -0,0 +1,45 @@ +import assert from "../../../lib/assert.js"; +import loadWASM, { writeFS, readFS } from "../../../helpers/loader_wasm.js"; + +export default async function(ITable) { + const { instance } = await loadWASM(import.meta.url, "./loader_symbol.wasm"); + + return class TableImpl extends ITable { + constructor(response) { + super(); + const fdIn = writeFS(new Uint8Array(response)); + const fdOut = writeFS(new Uint8Array([])); + const res = assert.truthy(instance.exports["execute"])(fdIn, fdOut); + if (res !== 0) throw new Error(`WASM exited with code=${res}`); + const buffer = readFS(fdOut); + + this.header = [ + { name: "Mode", size: 3 }, + { name: "Timestamp", size: 6 }, + { name: "Size", size: 4 }, + { name: "Owner", size: 6 }, + { name: "Group", size: 6 }, + { name: "Object", size: 40 }, + ]; + this.rows = new TextDecoder().decode(buffer).trimRight().split("\n").map((line) => { + const row = line.split(","); + return { + "Object": row[0], + "Timestamp": row[1], + "Size": row[5], + "Mode": row[4], + "Owner": row[2], + "Group": row[3], + }; + }); + } + + getHeader() { + return this.header; + } + + getBody() { + return this.rows; + } + }; +} diff --git a/public/assets/pages/viewerpage/application_table/loader_symbol.wasm b/public/assets/pages/viewerpage/application_table/loader_symbol.wasm new file mode 100755 index 00000000..058618fe Binary files /dev/null and b/public/assets/pages/viewerpage/application_table/loader_symbol.wasm differ diff --git a/public/assets/pages/viewerpage/mimetype.js b/public/assets/pages/viewerpage/mimetype.js index e8e66cf3..37f9757b 100644 --- a/public/assets/pages/viewerpage/mimetype.js +++ b/public/assets/pages/viewerpage/mimetype.js @@ -34,7 +34,7 @@ export function opener(file = "", mimes) { return ["3d", { mime }]; } else if (mime === "application/x-url") { return ["url", { mime }]; - } else if (["application/dbf"].indexOf(mime) !== -1) { + } else if (["application/dbf", "application/x-archive"].indexOf(mime) !== -1) { return ["table", { mime }]; } else if (type === "application" && mime !== "application/text") { return ["download", { mime }]; diff --git a/public/package.json b/public/package.json index 4248f521..9056bb4e 100644 --- a/public/package.json +++ b/public/package.json @@ -130,6 +130,9 @@ ], "new-cap": [ "off" + ], + "accessor-pairs": [ + "off" ] } },