1#!/usr/bin/env python3
2#
3#  Copyright (C) 2020 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#
17
18import os, sys, shutil
19
20def usage():
21  print("Usage: explode.py [--consolidate-leaves] [--remove-leaves] <inputFile> <outputPath>")
22  sys.exit(1)
23
24# Miscellaneous file utilities
25class FileIo(object):
26  def __init__(self):
27    return
28
29  def ensureDirExists(self, filePath):
30    if not os.path.isdir(filePath):
31      if os.path.isfile(filePath) or os.path.islink(filePath):
32        os.remove(filePath)
33      os.makedirs(filePath)
34
35  def removePath(self, filePath):
36    if len(os.path.split(filePath)) < 2:
37      raise Exception("Will not remove path at " + filePath + "; is too close to the root of the filesystem")
38    if os.path.islink(filePath):
39      os.remove(filePath)
40    elif os.path.isdir(filePath):
41      shutil.rmtree(filePath)
42    elif os.path.isfile(filePath):
43      os.remove(filePath)
44
45  def copyFile(self, fromPath, toPath):
46    self.ensureDirExists(os.path.dirname(toPath))
47    self.removePath(toPath)
48    if os.path.islink(fromPath):
49      linkText = os.readlink(fromPath)
50      os.symlink(linkText, toPath)
51    else:
52      shutil.copy2(fromPath, toPath)
53
54  def writeFile(self, path, text):
55    f = open(path, "w+")
56    f.write(text)
57    f.close()
58
59fileIo = FileIo()
60
61def countStartingSpaces(text):
62  for i in range(len(text)):
63    if text[i] not in (" ", "\t", "\n"):
64      return i
65  return len(text)
66
67# A Block represents some section of text from a source file and stores it as a file on disk
68# The text of each child block is expected to have text starting with <numSpaces> * " "
69class Block(object):
70  def __init__(self, numSpaces, fileName):
71    self.numSpaces = numSpaces
72    self.children = []
73    self.fileName = fileName
74
75  # Adds more text into this section
76  def addChild(self, newChild):
77    self.children.append(newChild)
78
79  # Displays the text represented by this Block
80  def display(self):
81    prefix = " " * (self.numSpaces + 1)
82    for i in range(len(self.children)):
83      print(prefix + str(i) + ":")
84      child = self.children[i]
85      child.display()
86
87  # Generates files at <filePath> representing this Block
88  def apply(self, filePath):
89    if not os.path.isdir(filePath):
90      os.mkdir(filePath)
91    for child in self.children:
92      child.apply(os.path.join(filePath, child.fileName))
93
94  def hasChildren(self):
95    return len(self.children) > 0
96
97  def startsFunction(self):
98    if len(self.children) < 1:
99      return False
100    return self.children[0].startsFunction()
101
102  # Removes any nodes that seem to be function bodies
103  def consolidateFunctionBodies(self, emitOptionalFunctionBodies):
104    consolidated = False
105    for i in range(1, len(self.children)):
106      prev = self.children[i - 1]
107      if (not prev.hasChildren()) and prev.startsFunction():
108        child = self.children[i]
109        if child.hasChildren():
110          child.consolidateSelf(emitOptionalFunctionBodies)
111          consolidated = True
112    for child in self.children:
113      if child.hasChildren():
114        child.consolidateFunctionBodies(emitOptionalFunctionBodies)
115
116  def consolidateSelf(self, emitOptionalFunctionBodies):
117    text = self.getText()
118    if emitOptionalFunctionBodies or " return " in text:
119      self.children = [TextBlock(text, "0")]
120    else:
121      self.children = []
122
123  def getText(self):
124    texts = [child.getText() for child in self.children]
125    return "".join(texts)
126
127# A TextBlock stores text for inclusion in a parent Block
128# Together, they store the result of parsing text from a file
129class TextBlock(object):
130  def __init__(self, text, fileName):
131    self.text = text
132    self.fileName = fileName
133
134  def getText(self):
135    return self.text
136
137  def display(self):
138    print(self.text)
139
140  def apply(self, filePath):
141    fileIo.writeFile(filePath, self.text)
142
143  def hasChildren(self):
144    return False
145
146  def startsFunction(self):
147    if "}" in self.text:
148      return False
149    if "class " in self.text:
150      return False
151    parenIndex = self.text.find(")")
152    curlyIndex = self.text.find("{")
153    if parenIndex >= 0 and curlyIndex >= parenIndex:
154      return True
155    return False
156
157def getLineName(lineNumber, numLines):
158  longestLineNumber = len(str(numLines - 1))
159  thisLineNumber = len(str(lineNumber))
160  extraZeros = "0" * (longestLineNumber - thisLineNumber)
161  return extraZeros + str(lineNumber)
162
163def main(args):
164  if len(args) < 2:
165    usage()
166  consolidateLeaves = False
167  emitLeaves = True
168  if args[0] == "--remove-leaves":
169    consolidateLeaves = True
170    emitLeaves = False
171    args = args[1:]
172  if args[0] == "--consolidate-leaves":
173    consolidateLeaves = True
174    args = args[1:]
175  if len(args) != 2:
176    usage()
177  stack = [Block(0, -1)]
178  inputPath = args[0]
179  outputPath = args[1]
180  inputFile = open(inputPath)
181  lines = []
182  try:
183    lines = inputFile.readlines()
184  except UnicodeDecodeError:
185    fileIo.copyFile(inputPath, outputPath + "/binary")
186  numLines = len(lines)
187  for i in range(numLines):
188    lineName = getLineName(i, numLines)
189    line = lines[i]
190    numSpaces = countStartingSpaces(line)
191    if line.strip() == "*/" and line.startswith(" "):
192      numSpaces -= 1
193    ignore = (numSpaces == len(line))
194    if not ignore:
195      # pop back to a previous scope
196      while numSpaces < stack[-1].numSpaces:
197        stack = stack[:-1]
198      if numSpaces > stack[-1].numSpaces:
199        newChild = Block(numSpaces, lineName)
200        stack[-1].addChild(newChild)
201        stack.append(newChild)
202    stack[-1].addChild(TextBlock(line, lineName))
203  if consolidateLeaves:
204    # Remove the nodes that the user considers to be leaf nodes
205    stack[0].consolidateFunctionBodies(emitLeaves)
206  #stack[0].display()
207  stack[0].apply(outputPath)
208
209
210if __name__ == "__main__":
211  main(sys.argv[1:])
212