mirror of
https://github.com/revanced/ARSCLib.git
synced 2025-05-06 08:44:31 +02:00
standalone archive management REAndroid/APKEditor#3 , REAndroid/APKEditor#6
This commit is contained in:
parent
4dd7eaf1ed
commit
3b669f6f92
97
src/main/java/com/reandroid/archive2/Archive.java
Normal file
97
src/main/java/com/reandroid/archive2/Archive.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
121
src/main/java/com/reandroid/archive2/ArchiveEntry.java
Normal file
121
src/main/java/com/reandroid/archive2/ArchiveEntry.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
41
src/main/java/com/reandroid/archive2/ZipSignature.java
Normal file
41
src/main/java/com/reandroid/archive2/ZipSignature.java
Normal 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();
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
402
src/main/java/com/reandroid/archive2/block/CommonHeader.java
Normal file
402
src/main/java/com/reandroid/archive2/block/CommonHeader.java
Normal 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;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
95
src/main/java/com/reandroid/archive2/block/EndRecord.java
Normal file
95
src/main/java/com/reandroid/archive2/block/EndRecord.java
Normal 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;
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
74
src/main/java/com/reandroid/archive2/block/ZipBlock.java
Normal file
74
src/main/java/com/reandroid/archive2/block/ZipBlock.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
88
src/main/java/com/reandroid/archive2/block/ZipHeader.java
Normal file
88
src/main/java/com/reandroid/archive2/block/ZipHeader.java
Normal 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;
|
||||||
|
}
|
@ -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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
95
src/main/java/com/reandroid/archive2/io/ArchiveFile.java
Normal file
95
src/main/java/com/reandroid/archive2/io/ArchiveFile.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
50
src/main/java/com/reandroid/archive2/io/ArchiveUtil.java
Normal file
50
src/main/java/com/reandroid/archive2/io/ArchiveUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
src/main/java/com/reandroid/archive2/io/SlicedInputStream.java
Normal file
107
src/main/java/com/reandroid/archive2/io/SlicedInputStream.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
28
src/main/java/com/reandroid/archive2/io/ZipSource.java
Normal file
28
src/main/java/com/reandroid/archive2/io/ZipSource.java
Normal 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;
|
||||||
|
}
|
75
src/main/java/com/reandroid/archive2/model/ApkSigBlock.java
Normal file
75
src/main/java/com/reandroid/archive2/model/ApkSigBlock.java
Normal 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;
|
||||||
|
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user