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"
]
}
},