diff --git a/src/main/java/com/reandroid/archive2/Archive.java b/src/main/java/com/reandroid/archive2/Archive.java new file mode 100644 index 0000000..966cfc4 --- /dev/null +++ b/src/main/java/com/reandroid/archive2/Archive.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2; + +import com.reandroid.archive2.block.CentralEntryHeader; +import com.reandroid.archive2.block.EndRecord; +import com.reandroid.archive2.block.LocalFileHeader; +import com.reandroid.archive2.io.ArchiveFile; +import com.reandroid.archive2.io.ArchiveUtil; +import com.reandroid.archive2.io.ZipSource; +import com.reandroid.archive2.model.LocalFileDirectory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; + +public class Archive { + private final ZipSource zipSource; + private final List entryList; + private final EndRecord endRecord; + public Archive(ZipSource zipSource) throws IOException { + this.zipSource = zipSource; + LocalFileDirectory lfd = new LocalFileDirectory(); + lfd.visit(zipSource); + List localFileHeaderList = lfd.getHeaderList(); + List centralEntryHeaderList = lfd.getCentralFileDirectory().getHeaderList(); + List entryList = new ArrayList<>(); + for(int i=0;i getEntryList() { + return entryList; + } + + // for test + public void extract(File dir) throws IOException { + for(ArchiveEntry archiveEntry:getEntryList()){ + if(archiveEntry.isDirectory()){ + continue; + } + extract(dir, archiveEntry); + } + } + private void extract(File dir, ArchiveEntry archiveEntry) throws IOException{ + File out = toFile(dir, archiveEntry); + File parent = out.getParentFile(); + if(!parent.exists()){ + parent.mkdirs(); + } + FileOutputStream outputStream = new FileOutputStream(out); + ArchiveUtil.writeAll(openInputStream(archiveEntry), outputStream); + outputStream.close(); + } + private File toFile(File dir, ArchiveEntry archiveEntry){ + String name = archiveEntry.getName().replace('/', File.separatorChar); + return new File(dir, name); + } +} diff --git a/src/main/java/com/reandroid/archive2/ArchiveEntry.java b/src/main/java/com/reandroid/archive2/ArchiveEntry.java new file mode 100644 index 0000000..628a873 --- /dev/null +++ b/src/main/java/com/reandroid/archive2/ArchiveEntry.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2; + +import com.reandroid.archive2.block.CentralEntryHeader; +import com.reandroid.archive2.block.LocalFileHeader; + +import java.util.zip.ZipEntry; + +public class ArchiveEntry extends ZipEntry { + private final CentralEntryHeader centralEntryHeader; + private final LocalFileHeader localFileHeader; + public ArchiveEntry(LocalFileHeader lfh, CentralEntryHeader ceh){ + super(lfh.getFileName()); + this.localFileHeader = lfh; + this.centralEntryHeader = ceh; + } + public ArchiveEntry(String name){ + this(new LocalFileHeader(name), new CentralEntryHeader(name)); + } + public ArchiveEntry(){ + this(new LocalFileHeader(), new CentralEntryHeader()); + } + + public long getDataSize(){ + if(getMethod() == ZipEntry.STORED){ + return getSize(); + } + return getCompressedSize(); + } + + @Override + public int getMethod(){ + return localFileHeader.getMethod(); + } + @Override + public void setMethod(int method){ + localFileHeader.setMethod(method); + centralEntryHeader.setMethod(method); + } + @Override + public long getSize() { + return localFileHeader.getSize(); + } + @Override + public void setSize(long size) { + centralEntryHeader.setSize(size); + localFileHeader.setSize(size); + } + @Override + public long getCrc() { + return localFileHeader.getCrc(); + } + @Override + public void setCrc(long crc) { + centralEntryHeader.setCrc(crc); + localFileHeader.setCrc(crc); + } + @Override + public long getCompressedSize() { + return localFileHeader.getCompressedSize(); + } + @Override + public void setCompressedSize(long csize) { + centralEntryHeader.setCompressedSize(csize); + localFileHeader.setCompressedSize(csize); + } + public long getFileOffset() { + return localFileHeader.getFileOffset(); + } + @Override + public String getName(){ + return localFileHeader.getFileName(); + } + public void setName(String name){ + centralEntryHeader.setFileName(name); + localFileHeader.setFileName(name); + } + @Override + public String getComment(){ + return centralEntryHeader.getComment(); + } + @Override + public void setComment(String name){ + centralEntryHeader.setComment(name); + } + @Override + public boolean isDirectory() { + return this.getName().endsWith("/"); + } + public CentralEntryHeader getCentralEntryHeader(){ + return centralEntryHeader; + } + public LocalFileHeader getLocalFileHeader() { + return localFileHeader; + } + public boolean matches(CentralEntryHeader centralEntryHeader){ + if(centralEntryHeader==null){ + return false; + } + return false; + } + + @Override + public String toString(){ + return "["+ getFileOffset()+"] "+getName()+getComment()+String.format(" 0x%08x", getCrc()); + } +} diff --git a/src/main/java/com/reandroid/archive2/ZipSignature.java b/src/main/java/com/reandroid/archive2/ZipSignature.java new file mode 100644 index 0000000..c9526dd --- /dev/null +++ b/src/main/java/com/reandroid/archive2/ZipSignature.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2; + +public enum ZipSignature { + CENTRAL_FILE(0X02014B50), + LOCAL_FILE(0X04034B50), + DATA_DESCRIPTOR(0X08074B50), + END_RECORD(0X06054B50); + + private final int value; + + ZipSignature(int value){ + this.value = value; + } + public int getValue() { + return value; + } + public static ZipSignature valueOf(int value){ + for(ZipSignature signature:VALUES){ + if(value == signature.getValue()){ + return signature; + } + } + return null; + } + private static final ZipSignature[] VALUES = values(); +} diff --git a/src/main/java/com/reandroid/archive2/block/CentralEntryHeader.java b/src/main/java/com/reandroid/archive2/block/CentralEntryHeader.java new file mode 100644 index 0000000..16bc36b --- /dev/null +++ b/src/main/java/com/reandroid/archive2/block/CentralEntryHeader.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.block; + +import com.reandroid.archive2.ZipSignature; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +public class CentralEntryHeader extends CommonHeader { + private String mComment; + public CentralEntryHeader(){ + super(OFFSET_fileName, ZipSignature.CENTRAL_FILE, OFFSET_general_purpose); + } + public CentralEntryHeader(String name){ + this(); + setFileName(name); + } + + @Override + int readComment(InputStream inputStream) throws IOException { + int commentLength = getCommentLength(); + if(commentLength==0){ + mComment = ""; + return 0; + } + setCommentLength(commentLength); + byte[] bytes = getBytesInternal(); + int read = inputStream.read(bytes, getOffsetComment(), commentLength); + if(read != commentLength){ + throw new IOException("Stream ended before reading comment: read=" + +read+", name length="+commentLength); + } + mComment = null; + return commentLength; + } + + public int getVersionExtract(){ + return getShortUnsigned(OFFSET_versionExtract); + } + public void setVersionExtract(int value){ + putShort(OFFSET_versionExtract, value); + } + public String getComment(){ + if(mComment == null){ + mComment = decodeComment(); + } + return mComment; + } + public void setComment(String comment){ + if(comment==null){ + comment=""; + } + byte[] strBytes = ZipStringEncoding.encodeString(isUtf8(), comment); + int length = strBytes.length; + setCommentLength(length); + if(length==0){ + mComment = comment; + return; + } + byte[] bytes = getBytesInternal(); + System.arraycopy(strBytes, 0, bytes, getOffsetComment(), length); + mComment = comment; + } + + + @Override + public int getCommentLength(){ + return getShortUnsigned(OFFSET_commentLength); + } + public void setCommentLength(int value){ + int length = getOffsetComment() + value; + setBytesLength(length, false); + putShort(OFFSET_commentLength, value); + } + @Override + void onUtf8Changed(boolean oldValue){ + String str = mComment; + if(str != null){ + setComment(str); + } + } + + public boolean matches(LocalFileHeader localFileHeader){ + if(localFileHeader==null){ + return false; + } + return getCrc() == localFileHeader.getCrc() + && Objects.equals(getFileName(), localFileHeader.getFileName()); + } + + @Override + public String toString(){ + if(countBytes()0){ + builder.append("name=").append(str); + appendOnce = true; + } + str = getComment(); + if(str.length()>0){ + if(appendOnce){ + builder.append(", "); + } + builder.append("comment=").append(str); + appendOnce = true; + } + if(appendOnce){ + builder.append(", "); + } + builder.append("SIG=").append(getSignature()); + builder.append(", versionMadeBy=").append(String.format("0x%04x", getVersionMadeBy())); + builder.append(", versionExtract=").append(String.format("0x%04x", getVersionExtract())); + builder.append(", GP={").append(getGeneralPurposeFlag()).append("}"); + builder.append(", method=").append(getMethod()); + builder.append(", date=").append(getDate()); + builder.append(", crc=").append(String.format("0x%08x", getCrc())); + builder.append(", cSize=").append(getCompressedSize()); + builder.append(", size=").append(getSize()); + builder.append(", fileNameLength=").append(getFileNameLength()); + builder.append(", extraLength=").append(getExtraLength()); + builder.append(", commentLength=").append(getCommentLength()); + return builder.toString(); + } + + + public static CentralEntryHeader fromLocalFileHeader(LocalFileHeader lfh){ + CentralEntryHeader ceh = new CentralEntryHeader(); + ceh.setSignature(ZipSignature.CENTRAL_FILE); + ceh.setVersionMadeBy(lfh.getVersionMadeBy()); + ceh.getGeneralPurposeFlag().setValue(lfh.getGeneralPurposeFlag().getValue()); + ceh.setMethod(lfh.getMethod()); + ceh.setDosTime(lfh.getDosTime()); + ceh.setCrc(lfh.getCrc()); + ceh.setCompressedSize(lfh.getCompressedSize()); + ceh.setSize(lfh.getSize()); + ceh.setFileName(lfh.getFileName()); + ceh.setExtra(lfh.getExtra()); + return ceh; + } + private static final int OFFSET_signature = 0; + private static final int OFFSET_versionMadeBy = 4; + private static final int OFFSET_versionExtract = 6; + private static final int OFFSET_general_purpose = 8; + private static final int OFFSET_method = 10; + private static final int OFFSET_dos_time = 12; + private static final int OFFSET_dos_date = 14; + private static final int OFFSET_crc = 16; + private static final int OFFSET_compressed_size = 20; + private static final int OFFSET_size = 24; + private static final int OFFSET_fileNameLength = 28; + private static final int OFFSET_extraLength = 30; + private static final int OFFSET_commentLength = 32; + private static final int OFFSET_diskStart = 34; + private static final int OFFSET_internalFileAttributes = 36; + private static final int OFFSET_externalFileAttributes = 38; + private static final int OFFSET_localRelativeOffset = 42; + private static final int OFFSET_fileName = 46; + +} diff --git a/src/main/java/com/reandroid/archive2/block/CommonHeader.java b/src/main/java/com/reandroid/archive2/block/CommonHeader.java new file mode 100644 index 0000000..6129bce --- /dev/null +++ b/src/main/java/com/reandroid/archive2/block/CommonHeader.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.block; + +import com.reandroid.archive2.ZipSignature; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.zip.ZipEntry; + +public abstract class CommonHeader extends ZipHeader { + private final int offsetFileName; + private final int offsetGeneralPurpose; + private final GeneralPurposeFlag generalPurposeFlag; + private String mFileName; + private long mFileOffset; + public CommonHeader(int offsetFileName, ZipSignature expectedSignature, int offsetGeneralPurpose){ + super(offsetFileName, expectedSignature); + this.offsetFileName = offsetFileName; + this.offsetGeneralPurpose = offsetGeneralPurpose; + this.generalPurposeFlag = new GeneralPurposeFlag(this, offsetGeneralPurpose); + } + public long getFileOffset() { + return mFileOffset; + } + public void setFileOffset(long fileOffset){ + this.mFileOffset = fileOffset; + } + public long getDataSize(){ + if(getMethod() == ZipEntry.STORED){ + return getSize(); + } + return getCompressedSize(); + } + + @Override + int readNext(InputStream inputStream) throws IOException { + int read = 0; + read += readFileName(inputStream); + read += readExtra(inputStream); + read += readComment(inputStream); + mFileName = null; + return read; + } + private int readFileName(InputStream inputStream) throws IOException { + int fileNameLength = getFileNameLength(); + if(fileNameLength==0){ + mFileName = ""; + return 0; + } + setFileNameLength(fileNameLength); + byte[] bytes = getBytesInternal(); + int read = inputStream.read(bytes, offsetFileName, fileNameLength); + if(read != fileNameLength){ + throw new IOException("Stream ended before reading file name: read=" + +read+", name length="+fileNameLength); + } + mFileName = null; + return fileNameLength; + } + private int readExtra(InputStream inputStream) throws IOException { + int extraLength = getExtraLength(); + if(extraLength==0){ + return 0; + } + setExtraLength(extraLength); + byte[] bytes = getBytesInternal(); + int offset = getOffsetExtra(); + int read = inputStream.read(bytes, offset, extraLength); + if(read != extraLength){ + throw new IOException("Stream ended before reading extra bytes: read=" + + read +", extra length="+extraLength); + } + return extraLength; + } + int readComment(InputStream inputStream) throws IOException { + return 0; + } + public int getVersionMadeBy(){ + return getShortUnsigned(OFFSET_versionMadeBy); + } + public void setVersionMadeBy(int value){ + putShort(OFFSET_versionMadeBy, value); + } + public int getPlatform(){ + return getByteUnsigned(OFFSET_platform); + } + public void setPlatform(int value){ + getBytesInternal()[OFFSET_platform] = (byte) value; + } + public GeneralPurposeFlag getGeneralPurposeFlag() { + return generalPurposeFlag; + } + public int getMethod(){ + return getShortUnsigned(offsetGeneralPurpose + 2); + } + public void setMethod(int value){ + putShort(offsetGeneralPurpose + 2, value); + } + public long getDosTime(){ + return getUnsignedLong(offsetGeneralPurpose + 4); + } + public void setDosTime(long value){ + putInteger(offsetGeneralPurpose + 4, value); + } + public Date getDate(){ + return dosToJavaDate(getDosTime()); + } + public void setDate(Date date){ + setDate(date==null ? 0L : date.getTime()); + } + public void setDate(long date){ + setDosTime(javaToDosTime(date)); + } + public long getCrc(){ + return getUnsignedLong(offsetGeneralPurpose + 8); + } + public void setCrc(long value){ + putInteger(offsetGeneralPurpose + 8, value); + } + public long getCompressedSize(){ + return getUnsignedLong(offsetGeneralPurpose + 12); + } + public void setCompressedSize(long value){ + putInteger(offsetGeneralPurpose + 12, value); + } + public long getSize(){ + return getUnsignedLong(offsetGeneralPurpose + 16); + } + public void setSize(long value){ + putInteger(offsetGeneralPurpose + 16, value); + } + public int getFileNameLength(){ + return getShortUnsigned(offsetGeneralPurpose + 20); + } + private void setFileNameLength(int value){ + int length = offsetFileName + value + getExtraLength() + getCommentLength(); + super.setBytesLength(length, false); + putShort(offsetGeneralPurpose + 20, value); + } + public int getExtraLength(){ + return getShortUnsigned(offsetGeneralPurpose + 22); + } + public void setExtraLength(int value){ + int length = offsetFileName + getFileNameLength() + value + getCommentLength(); + super.setBytesLength(length, false); + putShort(offsetGeneralPurpose + 22, value); + } + public byte[] getExtra(){ + int length = getExtraLength(); + byte[] result = new byte[length]; + if(length==0){ + return result; + } + byte[] bytes = getBytesInternal(); + int offset = getOffsetExtra(); + System.arraycopy(bytes, offset, result, 0, length); + return result; + } + public void setExtra(byte[] extra){ + if(extra == null){ + extra = new byte[0]; + } + int length = extra.length; + setExtraLength(length); + if(length == 0){ + return; + } + putBytes(extra, 0, getOffsetExtra(), length); + } + public int getCommentLength(){ + return 0; + } + int getOffsetComment(){ + return offsetFileName + getFileNameLength() + getExtraLength(); + } + private int getOffsetExtra(){ + return offsetFileName + getFileNameLength(); + } + + public String getFileName(){ + if(mFileName == null){ + mFileName = decodeFileName(); + } + return mFileName; + } + public void setFileName(String fileName){ + if(fileName==null){ + fileName=""; + } + byte[] nameBytes; + if(getGeneralPurposeFlag().getUtf8()){ + nameBytes = fileName.getBytes(StandardCharsets.UTF_8); + }else { + nameBytes = fileName.getBytes(); + } + int length = nameBytes.length; + setFileNameLength(length); + if(length==0){ + mFileName = fileName; + return; + } + byte[] bytes = getBytesInternal(); + System.arraycopy(nameBytes, 0, bytes, offsetFileName, length); + mFileName = fileName; + } + public boolean isUtf8(){ + return getGeneralPurposeFlag().getUtf8(); + } + public boolean hasDataDescriptor(){ + return getGeneralPurposeFlag().hasDataDescriptor(); + } + private String decodeFileName(){ + int length = getFileNameLength(); + byte[] bytes = getBytesInternal(); + int offset = offsetFileName; + int max = bytes.length - offset; + if(max<=0){ + return ""; + } + if(length>max){ + length = max; + } + return ZipStringEncoding.decode(getGeneralPurposeFlag().getUtf8(), + getBytesInternal(), offset, length); + } + public String decodeComment(){ + int length = getExtraLength(); + byte[] bytes = getBytesInternal(); + int offset = getOffsetExtra(); + int max = bytes.length - offset; + if(max<=0){ + return ""; + } + if(length>max){ + length = max; + } + return ZipStringEncoding.decode(getGeneralPurposeFlag().getUtf8(), + getBytesInternal(), offset, length); + } + void onUtf8Changed(boolean oldValue){ + String str = mFileName; + if(str != null){ + setFileName(str); + } + } + + @Override + public String toString(){ + if(countBytes()0){ + builder.append("name=").append(str); + appendOnce = true; + } + if(appendOnce){ + builder.append(", "); + } + builder.append("SIG=").append(getSignature()); + builder.append(", versionMadeBy=").append(String.format("0x%04x", getVersionMadeBy())); + builder.append(", platform=").append(String.format("0x%02x", getPlatform())); + builder.append(", GP={").append(getGeneralPurposeFlag()).append("}"); + builder.append(", method=").append(getMethod()); + builder.append(", date=").append(getDate()); + builder.append(", crc=").append(String.format("0x%08x", getCrc())); + builder.append(", cSize=").append(getCompressedSize()); + builder.append(", size=").append(getSize()); + builder.append(", fileNameLength=").append(getFileNameLength()); + builder.append(", extraLength=").append(getExtraLength()); + return builder.toString(); + } + + private static Date dosToJavaDate(final long dosTime) { + final Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); + cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); + cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); + cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); + cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); + cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime(); + } + private static long javaToDosTime(long javaTime) { + int date; + int time; + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new Date(javaTime)); + int year = cal.get(Calendar.YEAR); + if (year < 1980) { + date = 0x21; + time = 0; + } else { + date = cal.get(Calendar.DATE); + date = (cal.get(Calendar.MONTH) + 1 << 5) | date; + date = ((cal.get(Calendar.YEAR) - 1980) << 9) | date; + time = cal.get(Calendar.SECOND) >> 1; + time = (cal.get(Calendar.MINUTE) << 5) | time; + time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time; + } + return ((long) date << 16) | time; + } + + public static class GeneralPurposeFlag { + private final CommonHeader localFileHeader; + private final int offset; + public GeneralPurposeFlag(CommonHeader commonHeader, int offset){ + this.localFileHeader = commonHeader; + this.offset = offset; + } + + public boolean getEncryption(){ + return this.localFileHeader.getBit(offset, 0); + } + public void setEncryption(boolean flag){ + this.localFileHeader.putBit(offset, 0, flag); + } + public boolean hasDataDescriptor(){ + return this.localFileHeader.getBit(offset, 3); + } + public void setHasDataDescriptor(boolean flag){ + this.localFileHeader.putBit(offset, 3, flag); + } + public boolean getStrongEncryption(){ + return this.localFileHeader.getBit(offset, 6); + } + public void setStrongEncryption(boolean flag){ + this.localFileHeader.putBit(offset, 6, flag); + } + public boolean getUtf8(){ + return this.localFileHeader.getBit(offset + 1, 3); + } + public void setUtf8(boolean flag){ + boolean oldUtf8 = getUtf8(); + if(oldUtf8 == flag){ + return; + } + this.localFileHeader.putBit(offset +1, 3, flag); + this.localFileHeader.onUtf8Changed(oldUtf8); + } + + public int getValue(){ + return this.localFileHeader.getInteger(offset); + } + public void setValue(int value){ + if(value == getValue()){ + return; + } + boolean oldUtf8 = getUtf8(); + this.localFileHeader.putInteger(offset, value); + if(oldUtf8 != getUtf8()){ + this.localFileHeader.onUtf8Changed(oldUtf8); + } + } + + @Override + public String toString(){ + return "Enc="+ getEncryption() + +", Descriptor="+ hasDataDescriptor() + +", StrongEnc="+ getStrongEncryption() + +", UTF8="+ getUtf8(); + } + } + + private static final int OFFSET_versionMadeBy = 4; + private static final int OFFSET_platform = 5; + + private static final int OFFSET_general_purpose = 6; + + private static final int OFFSET_method = 8; + private static final int OFFSET_dos_time = 10; + private static final int OFFSET_crc = 14; + private static final int OFFSET_compressed_size = 18; + private static final int OFFSET_size = 22; + private static final int OFFSET_fileNameLength = 26; + private static final int OFFSET_extraLength = 28; + + private static final int OFFSET_fileName = 30; + +} diff --git a/src/main/java/com/reandroid/archive2/block/DataDescriptor.java b/src/main/java/com/reandroid/archive2/block/DataDescriptor.java new file mode 100644 index 0000000..92cf38a --- /dev/null +++ b/src/main/java/com/reandroid/archive2/block/DataDescriptor.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.block; + +import com.reandroid.archive2.ZipSignature; + +public class DataDescriptor extends ZipHeader{ + public DataDescriptor() { + super(MIN_LENGTH, ZipSignature.DATA_DESCRIPTOR); + } + public long getCrc(){ + return getUnsignedLong(OFFSET_crc); + } + public void setCrc(long value){ + putInteger(OFFSET_crc, value); + } + public long getCompressedSize(){ + return getUnsignedLong(OFFSET_compressed_size); + } + public void setCompressedSize(long value){ + putInteger(OFFSET_compressed_size, value); + } + public long getSize(){ + return getUnsignedLong(OFFSET_size); + } + public void setSize(long value){ + putInteger(OFFSET_size, value); + } + + @Override + public String toString(){ + StringBuilder builder = new StringBuilder(); + builder.append(getSignature()); + builder.append(", crc=").append(String.format("0x%08x", getCrc())); + builder.append(", compressed=").append(getCompressedSize()); + builder.append(", size=").append(getSize()); + return builder.toString(); + } + + private static final int OFFSET_crc = 4; + private static final int OFFSET_compressed_size = 8; + private static final int OFFSET_size = 12; + + public static final int MIN_LENGTH = 16; +} diff --git a/src/main/java/com/reandroid/archive2/block/EndRecord.java b/src/main/java/com/reandroid/archive2/block/EndRecord.java new file mode 100644 index 0000000..618e539 --- /dev/null +++ b/src/main/java/com/reandroid/archive2/block/EndRecord.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.block; + +import com.reandroid.archive2.ZipSignature; + +public class EndRecord extends ZipHeader{ + public EndRecord() { + super(MIN_LENGTH, ZipSignature.END_RECORD); + } + public int getNumberOfDisk(){ + return getShortUnsigned(OFFSET_numberOfDisk); + } + public void setNumberOfDisk(int value){ + putShort(OFFSET_numberOfDisk, value); + } + public int getCentralDirectoryStartDisk(){ + return getShortUnsigned(OFFSET_centralDirectoryStartDisk); + } + public void setCentralDirectoryStartDisk(int value){ + putShort(OFFSET_centralDirectoryStartDisk, value); + } + public int getNumberOfDirectories(){ + return getShortUnsigned(OFFSET_numberOfDirectories); + } + public void setNumberOfDirectories(int value){ + putShort(OFFSET_numberOfDirectories, value); + } + public int getTotalNumberOfDirectories(){ + return getShortUnsigned(OFFSET_totalNumberOfDirectories); + } + public void setTotalNumberOfDirectories(int value){ + putShort(OFFSET_totalNumberOfDirectories, value); + } + public long getLengthOfCentralDirectory(){ + return getUnsignedLong(OFFSET_lengthOfCentralDirectory); + } + public void setLengthOfCentralDirectory(long value){ + putInteger(OFFSET_lengthOfCentralDirectory, value); + } + public long getOffsetOfCentralDirectory(){ + return getUnsignedLong(OFFSET_offsetOfCentralDirectory); + } + public void setOffsetOfCentralDirectory(int value){ + putInteger(OFFSET_offsetOfCentralDirectory, value); + } + public int getLastShort(){ + return getShortUnsigned(OFFSET_lastShort); + } + public void getLastShort(int value){ + putShort(OFFSET_lastShort, value); + } + + + @Override + public String toString(){ + if(countBytes()> 12) & 0x0f]); + cb.put(HEX_CHARS[(c >> 8) & 0x0f]); + cb.put(HEX_CHARS[(c >> 4) & 0x0f]); + cb.put(HEX_CHARS[c & 0x0f]); + cb.flip(); + return cb; + } + + private static int estimateIncrementalEncodingSize(final CharsetEncoder enc, final int charCount) { + return (int) Math.ceil(charCount * enc.averageBytesPerChar()); + } + private static int estimateInitialBufferSize(final CharsetEncoder enc, final int charChount) { + final float first = enc.maxBytesPerChar(); + final float rest = (charChount - 1) * enc.averageBytesPerChar(); + return (int) Math.ceil(first + rest); + } + + private final Charset charset; + private final boolean useReplacement; + private final CharsetEncoder mEncoder; + private final CharsetDecoder mDecoder; + + ZipStringEncoding(final Charset charset, final boolean useReplacement) { + this.charset = charset; + this.useReplacement = useReplacement; + mEncoder = newEncoder(); + mDecoder = newDecoder(); + } + + public boolean canEncode(final String name) { + final CharsetEncoder enc = newEncoder(); + return enc.canEncode(name); + } + public String decode(byte[] data, int offset, int length) throws IOException { + return mDecoder.decode(ByteBuffer.wrap(data, offset, length)).toString(); + } + public byte[] encode(final String text) { + final CharsetEncoder enc = mEncoder; + + final CharBuffer cb = CharBuffer.wrap(text); + CharBuffer tmp = null; + ByteBuffer out = ByteBuffer.allocate(estimateInitialBufferSize(enc, cb.remaining())); + + while (cb.hasRemaining()) { + final CoderResult res = enc.encode(cb, out, false); + + if (res.isUnmappable() || res.isMalformed()) { + final int spaceForSurrogate = estimateIncrementalEncodingSize(enc, 6 * res.length()); + if (spaceForSurrogate > out.remaining()) { + int charCount = 0; + for (int i = cb.position() ; i < cb.limit(); i++) { + charCount += !enc.canEncode(cb.get(i)) ? 6 : 1; + } + final int totalExtraSpace = estimateIncrementalEncodingSize(enc, charCount); + out = growBufferBy(out, totalExtraSpace - out.remaining()); + } + if (tmp == null) { + tmp = CharBuffer.allocate(6); + } + for (int i = 0; i < res.length(); ++i) { + out = encodeFully(enc, encodeSurrogate(tmp, cb.get()), out); + } + + } else if (res.isOverflow()) { + final int increment = estimateIncrementalEncodingSize(enc, cb.remaining()); + out = growBufferBy(out, increment); + + } else if (res.isUnderflow() || res.isError()) { + break; + } + } + // tell the encoder we are done + enc.encode(cb, out, true); + // may have caused underflow, but that's been ignored traditionally + + out.limit(out.position()); + out.rewind(); + return out.array(); + } + + public Charset getCharset() { + return charset; + } + + private CharsetDecoder newDecoder() { + if (!useReplacement) { + return this.charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + return charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith(REPLACEMENT_STRING); + } + + private CharsetEncoder newEncoder() { + if (useReplacement) { + return charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE) + .replaceWith(REPLACEMENT_BYTES); + } + return charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + + private static final String UTF8 = UTF_8.name(); + + + private static ZipStringEncoding getZipEncoding(final String name) { + Charset cs = Charset.defaultCharset(); + if (name != null) { + try { + cs = Charset.forName(name); + } catch (final UnsupportedCharsetException e) { + } + } + final boolean useReplacement = isUTF8(cs.name()); + return new ZipStringEncoding(cs, useReplacement); + } + + static ByteBuffer growBufferBy(final ByteBuffer buffer, final int increment) { + buffer.limit(buffer.position()); + buffer.rewind(); + + final ByteBuffer on = ByteBuffer.allocate(buffer.capacity() + increment); + + on.put(buffer); + return on; + } + + private static boolean isUTF8(final String charsetName) { + final String actual = charsetName != null ? charsetName : Charset.defaultCharset().name(); + if (UTF_8.name().equalsIgnoreCase(actual)) { + return true; + } + return UTF_8.aliases().stream().anyMatch(alias -> alias.equalsIgnoreCase(actual)); + } + + public static String decode(boolean isUtf8, byte[] bytes, int offset, int length){ + if(isUtf8){ + return decodeUtf8(bytes, offset, length); + } + return decodeDefault(bytes, offset, length); + } + private static String decodeUtf8(byte[] bytes, int offset, int length){ + try { + return UTF8_ENCODING.decode(bytes, offset, length); + } catch (IOException exception) { + return new String(bytes, offset, length); + } + } + private static String decodeDefault(byte[] bytes, int offset, int length){ + return new String(bytes, offset, length); + } + public static byte[] encodeString(boolean isUtf8, String text){ + if(text==null || text.length()==0){ + return new byte[0]; + } + if(isUtf8){ + return UTF8_ENCODING.encode(text); + } + return DEFAULT_ENCODING.encode(text); + } + + private static final ZipStringEncoding UTF8_ENCODING = getZipEncoding(UTF8); + private static final ZipStringEncoding DEFAULT_ENCODING = getZipEncoding(Charset.defaultCharset().name()); + +} + diff --git a/src/main/java/com/reandroid/archive2/io/ArchiveFile.java b/src/main/java/com/reandroid/archive2/io/ArchiveFile.java new file mode 100644 index 0000000..37a73ee --- /dev/null +++ b/src/main/java/com/reandroid/archive2/io/ArchiveFile.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.io; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; + +public class ArchiveFile extends ZipSource{ + private final File file; + private FileChannel fileChannel; + private SlicedInputStream mCurrentInputStream; + public ArchiveFile(File file){ + this.file = file; + } + @Override + public long getLength(){ + return this.file.length(); + } + @Override + public byte[] getFooter(int minLength) throws IOException { + long position = getLength(); + if(minLength>position){ + minLength = (int) position; + } + position = position - minLength; + FileChannel fileChannel = getFileChannel(); + fileChannel.position(position); + ByteBuffer buffer = ByteBuffer.allocate(minLength); + fileChannel.read(buffer); + return buffer.array(); + } + @Override + public InputStream getInputStream(long offset, long length) throws IOException { + close(); + mCurrentInputStream = new SlicedInputStream(new FileInputStream(this.file), offset, length); + return mCurrentInputStream; + } + @Override + public OutputStream getOutputStream(long offset) throws IOException { + return null; + } + private FileChannel getFileChannel() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel != null){ + return fileChannel; + } + synchronized (this){ + fileChannel = FileChannel.open(this.file.toPath(), StandardOpenOption.READ); + this.fileChannel = fileChannel; + return fileChannel; + } + } + @Override + public void close() throws IOException { + closeChannel(); + closeCurrentInputStream(); + } + private void closeChannel() throws IOException { + FileChannel fileChannel = this.fileChannel; + if(fileChannel == null){ + return; + } + synchronized (this){ + fileChannel.close(); + this.fileChannel = null; + } + } + private void closeCurrentInputStream() throws IOException { + SlicedInputStream current = this.mCurrentInputStream; + if(current == null){ + return; + } + current.close(); + mCurrentInputStream = null; + } + @Override + public String toString(){ + return "File: " + this.file; + } +} diff --git a/src/main/java/com/reandroid/archive2/io/ArchiveUtil.java b/src/main/java/com/reandroid/archive2/io/ArchiveUtil.java new file mode 100644 index 0000000..35f3477 --- /dev/null +++ b/src/main/java/com/reandroid/archive2/io/ArchiveUtil.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ArchiveUtil { + + public static void writeAll(InputStream inputStream, OutputStream outputStream) throws IOException{ + int bufferLength = 1024 * 1000; + byte[] buffer = new byte[bufferLength]; + int read; + while ((read = inputStream.read(buffer, 0, bufferLength))>0){ + outputStream.write(buffer, 0, read); + } + } + public static void skip(InputStream inputStream, long amount) throws IOException { + if(amount==0){ + return; + } + int bufferLength = 1024*1024*100; + if(bufferLength>amount){ + bufferLength = (int) amount; + } + byte[] buffer = new byte[bufferLength]; + int read; + long remain = amount; + while (remain > 0 && (read = inputStream.read(buffer, 0, bufferLength))>0){ + remain = remain - read; + if(remain remain){ + len = (int) remain; + finishNext = true; + } + int read = inputStream.read(bytes, off, len); + mCount += read; + if(finishNext){ + onFinished(); + } + return read; + } + @Override + public int read(byte[] bytes) throws IOException{ + return this.read(bytes, 0, bytes.length); + } + @Override + public int read() throws IOException { + if(mFinished){ + return -1; + } + checkStarted(); + long remain = mLength - mCount; + if(remain <= 0){ + onFinished(); + return -1; + } + int result = inputStream.read(); + mCount = mCount + 1; + if(remain == 1){ + onFinished(); + } + return result; + } + @Override + public long skip(long n) throws IOException{ + checkStarted(); + long amount = inputStream.skip(n); + if(amount>0){ + mCount += amount; + } + return amount; + } + @Override + public void close() throws IOException { + onFinished(); + } + private void onFinished() throws IOException { + mFinished = true; + inputStream.close(); + } + private void checkStarted() throws IOException { + if(mStarted){ + return; + } + mStarted = true; + inputStream.skip(mOffset); + mCount = 0; + } + @Override + public String toString(){ + return "["+mOffset+","+mLength+"] "+mCount; + } +} diff --git a/src/main/java/com/reandroid/archive2/io/ZipSource.java b/src/main/java/com/reandroid/archive2/io/ZipSource.java new file mode 100644 index 0000000..48365bf --- /dev/null +++ b/src/main/java/com/reandroid/archive2/io/ZipSource.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.io; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public abstract class ZipSource implements Closeable { + public abstract long getLength(); + public abstract byte[] getFooter(int minLength) throws IOException; + public abstract InputStream getInputStream(long offset, long length) throws IOException; + public abstract OutputStream getOutputStream(long offset) throws IOException; +} diff --git a/src/main/java/com/reandroid/archive2/model/ApkSigBlock.java b/src/main/java/com/reandroid/archive2/model/ApkSigBlock.java new file mode 100644 index 0000000..8d1b660 --- /dev/null +++ b/src/main/java/com/reandroid/archive2/model/ApkSigBlock.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.model; + +import java.io.IOException; + +public class ApkSigBlock { + public ApkSigBlock(){ + } + + //TODO : implement all + public void parse(byte[] bytes) throws IOException { + int offset = findSignature(bytes); + if(offset<0){ + return; + } + offset += APK_SIGNING_BLOCK_MAGIC.length; + int length = bytes.length - offset; + } + private int findSignature(byte[] bytes){ + for(int i=0;i headerList; + private EndRecord endRecord; + public CentralFileDirectory(){ + this.headerList = new ArrayList<>(); + } + public CentralEntryHeader get(LocalFileHeader lfh){ + String name = lfh.getFileName(); + CentralEntryHeader ceh = get(lfh.getIndex()); + if(ceh!=null && Objects.equals(ceh.getFileName() , name)){ + return ceh; + } + return get(name); + } + public CentralEntryHeader get(String name){ + if(name == null){ + name = ""; + } + for(CentralEntryHeader ceh:getHeaderList()){ + if(name.equals(ceh.getFileName())){ + return ceh; + } + } + return null; + } + public CentralEntryHeader get(int i){ + if(i<0 || i>=headerList.size()){ + return null; + } + return headerList.get(i); + } + public int count(){ + return headerList.size(); + } + public List getHeaderList() { + return headerList; + } + public EndRecord getEndRecord() { + return endRecord; + } + public void visit(ZipSource zipSource) throws IOException { + byte[] footer = zipSource.getFooter(EndRecord.MAX_LENGTH); + EndRecord endRecord = findEndRecord(footer); + int length = (int) endRecord.getLengthOfCentralDirectory(); + int endLength = endRecord.countBytes(); + if(footer.length < (length + endLength)){ + footer = zipSource.getFooter(length + endLength); + } + int offset = footer.length - length - endLength; + this.endRecord = endRecord; + loadCentralFileHeaders(footer, offset, length); + } + private void loadCentralFileHeaders(byte[] footer, int offset, int length) throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(footer, offset, length); + loadCentralFileHeaders(inputStream); + } + private void loadCentralFileHeaders(InputStream inputStream) throws IOException { + List headerList = this.headerList; + CentralEntryHeader ceh = new CentralEntryHeader(); + ceh.readBytes(inputStream); + while (ceh.isValidSignature()){ + headerList.add(ceh); + ceh = new CentralEntryHeader(); + ceh.readBytes(inputStream); + } + inputStream.close(); + } + private EndRecord findEndRecord(byte[] footer) throws IOException{ + int length = footer.length; + int minLength = EndRecord.MIN_LENGTH; + int start = length - minLength; + for(int offset=start; offset>=0; offset--){ + EndRecord endRecord = new EndRecord(); + endRecord.putBytes(footer, offset, 0, minLength); + if(endRecord.isValidSignature()){ + return endRecord; + } + } + throw new IOException("Failed to find end record"); + } +} diff --git a/src/main/java/com/reandroid/archive2/model/LocalFileDirectory.java b/src/main/java/com/reandroid/archive2/model/LocalFileDirectory.java new file mode 100644 index 0000000..f6b8704 --- /dev/null +++ b/src/main/java/com/reandroid/archive2/model/LocalFileDirectory.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 github.com/REAndroid + * + * 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.reandroid.archive2.model; + +import com.reandroid.archive2.block.CentralEntryHeader; +import com.reandroid.archive2.block.DataDescriptor; +import com.reandroid.archive2.block.EndRecord; +import com.reandroid.archive2.block.LocalFileHeader; +import com.reandroid.archive2.io.ZipSource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class LocalFileDirectory { + private final CentralFileDirectory centralFileDirectory; + private final List headerList; + private ApkSigBlock apkSigBlock; + private long mTotalDataLength; + public LocalFileDirectory(CentralFileDirectory centralFileDirectory){ + this.centralFileDirectory = centralFileDirectory; + this.headerList = new ArrayList<>(); + } + public LocalFileDirectory(){ + this(new CentralFileDirectory()); + } + public void visit(ZipSource zipSource) throws IOException { + getCentralFileDirectory().visit(zipSource); + visitLocalFile(zipSource); + } + private void visitLocalFile(ZipSource zipSource) throws IOException { + EndRecord endRecord = getCentralFileDirectory().getEndRecord(); + InputStream inputStream = zipSource.getInputStream(0, endRecord.getOffsetOfCentralDirectory()); + visitLocalFile(inputStream); + visitApkSigBlock(inputStream); + inputStream.close(); + } + private void visitLocalFile(InputStream inputStream) throws IOException { + List headerList = this.getHeaderList(); + long offset = 0; + int read; + CentralFileDirectory centralFileDirectory = getCentralFileDirectory(); + LocalFileHeader lfh = new LocalFileHeader(); + read = lfh.readBytes(inputStream); + int index = 0; + while (lfh.isValidSignature()){ + offset += read; + lfh.setIndex(index); + CentralEntryHeader ceh = centralFileDirectory.get(lfh); + lfh.mergeZeroValues(ceh); + lfh.setFileOffset(offset); + ceh.setFileOffset(offset); + offset += inputStream.skip(lfh.getDataSize()); + DataDescriptor dataDescriptor = null; + if(lfh.hasDataDescriptor()){ + dataDescriptor = new DataDescriptor(); + read = dataDescriptor.readBytes(inputStream); + if(read>0){ + offset += read; + } + } + lfh.setDataDescriptor(dataDescriptor); + headerList.add(lfh); + index++; + + lfh = new LocalFileHeader(); + read = lfh.readBytes(inputStream); + } + mTotalDataLength = offset; + } + private void visitApkSigBlock(InputStream inputStream) throws IOException{ + int blockSize = (int) (getCentralFileDirectory().getEndRecord().getOffsetOfCentralDirectory() + - getTotalDataLength()); + if(blockSize<=0){ + return; + } + byte[] bytes = new byte[blockSize]; + inputStream.read(bytes, 0, bytes.length); + ApkSigBlock apkSigBlock = new ApkSigBlock(); + apkSigBlock.parse(bytes); + this.apkSigBlock = apkSigBlock; + } + + public CentralFileDirectory getCentralFileDirectory() { + return centralFileDirectory; + } + public List getHeaderList() { + return headerList; + } + public long getTotalDataLength() { + return mTotalDataLength; + } +}