import sys import re import os # Valid sections and the sets they belong to FILE_SECTIONS = [ "Files", "Directories", "Links" ] SCRIPT_SECTIONS = [ "Preinstall", "Postinstall", "Preuninstall", "Postuninstall", "iConfig", "rConfig", "Preupgrade" ] DEPENDENCY_SECTIONS = [ "Dependencies" ] VAR_SECTIONS = [ "Variables", "Defines" ] def error(s): sys.stderr.write("Internal Error: %s \n" % s) exit(1) def error(s, line): sys.stderr.write("Error[%s:%s]: \n" % (line[1], line[2]) + s) exit(1) def error_section(s, section): sys.stderr.write("Error[%s]: " % section + s) exit(1) def warning(s, line): sys.stderr.write("Warning[%s:%s]: \n" % (line[1], line[2]) + s) def info(s, line): sys.stderr.write("Info[%s:%s]: \n" % (line[1], line[2]) + s) def invalid_varname(s): if " " in s: return True return False def CheckIfCommand(s): if s in ["#if", "#ifdef", "#ifndef", "#elseif", "#else", "#elseifdef", "#endif", "#include"]: return True return False variable_usage = "Invalid variable line entry. Usage: VARIABLE_NAME: 'VALUE'" define_usage = "Invalid define line entry. Usage: DEFINE_NAME" too_many_ifs = "There is at least one open conditional (#if) that has not been closed by the end of this section" too_many_endifs = "There is at least one extra end conditional (#endif) in this section." def edge_quotes_match(s): if len(s) < 2: return False if s[0] == "'" and s[-1] == "'": return True if s[0] == '"' and s[-1] == '"': return True return False # FileEntry # param: tokens - a list containing [ stagedLocation, baseLocation, permissions, owner, group (, type) ] # which correspond exactly with a line in a Files section (the tokens separated by semicolons) class FileEntry: def __init__(self, tokens, line): if len(tokens) < 5 or len(tokens) > 6: error("Incorrect number of tokens in File entry", line) self.stagedLocation = tokens[0] self.baseLocation = tokens[1] self.permissions = tokens[2] self.owner = tokens[3] self.group = tokens[4] if len(tokens) == 6: self.type = tokens[5] else: self.type = "" def __str__(self): return self.stagedLocation + "; " + self.baseLocation + "; " + self.permissions + "; " + self.owner + "; " + self.group + "; " + self.type # LinkEntry # param: tokens - a list containing [ stagedLocation, baseLocation, permissions, owner, group ] # which correspond exactly with a line in a Links section (the tokens separated by semicolons) class LinkEntry: def __init__(self, tokens, line): if len(tokens) != 5: error("Incorrect number of tokens in Link entry", line) self.stagedLocation = tokens[0] self.baseLocation = tokens[1] self.permissions = tokens[2] self.owner = tokens[3] self.group = tokens[4] self.type = "" def __str__(self): return self.stagedLocation + "; " + self.baseLocation + "; " + self.permissions + "; " + self.owner + "; " + self.group # DirectoryEntry # param: tokens - a list containing [ stagedLocation, permissions, owner, group (, type) ] # which correspond exactly with a line in a Directories section (the tokens separated by semicolons) class DirectoryEntry: def __init__(self, tokens, line): if len(tokens) < 4 or len(tokens) > 5: error("Incorrect number of tokens in Directory entry", line) self.stagedLocation = tokens[0] self.permissions = tokens[1] self.owner = tokens[2] self.group = tokens[3] if len(tokens) == 5: self.type = tokens[4] else: self.type = "" def __str__(self): return self.stagedLocation + "; " + self.permissions + "; " + self.owner + "; " + self.group + "; " + self.type # ConditionalLevel # param: has_executed - whether this conditional level has evaluated to true before # param: currently_executing - whether this level is currently executing class ConditionalLevel: def __init__(self, has_executed, currently_executing): self.has_executed = has_executed self.currently_executing = currently_executing # ConditionalStack # Description: # This stack is the data structure used to keep track of the many levels of conditionals that can exist. class ConditionalStack: def __init__(self): self.level_stack = [] def IsCodePathActive(self): for level in self.level_stack: if not level.has_executed: return False if not level.currently_executing: return False return True # Adds a new level to the conditional stack. This occurs when there's a #if*. def AddLevel(self): self.level_stack.append(ConditionalLevel(False, False)) # Removes a level from the conditional stack. This occurs when there's a #endif def RemoveLevel(self): if len(self.level_stack) == 0: error("Cannot RemoveLevel, there are no levels on the ConditionalStack") self.level_stack.pop() # This notifies the conditional stack that we want to execute the current level of the stack. def ExecuteCurrentLevel(self): level = self.level_stack.pop() if level.has_executed == True or level.currently_executing == True: error("Trying to execute a conditional level that has already been executed or is currently executing.") self.level_stack.append(ConditionalLevel(True, True)) # This is called in an #else* clause. This is to turn off the "currently_executing" flag if it is true. def NextConditional(self): # End execution if currently_executing level = self.level_stack.pop() if level.currently_executing == True: level.currently_executing = False self.level_stack.append(level) # This is called in an #else* clause. # returns - True if the current level has not been executed yet, False otherwise. def CurrentLevelHasNotBeenExecutedYet(self): if self.Empty(): error("Expecting ConditionalStack to have at least one level in it") return not self.level_stack[-1].has_executed def Empty(self): return len(self.level_stack) == 0 # DataFileParser # Description: # This class reads the datafiles, parses them, and evaluates the commands. class DataFileParser: def __init__(self): self.variables = dict() self.defines = [] # Used for debugging def PrintSections(self): sorted_keys = sorted(self.sections.keys()) for key in sorted_keys: print("****************************************** " + key + " ******************************************") for line in self.sections[key]: print(str(line)) # This function handles all commands. Conditionals will be evaluted to either True and False and the conditional stack will be updated. # Includes will insert the included section into the including section (which is variable name 'section'). # param: line - The command to be evaluated. # param: ifstack - This is the class' ConditionalStack/ # param: section - The entire section currently being processed. # param: linenum - Used for error messages. # returns: 1. ifstack # 2. section (modified if there is an #include) # 3. linenum (modified if there is an #include) def HandleCommand(self, line, ifstack, section, linenum): commandline = line[0] if len(commandline) == 0: return ifstack tokens = commandline.split() firsttoken = tokens[0] if firsttoken == "#if": ifstack.AddLevel() if self.Evaluate(tokens[1:], line) == True: ifstack.ExecuteCurrentLevel() elif firsttoken == "#ifdef": ifstack.AddLevel() if self.IsDefined(tokens[1:], line) == True: ifstack.ExecuteCurrentLevel() elif firsttoken == "#ifndef": ifstack.AddLevel() if self.IsDefined(tokens[1:], line) == False: ifstack.ExecuteCurrentLevel() elif firsttoken == "#elseif": ifstack.NextConditional() if ifstack.CurrentLevelHasNotBeenExecutedYet() and self.Evaluate(tokens[1:], line) == True: ifstack.ExecuteCurrentLevel() elif firsttoken == "#elseifdef": ifstack.NextConditional() if ifstack.CurrentLevelHasNotBeenExecutedYet() and self.IsDefined(tokens[1:], line) == True: ifstack.ExecuteCurrentLevel() elif firsttoken == "#else": ifstack.NextConditional() if ifstack.CurrentLevelHasNotBeenExecutedYet(): ifstack.ExecuteCurrentLevel() elif firsttoken == "#endif": ifstack.RemoveLevel() elif firsttoken == "#include": middle_section = [] for L in self.EvaluateSection(tokens[1]): middle_section.append(L) section = section[0:linenum-1] + middle_section + section[linenum-1:] linenum += len(middle_section) return ifstack, section, linenum # Expression evaluator. Returns True if expression is True, False otherwise. def Evaluate(self, expressions, line): expr = expressions if len(expr) < 3: error("Bad syntax for #if") # This will be in the format of: #if VAR OP VALUE (and/or VAR OP VALUE)* var = expr[0] op = expr[1] value = expr[2] if self.variables.get(var) == None: error("Unable to find variable " + var + " in defined variables", line) if op == "==": return self.variables[var] == value elif op == "!=": return self.variables[var] != value elif op == ">": return self.variables[var] > float(value) elif op == ">=": return self.variables[var] >= float(value) elif op == "<": return self.variables[var] < float(value) elif op == "<=": return self.variables[var] <= float(value) else: error("operator %s is not valid" % op, line); # Returns True if the expression exists in the defines or variables, otherwise False. def IsDefined(self, expressions, line): expr = expressions if len(expr) < 1: error("Bad syntax for #ifdef", line) var = expr[0] if var in self.defines: return True if var in self.variables.keys(): return True return False # This is called on every line that isn't in the Variables or Defines sections, and it replaces any text inside # two sets of braces starting with a dollar sign with the value in the self.variables dict. "${{VAR_NAME}}" def ReplaceVariables(self, line): # replaces all variables that appear in the form of ${{\w+}} var_rex = re.compile(r"\$\{\{\w+\}\}") for m in var_rex.findall(line): line = line.replace(m, self.variables[m[3:-2]]) return line # This function combines, in numeric order, all of the script sections for a given 'name' (ex. "Preinstall") for all numeric values appended to the 'name'. # So if there are sections named Preinstall_10 and Preinstall_50 and Preinstall_3, this function when called for name="Preinstall" will return a section # that contains all of the lines in Preinstall_3, followed by all of the lines in Preinstall_10, followed by all of the lines in Preinstall_50. def GetCombinedInOrder(self, name): returnList = [] orderedSectionList = [] # Find all sections that match 'name' followed by some optional underscores, followed by any combination of digits, # then store those digits for numeric sorting and combining scripts for section in self.sections.keys(): section_rex = re.compile(r"%s_*(\d*)" % name) m = section_rex.match(section) if m != None: sectionPriority = m.group(1) if sectionPriority == "": sectionPriority = "0" orderedSectionList.append( (int(sectionPriority), section) ) orderedSectionList.sort() for orderedSection in orderedSectionList: for line in self.sections[ orderedSection[1] ]: returnList.append(line) return returnList # Reads all lines in each datafile and inserts all lines from each section into self.sections. def InhaleDataFiles(self, directory, filenames): sections = dict() for filename in filenames: state = None f = open(os.path.join(directory,filename)) lines = f.readlines() f.close() linenumber = 0 for line in lines: linenumber += 1 line = line.strip("\n") if len(line) > 0 and line[0] == '%': if len(line) > 1 and line[1] == '%': # This is a comment, ignore it continue # Begin new section state = line[1:] if sections.get(state) == None: sections[state] = [] elif state not in FILE_SECTIONS + DEPENDENCY_SECTIONS + VAR_SECTIONS: error_section("This script section is defined more than once.", state) else: # Handle states sections[state].append( (line, filename, linenumber) ) self.sections = sections # This function is called after the datafiles have been inhaled, and it parses and handles each entry in the Variables and Defines sections. # This allows for later sections to then rely on variables referenced by ${{VAR_NAME}}. def EvaluateVariablesAndDefines(self): # Add variables/defines from data files if "Variables" in self.sections: ifstack = ConditionalStack() for line in self.sections["Variables"]: varline = line[0].strip() if len(varline) == 0: continue if CheckIfCommand(varline.split(" ", 1)[0]): ifstack, dummy, dummy = self.HandleCommand(line, ifstack, [], 0) continue # If we're currently in a conditional part of the data file that evaluates to false if not ifstack.IsCodePathActive(): continue tokens = varline.split(":", 1) if len(tokens) != 2: error(variable_usage, line) key = tokens[0].strip() value = tokens[1].strip() if len(value) < 2 or not edge_quotes_match(value): error(variable_usage, line) # remove the quotes around the variable value value = value[1:-1] if self.variables.get(key) != None: info("Variable %s is already defined." % key, line) # add the parsed variable to the variables dict self.variables[key] = value if not ifstack.Empty(): error_section(too_many_ifs, "Variables") if "Defines" in self.sections: ifstack = ConditionalStack() for line in self.sections["Defines"]: defline = line[0].strip() if len(defline) == 0: continue if CheckIfCommand(defline.split(" ", 1)[0]): ifstack, dummy, dummy = self.HandleCommand(line, ifstack, [], 0) continue if not ifstack.IsCodePathActive(): continue if invalid_varname(defline): error(define_usage, line) if defline in self.defines: info("Define %s has already been defined." % defline, line) # add the define to the define list self.defines.append(defline) if not ifstack.Empty(): error_section(too_many_ifs, "Defines") # This evaluates all commands in a given section denoted by 'section', then returns the evaluated lines for the section. def EvaluateSection(self, section): newsection = [] ifstack = ConditionalStack() if section in SCRIPT_SECTIONS: # Combine and evaluate each script section cursection = self.GetCombinedInOrder(section) else: cursection = self.sections[section] linenum = 0 for line in cursection: linenum += 1 line_literal = line[0] if section in FILE_SECTIONS + DEPENDENCY_SECTIONS + VAR_SECTIONS: if len(line_literal.strip()) == 0: continue if CheckIfCommand(line_literal.split(" ", 1)[0]): ifstack, newsection, linenum = self.HandleCommand(line, ifstack, newsection, linenum) continue if not ifstack.IsCodePathActive(): continue line_literal = self.ReplaceVariables(line_literal) if section in FILE_SECTIONS: tokens = line_literal.split(";") newtokens = [] for token in tokens: newtokens.append(token.strip()) if section == "Files": newsection.append(FileEntry(newtokens, line)) elif section == "Directories": newsection.append(DirectoryEntry(newtokens, line)) elif section == "Links": newsection.append(LinkEntry(newtokens, line)) else: error("Failing to parse line type in '" + section + "'.", line) else: newsection.append(line_literal) if not ifstack.Empty(): error_section(too_many_ifs, section) return newsection # This calls EvaluateSection on each "Base section" as mentioned in the README. def EvaluateAllSections(self): # Read through each line, evaluating #if's/#define's, evaluating variables, tokenizing/classing relevant lines, adding to new sections list newsections = dict() section_keys = FILE_SECTIONS + SCRIPT_SECTIONS + DEPENDENCY_SECTIONS for section in section_keys: newsections[section] = self.EvaluateSection(section) self.sections = newsections # Used for debugging. def Debug(self): self.PrintSections() print("****************************** Variables ******************************") print(self.variables) print("****************************** Defines ******************************") print(self.defines)