coomdev
2 years ago
11 changed files with 187 additions and 43 deletions
@ -0,0 +1,149 @@ |
|||
import { buf } from "crc-32"; |
|||
import { Buffer } from "buffer"; |
|||
import type { ImageProcessor } from "./main"; |
|||
import { PNGDecoder, PNGEncoder } from "./png"; |
|||
import { decodeCoom3Payload } from "./utils"; |
|||
|
|||
const CUM0 = Buffer.from("CUM\0" + "0"); |
|||
const CUM3 = Buffer.from("CUM\0" + "3"); |
|||
|
|||
const BufferReadStream = (b: Buffer) => { |
|||
const ret = new ReadableStream<Buffer>({ |
|||
pull(cont) { |
|||
cont.enqueue(b); |
|||
cont.close(); |
|||
} |
|||
}); |
|||
return ret; |
|||
}; |
|||
|
|||
const extract = async (png: Buffer) => { |
|||
let magic = false; |
|||
let coom3 = false; |
|||
const reader = BufferReadStream(png).getReader(); |
|||
const sneed = new PNGDecoder(reader); |
|||
try { |
|||
let lastIDAT: Buffer | null = null; |
|||
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 + CUM0.length).equals(CUM0)) |
|||
magic = true; |
|||
if (buff.slice(4, 4 + CUM0.length).equals(CUM3)) { |
|||
coom3 = true; |
|||
magic = true; |
|||
} |
|||
break; |
|||
case 'IDAT': |
|||
if (magic) { |
|||
lastIDAT = chunk; |
|||
break; |
|||
} |
|||
// eslint-disable-next-line no-fallthrough
|
|||
case 'IEND': |
|||
if (!magic) |
|||
return; // Didn't find tExt Chunk;
|
|||
// eslint-disable-next-line no-fallthrough
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
if (lastIDAT) { |
|||
let data = (lastIDAT as Buffer).slice(4); |
|||
if (coom3) |
|||
return decodeCoom3Payload(data); |
|||
const fnsize = data.readUInt32LE(0); |
|||
const 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); |
|||
} 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]; |
|||
}; |
|||
|
|||
const inject = async (container: File, inj: File) => { |
|||
const [writestream, extract] = BufferWriteStream(); |
|||
const encoder = new PNGEncoder(writestream); |
|||
const decoder = new PNGDecoder(container.stream().getReader()); |
|||
|
|||
let magic = false; |
|||
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", CUM0), 0, 0]); |
|||
magic = true; |
|||
} |
|||
await encoder.insertchunk([name, chunk, crc, offset]); |
|||
} |
|||
const 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 extract(); |
|||
}; |
|||
|
|||
const has_embed = async (png: Buffer) => { |
|||
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 + CUM0.length).equals(CUM0)) |
|||
return true; |
|||
if (buff.slice(4, 4 + CUM0.length).equals(CUM3)) |
|||
return true; |
|||
break; |
|||
case 'IDAT': |
|||
// eslint-disable-next-line no-fallthrough
|
|||
case 'IEND': |
|||
return false; // 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; // possibly unexpected EOF, need more data to decide
|
|||
} finally { |
|||
reader.releaseLock(); |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
extract, |
|||
has_embed, |
|||
inject, |
|||
match: fn => !!fn.match(/\.png$/) |
|||
} as ImageProcessor; |
@ -0,0 +1,5 @@ |
|||
import type { Buffer } from "buffer"; |
|||
|
|||
export const decodeCoom3Payload = (buff: Buffer) => { |
|||
//
|
|||
}; |
Loading…
Reference in new issue