blob: 8339710e04c88e66746c5ad430ff715e947399f1 [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
// OWNER 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.mozc.android.inputmethod.japanese.util;
import org.mozc.android.inputmethod.japanese.MozcUtil;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.channels.ReadableByteChannel;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* This class is used to access the flatten image in a zip file via {@link ByteBuffer}.
*
* If the target entry is compressed, this class allocates required memory block and
* uncompresses the entry into the memory block. Therefore {@link #getBuffer(ZipFile, String)}
* method blocks a moment.
* If the target entry in "uncompressed", this class returns {@link MappedByteBuffer}
* of which back-end is the uncompressed target entry.
* In this case {@link #getBuffer(ZipFile, String)} blocks very little.
*
* We implemented this class instead of using ZipFile class because
* ZipFile lacks the API which can return the offset value of each entry.
* Such API is mandatory to implement the behavior for uncompressed entry described above.
*
*/
public class ZipFileUtil {
// Following constants come from ZipFile class.
// Android framework's ZipFile class lacks these constants so we define them by ourselves.
private static final long LOCSIG = 0x0000000004034b50;
private static final int LOCNAM = 26;
private static final int LOCEXT = 28;
private static final int LOCHDR = 30;
private static final long CENSIG = 0x0000000002014b50;
private static final int CENSIZ = 20;
private static final int CENNAM = 28;
private static final int CENEXT = 30;
private static final int CENCOM = 32;
private static final int CENOFF = 42;
private static final int CENHDR = 46;
private static final long ENDSIG = 0x0000000006054b50;
private static final int ENDOFF = 16;
private static final int ENDHDR = 22;
static class RandomAccessFileUtility {
private RandomAccessFileUtility() {
// Explicitly throw an exception because "private constructor" idiom cannot
// be applicable here.
throw new IllegalAccessError("This class should not instantiated.");
}
static short readPositiveShort(RandomAccessFile file, long offset) throws IOException {
file.seek(offset);
short result = Short.reverseBytes(file.readShort());
if (result < 0) {
throw new IOException(
"Unexpected negative value ; offset = " + offset + ", value = " + result);
}
return result;
}
static int readPositiveInt(RandomAccessFile file, long offset) throws IOException {
int result = readInt(file, offset);
if (result < 0) {
throw new IOException(
"Unexpected negative value ; offset = " + offset + ", value = " + result);
}
return result;
}
static int readInt(RandomAccessFile file, long offset) throws IOException {
file.seek(offset);
return Integer.reverseBytes(file.readInt());
}
static void readBytes(RandomAccessFile file, long offset, byte[] result, int length)
throws IOException {
file.seek(offset);
file.read(result, 0, length);
}
}
/**
* See http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* I. End of central directory record
*/
private static class EndOfCentralDirectory {
private final RandomAccessFile file;
EndOfCentralDirectory(RandomAccessFile file) throws IOException {
this.file = file;
// Check the signature.
if (RandomAccessFileUtility.readInt(file, file.length() - ENDHDR) != ENDSIG) {
throw new IOException("Invalid ENDHDR signature at " + (file.length() - ENDHDR));
}
}
CentralDirectory getCentralDirectory() throws IOException {
// NOTE : Expects that there is no zip-file-comment at the tail of the file
// becase aapt tool does not create such file which has "zip-file-comment" entry.
int centralDirectoryOffset =
RandomAccessFileUtility.readPositiveInt(file, file.length() - ENDHDR + ENDOFF);
return new CentralDirectory(file, centralDirectoryOffset);
}
}
/**
* See http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* F. Central directory structure
*/
private static class CentralDirectory {
private final RandomAccessFile file;
private final int centralDirectoryOffset;
// The length is intentionally 256 (not 2^15) in order to prevent from creating large array.
private static final int MAX_FILE_NAME_LENGTH = 256;
CentralDirectory(RandomAccessFile file, int offset) {
this.file = file;
this.centralDirectoryOffset = offset;
}
LocalDirectory getLocalDirectory(String fileName) throws IOException {
long offset = this.centralDirectoryOffset;
byte[] temp = new byte[MAX_FILE_NAME_LENGTH];
while (true) {
// Check the signature.
if (RandomAccessFileUtility.readInt(file, offset) != CENSIG) {
throw new IOException("Invalid CENSIG signature at " + offset);
}
int fileNameLength = RandomAccessFileUtility.readPositiveShort(file, offset + CENNAM);
if (fileNameLength > MAX_FILE_NAME_LENGTH) {
throw new IOException("Too long file name length ;" + fileNameLength);
}
RandomAccessFileUtility.readBytes(file, offset + CENHDR, temp, fileNameLength);
if (new String(temp, 0, fileNameLength, "UTF-8").equals(fileName)) {
int localOffset = RandomAccessFileUtility.readPositiveInt(file, offset + CENOFF);
int rawLength = RandomAccessFileUtility.readPositiveInt(file, offset + CENSIZ);
return new LocalDirectory(file, localOffset, rawLength);
} else {
int extLength = RandomAccessFileUtility.readPositiveShort(file, offset + CENEXT);
int commentLength = RandomAccessFileUtility.readPositiveShort(file, offset + CENCOM);
offset += CENHDR + fileNameLength + extLength + commentLength;
}
}
}
}
/**
* See http://www.pkware.com/documents/casestudies/APPNOTE.TXT
* A. Local file header
*/
private static class LocalDirectory {
private final RandomAccessFile file;
private final long offset;
private final int size;
LocalDirectory(RandomAccessFile file, long offset, int size) throws IOException {
this.file = file;
this.offset = offset;
this.size = size;
// Check the signature.
if (RandomAccessFileUtility.readInt(file, offset) != LOCSIG) {
throw new IOException("Invalid LOCSIG signature at " + offset);
}
}
MappedByteBuffer getReadOnlyMappedByteBuffer() throws IOException {
int fileNameLength = RandomAccessFileUtility.readPositiveShort(file, offset + LOCNAM);
int extLength = RandomAccessFileUtility.readPositiveShort(file, offset + LOCEXT);
FileChannel channel = file.getChannel();
boolean succeeded = false;
try {
MappedByteBuffer result =
channel.map(MapMode.READ_ONLY,
offset + LOCHDR + fileNameLength + extLength, size);
succeeded = true;
return result;
} finally {
// MappedByteBuffer is valid even after back-end Channel and/or RandomAccessFile
// is/are closed (documented spec).
MozcUtil.close(channel, !succeeded);
}
}
}
/**
* Gets a {@link ByteBuffer} to the entry's content.
*
* If the entry is compressed, returned buffer contains uncompressed data.
* See the class's document.
*
* @param zipFile the ZipFile to use
* @param fileName the file name to access
* @return the ByteBuffer
* @throws IOException
*/
public static ByteBuffer getBuffer(ZipFile zipFile, String fileName) throws IOException {
ZipEntry zipEntry = zipFile.getEntry(fileName);
if (zipEntry == null) {
throw new IOException(fileName + " is not found.");
}
switch (zipEntry.getMethod()) {
case ZipEntry.DEFLATED:
return getBufferDeflated(zipFile, zipEntry);
case ZipEntry.STORED:
return getBufferStored(zipFile, zipEntry);
default:
throw new IOException("Unsuppoerted storing method.");
}
}
private static ByteBuffer getBufferStored(ZipFile zipFile, ZipEntry entry) throws IOException {
RandomAccessFile apkFile = new RandomAccessFile(zipFile.getName(), "r");
boolean succeeded = false;
try {
CentralDirectory centralDirectory = new EndOfCentralDirectory(apkFile).getCentralDirectory();
LocalDirectory localDirectory = centralDirectory.getLocalDirectory(entry.getName());
ByteBuffer result = localDirectory.getReadOnlyMappedByteBuffer();
succeeded = true;
return result;
} finally {
// MappedByteBuffer is valid even after back-end Channel and/or RandomAccessFile
// is/are closed (documented spec).
MozcUtil.close(apkFile, !succeeded);
}
}
private static ByteBuffer getBufferDeflated(ZipFile zipFile, ZipEntry entry) throws IOException {
int size = (int) entry.getSize();
ByteBuffer buffer = ByteBuffer.allocateDirect(size);
ReadableByteChannel inputChannel = Channels.newChannel(zipFile.getInputStream(entry));
boolean succeeded = false;
try {
while (buffer.position() != size) {
inputChannel.read(buffer);
}
succeeded = true;
} finally {
MozcUtil.close(inputChannel, !succeeded);
}
return buffer;
}
}