pvbemu/web/_boot.js

311 lines
10 KiB
JavaScript

// Running as an async function
// Prepended by Bundle.java: buffer, image, manifest, name
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// Resource manager for bundled files
class Bundle extends Array {
//////////////////////////////// Constants ////////////////////////////////
// .ZIP support
static CRC_LOOKUP = new Uint32Array(256);
// Text processing
static DECODER = new TextDecoder();
static ENCODER = new TextEncoder();
static initializer() {
// Generate the CRC32 lookup table
for (let x = 0; x <= 255; x++) {
let l = x;
for (let j = 7; j >= 0; j--)
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
this.CRC_LOOKUP[x] = l;
}
}
///////////////////////// Initialization Methods //////////////////////////
constructor(name, url, settings, isDebug) {
super();
this.isDebug = isDebug;
this.name = name;
this.settings = settings;
this.url = url;
}
///////////////////////////// Public Methods //////////////////////////////
// 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 = Bundle.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 = [];
Bundle.writeInt(end, 4, 0x06054B50); // Signature
Bundle.writeInt(end, 2, 0); // Disk number
Bundle.writeInt(end, 2, 0); // Central dir start disk
Bundle.writeInt(end, 2, this.length); // # central dir this disk
Bundle.writeInt(end, 2, this.length); // # central dir total
Bundle.writeInt(end, 4, size); // Size of central dir
Bundle.writeInt(end, 4, offset); // Offset of central dir
Bundle.writeInt(end, 2, 0); // .ZIP comment length
// Prompt the user to save the resulting file
let a = document.createElement("a");
a.download = this.name + ".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 /////////////////////////////
// Add a BundledFile to the collection
add(file) {
file.bundle = this;
this.push(this[file.name] = file);
}
// Write a byte array into an output buffer
static writeBytes(data, bytes) {
for (let b of bytes)
data.push(b);
}
// Write an integer into an output buffer
static 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
static writeString(data, text) {
this.writeBytes(data, this.ENCODER.encode(text));
}
///////////////////////////// Private Methods /////////////////////////////
// Calculate the CRC32 checksum for a byte array
static crc32(data) {
let c = 0xFFFFFFFF;
for (let x = 0; x < data.length; x++)
c = ((c >>> 8) ^ this.CRC_LOOKUP[(c ^ data[x]) & 0xFF]);
return ~c & 0xFFFFFFFF;
}
}
Bundle.initializer();
///////////////////////////////////////////////////////////////////////////////
// BundledFile //
///////////////////////////////////////////////////////////////////////////////
// Individual file in the bundled data
class BundledFile {
//////////////////////////////// Constants ////////////////////////////////
// MIME types
static MIMES = {
".css" : "text/css;charset=UTF-8",
".frag" : "text/plain;charset=UTF-8",
".js" : "text/javascript;charset=UTF-8",
".json" : "application/json;charset=UTF-8",
".png" : "image/png",
".svg" : "image/svg+xml;charset=UTF-8",
".txt" : "text/plain;charset=UTF-8",
".vert" : "text/plain;charset=UTF-8",
".wasm" : "application/wasm",
".woff2": "font/woff2"
};
///////////////////////// Initialization Methods //////////////////////////
constructor(name, buffer, offset, length) {
// Configure instance fields
this.data = buffer.slice(offset, offset + length);
this.name = name;
// Resolve the MIME type
let index = name.lastIndexOf(".");
this.mime = index != -1 && BundledFile.MIMES[name.substring(index)] ||
"application/octet-stream";
}
///////////////////////////// Public Methods //////////////////////////////
// Represent the file with a blob URL
toBlobURL() {
return this.blobURL || (this.blobURL = URL.createObjectURL(
new Blob([ this.data ], { type: this.mime })));
}
// Encode the file data as a data URL
toDataURL() {
return "data:" + this.mime + ";base64," + btoa(
Array.from(this.data).map(b=>String.fromCharCode(b)).join(""));
}
// Pre-process URLs in a bundled file's contents
toProcURL(asDataURL = false) {
// The URL has already been computed
if (this.url)
return this.url;
// Working variables
let content = this.toString();
let pattern = /\/\*\*?\*\//g;
let parts = content.split(pattern);
let ret = [ parts.shift() ];
// Process all URLs prefixed with /**/ or /***/
for (let part of parts) {
let start = part.indexOf("\"");
let end = part.indexOf("\"", start + 1);
let filename = part.substring(start + 1, end);
let asData = pattern.exec(content)[0] == "/***/";
// Relative to current file
if (filename.startsWith(".")) {
let path = this.name.split("/");
path.pop(); // Current filename
// Navigate to the path of the target file
for (let dir of filename.split("/")) {
switch (dir) {
case "..": path.pop(); // Fallthrough
case "." : break;
default : path.push(dir);
}
}
// Produce the fully-qualified filename
filename = path.join("/");
}
// Append the file as a data URL
let file = this.bundle[filename];
ret.push(
part.substring(0, start + 1),
file[
file.mime.startsWith("text/javascript") ||
file.mime.startsWith("text/css") ?
"toProcURL" : asData ? "toDataURL" : "toBlobURL"
](asData),
part.substring(end)
);
}
// Represent the transformed source as a URL
return this.url = asDataURL ?
"data:" + this.mime + ";base64," + btoa(ret.join("")) :
URL.createObjectURL(new Blob(ret, { type: this.mime }))
;
}
// 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 //
///////////////////////////////////////////////////////////////////////////////
// Produce the application bundle
let bundle = new Bundle(name, image.src, image.getAttribute("settings") || "",
location.protocol != "file:" && location.hash == "#debug");
for (let x=0,offset=buffer.indexOf(0)-manifest[0][1]; x<manifest.length; x++) {
let entry = manifest[x];
bundle.add(new BundledFile(entry[0], buffer, offset, entry[1]));
offset += entry[1] + (x == 0 ? 1 : 0);
}
// Begin program operations
(await import(bundle.isDebug ?
"./web/App.js" :
bundle["web/App.js"].toProcURL()
)).App.main(bundle);