This commit is contained in:
REAndroid 2023-04-02 17:00:51 -04:00
parent 4dd7eaf1ed
commit 3b669f6f92
18 changed files with 2029 additions and 0 deletions

View File

@ -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<ArchiveEntry> entryList;
private final EndRecord endRecord;
public Archive(ZipSource zipSource) throws IOException {
this.zipSource = zipSource;
LocalFileDirectory lfd = new LocalFileDirectory();
lfd.visit(zipSource);
List<LocalFileHeader> localFileHeaderList = lfd.getHeaderList();
List<CentralEntryHeader> centralEntryHeaderList = lfd.getCentralFileDirectory().getHeaderList();
List<ArchiveEntry> entryList = new ArrayList<>();
for(int i=0;i<localFileHeaderList.size();i++){
LocalFileHeader lfh = localFileHeaderList.get(i);
CentralEntryHeader ceh = centralEntryHeaderList.get(i);
ArchiveEntry archiveEntry = new ArchiveEntry(lfh, ceh);
entryList.add(archiveEntry);
}
this.entryList = entryList;
this.endRecord = lfd.getCentralFileDirectory().getEndRecord();
}
public Archive(File file) throws IOException {
this(new ArchiveFile(file));
}
public InputStream openRawInputStream(ArchiveEntry archiveEntry) throws IOException {
return zipSource.getInputStream(archiveEntry.getFileOffset(), archiveEntry.getDataSize());
}
public InputStream openInputStream(ArchiveEntry archiveEntry) throws IOException {
InputStream rawInputStream = openRawInputStream(archiveEntry);
if(archiveEntry.getMethod() == ZipEntry.STORED){
return rawInputStream;
}
return new InflaterInputStream(rawInputStream,
new Inflater(true), 1024*1000);
}
public List<ArchiveEntry> 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);
}
}

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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()<getMinByteLength()){
return "Invalid";
}
StringBuilder builder = new StringBuilder();
builder.append('[').append(getFileOffset()).append(']');
String str = getFileName();
boolean appendOnce = false;
if(str.length()>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;
}

View File

@ -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()<getMinByteLength()){
return "Invalid";
}
StringBuilder builder = new StringBuilder();
builder.append('[').append(getFileOffset()).append("] ");
String str = getFileName();
boolean appendOnce = false;
if(str.length()>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;
}

View File

@ -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;
}

View File

@ -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()<getMinByteLength()){
return "Invalid";
}
StringBuilder builder = new StringBuilder();
builder.append(getSignature());
builder.append(", disks=").append(getNumberOfDisk());
builder.append(", start disk=").append(getCentralDirectoryStartDisk());
builder.append(", dirs=").append(getNumberOfDirectories());
builder.append(", total dirs=").append(getTotalNumberOfDirectories());
builder.append(", length=").append(getLengthOfCentralDirectory());
builder.append(", offset=").append(getOffsetOfCentralDirectory());
builder.append(", last=").append(String.format("0x%08x", getLastShort()));
return builder.toString();
}
private static final int OFFSET_numberOfDisk = 4;
private static final int OFFSET_centralDirectoryStartDisk = 6;
private static final int OFFSET_numberOfDirectories = 8;
private static final int OFFSET_totalNumberOfDirectories = 10;
private static final int OFFSET_lengthOfCentralDirectory = 12;
private static final int OFFSET_offsetOfCentralDirectory = 16;
private static final int OFFSET_lastShort = 20;
public static final int MIN_LENGTH = 22;
public static final int MAX_LENGTH = 0xffff + 22;
}

View File

@ -0,0 +1,81 @@
/*
* 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 LocalFileHeader extends CommonHeader {
private DataDescriptor dataDescriptor;
public LocalFileHeader(){
super(OFFSET_fileName, ZipSignature.LOCAL_FILE, OFFSET_general_purpose);
}
public LocalFileHeader(String name){
this();
setFileName(name);
}
public void mergeZeroValues(CentralEntryHeader ceh){
if(getCrc()==0){
setCrc(ceh.getCrc());
}
if(getSize()==0){
setSize(ceh.getSize());
}
if(getCompressedSize()==0){
setCompressedSize(ceh.getCompressedSize());
}
if(getGeneralPurposeFlag().getValue()==0){
getGeneralPurposeFlag().setValue(ceh.getGeneralPurposeFlag().getValue());
}
}
public DataDescriptor getDataDescriptor() {
return dataDescriptor;
}
public void setDataDescriptor(DataDescriptor dataDescriptor){
this.dataDescriptor = dataDescriptor;
}
public static LocalFileHeader fromCentralEntryHeader(CentralEntryHeader ceh){
LocalFileHeader lfh = new LocalFileHeader();
lfh.setSignature(ZipSignature.LOCAL_FILE);
lfh.setVersionMadeBy(ceh.getVersionMadeBy());
lfh.getGeneralPurposeFlag().setValue(ceh.getGeneralPurposeFlag().getValue());
lfh.setMethod(ceh.getMethod());
lfh.setDosTime(ceh.getDosTime());
lfh.setCrc(ceh.getCrc());
lfh.setCompressedSize(ceh.getCompressedSize());
lfh.setSize(ceh.getSize());
lfh.setFileName(ceh.getFileName());
lfh.setExtra(ceh.getExtra());
return lfh;
}
private static final int OFFSET_signature = 0;
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;
}

View File

@ -0,0 +1,74 @@
/*
* 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.arsc.io.BlockReader;
import com.reandroid.arsc.item.BlockItem;
import java.io.IOException;
import java.io.InputStream;
public abstract class ZipBlock extends BlockItem {
public ZipBlock(int bytesLength) {
super(bytesLength);
}
public void putBytes(byte[] bytes, int offset, int putOffset, int length){
if(length<=0 || bytes.length==0){
return;
}
int size = putOffset + length;
setBytesLength(size, false);
System.arraycopy(bytes, offset, getBytesInternal(), putOffset, length);
}
@Override
public abstract int readBytes(InputStream inputStream) throws IOException;
// should not use this method
@Override
public void onReadBytes(BlockReader blockReader) throws IOException {
this.readBytes((InputStream) blockReader);
}
long getUnsignedLong(int offset){
return getInteger(offset) & 0x00000000ffffffffL;
}
void putBit(int offset, int bitIndex, boolean bit){
putBit(getBytesInternal(), offset, bitIndex, bit);
}
boolean getBit(int offset, int bitIndex){
return getBit(getBytesInternal(), offset, bitIndex);
}
int getByteUnsigned(int offset){
return getBytesInternal()[offset] & 0xff;
}
int getShortUnsigned(int offset){
return getShort(getBytesInternal(), offset) & 0xffff;
}
int getInteger(int offset){
return getInteger(getBytesInternal(), offset);
}
void putInteger(int offset, int value){
putInteger(getBytesInternal(), offset, value);
}
void putInteger(int offset, long value){
putInteger(getBytesInternal(), offset, (int) value);
}
void putShort(int offset, int value){
putShort(getBytesInternal(), offset, (short) value);
}
}

View File

@ -0,0 +1,88 @@
/*
* 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;
public class ZipHeader extends ZipBlock{
private final ZipSignature expectedSignature;
private final int minByteLength;
public ZipHeader(int minByteLength, ZipSignature expectedSignature) {
super(minByteLength);
this.minByteLength = minByteLength;
this.expectedSignature = expectedSignature;
}
@Override
public int readBytes(InputStream inputStream) throws IOException {
int read = readBasic(inputStream);
ZipSignature sig=getSignature();
if(sig != getExpectedSignature()){
return read;
}
read += readNext(inputStream);
return read;
}
private int readBasic(InputStream inputStream) throws IOException {
setBytesLength(getMinByteLength(), false);
byte[] bytes = getBytesInternal();
int beginLength = bytes.length;
int read = inputStream.read(bytes, 0, beginLength);
if(read != beginLength){
setBytesLength(read, false);
if(getSignature()==expectedSignature){
setSignature(0);
}
return read;
}
return read;
}
int readNext(InputStream inputStream) throws IOException {
return 0;
}
public boolean isValidSignature(){
return getSignature() == getExpectedSignature();
}
ZipSignature getExpectedSignature(){
return expectedSignature;
}
int getMinByteLength() {
return minByteLength;
}
public ZipSignature getSignature(){
return ZipSignature.valueOf(getSignatureValue());
}
public int getSignatureValue(){
if(countBytes()<4){
return 0;
}
return getInteger(OFFSET_signature);
}
public void setSignature(int value){
if(countBytes()<4){
return;
}
putInteger(OFFSET_signature, value);
}
public void setSignature(ZipSignature signature){
setSignature(signature == null ? 0:signature.getValue());
}
private static final int OFFSET_signature = 0;
}

View File

@ -0,0 +1,221 @@
/*
* 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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public class ZipStringEncoding {
private static final char REPLACEMENT = '?';
private static final byte[] REPLACEMENT_BYTES = { (byte) REPLACEMENT };
private static final String REPLACEMENT_STRING = String.valueOf(REPLACEMENT);
private static final char[] HEX_CHARS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
private static ByteBuffer encodeFully(final CharsetEncoder enc, final CharBuffer cb, final ByteBuffer out) {
ByteBuffer o = out;
while (cb.hasRemaining()) {
final CoderResult result = enc.encode(cb, o, false);
if (result.isOverflow()) {
final int increment = estimateIncrementalEncodingSize(enc, cb.remaining());
o = growBufferBy(o, increment);
}
}
return o;
}
private static CharBuffer encodeSurrogate(final CharBuffer cb, final char c) {
cb.position(0).limit(6);
cb.put('%');
cb.put('U');
cb.put(HEX_CHARS[(c >> 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());
}

View File

@ -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;
}
}

View File

@ -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<bufferLength){
bufferLength = (int) remain;
}
}
}
}

View File

@ -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.io;
import java.io.IOException;
import java.io.InputStream;
public class SlicedInputStream extends InputStream{
private final InputStream inputStream;
private final long mOffset;
private final long mLength;
private long mCount;
private boolean mFinished;
private boolean mStarted;
public SlicedInputStream(InputStream inputStream, long offset, long length){
this.inputStream = inputStream;
this.mOffset = offset;
this.mLength = length;
}
@Override
public int read(byte[] bytes, int off, int len) throws IOException{
if(mFinished){
return -1;
}
checkStarted();
long remain = mLength - mCount;
if(remain <= 0){
onFinished();
return -1;
}
boolean finishNext = false;
if(len > 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;
}
}

View File

@ -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;
}

View File

@ -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<bytes.length;i++){
if (matchesSignature(bytes, i)){
return i;
}
}
return -1;
}
private boolean matchesSignature(byte[] bytes, int offset){
byte[] sig = APK_SIGNING_BLOCK_MAGIC;
int length = bytes.length - offset;
if (length<sig.length){
return false;
}
for(int i=0;i<sig.length;i++){
int j = offset+i;
if(sig[i] != bytes[j]){
return false;
}
if(bytes[j] ==0){
return false;
}
}
return true;
}
private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024;
public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096;
private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] {
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20,
0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32,
};
public static final int VERITY_PADDING_BLOCK_ID = 0x42726577;
public static final int VERSION_SOURCE_STAMP = 0;
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
}

View File

@ -0,0 +1,110 @@
/*
* 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.EndRecord;
import com.reandroid.archive2.block.LocalFileHeader;
import com.reandroid.archive2.io.ZipSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class CentralFileDirectory {
private final List<CentralEntryHeader> 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<CentralEntryHeader> 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<CentralEntryHeader> 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");
}
}

View File

@ -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<LocalFileHeader> 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<LocalFileHeader> 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<LocalFileHeader> getHeaderList() {
return headerList;
}
public long getTotalDataLength() {
return mTotalDataLength;
}
}