ARSCLib/src/main/java/com/reandroid/arsc/chunk/xml/ResXmlDocument.java
2023-03-11 05:14:01 -05:00

398 lines
14 KiB
Java
Executable File

/*
* 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.arsc.chunk.xml;
import com.reandroid.arsc.ApkFile;
import com.reandroid.arsc.chunk.*;
import com.reandroid.arsc.container.SingleBlockContainer;
import com.reandroid.arsc.header.HeaderBlock;
import com.reandroid.arsc.io.BlockReader;
import com.reandroid.arsc.pool.ResXmlStringPool;
import com.reandroid.arsc.pool.StringPool;
import com.reandroid.arsc.value.ValueType;
import com.reandroid.common.EntryStore;
import com.reandroid.json.JSONArray;
import com.reandroid.json.JSONConvert;
import com.reandroid.json.JSONObject;
import com.reandroid.xml.XMLDocument;
import com.reandroid.xml.XMLElement;
import com.reandroid.xml.XMLException;
import java.io.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ResXmlDocument extends Chunk<HeaderBlock>
implements MainChunk, ParentChunk, JSONConvert<JSONObject> {
private final ResXmlStringPool mResXmlStringPool;
private final ResXmlIDMap mResXmlIDMap;
private ResXmlElement mResXmlElement;
private final SingleBlockContainer<ResXmlElement> mResXmlElementContainer;
private ApkFile mApkFile;
public ResXmlDocument() {
super(new HeaderBlock(ChunkType.XML),3);
this.mResXmlStringPool=new ResXmlStringPool(true);
this.mResXmlIDMap=new ResXmlIDMap();
this.mResXmlElement=new ResXmlElement();
this.mResXmlElementContainer=new SingleBlockContainer<>();
this.mResXmlElementContainer.setItem(mResXmlElement);
addChild(mResXmlStringPool);
addChild(mResXmlIDMap);
addChild(mResXmlElementContainer);
}
public void destroy(){
ResXmlElement root = getResXmlElement();
if(root!=null){
root.clearChildes();
setResXmlElement(null);
}
getResXmlIDMap().destroy();
getStringPool().destroy();
refresh();
}
public void setAttributesUnitSize(int size, boolean setToAll){
ResXmlElement root = getResXmlElement();
if(root!=null){
root.setAttributesUnitSize(size, setToAll);
}
}
public ResXmlElement createRootElement(String tag){
int lineNo=1;
ResXmlElement resXmlElement=new ResXmlElement();
resXmlElement.newStartElement(lineNo);
setResXmlElement(resXmlElement);
if(tag!=null){
resXmlElement.setTag(tag);
}
return resXmlElement;
}
void linkStringReferences(){
ResXmlElement element=getResXmlElement();
if(element!=null){
element.linkStringReferences();
}
}
/*
* method Block.addBytes is inefficient for large size byte array
* so let's override here because this block is the largest
*/
@Override
public byte[] getBytes(){
ByteArrayOutputStream os=new ByteArrayOutputStream();
try {
writeBytes(os);
os.close();
} catch (IOException ignored) {
}
return os.toByteArray();
}
@Override
public void onReadBytes(BlockReader reader) throws IOException {
HeaderBlock headerBlock=reader.readHeaderBlock();
if(headerBlock==null){
return;
}
BlockReader chunkReader=reader.create(headerBlock.getChunkSize());
headerBlock=getHeaderBlock();
headerBlock.readBytes(chunkReader);
// android/aapt2 accepts 0x0000 (NULL) chunk type as XML, it could
// be android's bug and might be fixed in the future until then lets fix it ourselves
headerBlock.setType(ChunkType.XML);
while (chunkReader.isAvailable()){
boolean readOk=readNext(chunkReader);
if(!readOk){
break;
}
}
reader.offset(headerBlock.getChunkSize());
chunkReader.close();
onChunkLoaded();
}
@Override
public void onChunkLoaded(){
super.onChunkLoaded();
linkStringReferences();
}
private boolean readNext(BlockReader reader) throws IOException {
if(!reader.isAvailable()){
return false;
}
int position=reader.getPosition();
HeaderBlock headerBlock=reader.readHeaderBlock();
if(headerBlock==null){
return false;
}
ChunkType chunkType=headerBlock.getChunkType();
if(chunkType==ChunkType.STRING){
mResXmlStringPool.readBytes(reader);
}else if(chunkType==ChunkType.XML_RESOURCE_MAP){
mResXmlIDMap.readBytes(reader);
}else if(isElementChunk(chunkType)){
mResXmlElementContainer.readBytes(reader);
return reader.isAvailable();
}else {
throw new IOException("Unexpected chunk "+headerBlock);
}
return reader.isAvailable() && position!=reader.getPosition();
}
private boolean isElementChunk(ChunkType chunkType){
if(chunkType==ChunkType.XML_START_ELEMENT){
return true;
}
if(chunkType==ChunkType.XML_END_ELEMENT){
return true;
}
if(chunkType==ChunkType.XML_START_NAMESPACE){
return true;
}
if(chunkType==ChunkType.XML_END_NAMESPACE){
return true;
}
if(chunkType==ChunkType.XML_CDATA){
return true;
}
if(chunkType==ChunkType.XML_LAST_CHUNK){
return true;
}
return false;
}
@Override
public ResXmlStringPool getStringPool(){
return mResXmlStringPool;
}
@Override
public ApkFile getApkFile(){
return mApkFile;
}
@Override
public void setApkFile(ApkFile apkFile){
this.mApkFile = apkFile;
}
@Override
public StringPool<?> getSpecStringPool() {
return null;
}
public ResXmlIDMap getResXmlIDMap(){
return mResXmlIDMap;
}
public ResXmlElement getResXmlElement(){
return mResXmlElement;
}
public void setResXmlElement(ResXmlElement resXmlElement){
this.mResXmlElement=resXmlElement;
this.mResXmlElementContainer.setItem(resXmlElement);
}
@Override
protected void onChunkRefreshed() {
}
public void readBytes(File file) throws IOException{
BlockReader reader=new BlockReader(file);
super.readBytes(reader);
}
public void readBytes(InputStream inputStream) throws IOException{
BlockReader reader=new BlockReader(inputStream);
super.readBytes(reader);
}
public final int writeBytes(File file) throws IOException{
if(isNull()){
throw new IOException("Can NOT save null block");
}
File dir=file.getParentFile();
if(dir!=null && !dir.exists()){
dir.mkdirs();
}
OutputStream outputStream=new FileOutputStream(file);
int length = super.writeBytes(outputStream);
outputStream.close();
return length;
}
@Override
public JSONObject toJson() {
JSONObject jsonObject=new JSONObject();
jsonObject.put(ResXmlDocument.NAME_element, getResXmlElement().toJson());
JSONArray pool = getStringPool().toJson();
if(pool!=null){
jsonObject.put(ResXmlDocument.NAME_styled_strings, pool);
}
return jsonObject;
}
@Override
public void fromJson(JSONObject json) {
onFromJson(json);
ResXmlElement xmlElement=getResXmlElement();
xmlElement.fromJson(json.optJSONObject(ResXmlDocument.NAME_element));
refresh();
}
public XMLDocument decodeToXml() throws XMLException {
ApkFile apkFile = getApkFile();
if(apkFile == null){
throw new XMLException("Null parent apk file");
}
int currentPackageId = 0;
AndroidManifestBlock manifestBlock;
if(this instanceof AndroidManifestBlock){
manifestBlock = ((AndroidManifestBlock)this);
}else {
manifestBlock = apkFile.getAndroidManifestBlock();
}
if(manifestBlock!=null){
currentPackageId = manifestBlock.guessCurrentPackageId();
}
TableBlock tableBlock = apkFile.getTableBlock();
return decodeToXml(tableBlock, currentPackageId);
}
public XMLDocument decodeToXml(EntryStore entryStore, int currentPackageId) throws XMLException {
XMLDocument xmlDocument = new XMLDocument();
XMLElement xmlElement = getResXmlElement()
.decodeToXml(entryStore, currentPackageId);
xmlDocument.setDocumentElement(xmlElement);
return xmlDocument;
}
private void onFromJson(JSONObject json){
List<JSONObject> attributeList=recursiveAttributes(json.optJSONObject(ResXmlDocument.NAME_element));
buildResourceIds(attributeList);
Set<String> allStrings=recursiveStrings(json.optJSONObject(ResXmlDocument.NAME_element));
ResXmlStringPool stringPool = getStringPool();
stringPool.addStrings(allStrings);
stringPool.refresh();
}
private void buildResourceIds(List<JSONObject> attributeList){
ResIdBuilder builder=new ResIdBuilder();
for(JSONObject attribute:attributeList){
int id=attribute.getInt(ResXmlAttribute.NAME_id);
if(id==0){
continue;
}
String name=attribute.getString(ResXmlAttribute.NAME_name);
builder.add(id, name);
}
builder.buildTo(getResXmlIDMap());
}
private List<JSONObject> recursiveAttributes(JSONObject elementJson){
List<JSONObject> results = new ArrayList<>();
if(elementJson==null){
return results;
}
JSONArray attributes = elementJson.optJSONArray(ResXmlElement.NAME_attributes);
if(attributes != null){
int length = attributes.length();
for(int i=0; i<length; i++){
JSONObject attr=attributes.optJSONObject(i);
if(attr!=null){
results.add(attr);
}
}
}
JSONArray childElements = elementJson.optJSONArray(ResXmlElement.NAME_childes);
if(childElements!=null){
int length=childElements.length();
for(int i=0;i<length;i++){
JSONObject child=childElements.getJSONObject(i);
results.addAll(recursiveAttributes(child));
}
}
return results;
}
private Set<String> recursiveStrings(JSONObject elementJson){
Set<String> results = new HashSet<>();
if(elementJson==null){
return results;
}
results.add(elementJson.optString(ResXmlElement.NAME_namespace_uri));
results.add(elementJson.optString(ResXmlElement.NAME_name));
JSONArray namespaces=elementJson.optJSONArray(ResXmlElement.NAME_namespaces);
if(namespaces != null){
int length = namespaces.length();
for(int i=0; i<length; i++){
JSONObject nsObject=namespaces.getJSONObject(i);
results.add(nsObject.getString(ResXmlElement.NAME_namespace_uri));
results.add(nsObject.getString(ResXmlElement.NAME_namespace_prefix));
}
}
JSONArray attributes = elementJson.optJSONArray(ResXmlElement.NAME_attributes);
if(attributes != null){
int length = attributes.length();
for(int i=0; i<length; i++){
JSONObject attr=attributes.optJSONObject(i);
if(attr==null){
continue;
}
results.add(attr.getString(ResXmlAttribute.NAME_name));
ValueType valueType=ValueType.fromName(attr.getString(ResXmlAttribute.NAME_value_type));
if(valueType==ValueType.STRING){
results.add(attr.optString(ResXmlAttribute.NAME_data));
}
}
}
JSONArray childElements = elementJson.optJSONArray(ResXmlElement.NAME_childes);
if(childElements!=null){
int length=childElements.length();
for(int i=0;i<length;i++){
JSONObject child=childElements.getJSONObject(i);
results.addAll(recursiveStrings(child));
}
}
return results;
}
public static boolean isResXmlBlock(File file){
if(file==null){
return false;
}
try {
InputStream inputStream=new FileInputStream(file);
boolean result = isResXmlBlock(inputStream);
inputStream.close();
return result;
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(InputStream inputStream){
try {
HeaderBlock headerBlock=BlockReader.readHeaderBlock(inputStream);
return isResXmlBlock(headerBlock);
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(BlockReader blockReader){
if(blockReader==null){
return false;
}
try {
HeaderBlock headerBlock = blockReader.readHeaderBlock();
return isResXmlBlock(headerBlock);
} catch (IOException ignored) {
return false;
}
}
public static boolean isResXmlBlock(HeaderBlock headerBlock){
if(headerBlock==null){
return false;
}
ChunkType chunkType=headerBlock.getChunkType();
return chunkType==ChunkType.XML;
}
private static final String NAME_element ="element";
private static final String NAME_styled_strings="styled_strings";
}