Can embed any file in a PNG/WebM/GIF/JPEG and upload it to a third-party host through 4chan
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.
 
 
 

150 lines
5.1 KiB

import { Buffer } from "buffer";
import { EMBED_TYPES, ImageProcessor, EmbeddedFile, EMBED_STATUS } from './main';
import { PNGDecoder, PNGEncoder } from "./png";
import { buildPeeFile, decodeCoom3Payload, fireNotification } from "./utils";
import { GM_fetch } from "./requests";
const CUM3 = Buffer.from("doo\0" + "m");
const BufferReadStream = (b: Buffer) => {
const ret = new ReadableStream<Buffer>({
pull(cont) {
cont.enqueue(b);
cont.close();
}
});
return ret;
};
const extract = async (png: Buffer): Promise<EmbeddedFile[] | undefined> => {
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
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)) {
const results = await decodeCoom3Payload(buff.slice(4 + CUM3.length));
const filtered = results.filter(e => e != null) as EmbeddedFile[];
return filtered.length > 0 ? filtered : undefined;
}
break;
case 'IDAT':
// eslint-disable-next-line no-fallthrough
case 'IEND':
return;
// eslint-disable-next-line no-fallthrough
default:
break;
}
}
} catch (e) {
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]);
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
};
function parseForm(data: object) {
const form = new FormData();
Object.entries(data)
.filter(([key, value]) => value !== null)
.map(([key, value]) => form.append(key, value));
return form;
}
const inject = async (container: File, injs: File[]) => {
const [writestream, extract] = BufferWriteStream();
const encoder = new PNGEncoder(writestream);
const decoder = new PNGDecoder(container.stream().getReader());
let total = 0;
fireNotification('info', `Uploading ${injs.length} files...`);
const links = await Promise.all(injs.map(async inj => {
const ret = await (await GM_fetch("https://catbox.moe/user/api.php", {
method: 'POST',
body: parseForm({
reqtype: 'fileupload',
fileToUpload: await buildPeeFile(inj)
})
})).text();
fireNotification('info', `Uploaded files [${++total}/${injs.length}] ${ret}`);
return ret;
}));
let magic = false;
const injb = Buffer.from(links.join(' '));
for await (const [name, chunk, crc, offset] of decoder.chunks()) {
if (magic && name != "IDAT")
break;
if (!magic && name == "IDAT") {
await encoder.insertchunk(["tEXt", buildChunk("tEXt",
Buffer.concat([CUM3, injb])), 0, 0]);
magic = true;
}
await encoder.insertchunk([name, chunk, crc, offset]);
}
await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]);
return extract();
};
const has_embed = async (png: Buffer): Promise<EMBED_STATUS> => {
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader);
try {
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))
return EMBED_STATUS.SUCCESS;
break;
case 'IDAT':
// eslint-disable-next-line no-fallthrough
case 'IEND':
return EMBED_STATUS.NONE; // Didn't find tExt Chunk; Definite no
// eslint-disable-next-line no-fallthrough
default:
break;
}
}
// stream ended on chunk boundary, so no unexpected EOF was fired, need more data anyway
} catch (e) {
return EMBED_STATUS.PEE_UNDEFINED; // possibly unexpected EOF, need more data to decide
} finally {
reader.releaseLock();
}
return EMBED_STATUS.PEE_UNDEFINED; // possibly unexpected EOF, need more data to decide
};
export default {
extract,
has_embed,
inject,
match: fn => !!fn.match(/\.png$/)
} as ImageProcessor;