1#!/usr/bin/env python3 2# Copyright (C) 2021 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 16import os 17import re 18import lxml.etree as etree 19 20class ResourceLocation: 21 def __init__(self, file, line=None): 22 self.file = file 23 self.line = line 24 def __str__(self): 25 if self.line is not None: 26 return self.file + ':' + str(self.line) 27 else: 28 return self.file 29 30class Resource: 31 def __init__(self, name, type, location=None): 32 self.name = name 33 self.type = type 34 self.locations = [] 35 if location is not None: 36 self.locations.append(location) 37 def __eq__(self, other): 38 if isinstance(other, _Grab): 39 return other == self 40 return self.name == other.name and self.type == other.type 41 def __ne__(self, other): 42 if isinstance(other, _Grab): 43 return other != self 44 return self.name != other.name or self.type != other.type 45 def __hash__(self): 46 return hash((self.name, self.type)) 47 def __str__(self): 48 result = '' 49 for location in self.locations: 50 result += str(location) + ': ' 51 result += '<'+self.type+' name="'+self.name+'"' 52 return result + '>' 53 def __repr__(self): 54 return str(self) 55 56def get_all_resources(resDir, excluded_resource_files=[]): 57 excluded_resource_files = [os.path.abspath(file) for file in excluded_resource_files] 58 allResDirs = [f for f in os.listdir(resDir) if os.path.isdir(os.path.join(resDir, f))] 59 valuesDirs = [f for f in allResDirs if f.startswith('values')] 60 fileDirs = [f for f in allResDirs if not f.startswith('values')] 61 resources = set() 62 # Get the filenames of the all the files in all the fileDirs 63 for dir in fileDirs: 64 type = dir.split('-')[0] 65 for file in os.listdir(os.path.join(resDir, dir)): 66 filePath = os.path.abspath(os.path.join(resDir, dir, file)) 67 if file.endswith('.xml') and filePath not in excluded_resource_files: 68 add_resource_to_set(resources, 69 Resource(file[:-4], type, 70 ResourceLocation(os.path.join(resDir, dir, file)))) 71 if dir.startswith("layout"): 72 for resource in get_ids_from_layout_file(os.path.join(resDir, dir, file)): 73 add_resource_to_set(resources, resource) 74 for dir in valuesDirs: 75 for file in os.listdir(os.path.join(resDir, dir)): 76 filePath = os.path.abspath(os.path.join(resDir, dir, file)) 77 if file.endswith('.xml') and filePath not in excluded_resource_files: 78 for resource in get_resources_from_single_file(os.path.join(resDir, dir, file), 79 dir != "values"): 80 add_resource_to_set(resources, resource) 81 return resources 82 83def get_ids_from_layout_file(filename): 84 result = set() 85 with open(filename, 'r') as file: 86 r = re.compile("@\+id/([a-zA-Z0-9_]+)") 87 for i in r.findall(file.read()): 88 add_resource_to_set(result, Resource(i, 'id', ResourceLocation(filename))) 89 return result 90 91def get_resources_from_single_file(filename, ignore_strings=False): 92 doc = etree.parse(filename) 93 root = doc.getroot() 94 result = set() 95 for resource in root: 96 if resource.tag is etree.Comment: 97 continue 98 if resource.tag == 'declare-styleable': 99 for attr in resource: 100 resName = attr.get('name') 101 # Skip resources beginning with 'android:' as they are part of the framework 102 # resources. This script finds only the app's resources. 103 if resName is None or resName.startswith('android:'): 104 continue 105 resType = "attr" 106 add_resource_to_set(result, Resource(resName, resType, ResourceLocation(filename, attr.sourceline))) 107 continue 108 resName = resource.get('name') 109 resType = resource.tag 110 if resType == "string-array" or resType == "integer-array": 111 resType = "array" 112 if resource.tag == 'item' or resource.tag == 'public': 113 resType = resource.get('type') 114 if resType == 'string' and ignore_strings: 115 continue 116 if resType == 'overlayable': 117 for policy in resource: 118 for overlayable in policy: 119 resName = overlayable.get('name') 120 resType = overlayable.get('type') 121 add_resource_to_set(result, Resource(resName, resType, 122 ResourceLocation(filename, resource.sourceline))) 123 else: 124 add_resource_to_set(result, Resource(resName, resType, 125 ResourceLocation(filename, resource.sourceline))) 126 return result 127 128# Used to get objects out of sets 129class _Grab: 130 def __init__(self, value): 131 self.search_value = value 132 def __hash__(self): 133 return hash(self.search_value) 134 def __eq__(self, other): 135 if self.search_value == other: 136 self.actual_value = other 137 return True 138 return False 139 140def add_resource_to_set(resourceset, resource): 141 if (resource.name == None): 142 return 143 grabber = _Grab(resource) 144 if grabber in resourceset: 145 grabber.actual_value.locations.extend(resource.locations) 146 else: 147 resourceset.update([resource]) 148 149def merge_resources(set1, set2): 150 for resource in set2: 151 add_resource_to_set(set1, resource) 152