#
# Copyright 2024 WebAssembly Community Group participants
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Wasm extractor for testcases generated by the ClusterFuzz run.py script.

This is general enough to also handle Fuzzilli output.

Usage:

extract_wasms.py INFILE.js OUTFILE

That will find embedded wasm files in INFILE.js, of the form

  new Uint8Array([..wasm_contents..]);

and extract them into OUTFILE.0.wasm, OUTFILE.1.wasm, etc. It also emits
OUTFILE.js which will no longer contain the embedded contents, after which the
script can be run as

  d8 OUTFILE.js -- OUTFILE.0.wasm

That is, the embedded file can now be provided as a filename argument.
"""

import re
import sys

file_counter = 0


def get_wasm_filename():
    global file_counter
    file_counter += 1
    return f'{out}.{file_counter - 1}.wasm'


in_js = sys.argv[1]
out = sys.argv[2]

with open(in_js) as f:
    js = f.read()


def repl(match):
    text = match.group(0)

    # We found something of the form
    #
    #   new Uint8Array([..binary data as numbers..]);
    #
    # See if the numbers are the beginnings of a wasm file, "\0asm". If so, we
    # assume it is wasm. (We are careful here because Fuzzilli output can
    # contain normal JavaScript Typed Arrays, which we do not want to touch.)
    numbers = match.groups()[0]
    numbers = numbers.split(',')

    try:
        # Handle both base 10 and 16 by passing in base 0.
        parsed = [int(n, 0) for n in numbers]
        binary = bytes(parsed)
    except ValueError:
        # Not wasm; return the existing text.
        return text

    if binary[:4] != b'\0asm':
        return text

    # It is wasm. Parse out the numbers into a binary wasm file.
    with open(get_wasm_filename(), 'wb') as f:
        f.write(binary)

    # Replace the Uint8Array with undefined + a comment.
    return 'undefined /* extracted wasm */'


# Replace the wasm files and write them out. We investigate any new Uint8Array
# on an array of values like [100, 200] or [0x61, 0x6D, 0x6a] etc.
js = re.sub(r'new Uint8Array\(\[([\d,x a-fA-F]+)\]\)', repl, js)

# Write out the new JS.
with open(f'{out}.js', 'w') as f:
    f.write(js)
