From 348d3aae3149c7d59c08d1b04faced58de7e9bee Mon Sep 17 00:00:00 2001 From: coomdev Date: Tue, 4 Jan 2022 16:36:43 +0100 Subject: [PATCH] streaming performance --- .eslintrc.js => .eslintrc.cjs | 0 README.md | 4 +- main.d.ts | 2 - main.meta.js | 2 +- main.user.js | 735 ++++++++++++++++++++-------------- src/App.svelte | 12 +- src/SettingsButton.svelte | 25 ++ src/gif.ts | 67 ++-- src/main.ts | 160 +++----- src/png.ts | 94 +++-- src/requests.ts | 69 ++++ src/webm.ts | 39 +- 12 files changed, 724 insertions(+), 485 deletions(-) rename .eslintrc.js => .eslintrc.cjs (100%) create mode 100644 src/SettingsButton.svelte create mode 100644 src/requests.ts diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/README.md b/README.md index 471c081..f7a6534 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ PNG Extra Embedder (PEE) ======================== -Can embed any file in a PNG/WebM and upload it through 4chanX. +Can embed any file in a PNG/WebM/GIF and upload it through 4chanX. Requires 4chanX and violentmonkey. How to Build @@ -53,4 +53,4 @@ Bugs ==== - fails to find files in new posts after a thread update -- more to come \ No newline at end of file +- more to come diff --git a/main.d.ts b/main.d.ts index 55925b9..d15de7d 100644 --- a/main.d.ts +++ b/main.d.ts @@ -1,3 +1 @@ /* eslint-disable */ - -declare const GM_fetch = fetch; \ No newline at end of file diff --git a/main.meta.js b/main.meta.js index 8306718..940a020 100644 --- a/main.meta.js +++ b/main.meta.js @@ -1,7 +1,7 @@ // ==UserScript== // @name PNGExtraEmbed // @namespace https://coom.tech/ -// @version 0.54 +// @version 0.55 // @description uhh // @author You // @match https://boards.4channel.org/* diff --git a/main.user.js b/main.user.js index 22c02f2..9c7fad4 100644 --- a/main.user.js +++ b/main.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name PNGExtraEmbed // @namespace https://coom.tech/ -// @version 0.54 +// @version 0.55 // @description uhh // @author You // @match https://boards.4channel.org/* @@ -2296,8 +2296,8 @@ } return obj; } - function _classCallCheck(instance2, Constructor) { - if (!(instance2 instanceof Constructor)) { + function _classCallCheck(instance3, Constructor) { + if (!(instance3 instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } @@ -12300,7 +12300,10 @@ var supportedExtensions = new Set(extensions); var supportedMimeTypes = new Set(mimeTypes); - // src/App.svelte + // src/stores.ts + init_esbuild_inject(); + + // node_modules/svelte/store/index.mjs init_esbuild_inject(); // node_modules/svelte/internal/index.mjs @@ -12401,6 +12404,14 @@ function set_current_component(component) { current_component = component; } + function get_current_component() { + if (!current_component) + throw new Error("Function called outside component initialization"); + return current_component; + } + function onDestroy(fn) { + get_current_component().$$.on_destroy.push(fn); + } var dirty_components = []; var binding_callbacks = []; var render_callbacks = []; @@ -12499,7 +12510,7 @@ } component.$$.dirty[i / 31 | 0] |= 1 << i % 31; } - function init(component, options, instance2, create_fragment2, not_equal, props, append_styles2, dirty = [-1]) { + function init(component, options, instance3, create_fragment3, not_equal, props, append_styles2, dirty = [-1]) { const parent_component = current_component; set_current_component(component); const $$ = component.$$ = { @@ -12522,7 +12533,7 @@ }; append_styles2 && append_styles2($$.root); let ready = false; - $$.ctx = instance2 ? instance2(component, options.props || {}, (i, ret, ...rest) => { + $$.ctx = instance3 ? instance3(component, options.props || {}, (i, ret, ...rest) => { const value = rest.length ? rest[0] : ret; if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if (!$$.skip_bound && $$.bound[i]) @@ -12535,7 +12546,7 @@ $$.update(); ready = true; run_all($$.before_update); - $$.fragment = create_fragment2 ? create_fragment2($$.ctx) : false; + $$.fragment = create_fragment3 ? create_fragment3($$.ctx) : false; if (options.target) { if (options.hydrate) { start_hydrating(); @@ -12618,11 +12629,7 @@ } }; - // src/stores.ts - init_esbuild_inject(); - // node_modules/svelte/store/index.mjs - init_esbuild_inject(); var subscriber_queue = []; function writable(value, start = noop) { let stop; @@ -12681,210 +12688,10 @@ localSet("settings", newVal); }); - // src/App.svelte - function add_css(target) { - append_styles(target, "svelte-6ot9e6", ".enabled.svelte-6ot9e6{display:block}.disabled.svelte-6ot9e6{display:none}.glow.svelte-6ot9e6{text-shadow:0 0 4px red}.clickable.svelte-6ot9e6{cursor:pointer}.content.svelte-6ot9e6{display:flex;flex-direction:column}hr.svelte-6ot9e6{width:100%}h1.svelte-6ot9e6{text-align:center}.backpanel.svelte-6ot9e6{position:absolute;right:32px;padding:10px;width:10%;top:32px;border:1px solid;border-radius:5px;background-color:rgba(0, 0, 0, 0.2)}.clickable.svelte-6ot9e6:hover{text-shadow:0 0 2px palevioletred}"); - } - function create_fragment(ctx) { - let span; - let t1; - let div1; - let div0; - let h1; - let t3; - let hr; - let t4; - let label0; - let input0; - let t5; - let t6; - let label1; - let input1; - let t7; - let t8; - let label2; - let input2; - let t9; - let t10; - let label3; - let input3; - let t11; - let mounted; - let dispose; - return { - c() { - span = element("span"); - span.textContent = "[PEE Settings]"; - t1 = space(); - div1 = element("div"); - div0 = element("div"); - h1 = element("h1"); - h1.textContent = "PEE Settings"; - t3 = space(); - hr = element("hr"); - t4 = space(); - label0 = element("label"); - input0 = element("input"); - t5 = text("\n Autoplay Videos"); - t6 = space(); - label1 = element("label"); - input1 = element("input"); - t7 = text("\n Autoplay Audio"); - t8 = space(); - label2 = element("label"); - input2 = element("input"); - t9 = text("\n Autoexpand Images on opening."); - t10 = space(); - label3 = element("label"); - input3 = element("input"); - t11 = text("\n Autoexpand Videos on opening."); - attr(span, "class", "clickable svelte-6ot9e6"); - toggle_class(span, "glow", ctx[0]); - attr(h1, "class", "svelte-6ot9e6"); - attr(hr, "class", "svelte-6ot9e6"); - attr(input0, "type", "checkbox"); - attr(input1, "type", "checkbox"); - attr(input2, "type", "checkbox"); - attr(input3, "type", "checkbox"); - attr(div0, "class", "content svelte-6ot9e6"); - attr(div1, "class", "backpanel svelte-6ot9e6"); - toggle_class(div1, "enabled", ctx[0]); - toggle_class(div1, "disabled", !ctx[0]); - }, - m(target, anchor) { - insert(target, span, anchor); - insert(target, t1, anchor); - insert(target, div1, anchor); - append(div1, div0); - append(div0, h1); - append(div0, t3); - append(div0, hr); - append(div0, t4); - append(div0, label0); - append(label0, input0); - input0.checked = ctx[1].apv; - append(label0, t5); - append(div0, t6); - append(div0, label1); - append(label1, input1); - input1.checked = ctx[1].apa; - append(label1, t7); - append(div0, t8); - append(div0, label2); - append(label2, input2); - input2.checked = ctx[1].xpi; - append(label2, t9); - append(div0, t10); - append(div0, label3); - append(label3, input3); - input3.checked = ctx[1].xpv; - append(label3, t11); - if (!mounted) { - dispose = [ - listen(span, "click", ctx[3]), - listen(input0, "change", ctx[4]), - listen(input1, "change", ctx[5]), - listen(input2, "change", ctx[6]), - listen(input3, "change", ctx[7]) - ]; - mounted = true; - } - }, - p(ctx2, [dirty]) { - if (dirty & 1) { - toggle_class(span, "glow", ctx2[0]); - } - if (dirty & 2) { - input0.checked = ctx2[1].apv; - } - if (dirty & 2) { - input1.checked = ctx2[1].apa; - } - if (dirty & 2) { - input2.checked = ctx2[1].xpi; - } - if (dirty & 2) { - input3.checked = ctx2[1].xpv; - } - if (dirty & 1) { - toggle_class(div1, "enabled", ctx2[0]); - } - if (dirty & 1) { - toggle_class(div1, "disabled", !ctx2[0]); - } - }, - i: noop, - o: noop, - d(detaching) { - if (detaching) - detach(span); - if (detaching) - detach(t1); - if (detaching) - detach(div1); - mounted = false; - run_all(dispose); - } - }; - } - function instance($$self, $$props, $$invalidate) { - let $settings; - component_subscribe($$self, settings, ($$value) => $$invalidate(1, $settings = $$value)); - let visible = false; - function opensettings() { - $$invalidate(0, visible = !visible); - } - console.log($settings); - const click_handler = () => opensettings(); - function input0_change_handler() { - $settings.apv = this.checked; - settings.set($settings); - } - function input1_change_handler() { - $settings.apa = this.checked; - settings.set($settings); - } - function input2_change_handler() { - $settings.xpi = this.checked; - settings.set($settings); - } - function input3_change_handler() { - $settings.xpv = this.checked; - settings.set($settings); - } - return [ - visible, - $settings, - opensettings, - click_handler, - input0_change_handler, - input1_change_handler, - input2_change_handler, - input3_change_handler - ]; - } - var App = class extends SvelteComponent { - constructor(options) { - super(); - init(this, options, instance, create_fragment, safe_not_equal, {}, add_css); - } - }; - var App_default = App; - // src/png.ts init_esbuild_inject(); var import_crc_32 = __toESM(require_crc32(), 1); var import_buffer = __toESM(require_buffer(), 1); - var concatAB = (...bufs) => { - const sz = bufs.map((e) => e.byteLength).reduce((a, b) => a + b); - const ret = import_buffer.Buffer.alloc(sz); - let ptr = 0; - for (const b of bufs) { - b.copy(ret, ptr); - ptr += b.byteLength; - } - return ret; - }; var PNGDecoder = class { constructor(reader) { this.reader = reader; @@ -12895,9 +12702,10 @@ async catchup() { while (this.repr.byteLength < this.req) { const chunk = await this.reader.read(); - if (chunk.done) - throw new Error("Unexpected EOF"); - this.repr = concatAB(this.repr, import_buffer.Buffer.from(chunk.value)); + if (chunk.done) { + throw new Error(`Unexpected EOF, got ${this.repr.byteLength}, required ${this.req}, ${chunk.value}`); + } + this.repr = import_buffer.Buffer.concat([this.repr, chunk.value]); } } async *chunks() { @@ -12908,8 +12716,14 @@ const name = this.repr.slice(this.ptr + 4, this.ptr + 8).toString(); this.ptr += 4; this.req += length + 4; - await this.catchup(); - yield [name, this.repr.slice(this.ptr, this.ptr + length + 4), this.repr.readUInt32BE(this.ptr + length + 4), this.ptr]; + const pos = this.ptr; + yield [name, async () => { + await this.catchup(); + return this.repr.slice(pos, pos + length + 4); + }, async () => { + await this.catchup(); + return this.repr.readUInt32BE(this.ptr + length + 4); + }, this.ptr]; this.ptr += length + 8; if (name == "IEND") break; @@ -12927,8 +12741,9 @@ const b = import_buffer.Buffer.alloc(4); b.writeInt32BE(chunk[1].length - 4, 0); await this.writer.write(b); - await this.writer.write(chunk[1]); - b.writeInt32BE((0, import_crc_32.buf)(chunk[1]), 0); + const buff = await chunk[1](); + await this.writer.write(buff); + b.writeInt32BE((0, import_crc_32.buf)(buff), 0); await this.writer.write(b); } async dtor() { @@ -12937,20 +12752,32 @@ } }; var CUM0 = import_buffer.Buffer.from("CUM\x000"); - var extract = async (reader) => { + var BufferReadStream = (b) => { + const ret = new ReadableStream({ + pull(cont) { + cont.enqueue(b); + cont.close(); + } + }); + return ret; + }; + var extract = async (png) => { let magic2 = false; + const reader = BufferReadStream(png).getReader(); const sneed = new PNGDecoder(reader); try { let lastIDAT = null; for await (const [name, chunk, crc, offset] of sneed.chunks()) { + let buff; switch (name) { case "tEXt": - if (chunk.slice(4, 4 + CUM0.length).equals(CUM0)) + buff = await chunk(); + if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) magic2 = true; break; case "IDAT": if (magic2) { - lastIDAT = chunk; + lastIDAT = await chunk(); break; } case "IEND": @@ -12983,7 +12810,7 @@ let b = import_buffer.Buffer.from([]); const ret = new WritableStream({ write(chunk) { - b = concatAB(b, chunk); + b = import_buffer.Buffer.concat([b, chunk]); } }); return [ret, () => b]; @@ -12997,7 +12824,7 @@ if (magic2 && name != "IDAT") break; if (!magic2 && name == "IDAT") { - await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]); + await encoder.insertchunk(["tEXt", () => buildChunk("tEXt", CUM0), () => 0, 0]); magic2 = true; } await encoder.insertchunk([name, chunk, crc, offset]); @@ -13006,10 +12833,36 @@ injb.writeInt32LE(inj.name.length, 0); injb.write(inj.name, 4); import_buffer.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", import_buffer.Buffer.from([])), 0, 0]); + await encoder.insertchunk(["IDAT", () => buildChunk("IDAT", injb), () => 0, 0]); + await encoder.insertchunk(["IEND", () => buildChunk("IEND", import_buffer.Buffer.from([])), () => 0, 0]); return extract4(); }; + var has_embed = async (png) => { + const reader = BufferReadStream(png).getReader(); + const sneed = new PNGDecoder(reader); + try { + for await (const [name, chunk, crc, offset] of sneed.chunks()) { + let buff; + switch (name) { + case "tEXt": + buff = await chunk(); + if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) { + return true; + } + break; + case "IDAT": + case "IEND": + return false; + default: + break; + } + } + } catch (e) { + return; + } finally { + reader.releaseLock(); + } + }; // src/webm.ts init_esbuild_inject(); @@ -13085,7 +12938,7 @@ ]); return import_buffer2.Buffer.from(enc.encode(chunks.filter((e) => e.name != "unknown"))); }; - var extractBuff = (webm) => { + var extract2 = (webm) => { const dec = new ebml.Decoder(); const chunks = dec.decode(webm); const embed2 = chunks.findIndex((e) => e.name == "TagName" && e.type == "8" && e.value == "COOM"); @@ -13096,31 +12949,29 @@ return; const chk = chunks[embed2 + 1]; if (chk.type == "b" && chk.name == "TagBinary") - return chk.data; + return { filename: "string", data: chk.data }; }; - var extract2 = async (reader) => { - let total = import_buffer2.Buffer.from(""); - let chunk; - do { - chunk = await reader.read(); - if (chunk.value) - total = concatAB(total, import_buffer2.Buffer.from(chunk.value)); - } while (!chunk.done); - const data = extractBuff(total); - if (!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) => { + const dec = new ebml.Decoder(); + const chunks = dec.decode(webm); + const embed2 = chunks.findIndex((e) => e.name == "TagName" && e.type == "8" && e.value == "COOM"); + const cl = chunks.find((e) => e.name == "Cluster"); + if (cl && embed2 == -1) + return false; + if (embed2 == -1) return; - return { filename: "embedded", data }; + return true; }; - var inject2 = async (container, inj) => embed(import_buffer2.Buffer.from(await container.arrayBuffer()), import_buffer2.Buffer.from(await inj.arrayBuffer())); // src/gif.ts init_esbuild_inject(); var import_buffer3 = __toESM(require_buffer(), 1); - var netscape = import_buffer3.Buffer.from("!\xFF\vNETSCAPE2.0\0\0\0"); + var netscape = import_buffer3.Buffer.from("!\xFF\vNETSCAPE2.0", "ascii"); var magic = import_buffer3.Buffer.from("!\xFF\vCOOMTECH0.1", "ascii"); - var extractBuff2 = (gif) => { - let field = gif.readUInt8(10); - let gcte = !!(field & 1 << 7); + var extractBuff = (gif) => { + const field = gif.readUInt8(10); + const gcte = !!(field & 1 << 7); let end = 13; if (gcte) { end += 3 * (1 << (field & 7) + 1); @@ -13128,8 +12979,8 @@ while (gif.readUInt8(end) == "!".charCodeAt(0)) { if (magic.compare(gif, end, end + magic.byteLength) != 0) { end += 3 + gif.readUInt8(end + 2); - while (1) { - let v = gif.readUInt8(end++); + while (true) { + const v = gif.readUInt8(end++); if (!v) break; end += v; @@ -13142,7 +12993,7 @@ t += v; count += v + 1; } - let buff = import_buffer3.Buffer.alloc(t); + const buff = import_buffer3.Buffer.alloc(t); count = end + magic.byteLength; t = 0; while ((v = gif.readUInt8(count)) != 0) { @@ -13150,23 +13001,11 @@ t += v; count += v + 1; } - return buff; + return { filename: "embedded", data: buff }; } } }; - var extract3 = async (reader) => { - let total = import_buffer3.Buffer.from(""); - let chunk; - do { - chunk = await reader.read(); - if (chunk.value) - total = concatAB(total, import_buffer3.Buffer.from(chunk.value)); - } while (!chunk.done); - const data = extractBuff2(total); - if (!data) - return; - return { filename: "embedded", data }; - }; + var extract3 = extractBuff; var write_embedding = async (writer, inj) => { await writer.write(magic); const byte = import_buffer3.Buffer.from([0]); @@ -13187,9 +13026,9 @@ var inject3 = async (container, inj) => { const [writestream, extract4] = BufferWriteStream(); const writer = writestream.getWriter(); - let contbuff = import_buffer3.Buffer.from(await container.arrayBuffer()); - let field = contbuff.readUInt8(10); - let gcte = !!(field & 1 << 7); + const contbuff = import_buffer3.Buffer.from(await container.arrayBuffer()); + const field = contbuff.readUInt8(10); + const gcte = !!(field & 1 << 7); let endo = 13; if (gcte) endo += 3 * (1 << (field & 7) + 1); @@ -13200,16 +13039,39 @@ await writer.write(contbuff.slice(endo)); return extract4(); }; - - // src/main.ts - var csettings; - settings.subscribe((b) => csettings = b); - var xmlhttprequest = typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest; - var headerStringToObject = (s) => Object.fromEntries(s.split("\n").map((e) => { - const [name, ...rest] = e.split(":"); - return [name.toLowerCase(), rest.join(":").trim()]; - })); - function GM_head(...[url, opt]) { + var has_embed3 = (gif) => { + const field = gif.readUInt8(10); + const gcte = !!(field & 1 << 7); + let end = 13; + if (gcte) { + end += 3 * (1 << (field & 7) + 1); + } + while (end < gif.byteLength && gif.readUInt8(end) == "!".charCodeAt(0)) { + if (magic.compare(gif, end, end + magic.byteLength) != 0) { + end += 3 + gif.readUInt8(end + 2); + while (true) { + const v = gif.readUInt8(end++); + if (!v) + break; + end += v; + } + } else { + return true; + } + } + if (end >= gif.byteLength) + return; + return false; + }; + + // src/requests.ts + init_esbuild_inject(); + var xmlhttprequest = typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest; + var headerStringToObject = (s) => Object.fromEntries(s.split("\n").map((e) => { + const [name, ...rest] = e.split(":"); + return [name.toLowerCase(), rest.join(":").trim()]; + })); + function GM_head(...[url, opt]) { return new Promise((resolve, reject) => { const gmopt = { url: url.toString(), @@ -13272,7 +13134,268 @@ xmlhttprequest(gmopt); }); } - async function* streamRemote(url, chunkSize = 128 * 1024, fetchRestOnNonCanceled = true) { + + // src/App.svelte + init_esbuild_inject(); + + // node_modules/svelte/index.mjs + init_esbuild_inject(); + + // src/App.svelte + function add_css(target) { + append_styles(target, "svelte-6ot9e6", ".enabled.svelte-6ot9e6{display:block}.disabled.svelte-6ot9e6{display:none}.glow.svelte-6ot9e6{text-shadow:0 0 4px red}.clickable.svelte-6ot9e6{cursor:pointer}.content.svelte-6ot9e6{display:flex;flex-direction:column}hr.svelte-6ot9e6{width:100%}h1.svelte-6ot9e6{text-align:center}.backpanel.svelte-6ot9e6{position:absolute;right:32px;padding:10px;width:10%;top:32px;border:1px solid;border-radius:5px;background-color:rgba(0, 0, 0, 0.2)}.clickable.svelte-6ot9e6:hover{text-shadow:0 0 2px palevioletred}"); + } + function create_fragment(ctx) { + let span; + let t1; + let div1; + let div0; + let h1; + let t3; + let hr; + let t4; + let label0; + let input0; + let t5; + let t6; + let label1; + let input1; + let t7; + let t8; + let label2; + let input2; + let t9; + let t10; + let label3; + let input3; + let t11; + let mounted; + let dispose; + return { + c() { + span = element("span"); + span.textContent = "[PEE Settings]"; + t1 = space(); + div1 = element("div"); + div0 = element("div"); + h1 = element("h1"); + h1.textContent = "PEE Settings"; + t3 = space(); + hr = element("hr"); + t4 = space(); + label0 = element("label"); + input0 = element("input"); + t5 = text("\n Autoplay Videos"); + t6 = space(); + label1 = element("label"); + input1 = element("input"); + t7 = text("\n Autoplay Audio"); + t8 = space(); + label2 = element("label"); + input2 = element("input"); + t9 = text("\n Autoexpand Images on opening."); + t10 = space(); + label3 = element("label"); + input3 = element("input"); + t11 = text("\n Autoexpand Videos on opening."); + attr(span, "class", "clickable svelte-6ot9e6"); + toggle_class(span, "glow", ctx[0]); + attr(h1, "class", "svelte-6ot9e6"); + attr(hr, "class", "svelte-6ot9e6"); + attr(input0, "type", "checkbox"); + attr(input1, "type", "checkbox"); + attr(input2, "type", "checkbox"); + attr(input3, "type", "checkbox"); + attr(div0, "class", "content svelte-6ot9e6"); + attr(div1, "class", "backpanel svelte-6ot9e6"); + toggle_class(div1, "enabled", ctx[0]); + toggle_class(div1, "disabled", !ctx[0]); + }, + m(target, anchor) { + insert(target, span, anchor); + insert(target, t1, anchor); + insert(target, div1, anchor); + append(div1, div0); + append(div0, h1); + append(div0, t3); + append(div0, hr); + append(div0, t4); + append(div0, label0); + append(label0, input0); + input0.checked = ctx[1].apv; + append(label0, t5); + append(div0, t6); + append(div0, label1); + append(label1, input1); + input1.checked = ctx[1].apa; + append(label1, t7); + append(div0, t8); + append(div0, label2); + append(label2, input2); + input2.checked = ctx[1].xpi; + append(label2, t9); + append(div0, t10); + append(div0, label3); + append(label3, input3); + input3.checked = ctx[1].xpv; + append(label3, t11); + if (!mounted) { + dispose = [ + listen(span, "click", ctx[3]), + listen(input0, "change", ctx[4]), + listen(input1, "change", ctx[5]), + listen(input2, "change", ctx[6]), + listen(input3, "change", ctx[7]) + ]; + mounted = true; + } + }, + p(ctx2, [dirty]) { + if (dirty & 1) { + toggle_class(span, "glow", ctx2[0]); + } + if (dirty & 2) { + input0.checked = ctx2[1].apv; + } + if (dirty & 2) { + input1.checked = ctx2[1].apa; + } + if (dirty & 2) { + input2.checked = ctx2[1].xpi; + } + if (dirty & 2) { + input3.checked = ctx2[1].xpv; + } + if (dirty & 1) { + toggle_class(div1, "enabled", ctx2[0]); + } + if (dirty & 1) { + toggle_class(div1, "disabled", !ctx2[0]); + } + }, + i: noop, + o: noop, + d(detaching) { + if (detaching) + detach(span); + if (detaching) + detach(t1); + if (detaching) + detach(div1); + mounted = false; + run_all(dispose); + } + }; + } + function instance($$self, $$props, $$invalidate) { + let $settings; + component_subscribe($$self, settings, ($$value) => $$invalidate(1, $settings = $$value)); + let visible = false; + function opensettings() { + $$invalidate(0, visible = !visible); + } + let penisEvent = () => { + $$invalidate(0, visible = !visible); + }; + document.addEventListener("penis", penisEvent); + onDestroy(() => { + document.removeEventListener("penis", penisEvent); + }); + const click_handler = () => opensettings(); + function input0_change_handler() { + $settings.apv = this.checked; + settings.set($settings); + } + function input1_change_handler() { + $settings.apa = this.checked; + settings.set($settings); + } + function input2_change_handler() { + $settings.xpi = this.checked; + settings.set($settings); + } + function input3_change_handler() { + $settings.xpv = this.checked; + settings.set($settings); + } + return [ + visible, + $settings, + opensettings, + click_handler, + input0_change_handler, + input1_change_handler, + input2_change_handler, + input3_change_handler + ]; + } + var App = class extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance, create_fragment, safe_not_equal, {}, add_css); + } + }; + var App_default = App; + + // src/SettingsButton.svelte + init_esbuild_inject(); + function add_css2(target) { + append_styles(target, "svelte-101vs6b", ".glow.svelte-101vs6b{text-shadow:0 0 4px red}.clickable.svelte-101vs6b{cursor:pointer}.clickable.svelte-101vs6b:hover{text-shadow:0 0 2px palevioletred}"); + } + function create_fragment2(ctx) { + let span; + let mounted; + let dispose; + return { + c() { + span = element("span"); + span.textContent = "[PEE Settings]"; + attr(span, "class", "clickable svelte-101vs6b"); + toggle_class(span, "glow", ctx[0]); + }, + m(target, anchor) { + insert(target, span, anchor); + if (!mounted) { + dispose = listen(span, "click", ctx[2]); + mounted = true; + } + }, + p(ctx2, [dirty]) { + if (dirty & 1) { + toggle_class(span, "glow", ctx2[0]); + } + }, + i: noop, + o: noop, + d(detaching) { + if (detaching) + detach(span); + mounted = false; + dispose(); + } + }; + } + function instance2($$self, $$props, $$invalidate) { + "use strict"; + let visible = false; + function opensettings() { + $$invalidate(0, visible = !visible); + } + const click_handler = () => opensettings(); + return [visible, opensettings, click_handler]; + } + var SettingsButton = class extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance2, create_fragment2, safe_not_equal, {}, add_css2); + } + }; + var SettingsButton_default = SettingsButton; + + // src/main.ts + var csettings; + settings.subscribe((b) => csettings = b); + async function* streamRemote(url, chunkSize = 16 * 1024, fetchRestOnNonCanceled = true) { const headers = await GM_head(url); const h = headerStringToObject(headers); const size = +h["content-length"]; @@ -13281,41 +13404,52 @@ while (ptr != size) { const res = await GM_fetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } }); const obj = headerStringToObject(res.responseHeaders); - if (!("content-length" in obj)) - return; + if (!("content-length" in obj)) { + console.warn("no content lenght???", url); + break; + } const len = +obj["content-length"]; ptr += len; if (fetchRestOnNonCanceled) fetchSize = size; - yield import_buffer4.Buffer.from(await res.arrayBuffer()); - } - } - function iteratorToStream(iterator) { - return new ReadableStream({ - async pull(controller) { - const { value, done } = await iterator.next(); - if (done) { - controller.close(); - } else { - controller.enqueue(value); - } + const val = import_buffer4.Buffer.from(await res.arrayBuffer()); + const e = yield val; + if (e) { + break; } - }); + } } var processors = [ - [/\.png$/, extract, inject], - [/\.webm$/, extract2, inject2], - [/\.gif$/, extract3, inject3] + [/\.png$/, has_embed, extract, inject], + [/\.webm$/, has_embed2, extract2, inject2], + [/\.gif$/, has_embed3, extract3, inject3] ]; var processImage = async (src) => { const proc = processors.find((e) => src.match(e[0])); if (!proc) return; const iter = streamRemote(src); - const reader = iteratorToStream(iter); - if (!reader) + if (!iter) return; - return await proc[1](reader.getReader()); + let cumul = import_buffer4.Buffer.alloc(0); + let found; + let chunk = { done: true }; + do { + const { value, done } = await iter.next(found === false); + if (done) { + chunk = { done: true }; + } else { + chunk = { done: false, value }; + } + if (!done) + cumul = import_buffer4.Buffer.concat([cumul, value]); + found = await proc[1](cumul); + } while (found !== false && !chunk.done); + await iter.next(false); + if (found === false) { + return; + } + return await proc[2](cumul); }; var textToElement = (s) => document.createRange().createContextualFragment(s).children[0]; var processPost = async (post) => { @@ -13448,9 +13582,9 @@ var startup = async () => { await Promise.all([...document.querySelectorAll(".postContainer")].filter((e) => e.textContent?.includes("191 KB")).map((e) => processPost(e))); document.addEventListener("ThreadUpdate", async (e) => { - let newPosts = e.detail.newPosts; + const newPosts = e.detail.newPosts; for (const post of newPosts) { - let postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)); + const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)); processPost(postContainer); } }); @@ -13473,10 +13607,13 @@ const posts = [...document.querySelectorAll(".postContainer")]; const scts = document.getElementById("shortcuts"); const button = textToElement(``); - const app = new App_default({ + const settingsButton = new SettingsButton_default({ target: button }); scts?.appendChild(button); + const appHost = textToElement(`
`); + const appInstance = new App_default({ target: appHost }); + document.body.append(appHost); await Promise.all(posts.map((e) => processPost(e))); }; var getSelectedFile = () => { @@ -13508,7 +13645,7 @@ const proc = processors.find((e3) => file.name.match(e3[0])); if (!proc) throw new Error("Container filetype not supported"); - const buff = await proc[2](file, input.files[0]); + const buff = await proc[3](file, input.files[0]); document.dispatchEvent(new CustomEvent("QRSetFile", { detail: { file: new Blob([buff], { type }), name: file.name } })); diff --git a/src/App.svelte b/src/App.svelte index 0527746..52f9df8 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,10 +1,20 @@ opensettings()}> diff --git a/src/SettingsButton.svelte b/src/SettingsButton.svelte new file mode 100644 index 0000000..132aa95 --- /dev/null +++ b/src/SettingsButton.svelte @@ -0,0 +1,25 @@ + + + opensettings()}> + [PEE Settings] + + + diff --git a/src/gif.ts b/src/gif.ts index 48f3e16..12fb0a3 100644 --- a/src/gif.ts +++ b/src/gif.ts @@ -1,12 +1,12 @@ import { Buffer } from "buffer"; -import { BufferWriteStream, concatAB } from "./png"; +import { BufferWriteStream } from "./png"; -const netscape = Buffer.from("!\xFF\x0BNETSCAPE2.0\x03\x01\x00\x00\x00"); +const netscape = Buffer.from("!\xFF\x0BNETSCAPE2.0", 'ascii'); const magic = Buffer.from("!\xFF\x0B" + "COOMTECH0.1", 'ascii'); const extractBuff = (gif: Buffer) => { - let field = gif.readUInt8(10); - let gcte = !!(field & (1 << 7)); + const field = gif.readUInt8(10); + const gcte = !!(field & (1 << 7)); let end = 13; if (gcte) { end += 3 * (1 << ((field & 7) + 1)); @@ -15,8 +15,9 @@ const extractBuff = (gif: Buffer) => { while (gif.readUInt8(end) == '!'.charCodeAt(0)) { if (magic.compare(gif, end, end + magic.byteLength) != 0) { end += 3 + gif.readUInt8(end + 2); - while (1) { // skip sub blocks - let v = gif.readUInt8(end++); + // eslint-disable-next-line no-constant-condition + while (true) { // skip sub blocks + const v = gif.readUInt8(end++); if (!v) break; end += v; @@ -29,34 +30,21 @@ const extractBuff = (gif: Buffer) => { t += v; count += v + 1; } - let buff = Buffer.alloc(t); + const buff = Buffer.alloc(t); count = end + magic.byteLength; t = 0; while ((v = gif.readUInt8(count)) != 0) { - gif.copy(buff, t, count + 1, count + 1 + v) + gif.copy(buff, t, count + 1, count + 1 + v); t += v; count += v + 1; } - return buff; + return {filename: 'embedded', data: buff}; } } // metadata ended, nothing... }; -export const extract = async (reader: ReadableStreamDefaultReader): Promise<{ filename: string; data: Buffer } | undefined> => { - let total = Buffer.from(''); - let chunk: ReadableStreamDefaultReadResult; - // todo: early reject - 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 extract = extractBuff; const write_embedding = async (writer: WritableStreamDefaultWriter, inj: Buffer) => { await writer.write(magic); @@ -80,10 +68,10 @@ export const inject = async (container: File, inj: File) => { const [writestream, extract] = BufferWriteStream(); const writer = writestream.getWriter(); - let contbuff = Buffer.from(await container.arrayBuffer()); + const contbuff = Buffer.from(await container.arrayBuffer()); - let field = contbuff.readUInt8(10); - let gcte = !!(field & (1 << 0x7)) + const field = contbuff.readUInt8(10); + const gcte = !!(field & (1 << 0x7)); let endo = 13; if (gcte) endo += 3 * (1 << ((field & 7) + 1)); @@ -94,4 +82,31 @@ export const inject = async (container: File, inj: File) => { await write_embedding(writer, Buffer.from(await inj.arrayBuffer())); await writer.write(contbuff.slice(endo)); return extract(); +}; + +export const has_embed = (gif: Buffer) => { + const field = gif.readUInt8(10); + const gcte = !!(field & (1 << 7)); + let end = 13; + if (gcte) { + end += 3 * (1 << ((field & 7) + 1)); + } + // skip beeg blocks + while (end < gif.byteLength && gif.readUInt8(end) == '!'.charCodeAt(0)) { + if (magic.compare(gif, end, end + magic.byteLength) != 0) { + end += 3 + gif.readUInt8(end + 2); + // eslint-disable-next-line no-constant-condition + while (true) { // skip sub blocks + const v = gif.readUInt8(end++); + if (!v) + break; + end += v; + } + } else { + return true; + } + } + if (end >= gif.byteLength) + return; // Don't know yet, need more to decide. + return false; // no more extension blocks, so definite no }; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 4db2c5e..b2de5f1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,89 +1,21 @@ import { Buffer } from "buffer"; import { fileTypeFromBuffer } from 'file-type'; -import App from "./App.svelte"; import { settings } from "./stores"; import * as png from "./png"; import * as webm from "./webm"; import * as gif from "./gif"; -let csettings: any; - -settings.subscribe(b => csettings = b); - -type Awaited = T extends PromiseLike ? U : T - -const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ? GM_xmlhttpRequest : (typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest); - -const headerStringToObject = (s: string) => - Object.fromEntries(s.split('\n').map(e => { - const [name, ...rest] = e.split(':'); - return [name.toLowerCase(), rest.join(':').trim()]; - })); +import { GM_fetch, GM_head, headerStringToObject } from "./requests"; -function GM_head(...[url, opt]: Parameters) { - return new Promise((resolve, reject) => { - // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest - const gmopt: Tampermonkey.Request = { - url: url.toString(), - data: opt?.body?.toString(), - method: "HEAD", - onload: (resp) => { - resolve(resp.responseHeaders); - }, - ontimeout: () => reject("fetch timeout"), - onerror: () => reject("fetch error"), - onabort: () => reject("fetch abort") - }; - xmlhttprequest(gmopt); - }); -} +import App from "./App.svelte"; +import SettingsButton from './SettingsButton.svelte'; -function GM_fetch(...[url, opt]: Parameters) { - function blobTo(to: string, blob: Blob) { - if (to == "arrayBuffer" && blob.arrayBuffer) - return blob.arrayBuffer(); - return new Promise((resolve, reject) => { - 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"); - }); - } - return new Promise>((resolve, reject) => { - // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest - const gmopt: Tampermonkey.Request = { - url: url.toString(), - data: opt?.body?.toString(), - responseType: "blob", - headers: opt?.headers as any, - method: "GET", - onload: (resp) => { - const blob = resp.response as Blob; - const ref = resp as any as Awaited>; - ref.blob = () => Promise.resolve(blob); - ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise; - ref.text = () => blobTo("text", blob) as Promise; - ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise)); - resolve(resp as any); - }, - ontimeout: () => reject("fetch timeout"), - onerror: () => reject("fetch error"), - onabort: () => reject("fetch abort") - }; - xmlhttprequest(gmopt); - }); -} +let csettings: any; +settings.subscribe(b => csettings = b); -async function* streamRemote(url: string, chunkSize = 128 * 1024, fetchRestOnNonCanceled = true) { +// most pngs are encoded with 65k idat chunks +async function* streamRemote(url: string, chunkSize = 16 * 1024, fetchRestOnNonCanceled = true) { const headers = await GM_head(url); const h = headerStringToObject(headers); const size = +h['content-length']; @@ -92,49 +24,59 @@ async function* streamRemote(url: string, chunkSize = 128 * 1024, fetchRestOnNon while (ptr != size) { const res = await GM_fetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } }) as any as Tampermonkey.Response; const obj = headerStringToObject(res.responseHeaders); - if (!('content-length' in obj)) - return; - const len = +obj['content-length']; + if (!('content-length' in obj)) { + console.warn("no content lenght???", url); + break; + } const len = +obj['content-length']; ptr += len; if (fetchRestOnNonCanceled) fetchSize = size; - yield Buffer.from(await (res as any).arrayBuffer()); + const val = Buffer.from(await (res as any).arrayBuffer()); + const e = (yield val) as boolean; + if (e) { + break; + } } + //console.log("streaming ended, ", ptr, size); } -function iteratorToStream(iterator: AsyncGenerator) { - return new ReadableStream({ - async pull(controller) { - const { value, done } = await iterator.next(); - - if (done) { - controller.close(); - } else { - controller.enqueue(value); - } - }, - }); -} - +type EmbeddedFile = { filename: string; data: Buffer }; const processors: [RegExp, - (reader: ReadableStreamDefaultReader) => Promise<{ filename: string; data: Buffer } | undefined>, + (b: Buffer) => (boolean | undefined) | Promise, + (b: Buffer) => EmbeddedFile | undefined | Promise, (container: File, inj: File) => Promise][] = [ - [/\.png$/, png.extract, png.inject], - [/\.webm$/, webm.extract, webm.inject], - [/\.gif$/, gif.extract, gif.inject], + [/\.png$/, png.has_embed, png.extract, png.inject], + [/\.webm$/, webm.has_embed, webm.extract, webm.inject], + [/\.gif$/, gif.has_embed, gif.extract, gif.inject], ]; const processImage = async (src: string) => { const proc = processors.find(e => src.match(e[0])); if (!proc) return; - // const resp = await GM_fetch(src); - // const reader = resp.body; const iter = streamRemote(src); - const reader = iteratorToStream(iter); - if (!reader) + if (!iter) + return; + let cumul = Buffer.alloc(0); + let found: boolean | undefined; + let chunk: ReadableStreamDefaultReadResult = { done: true }; + do { + const { value, done } = await iter.next(found === false); + if (done) { + chunk = { done: true } as ReadableStreamDefaultReadDoneResult; + } else { + chunk = { done: false, value } as ReadableStreamDefaultReadValueResult; + } + if (!done) + cumul = Buffer.concat([cumul, value!]); + found = await proc[1](cumul); + } while (found !== false && !chunk.done); + await iter.next(false); + if (found === false) { + //console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`); return; - return await proc[1](reader.getReader()); + } + return await proc[2](cumul); }; const textToElement = (s: string) => @@ -289,13 +231,14 @@ const startup = async () => { // Basically this is a misnommer: fires even when inlining existings posts, also posts are inlined through some kind of dom projection document.addEventListener('ThreadUpdate', (async (e: CustomEvent) => { - let newPosts = e.detail.newPosts; + const newPosts = e.detail.newPosts; for (const post of newPosts) { - let postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)) as HTMLDivElement; + const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)) as HTMLDivElement; processPost(postContainer); } })); + // keep this to handle posts getting inlined const mo = new MutationObserver(reco => { for (const rec of reco) if (rec.type == "childList") @@ -318,11 +261,15 @@ const startup = async () => { const scts = document.getElementById('shortcuts'); const button = textToElement(``); - const app = new App({ + const settingsButton = new SettingsButton({ target: button }); scts?.appendChild(button); + const appHost = textToElement(`
`); + const appInstance = new App({ target: appHost }); + document.body.append(appHost); + await Promise.all(posts.map(e => processPost(e as any))); }; @@ -332,6 +279,7 @@ const getSelectedFile = () => { document.dispatchEvent(new CustomEvent('QRGetFile')); }); }; + document.addEventListener('4chanXInitFinished', startup); document.addEventListener('QRDialogCreation', ((e: CustomEvent) => { @@ -356,7 +304,7 @@ document.addEventListener('QRDialogCreation', ((e: CustomEvent) => const proc = processors.find(e => file.name.match(e[0])); if (!proc) throw new Error("Container filetype not supported"); - const buff = await proc[2](file, input.files[0]); + const buff = await proc[3](file, input.files[0]); document.dispatchEvent(new CustomEvent('QRSetFile', { //detail: { file: new Blob([buff]), name: file.name, type: file.type } detail: { file: new Blob([buff], { type }), name: file.name } diff --git a/src/png.ts b/src/png.ts index beb94c6..68a2d1e 100644 --- a/src/png.ts +++ b/src/png.ts @@ -1,18 +1,7 @@ import { buf } from "crc-32"; import { Buffer } from "buffer"; -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) { - b.copy(ret, ptr); - ptr += b.byteLength; - } - return ret; -}; - -export type PNGChunk = [string, Buffer, number, number]; +export type PNGChunk = [string, () => Buffer | Promise, () => number | Promise, number]; export class PNGDecoder { repr: Buffer; @@ -28,9 +17,10 @@ export class PNGDecoder { async catchup() { while (this.repr.byteLength < this.req) { const chunk = await this.reader.read(); - if (chunk.done) - throw new Error("Unexpected EOF"); - this.repr = concatAB(this.repr, Buffer.from(chunk.value)); + if (chunk.done) { + throw new Error(`Unexpected EOF, got ${this.repr.byteLength}, required ${this.req}, ${chunk.value}`); + } + this.repr = Buffer.concat([this.repr, chunk.value]); } } @@ -42,8 +32,15 @@ export class PNGDecoder { const 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; + //await this.catchup(); + const pos = this.ptr; + yield [name, async () => { + await this.catchup(); + return this.repr.slice(pos, pos + length + 4); + }, async () => { + await this.catchup(); + return this.repr.readUInt32BE(this.ptr + length + 4); + }, this.ptr] as PNGChunk; this.ptr += length + 8; if (name == 'IEND') break; @@ -67,8 +64,9 @@ export class PNGEncoder { 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 - b.writeInt32BE(buf(chunk[1]), 0); + const buff = await chunk[1](); + await this.writer.write(buff); // chunk includes chunkname + b.writeInt32BE(buf(buff), 0); await this.writer.write(b); } @@ -80,22 +78,34 @@ export class PNGEncoder { const CUM0 = Buffer.from("CUM\0" + "0"); -export const extract = async (reader: ReadableStreamDefaultReader) => { - let magic = false; +export const BufferReadStream = (b: Buffer) => { + const ret = new ReadableStream({ + pull(cont) { + cont.enqueue(b); + cont.close(); + } + }); + return ret; +}; +export const extract = async (png: Buffer) => { + let magic = 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': - if (chunk.slice(4, 4 + CUM0.length).equals(CUM0)) + buff = await chunk(); + if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) magic = true; break; case 'IDAT': if (magic) { - lastIDAT = chunk; + lastIDAT = await chunk(); break; } // eslint-disable-next-line no-fallthrough @@ -133,7 +143,7 @@ export const BufferWriteStream = () => { let b = Buffer.from([]); const ret = new WritableStream({ write(chunk) { - b = concatAB(b, chunk); + b = Buffer.concat([b, chunk]); } }); return [ret, () => b] as [WritableStream, () => Buffer]; @@ -149,7 +159,7 @@ export const inject = async (container: File, inj: File) => { if (magic && name != "IDAT") break; if (!magic && name == "IDAT") { - await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]); + await encoder.insertchunk(["tEXt", () => buildChunk("tEXt", CUM0), () => 0, 0]); magic = true; } await encoder.insertchunk([name, chunk, crc, offset]); @@ -158,7 +168,37 @@ export const inject = async (container: File, inj: File) => { 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]); + await encoder.insertchunk(["IDAT", () => buildChunk("IDAT", injb), () => 0, 0]); + await encoder.insertchunk(["IEND", () => buildChunk("IEND", Buffer.from([])), () => 0, 0]); return extract(); +}; + +export 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 = await chunk(); + if (buff.slice(4, 4 + CUM0.length).equals(CUM0)) { + 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(); + } }; \ No newline at end of file diff --git a/src/requests.ts b/src/requests.ts new file mode 100644 index 0000000..f3ee604 --- /dev/null +++ b/src/requests.ts @@ -0,0 +1,69 @@ +const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ? GM_xmlhttpRequest : (typeof GM != "undefined" ? GM.xmlHttpRequest : GM_xmlhttpRequest); + +export const headerStringToObject = (s: string) => + Object.fromEntries(s.split('\n').map(e => { + const [name, ...rest] = e.split(':'); + return [name.toLowerCase(), rest.join(':').trim()]; + })); + +export function GM_head(...[url, opt]: Parameters) { + return new Promise((resolve, reject) => { + // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest + const gmopt: Tampermonkey.Request = { + url: url.toString(), + data: opt?.body?.toString(), + method: "HEAD", + onload: (resp) => { + resolve(resp.responseHeaders); + }, + ontimeout: () => reject("fetch timeout"), + onerror: () => reject("fetch error"), + onabort: () => reject("fetch abort") + }; + xmlhttprequest(gmopt); + }); +} + +export function GM_fetch(...[url, opt]: Parameters) { + function blobTo(to: string, blob: Blob) { + if (to == "arrayBuffer" && blob.arrayBuffer) + return blob.arrayBuffer(); + return new Promise((resolve, reject) => { + 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"); + }); + } + return new Promise>((resolve, reject) => { + // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest + const gmopt: Tampermonkey.Request = { + url: url.toString(), + data: opt?.body?.toString(), + responseType: "blob", + headers: opt?.headers as any, + method: "GET", + onload: (resp) => { + const blob = resp.response as Blob; + const ref = resp as any as Awaited>; + ref.blob = () => Promise.resolve(blob); + ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise; + ref.text = () => blobTo("text", blob) as Promise; + ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise)); + resolve(resp as any); + }, + ontimeout: () => reject("fetch timeout"), + onerror: () => reject("fetch error"), + onabort: () => reject("fetch abort") + }; + xmlhttprequest(gmopt); + }); +} \ No newline at end of file diff --git a/src/webm.ts b/src/webm.ts index 8cbdb6c..9b8cbcf 100644 --- a/src/webm.ts +++ b/src/webm.ts @@ -1,6 +1,5 @@ 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"); @@ -71,9 +70,9 @@ const embed = (webm: Buffer, data: Buffer) => { chunks.splice(embed + 1, 0, ...stack as any); tags = findEnclosingTag(chunks, n); } - embed = tags![1]; + embed = tags![1]; }; - + findOrInsert('Tags'); findOrInsert('Tag'); findOrInsert('Targets'); @@ -108,7 +107,7 @@ const embed = (webm: Buffer, data: Buffer) => { return Buffer.from(enc.encode(chunks.filter(e => e.name != "unknown"))); }; -const extractBuff = (webm: Buffer) => { +export const extract = (webm: Buffer) => { const dec = new ebml.Decoder(); const chunks = dec.decode(webm); @@ -120,23 +119,21 @@ const extractBuff = (webm: Buffer) => { return; const chk = chunks[embed + 1]; if (chk.type == "b" && chk.name == "TagBinary") - return chk.data; -}; - -export const extract = async (reader: ReadableStreamDefaultReader): Promise<{ filename: string; data: Buffer } | undefined> => { - let total = Buffer.from(''); - let chunk: ReadableStreamDefaultReadResult; - // todo: early reject - 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 }; + return { filename: 'string', data: chk.data }; }; export const inject = async (container: File, inj: File): Promise => - embed(Buffer.from(await container.arrayBuffer()), Buffer.from(await inj.arrayBuffer())); \ No newline at end of file + embed(Buffer.from(await container.arrayBuffer()), Buffer.from(await inj.arrayBuffer())); + +export const has_embed = (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 false; // Tags appear before Cluster, so if we have a Cluster and no coomtag, then it's a definite no + if (embed == -1) // Found no coomtag, but no cluster, so it might be further + return; + return true; +}; \ No newline at end of file