| /* |
| * Copyright (C) 2012 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 com.android.jobb; |
| |
| import de.waldheinz.fs.BlockDevice; |
| import de.waldheinz.fs.FsDirectory; |
| import de.waldheinz.fs.FsDirectoryEntry; |
| import de.waldheinz.fs.FsFile; |
| import de.waldheinz.fs.fat.BootSector; |
| import de.waldheinz.fs.fat.Fat; |
| import de.waldheinz.fs.fat.FatFile; |
| import de.waldheinz.fs.fat.FatFileSystem; |
| import de.waldheinz.fs.fat.FatLfnDirectory; |
| import de.waldheinz.fs.fat.FatLfnDirectoryEntry; |
| import de.waldheinz.fs.fat.FatType; |
| import de.waldheinz.fs.fat.FatUtils; |
| import de.waldheinz.fs.fat.SuperFloppyFormatter; |
| import de.waldheinz.fs.util.FileDisk; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.math.BigInteger; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.nio.channels.ReadableByteChannel; |
| import java.security.InvalidKeyException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Iterator; |
| import java.util.Stack; |
| |
| public class Main { |
| |
| private static final int BLOCK_SIZE = 512; // MUST BE 512 |
| |
| public static void printArgs() { |
| System.out.println("Jobb -- Create OBB files for use on Android"); |
| System.out.println(); |
| System.out.println(" -d <directory> Use <directory> as input/output for OBB files"); |
| System.out.println(" -k <key> Use <key> as password to encrypt/decrypt OBB file"); |
| System.out.println(" -o <filename> Write OBB file out to <filename>"); |
| System.out.println(" -v Verbose mode"); |
| System.out.println(" -h Help; this usage screen"); |
| System.out.println(" -pn <package> Package name for OBB file"); |
| System.out.println(" -pv <version> Package version for OBB file"); |
| System.out.println(" -ov Set overlay flag"); |
| System.out.println(" -dump <file> Parse and dump OBB file"); |
| System.out.println(" -about Notices about this tool"); |
| System.out.println(); |
| System.out.println("Example: Dump the contents of the encrypted OBB file to the directory"); |
| System.out.println(" jobb -dump myfile.obb -d ./mydirectory -k mypassword"); |
| System.out.println(); |
| System.out.println("Example: Create an encrypted OBB file by recursing the directory path"); |
| System.out.println(" jobb -pn package.name -pv 1 -d ./mydirectory -k mypassword"); |
| } |
| |
| static String sDirectory; |
| static File sDirectoryFile; |
| static boolean sHasOutputDirectory; |
| static String sKey; |
| static String sOutputFile; |
| static boolean sVerboseMode; |
| static String sPackageName; |
| static int sPackageVersion = -1; |
| static byte[] sSalt; |
| static boolean sOverlay; |
| static int sFlags; |
| static String sInputFile; |
| static byte[] sFishKey; |
| |
| private interface FileProcessor { |
| void processFile(File f); |
| |
| void processDirectory(File f); |
| |
| /** |
| * @param dir |
| */ |
| void endDirectory(File dir); |
| } |
| |
| static ByteBuffer sTempBuf = ByteBuffer.allocate(1024*1024); |
| |
| static public void dumpDirectory(FsDirectory dir, int tabStop, File curDirectory) throws IOException { |
| Iterator<FsDirectoryEntry> i = dir.iterator(); |
| while (i.hasNext()) { |
| final FsDirectoryEntry e = i.next(); |
| if (e.isDirectory()) { |
| for (int idx = 0; idx < tabStop; idx++) |
| System.out.print(' '); |
| if (e.getName().equals(".") || e.getName().equals("..")) |
| continue; |
| for (int idx = 0; idx < tabStop; idx++) |
| System.out.print(" "); |
| System.out.println("[" + e + "]"); |
| dumpDirectory(e.getDirectory(), tabStop + 1, new File(curDirectory, e.getName())); |
| } else { |
| for ( int idx = 0; idx < tabStop; idx++ ) System.out.print(" "); |
| System.out.println( e ); |
| if ( sHasOutputDirectory ) { |
| if ( !curDirectory.exists() ) { |
| if ( false == curDirectory.mkdirs() ) { |
| throw new IOException("Unable to create directory: " + curDirectory); |
| } |
| } |
| File curFile = new File(curDirectory, e.getName()); |
| if ( curFile.exists() ) { |
| throw new IOException("File exists: " + curFile); |
| } else { |
| FsFile f = e.getFile(); |
| FileOutputStream fos = null; |
| try { |
| fos = new FileOutputStream(curFile); |
| FileChannel outputChannel = fos.getChannel(); |
| int capacity = sTempBuf.capacity(); |
| long length = f.getLength(); |
| for ( long pos = 0; pos < length; pos++ ) { |
| int readLength = (int)(length-pos > capacity ? capacity : length-pos); |
| sTempBuf.rewind(); |
| sTempBuf.limit(readLength); |
| f.read(pos, sTempBuf); |
| sTempBuf.rewind(); |
| while(sTempBuf.remaining() > 0) |
| outputChannel.write(sTempBuf); |
| pos += readLength; |
| } |
| } finally { |
| if ( null != fos ) fos.close(); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param args |
| */ |
| public static void main(String[] args) { |
| boolean displayHelp = false; |
| boolean isEncrypted = false; |
| try { |
| for (int i = 0; i < args.length; i++) { |
| String curArg = args[i]; |
| if (curArg.equals("-d")) { |
| sDirectory = args[++i]; |
| sDirectoryFile = new File(sDirectory); |
| sHasOutputDirectory = true; |
| } else if (curArg.equals("-k")) { |
| isEncrypted = true; |
| sKey = args[++i]; |
| } else if (curArg.equals("-o")) { |
| sOutputFile = args[++i]; |
| } else if (curArg.equals("-h")) { |
| displayHelp = true; |
| } else if (curArg.equals("-v")) { |
| sVerboseMode = true; |
| } else if (curArg.equals("-pn")) { |
| sPackageName = args[++i]; |
| } else if (curArg.equals("-pv")) { |
| sPackageVersion = Integer.parseInt(args[++i]); |
| } else if (curArg.equals("-ov")) { |
| sFlags |= ObbFile.OBB_OVERLAY; |
| } else if (curArg.equals("-dump")) { |
| sInputFile = args[++i]; |
| } else if (curArg.equals("-salt")) { |
| // special case --- use same salt |
| String saltString = args[++i]; |
| BigInteger bi = new BigInteger(saltString, 16); |
| sSalt = bi.toByteArray(); |
| if ( sSalt.length != PBKDF.SALT_LEN ) { |
| displayHelp = true; |
| } |
| } else if (curArg.equals("-about")) { |
| System.out.println("-------------------------------------------------------------------------------"); |
| System.out.println("Portions of this code:"); |
| System.out.println("-------------------------------------------------------------------------------"); |
| System.out.println("Copyright (c) 2000 The Legion Of The Bouncy Castle"); |
| System.out.println("(http://www.bouncycastle.org)"); |
| System.out.println(); |
| System.out.println("Permission is hereby granted, free of charge, to any person obtaining"); |
| System.out.println("a copy of this software and associated documentation files (the \"Software\""); |
| System.out.println("to deal in the Software without restriction, including without limitation"); |
| System.out.println("the rights to use, copy, modify, merge, publish, distribute, sublicense"); |
| System.out.println("and/or sell copies of the Software, and to permit persons to whom the Software"); |
| System.out.println("is furnished to do so, subject to the following conditions:"); |
| System.out.println(); |
| System.out.println("The above copyright notice and this permission notice shall be included in all"); |
| System.out.println("copies or substantial portions of the Software."); |
| System.out.println("-------------------------------------------------------------------------------"); |
| System.out.println("Twofish is uncopyrighted and license-free, and was created and analyzed by:"); |
| System.out.println("Bruce Schneier - John Kelsey - Doug Whiting"); |
| System.out.println("David Wagner - Chris Hall - Niels Ferguson"); |
| System.out.println("-------------------------------------------------------------------------------"); |
| System.out.println("Cryptix General License"); |
| System.out.println(); |
| System.out.println("Copyright (c) 1995-2005 The Cryptix Foundation Limited."); |
| System.out.println("All rights reserved."); |
| System.out.println(""); |
| System.out.println("Redistribution and use in source and binary forms, with or without"); |
| System.out.println("modification, are permitted provided that the following conditions are"); |
| System.out.println("met:"); |
| System.out.println(); |
| System.out.println(" 1. Redistributions of source code must retain the copyright notice,"); |
| System.out.println(" this list of conditions and the following disclaimer."); |
| System.out.println(" 2. Redistributions in binary form must reproduce the above copyright"); |
| System.out.println(" notice, this list of conditions and the following disclaimer in"); |
| System.out.println(" the documentation and/or other materials provided with the"); |
| System.out.println(" distribution."); |
| System.out.println("-------------------------------------------------------------------------------"); |
| return; |
| } |
| } |
| } catch (ArrayIndexOutOfBoundsException e) { |
| displayHelp = true; |
| } |
| if (null != sInputFile) { |
| ObbFile obbFile = new ObbFile(); |
| obbFile.readFrom(sInputFile); |
| System.out.print("Package Name: "); |
| System.out.println(obbFile.mPackageName); |
| System.out.print("Package Version: "); |
| System.out.println(obbFile.mPackageVersion); |
| if (0 != (obbFile.mFlags & ObbFile.OBB_SALTED)) { |
| System.out.print("SALT: "); |
| BigInteger bi = new BigInteger(obbFile.mSalt); |
| System.out.println(bi.toString(16)); |
| System.out.println(); |
| if ( null == sKey ) { |
| System.out.println("Encrypted file. Please add password."); |
| return; |
| } |
| try { |
| sFishKey = PBKDF.getKey(sKey,obbFile.mSalt); |
| bi = new BigInteger(sFishKey); |
| System.out.println(bi.toString(16)); |
| } catch (InvalidKeyException e) { |
| e.printStackTrace(); |
| } catch (NoSuchAlgorithmException e) { |
| e.printStackTrace(); |
| } catch (UnsupportedEncodingException e) { |
| e.printStackTrace(); |
| } |
| isEncrypted = true; |
| } else { |
| isEncrypted = false; |
| } |
| File obbInputFile = new File(sInputFile); |
| |
| BlockDevice fd; |
| try { |
| if ( isEncrypted ) { |
| EncryptedBlockFile ebf = new EncryptedBlockFile(sFishKey, obbInputFile, "r"); |
| fd = new FileDisk(ebf, ebf.getEncryptedFileChannel(), true); |
| } else { |
| fd = new FileDisk(obbInputFile, true); |
| } |
| final FatFileSystem fatFs = FatFileSystem.read(fd, true); |
| final BootSector bs = fatFs.getBootSector(); |
| final FsDirectory rootDir = fatFs.getRoot(); |
| if (sVerboseMode) { |
| System.out.print("Filesystem Type: "); |
| FatType ft = bs.getFatType(); |
| if (ft == FatType.FAT32) { |
| System.out.println("FAT32"); |
| } else if (ft == FatType.FAT16) { |
| System.out.println("FAT16"); |
| } else if (ft == FatType.FAT12) { |
| System.out.println("FAT12"); |
| } else { |
| System.out.println("Unknown"); |
| } |
| System.out.print(" OEM Name: "); |
| System.out.println(bs.getOemName()); |
| System.out.print(" Bytes Per Sector: "); |
| System.out.println(bs.getBytesPerSector()); |
| System.out.print("Sectors per cluster: "); |
| System.out.println(bs.getSectorsPerCluster()); |
| System.out.print(" Reserved Sectors: "); |
| System.out.println(bs.getNrReservedSectors()); |
| System.out.print(" Fats: "); |
| System.out.println(bs.getNrFats()); |
| System.out.print(" Root Dir Entries: "); |
| System.out.println(bs.getRootDirEntryCount()); |
| System.out.print(" Medium Descriptor: "); |
| System.out.println(bs.getMediumDescriptor()); |
| System.out.print(" Sectors: "); |
| System.out.println(bs.getSectorCount()); |
| System.out.print(" Sectors Per Fat: "); |
| System.out.println(bs.getSectorsPerFat()); |
| System.out.print(" Heads: "); |
| System.out.println(bs.getNrHeads()); |
| System.out.print(" Hidden Sectors: "); |
| System.out.println(bs.getNrHiddenSectors()); |
| System.out.print(" Fat Offset: "); |
| System.out.println(FatUtils.getFatOffset(bs, 0)); |
| System.out.println(" RootDir: " + rootDir); |
| } |
| dumpDirectory(rootDir, 0, sDirectoryFile); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } catch (InvalidKeyException e) { |
| e.printStackTrace(); |
| } |
| return; |
| } |
| boolean printArgs; |
| if (displayHelp) { |
| printArgs = true; |
| } else if ( null == sDirectory ) { |
| printArgs = true; |
| System.out.println("A directory to be recursed through [-d directory] is required when creating an OBB filesystem."); |
| } else if ( null == sOutputFile ) { |
| printArgs = true; |
| System.out.println("An output filename [-o outputfile] is required when creating an OBB filesystem."); |
| } else if ( null == sPackageName ) { |
| printArgs = true; |
| System.out.println("A package name [-pn package] is required when creating an OBB filesystem."); |
| } else if ( -1 == sPackageVersion ) { |
| printArgs = true; |
| System.out.println("A package version [-pv package] is required when creating an OBB filesystem."); |
| } else { |
| printArgs = false; |
| } |
| if (printArgs) { |
| printArgs(); |
| } else { |
| if (null != sKey) { |
| if ( null == sSalt ) { |
| sSalt = PBKDF.getRandomSalt(); |
| } |
| try { |
| sFishKey = PBKDF.getKey(sKey, sSalt); |
| } catch (Exception e) { |
| e.printStackTrace(); |
| return; |
| } |
| sFlags |= ObbFile.OBB_SALTED; |
| if (sVerboseMode) { |
| System.out.println("Crypto: "); |
| System.out.print("SALT: "); |
| for (byte b : sSalt) { |
| int out = b & 0xFF; |
| System.out.print(Integer.toHexString(out)); |
| System.out.print(" "); |
| } |
| System.out.print("\t"); |
| System.out.print("KEY: "); |
| for (byte b : sFishKey) { |
| int out = b & 0xFF; |
| System.out.print(Integer.toHexString(out)); |
| System.out.print(" "); |
| } |
| System.out.println(); |
| } |
| } |
| if (sVerboseMode) { |
| System.out.println("Scanning directory: " + sDirectory); |
| } |
| final File f = new File(sDirectory); |
| |
| long fileSize = getTotalFileSize(f, 0); |
| fileSize = getTotalFileSize(f, BLOCK_SIZE*SuperFloppyFormatter.clusterSizeFromSize(fileSize, BLOCK_SIZE)); |
| if (sVerboseMode) { |
| System.out.println("Total Files: " + fileSize); |
| } |
| long numSectors = fileSize / BLOCK_SIZE; |
| long clusterSize = SuperFloppyFormatter.clusterSizeFromSize(fileSize, BLOCK_SIZE); |
| long fatOverhead = 2*numSectors/clusterSize*4; |
| fatOverhead += clusterSize*BLOCK_SIZE - fatOverhead % (clusterSize*BLOCK_SIZE); |
| fatOverhead += 2*clusterSize; //start at second cluster |
| if (sVerboseMode) { |
| System.out.println("FAT Overhead: " + fatOverhead); |
| } |
| long clusterSizeInBytes = clusterSize * BLOCK_SIZE; |
| long filesystemSize = (( numSectors ) * BLOCK_SIZE + fatOverhead + clusterSizeInBytes -1 ) / clusterSizeInBytes * clusterSizeInBytes; |
| if (sVerboseMode) { |
| System.out.println("Filesystem Size: " + filesystemSize); |
| } |
| File fsFile = new File(sOutputFile); |
| if (fsFile.exists()) |
| fsFile.delete(); |
| try { |
| BlockDevice fd; |
| if ( isEncrypted ) { |
| try { |
| EncryptedBlockFile ebf = new EncryptedBlockFile(sFishKey, fsFile, "rw"); |
| ebf.setLength(filesystemSize); |
| fd = new FileDisk(ebf, ebf.getEncryptedFileChannel(), false); |
| } catch (InvalidKeyException e) { |
| e.printStackTrace(); |
| return; |
| } |
| } else { |
| fd = FileDisk.create(fsFile, filesystemSize); |
| } |
| // fat type set based on device size by SuperFloppyFormatter |
| final FatFileSystem fs = SuperFloppyFormatter.get(fd).format(); |
| final String rootPath = f.getAbsolutePath(); |
| // add the files into the filesystem |
| processAllFiles(f, new FileProcessor() { |
| Stack<FatLfnDirectory> mCurDir = new Stack<FatLfnDirectory>(); |
| |
| @Override |
| public void processDirectory(File curFile) { |
| String directory = curFile.getAbsolutePath().substring(rootPath.length()); |
| if (sVerboseMode) { |
| System.out.println("Processing Directory: " + directory + " at cluster " + fs.getFat().getLastFreeCluster()); |
| } |
| FatLfnDirectory curDir = fs.getRoot(); |
| if (directory.length() > 0) { |
| File tempFile = new File(directory); |
| Stack<String> pathStack = new Stack<String>(); |
| do { |
| pathStack.push(tempFile.getName()); |
| } while (null != (tempFile = tempFile.getParentFile())); |
| while (!pathStack.empty()) { |
| String name = pathStack.pop(); |
| if (0 == name.length()) |
| continue; |
| FatLfnDirectoryEntry entry = curDir.getEntry(name); |
| if (null != entry) { |
| if (!entry.isDirectory()) { |
| throw new RuntimeException( |
| "File path not FAT compatible - naming conflict!"); |
| } |
| } else { |
| try { |
| if (sVerboseMode) { |
| System.out.println("Adding Directory: " + name); |
| } |
| entry = curDir.addDirectory(name); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| throw new RuntimeException("Error adding directory!"); |
| } |
| } |
| try { |
| curDir = entry.getDirectory(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| throw new RuntimeException("Error getting directory"); |
| } |
| } |
| } |
| mCurDir.push(curDir); |
| } |
| |
| @Override |
| public void processFile(File curFile) { |
| FatLfnDirectoryEntry entry; |
| FatLfnDirectory curDir = mCurDir.peek(); |
| try { |
| if (sVerboseMode) { |
| System.out.println("Adding file: " |
| + curFile.getAbsolutePath().substring(rootPath.length()) |
| + " with length " + curFile.length() + " at cluster " + fs.getFat().getLastFreeCluster()); |
| } |
| entry = curDir.addFile(curFile.getName()); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| throw new RuntimeException("Error adding file with name: " |
| + curFile.getName()); |
| } |
| ReadableByteChannel channel = null; |
| try { |
| FatFile f = entry.getFile(); |
| channel = new FileInputStream(curFile).getChannel(); |
| ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 512); |
| int numRead = 0; |
| long offset = 0; |
| while (true) { |
| buf.clear(); |
| numRead = channel.read(buf); |
| if (numRead < 0) |
| break; |
| buf.rewind(); |
| buf.limit(numRead); |
| f.write(offset, buf); |
| offset += numRead; |
| } |
| f.flush(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| throw new RuntimeException("Error getting/writing file with name: " |
| + curFile.getName()); |
| } finally { |
| if (null != channel) |
| try { |
| channel.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| @Override |
| public void endDirectory(File dir) { |
| mCurDir.pop(); |
| } |
| |
| }); |
| fs.flush(); |
| fs.close(); |
| Fat fat = fs.getFat(); |
| ObbFile ob = new ObbFile(); |
| ob.setPackageName(sPackageName); |
| ob.setPackageVersion(sPackageVersion); |
| ob.setFlags(sFlags); |
| if (null != sSalt) { |
| ob.setSalt(sSalt); |
| } |
| ob.writeTo(fsFile); |
| if (sVerboseMode) { |
| System.out.println("Success!"); |
| System.out.println("" + fs.getTotalSpace() + " bytes total"); |
| System.out.println("" + fs.getFreeSpace() + " bytes free"); |
| } |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| public static long getTotalFileSize(File dir, final int clusterSize) { |
| final long[] mSize = new long[3]; |
| final boolean calculateSlop = clusterSize > 0; |
| processAllFiles(dir, new FileProcessor() { |
| Stack<int[]> mDirLen = new Stack<int[]>(); |
| |
| @Override |
| public void processFile(File f) { |
| if (sVerboseMode) { |
| System.out.println("Adding size for file: " + f.getAbsolutePath()); |
| } |
| long length = f.length(); |
| if ( calculateSlop && length > 0 ) { |
| int[] dirLen = mDirLen.peek(); |
| long realLength = ((clusterSize-1)+length) / clusterSize*clusterSize; |
| long slop = realLength-length; |
| length += slop; |
| mSize[0] += length; |
| mSize[1] += slop; |
| dirLen[0] += f.getName().length()/13+3; |
| } else { |
| mSize[0] += length; |
| } |
| } |
| |
| @Override |
| public void processDirectory(File f) { |
| if ( calculateSlop ) { |
| int[] dirLen = new int[1]; |
| dirLen[0] += f.getName().length()/13+4; |
| mDirLen.push(dirLen); |
| } |
| } |
| |
| @Override |
| public void endDirectory(File dir) { |
| if ( calculateSlop ) { |
| int[] dirLen = mDirLen.pop(); |
| long lastDirLen = dirLen[0] * 32; |
| if ( lastDirLen != 0 ) { |
| long realLength = ((clusterSize-1)+lastDirLen) / clusterSize*clusterSize; |
| long slop = realLength-lastDirLen; |
| mSize[0] += lastDirLen + slop; |
| mSize[1] += slop; |
| mSize[2] += lastDirLen; |
| } |
| } |
| } |
| }); |
| System.out.println("Slop: " + mSize[1] + " Directory Overhead: " + mSize[2] ); |
| return mSize[0]; |
| } |
| |
| // Process all files and directories under dir |
| public static void processAllFiles(File dir, FileProcessor fp) { |
| if (dir.isDirectory()) { |
| fp.processDirectory(dir); |
| String[] children = dir.list(); |
| for (int i = 0; i < children.length; i++) { |
| processAllFiles(new File(dir, children[i]), fp); |
| } |
| fp.endDirectory(dir); |
| } else { |
| fp.processFile(dir); |
| } |
| } |
| } |