/* Backport - Bring New Fixes to old Androids
 * Copyright (C) 2013  Jay Freeman (saurik)
*/

/* GNU Lesser General Public License, Version 3 {{{ */
/*
 * Substrate is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Substrate is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Substrate.  If not, see <http://www.gnu.org/licenses/>.
**/
/* }}} */

package com.saurik.backport;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;

import java.util.LinkedHashMap;

import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import android.util.Log;

import com.saurik.substrate.MS;

public class Hook {
    private static class WrongException
        extends RuntimeException
    {
        public WrongException(Throwable cause) {
            super(cause);
        }
    }

    private static Field scanField(Class clazz, String... names) {
        for (String name : names) try {
            return clazz.getDeclaredField(name);
        } catch (NoSuchFieldException e) {}
        return null;
    }

    private static Method findMethod(Class<?> clazz, String name, Class... args) {
        try {
            return clazz.getDeclaredMethod(name, args);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static Constructor findConstructor(Class<?> clazz, Class... args) {
        try {
            return clazz.getDeclaredConstructor(args);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    private static void fixZipEntry$init$() {
        final Constructor ZipEntry$init$ = findConstructor(ZipEntry.class, byte[].class, InputStream.class);
        if (ZipEntry$init$ == null)
            return;

        MS.hookMethod(ZipEntry.class, ZipEntry$init$,
            new MS.MethodAlteration<ZipEntry, Void>() {
                public Void invoked(ZipEntry thiz, Object... args)
                    throws Throwable
                {
                    byte[] header = (byte[]) args[0];

                    invoke(thiz, args);

                    if (thiz.getName().indexOf(0) != -1)
                        throw new ZipException("bug #10148349 [" + thiz.getName() + "]");

                    DataInputStream in = new DataInputStream(new ByteArrayInputStream(header));

                    in.skip(8);
                    for (int i = 0; i != 2; ++i)
                        if ((in.readShort() & 0x0080) != 0)
                            throw new ZipException("bug #9695860 [" + thiz.getName() + "]");

                    in.skip(16);
                    for (int i = 0; i != 3; ++i)
                        if ((in.readShort() & 0x0080) != 0)
                            throw new ZipException("bug #9695860 [" + thiz.getName() + "]");

                    return null;
                }
            }
        );
    }

    private static void fixZipFile$getInputStream() {
        final Field ZipEntry$compressedSize = scanField(ZipEntry.class, "compressedSize");
        if (ZipEntry$compressedSize == null)
            return;
        ZipEntry$compressedSize.setAccessible(true);

        final Field ZipEntry$compressionMethod = scanField(ZipEntry.class, "compressionMethod");
        if (ZipEntry$compressionMethod == null)
            return;
        ZipEntry$compressionMethod.setAccessible(true);

        final Field ZipEntry$size = scanField(ZipEntry.class, "size");
        if (ZipEntry$size == null)
            return;
        ZipEntry$size.setAccessible(true);

        final Field ZipEntry$nameLen = scanField(ZipEntry.class, "nameLen", "nameLength");
        if (ZipEntry$nameLen == null)
            return;
        ZipEntry$nameLen.setAccessible(true);

        final Field ZipFile$mRaf = scanField(ZipFile.class, "mRaf", "raf");
        if (ZipFile$mRaf == null)
            return;
        ZipFile$mRaf.setAccessible(true);

        final Field ZipEntry$mLocalHeaderRelOffset = scanField(ZipEntry.class, "mLocalHeaderRelOffset", "localHeaderRelOffset");
        if (ZipEntry$mLocalHeaderRelOffset == null)
            return;
        ZipEntry$mLocalHeaderRelOffset.setAccessible(true);

        final Method ZipFile$getInputStream = findMethod(ZipFile.class, "getInputStream", ZipEntry.class);
        if (ZipFile$getInputStream == null)
            return;

        MS.hookMethod(ZipFile.class, ZipFile$getInputStream,
            new MS.MethodAlteration<ZipFile, InputStream>() {
                public InputStream invoked(ZipFile thiz, Object... args)
                    throws Throwable
                {
                    ZipEntry entry = (ZipEntry) args[0];

                    RandomAccessFile raf = (RandomAccessFile) ZipFile$mRaf.get(thiz);
                    synchronized (raf) {
                        raf.seek(ZipEntry$mLocalHeaderRelOffset.getLong(entry));

                        raf.skipBytes(6);
                        if ((raf.readShort() & 0x0080) != 0)
                            throw new ZipException("bug #9695860 [" + thiz.getName() + "]");

                        raf.skipBytes(18);

                        int length = Short.reverseBytes(raf.readShort()) & 0xffff;
                        if (length != ZipEntry$nameLen.getInt(entry))
                            throw new ZipException("bug #9950697 [" + thiz.getName() + "]");

                        if ((raf.readShort() & 0x0080) != 0)
                            throw new ZipException("bug #9695860 [" + thiz.getName() + "]");
                    }

                    if (ZipEntry$compressionMethod.getInt(entry) == ZipEntry.STORED)
                        if (ZipEntry$compressedSize.getLong(entry) != ZipEntry$size.getLong(entry))
                            throw new ZipException("bug #10227498 [" + thiz.getName() + "]");

                    return invoke(thiz, args);
                }
            }
        );
    }

    private static void fixZipFile$readCentralDir() {
        final Field ZipFile$entries = scanField(ZipFile.class, "mEntries", "entries");
        if (ZipFile$entries == null)
            return;
        ZipFile$entries.setAccessible(true);

        final Method ZipFile$readCentralDir = findMethod(ZipFile.class, "readCentralDir");
        if (ZipFile$readCentralDir == null)
            return;

        MS.hookMethod(ZipFile.class, ZipFile$readCentralDir,
            new MS.MethodAlteration<ZipFile, Void>() {
                public Void invoked(ZipFile thiz, Object... args)
                    throws Throwable
                {
                    ZipFile$entries.set(thiz, new LinkedHashMap<String, ZipEntry>() {
                        public ZipEntry put(String key, ZipEntry value) {
                            if (super.put(key, value) != null)
                                throw new WrongException(new ZipException("bug #8219321 [" + key + "]"));
                            return null;
                        }
                    });

                    try {
                        return invoke(thiz, args);
                    } catch (WrongException wrong) {
                        throw wrong.getCause();
                    }
                }
            }
        );
    }

    public static void initialize() {
        fixZipEntry$init$();
        fixZipFile$getInputStream();
        fixZipFile$readCentralDir();
    }
}
