/**
It should be launch earlier in order to be aware of a maximun 
quantity of file descriptors.


@author @FrenchYeti
*/
Java.perform(function () {

    // ============= Config
    var CONFIG = {
        // if TRUE enable data dump 
        printEnable: true,
        // if TRUE enable libc.so open/read/write hook
        printLibc: false,
        // if TRUE print the stack trace for each hook
        printStackTrace: false,
        // to filter the file path whose data want to be dumped in ASCII 
        dump_ascii_If_Path_contains: [".log", ".xml", ".prop"],
        // to filter the file path whose data want to be NOT dumped in hexdump (useful for big chunk and excessive reads) 
        dump_hex_If_Path_NOT_contains: [".png", "/proc/self/task", "/system/lib", "base.apk", "cacert"],
        // to filter the file path whose data want to be NOT dumped fron libc read/write (useful for big chunk and excessive reads) 
        dump_raw_If_Path_NOT_contains: [".png", "/proc/self/task", "/system/lib", "base.apk", "cacert"]
    }

    // =============  Keep a trace of file descriptor, path, and so
    var TraceFD = {};
    var TraceFS = {};
    var TraceFile = {};
    var TraceSysFD = {};


    // ============= Get classes
    var CLS = {
        File: Java.use("java.io.File"),
        FileInputStream: Java.use("java.io.FileInputStream"),
        FileOutputStream: Java.use("java.io.FileOutputStream"),
        String: Java.use("java.lang.String"),
        FileChannel: Java.use("java.nio.channels.FileChannel"),
        FileDescriptor: Java.use("java.io.FileDescriptor"),
        Thread: Java.use("java.lang.Thread"),
        StackTraceElement: Java.use("java.lang.StackTraceElement"),
        AndroidDbSQLite: Java.use("android.database.sqlite.SQLiteDatabase")
    };
    var File = {
        new: [
            CLS.File.$init.overload("java.io.File", "java.lang.String"),
            CLS.File.$init.overload("java.lang.String"),
            CLS.File.$init.overload("java.lang.String", "java.lang.String"),
            CLS.File.$init.overload("java.net.URI"),
        ]
    };
    var FileInputStream = {
        new: [
            CLS.FileInputStream.$init.overload("java.io.File"),
            CLS.FileInputStream.$init.overload("java.io.FileDescriptor"),
            CLS.FileInputStream.$init.overload("java.lang.String"),
        ],
        read: [
            CLS.FileInputStream.read.overload(),
            CLS.FileInputStream.read.overload("[B"),
            CLS.FileInputStream.read.overload("[B", "int", "int"),
        ],
    };
    var FileOuputStream = {
        new: [
            CLS.FileOutputStream.$init.overload("java.io.File"),
            CLS.FileOutputStream.$init.overload("java.io.File", "boolean"),
            CLS.FileOutputStream.$init.overload("java.io.FileDescriptor"),
            CLS.FileOutputStream.$init.overload("java.lang.String"),
            CLS.FileOutputStream.$init.overload("java.lang.String", "boolean")
        ],
        write: [
            CLS.FileOutputStream.write.overload("[B"),
            CLS.FileOutputStream.write.overload("int"),
            CLS.FileOutputStream.write.overload("[B", "int", "int"),
        ],
    };



    // ============= Hook implementation

    File.new[1].implementation = function (a0) {
        prettyLog("[Java::File.new.1] New file : " + a0);

        var ret = File.new[1].call(this, a0);
        var f = Java.cast(this, CLS.File);
        TraceFile["f" + this.hashCode()] = a0;


        return ret;
    }
    File.new[2].implementation = function (a0, a1) {
        prettyLog("[Java::File.read.2] New file : " + a0 + "/" + a1);

        var ret = File.new[2].call(this, a0, a1);;
        var f = Java.cast(this, CLS.File);
        TraceFile["f" + this.hashCode()] = a0 + "/" + a1;

        return ret;
    }


    FileInputStream.new[0].implementation = function (a0) {
        var file = Java.cast(a0, CLS.File);
        var fname = TraceFile["f" + file.hashCode()];

        if (fname == null) {
            var p = file.getAbsolutePath();
            if (p !== null)
                fname = TraceFile["f" + file.hashCode()] = p;
        }
        if (fname == null)
            fname = "[unknow]"

        prettyLog("[Java::FileInputStream.new.0] New input stream from file (" + fname + "): ");

        var fis = FileInputStream.new[0].call(this, a0)
        var f = Java.cast(this, CLS.FileInputStream);

        TraceFS["fd" + this.hashCode()] = fname;

        var fd = Java.cast(this.getFD(), CLS.FileDescriptor);

        TraceFD["fd" + fd.hashCode()] = fname;

        return fis;
    }



    FileInputStream.read[1].implementation = function (a0) {
        var fname = TraceFS["fd" + this.hashCode()];
        var fd = null;
        if (fname == null) {
            fd = Java.cast(this.getFD(), CLS.FileDescriptor);
            fname = TraceFD["fd" + fd.hashCode()]
        }
        if (fname == null)
            fname = "[unknow]";

        var b = Java.array('byte', a0);

        prettyLog("[Java::FileInputStream.read.1] Read from file,offset (" + fname + "," + a0 + "):\n" +
            prettyPrint(fname, b));

        return FileInputStream.read[1].call(this, a0);
    }
    FileInputStream.read[2].implementation = function (a0, a1, a2) {
        var fname = TraceFS["fd" + this.hashCode()];
        var fd = null;
        if (fname == null) {
            fd = Java.cast(this.getFD(), CLS.FileDescriptor);
            fname = TraceFD["fd" + fd.hashCode()]
        }
        if (fname == null)
            fname = "[unknow]";

        var b = Java.array('byte', a0);

        prettyLog("[Java::FileInputStream.read.2] Read from file,offset,len (" + fname + "," + a1 + "," + a2 + ")\n" +
            prettyPrint(fname, b));

        return FileInputStream.read[2].call(this, a0, a1, a2);
    }



    // =============== File Output Stream ============



    FileOuputStream.new[0].implementation = function (a0) {
        var file = Java.cast(a0, CLS.File);
        var fname = TraceFile["f" + file.hashCode()];

        if (fname == null)
            fname = "[unknow]<File:" + file.hashCode() + ">";


        prettyLog("[Java::FileOuputStream.new.0] New output stream to file (" + fname + "): ");

        var fis = FileOuputStream.new[0].call(this, a0);

        TraceFS["fd" + this.hashCode()] = fname;

        var fd = Java.cast(this.getFD(), CLS.FileDescriptor);
        TraceFD["fd" + fd.hashCode()] = fname;

        return fis;
    }

    FileOuputStream.new[1].implementation = function (a0) {
        var file = Java.cast(a0, CLS.File);
        var fname = TraceFile["f" + file.hashCode()];

        if (fname == null)
            fname = "[unknow]";


        prettyLog("[Java::FileOuputStream.new.1] New output stream to file (" + fname + "): \n");

        var fis = FileOuputStream.new[1].call(this, a0);

        TraceFS["fd" + this.hashCode()] = fname;

        var fd = Java.cast(this.getFD(), CLS.FileDescriptor);

        TraceFD["fd" + fd.hashCode()] = fname;

        return fis;
    }

    FileOuputStream.new[2].implementation = function (a0) {
        var fd = Java.cast(a0, CLS.FileDescriptor);
        var fname = TraceFD["fd" + fd.hashCode()];

        if (fname == null)
            fname = "[unknow]";


        prettyLog("[Java::FileOuputStream.new.2] New output stream to FileDescriptor (" + fname + "): \n");
        var fis = FileOuputStream.new[1].call(this, a0)

        TraceFS["fd" + this.hashCode()] = fname;

        return fis;
    }
    FileOuputStream.new[3].implementation = function (a0) {
        prettyLog("[Java::FileOuputStream.new.3] New output stream to file (str=" + a0 + "): \n");

        var fis = FileOuputStream.new[1].call(this, a0)

        TraceFS["fd" + this.hashCode()] = a0;
        var fd = Java.cast(this.getFD(), CLS.FileDescriptor);
        TraceFD["fd" + fd.hashCode()] = a0;

        return fis;
    }
    FileOuputStream.new[4].implementation = function (a0) {
        prettyLog("[Java::FileOuputStream.new.4] New output stream to file (str=" + a0 + ",bool): \n");

        var fis = FileOuputStream.new[1].call(this, a0)
        TraceFS["fd" + this.hashCode()] = a0;
        var fd = Java.cast(this.getFD(), CLS.FileDescriptor);
        TraceFD["fd" + fd.hashCode()] = a0;

        return fis;
    }



    FileOuputStream.write[0].implementation = function (a0) {
        var fname = TraceFS["fd" + this.hashCode()];
        var fd = null;

        if (fname == null) {
            fd = Java.cast(this.getFD(), CLS.FileDescriptor);
            fname = TraceFD["fd" + fd.hashCode()]
        }
        if (fname == null)
            fname = "[unknow]";

        prettyLog("[Java::FileOuputStream.write.0] Write byte array (" + fname + "):\n" +
            prettyPrint(fname, a0));

        return FileOuputStream.write[0].call(this, a0);
    }
    FileOuputStream.write[1].implementation = function (a0) {

        var fname = TraceFS["fd" + this.hashCode()];
        var fd = null;
        if (fname == null) {
            fd = Java.cast(this.getFD(), CLS.FileDescriptor);
            fname = TraceFD["fd" + fd.hashCode()]
        }
        if (fname == null)
            fname = "[unknow]";

        prettyLog("[Java::FileOuputStream.write.1] Write int  (" + fname + "): " + a0);


        return FileOuputStream.write[1].call(this, a0);
    }
    FileOuputStream.write[2].implementation = function (a0, a1, a2) {

        var fname = TraceFS["fd" + this.hashCode()];
        var fd = null;
        if (fname == null) {
            fd = Java.cast(this.getFD(), CLS.FileDescriptor);
            fname = TraceFD["fd" + fd.hashCode()]
            if (fname == null)
                fname = "[unknow], fd=" + this.hashCode();
        }

        prettyLog("[Java::FileOuputStream.write.2] Write " + a2 + " bytes from " + a1 + "  (" + fname + "):\n" +
            prettyPrint(fname, a0));

        return FileOuputStream.write[2].call(this, a0, a1, a2);
    }

    // Native hooks — Frida 17.x compatible
    try {
        const libc = Process.getModuleByName("libc.so");

        Interceptor.attach(libc.getExportByName("read"), {
            onEnter: function (args) {
                if (typeof CONFIG !== "undefined" && CONFIG.printLibc === true) {
                    const bfr = args[1];
                    const sz = args[2].toInt32();
                    const fd = args[0].toInt32();
                    const path = TraceSysFD["fd-" + fd] || "[unknown path]";
                    let data = null;
                    try {
                        data = bfr.readByteArray(sz);
                    } catch (_) {
                        data = null;
                    }
                    prettyLog("[Libc::read] FD: " + path + " (" + fd + "), size: " + sz + "\n" + rawPrint(path, data));
                }
            }
        });

        Interceptor.attach(libc.getExportByName("open"), {
            onEnter: function (args) {
                this.path = args[0].isNull() ? "[null]" : args[0].readCString();
            },
            onLeave: function (ret) {
                const fd = ret.toInt32();
                TraceSysFD["fd-" + fd] = this.path;
                if (typeof CONFIG !== "undefined" && CONFIG.printLibc === true) {
                    prettyLog("[Libc::open] File opened: '" + this.path + "' (fd: " + fd + ")");
                }
            }
        });

        Interceptor.attach(libc.getExportByName("write"), {
            onEnter: function (args) {
                if (typeof CONFIG !== "undefined" && CONFIG.printLibc === true) {
                    const bfr = args[1];
                    const sz = args[2].toInt32();
                    const fd = args[0].toInt32();
                    const path = TraceSysFD["fd-" + fd] || "[unknown path]";
                    let data = null;
                    try {
                        data = bfr.readByteArray(sz);
                    } catch (_) {
                        data = null;
                    }
                    prettyLog("[Libc::write] FD: " + path + " (" + fd + "), size: " + sz + "\n" + rawPrint(path, data));
                }
            }
        });
    } catch (e) {
        send("Error hooking libc.so functions: " + e);
    }

    // helper functions
    function prettyLog(str) {
        send("---------------------------\n" + str);
        if (CONFIG.printStackTrace === true) {
            printStackTrace();
        }
    }

    function prettyPrint(path, buffer) {
        if (CONFIG.printEnable === false) return "";

        if (contains(path, CONFIG.dump_ascii_If_Path_contains)) {
            return b2s(buffer);
        } else if (!contains(path, CONFIG.dump_hex_If_Path_NOT_contains)) {
            return hexdump(b2s(buffer));
        }
        return "[dump skipped by config]";
    }

    function rawPrint(path, buffer) {
        if (CONFIG.printEnable === false) return "";

        if (!contains(path, CONFIG.dump_raw_If_Path_NOT_contains)) {
            return hexdump(buffer);
        }
        return "[dump skipped by config]";
    }

    function contains(path, patterns) {
        for (var i = 0; i < patterns.length; i++)
            if (path.indexOf(patterns[i]) > -1) return true;
        return false;
    }

    function printStackTrace() {
        var th = Java.cast(CLS.Thread.currentThread(), CLS.Thread);
        var stack = th.getStackTrace(),
            e = null;

        for (var i = 0; i < stack.length; i++) {
            send("\t" + stack[i].getClassName() + "." + stack[i].getMethodName() + "(" + stack[i].getFileName() + ")");
        }
    }

    function isZero(block) {
        var m = /^[0\s]+$/.exec(block);
        return m != null && m.length > 0 && (m[0] == block);
    }

    function hexdump(buffer, blockSize) {
        blockSize = blockSize || 16;
        var lines = [];
        var hex = "0123456789ABCDEF";
        var prevZero = false,
            ctrZero = 0;
        for (var b = 0; b < buffer.length; b += blockSize) {
            var block = buffer.slice(b, Math.min(b + blockSize, buffer.length));
            var addr = ("0000" + b.toString(16)).slice(-4);
            var codes = block.split('').map(function (ch) {
                var code = ch.charCodeAt(0);
                return " " + hex[(0xF0 & code) >> 4] + hex[0x0F & code];
            }).join("");
            codes += "   ".repeat(blockSize - block.length);
            var chars = block.replace(/[\\x00-\\x1F\\x20\n]/g, '.');
            chars += " ".repeat(blockSize - block.length);
            if (isZero(codes)) {
                ctrZero += blockSize;
                prevZero = true;
            } else {
                if (prevZero) {
                    lines.push("\t [" + ctrZero + "] bytes of zeroes");
                }
                lines.push(addr + " " + codes + "  " + chars);
                prevZero = false;
                ctrZero = 0;
            }
        }
        if (prevZero) {
            lines.push("\t [" + ctrZero + "] bytes of zeroes");
        }
        return lines.join("\\n");
    }

    function b2s(array) {
        var result = "";
        for (var i = 0; i < array.length; i++) {
            result += String.fromCharCode(modulus(array[i], 256));
        }
        return result;
    }

    function modulus(x, n) {
        return ((x % n) + n) % n;
    }

});