/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * 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.
 */

package dxconvext;

import dxconvext.util.FileUtils;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;

public class ClassFileAssembler {

    /**
     * @param args
     */
    public static void main(String[] args) {
        ClassFileAssembler cfa = new ClassFileAssembler();
        cfa.run(args);
    }

    private void run(String[] args) {
        // this class can be used to generate .class files that are somehow
        // damaged in order to test the dalvik vm verifier.
        // The input is a .cfh (class file hex) file.
        // The output is a java vm .class file.
        // The .cfh files can be generated as follows:
        // 1. create the initial .cfh file from an existing .class files by using
        //    the ClassFileParser
        // 2. modify some bytes to damage the structure of the .class file in a 
        //    way that would not be possible with e.g. jasmin (otherwise you are
        //    better off using jasmin).
        //    Uncomment the original bytes, and write "MOD:" meaning a modified 
        // entry (with the original commented out)
        //
        // Use the ClassFileAssembler to generate the .class file.
        // this class here simply takes all non-comment lines from the .cfh
        // file, parses them as hex values and writes the bytes to the class file
        File cfhF = new File(args[0]);
        if (!cfhF.getName().endsWith(".cfh") &&
            !cfhF.getName().endsWith(".dfh")) {
            System.out.println("file must be a .cfh or .dfh file, and its filename end with .cfh or .dfh");
            return;
        }
        
        String outBase = args[1];
        
        boolean isDex = cfhF.getName().endsWith(".dfh");
        
        byte[] cfhbytes = FileUtils.readFile(cfhF);
        ByteArrayInputStream bais = new ByteArrayInputStream(cfhbytes);
        // encoding should not matter, since we are skipping comment lines and parsing
        try {
            // get the package name
            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(cfhF)));
            String firstLine = br.readLine();
            br.close();
            String classHdr = "//@class:";
            String dexHdr = "// Processing '";
            String hdr;
            if(isDex)
                hdr = dexHdr;
            else
                hdr = classHdr;
            
            if (!firstLine.startsWith(hdr)) throw new RuntimeException("wrong format:"+firstLine +" isDex=" + isDex);
            String tFile;
            if(isDex) {
                tFile = outBase + "/classes.dex";
            } else {
                String classO = firstLine.substring(hdr.length()).trim();
                tFile = outBase +"/"+classO+".class";
            }
            File outFile = new File(tFile);
            System.out.println("outfile:" + outFile);
            String mkdir = tFile.substring(0, tFile.lastIndexOf("/"));
            new File(mkdir).mkdirs();
            
            Reader r = new InputStreamReader(bais,"utf-8");
            OutputStream os = new FileOutputStream(outFile);
            BufferedOutputStream bos = new BufferedOutputStream(os);
            writeClassFile(r, bos, isDex);
            bos.close();
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("problem while parsing .dfh or .cfh file: "+cfhF.getAbsolutePath(), e);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("problem while parsing .dfh or .cfh file: "+cfhF.getAbsolutePath(), e);
        } catch (IOException e) {
            throw new RuntimeException("problem while parsing .dfh or .cfh file: "+cfhF.getAbsolutePath(), e);
        } 
    }
    
    /**
     * Calculates the signature for the <code>.dex</code> file in the
     * given array, and modify the array to contain it.
     * 
     * Originally from com.android.dx.dex.file.DexFile.
     * 
     * @param bytes non-null; the bytes of the file
     */
    private void calcSignature(byte[] bytes) {
        MessageDigest md;

        try {
            md = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException(ex);
        }

        md.update(bytes, 32, bytes.length - 32);

        try {
            int amt = md.digest(bytes, 12, 20);
            if (amt != 20) {
                throw new RuntimeException("unexpected digest write: " + amt +
                                           " bytes");
            }
        } catch (DigestException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Calculates the checksum for the <code>.dex</code> file in the
     * given array, and modify the array to contain it.
     * 
     * Originally from com.android.dx.dex.file.DexFile.
     * 
     * @param bytes non-null; the bytes of the file
     */
    private void calcChecksum(byte[] bytes) {
        Adler32 a32 = new Adler32();

        a32.update(bytes, 12, bytes.length - 12);

        int sum = (int) a32.getValue();

        bytes[8]  = (byte) sum;
        bytes[9]  = (byte) (sum >> 8);
        bytes[10] = (byte) (sum >> 16);
        bytes[11] = (byte) (sum >> 24);
    }   

    public void writeClassFile(Reader r, OutputStream rOs, boolean isDex) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
        BufferedReader br = new BufferedReader(r);
        String line;
        String secondLine = null;
        int lineCnt = 0;
        try {
            while ((line = br.readLine()) != null) {
                if (isDex && lineCnt++ == 1) {
                    secondLine = line;
                }
                // skip it if it is a comment
                if (!line.trim().startsWith("//")) {
                    // we have a row like "    ae 08 21 ff" etc.
                    String[] parts = line.split("\\s+");
                    for (int i = 0; i < parts.length; i++) {
                        String part = parts[i].trim();
                        if (!part.equals("")) {
                            int res = Integer.parseInt(part, 16);
                            baos.write(res);
                        }
                    }
                }
            }
            
            // now for dex, update the checksum and the signature.
            // special case:
            // for two tests (currently T_f1_9.dfh and T_f1_10.dfh), we need
            // to keep the checksum or the signature, respectively.
            byte[] outBytes = baos.toByteArray();
            if (isDex) {
                boolean leaveChecksum = secondLine.contains("//@leaveChecksum");
                boolean leaveSignature= secondLine.contains("//@leaveSignature");
                // update checksum and signature for dex file            
                if(!leaveSignature)
                    calcSignature(outBytes);
                if(!leaveChecksum)
                    calcChecksum(outBytes);
            }
            rOs.write(outBytes);
            rOs.close();
        } catch (IOException e) {
            throw new RuntimeException("problem while writing file",e);
        }
    }

}