Greasy Fork is available in English.

wasm_patcher

WebAssemblyのbufferにパッチを当てる

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greasyfork.org/scripts/436749/1013324/wasm_patcher.js

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Wasm Patcher
// @version      0.1
// @description  WebAssemblyのbufferにパッチを当てる
// @author       nekocell
// @namespace    https://greasyfork.org/ja/users/762895-nekocell
// ==/UserScript==

class BufferReader {
    constructor(buffer) {
        this._buffer = buffer.slice(0);

        this._pos = 0;
    }

    get finished() {
        return this._pos >= this._buffer.length;
    }

    get length() {
        return this._buffer.length;
    }

    reset() {
        this._pos = 0;
    }

    readU8() {
        return this._buffer[this._pos++];
    }

    readU32() {
        return this.readU8() |
            (this.readU8() << 8) |
            (this.readU8() << 16) |
            (this.readU8() << 24);
    }

    readVarU32() {
        let result = 0;
        let shift = 0;

        let currentByte;

        do {
            currentByte = this.readU8();
            result |= (currentByte & 0x7F) << shift;
            shift += 7;
        } while (currentByte & 0x80);

        return result;
    }

    readBytes(count) {
        const bytes = []

        for (let i = 0; i < count; i++) {
            bytes.push(this.readU8());
        }

        return bytes;
    }

    readBytesToEnd() {
        const bytes = []

        while (!this.finished) {
            bytes.push(this.readU8());
        }

        return bytes;
    }
}


class BufferBuilder {
    constructor() {
        this._buffer = new Uint8Array(100);
        this._pos = 0;
    }

    get length() {
        return this._pos;
    }

    _resizeBuffer() {
        if (this._pos >= this._buffer.length - 1) {
            let tmp = this._buffer;
            this._buffer = new Uint8Array(this._buffer.length * 2);
            this._buffer.set(tmp);
        }
    }

    pushU8(val) {
        this._resizeBuffer();

        this._buffer[this._pos++] = val;
    }

    pushU32(val) {
        this.pushU8(val & 0x000000ff);
        this.pushU8((val & 0x0000ff00) >> 8);
        this.pushU8((val & 0x00ff0000) >> 16);
        this.pushU8((val & 0xff000000) >> 24);
    }

    pushVarU32(val) {
        let byte = 0;
        let value = val;

        if (val == 0) {
            this.pushU8(0);
            return;
        }

        while (value > 0) {
            byte = value & 0x7F;
            value >>= 7;

            if (value !== 0) {
                byte |= 0x80;
            }

            this.pushU8(byte);
        }
    }

    pushBytes(bytes) {
        for (let i = 0; i < bytes.length; i++) {
            this.pushU8(bytes[i]);
        }
    }

    build() {
        return this._buffer.slice(0, this._pos);
    }
}

const Section = Object.freeze({
    Custom: 0x00,
    Type: 0x01,
    Import: 0x02,
    Function: 0x03,
    Table: 0x04,
    Memory: 0x05,
    Global: 0x06,
    Export: 0x07,
    Start: 0x08,
    Element: 0x09,
    Code: 0x0A,
    Data: 0x0B,
    DataCount: 0x0C,
});

const ValueType = Object.freeze({
    i32: 0x7F,
    i64: 0x7E,
    f32: 0x7D,
    f64: 0x7C
});

const SignatureType = Object.freeze({
    func: 0x60
});

const ExternalKind = Object.freeze({
    Function: 0x00,
    Table: 0x01,
    Memory: 0x02,
    Global: 0x03
});

const OP = Object.freeze({
    unreachable: 0x00,
    nop: 0x01,
    block: 0x02,
    loop: 0x02,
    if: [0x04, 0x40],
    else: 0x05,
    end: 0x0B,
    return: 0x0F,
    drop: 0x1A,
    local: {
        get: 0x20,
        set: 0x21,
    },
    global: {
        get: 0x23,
        set: 0x24,
    },
    i32: {
        load: 0x28,
        store: 0x36,
        const: 0x41,
        eq: 0x46,
        add: 0x6A,
        sub: 0x6B,
        mul: 0x6C,
    },
    i64: {
        load: 0x29,
        store: 0x37,
        const: 0x41,
    },
    f32: {
        load: 0x2A,
        store: 0x38,
        const: 0x43,
        mul: 0x94,
    },
    f64: {
        load: 0x2B,
        store: 0x39,
        const: 0x44,
    },
});

const VAR = Object.freeze({
    u32: (val) => {
        const result = [];
        let value = val;

        if (val === 0) {
            return [0];
        }

        while (value > 0) {
            const byte = value & 0x7F;
            value >>= 7;

            if (value !== 0) {
                byte |= 0x80;
            }

            result.push(byte);
        }

        return result;
    },
    s32: (value) => {
        const result = [];

        while (true) {
            const byte = value & 0x7f;
            value >>= 7;

            if ((value === 0 && (byte & 0x40) === 0) ||
                (value === -1 && (byte & 0x40) !== 0)
            ) {
                result.push(byte);

                return result;
            }

            result.push(byte | 0x80);
        }
    },
    f32: (value) => {
        const f32ary = new Float32Array([value]);
        const f32bytes = new Uint8Array(f32ary.buffer);
        return [...f32bytes];
    },
    f64: (value) => {
        const f64ary = new Float64Array([value]);
        const f64bytes = new Uint8Array(f64ary.buffer);
        return [...f64bytes];
    }
});

class WasmIndex {
    constructor(index) {
        this._index = index;
    }
}

class WasmPatcher {
    constructor(wasmBuffer) {
        this._oldWasm = new BufferReader(new Uint8Array(wasmBuffer));
        this._newWasm = new BufferBuilder();

        this._importFunctionCount = 0;
        this._importGlobalCount = 0;

        this._aobPatchEntries = [];
        this._aobPatchFinished = false;

        this._addFunctionEntries = [];

        this._addGlobalVariableEntries = [];
    }

    _string2bytes(string) {
        let bytes = [];

        for (let i = 0; i < string.length; i++) {
            let code = string.charCodeAt(i);
            bytes.push(code);
        }

        return bytes;
    }

    _string2type(string) {
        switch (string) {
            case 's32':
            case 'u32':
            case 'i32': return ValueType.i32;
            case 's64':
            case 'u64':
            case 'i64': return ValueType.i64;
            case 'f32': return ValueType.f32;
            case 'f64': return ValueType.f64;
            default: throw new Error("Invalid string");
        }
    }

    _createInstantiationTimeInitializer(typeString, value) {
        switch (typeString) {
            case 'u32': return [OP.i32.const, ...VAR.u32(value), OP.end];
            case 's32': return [OP.i32.const, ...VAR.s32(value), OP.end];
            //case 'i64': return ValueType.i64;
            case 'f32': return [OP.f32.const, ...VAR.f32(value), OP.end];
            case 'f64': return [OP.f64.const, ...VAR.f64(value), OP.end];
            default: throw new Error("Invalid string");
        }
    }

    _parseTypeSection() {
        const sectionLen = this._oldWasm.readVarU32();
        const sectionBody = this._oldWasm.readBytes(sectionLen);

        const oldSection = new BufferReader(sectionBody);
        const newSection = new BufferBuilder();

        //TODO 既存のTypeIndexと紐付け なかったらついか
        // がいいけどめんどいから 追加だけ

        const addFunctionEntries = this._addFunctionEntries;

        const oldTypeCount = oldSection.readVarU32();
        const newTypeCount = oldTypeCount + addFunctionEntries.length;

        newSection.pushVarU32(newTypeCount);

        for (let i = 0; i < oldTypeCount; ++i) {
            const form = oldSection.readU8();
            const paramCount = oldSection.readVarU32();
            let params = [];

            for (let j = 0; j < paramCount; ++j) {
                const param = oldSection.readU8();
                params.push(param);
            }

            const returnCount = oldSection.readU8();
            let returnType;

            newSection.pushU8(form);
            newSection.pushVarU32(paramCount);
            newSection.pushBytes(params);
            newSection.pushU8(returnCount);

            if (returnCount === 1) {
                returnType = oldSection.readU8();
                newSection.pushU8(returnType);
            }
        }

        for (let i = 0; i < addFunctionEntries.length; ++i) {
            const entry = addFunctionEntries[i];

            const form = SignatureType.func;
            const params = entry.params;

            newSection.pushU8(form);
            newSection.pushVarU32(params.length);
            newSection.pushBytes(params);

            let returnType = entry.return;

            if (returnType) {
                newSection.pushU8(1);
                newSection.pushU8(returnType);
            }
            else {
                newSection.pushU8(0);
            }

            entry._typeIndex = i + oldTypeCount;
        }

        this._newWasm.pushVarU32(newSection.length);
        this._newWasm.pushBytes(newSection.build());
    }

    _parseImportSection() {
        const sectionLen = this._oldWasm.readVarU32();
        const sectionBody = this._oldWasm.readBytes(sectionLen);

        const oldSection = new BufferReader(sectionBody);
        const newSection = new BufferBuilder();

        const addFunctionEntries = this._addFunctionEntries;

        const importCount = oldSection.readVarU32();

        newSection.pushVarU32(importCount);

        for (let i = 0; i < importCount; ++i) {
            const moduleNameLen = oldSection.readVarU32();
            const moduleName = oldSection.readBytes(moduleNameLen);
            const exportNameLen = oldSection.readVarU32();
            const exportName = oldSection.readBytes(exportNameLen);

            newSection.pushVarU32(moduleNameLen);
            newSection.pushBytes(moduleName);
            newSection.pushVarU32(exportNameLen);
            newSection.pushBytes(exportName);

            const kind = oldSection.readU8();

            newSection.pushU8(kind);

            switch (kind) {
                case ExternalKind.Function:
                    this._importFunctionCount++;

                    const typeIndex = oldSection.readVarU32();
                    newSection.pushVarU32(typeIndex);
                    break;
                case ExternalKind.Table:
                    const elementType = oldSection.readU8();
                    const resizableFlags = oldSection.readU8();
                    const resizableMinimum = oldSection.readVarU32();

                    newSection.pushU8(elementType);
                    newSection.pushU8(resizableFlags);
                    newSection.pushVarU32(resizableMinimum);

                    if (resizableFlags) {
                        const resizableMaximum = oldSection.readVarU32();
                        newSection.pushVarU32(resizableMaximum);
                    }
                    break;
                case ExternalKind.Memory:
                    const limitsFlags = oldSection.readU8();
                    const limitsMinimum = oldSection.readVarU32();

                    newSection.pushU8(limitsFlags);
                    newSection.pushVarU32(limitsMinimum);

                    if (limitsFlags) {
                        const limitsMaximum = oldSection.readVarU32();
                        newSection.pushVarU32(limitsMinimum);
                    }
                    break;
                case ExternalKind.Global:
                    this._importGlobalCount++;

                    const variableType = oldSection.readU8();
                    const variableMutability = oldSection.readU8();

                    newSection.pushU8(variableType);
                    newSection.pushU8(variableMutability);
                    break;
                default:
                    throw new Error("Invalid Import kind");
            }
        }

        this._newWasm.pushVarU32(newSection.length);
        this._newWasm.pushBytes(newSection.build());
    }

    _readInstantiationTimeInitializer(reader, builder) {
        let byte;
        while ((byte = reader.readU8()) !== OP.end) {
            builder.pushU8(byte);

            switch (byte) {
                case OP.i32.const:
                case OP.i64.const: {
                    const value = reader.readVarU32();
                    builder.pushVarU32(value);
                    break;
                }
                case OP.f32.const: {
                    const valueBytes = reader.readBytes(4);
                    builder.pushVarU32(valueBytes);
                    break;
                }
                case OP.f64.const: {
                    const valueBytes = reader.readBytes(8);
                    builder.pushVarU32(valueBytes);
                    break;
                }
                case OP.global.get: {
                    const index = reader.readVarU32();
                    builder.pushVarU32(index);
                    break;
                }
                default:
                    throw new Error("Invalid byte");
            }
        }

        builder.pushU8(OP.end);
    }

    _parseGlobalSection() {
        const sectionLen = this._oldWasm.readVarU32();
        const sectionBody = this._oldWasm.readBytes(sectionLen);

        const oldSection = new BufferReader(sectionBody);
        const newSection = new BufferBuilder();

        const addGlobalVariableEntries = this._addGlobalVariableEntries;

        const oldGlobalCount = oldSection.readVarU32();
        const newGlobalCount = oldGlobalCount + addGlobalVariableEntries.length;

        newSection.pushVarU32(newGlobalCount);

        for (let i = 0; i < oldGlobalCount; ++i) {
            const contentType = oldSection.readU8();
            const mutability = oldSection.readU8();

            newSection.pushU8(contentType);
            newSection.pushU8(mutability);

            this._readInstantiationTimeInitializer(oldSection, newSection);
        }

        const newGlobalBaseIndex = this._importGlobalCount + oldGlobalCount;

        for (let i = 0; i < addGlobalVariableEntries.length; ++i) {
            const entry = addGlobalVariableEntries[i];

            const contentType = entry.type;
            const mutability = entry.mutability;
            const initializer = entry.initializer;

            newSection.pushU8(contentType);
            newSection.pushU8(mutability);
            newSection.pushBytes(initializer);

            entry.globalIndex._index = newGlobalBaseIndex + i;
        }

        this._newWasm.pushVarU32(newSection.length);
        this._newWasm.pushBytes(newSection.build());
    }

    _parseFunctionSection() {
        const sectionLen = this._oldWasm.readVarU32();
        const sectionBody = this._oldWasm.readBytes(sectionLen);

        const oldSection = new BufferReader(sectionBody);
        const newSection = new BufferBuilder();

        const addFunctionEntries = this._addFunctionEntries;

        const oldFuncCount = oldSection.readVarU32();
        const newFuncCount = oldFuncCount + addFunctionEntries.length;

        newSection.pushVarU32(newFuncCount);

        for (let i = 0; i < oldFuncCount; ++i) {
            const typeIndex = oldSection.readVarU32();
            newSection.pushVarU32(typeIndex);
        }

        const newFuncBaseIndex = this._importFunctionCount + oldFuncCount;

        for (let i = 0; i < addFunctionEntries.length; ++i) {
            const entry = addFunctionEntries[i];

            const typeIndex = entry._typeIndex;
            newSection.pushVarU32(typeIndex);

            entry.functionIndex._index = newFuncBaseIndex + i;
        }

        this._newWasm.pushVarU32(newSection.length);
        this._newWasm.pushBytes(newSection.build());
    }

    _parseExportSection() {
        const sectionLen = this._oldWasm.readVarU32();
        const sectionBody = this._oldWasm.readBytes(sectionLen);

        const oldSection = new BufferReader(sectionBody);
        const newSection = new BufferBuilder();

        const addFunctionEntries = this._addFunctionEntries;
        const addFunctionExportEntries = addFunctionEntries.filter(entry => {
            return entry?.exportName instanceof Array
        });

        const addGlobalVariableEntries = this._addGlobalVariableEntries;
        const addGlobalVariableExportEntries = addGlobalVariableEntries.filter(entry => {
            return entry?.exportName instanceof Array
        })

        const oldExportCount = oldSection.readVarU32();
        const newExportCount = oldExportCount +
            addFunctionExportEntries.length +
            addGlobalVariableExportEntries.length;

        newSection.pushVarU32(newExportCount);

        for (let i = 0; i < oldExportCount; ++i) {
            const fieldNameLen = oldSection.readVarU32();
            const fieldName = oldSection.readBytes(fieldNameLen);

            const kind = oldSection.readU8();
            const index = oldSection.readVarU32();

            newSection.pushVarU32(fieldNameLen);
            newSection.pushBytes(fieldName);
            newSection.pushU8(kind);
            newSection.pushVarU32(index);
        }

        for (let i = 0; i < addFunctionExportEntries.length; ++i) {
            const entry = addFunctionExportEntries[i];
            const fieldNameLen = entry.exportName.length;
            const fieldName = entry.exportName;

            const kind = ExternalKind.Function;
            const index = entry.functionIndex._index;

            newSection.pushVarU32(fieldNameLen);
            newSection.pushBytes(fieldName);
            newSection.pushU8(kind);
            newSection.pushVarU32(index);
        }

        for (let i = 0; i < addGlobalVariableExportEntries.length; ++i) {
            const entry = addGlobalVariableExportEntries[i];
            const fieldNameLen = entry.exportName.length;
            const fieldName = entry.exportName;

            const kind = ExternalKind.Global;
            const index = entry.globalIndex._index;

            newSection.pushVarU32(fieldNameLen);
            newSection.pushBytes(fieldName);
            newSection.pushU8(kind);
            newSection.pushVarU32(index);
        }

        this._newWasm.pushVarU32(newSection.length);
        this._newWasm.pushBytes(newSection.build());
    }

    _expandCodes(codes) {
        return codes.map(code => {
            let newCode = [];

            code.forEach(c => {
                if (c instanceof WasmIndex) {
                    newCode.push(...VAR.u32(c._index));
                } else {
                    newCode.push(c);
                }
            });

            return newCode;
        });
    }

    _expandCode(code) {
        let newCode = [];

        code.forEach(part => {
            console.log(part)
            if (part instanceof WasmIndex) {
                newCode.push(...VAR.u32(part._index));
            }
            else {
                newCode.push(part);
            }
        })

        return newCode;
    }

    _aobScan(data, scan) {
        let scanIndex = 0;

        for (let i = 0; i < data.length;) {
            const val = data[i];

            const scanNow = scan[scanIndex];
            const scanMode = scanNow.mode;
            let scanVal;

            switch (scanMode) {
                case 'insert':
                case 'replace_start':
                case 'replace_end':
                    scanIndex++;
                    break;
                case 'skip':
                    scanIndex++;
                    i++;
                    break;
                case 'value':
                    scanVal = scanNow.value;

                    if (val === scanVal) {
                        scanIndex++;
                    }
                    else {
                        scanIndex = 0;
                    }
                    i++;
                    break;
            }

            if (scanIndex === scan.length) {
                return i - 1;
            }
        }

        return -1;
    }

    _applyAobPatch(oldBody) {
        let body = oldBody;
        let newBody = null;
        let alldone = true;

        this._aobPatchEntries.forEach(entry => {
            if (entry.done) return;

            alldone = false;

            const scan = entry.scan;
            const matchIndex = this._aobScan(body, scan);

            if (matchIndex === -1) return;

            const oldBodyReader = new BufferReader(body);
            const newBodyBuilder = new BufferBuilder();

            const totalMatches = entry.totalMatches;
            const matchEndIndex = matchIndex;
            const matchFirstIndex = matchEndIndex - totalMatches + 1;
            const beforeMatchBytes = oldBodyReader.readBytes(matchFirstIndex);
            const matchBytes = oldBodyReader.readBytes(matchEndIndex - matchFirstIndex + 1);
            const afterMatchBytes = oldBodyReader.readBytesToEnd();
            const matchBytesReader = new BufferReader(matchBytes);

            const codes = entry.codes;

            let codesIndex = 0;
            let startReplace = false;

            newBodyBuilder.pushBytes(beforeMatchBytes);

            scan.forEach(now => {
                switch (now.mode) {
                    case 'skip':
                    case 'value': {
                        const val = matchBytesReader.readU8();

                        if (!startReplace) {
                            newBodyBuilder.pushU8(val);
                        }
                    } break;
                    case 'insert':
                        newBodyBuilder.pushBytes(codes[codesIndex++]);
                        break;
                    case 'replace_start':
                        newBodyBuilder.pushBytes(codes[codesIndex++]);

                        startReplace = true;
                        break;
                    case 'replace_end':
                        startReplace = false;
                        break;
                }
            });

            newBodyBuilder.pushBytes(afterMatchBytes);

            body = newBodyBuilder.build();
            newBody = newBodyBuilder.build();

            entry.onsuccess();
            entry.done = true;
        });

        this._aobPatchFinished = alldone;

        return newBody;
    }

    _parseCodeSection() {
        const sectionLen = this._oldWasm.readVarU32();
        const sectionBody = this._oldWasm.readBytes(sectionLen);

        const oldSection = new BufferReader(sectionBody);
        const newSection = new BufferBuilder();

        const addFunctionEntries = this._addFunctionEntries;

        const oldCodeCount = oldSection.readVarU32();
        const newCodeCount = oldCodeCount + addFunctionEntries.length;

        newSection.pushVarU32(newCodeCount);

        this._aobPatchEntries.forEach(entry => {
            entry.codes = this._expandCodes(entry.codes);
        });

        for (let i = 0; i < oldCodeCount; ++i) {
            const oldFuncLen = oldSection.readVarU32();
            const oldFunc = oldSection.readBytes(oldFuncLen);

            const oldFuncData = new BufferReader(oldFunc);

            const headerBuilder = new BufferBuilder();
            const localCount = oldFuncData.readVarU32();

            headerBuilder.pushVarU32(localCount);

            for (let i = 0; i < localCount; ++i) {
                const count = oldFuncData.readVarU32();
                const varsType = oldFuncData.readU8();

                headerBuilder.pushVarU32(count);
                headerBuilder.pushVarU32(varsType);
            }

            const header = headerBuilder.build();
            const oldBody = oldFuncData.readBytesToEnd();

            if (this._aobPatchFinished) {
                newSection.pushVarU32(oldFuncLen);
                newSection.pushBytes(oldFunc);
                continue;
            }

            const newBody = this._applyAobPatch(oldBody);

            if (!newBody) {
                newSection.pushVarU32(oldFuncLen);
                newSection.pushBytes(oldFunc);
            }
            else {
                const newFuncData = new BufferBuilder();
                newFuncData.pushBytes(header);
                newFuncData.pushBytes(newBody);

                newSection.pushVarU32(newFuncData.length);
                newSection.pushBytes(newFuncData.build());
            }
        }

        for (let i = 0; i < addFunctionEntries.length; ++i) {
            const entry = addFunctionEntries[i];

            const headerBuilder = new BufferBuilder();
            const localCount = entry.locals.length;

            headerBuilder.pushVarU32(localCount);

            for (let i = 0; i < localCount; ++i) {
                const local = entry.locals[i];

                headerBuilder.pushU32(1);
                headerBuilder.pushU8(local);
            }

            const bodyBuilder = new BufferBuilder();
            const code = this._expandCode(entry.code);
            bodyBuilder.pushBytes(code);

            const header = headerBuilder.build();
            const body = bodyBuilder.build();

            const funcData = new BufferBuilder();
            funcData.pushBytes(header);
            funcData.pushBytes(body);

            newSection.pushVarU32(funcData.length);
            newSection.pushBytes(funcData.build());
        }

        this._newWasm.pushVarU32(newSection.length);
        this._newWasm.pushBytes(newSection.build());
    }

    _readSections() {
        while (!this._oldWasm.finished) {
            const sectionID = this._oldWasm.readU8();

            this._newWasm.pushU8(sectionID);

            switch (sectionID) {
                case Section.Type:
                    this._parseTypeSection();
                    break;
                case Section.Import:
                    this._parseImportSection();
                    break;
                case Section.Function:
                    this._parseFunctionSection();
                    break;
                case Section.Global:
                    this._parseGlobalSection();
                    break;
                case Section.Export:
                    this._parseExportSection();
                    break;
                case Section.Code:
                    this._parseCodeSection();
                    break;
                default:
                    if (sectionID >= Section.Custom && sectionID <= Section.DataCount) {
                        const sectionLen = this._oldWasm.readVarU32();
                        const sectionBody = this._oldWasm.readBytes(sectionLen);

                        this._newWasm.pushVarU32(sectionLen);
                        this._newWasm.pushBytes(sectionBody);
                    }
                    else {
                        throw new Error("Invalid section");
                    }
                    break;
            }
        }
    }

    patch() {
        const magic = this._oldWasm.readU32();
        const version = this._oldWasm.readU32();

        if (magic !== 0x6D736100) {
            throw new Error("Invalid magic");
        }

        this._newWasm.pushU32(magic);
        this._newWasm.pushU32(version);

        this._readSections();

        //Download(this._newWasm.build(), "a.wasm");

        return this._newWasm.build();
    }

    _parseScanStr(scanStr) {
        const scanAry = scanStr.split(' ');
        let previousBracket = '';
        let previousScan = '';

        let totalMatches = 0;
        let parsedScan = [];

        const throwErr = function () {
            throw new Error('Invalid entry(aobPatchEntry).scan');
        };

        scanAry.forEach(scan => {
            switch (scan) {
                case '?':
                    if (previousBracket === '[') {
                        throwErr();
                    }

                    parsedScan.push({
                        mode: 'skip'
                    });

                    totalMatches++;
                    break;
                case '[':
                    if (previousBracket === '[') {
                        throwErr();
                    }

                    parsedScan.push({
                        mode: 'replace_start'
                    });

                    previousBracket = '[';
                    break;
                case ']':
                    if (previousBracket === ']' || previousScan === '[') {
                        throwErr();
                    }

                    parsedScan.push({
                        mode: 'replace_end'
                    });

                    previousBracket = ']';
                    break;
                case '|':
                    if (previousBracket === '[' || previousScan === '|') {
                        throwErr();
                    }

                    parsedScan.push({
                        mode: 'insert'
                    });
                    break;
                default: {
                    let parsedVal = parseInt(scan, 16);

                    if (isNaN(parsedVal)) {
                        throwErr();
                    }

                    parsedScan.push({
                        mode: 'value',
                        value: parsedVal
                    });

                    totalMatches++;
                } break;
            }

            previousScan = scan;
        });

        return {
            scan: parsedScan,
            totalMatches: totalMatches
        };
    }

    aobPatchEntry(entry) {
        const scanStr = entry.scan;
        const parsed = this._parseScanStr(scanStr);

        const needCodeCount = parsed.scan.filter(scanUnit => {
            switch (scanUnit.mode) {
                case 'insert':
                case 'replace_start':
                    return true;
                default:
                    return false;
            }
        }).length;

        const entryCode = entry.code;
        const entryCodes = entry.codes;
        let codes;

        if ((entryCode && entryCodes) ||
            (!entryCode && !entryCodes)) {
            throw new Error("Invalid entry.code entry.codes parameter");
        }

        if (needCodeCount === 0) {
            throw new Error("Invalid entry.code entry.codes parameter");
        }
        else if (needCodeCount === 1) {
            if (!entryCode) throw new Error("Invalid entry.code");

            codes = [];
            codes.push(entryCode);
        }
        else {
            if (!entryCodes) throw new Error("Invalid entry.codes");

            codes = entryCodes;
        }

        codes = codes.map(code => {
            let newCode = [];

            code.forEach(c => {
                if (c instanceof Array) {
                    newCode.push(...c);
                } else {
                    newCode.push(c);
                }
            });

            return newCode;
        });

        this._aobPatchEntries.push({
            name: entry.name,
            scan: parsed.scan,
            totalMatches: parsed.totalMatches,
            codes: codes,
            onsuccess: entry.onsuccess,
            done: false,
        });
    }

    addFunctionEntry(entry) {
        if (!entry.params ||
            !entry.return ||
            !entry.code ||
            !(entry.params instanceof Array) ||
            !(entry.code instanceof Array)
        ) {
            throw new Error("Invalid entry");
        }

        if (!entry.locals) {
            entry.locals = [];
        }

        const locals = entry.locals;
        const fixedLocals = locals.map(local => {
            switch (typeof local) {
                case "number": return local;
                case "string": return this._string2type(local);
                default: throw new Error("Invalid locals");
            }
        });

        entry.locals = fixedLocals;

        const params = entry.params;
        const fixedParams = params.map(params => {
            switch (typeof params) {
                case "number": return params;
                case "string": return this._string2type(params);
                default: throw new Error("Invalid locals");
            }
        });

        entry.params = fixedParams;
        entry.return = this._string2type(entry.return);

        if (entry.exportName) {
            entry.exportName = this._string2bytes(entry.exportName);
        }

        let newCode = [];

        entry.code.forEach(part => {
            console.log(part)
            if (part instanceof Array) {
                newCode.push(...part);
            }
            else {
                newCode.push(part);
            }
        })

        entry.code = newCode;

        const index = new WasmIndex();
        entry.functionIndex = index;

        this._addFunctionEntries.push(entry);

        return index;
    }

    addGlobalVariableEntry(entry) {
        /*
        if (!entry.type ||
            !entry.value ||
            !entry.mutability
        ) {
            throw new Error("Invalid entry");
        }
        */

        entry.mutability = new Number(entry.mutability); // boolean to number
        entry.initializer = this._createInstantiationTimeInitializer(entry.type, entry.value);

        entry.type = this._string2type(entry.type);

        const index = new WasmIndex();
        entry.globalIndex = index;

        if (entry.exportName) {
            entry.exportName = this._string2bytes(entry.exportName);
        }

        this._addGlobalVariableEntries.push(entry);

        return index;
    }
}

// EXAMPLE CODE //

/*
const wasm = WebAssembly;

wasm.instantiateStreaming = async function (source, importObject) {
    let bufferSource = null;

    if (source instanceof Promise) {
        const response = await source;
        bufferSource = await response.arrayBuffer();
    }
    else if (source instanceof Response) {
        bufferSource = await source.arrayBuffer();
    }

    const patcher = new WasmPatcher(bufferSource);

    patcher.aobPatchEntry({
        scan: '28 2 C8 F 21 13 41 1 21 14 20 13 20 14 | 6A 21 15',
        code: [
            OP.drop,
            OP.i32.const, VAR.s32(-3)
        ],
        onsuccess: () => console.log("decrease value")
    });

    patcher.addFunction({
        params: ['i32', 'i32'],
        locals: ['f32'],
        code: [],
        return: 'i32',
        export: {
            name: 'magic'
        }
    });

    patcher.addGlobalVariable

    const newBufferSource = patcher.patch();

    return wasm.instantiate(newBufferSource, importObject);
};
*/