Files
Simon James b54a3139c7 Initial commit: Qortal Web Builder monorepo.
Includes QWB, Qortal Web, and Q-Shops Q-Apps with shared packages and build scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 12:17:29 +00:00

200 lines
5.1 KiB
JavaScript

/**
* Bundled to public/zip-store.js (see scripts/bundle-zip-store.mjs).
* Exposes window.buildZipStore for in-app WEBSITE publish.
*
* Uses a minimal PKZIP (STORE) implementation — the same family as
* /zip-store.js at repo root. Some fflate/zipSync outputs (UTF-8 flags, extra
* fields) are rejected or misread by the Java path Qortal uses for WEBSITE
* validation, yielding false MISSING_INDEX_FILE. This format matches the
* expectations of java.util zip readers / ZipUtils in core.
*/
const crcTable = (function makeTable() {
const t = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
t[i] = c >>> 0;
}
return t;
})();
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = crcTable[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function toUtf8(v) {
if (v instanceof Uint8Array) return v;
if (typeof v === 'string') return new TextEncoder().encode(String(v));
return new Uint8Array(v);
}
/** Qortal: root index* / default* / home* first, then a stable order. */
const ROOT_FIRST = [
'index.html',
'index.htm',
'default.html',
'default.htm',
'home.html',
'home.htm',
'manifest.json',
];
function sortZipKeys(keys) {
const remaining = new Set(keys);
const out = [];
for (const p of ROOT_FIRST) {
if (remaining.has(p)) {
out.push(p);
remaining.delete(p);
}
}
out.push(...Array.from(remaining).sort((a, b) => a.localeCompare(b)));
return out;
}
/**
* @param {Record<string, string|ArrayBuffer|Uint8Array>} files
* @returns {Uint8Array}
*/
function buildZipStoreRaw(files) {
const names = sortZipKeys(Object.keys(files));
const entries = names.map((name) => {
const data = toUtf8(files[name]);
return { name, data };
});
let offset = 0;
const parts = [];
const central = [];
for (const e of entries) {
const nameBytes = toUtf8(e.name);
const nameLen = nameBytes.length;
if (nameLen > 65535) {
throw new Error('zip path too long: ' + e.name);
}
const data = e.data;
const size = data.length;
const crc = crc32(data);
const sig = 0x04034b50;
const ver = 20;
const flags = 0;
const method = 0;
const time = 0;
const date = 0;
const csize = size;
const hlen = 30 + nameLen;
const local = new DataView(new ArrayBuffer(hlen + size));
let p = 0;
local.setUint32(p, sig, true);
p += 4;
local.setUint16(p, ver, true);
p += 2;
local.setUint16(p, flags, true);
p += 2;
local.setUint16(p, method, true);
p += 2;
local.setUint16(p, time, true);
p += 2;
local.setUint16(p, date, true);
p += 2;
local.setUint32(p, crc, true);
p += 4;
local.setUint32(p, csize, true);
p += 4;
local.setUint32(p, size, true);
p += 4;
local.setUint16(p, nameLen, true);
p += 2;
local.setUint16(p, 0, true);
p += 2;
new Uint8Array(local.buffer).set(nameBytes, p);
p += nameLen;
new Uint8Array(local.buffer).set(data, p);
parts.push(new Uint8Array(local.buffer));
central.push({ nameBytes, crc, size, csize, localOffset: offset });
offset += local.buffer.byteLength;
}
const cdParts = [];
let cdSize = 0;
const cdOffset = offset;
for (let i = 0; i < central.length; i++) {
const c = central[i];
const nameLen = c.nameBytes.length;
const hlen = 46 + nameLen;
const buf = new DataView(new ArrayBuffer(46));
let q = 0;
buf.setUint32(q, 0x02014b50, true);
q += 4;
buf.setUint16(q, 20, true);
q += 2;
buf.setUint16(q, 20, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint32(q, c.crc, true);
q += 4;
buf.setUint32(q, c.csize, true);
q += 4;
buf.setUint32(q, c.size, true);
q += 4;
buf.setUint16(q, nameLen, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint16(q, 0, true);
q += 2;
buf.setUint32(q, 0, true);
q += 4;
buf.setUint32(q, c.localOffset, true);
q += 4;
const header = new Uint8Array(buf.buffer);
const merged = new Uint8Array(hlen);
merged.set(header, 0);
merged.set(c.nameBytes, 46);
cdParts.push(merged);
cdSize += merged.length;
}
const eoc = new DataView(new ArrayBuffer(22));
eoc.setUint32(0, 0x06054b50, true);
eoc.setUint16(4, 0, true);
eoc.setUint16(6, 0, true);
eoc.setUint16(8, entries.length, true);
eoc.setUint16(10, entries.length, true);
eoc.setUint32(12, cdSize, true);
eoc.setUint32(16, cdOffset, true);
eoc.setUint16(20, 0, true);
const totalLen = offset + cdSize + 22;
const out = new Uint8Array(totalLen);
let o = 0;
for (const part of parts) {
out.set(part, o);
o += part.length;
}
for (const part of cdParts) {
out.set(part, o);
o += part.length;
}
out.set(new Uint8Array(eoc.buffer), o);
return out;
}
export { buildZipStoreRaw };