From ea4e937358c060ad7a03f64fca330d3bb0ba793d Mon Sep 17 00:00:00 2001 From: coomdev Date: Fri, 24 Dec 2021 06:12:08 +0100 Subject: [PATCH] Use newer system to allow for big embeds --- src/main.ts | 188 +++++++++++++++++++++++----------------------------- src/png.ts | 77 +++++++++++++++++++++ 2 files changed, 161 insertions(+), 104 deletions(-) create mode 100644 src/png.ts diff --git a/src/main.ts b/src/main.ts index 171e26f..3b085f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,76 +1,49 @@ /* eslint-disable */ import { Buffer } from "buffer"; -import { buf } from "crc-32"; import { fileTypeFromBuffer } from 'file-type'; -import { Readable } from "stream"; +import { concatAB, PNGDecoder, PNGEncoder } from "./png"; const IDAT = Buffer.from("IDAT"); const IEND = Buffer.from("IEND"); const tEXt = Buffer.from("tEXt"); -const CUM0 = Buffer.from("CUM0"); +const CUM0 = Buffer.from("CUM0\0IMGONNACOOMAAAAAAAAA"); -let concatAB = (...bufs: Buffer[]) => { - let sz = bufs.map(e => e.byteLength).reduce((a, b) => a + b); - const ret = Buffer.alloc(sz); - let ptr = 0; - for (const b of bufs) { - b.copy(ret, ptr); - ptr += b.byteLength; - } - return ret; -} - -let extractTextData = async (reader: ReadableStreamDefaultReader) => { - let total = Buffer.from(''); - let ptr = 8; - let req = 8; // required bytes: require the png signature +let extractEmbedded = async (reader: ReadableStreamDefaultReader) => { + let magic = false; + let sneed = new PNGDecoder(reader); try { - let chunk: ReadableStreamDefaultReadResult; - - const catchup = async () => { - while (total.byteLength < req) { - chunk = await reader.read(); - if (chunk.done) - throw new Error("Unexpected EOF"); - total = concatAB(total, Buffer.from(chunk.value)); + let lastIDAT: Buffer | null = null; + for await (let [name, chunk, crc, offset] of sneed.chunks()) { + switch (name) { + case 'tEXt': // should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk + if (chunk.slice(4, 4 + CUM0.length).equals(CUM0)) + magic = true; + break; + case 'IDAT': + if (magic) { + lastIDAT = chunk; + break; + } + case 'IEND': + if (!magic) + throw "Didn't find tExt Chunk"; + default: + break; } } - - do { - req += 8; // require the bytes that store the length of the next chunk and its name - await catchup(); - // at this point, ptr pointing to length of current chunk - let length = total.readInt32BE(ptr); - // ptr pointing to type of current chunk - ptr += 4; - const name = total.slice(ptr, ptr + 4); - if (Buffer.compare(IDAT, name) == 0 || - Buffer.compare(IEND, name) == 0) { - // reached idat or iend before finding a tEXt, bail out - throw new Error("Couldn't find tEXt chunk"); - } - req += length + 4; // require the rest of the chunk + CRC - //let crc = total.readInt32BE(ptr + 4 + length); // dont really care - ptr += 4; // ptr now points to the chunk data - if (Buffer.compare(tEXt, name) == 0) { - // our specific format stores a single file, CUM0 stores it as Base64. Could be enhanced to use more characters (the whole printable ascii characters, ie base85, but we lack good encoders...) - // catchup because we need to know the type; - await catchup(); - if (Buffer.compare(total.slice(ptr, ptr + 4), CUM0) == 0) { - let data = Buffer.from(total.slice(ptr + 4, ptr + length - 4).toString(), 'base64'); - let fns = data.readUInt32LE(0); - let filename = data.slice(4, 4 + fns).toString(); - return { data: data.slice(4 + fns), filename }; - } - // Unknown tEXt format - } - ptr += length + 4; // skips over data section and crc - } while (!chunk!.done); + if (lastIDAT) { + let data = (lastIDAT as Buffer).slice(4); + let fnsize = data.readUInt32LE(0); + let fn = data.slice(4, 4 + fnsize).toString(); + // Todo: xor the buffer to prevent scanning for file signatures (4chan embedded file detection)? + data = data.slice(4 + fnsize); + return { filename: fn, data }; + } } catch (e) { - //console.error(e); - await reader.cancel(); + console.error(e); + } finally { reader.releaseLock(); } } @@ -82,7 +55,7 @@ let processImage = async (src: string) => { let reader = (await resp.blob()).stream(); if (!reader) return; - return await extractTextData(new ReadableStreamDefaultReader(reader)); + return await extractEmbedded(reader.getReader()); }; /* Used for debugging */ @@ -189,54 +162,45 @@ let processPost = async (post: HTMLDivElement) => { fi.children[1].insertAdjacentElement('afterend', a); } -let buildTextChunk = async (f: File) => { - let ab = await f.arrayBuffer(); - let fns = Buffer.alloc(4); - fns.writeInt32LE(f.name.length, 0) - let fb = Buffer.from(await new Blob([fns, f.name, ab]).arrayBuffer()).toString('base64'); - let buff = Buffer.alloc(4 /*Length storage*/ + 4 /*Chunk Type*/ + 4 /*Magic*/ + 1 /*Null separator*/ + fb.length + 4 /* CRC */); - let ptr = 0; - buff.writeInt32BE(buff.byteLength - 12, ptr); // doesn't count chunk type, lenght storage and crc - ptr += 4; - buff.write("tEXtCUM0\0", ptr); // Writes Chunktype+ Magic+null byte - ptr += 9; - buff.write(fb, ptr); - ptr += fb.length; - // CRC over the chunk name to the last piece of data - let checksum = buf(buff.slice(4, -4)) - buff.writeInt32BE(checksum, ptr); - return buff; +let buildChunk = (tag: string, data: Buffer) => { + let ret = Buffer.alloc(data.byteLength + 4); + ret.write(tag.substr(0, 4), 0); + data.copy(ret, 4); + return ret; +} + +let BufferWriteStream = () => { + let b = Buffer.from([]) + let ret = new WritableStream({ + write(chunk) { + b = concatAB(b, chunk); + } + }); + return [ret, () => b] as [WritableStream, () => Buffer]; } let buildInjection = async (container: File, inj: File) => { - let tEXtChunk = await buildTextChunk(inj); - let ogFile = Buffer.from(await container.arrayBuffer()); - let ret = Buffer.alloc(tEXtChunk.byteLength + ogFile.byteLength); - let ptr = 8; - let wptr = 8; - let wrote = false; - ogFile.copy(ret, 0, 0, ptr);// copy PNG signature - // copy every chunk as is except inject the text chunk before the first IDAT or END - while (ptr < ogFile.byteLength) { - let len = ogFile.readInt32BE(ptr); - let name = ogFile.slice(ptr + 4, ptr + 8); - if (name.equals(IDAT) || name.equals(IEND)) { - if (!wrote) { - wrote = true; - tEXtChunk.copy(ret, wptr); - wptr += tEXtChunk.byteLength; - } + let [writestream, extract] = BufferWriteStream(); + let encoder = new PNGEncoder(writestream); + let decoder = new PNGDecoder(container.stream().getReader()); + + let magic = false; + for await (let [name, chunk, crc, offset] of decoder.chunks()) { + if (magic && name != "IDAT") + break; + if (!magic && name == "IDAT") { + await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]); + magic = true; } - ret.writeInt32BE(len, wptr); - wptr += 4; - name.copy(ret, wptr); - wptr += 4; - ogFile.slice(ptr + 8, ptr + 8 + len + 4).copy(ret, wptr); - ptr += len + 8 + 4; - wptr += len + 4; + await encoder.insertchunk([name, chunk, crc, offset]); } - - return { file: new Blob([ret]), name: container.name }; + let injb = Buffer.alloc(4 + inj.name.length + inj.size); + injb.writeInt32LE(inj.name.length, 0); + injb.write(inj.name, 4); + Buffer.from(await inj.arrayBuffer()).copy(injb, 4 + inj.name.length); + await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]); + await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]); + return { file: new Blob([extract()]), name: container.name }; } const startup = async () => { @@ -279,9 +243,25 @@ const startup = async () => { document.dispatchEvent(new CustomEvent('QRSetFile', { detail: await buildInjection(file, input.files[0]) })) }) input.click(); - } })); }; document.addEventListener('4chanXInitFinished', startup); + + +onload = () => { + let container = document.getElementById("container") as HTMLInputElement; + let injection = document.getElementById("injection") as HTMLInputElement; + + container.onchange = injection.onchange = async () => { + if (container.files?.length && injection.files?.length) { + let res = await buildInjection(container.files[0], injection.files[0]); + let result = document.getElementById("result") as HTMLImageElement; + let extracted = document.getElementById("extracted") as HTMLImageElement; + result.src = URL.createObjectURL(res.file); + let embedded = await extractEmbedded(res.file.stream().getReader()); + extracted.src = URL.createObjectURL(new Blob([embedded?.data!])); + } + } +} diff --git a/src/png.ts b/src/png.ts new file mode 100644 index 0000000..0387e05 --- /dev/null +++ b/src/png.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ +import { buf } from "crc-32"; +import { Buffer } from "buffer"; + +export let concatAB = (...bufs: Buffer[]) => { + let sz = bufs.map(e => e.byteLength).reduce((a, b) => a + b); + const ret = Buffer.alloc(sz); + let ptr = 0; + for (const b of bufs) { + b.copy(ret, ptr); + ptr += b.byteLength; + } + return ret; +} + +export type PNGChunk = [string, Buffer, number, number]; + +export class PNGDecoder { + repr: Buffer; + req = 8; + ptr = 8; + + constructor(private reader: ReadableStreamDefaultReader) { + this.repr = Buffer.from([]); + } + + async catchup() { + while (this.repr.byteLength < this.req) { + let chunk = await this.reader.read(); + if (chunk.done) + throw new Error("Unexpected EOF"); + this.repr = concatAB(this.repr, Buffer.from(chunk.value)); + } + } + + async *chunks() { + while (true) { + this.req += 8; // req length and name + await this.catchup(); + let length = this.repr.readUInt32BE(this.ptr); + let name = this.repr.slice(this.ptr + 4, this.ptr + 8).toString(); + this.ptr += 4; + this.req += length + 4; // crc + await this.catchup(); + yield [name, this.repr.slice(this.ptr, this.ptr + length + 4 /* chunkname included in buffer for easier crc fixup */), this.repr.readUInt32BE(this.ptr + length + 4), this.ptr] as PNGChunk; + this.ptr += length + 8; + if (name == 'IEND') + break; + } + } + + async dtor() { + } +} + +export class PNGEncoder { + writer: WritableStreamDefaultWriter; + + constructor(bytes: WritableStream) { + this.writer = bytes.getWriter(); + this.writer.write(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])); + } + + async insertchunk(chunk: PNGChunk) { + let b = Buffer.alloc(4); + b.writeInt32BE(chunk[1].length - 4, 0); + await this.writer.write(b); // write length + await this.writer.write(chunk[1]); // chunk includes chunkname + b.writeInt32BE(buf(chunk[1]), 0); + await this.writer.write(b); + } + + async dtor() { + this.writer.releaseLock(); + await this.writer.close(); + } +} \ No newline at end of file