PackageIdentifier.java
1 /* 2 * Copyright (C) 2022 github.com/REAndroid 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.reandroid.identifiers; 17 18 import com.reandroid.arsc.chunk.PackageBlock; 19 import com.reandroid.arsc.chunk.TypeBlock; 20 import com.reandroid.arsc.container.SpecTypePair; 21 import com.reandroid.arsc.model.ResourceEntry; 22 import com.reandroid.utils.HexUtil; 23 import com.reandroid.utils.io.IOUtil; 24 import com.reandroid.arsc.value.Entry; 25 import com.reandroid.json.JSONObject; 26 import com.reandroid.xml.XMLFactory; 27 import org.xmlpull.v1.XmlPullParser; 28 import org.xmlpull.v1.XmlPullParserException; 29 import org.xmlpull.v1.XmlSerializer; 30 31 import java.io.*; 32 import java.nio.charset.StandardCharsets; 33 import java.util.ArrayList; 34 import java.util.Iterator; 35 import java.util.List; 36 37 public class PackageIdentifier extends IdentifierMap<TypeIdentifier>{ 38 private PackageBlock mPackageBlock; 39 private int mPackageLoadStamp; 40 public PackageIdentifier(int id, String name){ 41 super(id, name); 42 } 43 public PackageIdentifier(){ 44 this(0, null); 45 } 46 47 private void initializePackageJson(PackageBlock packageBlock){ 48 File jsonFile = searchPackageJsonFromTag(); 49 if(jsonFile == null){ 50 return; 51 } 52 try { 53 JSONObject jsonObject = new JSONObject(new FileInputStream(jsonFile)); 54 packageBlock.fromJson(jsonObject); 55 if(getName() == null){ 56 setName(packageBlock.getName()); 57 } 58 } catch (FileNotFoundException ignored) { 59 } 60 } 61 // public.xml file is assumed to be stored via setTag during loadPublicXml(File) 62 private File searchPackageJsonFromTag(){ 63 Object tag = getTag(); 64 if(!(tag instanceof File)){ 65 return null; 66 } 67 File publicXml = (File) tag; 68 File dir = publicXml.getParentFile(); 69 //values 70 if(dir == null || !"values".equals(dir.getName())){ 71 return null; 72 } 73 dir = dir.getParentFile(); 74 //res 75 if(dir == null){ 76 return null; 77 } 78 dir = dir.getParentFile(); 79 if(dir == null){ 80 return null; 81 } 82 File json = new File(dir, "package.json"); 83 if(!json.isFile()){ 84 return null; 85 } 86 return json; 87 } 88 public List<ResourceIdentifier> listDuplicateResources(){ 89 List<ResourceIdentifier> results = new ArrayList<>(); 90 for(TypeIdentifier typeIdentifier : list()){ 91 results.addAll(typeIdentifier.listDuplicates()); 92 } 93 return results; 94 } 95 public boolean hasDuplicateResources(){ 96 for(TypeIdentifier typeIdentifier : getItems()){ 97 if(typeIdentifier.hasDuplicates()){ 98 return true; 99 } 100 } 101 return false; 102 } 103 public List<ResourceIdentifier> ensureUniqueResourceNames(){ 104 List<ResourceIdentifier> results = new ArrayList<>(); 105 for(TypeIdentifier typeIdentifier : list()){ 106 results.addAll(typeIdentifier.ensureUniqueResourceNames()); 107 } 108 return results; 109 } 110 public void setResourceNamesToPackage(){ 111 setResourceNamesToPackage(getPackageBlock()); 112 } 113 public void setResourceNamesToPackage(PackageBlock packageBlock){ 114 if(packageBlock == null){ 115 return; 116 } 117 for(SpecTypePair specTypePair:packageBlock.listSpecTypePairs()){ 118 Iterator<ResourceEntry> itr = specTypePair.getResources(); 119 while (itr.hasNext()){ 120 setResourceNamesToEntry(itr.next()); 121 } 122 } 123 } 124 public void setResourceNamesToEntry(ResourceEntry resourceEntry){ 125 ResourceIdentifier ri = getResourceIdentifier(resourceEntry.getResourceId()); 126 if(ri == null){ 127 return; 128 } 129 resourceEntry.setName(ri.getName()); 130 } 131 public ResourceIdentifier getResourceIdentifier(int resourceId){ 132 TypeIdentifier typeIdentifier = get((resourceId >> 16) & 0xff); 133 if(typeIdentifier != null){ 134 return typeIdentifier.get(resourceId & 0xffff); 135 } 136 return null; 137 } 138 public ResourceIdentifier getResourceIdentifier(String type, String name){ 139 TypeIdentifier typeIdentifier = get(type); 140 if(typeIdentifier != null){ 141 return typeIdentifier.get(name); 142 } 143 return null; 144 } 145 public int getResourcesCount(){ 146 int result = 0; 147 for(TypeIdentifier ti : getItems()){ 148 result += ti.size(); 149 } 150 return result; 151 } 152 153 public void writePublicXml(File file) throws IOException { 154 XmlSerializer serializer = XMLFactory.newSerializer(file); 155 write(serializer); 156 IOUtil.close(serializer); 157 } 158 public void writePublicXml(OutputStream outputStream) throws IOException { 159 XmlSerializer serializer = XMLFactory.newSerializer(outputStream); 160 serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()); 161 write(serializer); 162 } 163 public void write(XmlSerializer serializer) throws IOException { 164 serializer.startDocument("utf-8", null); 165 serializer.text("\n"); 166 serializer.startTag(null, XML_TAG_RESOURCES); 167 writePackageInfo(serializer); 168 writeTypes(serializer); 169 serializer.text("\n"); 170 serializer.endTag(null, XML_TAG_RESOURCES); 171 serializer.endDocument(); 172 closeSerializer(serializer); 173 } 174 private void writePackageInfo(XmlSerializer serializer) throws IOException { 175 String name = getName(); 176 if(name != null){ 177 serializer.attribute(null, XML_ATTRIBUTE_PACKAGE, name); 178 } 179 int id = getId(); 180 if(id != 0){ 181 serializer.attribute(null, XML_ATTRIBUTE_ID, HexUtil.toHex2((byte)id)); 182 } 183 } 184 private void writeTypes(XmlSerializer serializer) throws IOException { 185 for(TypeIdentifier typeIdentifier : list()){ 186 typeIdentifier.write(serializer); 187 } 188 } 189 public void load(PackageBlock packageBlock){ 190 setId(packageBlock.getId()); 191 setName(packageBlock.getName()); 192 loadEntryGroups(packageBlock); 193 setPackageBlock(packageBlock); 194 } 195 private void loadEntryGroups(PackageBlock packageBlock){ 196 for(SpecTypePair specTypePair:packageBlock.listSpecTypePairs()){ 197 Iterator<ResourceEntry> itr = specTypePair.getResources(); 198 while (itr.hasNext()){ 199 add(itr.next()); 200 } 201 } 202 } 203 public void loadPublicXml(File file) throws IOException, XmlPullParserException { 204 XmlPullParser parser = XMLFactory.newPullParser(file); 205 loadPublicXml(parser); 206 } 207 public void loadPublicXml(InputStream inputStream) throws IOException, XmlPullParserException { 208 XmlPullParser parser = XMLFactory.newPullParser(inputStream); 209 loadPublicXml(parser); 210 } 211 public void loadPublicXml(Reader reader) throws IOException, XmlPullParserException { 212 XmlPullParser parser = XMLFactory.newPullParser(reader); 213 loadPublicXml(parser); 214 } 215 public void loadPublicXml(XmlPullParser parser) throws IOException, XmlPullParserException { 216 boolean resourcesFound = false; 217 int event; 218 while ((event = parser.nextToken()) != XmlPullParser.END_DOCUMENT){ 219 if(event != XmlPullParser.START_TAG){ 220 continue; 221 } 222 if(!resourcesFound){ 223 resourcesFound = parser.getName().equals(XML_TAG_RESOURCES); 224 if(!resourcesFound){ 225 throw new XmlPullParserException("Invalid public.xml, expecting first tag '" 226 + getName() + "' " + parser.getPositionDescription()); 227 } 228 loadPackageInfo(parser); 229 continue; 230 } 231 parseEntry(parser); 232 } 233 closeParser(parser); 234 } 235 private void closeParser(XmlPullParser parser){ 236 if(!(parser instanceof Closeable)){ 237 return; 238 } 239 Closeable closeable = (Closeable)parser; 240 try { 241 closeable.close(); 242 } catch (IOException ignored) { 243 } 244 } 245 private void closeSerializer(XmlSerializer serializer){ 246 if(!(serializer instanceof Closeable)){ 247 return; 248 } 249 Closeable closeable = (Closeable)serializer; 250 try { 251 closeable.close(); 252 } catch (IOException ignored) { 253 } 254 } 255 private void loadPackageInfo(XmlPullParser parser){ 256 int count = parser.getAttributeCount(); 257 for(int i = 0; i < count; i++){ 258 if(XML_ATTRIBUTE_PACKAGE.equals(parser.getAttributeName(i))){ 259 setName(parser.getAttributeValue(i)); 260 }else if(XML_ATTRIBUTE_ID.equals(parser.getAttributeName(i))){ 261 int id = Integer.decode(parser.getAttributeValue(i)); 262 if(id != 0){ 263 setId(id); 264 } 265 } 266 } 267 } 268 private void parseEntry(XmlPullParser parser) throws XmlPullParserException { 269 if(!XML_TAG_PUBLIC.equals(parser.getName())){ 270 throw new XmlPullParserException("Invalid tag, expecting '" 271 + XML_TAG_PUBLIC + "' " + parser.getPositionDescription()); 272 } 273 String resourceIdStr = null; 274 String typeName = null; 275 String entryName = null; 276 int count = parser.getAttributeCount(); 277 for(int i = 0; i < count; i++){ 278 String attrName = parser.getAttributeName(i); 279 String value = parser.getAttributeValue(i); 280 if(XML_ATTRIBUTE_ID.equals(attrName)){ 281 resourceIdStr = value; 282 }else if(XML_ATTRIBUTE_TYPE.equals(attrName)){ 283 typeName = value; 284 }else if(XML_ATTRIBUTE_NAME.equals(attrName)){ 285 entryName = value; 286 } 287 } 288 if(typeName == null){ 289 throw new XmlPullParserException("Missing attribute '" 290 + XML_ATTRIBUTE_TYPE + "' " + parser.getPositionDescription()); 291 } 292 if(resourceIdStr == null){ 293 throw new XmlPullParserException("Missing attribute '" 294 + XML_ATTRIBUTE_ID + "' " + parser.getPositionDescription()); 295 } 296 if(entryName == null){ 297 throw new XmlPullParserException("Missing attribute '" 298 + XML_ATTRIBUTE_NAME + "' " + parser.getPositionDescription()); 299 } 300 301 int resourceId = (int) Long.decode(resourceIdStr).longValue(); 302 int packageId = (resourceId >> 24) & 0xff; 303 int typeId = (resourceId >> 16) & 0xff; 304 int entryId = resourceId & 0xffff; 305 306 TypeIdentifier typeIdentifier = getOrCreate(typeId, typeName); 307 ResourceIdentifier entry = new ResourceIdentifier(entryId, entryName); 308 typeIdentifier.add(entry); 309 if(getId() == 0){ 310 setId(packageId); 311 } 312 } 313 314 public PackageBlock getPackageBlock() { 315 return mPackageBlock; 316 } 317 public void setPackageBlock(PackageBlock packageBlock) { 318 this.mPackageBlock = packageBlock; 319 } 320 321 public void add(ResourceEntry resourceEntry){ 322 add(resourceEntry.get()); 323 } 324 public void add(Entry entry){ 325 if(entry == null || entry.isNull()){ 326 return; 327 } 328 TypeBlock typeBlock = entry.getTypeBlock(); 329 TypeIdentifier typeIdentifier = getOrCreate(typeBlock.getId(), typeBlock.getTypeName()); 330 ResourceIdentifier resourceIdentifier = new ResourceIdentifier(entry.getId(), entry.getName()); 331 typeIdentifier.add(resourceIdentifier); 332 } 333 private TypeIdentifier getOrCreate(int typeId, String typeName){ 334 TypeIdentifier identifier = get(typeId); 335 if(identifier == null){ 336 return super.add(new TypeIdentifier(typeId, typeName)); 337 } 338 if(typeName !=null && identifier.getName() == null){ 339 identifier.setName(typeName); 340 identifier = super.add(identifier); 341 } 342 return identifier; 343 } 344 @Override 345 public void clear(){ 346 for(TypeIdentifier identifier : getItems()){ 347 identifier.clear(); 348 } 349 super.clear(); 350 } 351 352 public int renameSpecs(){ 353 int result = 0; 354 for(TypeIdentifier ti : getItems()){ 355 int renamed = ti.renameSpecs(); 356 result = result + renamed; 357 } 358 return result; 359 } 360 public int renameDuplicateSpecs(){ 361 int result = 0; 362 for(TypeIdentifier ti : getItems()){ 363 int renamed = ti.renameDuplicateSpecs(); 364 result = result + renamed; 365 } 366 return result; 367 } 368 public int renameBadSpecs(){ 369 int result = 0; 370 for(TypeIdentifier ti : getItems()){ 371 int renamed = ti.renameBadSpecs(); 372 result = result + renamed; 373 } 374 return result; 375 } 376 public String validateSpecNames(){ 377 int duplicates = renameDuplicateSpecs(); 378 int bad = renameBadSpecs(); 379 if(duplicates == 0 && bad == 0){ 380 return null; 381 } 382 return "Spec names validated, duplicates = " + duplicates 383 + ", bad = " + bad; 384 } 385 @Override 386 void setCaseInsensitive(boolean caseInsensitive){ 387 super.setCaseInsensitive(caseInsensitive); 388 for(TypeIdentifier ti : getItems()){ 389 ti.setCaseInsensitive(caseInsensitive); 390 } 391 } 392 }