pvbemu/app/_boot.js

309 lines
11 KiB
JavaScript

/*
The Bundle.java utility prepends a file manifest to this script before
execution is started. This script runs within the context of an async
function.
*/
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// Bundled file manager
let Bundle = globalThis.Bundle = new class Bundle extends Array {
constructor() {
super();
// Configure instance fields
this.debug =
location.protocol != "file:" && location.hash == "#debug";
this.decoder = new TextDecoder();
this.encoder = new TextEncoder();
this.moduleCall = (... a)=>this.module (... a);
this.resourceCall = (... a)=>this.resource(... a);
// Generate the CRC32 lookup table
this.crcLookup = new Uint32Array(256);
for (let x = 0; x <= 255; x++) {
let l = x;
for (let j = 7; j >= 0; j--)
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
this.crcLookup[x] = l;
}
}
///////////////////////////// Public Methods //////////////////////////////
// Add a file to the bundle
add(name, data) {
this.push(this[name] = new BundledFile(name, data));
}
// Export all bundled resources to a .ZIP file
save() {
let centrals = new Array(this.length);
let locals = new Array(this.length);
let offset = 0;
let size = 0;
// Encode file and directory entries
for (let x = 0; x < this.length; x++) {
let file = this[x];
let sum = this.crc32(file.data);
locals [x] = file.toZipHeader(sum);
centrals[x] = file.toZipHeader(sum, offset);
offset += locals [x].length;
size += centrals[x].length;
}
// Encode end of central directory
let end = [];
this.writeInt(end, 4, 0x06054B50); // Signature
this.writeInt(end, 2, 0); // Disk number
this.writeInt(end, 2, 0); // Central dir start disk
this.writeInt(end, 2, this.length); // # central dir this disk
this.writeInt(end, 2, this.length); // # central dir total
this.writeInt(end, 4, size); // Size of central dir
this.writeInt(end, 4, offset); // Offset of central dir
this.writeInt(end, 2, 0); // .ZIP comment length
// Prompt the user to save the resulting file
let a = document.createElement("a");
a.download = bundleName + ".zip";
a.href = URL.createObjectURL(new Blob(
locals.concat(centrals).concat([Uint8Array.from(end)]),
{ type: "application/zip" }
));
a.style.visibility = "hidden";
document.body.appendChild(a);
a.click();
a.remove();
}
///////////////////////////// Package Methods /////////////////////////////
// Write a byte array into an output buffer
writeBytes(data, bytes) {
//data.push(... bytes);
for (let b of bytes)
data.push(b);
}
// Write an integer into an output buffer
writeInt(data, size, value) {
for (; size > 0; size--, value >>= 8)
data.push(value & 0xFF);
}
// Write a string of text as bytes into an output buffer
writeString(data, text) {
data.push(... this.encoder.encode(text));
}
///////////////////////////// Private Methods /////////////////////////////
// Calculate the CRC32 checksum for a byte array
crc32(data) {
let c = 0xFFFFFFFF;
for (let x = 0; x < data.length; x++)
c = ((c >>> 8) ^ this.crcLookup[(c ^ data[x]) & 0xFF]);
return ~c & 0xFFFFFFFF;
}
}();
///////////////////////////////////////////////////////////////////////////////
// BundledFile //
///////////////////////////////////////////////////////////////////////////////
// Individual bundled file
class BundledFile {
constructor(name, data) {
// Configure instance fields
this.data = data;
this.name = name;
// Resolve the MIME type
this.mime =
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
name.endsWith(".json" ) ? "application/json;charset=UTF-8" :
name.endsWith(".png" ) ? "image/png" :
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".wasm" ) ? "application/wasm" :
name.endsWith(".woff2") ? "font/woff2" :
"application/octet-stream"
;
}
///////////////////////////// Public Methods //////////////////////////////
// Install a font from a bundled resource
async installFont(name) {
if (name === undefined) {
name = "/" + this.name;
name = name.substring(name.lastIndexOf("/") + 1);
}
let ret = new FontFace(name, "url('"+
(Bundle.debug ? this.name : this.toDataURL()) + "'");
await ret.load();
document.fonts.add(ret);
return ret;
}
// Install an image as a CSS icon from a bundled resource
installImage(name, filename) {
document.documentElement.style.setProperty("--" +
name || this.name.replaceAll(/\/\./, "_"),
"url('" + (Bundle.debug ?
filename || this.name : this.toDataURL()) + "')");
return name;
}
// Install a stylesheet from a bundled resource
installStylesheet(enabled) {
let ret = document.createElement("link");
ret.href = Bundle.debug ? this.name : this.toDataURL();
ret.rel = "stylesheet";
ret.type = "text/css";
ret.setEnabled = enabled=>{
if (enabled)
ret.removeAttribute("disabled");
else ret.setAttribute("disabled", "");
};
ret.setEnabled(!!enabled);
document.head.appendChild(ret);
return ret;
}
// Encode the file data as a data URL
toDataURL() {
return "data:" + this.mime + ";base64," +
btoa(String.fromCharCode(...this.data));
}
// Interpret the file's contents as bundled script source data URL
toScript() {
// Process all URL strings prefixed with /**/
let parts = this.toString().split("/**/");
let src = parts.shift();
for (let part of parts) {
let quote = part.indexOf("\"", 1);
// Begin with the path of the current file
let path = this.name.split("/");
path.pop();
// Navigate to the path of the target file
let file = part.substring(1, quote).split("/");
while (file.length > 0) {
let sub = file.shift();
switch (sub) {
case "..": path.pop(); // Fallthrough
case "." : break;
default : path.push(sub);
}
}
// Append the file as a data URL
file = Bundle[path.join("/")];
src += "\"" + file[
file.mime.startsWith("text/javascript") ?
"toScript" : "toDataURL"
]() + "\"" + part.substring(quote + 1);
}
// Encode the transformed source as a data URL
return "data:" + this.mime + ";base64," + btoa(src);
}
// Decode the file data as a UTF-8 string
toString() {
return Bundle.decoder.decode(this.data);
}
///////////////////////////// Package Methods /////////////////////////////
// Produce a .ZIP header for export
toZipHeader(crc32, offset) {
let central = offset !== undefined;
let ret = [];
if (central) {
Bundle.writeInt (ret, 4, 0x02014B50); // Signature
Bundle.writeInt (ret, 2, 20); // Version created by
} else
Bundle.writeInt (ret, 4, 0x04034B50); // Signature
Bundle.writeInt (ret, 2, 20); // Version required
Bundle.writeInt (ret, 2, 0); // Bit flags
Bundle.writeInt (ret, 2, 0); // Compression method
Bundle.writeInt (ret, 2, 0); // Modified time
Bundle.writeInt (ret, 2, 0); // Modified date
Bundle.writeInt (ret, 4, crc32); // Checksum
Bundle.writeInt (ret, 4, this.data.length); // Compressed size
Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size
Bundle.writeInt (ret, 2, this.name.length); // Filename length
Bundle.writeInt (ret, 2, 0); // Extra field length
if (central) {
Bundle.writeInt (ret, 2, 0); // File comment length
Bundle.writeInt (ret, 2, 0); // Disk number start
Bundle.writeInt (ret, 2, 0); // Internal attributes
Bundle.writeInt (ret, 4, 0); // External attributes
Bundle.writeInt (ret, 4, offset); // Relative offset
}
Bundle.writeString (ret, this.name); // Filename
if (!central)
Bundle.writeBytes(ret, this.data); // File data
return Uint8Array.from(ret);
}
}
///////////////////////////////////////////////////////////////////////////////
// Boot Program //
///////////////////////////////////////////////////////////////////////////////
// De-register the boot function
delete globalThis.a;
// Remove the bundle image element from the document
Bundle.src = arguments[0].src;
arguments[0].remove();
// Convert the file manifest into BundledFile objects
let buffer = arguments[1];
let offset = arguments[2] - manifest[0][1];
for (let entry of manifest) {
Bundle.add(entry[0], buffer.subarray(offset, offset + entry[1]));
offset += entry[1];
if (Bundle.length == 1)
offset++; // Skip null delimiter
}
// Begin program operations
import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript());