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 */
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<Uint8Array>) => {
let total = Buffer.from('');
let ptr = 8;
let req = 8; // required bytes: require the png signature
let extractEmbedded = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let magic = false;
let sneed = new PNGDecoder(reader);
try {
let chunk: ReadableStreamDefaultReadResult<Uint8Array>;
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<Buffer>({
write(chunk) {
b = concatAB(b, chunk);
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => 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!]));
}
}
}

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