Blob Blame History Raw
#!/usr/bin/python

#  Author: Donald Miner <dminer@tresys.com>
#
# Copyright (C) 2003 - 2005 Tresys Technology, LLC
#      This program is free software; you can redistribute it and/or modify
#      it under the terms of the GNU General Public License as published by
#      the Free Software Foundation, version 2.

"""
	This script generates XML documentation information for layers specified
	by the user.
"""

import sys
import os
import glob


# GLOBALS
class dec_style:
	'''
	"Declaration Style"
	Specifies the syntax of a declaration. Intended to be used with
	getParams().
	'''

	# Example of a line: foo(bar,one,two);
	# A style that would fit this: dec_style("foo(",3,",",");")
	#  "foo(" - the opening of it, ends at the begining of the first param.
	#  3 - the number of parameters.
	#  "," - the delimeter to parse apart parameters.
	#  ");" - the end of the declaration statement.

	def __init__(self,open_str,params,delim,close_str):
		self.open_str = open_str
		self.params = params
		self.delim = delim
		self.close_str = close_str


INTERFACE = dec_style("interface(`",1,None,"'")
TEMPLATE = dec_style("template(`",1,None,"'")
TUNABLE = dec_style("gen_tunable(",2,",",")")
# boolean FIXME: may have to change in the future.
BOOLEAN = dec_style("gen_bool(",2,",",")")


# Default values of command line arguments.
directory = "./"
warn = False
meta = "metadata"
layers = []
tunable_files = []



# FUNCTIONS
def getXMLComment(line):
	'''
	Returns the XML comment, (removes "## " from the front of the line).
	Returns False if the line is not an XML comment.
	'''

	for i in range(0,len(line)-1):
		# Check if the first 3 characters are "## "
		if line[i:i+3] in ("## ", "##\t"):
			# The chars before '#' in the line must be whitespace.
			if i > 0 and not line[0:i-1].isspace():
				return False
			else:
				return line[i+3:]

	# No XML comment.
	return False	

def getParams(line, style):
	'''
	Returns a list of items, containing the values of each parameter.
	'''

	# Clean out whitespace.
	temp_line = line.strip()

	# Check to see if the string begins with the specified opening
	# string specified by style.
	if temp_line[0:len(style.open_str)] == style.open_str:
		temp_line = temp_line[len(style.open_str):].strip()
	else:
		return False

	# If there is a delimeter.
	if style.delim:
		temp_line = temp_line.split(style.delim)
	else:
		temp_line = [temp_line]

	# Only interested in a sertain number of tokens, specified by style.
	temp_line = temp_line[:style.params]

	# Remove the end of the declaration, specified by style.
	end = temp_line[-1].find(style.close_str)
	if end == -1:
		warning("line \"%s\" may be syntactically incorrect"\
			% line.strip())
		return False

	temp_line[-1] = temp_line[-1][:end]

	# Remove whitespace
	for i in range(0,len(temp_line)-1):
		temp_line[i] = temp_line[i].strip()

	return temp_line

def getModuleXML(file_name):
	'''
	Returns the XML data for a module in a list, one line per list item.
	'''

	# Try to open the file, if it cant, just ignore it.
	try:
		module_file = open(file_name, "r")
		module_code = module_file.readlines()
		module_file.close()
	except:
		warning("cannot open file %s for read, skipping" % file_name)
		return []

	module_buf = []

	# Infer the module name, which is the base of the file name.
	module_buf.append("<module name=\"%s\">\n" 
		% os.path.splitext(os.path.split(file_name)[-1])[0])

	temp_buf = []

	# Phases:	find header - looking for the header of the file.
	#		get header - get the header comments and stop when first
	#			     whitespace is encountered.
	#		find interface - looking for interfaces to get info for.
	phase = "find header"

	# Go line by line and figure out what to do with it.
	for line in module_code:
		# In this phase, whitespace and stray code is ignored at the
		# top of the file.
		if phase == "find header":
			if line.isspace():
				continue
			# Once a comment is encountered, start trying to get the
			# header documentation.
			elif getXMLComment(line):
				phase = "get header"
			# If an interface is found, there is no header, and no
			# documentation for the interface.
			elif getParams(line,INTERFACE)\
				 or getParams(line,TEMPLATE):
				phase = "find interface"

		# In this phase, XML comments are being retrieved for the file.
		if phase == "get header":
			if getXMLComment(line):
				temp_buf.append(getXMLComment(line))
				continue
			# If the line is whitespace, the file header is over,
			# continue on to find interfaces.
			elif line.isspace():
				module_buf += temp_buf
				temp_buf = []
				phase = "find interface"
				continue
			# Oops! The comments we have been getting weren't part
			# of the header so attribute them to an interface
			# instead.
			elif getParams(line,INTERFACE)\
				 or getParams(line,TEMPLATE):
				phase = "find interface"

		# In this phase, XML comments are being attributed
		if phase == "find interface":
			if getXMLComment(line):
				temp_buf.append(getXMLComment(line))
				continue
			# If the line is the declaration of a interface,
			# infer the interface name and add all the comments
			# to the main buffer.
			elif getParams(line,INTERFACE):
				module_buf.append("<interface name=\"%s\">\n"\
					% getParams(line,INTERFACE)[0])
				if len(temp_buf):
					module_buf += temp_buf
				else:
					module_buf.append("<summary>\n")
					module_buf.append("Summary is missing!\n")
					module_buf.append("</summary>\n")
					module_buf.append("<param name=\"?\">\n")
					module_buf.append("Parameter descriptions are missing!\n")
					module_buf.append("</param>\n")
				temp_buf = []
				module_buf.append("</interface>\n")
				continue
			elif getParams(line,TEMPLATE):
				module_buf.append("<template name =\"%s\">\n"\
					% getParams(line,TEMPLATE)[0])
				if len(temp_buf):
					module_buf += temp_buf
				else:
					module_buf.append("<summary>\n")
					module_buf.append("Summary is missing!\n")
					module_buf.append("</summary>\n")
					module_buf.append("<param name=\"?\">\n")
					module_buf.append("Parameter descriptions are missing!\n")
					module_buf.append("</param>\n")
				temp_buf = []
				module_buf.append("</template>\n")

	# If there are XML comments at the end of the file, they arn't
	# attributed to anything. These are ignored.
	if len(temp_buf):
		warning("orphan XML comments at bottom of file %s" % file_name)
		
	module_buf.append("</module>\n")

	return module_buf

def getLayerXML(directory):
	'''
	Returns the XML documentation for a layer.
	'''

	layer_buf = []

	# Infer the layer name from the directory name.
	layer_buf.append("<layer name=\"%s\">\n" % os.path.basename(directory))

	# Try to open the metadata file for this directory and if it exists,
	# append the contents to the buffer.
	try:
		layer_meta = open(directory+"/"+meta, "r")
		layer_buf += layer_meta.readlines()
		layer_meta.close()
	except:
		warning("cannot open file %s for read, assuming no data"\
			% meta)

	# For each module file in the layer, add its XML.
	for module in glob.glob("%s/*.if" % directory):
		layer_buf += getModuleXML(module)

	layer_buf.append("</layer>\n")

	return layer_buf

def getTunableXML(file_name):
	'''
	Return all the XML for the tunables in the file specified.
	'''

	# Try to open the file, if it cant, just ignore it.
	try:
		tunable_file = open(file_name, "r")
		tunable_code = tunable_file.readlines()
		tunable_file.close()
	except:
		warning("cannot open file %s for read, skipping" % file_name)
		return []

	tunable_buf = []
	temp_buf = []

	# Find tunables and booleans line by line and use the comments above
	# them.
	for line in tunable_code:
		# If it is an XML comment, add it to the buffer and go on.
		if getXMLComment(line):
			temp_buf.append(getXMLComment(line))
			continue

		# Get the parameters of a TUNABLE style line.
		params = getParams(line,TUNABLE)
		tag = "tunable"

		# If the line is not a TUNABLE style declaration, try BOOLEAN.
		if not params:
			params = getParams(line,BOOLEAN)
			tag = "boolean"

		# If the line is one of the two styles above, add a tunable tag
		# and give it the data from the temprorary buffer.
		if params:
			tunable_buf.append\
				("<%s name=\"%s\" dftval=\"%s\">\n"
				% (tag, params[0], params[1]))
			tunable_buf += temp_buf
			temp_buf = []
			tunable_buf.append("</%s>\n" % tag)

	# If there are XML comments at the end of the file, they arn't
	# attributed to anything. These are ignored.
	if len(temp_buf):
		warning("orphan XML comments at bottom of file %s" % file_name)

	return tunable_buf

def getPolicyXML(directory):
	'''
	Return the compelete reference policy XML documentation through a list,
	one line per item.
	'''

	# Keep track of original path so that it will change back at the end.
	old_dir = os.path.abspath(os.path.curdir)

	# Attempt to change directory into the policy directory. If it doesn't
	# exist just return an empty documentation.
	try:
		os.chdir(directory)
	except:
		warning("cannot change directory to %s, ignoring"\
			% directory)
		return []

	policy_buf = []
	policy_buf.append("<policy>\n")

	# Add to the XML each layer specified by the user.
	for layer in layers:
		policy_buf += getLayerXML(layer)

	# Add to the XML each tunable specified by the user.
	for tunable_file in tunable_files:
		policy_buf += getTunableXML(tunable_file)


	policy_buf.append("</policy>\n")

	# Return to old directory.
	try:
		os.chdir(old_dir)
	except:
		error("cannot change directory to %s" % old_dir)

	return policy_buf

def usage():
	"""
	Displays a message describing the proper usage of this script.
	"""

	sys.stdout.write("usage: %s [-w] [-d directory] [-m file] "\
		% sys.argv[0])

	sys.stdout.write("layerdirectory [layerdirectory...]\n\n")

	sys.stdout.write("Options:\n")

	sys.stdout.write("-w --warn		--	"+\
				"show warnings\n")

	sys.stdout.write("-m --meta <file>	--	"+\
				"the filename of the metadata in each layer\n")

	sys.stdout.write("-d --directory <dir>	--	"+\
				"directory where the layers are\n")

	sys.stdout.write("-t --tunable <file>	--	"+\
				"A file containing tunable declarations\n")

def warning(description):
	'''
	Warns the user of a non-critical error.
	'''

	if warn:
		sys.stderr.write("%s: " % sys.argv[0] )
		sys.stderr.write("warning: " + description + "\n")

def error(description):
	'''
	Describes an error and exists the program.
	'''

	sys.stderr.write("%s: " % sys.argv[0] )
        sys.stderr.write("error: " + description + "\n")
        sys.stderr.flush()
        sys.exit(1)



# MAIN PROGRAM
# Check that there are command line arguments.
if len(sys.argv) <= 1:
	usage()
	sys.exit(1)


# Parse the command line arguments
for i in range(1, len(sys.argv)):
	if sys.argv[i-1] in ("-d", "--directory", "-m", "--meta",\
					"-t", "--tunable"):
		continue
	elif sys.argv[i] in ("-w", "--warn"):
		warn = True
	elif sys.argv[i] in ("-d", "--directory"):
		if i < len(sys.argv)-1:
			directory = sys.argv[i+1]
		else:
			usage()
	elif sys.argv[i] in ("-m", "--meta"):
		if i < len(sys.argv)-1:
			meta = sys.argv[i+1]
		else:
			usage()
	elif sys.argv[i] in ("-t", "--tunable"):
		if i < len(sys.argv)-1:
			tunable_files.append(sys.argv[i+1])
		else:
			usage()
	else:
		layers.append(sys.argv[i])


# Generate the XML and output it to a file
lines = getPolicyXML(directory)
for s in lines:
	sys.stdout.write(s)