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