tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UnusedResources.java (25216B)


      1 /*
      2 * Copyright (C) 2014 The Android Open Source Project
      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 
     17 // Modifications are owned by the Chromium Authors.
     18 // Copyright 2021 The Chromium Authors
     19 // Use of this source code is governed by a BSD-style license that can be
     20 // found in the LICENSE file.
     21 
     22 package build.android.unused_resources;
     23 
     24 import static com.android.ide.common.symbols.SymbolIo.readFromAapt;
     25 import static com.android.utils.SdkUtils.endsWithIgnoreCase;
     26 
     27 import static com.google.common.base.Charsets.UTF_8;
     28 
     29 import com.android.ide.common.resources.usage.ResourceUsageModel;
     30 import com.android.ide.common.resources.usage.ResourceUsageModel.Resource;
     31 import com.android.ide.common.symbols.Symbol;
     32 import com.android.ide.common.symbols.SymbolTable;
     33 import com.android.resources.ResourceFolderType;
     34 import com.android.resources.ResourceType;
     35 import com.android.tools.r8.CompilationFailedException;
     36 import com.android.tools.r8.ProgramResource;
     37 import com.android.tools.r8.ProgramResourceProvider;
     38 import com.android.tools.r8.ResourceShrinker;
     39 import com.android.tools.r8.ResourceShrinker.Command;
     40 import com.android.tools.r8.ResourceShrinker.ReferenceChecker;
     41 import com.android.tools.r8.origin.PathOrigin;
     42 import com.android.utils.XmlUtils;
     43 
     44 import com.google.common.base.Charsets;
     45 import com.google.common.collect.Maps;
     46 import com.google.common.io.ByteStreams;
     47 import com.google.common.io.Closeables;
     48 import com.google.common.io.Files;
     49 
     50 import org.w3c.dom.Document;
     51 import org.w3c.dom.Node;
     52 import org.xml.sax.SAXException;
     53 
     54 import java.io.File;
     55 import java.io.FileInputStream;
     56 import java.io.IOException;
     57 import java.io.PrintWriter;
     58 import java.io.StringWriter;
     59 import java.nio.file.Path;
     60 import java.nio.file.Paths;
     61 import java.util.Arrays;
     62 import java.util.Collections;
     63 import java.util.List;
     64 import java.util.Map;
     65 import java.util.concurrent.ExecutionException;
     66 import java.util.stream.Collectors;
     67 import java.util.zip.ZipEntry;
     68 import java.util.zip.ZipInputStream;
     69 
     70 import javax.xml.parsers.ParserConfigurationException;
     71 
     72 /**
     73  Copied with modifications from gradle core source
     74  https://cs.android.com/search?q=f:build-system.*ResourceUsageAnalyzer.java
     75 
     76  Modifications are mostly to:
     77    - Remove unused code paths to reduce complexity.
     78    - Reduce dependencies unless absolutely required.
     79 */
     80 
     81 public class UnusedResources {
     82    private static final String ANDROID_RES = "android_res/";
     83    private static final String DOT_DEX = ".dex";
     84    private static final String DOT_CLASS = ".class";
     85    private static final String DOT_XML = ".xml";
     86    private static final String DOT_JAR = ".jar";
     87    private static final String FN_RESOURCE_TEXT = "R.txt";
     88 
     89    /* A source of resource classes to track, can be either a folder or a jar */
     90    private final Iterable<File> mRTxtFiles;
     91    private final File mProguardMapping;
     92    /** These can be class or dex files. */
     93    private final Iterable<File> mClasses;
     94    private final Iterable<File> mManifests;
     95    private final Iterable<File> mResourceDirs;
     96 
     97    private final File mReportFile;
     98    private final StringWriter mDebugOutput;
     99    private final PrintWriter mDebugPrinter;
    100 
    101    /** The computed set of unused resources */
    102    private List<Resource> mUnused;
    103 
    104    /**
    105     * Map from resource class owners (VM format class) to corresponding resource entries.
    106     * This lets us map back from code references (obfuscated class and possibly obfuscated field
    107     * reference) back to the corresponding resource type and name.
    108     */
    109    private Map<String, Pair<ResourceType, Map<String, String>>> mResourceObfuscation =
    110            Maps.newHashMapWithExpectedSize(30);
    111 
    112    /** Obfuscated name of android/support/v7/widget/SuggestionsAdapter.java */
    113    private String mSuggestionsAdapter;
    114 
    115    /** Obfuscated name of android/support/v7/internal/widget/ResourcesWrapper.java */
    116    private String mResourcesWrapper;
    117 
    118    /* A Pair class because java does not come with batteries included. */
    119    private static class Pair<U, V> {
    120        private U mFirst;
    121        private V mSecond;
    122 
    123        Pair(U first, V second) {
    124            this.mFirst = first;
    125            this.mSecond = second;
    126        }
    127 
    128        public U getFirst() {
    129            return mFirst;
    130        }
    131 
    132        public V getSecond() {
    133            return mSecond;
    134        }
    135    }
    136 
    137    public UnusedResources(Iterable<File> rTxtFiles, Iterable<File> classes,
    138            Iterable<File> manifests, File mapping, Iterable<File> resources, File reportFile) {
    139        mRTxtFiles = rTxtFiles;
    140        mProguardMapping = mapping;
    141        mClasses = classes;
    142        mManifests = manifests;
    143        mResourceDirs = resources;
    144 
    145        mReportFile = reportFile;
    146        if (reportFile != null) {
    147            mDebugOutput = new StringWriter(8 * 1024);
    148            mDebugPrinter = new PrintWriter(mDebugOutput);
    149        } else {
    150            mDebugOutput = null;
    151            mDebugPrinter = null;
    152        }
    153    }
    154 
    155    public void close() {
    156        if (mDebugOutput != null) {
    157            String output = mDebugOutput.toString();
    158 
    159            if (mReportFile != null) {
    160                File dir = mReportFile.getParentFile();
    161                if (dir != null) {
    162                    if ((dir.exists() || dir.mkdir()) && dir.canWrite()) {
    163                        try {
    164                            Files.asCharSink(mReportFile, Charsets.UTF_8).write(output);
    165                        } catch (IOException ignore) {
    166                        }
    167                    }
    168                }
    169            }
    170        }
    171    }
    172 
    173    public void analyze() throws IOException, ParserConfigurationException, SAXException {
    174        gatherResourceValues(mRTxtFiles);
    175        recordMapping(mProguardMapping);
    176 
    177        for (File jarOrDir : mClasses) {
    178            recordClassUsages(jarOrDir);
    179        }
    180        recordManifestUsages(mManifests);
    181        recordResources(mResourceDirs);
    182        dumpReferences();
    183        mModel.processToolsAttributes();
    184        mUnused = mModel.findUnused();
    185    }
    186 
    187    public void emitConfig(Path destination) throws IOException {
    188        File destinationFile = destination.toFile();
    189        if (!destinationFile.exists()) {
    190            destinationFile.getParentFile().mkdirs();
    191            boolean success = destinationFile.createNewFile();
    192            if (!success) {
    193                throw new IOException("Could not create " + destination);
    194            }
    195        }
    196        StringBuilder sb = new StringBuilder();
    197        Collections.sort(mUnused);
    198        for (Resource resource : mUnused) {
    199            if (resource.type.isSynthetic()) {
    200                // Ignore synthetic resources like overlayable or macro that are
    201                // not actually listed in the ResourceTable.
    202                continue;
    203            }
    204            sb.append(resource.type + "/" + resource.name + "#remove\n");
    205        }
    206        Files.asCharSink(destinationFile, UTF_8).write(sb.toString());
    207    }
    208 
    209    private void dumpReferences() {
    210        if (mDebugPrinter != null) {
    211            mDebugPrinter.print(mModel.dumpReferences());
    212        }
    213    }
    214 
    215    private void dumpModel() {
    216        if (mDebugPrinter != null) {
    217            mDebugPrinter.print(mModel.dumpResourceModel());
    218        }
    219    }
    220 
    221    private void recordResources(Iterable<File> resources)
    222            throws IOException, SAXException, ParserConfigurationException {
    223        for (File resDir : resources) {
    224            File[] resourceFolders = resDir.listFiles();
    225            assert resourceFolders != null : "Invalid resource directory " + resDir;
    226            for (File folder : resourceFolders) {
    227                ResourceFolderType folderType = ResourceFolderType.getFolderType(folder.getName());
    228                if (folderType != null) {
    229                    recordResources(folderType, folder);
    230                }
    231            }
    232        }
    233    }
    234 
    235    private void recordResources(ResourceFolderType folderType, File folder)
    236            throws ParserConfigurationException, SAXException, IOException {
    237        File[] files = folder.listFiles();
    238        if (files != null) {
    239            for (File file : files) {
    240                String path = file.getPath();
    241                mModel.file = file;
    242                try {
    243                    boolean isXml = endsWithIgnoreCase(path, DOT_XML);
    244                    if (isXml) {
    245                        String xml = Files.toString(file, UTF_8);
    246                        Document document = XmlUtils.parseDocument(xml, true);
    247                        mModel.visitXmlDocument(file, folderType, document);
    248                    } else {
    249                        mModel.visitBinaryResource(folderType, file);
    250                    }
    251                } finally {
    252                    mModel.file = null;
    253                }
    254            }
    255        }
    256    }
    257 
    258    void recordMapping(File mapping) throws IOException {
    259        if (mapping == null || !mapping.exists()) {
    260            return;
    261        }
    262        final String arrowString = " -> ";
    263        final String resourceString = ".R$";
    264        Map<String, String> nameMap = null;
    265        for (String line : Files.readLines(mapping, UTF_8)) {
    266            // Ignore R8's mapping comments.
    267            if (line.startsWith("#")) {
    268                continue;
    269            }
    270            if (line.startsWith(" ") || line.startsWith("\t")) {
    271                if (nameMap != null) {
    272                    // We're processing the members of a resource class: record names into the map
    273                    int n = line.length();
    274                    int i = 0;
    275                    for (; i < n; i++) {
    276                        if (!Character.isWhitespace(line.charAt(i))) {
    277                            break;
    278                        }
    279                    }
    280                    if (i < n && line.startsWith("int", i)) { // int or int[]
    281                        int start = line.indexOf(' ', i + 3) + 1;
    282                        int arrow = line.indexOf(arrowString);
    283                        if (start > 0 && arrow != -1) {
    284                            int end = line.indexOf(' ', start + 1);
    285                            if (end != -1) {
    286                                String oldName = line.substring(start, end);
    287                                String newName =
    288                                        line.substring(arrow + arrowString.length()).trim();
    289                                if (!newName.equals(oldName)) {
    290                                    nameMap.put(newName, oldName);
    291                                }
    292                            }
    293                        }
    294                    }
    295                }
    296                continue;
    297            } else {
    298                nameMap = null;
    299            }
    300            int index = line.indexOf(resourceString);
    301            if (index == -1) {
    302                // Record obfuscated names of a few known appcompat usages of
    303                // Resources#getIdentifier that are unlikely to be used for general
    304                // resource name reflection
    305                if (line.startsWith("android.support.v7.widget.SuggestionsAdapter ")) {
    306                    mSuggestionsAdapter =
    307                            line.substring(line.indexOf(arrowString) + arrowString.length(),
    308                                        line.indexOf(':') != -1 ? line.indexOf(':') : line.length())
    309                                    .trim()
    310                                    .replace('.', '/')
    311                            + DOT_CLASS;
    312                } else if (line.startsWith("android.support.v7.internal.widget.ResourcesWrapper ")
    313                        || line.startsWith("android.support.v7.widget.ResourcesWrapper ")
    314                        || (mResourcesWrapper == null // Recently wrapper moved
    315                                && line.startsWith(
    316                                        "android.support.v7.widget.TintContextWrapper$TintResources"
    317                                                + " "))) {
    318                    mResourcesWrapper =
    319                            line.substring(line.indexOf(arrowString) + arrowString.length(),
    320                                        line.indexOf(':') != -1 ? line.indexOf(':') : line.length())
    321                                    .trim()
    322                                    .replace('.', '/')
    323                            + DOT_CLASS;
    324                }
    325                continue;
    326            }
    327            int arrow = line.indexOf(arrowString, index + 3);
    328            if (arrow == -1) {
    329                continue;
    330            }
    331            String typeName = line.substring(index + resourceString.length(), arrow);
    332            ResourceType type = ResourceType.fromClassName(typeName);
    333            if (type == null) {
    334                continue;
    335            }
    336            int end = line.indexOf(':', arrow + arrowString.length());
    337            if (end == -1) {
    338                end = line.length();
    339            }
    340            String target = line.substring(arrow + arrowString.length(), end).trim();
    341            String ownerName = target.replace('.', '/');
    342 
    343            nameMap = Maps.newHashMap();
    344            Pair<ResourceType, Map<String, String>> pair = new Pair(type, nameMap);
    345            mResourceObfuscation.put(ownerName, pair);
    346            // For fast lookup in isResourceClass
    347            mResourceObfuscation.put(ownerName + DOT_CLASS, pair);
    348        }
    349    }
    350 
    351    private void recordManifestUsages(File manifest)
    352            throws IOException, ParserConfigurationException, SAXException {
    353        String xml = Files.toString(manifest, UTF_8);
    354        Document document = XmlUtils.parseDocument(xml, true);
    355        mModel.visitXmlDocument(manifest, null, document);
    356    }
    357 
    358    private void recordManifestUsages(Iterable<File> manifests)
    359            throws IOException, ParserConfigurationException, SAXException {
    360        for (File manifest : manifests) {
    361            recordManifestUsages(manifest);
    362        }
    363    }
    364 
    365    private void recordClassUsages(File file) throws IOException {
    366        assert file.isFile();
    367        if (file.getPath().endsWith(DOT_DEX)) {
    368            byte[] bytes = Files.toByteArray(file);
    369            recordClassUsages(file, file.getName(), bytes);
    370        } else if (file.getPath().endsWith(DOT_JAR)) {
    371            ZipInputStream zis = null;
    372            try {
    373                FileInputStream fis = new FileInputStream(file);
    374                try {
    375                    zis = new ZipInputStream(fis);
    376                    ZipEntry entry = zis.getNextEntry();
    377                    while (entry != null) {
    378                        String name = entry.getName();
    379                        if (name.endsWith(DOT_DEX)) {
    380                            byte[] bytes = ByteStreams.toByteArray(zis);
    381                            if (bytes != null) {
    382                                recordClassUsages(file, name, bytes);
    383                            }
    384                        }
    385 
    386                        entry = zis.getNextEntry();
    387                    }
    388                } finally {
    389                    Closeables.close(fis, true);
    390                }
    391            } finally {
    392                Closeables.close(zis, true);
    393            }
    394        }
    395    }
    396 
    397    private String stringifyResource(Resource resource) {
    398        return String.format("%s:%s:0x%08x", resource.type, resource.name, resource.value);
    399    }
    400 
    401    private void recordClassUsages(File file, String name, byte[] bytes) {
    402        assert name.endsWith(DOT_DEX);
    403        ReferenceChecker callback = new ReferenceChecker() {
    404            @Override
    405            public boolean shouldProcess(String internalName) {
    406                // We do not need to ignore R subclasses since R8 now removes
    407                // unused resource id fields in R subclasses thus their
    408                // remaining presence means real usage.
    409                return true;
    410            }
    411 
    412            @Override
    413            public void referencedInt(int value) {
    414                UnusedResources.this.referencedInt("dex", value, file, name);
    415            }
    416 
    417            @Override
    418            public void referencedString(String value) {
    419                // do nothing.
    420            }
    421 
    422            @Override
    423            public void referencedStaticField(String internalName, String fieldName) {
    424                Resource resource = getResourceFromCode(internalName, fieldName);
    425                if (resource != null) {
    426                    ResourceUsageModel.markReachable(resource);
    427                    if (mDebugPrinter != null) {
    428                        mDebugPrinter.println("Marking " + stringifyResource(resource)
    429                                + " reachable: referenced from dex"
    430                                + " in " + file + ":" + name + " (static field access "
    431                                + internalName + "." + fieldName + ")");
    432                    }
    433                }
    434            }
    435 
    436            @Override
    437            public void referencedMethod(
    438                    String internalName, String methodName, String methodDescriptor) {
    439                // Do nothing.
    440            }
    441        };
    442        ProgramResource resource = ProgramResource.fromBytes(
    443                new PathOrigin(file.toPath()), ProgramResource.Kind.DEX, bytes, null);
    444        ProgramResourceProvider provider = () -> Arrays.asList(resource);
    445        try {
    446            Command command =
    447                    (new ResourceShrinker.Builder()).addProgramResourceProvider(provider).build();
    448            ResourceShrinker.run(command, callback);
    449        } catch (CompilationFailedException e) {
    450            e.printStackTrace();
    451        } catch (IOException e) {
    452            e.printStackTrace();
    453        } catch (ExecutionException e) {
    454            e.printStackTrace();
    455        }
    456    }
    457 
    458    /** Returns whether the given class file name points to an aapt-generated compiled R class. */
    459    boolean isResourceClass(String name) {
    460        if (mResourceObfuscation.containsKey(name)) {
    461            return true;
    462        }
    463        int index = name.lastIndexOf('/');
    464        if (index != -1 && name.startsWith("R$", index + 1) && name.endsWith(DOT_CLASS)) {
    465            String typeName = name.substring(index + 3, name.length() - DOT_CLASS.length());
    466            return ResourceType.fromClassName(typeName) != null;
    467        }
    468        return false;
    469    }
    470 
    471    Resource getResourceFromCode(String owner, String name) {
    472        Pair<ResourceType, Map<String, String>> pair = mResourceObfuscation.get(owner);
    473        if (pair != null) {
    474            ResourceType type = pair.getFirst();
    475            Map<String, String> nameMap = pair.getSecond();
    476            String renamedField = nameMap.get(name);
    477            if (renamedField != null) {
    478                name = renamedField;
    479            }
    480            return mModel.getResource(type, name);
    481        }
    482        if (isValidResourceType(owner)) {
    483            ResourceType type =
    484                    ResourceType.fromClassName(owner.substring(owner.lastIndexOf('$') + 1));
    485            if (type != null) {
    486                return mModel.getResource(type, name);
    487            }
    488        }
    489        return null;
    490    }
    491 
    492    private Boolean isValidResourceType(String candidateString) {
    493        return candidateString.contains("/")
    494                && candidateString.substring(candidateString.lastIndexOf('/') + 1).contains("$");
    495    }
    496 
    497    private void gatherResourceValues(Iterable<File> rTxts) throws IOException {
    498        for (File rTxt : rTxts) {
    499            assert rTxt.isFile();
    500            assert rTxt.getName().endsWith(FN_RESOURCE_TEXT);
    501            addResourcesFromRTxtFile(rTxt);
    502        }
    503    }
    504 
    505    private void addResourcesFromRTxtFile(File file) {
    506        try {
    507            SymbolTable st = readFromAapt(file, null);
    508            for (Symbol symbol : st.getSymbols().values()) {
    509                String symbolValue = symbol.getValue();
    510                if (symbol.getResourceType() == ResourceType.STYLEABLE) {
    511                    if (symbolValue.trim().startsWith("{")) {
    512                        // Only add the styleable parent, styleable children are not yet supported.
    513                        mModel.addResource(symbol.getResourceType(), symbol.getName(), null);
    514                    }
    515                } else {
    516                    if (mDebugPrinter != null) {
    517                        mDebugPrinter.println("Extracted R.txt resource: "
    518                                + symbol.getResourceType() + ":" + symbol.getName() + ":"
    519                                + String.format(
    520                                        "0x%08x", Integer.parseInt(symbolValue.substring(2), 16)));
    521                    }
    522                    mModel.addResource(symbol.getResourceType(), symbol.getName(), symbolValue);
    523                }
    524            }
    525        } catch (Exception e) {
    526            e.printStackTrace();
    527        }
    528    }
    529 
    530    ResourceUsageModel getModel() {
    531        return mModel;
    532    }
    533 
    534    private void referencedInt(String context, int value, File file, String currentClass) {
    535        Resource resource = mModel.getResource(value);
    536        if (ResourceUsageModel.markReachable(resource) && mDebugPrinter != null) {
    537            mDebugPrinter.println("Marking " + stringifyResource(resource)
    538                    + " reachable: referenced from " + context + " in " + file + ":"
    539                    + currentClass);
    540        }
    541    }
    542 
    543    private final ResourceShrinkerUsageModel mModel = new ResourceShrinkerUsageModel();
    544 
    545    private class ResourceShrinkerUsageModel extends ResourceUsageModel {
    546        public File file;
    547 
    548        /**
    549         * Whether we should ignore tools attribute resource references.
    550         * <p>
    551         * For example, for resource shrinking we want to ignore tools attributes,
    552         * whereas for resource refactoring on the source code we do not.
    553         *
    554         * @return whether tools attributes should be ignored
    555         */
    556        @Override
    557        protected boolean ignoreToolsAttributes() {
    558            return true;
    559        }
    560 
    561        @Override
    562        protected void onRootResourcesFound(List<Resource> roots) {
    563            if (mDebugPrinter != null) {
    564                mDebugPrinter.println("\nThe root reachable resources are:");
    565                for (Resource root : roots) {
    566                    mDebugPrinter.println("   " + stringifyResource(root) + ",");
    567                }
    568            }
    569        }
    570 
    571        @Override
    572        protected Resource declareResource(ResourceType type, String name, Node node) {
    573            Resource resource = super.declareResource(type, name, node);
    574            resource.addLocation(file);
    575            return resource;
    576        }
    577 
    578        @Override
    579        protected void referencedString(String string) {
    580            // Do nothing
    581        }
    582    }
    583 
    584    private static List<File> parsePathsFromFile(String path) throws IOException {
    585        return java.nio.file.Files.readAllLines(new File(path).toPath()).stream()
    586                .map(File::new)
    587                .collect(Collectors.toList());
    588    }
    589 
    590    public static void main(String[] args) throws Exception {
    591        List<File> rTxtFiles = null; // R.txt files
    592        List<File> classes = null; // Dex/jar w dex
    593        List<File> manifests = null; // manifests
    594        File mapping = null; // mapping
    595        List<File> resources = null; // resources dirs
    596        File log = null; // output log for debugging
    597        Path configPath = null; // output config
    598        for (int i = 0; i < args.length; i += 2) {
    599            switch (args[i]) {
    600                case "--rtxts":
    601                    rTxtFiles = Arrays.stream(args[i + 1].split(":"))
    602                                        .map(s -> new File(s))
    603                                        .collect(Collectors.toList());
    604                    break;
    605                case "--dexes":
    606                    classes = parsePathsFromFile(args[i + 1]);
    607                    break;
    608                case "--manifests":
    609                    manifests = parsePathsFromFile(args[i + 1]);
    610                    break;
    611                case "--mapping":
    612                    mapping = new File(args[i + 1]);
    613                    break;
    614                case "--resourceDirs":
    615                    resources = parsePathsFromFile(args[i + 1]);
    616                    break;
    617                case "--log":
    618                    log = new File(args[i + 1]);
    619                    break;
    620                case "--outputConfig":
    621                    configPath = Paths.get(args[i + 1]);
    622                    break;
    623                default:
    624                    throw new IllegalArgumentException(args[i] + " is not a valid arg.");
    625            }
    626        }
    627        UnusedResources unusedResources =
    628                new UnusedResources(rTxtFiles, classes, manifests, mapping, resources, log);
    629        unusedResources.analyze();
    630        unusedResources.close();
    631        unusedResources.emitConfig(configPath);
    632    }
    633 }