CoroCoroRipper

Download Images From CoroCoro

// ==UserScript==
// @name        CoroCoroRipper
// @namespace   adrian
// @author      adrian
// @match       https://www.corocoro.jp/*
// @version     1.0
// @description Download Images From CoroCoro
// @require     https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// @require     https://unpkg.com/@zip.js/[email protected]/dist/zip-full.min.js
// @grant       GM_registerMenuCommand
// @license     MIT
// ==/UserScript==

const SHIFT_LEFT_32 = (1 << 16) * (1 << 16);
const SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;

// Threshold chosen based on both benchmarking and knowledge about browser string
// data structures (which currently switch structure types at 12 bytes or more)
const TEXT_DECODER_MIN_LENGTH = 12;
const utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');

const PBF_VARINT = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum
const PBF_FIXED64 = 1; // 64-bit: double, fixed64, sfixed64
const PBF_BYTES = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields
const PBF_FIXED32 = 5; // 32-bit: float, fixed32, sfixed32

class Pbf {
	/**
	 * @param {Uint8Array | ArrayBuffer} [buf]
	 */
	constructor(buf = new Uint8Array(16)) {
		this.buf = ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf);
		this.dataView = new DataView(this.buf.buffer);
		this.pos = 0;
		this.type = 0;
		this.length = this.buf.length;
	}

	// === READING =================================================================

	/**
	 * @template T
	 * @param {(tag: number, result: T, pbf: Pbf) => void} readField
	 * @param {T} result
	 * @param {number} [end]
	 */
	readFields(readField, result, end = this.length) {
		while (this.pos < end) {
			const val = this.readVarint(),
				tag = val >> 3,
				startPos = this.pos;

			this.type = val & 0x7;
			readField(tag, result, this);

			if (this.pos === startPos) this.skip(val);
		}
		return result;
	}

	/**
	 * @template T
	 * @param {(tag: number, result: T, pbf: Pbf) => void} readField
	 * @param {T} result
	 */
	readMessage(readField, result) {
		return this.readFields(readField, result, this.readVarint() + this.pos);
	}

	readFixed32() {
		const val = this.dataView.getUint32(this.pos, true);
		this.pos += 4;
		return val;
	}

	readSFixed32() {
		const val = this.dataView.getInt32(this.pos, true);
		this.pos += 4;
		return val;
	}

	// 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)

	readFixed64() {
		const val = this.dataView.getUint32(this.pos, true) + this.dataView.getUint32(this.pos + 4, true) * SHIFT_LEFT_32;
		this.pos += 8;
		return val;
	}

	readSFixed64() {
		const val = this.dataView.getUint32(this.pos, true) + this.dataView.getInt32(this.pos + 4, true) * SHIFT_LEFT_32;
		this.pos += 8;
		return val;
	}

	readFloat() {
		const val = this.dataView.getFloat32(this.pos, true);
		this.pos += 4;
		return val;
	}

	readDouble() {
		const val = this.dataView.getFloat64(this.pos, true);
		this.pos += 8;
		return val;
	}

	/**
	 * @param {boolean} [isSigned]
	 */
	readVarint(isSigned) {
		const buf = this.buf;
		let val, b;

		b = buf[this.pos++]; val = b & 0x7f; if (b < 0x80) return val;
		b = buf[this.pos++]; val |= (b & 0x7f) << 7; if (b < 0x80) return val;
		b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;
		b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;
		b = buf[this.pos]; val |= (b & 0x0f) << 28;

		return readVarintRemainder(val, isSigned, this);
	}

	readVarint64() { // for compatibility with v2.0.1
		return this.readVarint(true);
	}

	readSVarint() {
		const num = this.readVarint();
		return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding
	}

	readBoolean() {
		return Boolean(this.readVarint());
	}

	readString() {
		const end = this.readVarint() + this.pos;
		const pos = this.pos;
		this.pos = end;

		if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {
			// longer strings are fast with the built-in browser TextDecoder API
			return utf8TextDecoder.decode(this.buf.subarray(pos, end));
		}
		// short strings are fast with our custom implementation
		return readUtf8(this.buf, pos, end);
	}

	readBytes() {
		const end = this.readVarint() + this.pos,
			buffer = this.buf.subarray(this.pos, end);
		this.pos = end;
		return buffer;
	}

	// verbose for performance reasons; doesn't affect gzipped size

	/**
	 * @param {number[]} [arr]
	 * @param {boolean} [isSigned]
	 */
	readPackedVarint(arr = [], isSigned) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readVarint(isSigned));
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedSVarint(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readSVarint());
		return arr;
	}
	/** @param {boolean[]} [arr] */
	readPackedBoolean(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readBoolean());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedFloat(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readFloat());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedDouble(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readDouble());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedFixed32(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readFixed32());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedSFixed32(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readSFixed32());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedFixed64(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readFixed64());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedSFixed64(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readSFixed64());
		return arr;
	}
	readPackedEnd() {
		return this.type === PBF_BYTES ? this.readVarint() + this.pos : this.pos + 1;
	}

	/** @param {number} val */
	skip(val) {
		const type = val & 0x7;
		if (type === PBF_VARINT) while (this.buf[this.pos++] > 0x7f) { }
		else if (type === PBF_BYTES) this.pos = this.readVarint() + this.pos;
		else if (type === PBF_FIXED32) this.pos += 4;
		else if (type === PBF_FIXED64) this.pos += 8;
		else throw new Error(`Unimplemented type: ${type}`);
	}

	// === WRITING =================================================================

	/**
	 * @param {number} tag
	 * @param {number} type
	 */
	writeTag(tag, type) {
		this.writeVarint((tag << 3) | type);
	}

	/** @param {number} min */
	realloc(min) {
		let length = this.length || 16;

		while (length < this.pos + min) length *= 2;

		if (length !== this.length) {
			const buf = new Uint8Array(length);
			buf.set(this.buf);
			this.buf = buf;
			this.dataView = new DataView(buf.buffer);
			this.length = length;
		}
	}

	finish() {
		this.length = this.pos;
		this.pos = 0;
		return this.buf.subarray(0, this.length);
	}

	/** @param {number} val */
	writeFixed32(val) {
		this.realloc(4);
		this.dataView.setInt32(this.pos, val, true);
		this.pos += 4;
	}

	/** @param {number} val */
	writeSFixed32(val) {
		this.realloc(4);
		this.dataView.setInt32(this.pos, val, true);
		this.pos += 4;
	}

	/** @param {number} val */
	writeFixed64(val) {
		this.realloc(8);
		this.dataView.setInt32(this.pos, val & -1, true);
		this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);
		this.pos += 8;
	}

	/** @param {number} val */
	writeSFixed64(val) {
		this.realloc(8);
		this.dataView.setInt32(this.pos, val & -1, true);
		this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);
		this.pos += 8;
	}

	/** @param {number} val */
	writeVarint(val) {
		val = +val || 0;

		if (val > 0xfffffff || val < 0) {
			writeBigVarint(val, this);
			return;
		}

		this.realloc(4);

		this.buf[this.pos++] = val & 0x7f | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
		this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
		this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
		this.buf[this.pos++] = (val >>> 7) & 0x7f;
	}

	/** @param {number} val */
	writeSVarint(val) {
		this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);
	}

	/** @param {boolean} val */
	writeBoolean(val) {
		this.writeVarint(+val);
	}

	/** @param {string} str */
	writeString(str) {
		str = String(str);
		this.realloc(str.length * 4);

		this.pos++; // reserve 1 byte for short string length

		const startPos = this.pos;
		// write the string directly to the buffer and see how much was written
		this.pos = writeUtf8(this.buf, str, this.pos);
		const len = this.pos - startPos;

		if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);

		// finally, write the message length in the reserved place and restore the position
		this.pos = startPos - 1;
		this.writeVarint(len);
		this.pos += len;
	}

	/** @param {number} val */
	writeFloat(val) {
		this.realloc(4);
		this.dataView.setFloat32(this.pos, val, true);
		this.pos += 4;
	}

	/** @param {number} val */
	writeDouble(val) {
		this.realloc(8);
		this.dataView.setFloat64(this.pos, val, true);
		this.pos += 8;
	}

	/** @param {Uint8Array} buffer */
	writeBytes(buffer) {
		const len = buffer.length;
		this.writeVarint(len);
		this.realloc(len);
		for (let i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];
	}

	/**
	 * @template T
	 * @param {(obj: T, pbf: Pbf) => void} fn
	 * @param {T} obj
	 */
	writeRawMessage(fn, obj) {
		this.pos++; // reserve 1 byte for short message length

		// write the message directly to the buffer and see how much was written
		const startPos = this.pos;
		fn(obj, this);
		const len = this.pos - startPos;

		if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);

		// finally, write the message length in the reserved place and restore the position
		this.pos = startPos - 1;
		this.writeVarint(len);
		this.pos += len;
	}

	/**
	 * @template T
	 * @param {number} tag
	 * @param {(obj: T, pbf: Pbf) => void} fn
	 * @param {T} obj
	 */
	writeMessage(tag, fn, obj) {
		this.writeTag(tag, PBF_BYTES);
		this.writeRawMessage(fn, obj);
	}

	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedVarint(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedVarint, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedSVarint(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);
	}
	/**
	 * @param {number} tag
	 * @param {boolean[]} arr
	 */
	writePackedBoolean(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedFloat(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedFloat, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedDouble(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedDouble, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedFixed32(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedSFixed32(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedFixed64(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedSFixed64(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr);
	}

	/**
	 * @param {number} tag
	 * @param {Uint8Array} buffer
	 */
	writeBytesField(tag, buffer) {
		this.writeTag(tag, PBF_BYTES);
		this.writeBytes(buffer);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeFixed32Field(tag, val) {
		this.writeTag(tag, PBF_FIXED32);
		this.writeFixed32(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeSFixed32Field(tag, val) {
		this.writeTag(tag, PBF_FIXED32);
		this.writeSFixed32(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeFixed64Field(tag, val) {
		this.writeTag(tag, PBF_FIXED64);
		this.writeFixed64(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeSFixed64Field(tag, val) {
		this.writeTag(tag, PBF_FIXED64);
		this.writeSFixed64(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeVarintField(tag, val) {
		this.writeTag(tag, PBF_VARINT);
		this.writeVarint(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeSVarintField(tag, val) {
		this.writeTag(tag, PBF_VARINT);
		this.writeSVarint(val);
	}
	/**
	 * @param {number} tag
	 * @param {string} str
	 */
	writeStringField(tag, str) {
		this.writeTag(tag, PBF_BYTES);
		this.writeString(str);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeFloatField(tag, val) {
		this.writeTag(tag, PBF_FIXED32);
		this.writeFloat(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeDoubleField(tag, val) {
		this.writeTag(tag, PBF_FIXED64);
		this.writeDouble(val);
	}
	/**
	 * @param {number} tag
	 * @param {boolean} val
	 */
	writeBooleanField(tag, val) {
		this.writeVarintField(tag, +val);
	}
};

/**
 * @param {number} l
 * @param {boolean | undefined} s
 * @param {Pbf} p
 */
function readVarintRemainder(l, s, p) {
	const buf = p.buf;
	let h, b;

	b = buf[p.pos++]; h = (b & 0x70) >> 4; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 3; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);

	throw new Error('Expected varint not more than 10 bytes');
}

/**
 * @param {number} low
 * @param {number} high
 * @param {boolean} [isSigned]
 */
function toNum(low, high, isSigned) {
	return isSigned ? high * 0x100000000 + (low >>> 0) : ((high >>> 0) * 0x100000000) + (low >>> 0);
}

/**
 * @param {number} val
 * @param {Pbf} pbf
 */
function writeBigVarint(val, pbf) {
	let low, high;

	if (val >= 0) {
		low = (val % 0x100000000) | 0;
		high = (val / 0x100000000) | 0;
	} else {
		low = ~(-val % 0x100000000);
		high = ~(-val / 0x100000000);

		if (low ^ 0xffffffff) {
			low = (low + 1) | 0;
		} else {
			low = 0;
			high = (high + 1) | 0;
		}
	}

	if (val >= 0x10000000000000000 || val < -0x10000000000000000) {
		throw new Error('Given varint doesn\'t fit into 10 bytes');
	}

	pbf.realloc(10);

	writeBigVarintLow(low, high, pbf);
	writeBigVarintHigh(high, pbf);
}

/**
 * @param {number} high
 * @param {number} low
 * @param {Pbf} pbf
 */
function writeBigVarintLow(low, high, pbf) {
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos] = low & 0x7f;
}

/**
 * @param {number} high
 * @param {Pbf} pbf
 */
function writeBigVarintHigh(high, pbf) {
	const lsb = (high & 0x07) << 4;

	pbf.buf[pbf.pos++] |= lsb | ((high >>>= 3) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f;
}

/**
 * @param {number} startPos
 * @param {number} len
 * @param {Pbf} pbf
 */
function makeRoomForExtraLength(startPos, len, pbf) {
	const extraLen =
		len <= 0x3fff ? 1 :
			len <= 0x1fffff ? 2 :
				len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));

	// if 1 byte isn't enough for encoding message length, shift the data to the right
	pbf.realloc(extraLen);
	for (let i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];
}

/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedVarint(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedSVarint(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedFloat(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedDouble(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);
}
/**
 * @param {boolean[]} arr
 * @param {Pbf} pbf
 */
function writePackedBoolean(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedFixed32(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedSFixed32(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedFixed64(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedSFixed64(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]);
}

// Buffer code below from https://github.com/feross/buffer, MIT-licensed

/**
 * @param {Uint8Array} buf
 * @param {number} pos
 * @param {number} end
 */
function readUtf8(buf, pos, end) {
	let str = '';
	let i = pos;

	while (i < end) {
		const b0 = buf[i];
		let c = null; // codepoint
		let bytesPerSequence =
			b0 > 0xEF ? 4 :
				b0 > 0xDF ? 3 :
					b0 > 0xBF ? 2 : 1;

		if (i + bytesPerSequence > end) break;

		let b1, b2, b3;

		if (bytesPerSequence === 1) {
			if (b0 < 0x80) {
				c = b0;
			}
		} else if (bytesPerSequence === 2) {
			b1 = buf[i + 1];
			if ((b1 & 0xC0) === 0x80) {
				c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);
				if (c <= 0x7F) {
					c = null;
				}
			}
		} else if (bytesPerSequence === 3) {
			b1 = buf[i + 1];
			b2 = buf[i + 2];
			if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {
				c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);
				if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {
					c = null;
				}
			}
		} else if (bytesPerSequence === 4) {
			b1 = buf[i + 1];
			b2 = buf[i + 2];
			b3 = buf[i + 3];
			if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {
				c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);
				if (c <= 0xFFFF || c >= 0x110000) {
					c = null;
				}
			}
		}

		if (c === null) {
			c = 0xFFFD;
			bytesPerSequence = 1;

		} else if (c > 0xFFFF) {
			c -= 0x10000;
			str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);
			c = 0xDC00 | c & 0x3FF;
		}

		str += String.fromCharCode(c);
		i += bytesPerSequence;
	}

	return str;
}

/**
 * @param {Uint8Array} buf
 * @param {string} str
 * @param {number} pos
 */
function writeUtf8(buf, str, pos) {
	for (let i = 0, c, lead; i < str.length; i++) {
		c = str.charCodeAt(i); // code point

		if (c > 0xD7FF && c < 0xE000) {
			if (lead) {
				if (c < 0xDC00) {
					buf[pos++] = 0xEF;
					buf[pos++] = 0xBF;
					buf[pos++] = 0xBD;
					lead = c;
					continue;
				} else {
					c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;
					lead = null;
				}
			} else {
				if (c > 0xDBFF || (i + 1 === str.length)) {
					buf[pos++] = 0xEF;
					buf[pos++] = 0xBF;
					buf[pos++] = 0xBD;
				} else {
					lead = c;
				}
				continue;
			}
		} else if (lead) {
			buf[pos++] = 0xEF;
			buf[pos++] = 0xBF;
			buf[pos++] = 0xBD;
			lead = null;
		}

		if (c < 0x80) {
			buf[pos++] = c;
		} else {
			if (c < 0x800) {
				buf[pos++] = c >> 0x6 | 0xC0;
			} else {
				if (c < 0x10000) {
					buf[pos++] = c >> 0xC | 0xE0;
				} else {
					buf[pos++] = c >> 0x12 | 0xF0;
					buf[pos++] = c >> 0xC & 0x3F | 0x80;
				}
				buf[pos++] = c >> 0x6 & 0x3F | 0x80;
			}
			buf[pos++] = c & 0x3F | 0x80;
		}
	}
	return pos;
}

const fromHexString = (hexString) =>
	Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));

async function createDecryptFunc(keyHex, ivHex) {
	const key = await window.crypto.subtle.importKey("raw", fromHexString(keyHex), "AES-CBC", true, [
		"decrypt",
	]);
	const iv = fromHexString(ivHex);
	return async (data) => {
		return await window.crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, data);
	};
}

function toPng(webp) {
	return new Promise((resolve, reject) => {
		const canvas = document.createElement("canvas");
		const context = canvas.getContext("2d");
		const image = new Image();
		image.src = URL.createObjectURL(new Blob([webp]));
		image.crossOrigin = "anonymous";
		image.onload = (e) => {
			canvas.width = image.width;
			canvas.height = image.height;
			URL.revokeObjectURL(e.target.src);
			context.drawImage(e.target, 0, 0, canvas.width, canvas.height);
			canvas.toBlob(
				(data) => {
					resolve(data);
				},
				"image/png",
				100,
			);
		};
		image.onerror = (e) => reject(e);
	});
}

function readRoot(tag, data, pbf) {
	if (tag === 2) {
		data.images = pbf.readMessage(readImageMessage, data.images);
		return;
	}
	if (tag === 19) {
		data.key = pbf.readString();
		return;
	}
	if (tag === 20) {
		data.iv = pbf.readString();
		return;
	}
}

function readImageMessage(tag, message, pbf) {
	if (tag === 1) {
		message.push(pbf.readString());
		return;
	}
}

const decodeProtobuf = (data) => new Pbf(data).readFields(readRoot, { images: [] });

const downloadImages = async () => {
	if (
		!/https:\/\/www\.corocoro\.jp\/chapter\/.*\/viewer/.test(
			window.location.href,
		)
	)
		return;
	const progressBar = document.createElement("div");
	progressBar.id = "dl-progress";
	progressBar.textContent = "Starting...";
	progressBar.style.padding = "20px";
	progressBar.style.backgroundColor = "black";
	progressBar.style.borderRadius = "10px";
	progressBar.style.border = "1px solid white";
	progressBar.style.boxShadow = "0 25px 50px -12px rgb(0 0 0 / 0.25)";
	progressBar.style.position = "fixed";
	progressBar.style.left = "50%";
	progressBar.style.top = "50%";
	progressBar.style.transform = "translate(-50%,-50%)";
	progressBar.style.zIndex = "9999";
	progressBar.style.fontSize = "20px";
	progressBar.style.color = "white";
	document.body.appendChild(progressBar);

	const currentPath = window.location.pathname;
	const pathSplit = currentPath.split("/");
	pathSplit.pop();
	const chapterId = pathSplit.pop();
	const apiData = decodeProtobuf(await fetch(
		`https://www.corocoro.jp/api/csr?rq=chapter/viewer&chapter_id=${chapterId}`,
		{
			method: "PUT",
		},
	).then((res) => res.arrayBuffer()));

	const images = apiData.images;
	console.log(images);
	progressBar.textContent = `${images.length} images found.`;
	const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), {
		bufferedWrite: true,
	});
	const decrypt = await createDecryptFunc(apiData.key, apiData.iv);
	for (let i = 0; i < images.length; i++) {
		const image = images[i];
		const response = await fetch(image);
		if (!response.ok) {
			progressBar.textContent = `failed to fetch image ${i + 1}/${images.length}`;
			throw new Error("Failed to fetch image");
		}
		const arrayBuffer = await response.arrayBuffer();
		const decryptedData = await decrypt(new Uint8Array(arrayBuffer));
		zipWriter.add(
			`${i + 1}.png`,
			new zip.BlobReader(await toPng(decryptedData)),
			{},
		);
		progressBar.textContent = `fetched and decrypted image ${i + 1}/${images.length}`;
		console.log("done with ", i + 1);
	}
	console.log("image fetching done. generating zip");
	progressBar.textContent = "image fetching done. generating zip";
	const blobURL = URL.createObjectURL(await zipWriter.close());
	const link = document.createElement("a");
	link.href = blobURL;
	link.download = `${document.title}.zip`;
	link.click();
	progressBar.textContent = "done.";
	setTimeout(() => progressBar.remove(), 1000);
};

const updateButton = () => {
	console.log("loading");
	let dlButton = document.body.querySelector("#dl-button");
	if (!dlButton) {
		dlButton = document.createElement("button");
		dlButton.id = "dl-button";
		dlButton.textContent = "Download";
		dlButton.style.padding = "5px 12px";
		dlButton.style.backgroundColor = "#ef0029";
		dlButton.style.borderRadius = "8px";
		dlButton.style.border = "3px solid #000";
		dlButton.style.boxShadow = "0 4px 0 #000";
		dlButton.style.position = "absolute";
		dlButton.style.right = "5px";
		dlButton.style.bottom = "5px";
		dlButton.style.zIndex = "9999";
		dlButton.style.fontSize = ".75rem";
		dlButton.style.fontWeight = "800";
		dlButton.style.color = "white";
		dlButton.addEventListener("click", () => downloadImages());
		document.body.appendChild(dlButton);
	}
	dlButton.style.display = /https:\/\/www\.corocoro\.jp\/chapter\/.*\/viewer/.test(
		window.location.href,
	) ? "block" : "none";
};

updateButton();

(() => {
	let oldPushState = history.pushState;
	history.pushState = function pushState() {
		let ret = oldPushState.apply(this, arguments);
		window.dispatchEvent(new Event('pushstate'));
		window.dispatchEvent(new Event('locationchange'));
		return ret;
	};

	let oldReplaceState = history.replaceState;
	history.replaceState = function replaceState() {
		let ret = oldReplaceState.apply(this, arguments);
		window.dispatchEvent(new Event('replacestate'));
		window.dispatchEvent(new Event('locationchange'));
		return ret;
	};

	window.addEventListener('popstate', () => {
		window.dispatchEvent(new Event('locationchange'));
	});
})();

window.addEventListener('locationchange', function () {
	updateButton();
});

VM.shortcut.register("cm-s", () => downloadImages());
VM.shortcut.enable();

GM_registerMenuCommand("Download Images (Ctrl/Cmd + S)", () =>
	downloadImages(),
);