1# -*- coding: utf-8 -*-
2#
3# Copyright 2013 Oliver Tappe
4# Distributed under the terms of the MIT License.
5
6# -- Modules ------------------------------------------------------------------
7
8import functools
9from subprocess import CalledProcessError, check_output
10
11from .RecipeTypes import (Architectures, Extendable, LinesOfText,
12                          MachineArchitecture, Phase, ProvidesList,
13                          RequiresList, Status, YesNo)
14from .ShellScriptlets import configFileEvaluatorScript, getShellVariableSetters
15from .Utils import filteredEnvironment, sysExit, warn
16
17# -- haikuports.conf and *.recipe parser --------------------------------
18
19class ConfigParser(object):
20	def __init__(self, filename, attributes, shellVariables):
21
22		## REFACTOR environment setup and conf location into a single function
23		## that then calls the ConfigParser and either passes in the file path
24		## or the contents of the file
25
26		# set up the shell environment -- we want it to inherit some of our
27		# variables
28		shellEnv = filteredEnvironment()
29		shellEnv['recipePhases'] = ' '.join(Phase.getAllowedValues())
30
31		# execute the config file via the shell ....
32		supportedKeysString = '|'.join(attributes.keys())
33		shellVariables = shellVariables.copy()
34		shellVariables['supportedKeysPattern'] = supportedKeysString
35		shellVariables['fileToParse'] = filename
36
37		wrapperScript = (getShellVariableSetters(shellVariables)
38						 + configFileEvaluatorScript)
39		try:
40			output = check_output(['bash', '-c', wrapperScript], env=shellEnv).decode('utf-8')
41		except (OSError, CalledProcessError):
42			sysExit("Can't evaluate config file: " + filename)
43
44		# ... and collect the resulting configurations (one per line)
45
46		self.entriesByExtension = {}
47		self.definedPhases = []
48
49		lines = output.splitlines()
50		for line in lines:
51			## REFACTOR into a testable method that can parse a single line
52			key, separator, valueString = line.partition('=')
53			if not separator:
54				sysExit('evaluating file %s produced illegal '
55						'key-values line:\n	 %s\nexpected "<key>=<value>"\n'
56						'output of configuration script was: %s\n'
57						% (filename, line, output))
58
59			# some keys may have a package-specific extension, check:
60			if key in attributes:
61				# unextended key
62				baseKey = key
63				extension = ''
64				index = '1'
65			else:
66				baseKey = ''
67				subKeys = key.split('_')
68				while subKeys:
69					subKey = subKeys.pop(0)
70					baseKey += ('_' if baseKey else '') + subKey
71					if baseKey in attributes:
72						if attributes[baseKey]['extendable'] != Extendable.NO:
73							extension = '_'.join(subKeys)
74							break
75						if attributes[baseKey]['indexable']:
76							index = None
77							if len(subKeys) == 0:
78								index = '1'
79								break
80							if len(subKeys) == 1 and subKeys[0].isdigit():
81								index = subKeys[0]
82								break
83						warn('Ignoring key %s in file %s' % (key, filename))
84						continue
85				else:
86					# might be a <PHASE>_DEFINED
87					isPhaseKey = False
88					if key.endswith('_DEFINED'):
89						phase = key[:-8]
90						if phase in Phase.getAllowedValues():
91							isPhaseKey = True
92							self.definedPhases.append(phase)
93
94					if not isPhaseKey:
95						# skip unsupported key, just in case
96						warn('Key %s in file %s is unsupported, ignoring it'
97							 % (key, filename))
98					continue
99
100			# create empty dictionary for new extension
101			if extension not in self.entriesByExtension:
102				self.entriesByExtension[extension] = {}
103
104			entries = self.entriesByExtension[extension]
105
106			valueString = valueString.replace(r'\n', '\n')
107			# replace quoted newlines by real newlines
108
109			if attributes[baseKey]['indexable']:
110				if baseKey not in entries:
111					entries[baseKey] = {}
112
113			## REFACTOR into one method per if/elif branch
114			attrType = attributes[baseKey]['type']
115			if attrType == bytes:
116				if attributes[baseKey]['indexable']:
117					entries[baseKey][index] = valueString
118				else:
119					entries[key] = valueString
120			elif attrType == int:
121				try:
122					if attributes[baseKey]['indexable']:
123						entries[baseKey][index] = int(valueString)
124					else:
125						entries[key] = int(valueString)
126				except ValueError:
127					sysExit('evaluating file %s produced illegal value '
128							'"%s" for key %s, expected an <integer> value'
129							% (filename, valueString, key))
130			elif attrType in [list, ProvidesList, RequiresList]:
131				values = [v.strip() for v in valueString.splitlines()]
132				values = [v for v in values if len(v) > 0]
133				# explicitly protect against '-' in names of provides or
134				# requires declarations
135				if attrType in [ProvidesList, RequiresList]:
136					values = [v.lower() for v in values]
137					for value in values:
138						if '-' in value.split()[0]:
139							sysExit('evaluating file %s produced illegal value '
140									'"%s" for key %s\n'
141									'dashes are not allowed in provides- or '
142									'requires declarations'
143									% (filename, value, key))
144				if attributes[baseKey]['indexable']:
145					entries[baseKey][index] = values
146				else:
147					entries[key] = values
148			elif attrType == LinesOfText:
149				# like a list, but only strip empty lines in front of and
150				# after the text
151				values = [v.strip() for v in valueString.splitlines()]
152				while values and len(values[0]) == 0:
153					values.pop(0)
154				while values and len(values[-1]) == 0:
155					values.pop()
156				entries[key] = values
157			elif attrType == Phase:
158				if valueString.upper() not in Phase.getAllowedValues():
159					sysExit('evaluating file %s\nproduced illegal value "%s" '
160							'for key %s\nexpected one of: %s'
161							% (filename, valueString, key,
162							   ','.join(Phase.getAllowedValues())))
163				entries[key] = valueString.upper()
164			elif attrType == MachineArchitecture:
165				entries[key] = {}
166				knownArchitectures = MachineArchitecture.getAll()
167				valueString = valueString.lower()
168				if valueString not in knownArchitectures:
169					architectures = ','.join(knownArchitectures)
170					sysExit('%s refers to unknown machine-architecture %s\n'
171							'known machine-architectures: %s'
172							% (filename, valueString, architectures))
173				entries[key] = valueString
174			elif attrType == Architectures:
175				entries[key] = {}
176				for value in [v.lower() for v in valueString.split()]:
177					architecture = ''
178					if value.startswith('?'):
179						status = Status.UNTESTED
180						architecture = value[1:]
181					elif value.startswith('!'):
182						status = Status.BROKEN
183						architecture = value[1:]
184					else:
185						status = Status.STABLE
186						architecture = value
187					knownArchitectures = Architectures.getAll()
188					if architecture == 'all':
189						for machineArch in MachineArchitecture.getAll():
190							entries[key][machineArch] = status
191					else:
192						if architecture not in knownArchitectures:
193							architectures = ','.join(knownArchitectures)
194							sysExit('%s refers to unknown architecture %s\n'
195									'known architectures: %s'
196									% (filename, architecture, architectures))
197						entries[key][architecture] = status
198				if 'any' in entries[key] and len(entries[key]) > 1:
199					sysExit("%s specifies both 'any' and other architectures"
200							% (filename))
201				if 'source' in entries[key] and len(entries[key]) > 1:
202					sysExit("%s specifies both 'source' and other architectures"
203							% (filename))
204			elif attrType == YesNo:
205				valueString = valueString.lower()
206				if valueString not in YesNo.getAllowedValues():
207					sysExit("Value for %s should be 'yes' or 'no' in %s"
208							% (key, filename))
209				entries[key] = YesNo.toBool(valueString)
210			else:
211				sysExit('type of key %s in file %s is unsupported'
212						% (key, filename))
213				# for entries in self.entriesByExtension.values():
214				# for key in entries:
215				#		print key + " = " + str(entries[key])
216
217	def getEntriesForExtension(self, extension):
218		if extension in self.entriesByExtension:
219			return self.entriesByExtension[extension]
220		else:
221			return {}
222
223	@property
224	def extensions(self):
225		return self.entriesByExtension.keys()
226
227
228	## REFACTOR - consider using simple functions for this
229	@staticmethod
230	def splitItem(string):
231		components = []
232		if not string:
233			return components
234
235		component = ''
236		inQuote = False
237		for c in string:
238			if inQuote:
239				component += c
240				if c == '"':
241					inQuote = False
242				continue
243
244			if c.isspace():
245				if component:
246					components.append(component)
247					component = ''
248				continue
249
250			component += c
251			if c == '"':
252				inQuote = True
253
254		if component:
255			components.append(component)
256			component = ''
257
258		return components
259
260	@staticmethod
261	def splitItemAndUnquote(string):
262		components = ConfigParser.splitItem(string)
263		unquotedComponents = []
264		for component in components:
265			if component and component[0] == '"' and component[-1] == '"':
266					# use a regex if this called a lot
267				component = component[1:-1]
268			unquotedComponents.append(component)
269		return unquotedComponents
270
271	@staticmethod
272	def configurationStringFromDict(config):
273		configurationString = ''
274		for key in config.keys():
275			configurationString += key + '="'
276
277			if isinstance(config[key], list):
278				configurationString += functools.reduce(
279					lambda result, item: result + ' ' + item, config[key],
280					'').strip()
281			elif type(config[key]) is bool:
282				configurationString += 'yes' if config[key] else 'no'
283			else:
284				configurationString += str(config[key])
285
286			configurationString += '"\n'
287
288		return configurationString
289