'''Look at file names and make educated guesses about them

Copyright 2000-2002 by Daniel Fandrich <dan@coneharvesters.com>

$Id: fileanalyze.py,v 1.6 2003/03/09 07:40:19 dan Exp $
'''
# TO DO:
#  - fix some of the too-aggresive Group: detection lines
#  - incorporate the classifyfile dependencies into here

import regex
import posixpath
import classifyfile

#debuglog = printerr			# enable this for debug logs
def debuglog(errstr): pass		# enable this to stop debug logs


class FileInfo:
	'Structure for holding information about an installed file'

	def __init__(self,name=None,mode=None,owner=None,group=None,isdir=None):
		self.name = name
		self.mode = mode
		self.owner = owner
		self.group = group
		self.isdir = isdir

	def setattrib(self,mode=None,owner=None,group=None,isdir=None):
		'Overwrite the file attributes, if specified'
		if mode != None:
			self.mode = mode
		if owner != None:
			self.owner = owner
		if group != None:
			self.group = group
		if isdir != None:
			self.isdir = isdir

	def isadir(self):
		'Returns nonzero if the file is a directory'
		return self.isdir

	# Subsume the old classifyfile functions.
	# I should really obsolete that one and move that stuff in here.

	def lookslikedir(self):
		'Returns nonzero if the file looks like a directory'
		return classifyfile.lookslikedir(self.name)

	def lookslikeconfig(self):
		'Returns nonzero if the file looks like a configuration file'
		return classifyfile.lookslikeconfigfile(self.name)

	def re_match(self,re):
		'Returns nonzero if the compiled regex matches the file name'
		return re.search(self.name) >= 0

	def re_match_base(self,re):
		'Returns nonzero if the compiled regex matches the base file name'
		return re.search(posixpath.basename(self.name)) >= 0

	# Patterns matching license documentation files
	license_file_re = regex.compile(
				  '^[0-]*\('		# This can apply to many of the following
					'Artistic\|'
					'COPYING\|'
					'Copyleft\|'
					'Copyright\|'
					'CPYRIGHT\|'
					'DISCLAIMER\|'
					'GPL\|'
					'LEGAL\|'
					'LGPL\|'
					'LICENSE\|'
					'POLICY$\|'
					'PUBLIC$\|'
					'shareware\|'
					'WARRANTY'
					'\)'
					, regex.casefold)

	def lookslikelicense(self):
		'Returns 1 if a file has a common license file name'
		return self.re_match_base(self.license_file_re)

	# Patterns matching extra documentation files
	# Don't forget to put complex regular expressions in parenthesis to prevent
	# the special characters from applying to too much
	# These are all case insensitive
	doc_file_re = regex.compile(
				  '^[0-]*\('		# This can apply to many of the following
					'\(READ[\._-]*ME.*\)\|'
					'ABOUT-\|'
					'ANNOUNCE\|'
					'Artistic\|'
					'AUTHOR\|'
					'BACKLOG\|'
					'bugreports\|'
					'BUGS\|'
					'CHANGES\|'
					'ChangeLog\|'
					'COPYING\|'
					'Copyleft\|'
					'Copyright\|'
					'CPYRIGHT\|'
					'CREDITS\|'
					'DISCLAIMER\|'
					'\(-?docs?$\)\|'	# often directory
					'documentation\|'	# often directory
					'documents\|'
					'FAQ\|'
					'FEATURES\|'
					'GOTCHAS\|'
					'GPL\|'
					'guide$\|'
					'HINTS\|'
					'HISTORY$\|'
					'HOWTO$\|'
					'INFO\|'
					'INSTALL$\|'
					'KNOWNBUGS\|'
					'LEGAL\|'
					'LGPL\|'
					'License\|'
					'manual\|'
					'NEWS\|'
					'NOTES$\|'
					'NOTICE\|'
					'OVERVIEW\|'
					'POLICY$\|'
					'Problems\|'
					'QuickStart\|'
					'referenc\|'
					'RELEASE\|'
					'RELNOTES\|'
					'shareware\|'
					'THANKS\|'
					'TODO\|'
					'TO-DO\|'
					'TROUBLE\|'
					'TUTORIAL\|'
					'Usage\|'
					'WARRANTY\|'
					'WHATSNEW'
					'\)\|\('			# Following are suffixes
					'\.doc$\|'
					'\(\.html?$\)\|'
					'\.info\|'			# Not always a pure suffix
					'\.man$\|'
					'\.me$\|'
					'\.pdf$\|'
					'\.ps$\|'
					'\.txt$\|'
					'-LICENSE$'
					'\)'
					, regex.casefold)

	def lookslikedoc(self):
		'Returns 1 if a file has a common documentation file name'
		return classifyfile.lookslikedocfile(self.name) or \
			self.re_match_base(self.doc_file_re)

	# Patterns matching readme documentation files
	readme_file_re = regex.compile(
				  '^[0-]*\('		# This can apply to many of the following
					'\(READ[\._-]*ME.*\)'
					'\)'
					, regex.casefold)

	def lookslikereadme(self):
		'Returns 1 if file has a common README file name'
		if self.re_match_base(self.readme_file_re):
			return self.lookslikedoc()
		return 0

	devlib_re = regex.compile('\(\<lib[^\./]*\.so\)\|\(\<lib[^\./]*\.a\)$')
	def lookslikedevlib(self):
		'Return nonzero if file looks like a development library'
		return self.re_match(self.devlib_re)

	lib_re = regex.compile('\<lib[^/]+\.so[\.0-9]+$')
	def lookslikelib(self):
		'Return nonzero if file looks like a library of any kind'
		return self.re_match(self.lib_re) or self.lookslikedevlib()

	x11font_re = regex.compile('\(/X11/fonts/\)\|\(X11R6/lib/fonts/\)')
	def lookslikex11font(self):
		'Return nonzero if file looks like an X11 font'
		return self.re_match(self.x11font_re)

	x_re = regex.compile('\(\<bin/[xX].*\)\|\(\<X11\(R[0-9]\)?/bin\>\)')
	def lookslikex(self):
		'Return nonzero if file looks like an X11 binary.'
		return self.re_match(self.x_re)

	xapp_re = regex.compile('\(/etc/X11/wmconfig/\)\|\(/etc/X11/applnk/\)\|\(/share/gnome/apps/\)\|\(/share/applnk/\)\|\(/share/applets/\)\|\(/usr/lib/menu/\)')
	def lookslikexapp(self):
		'Return nonzero if file looks like an X11 application.'
		return self.re_match(self.xapp_re)

	include_re = regex.compile('\<include/')
	def lookslikeinclude(self):
		'Return nonzero if file looks like an include file.'
		return self.re_match(self.include_re)

	autoconf_re = regex.compile('/aclocal/')
	def lookslikeautoconf(self):
		'Return nonzero if file looks like an autoconf script.'
		return self.re_match(self.autoconf_re)

	module_re = regex.compile('\<lib/modules/')
	def lookslikemodule(self):
		'Return nonzero if file looks like a kernel module file.'
		return self.re_match(self.module_re)

	inet_re = regex.compile('\<etc/xinet.d/')
	def lookslikeinet(self):
		'Return nonzero if file looks like an Internet daemon.'
		return self.re_match(self.inet_re)

	game_re = regex.compile('\(\<games\>\)\|\(scores?\)')
	def lookslikegame(self):
		'Return nonzero if file looks like a game file.'
		return self.re_match(self.game_re)

	cron_re = regex.compile('/etc/cron\.')
	def lookslikecron(self):
		'Return nonzero if file has a cron entry.'
		return self.re_match(self.cron_re)

	sbin_re = regex.compile('/sbin/')
	def lookslikesbin(self):
		'Return nonzero if file is a system binary.'
		return self.re_match(self.sbin_re)

	rcdinit_re = regex.compile('/etc/rc.d/init.d/')
	def lookslikercdinit(self):
		'Return nonzero if file is an /etc/rc.d/init/ file.'
		return self.re_match(self.rcdinit_re)


class FileList:
	'List-like class to hold information about files'

	# Put class here so users have easy access
	FileInfo = FileInfo

	def __init__(self,names=None):
		'''Create the file list object.  It can be created from a list of
		file names or FileInfo objects; in the former case, they are
		converted to FileInfo objects and deduped.'''
		self.filelist = []
		if names:
			for name in names:
				if type(name) == type(''):
					self.getentry(name)
				else:
					self.append(name)

	def __add__(self, otherlist):
		'Add two FileList objects and return a new object'
		return self.__class__(self.filelist + otherlist.filelist)

	def __radd__(self, otherlist):
		'Add two FileList objects and return a new object'
		return self.__class__(self.filelist + otherlist.filelist)

	def __len__(self):
		return len(self.filelist)

	def __getitem__(self,index):
		return self.filelist[index]

	def has_key(self, name):
		if self.find(name):
			return 1
		return 0

	def find(self, name):
		'Returns FileInfo entry if a file is in the list'
		try:
			namelist = self.filenames()
			return self.filelist[namelist.index(name)]
		except ValueError:
			pass
		return None	# Not found

	def append(self, info):
		'Add a FileInfo object to the file list'
		self.filelist.append(info)

	def getentry(self, name):
		'Return an existing FileInfo object from the list, or a new one'
		fileinfo = self.find(name)
		if not fileinfo:
			# Create a new entry
			fileinfo = self.FileInfo()
			fileinfo.name = name
			self.append(fileinfo)
		return fileinfo

	def filenames(self):
		'Return list of all file names'
		return map(lambda f: f.name, self.filelist)


class FileListAnalyze(FileList):
	'Analyze the files in the list and make inferences about them'

	def finddir(self, name):
		'Returns 1 if a file is in the list and is marked as a directory'
		# TODO: search for files within this directory as well
		# e.g. self.finddir('/tmp/foo') will return 0 despite the file
		# '/tmp/foo/bar' being in the list
		file = self.find(name)
		if file:
			return file.isadir()
		return 0

	def onlynondocfiles(self):
		'''Return a FileListAnalyze of only the file that don't look like
		%doc files'''
		newlist = FileListAnalyze()
		for file in self:
			# If it's a directory or executable, it's not a %doc file
			if not file.lookslikedoc() or file.isadir() or \
				(file.mode and (file.mode & 0111)):
				newlist.append(file)
		return newlist

	def onlyreadmefiles(self):
		'Return a FileListAnalyze object with only the README files'
		newlist = FileListAnalyze()
		for file in self:
			if file.lookslikereadme():
				newlist.append(file)
		return newlist

	def matchany(self,checkmethod):
		'''Return nonzero if any file in the list returns nonzero when the
		given method is called.'''
		return reduce(lambda flag,item,c=checkmethod: \
							flag or getattr(item,c)(),
					  self.filelist, 0)

	def matchall(self,checkmethod):
		'''Return nonzero if all files in the list return nonzero when the
		given method is called.'''
		return reduce(lambda flag,item,c=checkmethod: \
							flag and getattr(item,c)(),
					  self.filelist, 1)

	def isalldevlib(self):
		'''Return nonzero if all files in the FileList object look like
		development libraries.'''
		return self.matchall('lookslikedevlib')

	def isalllib(self):
		'Return nonzero if all files in the FileList object look like libraries'
		return self.matchall('lookslikelib')

	def isanylib(self):
		'Return nonzero if any file in the FileList object looks like a library'
		return self.matchany('lookslikelib')

	def isalldoc(self):
		'Return nonzero if all files in FileList object look like %doc files'
		return self.matchall('lookslikedoc')

	def isallx11font(self):
		'Return nonzero if all files in the FileList object look like X11 fonts'
		return self.matchall('lookslikex11font')

	def isanyx(self):
		'''Return nonzero if at least one file in the FileList object looks
		like an X11 binary.'''
		return self.matchany('lookslikex')

	def isanyxapp(self):
		'''Return nonzero if at least one file in the FileList object looks
		like an X11 application.'''
		return self.matchany('lookslikexapp')

	def isanyinclude(self):
		'''Return nonzero if at least one file in the FileList object looks like
		an include file.'''
		return self.matchany('lookslikeinclude')

	def isanyautoconf(self):
		'''Return nonzero if at least one file in the FileList object looks like
		an autoconf script.'''
		return self.matchany('lookslikeautoconf')

	def isanymodule(self):
		'''Return nonzero if at least one file in the FileList object looks like
		a kernel module file.'''
		return self.matchany('lookslikemodule')

	def isanyinet(self):
		'''Return nonzero if at least one file in the FileList object looks like
		an Internet daemon.'''
		return self.matchany('lookslikeinet')

	def isanygame(self):
		'''Return nonzero if at least one file in the FileList object looks like
		a game file.'''
		return self.matchany('lookslikegame')

	def isanycron(self):
		'''Return nonzero if at least one file in the FileList object has a cron
		entry.'''
		return self.matchany('lookslikecron')

	def isanysbin(self):
		'''Return nonzero if at least one file in the FileList object is a
		system binary.'''
		return self.matchany('lookslikesbin')

	def isanyrcdinit(self):
		'''Return nonzero if at least one file in the FileList object is a
		/etc/rc.d/init/ file.'''
		return self.matchany('lookslikercdinit')

	def __match_distribution(self,dist):
		'Return the group index appropriate for the given distribution'

		# List of known distributions and the index into the group list given
		# to guessgroup.
		# Names match from the left.
		# List them in order from most specific to least specific.
		supported_distributions = [
			('Red Hat Linux release 4',0),
			('Red Hat Linux release 5',0),# Same as release 4
			('Red Hat Linux release 6',1),
			('Red Hat Linux release 7',1),# Same as release 6
			('Red Hat Linux release 8',1),# Same as release 6
			('Red Hat Linux release',1),  # Red Hat catch-all
			('ASPLinux release 7',1),	# Same as Red Hat 7
			('ASPLinux release',1),	# ASPLinux catch-all
			('SuSE Linux 7',2),
			('SuSE Linux',2),	# SuSE catch-all
			('Mandrake Linux release 8',3),
			('Mandrake Linux release 9',4),
			('Mandrake Linux release',4), # Mandrake catch-all
			('Conectiva Linux 8',5),
			('Conectiva Linux',5), # Conectiva catch-all
			#('Turbolinux Server 7',6),
			#('Turbolinux Server',6),	# Turbolinux catch-all
		]
		# Distribution to use if no better match was found
		default_dist = 'Red Hat Linux release 7'

		for distribution,index in supported_distributions:
			if dist[:len(distribution)] == distribution:
				return index
		# Recursive call to match the default distribution (which had
		# better exist...)
		return self.__match_distribution(default_dist)


	def guessgroup(self,distribution=''):
		'''Look at files in the list and guess which RPM group best
		describes them'''
		# TODO: maybe change choosegroup to take a dictionary instead of
		#       a list; dict takes short handles e.g. RH4 so it's easier
		#       to see group belongs to which distribution

		if not distribution:
			distribution = whichdistribution()

		# Create a function to return the appropriate group name
		def choosegroup(grouplist,
						distindex=self.__match_distribution(distribution)):
			'Return the appropriate group name from a list'
			return grouplist[distindex]

		nondocfiles = self.onlynondocfiles()
		group = None
		if self.isalldoc():
			group = choosegroup([
						'Documentation',
						'Documentation',
						'Documentation',
						'Books',
						'Books',
						'Documentation',
						])

		elif self.isanyinclude():
			# This may be a bit aggressive...see Wine, xdigger
			group = choosegroup([
						'Development/Libraries',
						'Development/Libraries',
						None,	# SuSE combines development files with main package
						'Development/Other',
						'Development/Other',
						'Development',
						])

		elif self.isalldevlib():
			group = choosegroup([
						'Development/Libraries',
						'Development/Libraries',
						'Development/Libraries',
						'Development/Other',
						'Development/Other',
						'Development',
						])

		elif self.isanymodule():
			group = choosegroup([
						'Base/Kernel',
						'System Environment/Base',
						'Base/Kernel',
						'System/Kernel and hardware',
						'System/Kernel and hardware',
						None,	# What is the group for this?
						])

		elif nondocfiles.isallx11font():
			group = choosegroup([
						'X11/Fonts',
						'User Interface/X',
						'X11/Fonts',
						'System/Fonts',
						'System/Fonts',
						'X11',
						])

		elif self.isanyx():
			# X11 group of some sort
			group = choosegroup([
						'X11',
						'Applications',
						'X11',
						'Applications',
						'Applications',
						None,
						])
			if self.isanygame():
				group = choosegroup([
						'X11/Games',
						'Amusements/Games',
						'X11/Amusements',
						'Games',
						'Games',
						'Games',
						])

			elif nondocfiles.isalllib():
				group = choosegroup([
						'X11/Libraries',
						'System Environment/Libraries',
						'X11/Libraries',
						'System/Libraries',
						'System/Libraries',
						'Libraries',
						])

		elif self.isanyxapp():
			# X11 application of some sort
			group = choosegroup([
						'X11',
						'Applications',
						'X11',
						'Applications',
						None,
						None,
						])
		elif self.isanyautoconf():
			group = choosegroup([
						'Development',
						'Development',
						'Development',
						'Development',
						'Development',
						'Development',
						])

		elif self.isanyinet():
			group = choosegroup([
						'Networking',
						'Applications/Internet',
						'Networking',
						'Networking',
						'Networking',
						'Networking',
						])

		elif self.isanygame():
			group = choosegroup([
						'Games',
						'Amusements/Games',
						'Games',
						'Games',
						'Games',
						'Games',
						])

		elif self.isanycron():
			group = choosegroup([
						'Utilities',
						'System Environment/Base',
						'Utilities',
						None,
						None,
						None,
						])

		elif nondocfiles.isalllib():
			group = choosegroup([
						'Libraries',
						'System Environment/Libraries',
						'System Environment/Libraries',
						'System/Libraries',
						'System/Libraries',
						'Libraries',
						])

		elif nondocfiles.isanyrcdinit():
			group = choosegroup([
						'Daemons',
						'System Environment/Daemons',
						'System Environment/Daemons',
						'System Environment/Daemons',
						'System/Servers',
						None,
						])

		elif nondocfiles.isanysbin():
			group = choosegroup([
						'Utilities/System',
						'Applications/System',
						'Utilities/System',
						'System',
						'System',
						'Utilities',
						])

		return group

# List of known licenses and regexes to match.
# The regexes are searched in order and the first match wins.
# The '[^t] +' substrings are an ugly way to eliminate 'NOT public domain'
# et. al. from being matched.
licenses = [
	('GPL',
	 regex.compile('\<GNU GENERAL PUBLIC LICENSE\>',regex.casefold)),
	('LGPL',
	 regex.compile('\<GNU \(\(LIBRARY\)\|\(LESSER\)\) GENERAL PUBLIC LICENSE\>',regex.casefold)),
	('Mozilla Public Licence',
	 regex.compile('\<MOZILLA PUBLIC LICENSE\>',regex.casefold)),
	('Q Public License',
	 regex.compile('\<THE Q PUBLIC LICENSE\>',regex.casefold)),
	('Artistic License',
	 regex.compile('\<The \(Clarified\)? *"?Artistic License"?\>',regex.casefold)),
	('GNU GNU Free Documentation License',
	 regex.compile('\<GNU Free Documentation License\>',regex.casefold)),
	('Open Public License',
	 regex.compile('\<OPEN PUBLIC LICENSE\>',regex.casefold)),
	('Open Publication License',
	 regex.compile('\<Open Publication License\>',regex.casefold)),
	('OpenContent License',
	 regex.compile('\<OpenContent License\>',regex.casefold)),
	('BSD-like',
	 regex.compile('hereby grant permission to use, copy, modify, distribute,',regex.casefold)),
	('BSD-like',
	 regex.compile('\<Redistributions of source code must retain \(\(any existing\)\|\(the above\)\) copyright\>',regex.casefold)),
	('BSD-like',
	 regex.compile('\<BSD[- ]style\>',regex.casefold)),
	('shareware', regex.compile('[^t] +["\']?shareware\>',regex.casefold)),
	('freeware', regex.compile('[^t] +["\']?freeware\>',regex.casefold)),
	('freeware', regex.compile('\<free for commercial and non-?commercial use\>',regex.casefold)),
	('postcardware', regex.compile('["\']?postcard',regex.casefold)),
	('public domain', regex.compile('[^t] +["\']?public[ -]*domain\>',regex.casefold)),
	('distributable', regex.compile('\<you may distribute\>',regex.casefold)),
	('distributable', regex.compile('\<may be distributed\>',regex.casefold)),
	('distributable', regex.compile('\<distributed free of charge\>',regex.casefold)),
]

def lookforlicense(docfiles):
	'Look through the files in the list find recognized licenses'
	numlines = 15	# number of lines to search before giving up

	# Look through all 'extra' documentation files
	for fn in docfiles:
		debuglog('Checking %s for a known license' % fn)
		try:
			f = open(fn)
		except IOError:
			continue

		# Check first numlines in each file
		count = numlines
		while count:
			l = f.readline()
			if not l:
				break
			# Search all known licenses
			for license_match in licenses:
				if license_match[1].search(l) >= 0:
					debuglog('Matched the %s license' % license_match[0])
					debuglog('on the line %s' % l)
					# We can't guarantee this is the license we think it is
					# with this method, so put a question mark after each one.
					return license_match[0]+'?'

			count = count - 1
		f.close()
	return None


def whichdistribution():
	'Look at files on the local drive to find the OS distribution in use'
	checkfiles = [
		'/etc/asplinux-release',# This one must be before redhat-release because
								# ASP also includes a false redhat-release
		'/etc/redhat-release',
		'/etc/SuSE-release',
		'/etc/mandrake-release',
		'/etc/conectiva-release',
		'/etc/turbolinux-release',
	]
	for fn in checkfiles:
		try:
			f = open(fn)
			return f.readline()
		except IOError:
			# Ignore any files that can't be found
			pass
	return ''	# No distribution found

