You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
383 lines
24 KiB
383 lines
24 KiB
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<typeof settings['set']>[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<Buffer>({
|
|
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)
|
|
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))
|
|
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 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<Buffer>({
|
|
write(chunk) {
|
|
b = Buffer.concat([b, chunk]);
|
|
console.log("finished appending");
|
|
}
|
|
});
|
|
return [ret, () => b] as [WritableStream<Buffer>, () => 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;
|
|
|