better xml namespace handling

This commit is contained in:
REAndroid 2023-05-07 16:58:15 +02:00
parent 75d6ed704b
commit b07b8bddfb
7 changed files with 166 additions and 176 deletions

View File

@ -211,8 +211,7 @@ public class ApkModuleXmlDecoder extends ApkDecoder implements Predicate<Entry>
} }
private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile) private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile)
throws IOException { throws IOException {
XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(document); XMLNamespaceValidator.validateNamespaces(document);
namespaceValidator.validate();
ResXmlDocumentSerializer serializer = getDocumentSerializer(); ResXmlDocumentSerializer serializer = getDocumentSerializer();
if(currentPackageId != 0){ if(currentPackageId != 0){
serializer.getDecoder().setCurrentPackageId(currentPackageId); serializer.getDecoder().setCurrentPackageId(currentPackageId);

View File

@ -121,8 +121,7 @@ public class ResXmlDocumentSerializer implements ResXmlPullParser.DocumentLoaded
if(!validateXmlNamespace){ if(!validateXmlNamespace){
return resXmlDocument; return resXmlDocument;
} }
XMLNamespaceValidator namespaceValidator = new XMLNamespaceValidator(resXmlDocument); XMLNamespaceValidator.validateNamespaces(resXmlDocument);
namespaceValidator.validate();
return resXmlDocument; return resXmlDocument;
} }
private IOException getError(Exception exception){ private IOException getError(Exception exception){

View File

@ -1,4 +1,4 @@
/* /*
* Copyright (C) 2022 github.com/REAndroid * Copyright (C) 2022 github.com/REAndroid
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -17,54 +17,65 @@ package com.reandroid.apk.xmldecoder;
import com.reandroid.arsc.chunk.xml.*; import com.reandroid.arsc.chunk.xml.*;
import java.util.ArrayList; import java.util.Collection;
import java.util.List;
public class XMLNamespaceValidator { public class XMLNamespaceValidator {
private static final String URI_ANDROID="http://schemas.android.com/apk/res/android"; private static final String URI_ANDROID = "http://schemas.android.com/apk/res/android";
private static final String URI_APP="http://schemas.android.com/apk/res-auto"; private static final String URI_APP = "http://schemas.android.com/apk/res-auto";
private static final String PREFIX_ANDROID ="android"; private static final String PREFIX_ANDROID = "android";
private static final String PREFIX_APP="app"; private static final String PREFIX_APP = "app";
private final ResXmlDocument xmlBlock; private final ResXmlDocument xmlBlock;
private List<ResXmlAttribute> mAttributeList;
private ResXmlElement mRootElement;
private ResXmlStartNamespace mNsAndroid;
private ResXmlStartNamespace mNsApp;
public XMLNamespaceValidator(ResXmlDocument xmlBlock){ public XMLNamespaceValidator(ResXmlDocument xmlBlock){
this.xmlBlock=xmlBlock; this.xmlBlock=xmlBlock;
} }
public void validate(){ public void validate(){
if(getRootElement()==null){ validateNamespaces(xmlBlock);
return;
} }
for(ResXmlAttribute attribute:getAttributes()){
validate(attribute); public static boolean isValid(ResXmlAttribute attribute){
int resourceId = attribute.getNameResourceID();
if(resourceId == 0){
return attribute.getUri() == null;
} }
} if(isAndroid(toPackageId(resourceId))){
private void validate(ResXmlAttribute attribute){ return isValidAndroidNamespace(attribute);
int resourceId=attribute.getNameResourceID();
if(resourceId==0){
removeNamespace(attribute);
return;
}
int pkgId=toPackageId(resourceId);
if(isAndroid(pkgId)){
setAndroidNamespace(attribute);
}else { }else {
setAppNamespace(attribute); return isValidAppNamespace(attribute);
} }
} }
private void removeNamespace(ResXmlAttribute attribute){ public static void validateNamespaces(ResXmlDocument resXmlDocument){
validateNamespaces(resXmlDocument.getResXmlElement());
}
public static void validateNamespaces(ResXmlElement element){
validateNamespaces(element.listAttributes());
for(ResXmlElement child : element.listElements()){
validateNamespaces(child);
}
}
private static void validateNamespaces(Collection<ResXmlAttribute> attributeList){
for(ResXmlAttribute attribute : attributeList){
validateNamespace(attribute);
}
}
private static void validateNamespace(ResXmlAttribute attribute){
int resourceId = attribute.getNameResourceID();
if(resourceId == 0){
attribute.setNamespaceReference(-1); attribute.setNamespaceReference(-1);
}
private void setAppNamespace(ResXmlAttribute attribute){
if(isValidAppNamespace(attribute)){
return; return;
} }
ResXmlStartNamespace ns = getOrCreateApp(); if(isAndroid(toPackageId(resourceId))){
attribute.setNamespaceReference(ns.getUriReference()); if(!isValidAndroidNamespace(attribute)){
attribute.setNamespace(URI_ANDROID, PREFIX_ANDROID);
} }
private boolean isValidAppNamespace(ResXmlAttribute attribute){ }else {
if(!isValidAppNamespace(attribute)){
attribute.setNamespace(URI_APP, PREFIX_APP);
}
}
}
private static boolean isValidAppNamespace(ResXmlAttribute attribute){
String uri = attribute.getUri(); String uri = attribute.getUri();
String prefix = attribute.getNamePrefix(); String prefix = attribute.getNamePrefix();
if(URI_ANDROID.equals(uri) || PREFIX_ANDROID.equals(prefix)){ if(URI_ANDROID.equals(uri) || PREFIX_ANDROID.equals(prefix)){
@ -75,62 +86,15 @@ public class XMLNamespaceValidator {
} }
return true; return true;
} }
private void setAndroidNamespace(ResXmlAttribute attribute){ private static boolean isValidAndroidNamespace(ResXmlAttribute attribute){
if(URI_ANDROID.equals(attribute.getUri()) return URI_ANDROID.equals(attribute.getUri())
&& PREFIX_ANDROID.equals(attribute.getNamePrefix())){ && PREFIX_ANDROID.equals(attribute.getNamePrefix());
return;
} }
ResXmlStartNamespace ns = getOrCreateAndroid();
attribute.setNamespaceReference(ns.getUriReference()); private static boolean isAndroid(int pkgId){
}
private ResXmlStartNamespace getOrCreateApp(){
if(mNsApp !=null){
return mNsApp;
}
ResXmlElement root=getRootElement();
ResXmlStartNamespace ns = root.getOrCreateNamespace(URI_APP, PREFIX_APP);
String prefix=ns.getPrefix();
if(PREFIX_ANDROID.equals(prefix) || isEmpty(prefix)){
ns.setPrefix(PREFIX_APP);
}
mNsApp = ns;
return mNsApp;
}
private ResXmlStartNamespace getOrCreateAndroid(){
if(mNsAndroid !=null){
return mNsAndroid;
}
ResXmlElement root=getRootElement();
ResXmlStartNamespace ns = root.getOrCreateNamespace(URI_ANDROID, PREFIX_ANDROID);
if(!PREFIX_ANDROID.equals(ns.getPrefix())){
ns.setPrefix(PREFIX_ANDROID);
}
mNsAndroid =ns;
return mNsAndroid;
}
private ResXmlElement getRootElement() {
if(mRootElement==null){
mRootElement = xmlBlock.getResXmlElement();
}
return mRootElement;
}
private List<ResXmlAttribute> getAttributes(){
if(mAttributeList==null){
mAttributeList=listAttributes(xmlBlock.getResXmlElement());
}
return mAttributeList;
}
private List<ResXmlAttribute> listAttributes(ResXmlElement element){
List<ResXmlAttribute> results = new ArrayList<>(element.listAttributes());
for(ResXmlElement child:element.listElements()){
results.addAll(listAttributes(child));
}
return results;
}
private boolean isAndroid(int pkgId){
return pkgId==1; return pkgId==1;
} }
private int toPackageId(int resId){ private static int toPackageId(int resId){
return (resId >> 24 & 0xFF); return (resId >> 24 & 0xFF);
} }
private static boolean isEmpty(String str){ private static boolean isEmpty(String str){

View File

@ -184,6 +184,18 @@ public class ResXmlAttribute extends ValueItem implements AttributeValue, Compar
int getNamespaceReference(){ int getNamespaceReference(){
return getInteger(getBytesInternal(), OFFSET_NS); return getInteger(getBytesInternal(), OFFSET_NS);
} }
public void setNamespace(String uri, String prefix){
if(uri == null || prefix == null){
setNamespaceReference(-1);
return;
}
ResXmlElement parentElement = getParentResXmlElement();
if(parentElement == null){
return;
}
ResXmlStartNamespace ns = parentElement.getOrCreateNamespace(uri, prefix);
setNamespaceReference(ns.getUriReference());
}
public void setNamespaceReference(int ref){ public void setNamespaceReference(int ref){
if(ref == getNamespaceReference()){ if(ref == getNamespaceReference()){
return; return;

View File

@ -110,7 +110,7 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
} }
return null; return null;
} }
public String getEndComment(){ String getEndComment(){
ResXmlEndElement end = getEndElement(); ResXmlEndElement end = getEndElement();
if(end!=null){ if(end!=null){
return end.getComment(); return end.getComment();
@ -143,36 +143,30 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
public ResXmlAttribute newAttribute(){ public ResXmlAttribute newAttribute(){
return getStartElement().newAttribute(); return getStartElement().newAttribute();
} }
@Override
void onRemoved(){ void onRemoved(){
for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){ for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){
startNamespace.onRemoved(); startNamespace.onRemoved();
} }
ResXmlStartElement start = getStartElement(); ResXmlStartElement start = getStartElement();
if(start!=null){ if(start != null){
start.onRemoved(); start.onRemoved();
} }
ResXmlText resXmlText=getResXmlText(); for(ResXmlNode xmlNode : listXmlNodes()){
if(resXmlText!=null){ xmlNode.onRemoved();
resXmlText.onRemoved();
}
for(ResXmlElement child:listElements()){
child.onRemoved();
} }
} }
@Override
void linkStringReferences(){ void linkStringReferences(){
for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){ for(ResXmlStartNamespace startNamespace:getStartNamespaceList()){
startNamespace.linkStringReferences(); startNamespace.linkStringReferences();
} }
ResXmlStartElement start = getStartElement(); ResXmlStartElement start = getStartElement();
if(start!=null){ if(start != null){
start.linkStringReferences(); start.linkStringReferences();
} }
ResXmlText resXmlText=getResXmlText(); for(ResXmlNode xmlNode : getXmlNodes()){
if(resXmlText!=null){ xmlNode.linkStringReferences();
resXmlText.linkStringReferences();
}
for(ResXmlElement child:listElements()){
child.linkStringReferences();
} }
} }
public ResXmlElement createChildElement(){ public ResXmlElement createChildElement(){
@ -355,13 +349,11 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
@Override @Override
public int getDepth(){ public int getDepth(){
int depth = 0;
ResXmlElement parent = getParentResXmlElement(); ResXmlElement parent = getParentResXmlElement();
while (parent!=null){ if(parent != null){
depth++; return parent.getDepth() + 1;
parent = parent.getParentResXmlElement();
} }
return depth; return 0;
} }
@Override @Override
void addEvents(ParserEventList parserEventList){ void addEvents(ParserEventList parserEventList){
@ -423,7 +415,7 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
if(xmlNode==null){ if(xmlNode==null){
continue; continue;
} }
xmlNode.onRemove(); xmlNode.onRemoved();
mBody.remove(xmlNode); mBody.remove(xmlNode);
} }
} }
@ -433,6 +425,22 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
public int countResXmlNodes(){ public int countResXmlNodes(){
return mBody.size(); return mBody.size();
} }
public boolean hasText(){
for(ResXmlNode xmlNode : getXmlNodes()){
if(xmlNode instanceof ResXmlTextNode){
return true;
}
}
return false;
}
public boolean hasElement(){
for(ResXmlNode xmlNode : getXmlNodes()){
if(xmlNode instanceof ResXmlElement){
return true;
}
}
return false;
}
public List<ResXmlNode> listXmlNodes(){ public List<ResXmlNode> listXmlNodes(){
return new ArrayList<>(getXmlNodes()); return new ArrayList<>(getXmlNodes());
} }
@ -479,8 +487,8 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
return results; return results;
} }
public ResXmlElement getRootResXmlElement(){ public ResXmlElement getRootResXmlElement(){
ResXmlElement parent=getParentResXmlElement(); ResXmlElement parent = getParentResXmlElement();
if(parent!=null){ if(parent != null){
return parent.getRootResXmlElement(); return parent.getRootResXmlElement();
} }
return this; return this;
@ -503,13 +511,31 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
} }
return null; return null;
} }
public ResXmlStartNamespace getNamespace(String uri, String prefix){
if(uri == null || prefix == null){
return null;
}
for(ResXmlStartNamespace ns : mStartNamespaceList.getChildes()){
if(uri.equals(ns.getUri()) && prefix.equals(ns.getPrefix())){
return ns;
}
}
ResXmlElement xmlElement = getParentResXmlElement();
if(xmlElement != null){
return xmlElement.getNamespace(uri, prefix);
}
return null;
}
public ResXmlStartNamespace getOrCreateNamespace(String uri, String prefix){ public ResXmlStartNamespace getOrCreateNamespace(String uri, String prefix){
ResXmlStartNamespace exist=getStartNamespaceByUri(uri); ResXmlStartNamespace exist = getNamespace(uri, prefix);
if(exist!=null){ if(exist != null){
return exist; return exist;
} }
ResXmlStartNamespace startNamespace=new ResXmlStartNamespace(); return getRootResXmlElement().createNamespace(uri, prefix);
ResXmlEndNamespace endNamespace=new ResXmlEndNamespace(); }
public ResXmlStartNamespace createNamespace(String uri, String prefix){
ResXmlStartNamespace startNamespace = new ResXmlStartNamespace();
ResXmlEndNamespace endNamespace = new ResXmlEndNamespace();
startNamespace.setEnd(endNamespace); startNamespace.setEnd(endNamespace);
addStartNamespace(startNamespace); addStartNamespace(startNamespace);
@ -562,7 +588,7 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
public void addStartNamespace(ResXmlStartNamespace item){ public void addStartNamespace(ResXmlStartNamespace item){
mStartNamespaceList.add(item); mStartNamespaceList.add(item);
} }
public List<ResXmlEndNamespace> getEndNamespaceList(){ private List<ResXmlEndNamespace> getEndNamespaceList(){
return mEndNamespaceList.getChildes(); return mEndNamespaceList.getChildes();
} }
public void addEndNamespace(ResXmlEndNamespace item){ public void addEndNamespace(ResXmlEndNamespace item){
@ -596,26 +622,17 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
public ResXmlStartElement getStartElement(){ public ResXmlStartElement getStartElement(){
return mStartElementContainer.getItem(); return mStartElementContainer.getItem();
} }
public void setStartElement(ResXmlStartElement item){ private void setStartElement(ResXmlStartElement item){
mStartElementContainer.setItem(item); mStartElementContainer.setItem(item);
} }
public ResXmlEndElement getEndElement(){ private ResXmlEndElement getEndElement(){
return mEndElementContainer.getItem(); return mEndElementContainer.getItem();
} }
public void setEndElement(ResXmlEndElement item){ private void setEndElement(ResXmlEndElement item){
mEndElementContainer.setItem(item); mEndElementContainer.setItem(item);
} }
// Use listXmlText() instead to be removed on next version
@Deprecated
public ResXmlText getResXmlText(){
List<ResXmlText> xmlTextList=listXmlText();
if(xmlTextList.size()==0){
return null;
}
return xmlTextList.get(0);
}
public void addResXmlTextNode(ResXmlTextNode xmlTextNode){ public void addResXmlTextNode(ResXmlTextNode xmlTextNode){
mBody.add(xmlTextNode); mBody.add(xmlTextNode);
} }
@ -624,16 +641,6 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
addResXmlTextNode(new ResXmlTextNode(xmlText)); addResXmlTextNode(new ResXmlTextNode(xmlText));
} }
} }
// Use addResXmlText()
@Deprecated
public void setResXmlText(ResXmlText xmlText){
addResXmlText(xmlText);
}
@Deprecated
public void setResXmlText(String text){
clearChildes();
addResXmlText(text);
}
public void addResXmlText(String text){ public void addResXmlText(String text){
if(text==null){ if(text==null){
return; return;
@ -995,13 +1002,14 @@ public class ResXmlElement extends ResXmlNode implements JSONConvert<JSONObject>
public String toString(){ public String toString(){
ResXmlStartElement start = getStartElement(); ResXmlStartElement start = getStartElement();
if(start!=null){ if(start!=null){
ResXmlText text=getResXmlText();
StringBuilder builder=new StringBuilder(); StringBuilder builder=new StringBuilder();
builder.append("<"); builder.append("<");
builder.append(start.toString()); builder.append(start.toString());
if(text!=null){ if(hasText() && !hasElement()){
builder.append(">"); builder.append(">");
builder.append(text.toString()); for(ResXmlText xmlText : listXmlText()){
builder.append(xmlText.getText());
}
builder.append("</"); builder.append("</");
builder.append(start.getTagName()); builder.append(start.getTagName());
builder.append(">"); builder.append(">");

View File

@ -1,4 +1,4 @@
/* /*
* Copyright (C) 2022 github.com/REAndroid * Copyright (C) 2022 github.com/REAndroid
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -23,10 +23,10 @@ public abstract class ResXmlNode extends FixedBlockContainer implements JSONCon
ResXmlNode(int childesCount) { ResXmlNode(int childesCount) {
super(childesCount); super(childesCount);
} }
void onRemove(){ abstract void onRemoved();
} abstract void linkStringReferences();
public abstract int getDepth(); public abstract int getDepth();
abstract void addEvents(ParserEventList parserEventList); abstract void addEvents(ParserEventList parserEventList);
public static final String NAME_node_type="node_type"; public static final String NAME_node_type = "node_type";
} }

View File

@ -1,4 +1,4 @@
/* /*
* Copyright (C) 2022 github.com/REAndroid * Copyright (C) 2022 github.com/REAndroid
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -75,6 +75,14 @@ public class ResXmlTextNode extends ResXmlNode {
getResXmlText().setTextReference(ref); getResXmlText().setTextReference(ref);
} }
@Override @Override
void onRemoved(){
getResXmlText().onRemoved();
}
@Override
void linkStringReferences(){
getResXmlText().linkStringReferences();
}
@Override
public String toString(){ public String toString(){
String txt=getText(); String txt=getText();
if(txt!=null){ if(txt!=null){