/* * Copyright 2017-present Facebook, Inc. * * 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.facebook.buck.android.resources; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import java.io.PrintStream; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; /** * ResourcesXml handles Android's compiled xml format. It consists of: ResChunk_header u16 * chunk_type u16 header_size u32 chunk_size * * <p>The header is followed by a StringPool containing all the strings used in the xml. This is * then followed by an optional RefMap. After the StringPool/RefMap comes a list of xml nodes of the * form: ResChunk_header u16 chunk_type u16 header_size u32 chunk_size u32 lineNumber StringRef * comment * * <p>Each node contains extra information depending on the type: * * <p>XML_START_NS: StringRef prefix StringRef uri * * <p>XML_END_NS: StringRef prefix StringRef uri * * <p>XML_CDATA: StringRef data Res_value typedData * * <p>XML_END_ELEMENT: StringRef namespace StringRef name * * <p>XML_START_ELEMENT: StringRef namespace StringRef name u16 attrStart u16 attrSize u16 attrCount * u16 idIndex u16 classIndex u16 styleIndex * * <p>XML_START_ELEMENT is then followed by attrCount attributes of the form: StringRef namespace * StringRef name StringRef rawValue Res_value typedValue */ public class ResourcesXml extends ResChunk { public static final int HEADER_SIZE = 8; private static final int XML_FIRST_TYPE = 0x100; private static final int XML_START_NS = 0x100; private static final int XML_END_NS = 0x101; private static final int XML_START_ELEMENT = 0x102; private static final int XML_END_ELEMENT = 0x103; private static final int XML_CDATA = 0x104; private static final int XML_LAST_TYPE = 0x104; private final StringPool strings; private final Optional<RefMap> refMap; private final ByteBuffer nodeBuf; public static ResourcesXml get(ByteBuffer buf) { int type = buf.getShort(); int headerSize = buf.getShort(); int chunkSize = buf.getInt(); Preconditions.checkState(type == CHUNK_XML_TREE); Preconditions.checkState(headerSize == HEADER_SIZE); Preconditions.checkState(chunkSize == buf.limit()); // The header should be immediately followed by a string pool. StringPool strings = StringPool.get(slice(buf, HEADER_SIZE)); buf.position(HEADER_SIZE + strings.getChunkSize()); int nextType = buf.getShort(buf.position()); Optional<RefMap> refMap = Optional.empty(); if (nextType == ResChunk.CHUNK_XML_REF_MAP) { RefMap map = new RefMap(slice(buf, buf.position())); refMap = Optional.of(map); buf.position(buf.position() + map.getChunkSize()); } return new ResourcesXml(buf.limit(), strings, refMap, slice(buf, buf.position())); } @Override public void put(ByteBuffer buf) { putChunkHeader(buf); strings.put(buf); refMap.ifPresent(m -> m.put(buf)); buf.put(slice(nodeBuf, 0)); } private ResourcesXml( int chunkSize, StringPool strings, Optional<RefMap> refMap, ByteBuffer nodeBuf) { super(CHUNK_XML_TREE, HEADER_SIZE, chunkSize); this.strings = strings; this.refMap = refMap; this.nodeBuf = nodeBuf; } public void transformReferences(RefTransformer visitor) { refMap.ifPresent(m -> m.visitReferences(visitor)); int offset = 0; while (offset < nodeBuf.limit()) { int type = nodeBuf.getShort(offset); if (type == XML_START_ELEMENT) { int nodeHeaderSize = nodeBuf.getShort(offset + 2); int extOffset = offset + nodeHeaderSize; int attrStart = extOffset + nodeBuf.getShort(extOffset + 8); Preconditions.checkState(attrStart == extOffset + 20); int attrCount = nodeBuf.getShort(extOffset + 12); for (int i = 0; i < attrCount; i++) { int attrOffset = attrStart + i * 20; int attrType = nodeBuf.get(attrOffset + 15); switch (attrType) { case RES_REFERENCE: case RES_ATTRIBUTE: transformEntryDataOffset(nodeBuf, attrOffset + 16, visitor); break; case RES_DYNAMIC_ATTRIBUTE: case RES_DYNAMIC_REFERENCE: throw new UnsupportedOperationException(); default: break; } } } int chunkSize = nodeBuf.getInt(offset + 4); offset += chunkSize; } } public void visitReferences(RefVisitor visitor) { transformReferences( i -> { visitor.visit(i); return i; }); } public void dump(PrintStream out) { int indent = 0; final Map<String, String> nsMap = new HashMap<>(); nodeBuf.position(0); while (nodeBuf.position() < nodeBuf.limit()) { int nodeSize = nodeBuf.get(nodeBuf.position() + 4); indent = dumpNode( out, strings, refMap, indent, nsMap, slice(nodeBuf, nodeBuf.position(), nodeSize)); nodeBuf.position(nodeBuf.position() + nodeSize); } } private static int dumpNode( PrintStream out, StringPool strings, Optional<RefMap> refMap, int indent, Map<String, String> nsMap, ByteBuffer buf) { int type = buf.getShort(0); Preconditions.checkState(type >= XML_FIRST_TYPE && type <= XML_LAST_TYPE); switch (type) { case XML_START_NS: { // start namespace int prefixId = buf.getInt(16); String prefix = prefixId == -1 ? "" : strings.getString(prefixId); int uriId = buf.getInt(20); String uri = strings.getString(uriId); out.format("%sN: %s=%s\n", Strings.padEnd("", indent * 2, ' '), prefix, uri); indent++; nsMap.put(uri, prefix); break; } case XML_END_NS: indent--; break; case XML_START_ELEMENT: { // start element int lineNumber = buf.getInt(8); int nameId = buf.getInt(20); String name = strings.getString(nameId); int attrCount = buf.getShort(28); ByteBuffer attrExt = slice(buf, 36); out.format("%sE: %s (line=%d)\n", Strings.padEnd("", indent * 2, ' '), name, lineNumber); indent++; for (int i = 0; i < attrCount; i++) { dumpAttribute(out, strings, refMap, slice(attrExt, attrExt.position()), indent, nsMap); attrExt.position(attrExt.position() + 20); } break; } case XML_END_ELEMENT: indent--; break; case XML_CDATA: throw new UnsupportedOperationException(); } return indent; } public StringPool getStrings() { return strings; } private static void dumpAttribute( PrintStream out, StringPool strings, Optional<RefMap> refMap, ByteBuffer buf, int indent, Map<String, String> nsMap) { int nsId = buf.getInt(0); String namespace = nsId == -1 ? "" : strings.getString(nsId); String shortNs = nsMap.get(namespace); int nameIndex = buf.getInt(4); String name = strings.getString(nameIndex); int resValue = refMap.isPresent() ? refMap.get().getRef(nameIndex) : -1; int rawValueIndex = buf.getInt(8); String rawValue = rawValueIndex < 0 ? null : strings.getOutputNormalizedString(rawValueIndex); int attrType = buf.get(15); int data = buf.getInt(16); String dumpValue = getValueForDump(strings, rawValue, attrType, data); out.format( "%sA: %s%s%s=%s%s\n", Strings.padEnd("", indent * 2, ' '), shortNs == null ? "" : shortNs + ":", name, resValue == -1 ? "" : String.format("(0x%08x)", resValue), dumpValue, rawValue == null ? "" : String.format(" (Raw: \"%s\")", rawValue)); } @Nullable private static String getValueForDump( StringPool strings, @Nullable String rawValue, int attrType, int data) { switch (attrType) { case RES_REFERENCE: return String.format("@0x%x", data); case RES_STRING: return String.format("\"%s\"", strings.getString(data).replace("\\", "\\\\")); case RES_FLOAT: case RES_DECIMAL: case RES_HEX: case RES_BOOL: return String.format("(type 0x%x)0x%x", attrType, data); case RES_DYNAMIC_ATTRIBUTE: case RES_DYNAMIC_REFERENCE: throw new UnsupportedOperationException(); default: return rawValue; } } /** * A RefMap is an optional entry in a compiled xml that provides the reference ids for some of the * strings in the stringpool. It consists of ResTable_header u32 chunk_type u32 header_size u32 * chunk_size * * <p>The header is followed by an array of u32 ids of size (chunk_size - header_size) / 4. Each * entry is the reference id for the string with the same position in the string pool. */ private static class RefMap extends ResChunk { private final ByteBuffer buf; private final int refCount; RefMap(ByteBuffer buf) { super(buf.getShort(), buf.getShort(), buf.getInt()); this.buf = slice(buf, 0, getChunkSize()); this.refCount = (getChunkSize() - getHeaderSize()) / 4; Preconditions.checkState(refCount >= 0); } @Override public void put(ByteBuffer output) { output.put(slice(buf, 0)); } int getRef(int index) { if (index < refCount) { return buf.getInt(getHeaderSize() + index * 4); } return -1; } public void visitReferences(RefTransformer visitor) { for (int i = 0; i < refCount; i++) { transformEntryDataOffset(buf, getHeaderSize() + i * 4, visitor); } } } }