From 0c8dd166fa32e29bcc9e967a843bf74ecf2d8987 Mon Sep 17 00:00:00 2001 From: coomdev Date: Wed, 12 Jan 2022 07:58:46 +0100 Subject: [PATCH] Fix Regex matching and hover thing? --- main.meta.js | 2 +- main.user.js | 29 ++++----- src/Embedding.svelte | 2 + src/gif.ts | 2 +- src/main.ts | 10 +-- src/png.ts | 10 +-- src/pngv3.ts | 149 +++++++++++++++++++++++++++++++++++++++++++ src/pomf.ts | 18 ++---- src/thirdeye.ts | 1 - src/utils.ts | 5 ++ src/webm.ts | 2 +- 11 files changed, 187 insertions(+), 43 deletions(-) create mode 100644 src/pngv3.ts create mode 100644 src/utils.ts diff --git a/main.meta.js b/main.meta.js index ddceb70..b7153e9 100644 --- a/main.meta.js +++ b/main.meta.js @@ -1,7 +1,7 @@ // ==UserScript== // @name PNGExtraEmbed // @namespace https://coom.tech/ -// @version 0.115 +// @version 0.116 // @description uhh // @author You // @match https://boards.4channel.org/* diff --git a/main.user.js b/main.user.js index 8c1e947..7e32861 100644 --- a/main.user.js +++ b/main.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name PNGExtraEmbed // @namespace https://coom.tech/ -// @version 0.115 +// @version 0.116 // @description uhh // @author You // @match https://boards.4channel.org/* @@ -11181,7 +11181,7 @@ const fnsize = data.readUInt32LE(0); const fn = data.slice(4, 4 + fnsize).toString(); data = data.slice(4 + fnsize); - return { filename: fn, data }; + return [{ filename: fn, data }]; } } catch (e) { console.error(e); @@ -11191,7 +11191,7 @@ }; var buildChunk = (tag, data) => { const ret = import_buffer.Buffer.alloc(data.byteLength + 4); - ret.write(tag.substr(0, 4), 0); + ret.write(tag.slice(0, 4), 0); data.copy(ret, 4); return ret; }; @@ -11345,7 +11345,7 @@ return; const chk = chunks[embed2 + 1]; if (chk.type == "b" && chk.name == "TagBinary") - return { filename: "string", data: chk.data }; + return [{ filename: "string", data: chk.data }]; }; var inject2 = async (container, inj) => embed(import_buffer2.Buffer.from(await container.arrayBuffer()), import_buffer2.Buffer.from(await inj.arrayBuffer())); var has_embed2 = (webm) => { @@ -11407,7 +11407,7 @@ ptr += sec.data.byteLength; end = sec.end; } while (sec.appname == "COOMTECH" && gif[end] == "!".charCodeAt(0)); - return { data: ret, filename: "embedded" }; + return [{ data: ret, filename: "embedded" }]; } end = sec.end; } @@ -11629,7 +11629,6 @@ var unlockQueue = Promise.resolve(); var queryCache = {}; var processQueries = async () => { - console.log("======== FIRIN ======="); let unlock; unlockQueue = new Promise((_) => unlock = _); const md5 = reqQueue.map((e) => e[0]).filter((e) => !(e in queryCache)); @@ -11747,7 +11746,7 @@ ]; var getExt = (fn) => { const isDum = fn.match(/^([a-z0-9]{6}\.(?:jpe?g|png|webm|gif))/gi); - const isB64 = fn.match(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/); + const isB64 = fn.match(/^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))?\.(gif|jpe?g|png|webm)/); const isExt = fn.match(/\[.*=(.*)\]/); let ext; if (isDum) { @@ -11770,7 +11769,7 @@ } catch { } } - return { + return [{ filename: ext, data: async (lsn) => { try { @@ -11779,7 +11778,7 @@ } }, thumbnail: hasembed_default - }; + }]; }; var has_embed5 = async (b, fn) => { const ext = getExt(fn); @@ -11798,13 +11797,7 @@ skip: true, extract: extract5, has_embed: has_embed5, - match: (fn) => { - const base = fn.split(".").slice(0, -1).join("."); - const isDum = !!fn.match(/^([a-z0-9]{6}\.(?:jpe?g|png|webm|gif))/gi); - const isB64 = !!fn.match(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/); - const isExt = !!fn.match(/\[.*=.*\]/); - return isB64 || isExt || isDum; - } + match: (fn) => !!getExt(fn) }; // src/App.svelte @@ -15847,6 +15840,8 @@ if (!contracted) return; const [sw, sh] = [visualViewport.width, visualViewport.height]; + if (dims[0] == 0 && dims[1] == 0) + recompute(); let width = dims[0]; let height = dims[1] + 25; let { clientX, clientY } = ev || lastev; @@ -16647,7 +16642,7 @@ res2 = res2?.filter((e) => e); if (!res2 || res2.length == 0) return; - processAttachments(post, res2?.filter((e) => e)); + processAttachments(post, res2?.filter((e) => e).flatMap((e) => e[0].map((k) => [k, e[1]]))); }; var startup = async () => { if (typeof window["FCX"] != "undefined") diff --git a/src/Embedding.svelte b/src/Embedding.svelte index cdbc964..51eccd7 100644 --- a/src/Embedding.svelte +++ b/src/Embedding.svelte @@ -211,6 +211,8 @@ if (!contracted) return const [sw, sh] = [visualViewport.width, visualViewport.height] // shamelessly stolen from 4chanX + if (dims[0] == 0 && dims[1] == 0) + recompute(); let width = dims[0] let height = dims[1] + 25 let { clientX, clientY } = (ev || lastev!) diff --git a/src/gif.ts b/src/gif.ts index 65d724d..87e3077 100644 --- a/src/gif.ts +++ b/src/gif.ts @@ -43,7 +43,7 @@ const extractBuff = (gif: Buffer) => { ptr += sec.data.byteLength; end = sec.end; } while (sec.appname == "COOMTECH" && gif[end] == '!'.charCodeAt(0)); - return { data: ret, filename: 'embedded' } as EmbeddedFile; + return [{ data: ret, filename: 'embedded' }] as EmbeddedFile[]; } end = sec.end; } diff --git a/src/main.ts b/src/main.ts index dac08af..680f7e1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,7 +21,7 @@ export interface ImageProcessor { skip?: true; match(fn: string): boolean; has_embed(b: Buffer, fn?: string): boolean | Promise; - extract(b: Buffer, fn?: string): EmbeddedFile | Promise; + extract(b: Buffer, fn?: string): EmbeddedFile[] | Promise; inject?(b: File, c: File): Buffer | Promise; } @@ -86,14 +86,14 @@ type EmbeddedFileWithoutPreview = { export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview; -const processImage = async (src: string, fn: string, hex: string): Promise<([EmbeddedFile, boolean] | undefined)[]> => { +const processImage = async (src: string, fn: string, hex: string): Promise<([EmbeddedFile[], boolean] | undefined)[]> => { return Promise.all(processors.filter(e => e.match(fn)).map(async proc => { if (proc.skip) { // skip file downloading, file is referenced from the filename // basically does things like filtering out blacklisted tags const md5 = Buffer.from(hex, 'base64'); if (await proc.has_embed(md5, fn) === true) - return [await proc.extract(md5, fn), true] as [EmbeddedFile, boolean]; + return [await proc.extract(md5, fn), true] as [EmbeddedFile[], boolean]; return; } const iter = streamRemote(src); @@ -118,7 +118,7 @@ const processImage = async (src: string, fn: string, hex: string): Promise<([Emb //console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`); return; } - return [await proc.extract(cumul), false] as [EmbeddedFile, boolean]; + return [await proc.extract(cumul), false] as [EmbeddedFile[], boolean]; })); }; @@ -136,7 +136,7 @@ const processPost = async (post: HTMLDivElement) => { res2 = res2?.filter(e => e); if (!res2 || res2.length == 0) return; - processAttachments(post, res2?.filter(e => e) as [EmbeddedFile, boolean][]); + processAttachments(post, res2?.filter(e => e).flatMap(e => e![0].map(k => [k, e![1]] as [EmbeddedFile, boolean]))); }; const startup = async () => { diff --git a/src/png.ts b/src/png.ts index 38963ab..c966643 100644 --- a/src/png.ts +++ b/src/png.ts @@ -2,9 +2,9 @@ import { buf } from "crc-32"; import { Buffer } from "buffer"; import type { ImageProcessor } from "./main"; -type PNGChunk = [string, Buffer, number, number]; +export type PNGChunk = [string, Buffer, number, number]; -class PNGDecoder { +export class PNGDecoder { repr: Buffer; req = 8; @@ -47,7 +47,7 @@ class PNGDecoder { } } -class PNGEncoder { +export class PNGEncoder { writer: WritableStreamDefaultWriter; constructor(bytes: WritableStream) { @@ -118,7 +118,7 @@ const extract = async (png: Buffer) => { 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 }; + return [{ filename: fn, data }]; } } catch (e) { console.error(e); @@ -129,7 +129,7 @@ const extract = async (png: Buffer) => { const buildChunk = (tag: string, data: Buffer) => { const ret = Buffer.alloc(data.byteLength + 4); - ret.write(tag.substr(0, 4), 0); + ret.write(tag.slice(0, 4), 0); data.copy(ret, 4); return ret; }; diff --git a/src/pngv3.ts b/src/pngv3.ts new file mode 100644 index 0000000..735e540 --- /dev/null +++ b/src/pngv3.ts @@ -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({ + 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({ + write(chunk) { + b = Buffer.concat([b, chunk]); + } + }); + return [ret, () => b] as [WritableStream, () => 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; diff --git a/src/pomf.ts b/src/pomf.ts index 2ab9f02..562b33c 100644 --- a/src/pomf.ts +++ b/src/pomf.ts @@ -11,7 +11,7 @@ const sources = [ const getExt = (fn: string) => { const isDum = fn!.match(/^([a-z0-9]{6}\.(?:jpe?g|png|webm|gif))/gi); - const isB64 = fn!.match(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/); + const isB64 = fn!.match(/^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))?\.(gif|jpe?g|png|webm)/); const isExt = fn!.match(/\[.*=(.*)\]/); let ext; if (isDum) { @@ -37,8 +37,8 @@ const extract = async (b: Buffer, fn?: string) => { // 404 } } - - return { + + return [{ filename: ext, data: async (lsn) => { try { @@ -48,7 +48,7 @@ const extract = async (b: Buffer, fn?: string) => { } }, thumbnail - } as EmbeddedFile; + } as EmbeddedFile]; }; const has_embed = async (b: Buffer, fn?: string) => { @@ -63,7 +63,7 @@ const has_embed = async (b: Buffer, fn?: string) => { // 404 } } - + return false; }; @@ -71,11 +71,5 @@ export default { skip: true, extract, has_embed, - match: fn => { - const base = fn.split('.').slice(0, -1).join('.'); - const isDum = !!fn.match(/^([a-z0-9]{6}\.(?:jpe?g|png|webm|gif))/gi); - const isB64 = !!fn.match(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/); - const isExt = !!fn.match(/\[.*=.*\]/); - return isB64 || isExt || isDum; - } + match: fn => !!getExt(fn) } as ImageProcessor; \ No newline at end of file diff --git a/src/thirdeye.ts b/src/thirdeye.ts index 88d8316..d7d58de 100644 --- a/src/thirdeye.ts +++ b/src/thirdeye.ts @@ -71,7 +71,6 @@ let unlockQueue = Promise.resolve(); const queryCache: ApiResult = {}; const processQueries = async () => { - console.log("======== FIRIN ======="); let unlock!: () => void; unlockQueue = new Promise(_ => unlock = _); const md5 = reqQueue.map(e => e[0]).filter(e => !(e in queryCache)); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..602b521 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,5 @@ +import type { Buffer } from "buffer"; + +export const decodeCoom3Payload = (buff: Buffer) => { + // +}; \ No newline at end of file diff --git a/src/webm.ts b/src/webm.ts index 182e27d..cd69f9f 100644 --- a/src/webm.ts +++ b/src/webm.ts @@ -120,7 +120,7 @@ const extract = (webm: Buffer) => { return; const chk = chunks[embed + 1]; if (chk.type == "b" && chk.name == "TagBinary") - return { filename: 'string', data: chk.data }; + return [{ filename: 'string', data: chk.data }]; }; const inject = async (container: File, inj: File): Promise =>