
/**
 * JLHAFrontEnd.java
 *
 * Copyright (c) 2006 Ying-Chun Liu (PaulLiu)
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are 
 * met:
 *
 *  1. Redistributions of source code must retain the copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND
 * CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

package org.jlhafrontend;
import java.io.*;
import java.util.logging.*;
import java.util.*;
import jp.gr.java_conf.dangan.util.lha.*;


/**
 *
 * LHA front-end startup class 
 *
 */
public class JLHAFrontEnd {

    /**
     * main instance 
     */
    private static JLHAFrontEnd instance=null;

    /**
     * debug logger
     */
    private Logger mylogger=null;

    /**
     * constructor
     */
    public JLHAFrontEnd() {
	/* setup logger */
	mylogger = Logger.getLogger("org.jlhafrontend");
    }


    /**
     * get instance
     * @return instance
     */
    public static JLHAFrontEnd getInstance() {

	if (instance==null) {
	    instance = new JLHAFrontEnd();
	}
	return instance;
    }

    /**
     * start parsing args and execute command
     * @param args command-line arguments
     */
    public void start(String[] args) {
	Opts useropts = new Opts();
	String archiveFilename=null;
	String[] filesFilename=null;
	int i;

	/* set file encoding to ISO8859-1 as default */
	// java.util.Locale.setDefault(java.util.Locale.US); /* FIXME: ugly, but work */

	/* check args */
	if (args.length < 2 && args.length != 1) {
	    usage(args);
	    return;
	}

	if (args.length == 1) {
	    useropts.setOpts("l");
	    archiveFilename=args[0];
	    filesFilename=null;
	} else {
	    /* parse args */
	    if (!useropts.setOpts(args[0])) {
		usage(args);
		return;
	    }

	    /* other opts */
	    archiveFilename=args[1];
	    if (args.length > 2) {
		filesFilename = new String[args.length-2];
		for (i=2 ; i<args.length; i++) {
		    filesFilename[i-2]=args[i];
		}
	    }
	}

	/* set logger */
	if (useropts.getQuiet() == 995) {
	    mylogger.setLevel(Level.ALL);
	    useropts.setQuiet(0);
	}
	
	/* execute cmds */
	if (useropts.getCmd()==Opts.CMD_ADD) {
	    add(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_EXTRACT) {
	    extract(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_LIST) {
	    list(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_VERBOSELIST) {
	    verboseList(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_UPDATE) {
	    update(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_DELETE) {
	    del(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_RECONSTRUCT) {
	    create(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_PRINTSTDOUT) {
	    print(useropts,archiveFilename,filesFilename);
	} else if (useropts.getCmd()==Opts.CMD_TEST) {
	    test(useropts,archiveFilename,filesFilename);
	}
    }


    /**
     * Decide a string is in a string array or not
     *
     * @param a the string
     * @param b the array
     * @return true if a is in b
     */
    private boolean isInArray(String a,String [] b) {
	int i;
	for (i=0 ; i<b.length ; i++) {
	    if (a.compareTo(b[i])==0) {
		return true;
	    }
	}
	return false;
    }

    /**
     * Transfer an InputStream to an OutputStream
     *
     * @param in the input stream
     * @param out the output stream
     * @return number of blocks (8k bytes) copied
     *
     */
    public int transferTo(InputStream in, OutputStream out) {
	int i,bufRead;
	byte[] buf = new byte[1024*8];
	for (i=0; true; i++) {
	    try {
		bufRead=in.read(buf);
	    } catch (Exception e) {
		mylogger.severe("read from in "+e.toString());
		break;
	    }
	    if (bufRead<=0) break;
	    try {
		out.write(buf,0,bufRead);
	    } catch (Exception e) {
		mylogger.severe("write to out "+e.toString());
		break;
	    }
	}
	return i;
    }

    /**
     * transfer files in backup file to new archive file
     *
     * @param bak the backup file
     * @param arc the archive file
     * @param blackList the files to be excluded
     * @return the files be deleted successfully
     */
    public Vector transferBackupToArchive(LhaFile bak, LhaOutputStream arc, String[] blackList) {
	int i=0;
	String filename=null;
	LhaHeader[] lhaEntries=null;
	InputStream compressedInputStream=null;
	Vector deleteFiles = new Vector();

	/* list archive file */
	lhaEntries = bak.getEntries();

	/* copy old entries */
	for (i=0 ; i<lhaEntries.length; i++) {
	    filename = lhaEntries[i].getPath();
	    /* check if the file is in list or not */
	    if (blackList != null && isInArray(filename,blackList)) {
		deleteFiles.add(filename);
		continue;
	    }
	    /* copy */
	    try {
		arc.putNextEntryAlreadyCompressed( lhaEntries[i] );
	    } catch (IOException e) {
		mylogger.severe("putNextEntry error "+e.toString());
	    }
	    if (lhaEntries[i].getCompressMethod().compareTo(CompressMethod.LHD)!=0) {
		compressedInputStream = bak.getInputStreamWithoutExtract(lhaEntries[i]);
		transferTo(compressedInputStream,arc);
		try {
		    compressedInputStream.close();
		} catch (IOException e) {
		    mylogger.severe("closing compressedInputStream "+e.toString());
		}
	    }
	    /* close streams */
	    try {
		arc.closeEntry();
	    } catch (IOException e) {
		mylogger.severe("closing uncompressedOutputStream "+e.toString());
	    }
	}	    
	return deleteFiles;
    }

    /**
     * make a backup file (renaming a file to *.bak)
     *
     * @param filename the name of the file need to rename to .bak
     * @return the name of the backup file
     */
    public String mkbackup(String filename) {
	String bakfilename=null;
	File fd=null;

	try {
	    if (filename.length()>=4 && filename.charAt(filename.length()-4)=='.' && filename.substring(filename.length()-3).compareToIgnoreCase("bak")!=0) {
		bakfilename = filename.substring(0,filename.length()-3)+"bak";
	    } else {
		bakfilename = filename+".bak";
	    }
	} catch (IndexOutOfBoundsException e) {
	    mylogger.severe("generate bakfilename error "+e.toString());
	    return null;
	}

	try {
	    fd=new File(bakfilename);
	    if (fd.exists()) {
		fd.delete();
	    }
	} catch (SecurityException e) {
	    mylogger.info("delete old file error");
	}

	try {
	    fd = new File(filename);
	    if (fd.exists()) {
		fd.renameTo(new File(bakfilename));
	    }
	} catch (SecurityException e) {
	    mylogger.info("rename file");
	}
	return bakfilename;
    }

    /**
     * execute add command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be extracted
     */
    public void add(Opts useropts, String archiveFilename, String[] filesFilename) {
	String[] list;
	int i=0;
	File fl=null;
	String backupFilename=null;
	LhaFile backupFile=null;
	LhaWithEntropyOutputStream lio=null;
        FileList fileList=null;

	/* open archive file */
	try {
	    fl = (new File(archiveFilename));
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());
	    return;
	}

	/* if the file is not exist, call create */
	try {
	    if (!fl.exists()) {
		create(useropts,archiveFilename,filesFilename);
		return;
	    }
	} catch (SecurityException e) {
	    mylogger.severe("Cannot test exist");
	    return;
	}


	/* make backup */
	backupFilename = mkbackup(archiveFilename);

	/* open new archive file */
	try {
	    lio = new LhaWithEntropyOutputStream(new FileOutputStream(archiveFilename));
	} catch (Exception e) {
	    mylogger.severe("new LhaImeediateOutputStream with "+archiveFilename+" error: "+e.toString());
	    return;
	}

	/* generate list */
	fileList = new FileList(filesFilename);
	list = fileList.getFiles();

	/* open backup file */
	try {
	    backupFile = new LhaFile (new File(backupFilename));
	} catch (FileNotFoundException e) {
	    System.err.println("LHa: Fatal error: "+archiveFilename+": No such file or directory");
	    return;
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());		    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}

	transferBackupToArchive(backupFile,lio,list);

	/* compress new entries */
	for (i=0 ; i<filesFilename.length ; i++) {
	    createSwitcher(useropts,lio,filesFilename[i]);
	}
	try {
	    lio.close();
	} catch (IOException e) {
	    mylogger.severe("lio.close() "+e.toString());
	}
	try {
	    backupFile.close();
	} catch (IOException e) {
	    mylogger.severe("backupFile.close() "+e.toString());
	}

    }


    /**
     * execute update command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be updated
     */
    public void update(Opts useropts, String archiveFilename, String[] filesFilename) {
	String[] list;
	int i=0,j=0;
	File fl=null;
	String backupFilename=null;
	LhaFile backupFile=null;
	LhaHeader[] backupFileEntries=null;
	LhaWithEntropyOutputStream lio=null;
	FileList fileList=null;

	/* open archive file */
	try {
	    fl = (new File(archiveFilename));
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());
	    return;
	}

	/* if the file is not exist, call create */
	try {
	    if (!fl.exists()) {
		create(useropts,archiveFilename,filesFilename);
		return;
	    }
	} catch (SecurityException e) {
	    mylogger.severe("Cannot test exist");
	    return;
	}


	/* make backup */
	backupFilename = mkbackup(archiveFilename);

	/* open new archive file */
	try {
	    lio = new LhaWithEntropyOutputStream(new FileOutputStream(archiveFilename));
	} catch (Exception e) {
	    mylogger.severe("new LhaImeediateOutputStream with "+archiveFilename+" error: "+e.toString());
	    return;
	}

	/* generate list */
	fileList = new FileList(filesFilename);
	list = fileList.getFiles();

	/* open backup file */
	try {
	    backupFile = new LhaFile (new File(backupFilename));
	} catch (FileNotFoundException e) {
	    System.err.println("LHa: Fatal error: "+archiveFilename+": No such file or directory");
	    return;
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());		    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}

	/* compare createtime */
	backupFileEntries = backupFile.getEntries();
	for (i=0 ; list != null && i<list.length ; i++) {
	    File f1;
	    long modtime,oldmodtime;

	    f1=null;
	    modtime=0;
	    oldmodtime=0;

	    try {
		f1 = new File(list[i]);
		modtime = f1.lastModified();
	    } catch (SecurityException e) {
	        mylogger.warning("cannot get file modified time "+e.toString());
	    }

	    if (f1==null || f1.isDirectory()) {
		continue;
	    }
	    
	    if (modtime==0) {
	        fileList.setFileMark(list[i]);
		continue;
	    }

	    for (j=0 ; j<backupFileEntries.length ; j++) {
		if (backupFileEntries[j].getPath().equals(list[i])) {
		    oldmodtime = backupFileEntries[j].getLastModified().getTime();
		    break;
		}
	    }

	    if (oldmodtime == 0 || modtime > oldmodtime) {
	        fileList.setFileMark(list[i]);
            }
	    
	}

	list = fileList.getMarkedFiles();

	transferBackupToArchive(backupFile,lio,list);

	/* compress new entries */
	if (list != null) {
	  for (i=0 ; i<list.length ; i++) {
	      createSwitcher(useropts,lio,list[i]);
	  }
        }
	try {
	    lio.close();
	} catch (IOException e) {
	    mylogger.severe("lio.close() "+e.toString());
	}
	try {
	    backupFile.close();
	} catch (IOException e) {
	    mylogger.severe("backupFile.close() "+e.toString());
	}
	
    }
    


    /**
     * execute delete command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be extracted
     */
    public void del(Opts useropts, String archiveFilename, String[] filesFilename) {
	int i=0;
	File fl=null;
	String backupFilename=null;
	LhaFile backupFile=null;
	LhaOutputStream lio=null;
	Vector deleteFiles=null;

	/* open archive file */
	try {
	    fl = (new File(archiveFilename));
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());
	    return;
	}

	/* if the file is not exist, return */
	try {
	    if (!fl.exists()) {
		return;
	    }
	} catch (Exception e) {
	    mylogger.severe("Cannot test exist");
	    return;
	}


	/* make backup */
	backupFilename = mkbackup(archiveFilename);

	/* open new archive file */
	try {
	    lio = new LhaOutputStream(new FileOutputStream(archiveFilename));
	} catch (Exception e) {
	    mylogger.severe("new LhaImeediateOutputStream with "+archiveFilename+" error: "+e.toString());
	    return;
	}

	/* open backup file */
	try {
	    backupFile = new LhaFile (new File(backupFilename));
	} catch (FileNotFoundException e) {
	    System.err.println("LHa: Fatal error: "+archiveFilename+": No such file or directory");
	    return;
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());		    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}

	deleteFiles = transferBackupToArchive(backupFile,lio,filesFilename);

	/* close files */
	try {
	    lio.close();
	} catch (IOException e) {
	    mylogger.severe("lio.close() "+e.toString());
	}
	try {
	    backupFile.close();
	} catch (IOException e) {
	    mylogger.severe("backupFile.close() "+e.toString());
	}

	/* show delete entries */
	for (i=0 ; i<deleteFiles.size() ; i++) {
	    System.out.println("delete "+((String)deleteFiles.elementAt(i)));
	}

    }


    /**
     * check if the name is probably a symbolic link
     *
     * @param filename The file name need to be checked
     */
    public boolean isProbableSymLink(String filename) {
        if (filename.indexOf("|")==-1) {
          return false;
        }
        return true;
    }

    /** 
     * check arbitrary directory traversal
     *
     * @param filename The file name need to be checked
     * @return true: not safe, false: safe
     */
    public boolean checkDirTraversal (String filename) {
    
        String[] splitstr= null;
        int i,j;

        if (filename.length() <= 0) {
            return false;
        }

        if (filename.indexOf("..") == -1) {
            return false;
        }

        splitstr = filename.split("["+File.separator+"]");
        
        j=0;
        for (i=0 ; i<splitstr.length ; i++) {
            if (splitstr[i]==null || splitstr[i].length()==0) {
              return true;
            }
            if (splitstr[i].compareTo("..")==0) {
                j--;
            } else if (splitstr[i].compareTo(".")==0) {
            } else {
                j++;
            }
            if (j<0) {
              return true;
            }
        }
        
        return false;
        
    }

    /**
     * execute extract command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be extracted
     */
    public void extract(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaFile archiveFile=null;
	LhaHeader[] lhaEntries=null;
	int i,j,k=0;
	InputStream compressedInputStream=null;
	OutputStream uncompressedOutputStream=null;
	boolean force=false,ignorePath=false,verbose=false;
	File uncompressedFile,uncompressedFileParent=null;
	String filenameOrigin=null,workingDir=null,filename=null;
	int quiet=0;
	byte[] buf = new byte[1024*8];
	int bufRead=0;

	/* open archive file */
	try {
	    archiveFile = new LhaFile (new File(archiveFilename));
	} catch (FileNotFoundException e) {
	    System.err.println("LHa: Fatal error: "+archiveFilename+": No such file or directory");
	    return;
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());		    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}
	
	/* get opts */
	force = useropts.getForce();
	ignorePath = useropts.getIgnorePath();
	verbose = useropts.getVerbose();
	workingDir = useropts.getSpecifyExtractDirectory();
	quiet=useropts.getQuiet();

	/* list archive file */
	lhaEntries = archiveFile.getEntries();

	/* extract entries */
	for (i=0 ; i<lhaEntries.length; i++) {
	    filenameOrigin = lhaEntries[i].getPath();
	    /* remove beginning '/'s */
	    while (filenameOrigin.startsWith(File.separator)) {
	        filenameOrigin = filenameOrigin.substring(File.separator.length());
	    }
	    /* check if the file is in list or not */
	    if (filesFilename != null && !isInArray(lhaEntries[i].getPath(),filesFilename)) {
		continue;
	    }
	    /* check dir traversal for security */
            if (!ignorePath && checkDirTraversal(filenameOrigin)) {
                System.err.println("Arbitrary directory traversal attempt: "+filenameOrigin);
               continue;
            }
            /* generate filename */
            if (ignorePath) {
                File tmpfile=null;
                tmpfile = new File(filenameOrigin);
                filename = tmpfile.getName();
                tmpfile = null;
            } else {
                filename = filenameOrigin;
            }
	    /* add working directory */
	    if (workingDir != null) {
	        File tmpfile=null;
		filename = FileList.addFileSeparator(workingDir)+filename;
		tmpfile = new File(workingDir);
		if (tmpfile != null) {
		  tmpfile.mkdirs();
                }
                tmpfile=null;
	    }
            /* create uncompressedFile */
	    uncompressedFile = new File(filename);
	    /* extracting directories */
	    if (!ignorePath 
	    && lhaEntries[i].getCompressMethod().compareTo(CompressMethod.LHD)==0) {
	        if (lhaEntries[i].getOSID()!='U' || !isProbableSymLink(filename)) {  /* FIXME - should not handle symlink like this */
	      	    uncompressedFile.mkdirs();
                }
		if (verbose) {
		    System.out.println("Making directory \""+filename+"\".");
		}
		continue;
	    }
	    if (!force && uncompressedFile.exists()) {
	        if (quiet == 0) {
		  System.out.println(filename+" : Skipped...");
                }
		continue;
	    }
	    if (!ignorePath) {
		uncompressedFileParent = uncompressedFile.getParentFile();
		if (uncompressedFileParent != null) {
		    uncompressedFileParent.mkdirs();
                }
	    }
	    if (quiet == 0) {
	        System.out.print(filename);
            }
            k = filename.length();
	    if (quiet == 0) {
	      System.out.print("\t- Melted   :  ");
            }
	    System.out.flush();
	    compressedInputStream = archiveFile.getInputStream(lhaEntries[i]);
	    try {
		uncompressedOutputStream = new FileOutputStream(uncompressedFile);
	    } catch (Exception e) {
		mylogger.severe("open OutputStream error "+e.toString());
		continue;
	    }
	    if (useropts.getTextMode()) {
		uncompressedOutputStream = new DOS2UnixOutputStream(uncompressedOutputStream);
	    }
	    if (useropts.getEUCMode()) {
		uncompressedOutputStream = new SJIS2EUCJPOutputStream(uncompressedOutputStream);
	    }

	    for (j=0; true; j++) {
		try {
		    bufRead=compressedInputStream.read(buf);
		} catch (Exception e) {
		    mylogger.severe("read from compressedInputStream "+e.toString());
		    break;
		}
		if (bufRead<=0) break;
		if (j<80 - ((k/8)+1)*8 - 14 - 1) {
		    if (quiet == 0) {
		      System.out.print("o");
                    }
		    System.out.flush();
		}
		try {
		    uncompressedOutputStream.write(buf,0,bufRead);
		} catch (Exception e) {
		    mylogger.severe("write to uncompressedOutputStream "+e.toString());
		    break;
		}
	    }

	    try {
		compressedInputStream.close();
	    } catch (IOException e) {
		mylogger.severe("closing compressedInputStream "+e.toString());
	    }
	    try {
		uncompressedOutputStream.close();
	    } catch (IOException e) {
		mylogger.severe("closing uncompressedOutputStream "+e.toString());
	    }
            if (quiet == 0) {
	      System.out.println("");
            }
	}

	try {
	    archiveFile.close();
	} catch (IOException e) {
	    mylogger.warning("Close "+archiveFilename+" error:"+e.toString());
	}
	
    }

    /**
     * execute test command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be processed
     */
    public void test(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaFile archiveFile=null;
	LhaHeader[] lhaEntries=null;
	int i,j,k=0;
	InputStream compressedInputStream=null;
	boolean ignorePath=false,verbose=false;
	File uncompressedFile;
	String filename;
	byte[] buf = new byte[1024*8];
	int bufRead=0;

	try {
	    archiveFile = new LhaFile (new File(archiveFilename));
	} catch (FileNotFoundException e) {
	    System.err.println("LHa: Fatal error: "+archiveFilename+": No such file or directory");
	    return;
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());		    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}
	
	ignorePath = useropts.getIgnorePath();
	verbose = useropts.getVerbose();

	lhaEntries = archiveFile.getEntries();

	for (i=0 ; i<lhaEntries.length; i++) {
	    filename = lhaEntries[i].getPath();
	    if (filesFilename != null && !isInArray(filename,filesFilename)) {
		continue;
	    }
	    uncompressedFile = new File(filename);
	    /* extract path */
	    if (lhaEntries[i].getCompressMethod().compareTo(CompressMethod.LHD)==0) {
		continue;
	    }
	    if (ignorePath) {
		uncompressedFile = new File(uncompressedFile.getName());
	    }
	    if (ignorePath) {
		System.out.print(uncompressedFile.getName());
		k = uncompressedFile.getName().length();
	    } else {
		System.out.print(filename);
		k = filename.length();
	    }
	    System.out.print("\t- Tested   :  ");
	    System.out.flush();
	    compressedInputStream = archiveFile.getInputStream(lhaEntries[i]);
	    for (j=0; true; j++) {
		try {
		    bufRead=compressedInputStream.read(buf);
		} catch (Exception e) {
		    mylogger.severe("read from compressedInputStream "+e.toString());
		    break;
		}
		if (bufRead<=0) break;
		if (j<80 - ((k/8)+1)*8 - 14 - 1) {
		    System.out.print("o");
		    System.out.flush();
		}
	    }

	    try {
		compressedInputStream.close();
	    } catch (IOException e) {
		mylogger.severe("closing compressedInputStream "+e.toString());
	    }

	    System.out.println("");
	}

	try {
	    archiveFile.close();
	} catch (IOException e) {
	    mylogger.warning("Close "+archiveFilename+" error:"+e.toString());
	}
	
    }

    /**
     * execute print to stdout command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be processed
     */
    public void print(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaFile archiveFile=null;
	LhaHeader[] lhaEntries=null;
	int i,j,k=0;
	InputStream compressedInputStream=null;
	boolean ignorePath=false,verbose=false;
	File uncompressedFile;
	String filename;
	byte[] buf = new byte[1024*8];
	int bufRead=0;

	try {
	    archiveFile = new LhaFile (new File(archiveFilename));
	} catch (FileNotFoundException e) {
	    System.err.println("LHa: Fatal error: "+archiveFilename+": No such file or directory");
	    return;
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());		    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}
	
	ignorePath = useropts.getIgnorePath();
	verbose = useropts.getVerbose();

	lhaEntries = archiveFile.getEntries();

	for (i=0 ; i<lhaEntries.length; i++) {
	    filename = lhaEntries[i].getPath();
	    if (filesFilename != null && !isInArray(filename,filesFilename)) {
		continue;
	    }
	    uncompressedFile = new File(filename);
	    /* extract path */
	    if (lhaEntries[i].getCompressMethod().compareTo(CompressMethod.LHD)==0) {
		continue;
	    }
	    if (ignorePath) {
		uncompressedFile = new File(uncompressedFile.getName());
	    }
	    System.out.println("::::::::");
	    if (ignorePath) {
		System.out.print(uncompressedFile.getName());
		k = uncompressedFile.getName().length();
	    } else {
		System.out.print(filename);
		k = filename.length();
	    }
	    System.out.println("");
	    System.out.println("::::::::");
	    compressedInputStream = archiveFile.getInputStream(lhaEntries[i]);

	    for (j=0; true; j++) {
		try {
		    bufRead=compressedInputStream.read(buf);
		} catch (Exception e) {
		    mylogger.severe("read from compressedInputStream "+e.toString());
		    break;
		}
		if (bufRead<=0) break;
		try {
		    System.out.write(buf,0,bufRead);
		} catch (Exception e) {
		    mylogger.severe("write to uncompressedOutputStream "+e.toString());
		    break;
		}
	    }

	    try {
		compressedInputStream.close();
	    } catch (IOException e) {
		mylogger.severe("closing compressedInputStream "+e.toString());
	    }
	}

	try {
	    archiveFile.close();
	} catch (IOException e) {
	    mylogger.warning("Close "+archiveFilename+" error:"+e.toString());
	}
	
    }

    /**
     * list/verbose list core
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be processed
     */
    private LhaHeader[] listCore(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaFile archiveFile=null;
	LhaHeader[] lhaEntries=null;

	try {
	    archiveFile = new LhaFile (new File(archiveFilename));
	} catch (Exception e) {
	    mylogger.warning("Open "+archiveFilename+" error:"+e.toString());
	    return null;
	}

	lhaEntries = archiveFile.getEntries();
	
	try {
	    archiveFile.close();
	} catch (IOException e) {
	    mylogger.warning("Close "+archiveFilename+" error:"+e.toString());
	}
	return lhaEntries;
    }

    /**
     * execute list command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be processed
     */
    public void list(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaHeader[] data;
	int i,N=0,quiet=0;
	ListFormatter lf=new ListFormatter();
	long size_sum=0,pack_sum=0,ratio_avg=-1;
	boolean verbose=false;

	verbose=useropts.getVerbose();
	quiet=useropts.getQuiet();
	data = listCore(useropts,archiveFilename,filesFilename);
	if (quiet==0) {
	    System.out.print(" PERMSSN    UID  GID      SIZE  RATIO     STAMP");
	    if (!verbose) { 
		System.out.print("           NAME"); 
	    }
	    System.out.println("");
	    System.out.print("---------- ----------- ------- ------ ------------");
	    if (!verbose) {
		System.out.print(" --------------------");
	    }
	    System.out.println("");
	}
	if (data==null) {
	    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}
	for (i=0 ; i<data.length ; i++) {

	    if (filesFilename != null && !isInArray(data[i].getPath(),filesFilename)) {
		continue;
	    }
	    lf = new ListFormatter(data[i]);
	    size_sum += data[i].getOriginalSize();
	    pack_sum += data[i].getCompressedSize();
	    if (verbose) {
		System.out.println(data[i].getPath());
	    }
	    System.out.print(lf.getPERMSSN());
	    System.out.print(" ");
	    System.out.print(lf.getUIDGID());
	    System.out.print(" ");
	    System.out.print(lf.getSIZE());
	    System.out.print(" ");
	    System.out.print(lf.getRATIO());
	    System.out.print(" ");
	    System.out.print(lf.getSTAMP());
	    System.out.print(" ");
	    if (!verbose) {
		System.out.print(data[i].getPath());
	    } else {
		System.out.print("[2]");
	    }
	    System.out.println("");
	    N++;
	}
	if (size_sum>0) {
	    ratio_avg = pack_sum*1000/size_sum;
	} else {
	    ratio_avg = -1;
	}
	if (quiet==0) {
	    System.out.print("---------- ----------- ------- ------ ------------");
	    if (!verbose) {
		System.out.print(" --------------------");
	    }
	    System.out.println("");
	    System.out.print(" Total    ");
	    System.out.print(" ");
	    System.out.print(lf.toField(Integer.toString(N),5,false));
	    if (N==1) {
		System.out.print(" file ");
	    } else {
		System.out.print(" files");
	    }
	    System.out.print(" ");
	    System.out.print(lf.toField(Long.toString(size_sum),7,false));
	    System.out.print(" ");
	    if (ratio_avg >= 0) {
		System.out.print(lf.toField(Long.toString(ratio_avg/10)+"."+Long.toString(ratio_avg%10)+"%",6,false));
	    } else {
		System.out.print("******");
	    }
	    System.out.print(" ");
	    System.out.print(ListFormatter.genFileDate(archiveFilename));
	    System.out.println("");
	}
    }

    /**
     * execute verbose list command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be processed
     */
    public void verboseList(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaHeader[] data;
	int i,N=0,quiet=0;
	ListFormatter lf=new ListFormatter();
	long size_sum=0,pack_sum=0,ratio_avg=-1;
	boolean verbose=false;

	verbose=useropts.getVerbose();
	quiet=useropts.getQuiet();
	data = listCore(useropts,archiveFilename,filesFilename);
	if (quiet==0) {
	    System.out.print(" PERMSSN    UID  GID    PACKED    SIZE  RATIO METHOD CRC     STAMP");
	    if (!verbose) { 
		System.out.print("          NAME"); 
	    }
	    System.out.println("");
	    System.out.print("---------- ----------- ------- ------- ------ ---------- ------------");
	    if (!verbose) {
		System.out.print(" -------------");
	    }
	    System.out.println("");
	}
	if (data==null) {
	    System.err.println("LHa: Warning: Checksum error (LHarc file?)");
	    System.err.println("LHa: Fatal error: Unkonwn header (lha file?)");
	    return;
	}
	for (i=0 ; i<data.length ; i++) {

	    if (filesFilename != null && !isInArray(data[i].getPath(),filesFilename)) {
		continue;
	    }
	    lf = new ListFormatter(data[i]);
	    size_sum += data[i].getOriginalSize();
	    pack_sum += data[i].getCompressedSize();
	    if (verbose) {
		System.out.println(data[i].getPath());
	    }
	    System.out.print(lf.getPERMSSN());
	    System.out.print(" ");
	    System.out.print(lf.getUIDGID());
	    System.out.print(" ");
	    System.out.print(lf.getPACKED());
	    System.out.print(" ");
	    System.out.print(lf.getSIZE());
	    System.out.print(" ");
	    System.out.print(lf.getRATIO());
	    System.out.print(" ");
	    System.out.print(lf.getMETHOD());
	    System.out.print(" ");
	    System.out.print(lf.getCRC());
	    System.out.print(" ");
	    System.out.print(lf.getSTAMP());
	    System.out.print(" ");
	    if (!verbose) {
		System.out.print(data[i].getPath());
	    } else {
		System.out.print("[2]");
	    }
	    System.out.println("");
	    N++;
	}
	if (size_sum>0) {
	    ratio_avg = pack_sum*1000/size_sum;
	} else {
	    ratio_avg = -1;
	}
	if (quiet==0) {
	    System.out.print("---------- ----------- ------- ------- ------ ---------- ------------");
	    if (!verbose) {
		System.out.print(" -------------");
	    }
	    System.out.println("");
	    System.out.print(" Total    ");
	    System.out.print(" ");
	    System.out.print(lf.toField(Integer.toString(N),5,false));
	    if (N==1) {
		System.out.print(" file ");
	    } else {
		System.out.print(" files");
	    }
	    System.out.print(" ");
	    System.out.print(lf.toField(Long.toString(pack_sum),7,false));
	    System.out.print(" ");
	    System.out.print(lf.toField(Long.toString(size_sum),7,false));
	    System.out.print(" ");
	    if (ratio_avg >= 0) {
		System.out.print(lf.toField(Long.toString(ratio_avg/10)+"."+Long.toString(ratio_avg%10)+"%",6,false));
	    } else {
		System.out.print("******");
	    }
	    System.out.print(" ");
	    System.out.print("          ");
	    System.out.print(" ");
	    System.out.print(ListFormatter.genFileDate(archiveFilename));
	    System.out.println("");
	}
    }

    /**
     * compress a file into a LhaOutputStream
     *
     * @param useropts Options input by users
     * @param lio the LhaOutputStream for an archive file
     * @param filename the file need to be compressed
     */
    public void createFile(Opts useropts, LhaWithEntropyOutputStream lio, String filename) {
	LhaHeader header = new LhaHeader( filename );
	FileInputStream raf = null;
	File fl;
	int i,j,quiet=0;
	boolean fileNotCompress=false,deleteFiles=false;
	ListFormatter lf = new ListFormatter();
	int compressMethod=0;
	long compressedSize=0;
	OutputStream liodata;

	compressMethod = useropts.getCompressionMethod();
	fileNotCompress = useropts.getFileNotCompress();
	deleteFiles = useropts.getDeleteFiles();
	quiet = useropts.getQuiet();
	try {
	    fl = new File(filename);
	} catch (Exception e) {
	    mylogger.severe("new File error: "+e.toString());
	    return;
	}

	try {
	    raf = new FileInputStream( fl );
	} catch (Exception e) {
	    mylogger.severe("new FileInputStream: "+e.toString());
	    return;
	}
        if (useropts.getHeaderLevel() != -1) {
	  header.setHeaderLevel(useropts.getHeaderLevel());
        }
	header.setLastModified( new Date( fl.lastModified() ) );
	header.setOriginalSize( fl.length());
	header.setOSID((byte)0);
	if (compressMethod == 5) {
	    header.setCompressMethod ( CompressMethod.LH5 );
	} else if (compressMethod == 6) {
	    header.setCompressMethod ( CompressMethod.LH6 );
	} else if (compressMethod == 7) {
	    header.setCompressMethod ( CompressMethod.LH7 );
	}
	if (fileNotCompress) {
	    header.setCompressMethod ( CompressMethod.LH0 );
	}
	try {
	    lio.putNextEntry( header );
	} catch (IOException e) {
	    mylogger.severe("putNextEntry error #1: "+e.toString());
	}
	if (quiet==0) {
	    System.out.print(filename);
	    System.out.print("\t- Frozen");
	    System.out.flush();
	}
	liodata = lio;
	if (useropts.getTextMode()) {
	    liodata = new Unix2DOSOutputStream(liodata);
	}
	if (useropts.getEUCMode()) {
	    liodata = new EUCJP2SJISOutputStream(liodata);
	}
	j=transferTo(raf,liodata);
	try {
	    lio.closeEntry();
	} catch (IOException e) {
	    mylogger.severe("lio.closeEntry #1: "+e.toString());
	}
	if (quiet==0) {
	    //compressedSize = header.getCompressedSize();
	    compressedSize = lio.getEntropyLength(); /* FIXME: display correct compression rate */
	    if (header.getOriginalSize() > 0 && header.getOriginalSize() > compressedSize) {
        	System.out.print(lf.toField("("+Long.toString(compressedSize*100/header.getOriginalSize())+"%)",6,true));
            } else {
                System.out.print(lf.toField("(100%)",6,true));
            }
                    
	    for (i=0 ; i<j && i<80-(filename.length()/8+1)*8-14-1 ; i++) {
		System.out.print("o");
                System.out.flush();
	    }
	}
	if (deleteFiles) {
	    try {
		fl.delete();
	    } catch (Exception e) {
		mylogger.warning("cannot delete files"+fl.getAbsolutePath()+" : "+e.toString());
	    }
	}
	if (quiet==0) {
	    System.out.println("");
	}
    }

    /**
     * compress a directory into a LhaOutputStream
     *
     * @param useropts Options input by users
     * @param lio the LhaOutputStream for an archive file
     * @param filename the directory need to be compressed
     */
    public void createDirectory(Opts useropts,LhaWithEntropyOutputStream lio, String dirname) {
	File fl=null;
	File[] listfiles=null;
	String prefix=null;
	int i;
	LhaHeader header;
	boolean deleteFiles=false;

	deleteFiles=useropts.getDeleteFiles();

	prefix = FileList.addFileSeparator(dirname);

	try {
	    fl = new File(dirname);
	} catch (Exception e) {
	    mylogger.severe("new File error: "+e.toString());
	    return;
	}
	/* add -lhd- record */
	header = new LhaHeader(prefix);
        if (useropts.getHeaderLevel() != -1) {
	  header.setHeaderLevel(useropts.getHeaderLevel());
        }
	header.setCompressMethod(CompressMethod.LHD);
	header.setLastModified( new Date( fl.lastModified() ) );
	header.setOSID((byte)0);
	header.setOriginalSize(0);
	try {
	    lio.putNextEntry( header );
	} catch (IOException e) {
	    mylogger.severe("putNextEntry error: "+e.toString());
	}
	try {
	    lio.closeEntry();
	} catch (IOException e) {
	    mylogger.warning("lio.closeEntry: "+e.toString());
	}
	try {
	    listfiles = fl.listFiles();
	} catch (Exception e) {
	    mylogger.warning("open dir \""+dirname+"\" error: "+e.toString());
	    return;
	}
	for (i=0 ; i<listfiles.length ; i++) {
	    createSwitcher(useropts,lio, new String(prefix+listfiles[i].getName()));
	}
	if (deleteFiles) {
	    try {
		fl.delete();
	    } catch (Exception e) {
		mylogger.warning("cannot remove directory "+fl.getAbsolutePath()+" : "+e.toString());
	    }
	}
    }

    /**
     * compress a directory or a file into a LhaOutputStream
     *
     * @param useropts Options input by users
     * @param lio the LhaOutputStream for an archive file
     * @param filename the filename
     */
    public void createSwitcher(Opts useropts,LhaWithEntropyOutputStream lio, String filename) {
	File fl=null;
	try {
	    fl = new File(filename);
	} catch (Exception e) {
	    mylogger.severe("new File error: "+e.toString());
	    return;
	}

	try {
	    if (!fl.exists()) {
		System.err.println("LHa: Cannot access \""+filename+"\", ignored.");
		return;
	    }
	} catch (SecurityException e) {
	    System.err.println("LHa: Cannot access \""+filename+"\", ignored.");
	    return;
	}

	try {
	    if (fl.isFile()) {
		createFile(useropts,lio, filename);
	    } else if (fl.isDirectory()) {
		createDirectory(useropts,lio, filename);
	    } else {
		System.err.println("LHa: Cannot access \""+filename+"\", ignored.");
	    }
	} catch (SecurityException e) {
	    System.err.println("LHa: Cannot access \""+filename+"\", ignored.");
	}
    }

    /**
     * execute create command
     *
     * @param useropts Options input by users
     * @param archiveFilename the filename of the archive file
     * @param filesFilename the array of filenames to be processed
     */
    public void create(Opts useropts, String archiveFilename, String[] filesFilename) {
	LhaWithEntropyOutputStream lio=null;
	int i;
	if (filesFilename == null || filesFilename.length==0) {
	    System.err.println("LHa: Error: No files given in argument, do nothing.");
	    return;
	}
	try {
	    lio = new LhaWithEntropyOutputStream(new FileOutputStream(archiveFilename));
	} catch (Exception e) {
	    mylogger.severe("new LhaImeediateOutputStream with "+archiveFilename+" error: "+e.toString());
	    return;
	}
	for (i=0 ; i<filesFilename.length ; i++) {
	    createSwitcher(useropts,lio,filesFilename[i]);
	}
	try {
	    lio.close();
	} catch (IOException e) {
	    mylogger.severe("lio.close() "+e.toString());
	}
    }

    /**
     * show usage data
     *
     * @param args command-line args
     */
    public void usage(String[] args) {
	int i,j,k;

	String[] cmds = { "commands:", " a   Add to archive", " x,e Extract from archive", " l,v List / Verbose list", " u   Update newer files to archive", " d   Delete from archive", " m   Move to archive", " c   Create new archive", " p   Print to STDOUT from archive", " t   Test archive"  };
	String[] opts = { "options:", " q{num} quiet mode", " v  verbose", " n  not execute" , " f  force", " t  FILES are TEXT files" , " o[567] compression method (a/u)", " w=<dir> specify working directory" ," d  delete files after (a/u/c)", " i  ignore directory path (x/e)", " z  files not compress (a/u)", " 0/1/2  header level (a/u)", " e  TEXT code convert from/to EUC-JP" , " y  filename multibyte convert"  };
	System.out.print(ListFormatter._toField("jLHA",27,true));
	System.out.println("Copyright(C) 2002  Michel Ishizuka");
	System.out.print(ListFormatter._toField("jlhafrontend",27,true));
	System.out.println("Copyright(C) 2006  Ying-Chun Liu");
	System.out.println("usage: JLHAFrontEnd [-]{axelvudmcpt[q[num]][vnfodizg012]}[w=<dir>] archive_file [file...]");

	for (i=0 ; i<cmds.length || i<opts.length ; i++) {
	    k=0;
	    if (i<cmds.length) {
		System.out.print(cmds[i]);
		k=cmds[i].length();
	    }
	    for (j=0 ; j<40-k; j++) {
		System.out.print(" ");
	    }
	    if (i<opts.length) {
		System.out.print(opts[i]);
	    }
	    System.out.println("");
	}
	
    }

    /**
     * main function
     * @param args command-line args
     */
    public static void main(String[] args) {

	Logger mylogger=null;
	JLHAFrontEnd mainInstance = null;

	mylogger = Logger.getLogger("org.jlhafrontend");
	mylogger.setLevel(Level.SEVERE);
	mylogger.addHandler(new ConsoleHandler());
	mainInstance = JLHAFrontEnd.getInstance();
	mainInstance.start(args);

    }
}
