1#!/usr/bin/env python3
2#
3# Copyright 2019, The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import os
18import re
19
20class ResourceLocation:
21    def __init__(self, file, line=None):
22        self.file = file
23        self.line = line
24
25    def __str__(self):
26        if self.line is not None:
27            return self.file + ':' + str(self.line)
28        else:
29            return self.file
30
31class Resource:
32    def __init__(self, name, type, location=None):
33        self.name = name
34        self.type = type
35        self.locations = []
36        if location is not None:
37            self.locations.append(location)
38
39    def __eq__(self, other):
40        if isinstance(other, _Grab):
41            return other == self
42        return self.name == other.name and self.type == other.type
43
44    def __ne__(self, other):
45        if isinstance(other, _Grab):
46            return other != self
47        return self.name != other.name or self.type != other.type
48
49    def __hash__(self):
50        return hash((self.name, self.type))
51
52    def __str__(self):
53        result = ''
54        for location in self.locations:
55            result += str(location) + ': '
56        result += '<'+self.type+' name="'+self.name+'"'
57
58        return result + '>'
59
60    def __repr__(self):
61        return str(self)
62
63def get_all_resources(resDir):
64    allResDirs = [f for f in os.listdir(resDir) if os.path.isdir(os.path.join(resDir, f))]
65    valuesDirs = [f for f in allResDirs if f.startswith('values')]
66    fileDirs = [f for f in allResDirs if not f.startswith('values')]
67
68    resources = set()
69
70    # Get the filenames of the all the files in all the fileDirs
71    for dir in fileDirs:
72        type = dir.split('-')[0]
73        for file in os.listdir(os.path.join(resDir, dir)):
74            if file.endswith('.xml'):
75                add_resource_to_set(resources,
76                                    Resource(file[:-4], type,
77                                             ResourceLocation(os.path.join(resDir, dir, file))))
78                if dir.startswith("layout"):
79                    for resource in get_ids_from_layout_file(os.path.join(resDir, dir, file)):
80                        add_resource_to_set(resources, resource)
81
82    for dir in valuesDirs:
83        for file in os.listdir(os.path.join(resDir, dir)):
84            if file.endswith('.xml'):
85                for resource in get_resources_from_single_file(os.path.join(resDir, dir, file)):
86                    add_resource_to_set(resources, resource)
87
88    return resources
89
90def get_ids_from_layout_file(filename):
91    result = set()
92    with open(filename, 'r') as file:
93        r = re.compile("@\+id/([a-zA-Z0-9_]+)")
94        for i in r.findall(file.read()):
95            add_resource_to_set(result, Resource(i, 'id', ResourceLocation(filename)))
96    return result
97
98def get_resources_from_single_file(filename):
99    # defer importing lxml to here so that people who aren't editing chassis don't have to have
100    # lxml installed
101    import lxml.etree as etree
102    doc = etree.parse(filename)
103    root = doc.getroot()
104    result = set()
105    for resource in root:
106        if resource.tag == 'declare-styleable' or resource.tag is etree.Comment:
107            attrs = resource.findall("attr")
108            for attr in attrs:
109                resName = attr.get('name')
110                resType = "attr"
111                if "android:" not in attr.get('name'):
112                    add_resource_to_set(result, Resource(resName, resType,
113                                                        ResourceLocation(filename, resource.sourceline)))
114            continue
115
116        resName = resource.get('name')
117        resType = resource.tag
118        if resType == "string-array":
119            resType = "array"
120        if resource.tag == 'item' or resource.tag == 'public':
121            resType = resource.get('type')
122
123        if resType == 'overlayable':
124            for policy in resource:
125                for overlayable in policy:
126                    resName = overlayable.get('name')
127                    resType = overlayable.get('type')
128                    add_resource_to_set(result, Resource(resName, resType,
129                                                        ResourceLocation(filename, resource.sourceline)))
130        else:
131            add_resource_to_set(result, Resource(resName, resType,
132                                                 ResourceLocation(filename, resource.sourceline)))
133
134    return result
135
136# Used to get objects out of sets
137class _Grab:
138    def __init__(self, value):
139        self.search_value = value
140    def __hash__(self):
141        return hash(self.search_value)
142    def __eq__(self, other):
143        if self.search_value == other:
144            self.actual_value = other
145            return True
146        return False
147
148def add_resource_to_set(resourceset, resource):
149    grabber = _Grab(resource)
150    if grabber in resourceset:
151        grabber.actual_value.locations.extend(resource.locations)
152    else:
153        resourceset.update([resource])
154
155def merge_resources(set1, set2):
156    for resource in set2:
157        add_resource_to_set(set1, resource)
158