Browse Source

Use newer system to allow for big embeds

pull/4/head
coomdev 2 years ago
parent
commit
ea4e937358
  1. 188
      src/main.ts
  2. 77
      src/png.ts

188
src/main.ts

@ -1,76 +1,49 @@
/* eslint-disable */ /* eslint-disable */
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { buf } from "crc-32";
import { fileTypeFromBuffer } from 'file-type'; import { fileTypeFromBuffer } from 'file-type';
import { Readable } from "stream"; import { concatAB, PNGDecoder, PNGEncoder } from "./png";
const IDAT = Buffer.from("IDAT"); const IDAT = Buffer.from("IDAT");
const IEND = Buffer.from("IEND"); const IEND = Buffer.from("IEND");
const tEXt = Buffer.from("tEXt"); const tEXt = Buffer.from("tEXt");
const CUM0 = Buffer.from("CUM0"); const CUM0 = Buffer.from("CUM0\0IMGONNACOOMAAAAAAAAA");
let concatAB = (...bufs: Buffer[]) => { let extractEmbedded = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let sz = bufs.map(e => e.byteLength).reduce((a, b) => a + b); let magic = false;
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<Uint8Array>) => {
let total = Buffer.from('');
let ptr = 8;
let req = 8; // required bytes: require the png signature
let sneed = new PNGDecoder(reader);
try { try {
let chunk: ReadableStreamDefaultReadResult<Uint8Array>; let lastIDAT: Buffer | null = null;
for await (let [name, chunk, crc, offset] of sneed.chunks()) {
const catchup = async () => { switch (name) {
while (total.byteLength < req) { case 'tEXt': // should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
chunk = await reader.read(); if (chunk.slice(4, 4 + CUM0.length).equals(CUM0))
if (chunk.done) magic = true;
throw new Error("Unexpected EOF"); break;
total = concatAB(total, Buffer.from(chunk.value)); case 'IDAT':
if (magic) {
lastIDAT = chunk;
break;
}
case 'IEND':
if (!magic)
throw "Didn't find tExt Chunk";
default:
break;
} }
} }
if (lastIDAT) {
do { let data = (lastIDAT as Buffer).slice(4);
req += 8; // require the bytes that store the length of the next chunk and its name let fnsize = data.readUInt32LE(0);
await catchup(); let fn = data.slice(4, 4 + fnsize).toString();
// at this point, ptr pointing to length of current chunk // Todo: xor the buffer to prevent scanning for file signatures (4chan embedded file detection)?
let length = total.readInt32BE(ptr); data = data.slice(4 + fnsize);
// ptr pointing to type of current chunk return { filename: fn, data };
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);
} catch (e) { } catch (e) {
//console.error(e); console.error(e);
await reader.cancel(); } finally {
reader.releaseLock(); reader.releaseLock();
} }
} }
@ -82,7 +55,7 @@ let processImage = async (src: string) => {
let reader = (await resp.blob()).stream(); let reader = (await resp.blob()).stream();
if (!reader) if (!reader)
return; return;
return await extractTextData(new ReadableStreamDefaultReader(reader)); return await extractEmbedded(reader.getReader());
}; };
/* Used for debugging */ /* Used for debugging */
@ -189,54 +162,45 @@ let processPost = async (post: HTMLDivElement) => {
fi.children[1].insertAdjacentElement('afterend', a); fi.children[1].insertAdjacentElement('afterend', a);
} }
let buildTextChunk = async (f: File) => { let buildChunk = (tag: string, data: Buffer) => {
let ab = await f.arrayBuffer(); let ret = Buffer.alloc(data.byteLength + 4);
let fns = Buffer.alloc(4); ret.write(tag.substr(0, 4), 0);
fns.writeInt32LE(f.name.length, 0) data.copy(ret, 4);
let fb = Buffer.from(await new Blob([fns, f.name, ab]).arrayBuffer()).toString('base64'); return ret;
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 let BufferWriteStream = () => {
ptr += 4; let b = Buffer.from([])
buff.write("tEXtCUM0\0", ptr); // Writes Chunktype+ Magic+null byte let ret = new WritableStream<Buffer>({
ptr += 9; write(chunk) {
buff.write(fb, ptr); b = concatAB(b, chunk);
ptr += fb.length; }
// CRC over the chunk name to the last piece of data });
let checksum = buf(buff.slice(4, -4)) return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
buff.writeInt32BE(checksum, ptr);
return buff;
} }
let buildInjection = async (container: File, inj: File) => { let buildInjection = async (container: File, inj: File) => {
let tEXtChunk = await buildTextChunk(inj); let [writestream, extract] = BufferWriteStream();
let ogFile = Buffer.from(await container.arrayBuffer()); let encoder = new PNGEncoder(writestream);
let ret = Buffer.alloc(tEXtChunk.byteLength + ogFile.byteLength); let decoder = new PNGDecoder(container.stream().getReader());
let ptr = 8;
let wptr = 8; let magic = false;
let wrote = false; for await (let [name, chunk, crc, offset] of decoder.chunks()) {
ogFile.copy(ret, 0, 0, ptr);// copy PNG signature if (magic && name != "IDAT")
// copy every chunk as is except inject the text chunk before the first IDAT or END break;
while (ptr < ogFile.byteLength) { if (!magic && name == "IDAT") {
let len = ogFile.readInt32BE(ptr); await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]);
let name = ogFile.slice(ptr + 4, ptr + 8); magic = true;
if (name.equals(IDAT) || name.equals(IEND)) {
if (!wrote) {
wrote = true;
tEXtChunk.copy(ret, wptr);
wptr += tEXtChunk.byteLength;
}
} }
ret.writeInt32BE(len, wptr); await encoder.insertchunk([name, chunk, crc, offset]);
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;
} }
let injb = Buffer.alloc(4 + inj.name.length + inj.size);
return { file: new Blob([ret]), name: container.name }; 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 () => { const startup = async () => {
@ -279,9 +243,25 @@ const startup = async () => {
document.dispatchEvent(new CustomEvent('QRSetFile', { detail: await buildInjection(file, input.files[0]) })) document.dispatchEvent(new CustomEvent('QRSetFile', { detail: await buildInjection(file, input.files[0]) }))
}) })
input.click(); input.click();
} }
})); }));
}; };
document.addEventListener('4chanXInitFinished', startup); 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!]));
}
}
}

77
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<Uint8Array>) {
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<Buffer>;
constructor(bytes: WritableStream<Buffer>) {
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();
}
}
Loading…
Cancel
Save