import { Buffer } from "buffer"; import type { WorkerEmbeddedFile, ImageProcessor } from "./processor.worker"; import { PNGDecoder, PNGEncoder } from "./png"; import { decodeCoom3Payload } from "./utils"; import { settings } from "./stores"; import { filehosts } from "./filehosts"; import * as bs58 from 'bs58'; import { BitstreamReader, BitstreamWriter } from "./bitstream"; import { tinf_uncompress } from "./dh-deflate"; export let csettings: Parameters[0]; settings.subscribe(b => { csettings = b; }); const CUM3 = Buffer.from("doo\0" + "m"); const CUM4 = Buffer.from("voo\0" + "m"); const CUM5 = Buffer.from("boo\0"); const CUM6 = Buffer.from("Creation Time\0"); const CUM7 = Buffer.from("Software\0"); const BufferReadStream = (b: Buffer) => { const ret = new ReadableStream({ pull(cont) { cont.enqueue(b); cont.close(); } }); return ret; }; const password = Buffer.from("NOA"); const xor = (a: Buffer, p: Buffer) => { let n = 0; for (let i = 0; i < a.byteLength; ++i) { a[i] ^= p[n]; n++; n %= p.byteLength; } }; const prefs: any = { 'files.catbox.moe': 'c', 'a.pomf.cat': 'p', 'take-me-to.space': 't', 'z.zz.fo': 'z' }; const rprefs: any = { 'c': 'files.catbox.moe', 'p': 'a.pomf.cat', 't': 'take-me-to.space', 'z': 'z.zz.fo', }; const extractFromRawDeflate = (b: Buffer) => { const src = new BitstreamReader(); src.addBuffer(b); const chnks: number[] = []; const hidden = new BitstreamWriter({ write(chunk) { for (const i of chunk) { if (i >= 0x20 && i <= 128) // only printable ascii chnks.push(i); else throw "Finish"; /* NOOOOOO YOU CANT JUST THROW DURING NORMAL OPERATION THATS A CODE SMEL-ACK! %@@@@@(////////@ @&/////(@&/////@. #@@@@@&&(, %@/////////@@@@@ #@@& (@@@@&. &%/////@@#/////%@ &@@ .&@@% ,* (@@@@ @(/%@&/////////(@ *@@ ,@@@@, @@@@. @@.*@%@@ @@@(///////////(@ @@# .@*@@@@@@@. &@& *@@# *@@ %@@* @@%/////#@@&////#@ %@/ , %@@@@@@. @@/ (@@* @% &@@ @(///////////%@@@# ,@@ @@ @@@# .@@, ,@@,.@* @@ @(////////@@///@% @@, *@& @@@ @@@ .@@@ @& @(/////%@///////@ @@ #@* ,@ @@@ *@@@@% @(///&@/////////@ @@ #@ *, @@( /@@@@@@@@( @@ *@@% @(/(@(//////////@, #@ (@* %@% @@ @@ #@ %@@ %@@@@@#/////////@, (@. *@@ %@ @@* @& @@@@@@@@, .@, %@( @//////#@@#////@ @@ @@ @( @@. @@ @@@@@@% .@* @@/ @#///////////%@@@ @* (@ @@ @ @@@% %@. %@@@ @////////@@&///@% @. @@ @( (@ @@/ @@ (@@ .@/////@@(///////@ @, @@@@@.@@ @@@& .@@ @@ (@//(@#//////////@@ @% @@ .@@@@ #@@ #@, @&&@(////////////@@ #@ @@@ *@@@@@ @& @% /@@& &@@, ,@. @@(//////%@@%////%@ @, @@@@* @@ *((#&@@@@@ @@ @%///////////%@@@@@@ #@ ,@@@@ &@@@@@@@@# ,@@@@@@@@@@@& @ /@////////%@@///////@ @@ @@@ @@/ @@@@@@ @ .@@@@/ @ (@/////@@///////////@ @@ @@.(@ &@,*@@@ @@# @@ @@@@& @@ *&@@@@%, @. @///@%/////////////@ @ @@ @@@@# ,@ @# %@ @# @ (@@@( @& @/@@//////////////#@ &@ @@ @@@@ @@@, @ /@ .@@@ @@ @#/////////////(@( @& #@@ @@@@ &@@@@@@@@@@@ @@@, #@@@ @@@ @@@///////////(@@, @@ @@ @@ @@ @% @ @# *@@ @@@@@@@@@@@&///////////(@@ @& /, @@ @@ (/, @@ @* @/ ,((. .@@@@@@@@@@@@@. *@/ *@@//////////////////////@, @* @.#@ @% %@@@@@@# @@ @% @@. @@ @@&/@@@@ @@ *@/@@@ .@# @@@///////////////////@@ @# @& @# @@@@@. /@. %@ @& @@ #@@((@& @@ #@@@@////@& @@@@@@@@@@@@@@@@@@@@( @@@. @, @@ .@* @& @@@@* @@@@. %@@/ @@@///////////@@. @@&%///////////////@@ /@@ @@,@. *@* #@* ,@@(/@* @@@@@@#/////////////(@@ @* @@@@@//////////////@@ @@ @( &@. @. #@@@ %@& @@@@@@@@@@@@/////////////&@* @@ .&@@@@&////////////@& ,@. @@ @@ @. @@/(@. @@@@@@@@@@@@@@@@@@@@@(/////////@ (@ @@@@@@@@@@@@@@@ *@@ %&*@, /@# @. &@/(@@@@, .@@@@@@@@%,,,,,,,,,,,,,,,@@@%/////@@ @ %@,&#@@&////////////@* @@@& %@@* ,@@ @. @(//@@ ,@#@@@@@@%(@*,,,,,,,,,,,,,,,,,,@@@@@///&@ #@ &@ (@@@#//////////@ .@&@@ #@@@@@@@@@@& @, @@@*%@@@@*@@@@@@@,,,,,@@,,,,,,,,,,,,,,,,,,@(. /@//#@ #@ %@ @&/////////(@* @@@@& @@ @, ,@, %@@, %@@@@@@@,,,,,,,,@@,,,,,,,,,,,,,,,,*@ @@///@/ /@ %@@&%@ @@@@&@@@@@( %@@@@/@% @ .@.%@( .@@@@@@@@,,,,,,,,,,@@,,,,,,,,,,,,,,,,@@@ @@/@@ *@, @@@@@# @#///%@@ @@#@@@ @ @@( @@///@@@@@/,,,,,,,,,,&@,,,,,,,,,,,,,,,,%@&.,, *@/@@ %@*@@@@@@@@@@@@@%////@, &@@@@ @ &@////////@@@,,,,,,,,,,,#@,,,,,,,,,,,,,,,,(@/%@@@@@@@(&@ &@ @@ @#///////@# .@( @/ ,@/////////@*,,,,,,,,,,#@,,,,,,,,,,,,,,,,/@///&@. .@/@@ &@ @@@ @@ @&////////@ /@# &@ @@///////@*,,,,,,,,,,&@,,,,,,,,,,,,,,,,/@/////@@, ,@/@# %@@ @@@@@@@@///////@# @@ .@* @@/////@/,,,,,,,,,,@@,,,,,,,,,,,,,,,,/@/////&@,..@@/@ *@@@* *@@@@//////@/ (@/ @@ @@///@&,,,,,,,,,,@*,,,,,,,,,,,,,,,&@//////@@. @//@* #@@@@#/#@@ @@@@@ @@ @@/@@,,,,,,,,,/@,,,,,,,,,,,,,,*@@@@/////@@@@@@//@* /@@@@@@@@@* @@/////(@@ @@ @@@,,,,,,,,,#@,,,,,,,,,,,,,%@///////@@@@&,@#/@& ,&@@@/@@@@@@@////@# %@@% @/////////%@@ %@ @(,,,,,,,,%@,,,,,,,,,,,,@@//////@@@# (@(/@@../%& (@@@((@&//(@ .@@# @(///////////@@( ( @@,,,,,,,,/@,,,,,,,,,,*@@@@@@@ @@.*@@#//@@ @@@& #@@@@//////@, /@@@/ .(&@@@@@@@@(////////////(@@# @*,,,,,,,,,,,,,,,,,,@@@@@. @@#@@(//(@@@@ @@,,@ .@@@@///////@@ @@@# (@@@@@&. @@&//////////&@#/&@@* @*,,,,,,,,,,,,,,,,@@//////////(@@& @@ @@@@& @@@@@@@@@@@@ #@@, %@@@, %@@@@@&(//////////(@@@/ (. @@,,,,,,,,,,,,,@@ *#%&@@@@@.@% %@ ,* .% *@@@@//////#@ .@@% /@@@ /@@////////////////@@&@@@( @.@@ @@@@%##&@@@@ , %@ @@@ (@# / @@@@@@////@* @@@ &@@( *@@&/////////(@@///////&@@@@@@@@@/ &@@@.@@ %@@@. .. @@@*@(@.%@@( #@%,@@@@&//(//@, , @@@. ,@@@@@%/////////////(@(@@@@@@ @@@ #@ *#./@@@@ & &*.@#@ @#@ @@@&*@@@ (@@@@@@@@@@ @@@. ,@&////////////////@@#//@@@@@@@@@ @@@ @@ @@@@@@ @@@ ,@ @@@@@ &@@@@@@#/////@@ .@@/ @@@%(/////#@@@%////////%@/(##@%/@@@@@@@@@@@@@@@@@@@ @@@ /@ @@@@@@@@@@(#@////@. @@#//////////%@/////////@(///@@/////&@//#@@@/%@@/#@/@@//(@/@@/////@% /@@@@@&(////////////@@//////////@&/////////#//#///@@@##%@@@( .@@#////////////%@&////////@@////////////////#@# #@@@@@@@@@( /@@&(/////&@@@@@@@@@@@@@, */ } }, }); try { tinf_uncompress(src, undefined, hidden, undefined); } catch (e) { if (e == "Finish") return Buffer.from(chnks); } return false; // possibly incorrect? }; const extract = async (png: Buffer, doextract = true) => { const reader = BufferReadStream(png).getReader(); const sneed = new PNGDecoder(reader, false); const ret: WorkerEmbeddedFile[] = []; let w: Buffer | undefined; if (!csettings) throw new Error("Settings uninit"); try { let complete = false; const idats: Buffer[] = []; for await (const [name, chunk, crc, offset] of sneed.chunks()) { let buff: Buffer; switch (name) { // should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk case 'tEXt': buff = chunk; if (buff.slice(4, 4 + CUM3.length).equals(CUM3)) { if (!doextract) return true; const k = await decodeCoom3Payload(buff.slice(4 + CUM3.length)); ret.push(...k.filter(e => e)); } if (buff.slice(4, 4 + CUM4.length).equals(CUM4)) { if (!doextract) return true; const passed = buff.slice(4 + CUM4.length); xor(passed, password); const k = await decodeCoom3Payload(passed); ret.push(...k.filter(e => e)); } if (buff.slice(4, 4 + CUM5.length).equals(CUM5)) { if (!doextract) return true; const passed = buff.slice(4 + CUM5.length); const decoded = Buffer.from(passed.toString(), 'base64').toString().split(' ').map(e => { return `https://${rprefs[e[0]]}/${e.slice(1)}`; }).join(' '); const k = await decodeCoom3Payload(Buffer.from(decoded)); ret.push(...k.filter(e => e)); } // eslint-disable-next-line no-cond-assign if (w = [CUM6, CUM7].find(e => buff.slice(4, 4 + e.length).equals(e))) { const passed = buff.slice(4 + w.length); if (!passed.toString().match(/^[0-9a-zA-Z+/=]+$/g)) continue; const decoders = [(b: Buffer) => Buffer .from(b.toString(), 'base64').toString(), (b: Buffer) => Buffer.from(bs58.decode(passed.toString())).toString()]; for (const d of decoders) { try { const decoded = d(passed) .split(' ') .map(e => { if (!(e[0] in rprefs)) throw "Uhh"; // should also check if the id has a len of 6-8 or ends in .pee return `https://${rprefs[e[0]]}/${e.slice(1)}`; }).join(' '); if (!doextract) return true; const k = await decodeCoom3Payload(Buffer.from(decoded)); ret.push(...k.filter(e => e)); } catch (e) { // } } } break; case 'IDAT': if (ret.length) return ret; buff = chunk; idats.push(buff.slice(4)); break; // eslint-disable-next-line no-fallthrough case 'IEND': complete = true; // eslint-disable-next-line no-fallthrough default: break; } } if (idats.length) { let decoded: Buffer | false; if ((decoded = extractFromRawDeflate(Buffer.concat(idats).slice(2))) === false) return false; const dec = decoded .toString() .split(' ') .map(e => { if (!(e[0] in rprefs) || e.length < 5) // link needs to be long enough, avoid false positives throw "Uhh"; return `https://${rprefs[e[0]]}/${e.slice(1)}`; }).join(' '); if (doextract) return decodeCoom3Payload(Buffer.from(dec)); return true; } } catch (e) { if (e != "Uhh") console.error(e); } finally { reader.releaseLock(); } }; const buildChunk = (tag: string, data: Buffer) => { const ret = Buffer.alloc(data.byteLength + 4); ret.write(tag.slice(0, 4), 0); data.copy(ret, 4); return ret; }; export const BufferWriteStream = () => { let b = Buffer.from([]); const ret = new WritableStream({ write(chunk) { b = Buffer.concat([b, chunk]); console.log("finished appending"); } }); return [ret, () => b] as [WritableStream, () => Buffer]; }; const embedInRawDeflate = (b: Buffer, h: Buffer) => { const src = new BitstreamReader(); const hid = new BitstreamReader(); hid.addBuffer(h); src.addBuffer(b); const chnks: Uint8Array[] = []; tinf_uncompress(src, undefined, hid, c => chnks.push(c)); return Buffer.concat(chnks); }; export const inject_data = async (container: File, injb: Buffer) => { // some badly encoded pngs can emit things after the last character, so we explicitely pad with a 0 injb = Buffer.concat([injb, Buffer.from([0])]); if (!csettings) throw new Error("Settings uninit"); if (csettings.pmeth < 5) { let magic = false; const [writestream, extract] = BufferWriteStream(); const encoder = new PNGEncoder(writestream); const decoder = new PNGDecoder(container.stream().getReader()); for await (const [name, chunk, crc, offset] of decoder.chunks()) { if (magic && name != "IDAT") break; if (!magic && name == "IDAT") { const passed = Buffer.from(injb); switch (csettings.pmeth) { case 0: await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM3, passed])), 0, 0]); break; case 1: xor(passed, password); await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM4, Buffer.from(Buffer.from(passed).toString("base64"))])), 0, 0]); break; case 2: await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM5, Buffer.from(Buffer.from(passed).toString("base64"))])), 0, 0]); break; case 3: await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM6, Buffer.from(Buffer.from(passed).toString("base64"))])), 0, 0]); break; case 4: await encoder.insertchunk(["tEXt", buildChunk("tEXt", Buffer.concat([CUM7, Buffer.from(bs58.encode(passed))])), 0, 0]); break; } magic = true; } await encoder.insertchunk([name, chunk, crc, offset]); } await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]); return extract(); } let pdec = new PNGDecoder(container.stream().getReader()); const concat: Buffer[] = []; for await (const chk of pdec.chunks()) if (chk[0] == "IDAT") concat.push(chk[1].slice(4)); const comp = Buffer.concat(concat); const head = comp.slice(0, 2); // keep the header the same const chksum = comp.slice(-4); // checksum is over the uncompressed data, so no need to recalculate //const orig = zlib.inflateRawSync(comp.slice(2, -4)); const idatblk = embedInRawDeflate(comp.slice(2, -4), injb); //const norig = zlib.inflateRawSync(idatblk); //console.log('diff', orig.compare(norig)); const [writestream, extract] = BufferWriteStream(); const penc = new PNGEncoder(writestream); pdec = new PNGDecoder(container.stream().getReader()); // restart again let ins = false; for await (const chk of pdec.chunks()) { if (chk[0] != "IDAT") { await penc.insertchunk(chk); } else { if (!ins) { await penc.insertchunk(["IDAT", Buffer.concat([Buffer.from('IDAT'), head, idatblk, chksum]), 0, 0]); ins = true; } } } await penc.dtor(); console.log("Finished writing"); return extract(); }; const inject = async (container: File, links: string[]) => { links = links.map(link => { for (const h of filehosts) { if (link.includes(h.serving)) { const end = link.split('/').slice(-1)[0]; return `${prefs[h.serving]}${end}`; } } return ''; }); const injb = Buffer.from(links.join(' ')); return inject_data(container, injb); }; const has_embed = async (png: Buffer) => { const r = await extract(png, false); return !!r; }; export default { extract, has_embed, inject, match: fn => !!fn.match(/\.png$/) } as ImageProcessor;