Browse Source

Tentative webm embedding support

pull/7/head
coomdev 2 years ago
parent
commit
53306abe05
  1. 1
      .eslintignore
  2. 25
      .eslintrc.js
  3. 5611
      main.user.js
  4. 2132
      package-lock.json
  5. 3
      package.json
  6. 248
      src/main.ts
  7. 105
      src/png.ts
  8. 103
      src/webm.ts

1
.eslintignore

@ -0,0 +1 @@
*.js

25
.eslintrc.js

@ -0,0 +1,25 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
semi: ['error', 'always'],
'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }],
'no-unused-vars': "off",
"@typescript-eslint/no-unused-vars": ["off"],
'lines-between-class-members': ["error", "always"],
'no-debugger': ["off"],
'no-undef': ["off"],
'@typescript-eslint/explicit-function-return-type': ["off"],
'@typescript-eslint/no-non-null-assertion': ['off'],
'@typescript-eslint/no-explicit-any': ['off'],
'@typescript-eslint/camelcase': ['off'],
'@typescript-eslint/consistent-type-assertions': ['off']
}
};

5611
main.user.js

File diff suppressed because it is too large

2132
package-lock.json

File diff suppressed because it is too large

3
package.json

@ -16,7 +16,8 @@
"crc-32": "^1.2.0",
"events": "^3.3.0",
"file-type": "^17.0.2",
"readable-stream": "^3.6.0"
"readable-stream": "^3.6.0",
"ts-ebml": "^2.0.2"
},
"devDependencies": {
"esbuild": "^0.14.7"

248
src/main.ts

@ -1,13 +1,7 @@
/* eslint-disable */
import { Buffer } from "buffer";
import { fileTypeFromBuffer } from 'file-type';
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("CUM\0" + "0");
import * as png from "./png";
import * as webm from "./webm";
type Awaited<T> = T extends PromiseLike<infer U> ? U : T
@ -16,136 +10,83 @@ const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ? GM_xmlhttpReque
function GM_fetch(...[url, opt]: Parameters<typeof fetch>) {
function blobTo(to: string, blob: Blob) {
if (to == "arrayBuffer" && blob.arrayBuffer)
return blob.arrayBuffer()
return blob.arrayBuffer();
return new Promise((resolve, reject) => {
var fileReader = new FileReader();
const fileReader = new FileReader();
fileReader.onload = function (event) {
if (!event) return;
if (to == "base64")
resolve(event.target!.result);
else
resolve(event.target!.result)
}
if (to == "arrayBuffer") fileReader.readAsArrayBuffer(blob)
else if (to == "base64") fileReader.readAsDataURL(blob) // "data:*/*;base64,......"
else if (to == "text") fileReader.readAsText(blob, "utf-8")
else reject("unknown to")
})
resolve(event.target!.result);
};
if (to == "arrayBuffer") fileReader.readAsArrayBuffer(blob);
else if (to == "base64") fileReader.readAsDataURL(blob); // "data:*/*;base64,......"
else if (to == "text") fileReader.readAsText(blob, "utf-8");
else reject("unknown to");
});
}
return new Promise<ReturnType<typeof fetch>>((resolve, reject) => {
// https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest
let gmopt: Tampermonkey.Request<any> = {
const gmopt: Tampermonkey.Request<any> = {
url: url.toString(),
data: opt?.body?.toString(),
responseType: "blob",
method: "GET",
onload: (resp) => {
let blob = resp.response as Blob;
const blob = resp.response as Blob;
const ref = resp as any as Awaited<ReturnType<typeof fetch>>;
ref.blob = () => Promise.resolve(blob)
ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise<ArrayBuffer>
ref.text = () => blobTo("text", blob) as Promise<string>
ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise<any>))
resolve(resp as any)
ref.blob = () => Promise.resolve(blob);
ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise<ArrayBuffer>;
ref.text = () => blobTo("text", blob) as Promise<string>;
ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise<any>));
resolve(resp as any);
},
ontimeout: () => reject("fetch timeout"),
onerror: () => reject("fetch error"),
onabort: () => reject("fetch abort")
}
xmlhttprequest(gmopt)
})
};
xmlhttprequest(gmopt);
});
}
let extractEmbedded = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let magic = false;
let sneed = new PNGDecoder(reader);
try {
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;
}
}
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);
} finally {
reader.releaseLock();
}
}
const processors: [RegExp,
(reader: ReadableStreamDefaultReader<Uint8Array>) => Promise<{ filename: string; data: Buffer } | undefined>,
(container: File, inj: File) => Promise<Buffer>][] = [
[/\.png$/, png.extract, png.inject],
[/\.webm$/, webm.extract, webm.inject]
];
let processImage = async (src: string) => {
if (!src.match(/\.png$/))
const processImage = async (src: string) => {
const proc = processors.find(e => src.match(e[0]));
if (!proc)
return;
let resp = await GM_fetch(src);
let reader = (await resp.blob()).stream();
const resp = await GM_fetch(src);
const reader = (await resp.blob()).stream();
if (!reader)
return;
return await extractEmbedded(reader.getReader());
return await proc[1](reader.getReader());
};
/* Used for debugging */
let processImage2 = async (src: string) => {
if (!src.match(/\.png$/))
return;
let resp = await GM_fetch(src);
let reader = resp.body!.getReader();
if (!reader)
return;
let data = Buffer.alloc(0);
let chunk;
while ((chunk = await reader.read()) && !chunk.done) {
data = concatAB(data, Buffer.from(chunk.value));
}
return {
filename: 'aaaa',
data
};
};
let processPost = async (post: HTMLDivElement) => {
const processPost = async (post: HTMLDivElement) => {
if (post.hasAttribute('data-processed'))
return;
post.setAttribute('data-processed', "true");
let thumb = post.querySelector(".fileThumb") as HTMLAnchorElement;
const thumb = post.querySelector(".fileThumb") as HTMLAnchorElement;
if (!thumb)
return;
console.log("Processing post", post)
let res = await processImage(thumb.href);
const res = await processImage(thumb.href);
if (!res)
return;
let replyBox = post.querySelector('.post');
const replyBox = post.querySelector('.post');
replyBox?.classList.toggle('hasembed');
// add buttons
let fi = post.querySelector(".file-info")!;
let cf = `
const fi = post.querySelector(".file-info")!;
const cf = `
<a class="fa fa-eye">
</a>`
let a = document.createRange().createContextualFragment(cf).children[0] as HTMLAnchorElement;
let type = await fileTypeFromBuffer(res.data);
</a>`;
const a = document.createRange().createContextualFragment(cf).children[0] as HTMLAnchorElement;
const type = await fileTypeFromBuffer(res.data);
let cont: HTMLImageElement | HTMLVideoElement;
let w: number, h: number;
if (type?.mime.startsWith("image")) {
@ -171,19 +112,19 @@ let processPost = async (post: HTMLDivElement) => {
h = cont.height;
}
let contract = () => {
}
const contract = () => {
// ugh
};
let expand = () => {
const expand = () => {
cont.style.width = `${w}px`;
cont.style.height = `${h}px`;
cont.style.maxWidth = "unset";
cont.style.maxHeight = "unset";
}
};
let imgcont = document.createElement('div');
let p = thumb.parentElement!;
const imgcont = document.createElement('div');
const p = thumb.parentElement!;
p.removeChild(thumb);
imgcont.appendChild(thumb);
p.appendChild(imgcont);
@ -198,61 +139,20 @@ let processPost = async (post: HTMLDivElement) => {
contracted = !contracted;
(contracted) ? contract() : expand();
e.stopPropagation();
}
};
let visible = false;
a.onclick = () => {
visible = !visible;
if (visible) {
imgcont.appendChild(cont)
imgcont.appendChild(cont);
} else {
imgcont.removeChild(cont);
}
a.classList.toggle("disabled");
}
};
fi.children[1].insertAdjacentElement('afterend', a);
}
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 [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;
}
await encoder.insertchunk([name, chunk, crc, offset]);
}
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 () => {
await Promise.all([...document.querySelectorAll('.postContainer')].map(e => processPost(e as any)));
@ -260,51 +160,57 @@ const startup = async () => {
//await Promise.all([...document.querySelectorAll('.postContainer')].filter(e => e.textContent?.includes("191 KB")).map(e => processPost(e as any)));
document.addEventListener('PostsInserted', <any>(async (e: CustomEvent<string>) => {
let threadelement = e.target as HTMLDivElement
let posts = [...threadelement.querySelectorAll("postContainer")].filter(e => e.hasAttribute('data-processed'));
const threadelement = e.target as HTMLDivElement;
const posts = [...threadelement.querySelectorAll("postContainer")].filter(e => e.hasAttribute('data-processed'));
posts.map(e => processPost(e as any));
}));
let getSelectedFile = () => {
const getSelectedFile = () => {
return new Promise<File>(res => {
document.addEventListener('QRFile', e => res((e as any).detail), { once: true });
document.dispatchEvent(new CustomEvent('QRGetFile'));
})
}
});
};
let injected = false;
document.addEventListener('QRDialogCreation', <any>((e: CustomEvent<string>) => {
if (injected)
return;
injected = true;
let target = e.target as HTMLDivElement;
let bts = target.querySelector('#qr-filename-container')
let i = document.createElement('i');
const target = e.target as HTMLDivElement;
const bts = target.querySelector('#qr-filename-container');
const i = document.createElement('i');
i.className = "fa fa-magnet";
let a = document.createElement('a')
const a = document.createElement('a');
a.appendChild(i);
a.title = "Embed File (Select a file before...)";
bts?.appendChild(a);
a.onclick = async (e) => {
let file = await getSelectedFile();
const file = await getSelectedFile();
if (!file)
return;
let input = document.createElement('input') as HTMLInputElement;
const input = document.createElement('input') as HTMLInputElement;
input.setAttribute("type", "file");
input.onchange = (async ev => {
if (input.files)
document.dispatchEvent(new CustomEvent('QRSetFile', { detail: await buildInjection(file, input.files[0]) }))
})
if (input.files) {
const proc = processors.find(e => file.name.match(e[0]));
if (!proc)
return;
document.dispatchEvent(new CustomEvent('QRSetFile', {
detail: await proc[2](file, input.files[0])
}));
}
});
input.click();
}
};
}));
};
document.addEventListener('4chanXInitFinished', startup);
let customStyles = document.createElement('style');
const customStyles = document.createElement('style');
customStyles.appendChild(document.createTextNode(
`
`
.extractedImg {
width:auto;
height:auto;
@ -318,7 +224,7 @@ customStyles.appendChild(document.createTextNode(
}
`
));
document.documentElement.insertBefore(customStyles, null);
document.documentElement.insertBefore(customStyles, null);
// onload = () => {
// let container = document.getElementById("container") as HTMLInputElement;

105
src/png.ts

@ -1,9 +1,8 @@
/* 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);
export const concatAB = (...bufs: Buffer[]) => {
const sz = bufs.map(e => e.byteLength).reduce((a, b) => a + b);
const ret = Buffer.alloc(sz);
let ptr = 0;
for (const b of bufs) {
@ -11,13 +10,15 @@ export let concatAB = (...bufs: Buffer[]) => {
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>) {
@ -26,7 +27,7 @@ export class PNGDecoder {
async catchup() {
while (this.repr.byteLength < this.req) {
let chunk = await this.reader.read();
const chunk = await this.reader.read();
if (chunk.done)
throw new Error("Unexpected EOF");
this.repr = concatAB(this.repr, Buffer.from(chunk.value));
@ -37,8 +38,8 @@ export class PNGDecoder {
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();
const length = this.repr.readUInt32BE(this.ptr);
const name = this.repr.slice(this.ptr + 4, this.ptr + 8).toString();
this.ptr += 4;
this.req += length + 4; // crc
await this.catchup();
@ -50,6 +51,7 @@ export class PNGDecoder {
}
async dtor() {
//ugh
}
}
@ -62,7 +64,7 @@ export class PNGEncoder {
}
async insertchunk(chunk: PNGChunk) {
let b = Buffer.alloc(4);
const 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
@ -74,4 +76,89 @@ export class PNGEncoder {
this.writer.releaseLock();
await this.writer.close();
}
}
}
const CUM0 = Buffer.from("CUM\0" + "0");
export const extract = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let magic = false;
const sneed = new PNGDecoder(reader);
try {
let lastIDAT: Buffer | null = null;
for await (const [name, chunk, crc, offset] of sneed.chunks()) {
switch (name) {
// should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
case 'tEXt':
if (chunk.slice(4, 4 + CUM0.length).equals(CUM0))
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);
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.substr(0, 4), 0);
data.copy(ret, 4);
return ret;
};
const BufferWriteStream = () => {
let b = Buffer.from([]);
const ret = new WritableStream<Buffer>({
write(chunk) {
b = concatAB(b, chunk);
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
};
export 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();
};

103
src/webm.ts

@ -0,0 +1,103 @@
import { Buffer } from "buffer";
import * as ebml from "ts-ebml";
import { concatAB } from "./png";
// unused, but will in case 4chan does file sig checks
//const password = Buffer.from("NOA");
const xor = (a: Buffer, p: Buffer) => {
let n = 0;
for (let i = 0; i < a.byteLength; ++i) {
a[i] ^= p[n];
n++;
n %= p.byteLength;
}
};
// just some debugging
const printChunks = (chunks: ebml.EBMLElementDetail[], ptr = 0, depth = 0): void => {
if (ptr >= chunks.length)
return;
const k = chunks[ptr];
const closing = ('isEnd' in k && k.isEnd ? 1 : 0);
console.log('\t'.repeat(depth - closing) + (closing ? '/' : '') + k.name);
switch (k.type) {
case "m":
if (k.isEnd) {
return printChunks(chunks, ptr + 1, depth - 1);
} else {
return printChunks(chunks, ptr + 1, depth + 1);
}
default:
return printChunks(chunks, ptr + 1, depth);
}
};
const embed = (webm: Buffer, data: Buffer) => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
const enc = new ebml.Encoder();
const embed = chunks.findIndex(e => e.name == "Targets" && e.type == "m" && e.isEnd);
if (embed == -1)
throw "Cannot embed, no tags section...";
// That's basically budget binary XML
chunks.splice(embed + 1, 0, ...[
{
type: "m",
isEnd: false,
name: 'SimpleTag',
data: Buffer.from('')
},
{
type: "8",
isEnd: false,
name: 'TagName',
data: Buffer.from('COOM')
},
{
type: "8",
isEnd: false,
name: 'TagBinary',
data
},
{
type: "m",
isEnd: true,
name: 'SimpleTag',
data: Buffer.from('')
}
] as any);
return Buffer.from(enc.encode(chunks.filter(e => e.name != "unknown")));
};
const extractBuff = (webm: Buffer) => {
const dec = new ebml.Decoder();
const chunks = dec.decode(webm);
const embed = chunks.findIndex(e => e.name == "TagName" && e.type == '8' && e.value == "COOM");
const cl = chunks.find(e => e.name == "Cluster");
if (cl && embed == -1)
return;
if (embed == -1)
return;
const chk = chunks[embed + 1];
if (chk.type == "b" && chk.name == "TagBinary")
return chk.data;
};
export const extract = async (reader: ReadableStreamDefaultReader<Uint8Array>): Promise<{ filename: string; data: Buffer } | undefined> => {
let total = Buffer.from('');
let chunk: ReadableStreamDefaultReadResult<Uint8Array>;
do {
chunk = await reader.read();
if (chunk.value)
total = concatAB(total, Buffer.from(chunk.value));
} while (!chunk.done);
const data = extractBuff(total);
if (!data)
return;
return { filename: 'embedded', data };
};
export const inject = async (container: File, inj: File): Promise<Buffer> =>
embed(Buffer.from(await container.arrayBuffer()), Buffer.from(await inj.arrayBuffer()));
Loading…
Cancel
Save