// SPDX-License-Identifier: GPL-2.0-or-later // CDJ-900NXS mods builder, in the browser: the official 1.31 C900NXS.UPD in, // a new one with the chosen mods out. Nothing is uploaded or downloaded. // // Ports of the patcher's upd.py (update file), the emulator checkout's // lzss_encode.py (packer) and patch_cdj900nxs.py's asset blob; the mods' // bytes are in builder/data.js (tools/make_builder_data.py), with the // SHA-256 of the finished application for every combination. (function () { 'use strict'; var D = window.BUILDER_DATA; var BASE = 0x08000000; var APP_START = 0x40000, APP_END = 0x7E0000, RECORD_DATA = 32; var ADDRESS_BYTES = { 0: 2, 1: 2, 2: 3, 3: 4, 5: 2, 6: 3, 7: 4, 8: 3, 9: 2 }; var LABEL = '1.32'; var UPSTREAM = ['extended_bpm', 'phasemeter', 'rgb_waveform']; var OURS = ['beatjump', 'cuecolours', 'hotcuekeys', 'ledsaver', 'keynotation', 'credit']; function fail(msg) { throw new Error(msg); } // ---------- bytes ---------- function hex(u8) { var s = ''; for (var i = 0; i < u8.length; i++) s += (u8[i] < 16 ? '0' : '') + u8[i].toString(16); return s; } function fromHex(s) { var out = new Uint8Array(s.length >> 1); for (var i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16); return out; } function fromB64(s) { var bin = atob(s), out = new Uint8Array(bin.length); for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } function equal(a, b) { if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } function u16(b, o) { return b[o] | b[o + 1] << 8; } function u32(b, o) { return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0; } function put16(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; } function put32(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; b[o + 2] = v >> 16 & 255; b[o + 3] = v >>> 24 & 255; } function grow(b, size) { if (b.length >= size) return b; var out = new Uint8Array(size); out.set(b); return out; } function latin1(u8) { var s = ''; for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); return s; } function ascii(s) { var out = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); return out; } function sha256(u8) { return crypto.subtle.digest('SHA-256', u8).then(function (h) { return hex(new Uint8Array(h)); }); } // CRC-16/XMODEM (Python's binascii.crc_hqx with 0) var CRC_TABLE = (function () { var t = new Uint16Array(256); for (var i = 0; i < 256; i++) { var c = i << 8; for (var k = 0; k < 8; k++) c = c & 0x8000 ? (c << 1 ^ 0x1021) & 0xFFFF : c << 1 & 0xFFFF; t[i] = c; } return t; })(); function crc16(u8, start, end) { var c = 0; for (var i = start; i < end; i++) c = (c << 8 & 0xFFFF) ^ CRC_TABLE[(c >> 8 ^ u8[i]) & 255]; return c; } // ---------- the update file (upd.py) ---------- var HEADER = /^CDJ-900NXS (DRIV|MAIN|PANL)Ver(\d\.\d\d)/; function parseSection(blob) { var n = blob.length; if (n < 36) fail('a section is truncated'); if (crc16(blob, 0, n - 2) !== u16(blob, n - 2)) fail('a section fails its CRC'); var header = blob.subarray(0, 32), m = HEADER.exec(latin1(header)); if (!m) fail('unexpected section header'); var sec = { blob: blob, header: header, name: m[1], version: m[2], records: [] }; var end = n - 2; if (blob[end - 2] !== 13 || blob[end - 1] !== 10) fail(sec.name + ': the S-records do not end in a line break'); var pos = 32, line = 0; while (pos < end) { var e = pos; while (!(blob[e] === 13 && blob[e + 1] === 10)) e++; line++; if (blob[pos] !== 0x53 || e - pos < 4 || (e - pos) % 2) fail(sec.name + ': S-record ' + line + ' is malformed'); var kind = blob[pos + 1] - 48; if (!(kind in ADDRESS_BYTES)) fail(sec.name + ': S-record ' + line + ' has type ' + kind); var len = (e - pos - 2) >> 1, raw = new Uint8Array(len), sum = 0; for (var i = 0; i < len; i++) { var h = blob[pos + 2 + i * 2], l = blob[pos + 3 + i * 2]; var v = (h <= 57 ? h - 48 : h - 55) << 4 | (l <= 57 ? l - 48 : l - 55); raw[i] = v; sum += v; } var width = ADDRESS_BYTES[kind]; if (len !== raw[0] + 1 || len < width + 2 || (sum & 255) !== 255) fail(sec.name + ': S-record ' + line + ' fails its length or checksum'); var addr = 0; for (var k = 0; k < width; k++) addr = addr * 256 + raw[1 + k]; sec.records.push({ kind: kind, address: addr, data: raw.subarray(1 + width, len - 1) }); pos = e + 2; } if (sec.records[0].kind !== 0 || [7, 8, 9].indexOf(sec.records[sec.records.length - 1].kind) < 0) fail(sec.name + ': no S-record header or end'); return sec; } function dataRecords(sec) { return sec.records.filter(function (r) { return r.kind >= 1 && r.kind <= 3; }); } function flashOf(sec, size) { size = size || APP_END; var img = new Uint8Array(size).fill(0xFF); dataRecords(sec).forEach(function (r) { if (r.address + r.data.length > size) fail(sec.name + ': a record reaches past 0x' + size.toString(16)); img.set(r.data, r.address); }); return img; } function parseUpd(raw) { var pos = 0, sizes = []; for (var s = 0; s < 3; s++) { var e = pos; while (e < raw.length && !(raw[e] === 13 && raw[e + 1] === 10)) e++; var t = latin1(raw.subarray(pos, e)); if (!/^\d+$/.test(t)) fail('the file does not start with three section lengths'); sizes.push(parseInt(t, 10)); pos = e + 2; } if (pos + sizes[0] + sizes[1] + sizes[2] !== raw.length) fail('the section lengths do not add up to the file'); var secs = sizes.map(function (n) { var b = raw.subarray(pos, pos + n); pos += n; return parseSection(b); }); if (secs.map(function (x) { return x.name; }).join() !== 'DRIV,MAIN,PANL') fail('the sections are not DRIV, MAIN, PANL'); return secs; } function unpackLzss(stream) { var ring = new Uint8Array(4096).fill(0x20); ring.fill(0, 4078); var r = 4078, out = new Uint8Array(Math.max(1 << 20, stream.length * 3)), o = 0, pos = 0, n = stream.length; function room(k) { if (o + k > out.length) { var b = new Uint8Array(out.length * 2); b.set(out); out = b; } } while (pos < n) { var flags = stream[pos++]; for (var bit = 0; bit < 8; bit++) { if (pos >= n) break; if (flags >> bit & 1) { room(1); var b1 = stream[pos++]; out[o++] = b1; ring[r] = b1; r = r + 1 & 0xFFF; } else { if (pos + 2 > n) fail('the packed application ends inside a match'); var lo = stream[pos], hi = stream[pos + 1]; pos += 2; var at = lo | (hi & 0xF0) << 4, count = (hi & 0x0F) + 3; room(count); for (var k = 0; k < count; k++) { var b2 = ring[at + k & 0xFFF]; out[o++] = b2; ring[r] = b2; r = r + 1 & 0xFFF; } } } } return out.slice(0, o); } function appRegion(flash) { var size = u32(flash, APP_START), end = APP_START + 4 + size; if (end + 2 > APP_END) fail('the packed application runs past 0x' + APP_END.toString(16)); var sum = 0; for (var i = APP_START; i < end; i++) sum += flash[i]; if (u16(flash, end) !== (sum & 0xFFFF)) fail('the packed application fails its additive checksum'); return { app: unpackLzss(flash.subarray(APP_START + 4, end)), region: flash.subarray(APP_START, end + 2), stream: flash.subarray(APP_START + 4, end) }; } // The official update, checked to be exactly the 1.31 file. function readStock(raw) { return sha256(raw).then(function (h) { if (h !== D.stockUpdSha256) fail('This is not the original CDJ-900NXS 1.31 update file (C900NXS.UPD). Its SHA-256 is ' + h + '.'); var secs = parseUpd(raw), main = secs[1]; if (main.version !== '1.31' || main.header[31] !== 0x30) fail('MAIN is not 1.31 in application-only mode'); main.records.forEach(function (r) { if ([0, 2, 7].indexOf(r.kind) < 0) fail('MAIN has unexpected record types'); }); var flash = flashOf(main), reg = appRegion(flash); return sha256(reg.app).then(function (ah) { if (reg.app.length !== D.appSize || ah !== D.stockAppSha256) fail('the 1.31 application inside does not match'); var boot = dataRecords(main).filter(function (r) { return r.address < APP_START; }); boot.forEach(function (r) { if (r.address + r.data.length > APP_START) fail('a stock record crosses into the application'); }); return { raw: raw, sections: secs, flash: flash, app: reg.app, stream: reg.stream, boot: boot }; }); }); } function encodeRecord(r) { var width = ADDRESS_BYTES[r.kind], body = new Uint8Array(width + r.data.length + 2), sum = 0; body[0] = width + r.data.length + 1; for (var k = 0; k < width; k++) body[1 + k] = Math.floor(r.address / Math.pow(256, width - 1 - k)) & 255; body.set(r.data, 1 + width); for (var i = 0; i < body.length - 1; i++) sum += body[i]; body[body.length - 1] = ~sum & 255; return 'S' + r.kind + hex(body).toUpperCase() + '\r\n'; } function relabel(header, version) { var s = latin1(header), m = HEADER.exec(s); var old = m[0], neu = old.slice(0, -4) + version; var out = s.replace(old, neu); if (out.slice(0, old.length - 4) !== s.slice(0, old.length - 4) || out.slice(old.length) !== s.slice(old.length)) fail('relabelling would change more than the version'); return ascii(out); } function encodeSection(header, records) { var parts = [header]; var text = ''; for (var i = 0; i < records.length; i++) { text += encodeRecord(records[i]); if (text.length > 1 << 20) { parts.push(ascii(text)); text = ''; } } parts.push(ascii(text)); var n = parts.reduce(function (a, p) { return a + p.length; }, 0), body = new Uint8Array(n + 2), o = 0; parts.forEach(function (p) { body.set(p, o); o += p.length; }); put16(body, n, crc16(body, 0, n)); return body; } function join(blobs) { var head = ascii(blobs.map(function (b) { return b.length + '\r\n'; }).join('')); var n = head.length + blobs.reduce(function (a, b) { return a + b.length; }, 0), out = new Uint8Array(n), o = head.length; out.set(head); blobs.forEach(function (b) { out.set(b, o); o += b.length; }); return out; } function packRegion(stream) { var n = 4 + stream.length + 2, pad = (RECORD_DATA - n % RECORD_DATA) % RECORD_DATA; var out = new Uint8Array(n + pad).fill(0xFF); put32(out, 0, stream.length); out.set(stream, 4); var sum = 0; for (var i = 0; i < 4 + stream.length; i++) sum += out[i]; put16(out, 4 + stream.length, sum & 0xFFFF); return out; } function buildUpd(stock, stream) { var main = stock.sections[1], reg = packRegion(stream); if (APP_START + reg.length > APP_END) fail('the packed application needs ' + reg.length + ' bytes, the flash has ' + (APP_END - APP_START)); var records = [main.records[0]].concat(stock.boot); for (var off = 0; off < reg.length; off += RECORD_DATA) records.push({ kind: 2, address: APP_START + off, data: reg.subarray(off, off + RECORD_DATA) }); records.push(main.records[main.records.length - 1]); var blob = encodeSection(relabel(main.header, LABEL), records); return join([stock.sections[0].blob, blob, stock.sections[2].blob]); } function sameRecord(a, b) { return a.kind === b.kind && a.address === b.address && equal(a.data, b.data); } // Every check on the finished file, as upd.verify(). function verify(raw, stock, app) { var done = [], secs = parseUpd(raw); done.push('section lengths, CRC-16 of every section, every S-record checksum'); if (!equal(secs[0].blob, stock.sections[0].blob) || !equal(secs[2].blob, stock.sections[2].blob)) fail('DRIV or PANL differs from stock'); done.push('DRIV and PANL byte for byte as stock'); var main = secs[1], smain = stock.sections[1]; if (!equal(main.header, relabel(smain.header, LABEL))) fail('the MAIN header differs beyond its version label'); if (main.header[31] !== 0x30) fail('the MAIN header no longer selects application-only mode'); done.push('MAIN header as stock but for the label ' + LABEL + '; application-only mode kept'); if (!sameRecord(main.records[0], smain.records[0]) || !sameRecord(main.records[main.records.length - 1], smain.records[smain.records.length - 1])) fail('the S0 header or S7 entry record changed'); done.push('S0 header record and S7 entry record as stock'); var low = dataRecords(main).filter(function (r) { return r.address < APP_START; }); if (low.length !== stock.boot.length || low.some(function (r, i) { return !sameRecord(r, stock.boot[i]); })) fail('the boot loader or recovery records changed'); var flash = flashOf(main); if (!equal(flash.subarray(0, APP_START), stock.flash.subarray(0, APP_START))) fail('the boot loader or recovery bytes changed'); done.push('boot loader and recovery (flash 0x0-0x3FFFF): every record and byte as stock'); if (dataRecords(main).some(function (r) { return r.address < APP_START && APP_START < r.address + r.data.length; })) fail('a record crosses into the boot area'); done.push('no record reaches past 0x7E0000 or into the boot area'); var reg = appRegion(flash); done.push('packed application: additive checksum'); if (!equal(reg.app, app)) fail('the update does not unpack to the patched application'); done.push('unpacked with the boot loader\'s LZSS: exactly the patched application'); done.push('flash used by the application: ' + reg.region.length + ' of ' + (APP_END - APP_START) + ' bytes'); return done; } // ---------- LZSS packer (lzss_encode.py), run in a worker ---------- function lzssPack(data) { var RING = 4096, START = RING - 18, MIN = 3, MAX = 18, DEPTH = 48; var n = data.length, total = START + n; function cb(i) { return i < START ? 0x20 : data[i - START]; } var head = new Int32Array(1 << 24).fill(-1), prev = new Int32Array(RING); function key(p) { return cb(p) << 16 | cb(p + 1) << 8 | cb(p + 2); } function insert(p) { if (p + 3 <= total) { var k = key(p); prev[p & (RING - 1)] = head[k]; head[k] = p; } } for (var p0 = 0; p0 < START; p0++) insert(p0); var out = new Uint8Array(n + (n >> 3) + 64), o = 0; var flagAt = o++, mask = 1, items = 0; out[flagAt] = 0; var k2 = 0; while (k2 < n) { var a = START + k2, limit = Math.min(MAX, total - a), bestLen = 0, bestPos = 0; if (limit >= MIN) { var lo = a - RING, p = head[key(a)], checked = 0; while (p >= 0 && p >= lo) { var len = 0; while (len < limit && cb(p + len) === cb(a + len)) len++; if (len > bestLen) { bestLen = len; bestPos = p; if (bestLen >= limit) break; } if (++checked >= DEPTH) break; p = prev[p & (RING - 1)]; } } var step; if (bestLen >= MIN) { step = bestLen; var off = bestPos & (RING - 1); out[o++] = off & 0xFF; out[o++] = (off >> 4 & 0xF0) | (step - MIN); } else { step = 1; out[flagAt] |= mask; out[o++] = cb(a); } for (var t = 0; t < step; t++) insert(a + t); k2 += step; items++; mask = mask << 1 & 0xFF; if (mask === 0) { mask = 1; if (k2 < n) { flagAt = o++; out[flagAt] = 0; } } } return out.slice(0, o); } function packInWorker(app) { return new Promise(function (resolve, reject) { var src = lzssPack.toString() + '\nonmessage = function (e) { var o = lzssPack(new Uint8Array(e.data)); postMessage(o.buffer, [o.buffer]); };'; var w; try { w = new Worker(URL.createObjectURL(new Blob([src], { type: 'text/javascript' }))); } catch (e) { setTimeout(function () { try { resolve(lzssPack(app)); } catch (x) { reject(x); } }, 30); return; } w.onmessage = function (e) { w.terminate(); resolve(new Uint8Array(e.data)); }; w.onerror = function (e) { w.terminate(); reject(new Error('packing failed: ' + e.message)); }; var copy = app.slice(); w.postMessage(copy.buffer, [copy.buffer]); }); } // ---------- arnoldasr's asset blob (patch_cdj900nxs.assets) ---------- var TABLE = 0x08100000, DESCRIPTOR = 0x2C, ORIGINAL_COUNT = 0x2A1, TEMPLATE = 0x1C8, DATA_BASE = 0x08FC2D00; var WIDTH = 200, HEIGHT = 24, MARKER_WIDTH = 8, BAR = 5, KEY = 0xF81F; var ORANGE_LINE = [120, 65, 20], ORANGE_MARK = [255, 140, 0], BLUE_LINE = [40, 90, 120], BLUE_MARK = [60, 170, 255]; var WAVE_COLOURS = [ 448, 457, 47, 458, 449, 111, 495, 466, 175, 467, 487, 239, 303, 479, 367, 459, 456, 431, 468, 465, 475, 476, 474, 39, 477, 31, 471, 46, 469, 45, 23, 460, 109, 484, 110, 485, 103, 478, 483, 493, 494, 486, 95, 15, 450, 167, 492, 173, 174, 231, 115, 470, 238, 44, 237, 159, 295, 87, 108, 302, 51, 301, 491, 482, 359, 223, 366, 365, 236, 300, 179, 423, 461, 287, 430, 429, 172, 364, 473, 428, 351, 114, 151, 463, 299, 363, 415, 215, 427, 178, 490, 50, 279, 235, 79, 462, 451, 343, 511, 298, 362, 400, 52, 116, 426, 234, 407, 7, 143, 180, 207, 113, 233, 243, 481, 57, 271, 452, 417, 177]; var WHITENESS = [[150, 20, 20], [220, 40, 30], [255, 110, 20], [255, 190, 30], [170, 230, 60], [40, 210, 170], [60, 160, 255], [225, 240, 255]]; function rgb565(c) { return (c[0] >> 3) << 11 | (c[1] >> 2) << 5 | c[2] >> 3; } function lineImage(colour) { var px = new Array(WIDTH * HEIGHT).fill(KEY), c = rgb565(colour); for (var x = 2; x < WIDTH - 2; x++) px[12 * WIDTH + x] = c; for (var i = 0; i < 5; i++) { var xx = 2 + Math.floor(i * (WIDTH - 5) / 4); for (var y = 7; y < 18; y++) px[y * WIDTH + xx] = c; } return px; } function markerImage(colour) { var px = [], c = rgb565(colour); for (var y = 0; y < HEIGHT; y++) for (var x = 0; x < MARKER_WIDTH; x++) px.push(y >= 3 && y <= 21 && x < BAR ? c : KEY); return px; } function rgb9(code) { return [6, 3, 0].map(function (s) { return Math.floor(((code >> s) & 7) * 255 / 7); }); } function assets(app) { var images = [[WIDTH, lineImage(ORANGE_LINE)], [WIDTH, lineImage(BLUE_LINE)], [MARKER_WIDTH, markerImage(ORANGE_MARK)], [MARKER_WIDTH, markerImage(BLUE_MARK)]]; var t0 = TABLE - BASE, count = ORIGINAL_COUNT + images.length, tableBytes = count * DESCRIPTOR; var pixelsBase = DATA_BASE + ((tableBytes + 31) & ~31); var template = app.subarray(t0 + TEMPLATE * DESCRIPTOR, t0 + (TEMPLATE + 1) * DESCRIPTOR); var pixLen = 0; images.forEach(function (im) { var l = im[1].length * 2; pixLen += l + (32 - l % 32) % 32; }); var blobLen = (pixelsBase - DATA_BASE) + pixLen, palette = 128 * 2, lut = 512; var blob = new Uint8Array(blobLen + palette + lut); blob.set(app.subarray(t0, t0 + ORIGINAL_COUNT * DESCRIPTOR)); var pix = pixelsBase - DATA_BASE, pl = 0; images.forEach(function (im, i) { var e = ORIGINAL_COUNT * DESCRIPTOR + i * DESCRIPTOR; blob.set(template, e); put16(blob, e + 4, im[0]); put16(blob, e + 6, HEIGHT); put32(blob, e + 0x1C, (ORIGINAL_COUNT + i) * DESCRIPTOR); put32(blob, e + 0x20, pixelsBase + pl - TABLE); im[1].forEach(function (v, j) { put16(blob, pix + pl + j * 2, v); }); pl += im[1].length * 2; pl += (32 - pl % 32) % 32; }); var paletteAt = DATA_BASE + blobLen; var colours = WHITENESS.concat(WAVE_COLOURS.map(rgb9)); colours.forEach(function (c, i) { put16(blob, blobLen + i * 2, rgb565(c)); }); for (var code = 0; code < 512; code++) { var r = code >> 6 & 7, g = code >> 3 & 7, b = code & 7, best = 0, bd = Infinity; for (var i = 0; i < WAVE_COLOURS.length; i++) { var w = WAVE_COLOURS[i], d = Math.pow((w >> 6 & 7) - r, 2) + Math.pow((w >> 3 & 7) - g, 2) + Math.pow((w & 7) - b, 2); if (d < bd) { bd = d; best = i; } } blob[blobLen + palette + code] = 8 + best; } return { blob: blob, phaseEnd: blobLen, paletteAt: paletteAt }; } // ---------- the mods' bytes (builder/data.js) ---------- function boxPixels(stock, res, bg, ink, text) { var B = D.box, rec = B.resTable + res * 44, w = u16(stock, rec + 4), h = u16(stock, rec + 6); var off = B.resTable + u32(stock, rec + 0x20), px = new Uint16Array(w * h); for (var i = 0; i < w * h; i++) px[i] = u16(stock, off + i * 2); for (var y = 3; y < 13; y++) for (var x = 3; x < w - 3; x++) px[y * w + x] = bg; // as box_pixels: width = sum(space for ' ', else glyph width + gap) - gap var width = -B.gap, c; for (c = 0; c < text.length; c++) width += text[c] === ' ' ? B.space : B.glyphs[text[c]][0].length + B.gap; var x0 = Math.floor((w - width) / 2), y0 = 4; for (c = 0; c < text.length; c++) { if (text[c] === ' ') { x0 += B.space; continue; } var g = B.glyphs[text[c]]; for (var gy = 0; gy < g.length; gy++) for (var gx = 0; gx < g[gy].length; gx++) if (g[gy][gx] === 'X') px[(y0 + gy) * w + x0 + gx] = ink; x0 += g[0].length + B.gap; } var out = new Uint8Array(w * h * 2); for (i = 0; i < w * h; i++) put16(out, i * 2, px[i]); return out; } // One variant of data.js: the application grown to its size, then its // operations: our bytes, a copy from the user's own application, the HOT // KEYS / LOADING boxes or arnoldasr's asset blob, both made from the user's application. function applyVariant(app, stock, v) { var out = grow(app.slice(), v.size), blob = null; v.ops.forEach(function (o) { var off = o[1] - BASE; if (o[0] === 'data') out.set(fromB64(D.chunks[o[2]]), off); else if (o[0] === 'copy') out.set(stock.subarray(o[2] - BASE, o[2] - BASE + o[3]), off); else if (o[0] === 'box') out.set(boxPixels(stock, o[2], o[3], o[4], o[5]), off); else if (o[0] === 'assets') { blob = blob || assets(stock).blob; out.set(blob.subarray(0, o[2]), off); } else fail('unknown operation ' + o[0]); }); return out; } // ---------- the whole build ---------- function patchApp(stock, names, log) { var up = UPSTREAM.filter(function (n) { return names.indexOf(n) >= 0; }); var ours = OURS.filter(function (n) { return names.indexOf(n) >= 0; }); var app = stock; if (up.length) { app = applyVariant(app, stock, D.upstream[up.join(',')]); log('arnoldasr\'s mods applied (' + up.join(', ') + ')'); } if (ours.length) { app = applyVariant(app, stock, D.ours[ours.join(',')]); log('our mods applied (' + ours.join(', ') + ')'); } var key = up.concat(ours).join(','); return sha256(app).then(function (h) { if (h !== D.expect[key]) fail('the patched application is not the one the patcher makes (SHA-256 ' + h + ')'); log('the patched application is exactly the patcher\'s (SHA-256 checked)'); return app; }); } function build(raw, names, log) { var stock; log('checking the update file...'); return readStock(raw).then(function (s) { stock = s; log('the official CDJ-900NXS 1.31 update: SHA-256 checked, application unpacked and checked'); if (!names.length) return { app: stock.app, stream: stock.stream }; return patchApp(stock.app, names, log).then(function (app) { log('packing the application (LZSS), this takes a while...'); var t = Date.now(); return packInWorker(app).then(function (stream) { log('packed ' + app.length + ' -> ' + stream.length + ' bytes in ' + Math.round((Date.now() - t) / 1000) + ' s'); return { app: app, stream: stream }; }); }); }).then(function (r) { log('writing the update file...'); var out = buildUpd(stock, r.stream); var checks = verify(out, stock, r.app); return Promise.all([sha256(out), sha256(r.app)]).then(function (h) { return { upd: out, checks: checks, updSha: h[0], appSha: h[1] }; }); }); } window.CDJBuilder = { build: build, UPSTREAM: UPSTREAM, OURS: OURS, lzssPack: lzssPack, unpackLzss: unpackLzss }; })();