# Copyright (C) 1997 Red Hat Software, Inc.
# Use of this software is subject to the terms of the GNU General
# Public License

from Conf import Conf
import regsub
import string

# TODO
# Add fs_specific functionality

# ConfFstab(Conf):
#  Yet another dictionary, with mountpoint for the key.
#  Look things up with fs[mount], which returns a dictionary with elements
#  device, mountpoint, fstype, options, dumpfreq, and fsckpass.
#  The first three are strings.  Options is a dictionary
#  with a constrained set of names and canonicalization.
#  dumpfreq and fsckpass are integers.
#  So fs[mount].options.rsize = 8192 sets the read size
#  fs[mount].options.<option>. returns the default if it is not set.
#  To find out if an option is explicitly set, see if
#  fs[mount].options.<option> is an empty string.
#  Of pairs of options, only one is supported -- no<option> is never
#  a correct name, instead, use "....<option> = 0".
#  Similarly, use "....rw = 0" instead of "....ro = 1"

_booleans_lookup = {
	# each element's value is a list of (name, value) tuples
	# or [name, {value:<assignment>...}] lists where
	# <assignment> is either a tuple or a list, recursively.
	'atime':[('atime', 1)],
	'noatime':[('atime', 0)],
	'auto':[('auto', 1)],
	'noauto':[('auto', 0)],
	'quiet':[('quiet', 1)],
	'soft':[('soft', 1)],
	'nosoft':[('soft', 0)],
	'hard':[('soft', 0)],
	'nohard':[('soft', 1)],
	'intr':[('intr', 1)],
	'nointr':[('intr', 0)],
	'rw':[('rw', 1)],
	'ro':[('rw', 0)],
	'fg':[('fg', 1)],
	'nofg':[('fg', 0)],
	'bg':[('fg', 0)],
	'nobg':[('fg', 1)],
	'defaults':[
	  # read: "if self.dict['rw'] is 0, set self.dict['rw'] to 1"
	  ['rw', {0:('rw', 1)}],
	  ['suid', {0:('suid', 1)}],
	  ['dev', {0:('dev', 1)}],
	  ['exec', {0:('exec', 1)}],
	  ['auto', {0:('auto', 1)}],
	  ['user', {1:('user', 0)}],
	  ['sync', {1:('sync', 0)}]
	],
	'user':[
	  ('user', 1),
	  ['suid', {1:('suid', 0)}],
	  ['dev', {1:('dev', 0)}],
	  ['exec', {1:('exec', 0)}]
	],
	'dev':[
	  ['dev', {0:('dev', '')}],
	  ['user', {1:('dev', 1)}],
	],
	'suid':[('suid', 1)],
	'nosuid':[('suid', 0)],
	'exec':[('exec', 1)],
	'noexec':[('exec', 0)],
	'sync':[('sync', 1)],
	'nosync':[('sync', 0)]
	
}

_booleans_assign = {
	# This table is only for variables that are completely
	# independent of all other variables
	'atime':{0:'noatime', 1:''},
	'auto':{0:'noauto', 1:''},
	'cruft':{0:'', 1:'cruft'},
	'debug':{0:'', 1:'debug'},
	'fg':{0:'bg', 1:''},
	'grpid':{0:'', 1:'grpid'},
	'intr':{0:'', 1:'intr'},
	# map should really be map/nomap
	# this kind of mapping doesn't fit the scheme; leave it as unknown now
	#'map':{0:'map=off', 1:''},
	'quiet':{0:'', 1:'quiet'},
	'rw':{0:'ro', 1:''},
	'soft':{0:'', 1:'soft'},
	'sync':{0:'', 1:'sync'},
	'user':{0:'', 1:'user'}
}

_fs_specific = {
	# This dictionary maps option names to a tuple of legal fs's
	'block':('iso9660',),
	'case':('hpfs', ),
	'check':('ext2', 'msdos'),
	'conv':('msdos', 'hpfs', 'iso9660'),
	'cruft':('iso9660',),
	'debug':('ext2', 'msdos'),
	'errors':('ext2',),
	'fat':('msdos',),
	'fg':('nfs',),
	'bg':('nfs',),
	'gid':('msdos', 'hpfs'),
	'grpid':('ext2',),
	'intr':('nfs',),
	'nointr':('nfs',),
	'map':('iso9660',),
	'quiet':('msdos',),
	'rsize':('nfs',),
	'soft':('nfs',),
	'nosoft':('nfs',),
	'hard':('nfs',),
	'nohard':('nfs',),
	'sb':('ext2',),
	'uid':('msdos', 'hpfs'),
	'umask':('msdos', 'hpfs'),
	'wsize':('nfs',),
}

# invert that dictionary for lookup later; doing this by hand would
# invite mistakes.
_fs_specific_by_fs = {}
for option in _fs_specific.keys():
    for fs in _fs_specific[option]:
	if not _fs_specific_by_fs.has_key(fs):
	    _fs_specific_by_fs[fs] = []
	_fs_specific_by_fs[fs].append(option)
del option
del fs

_values = (
	# This table is only for variables that can be set to
	# '<value>=' + str(self.dict[<value>])
	'case',
	'check',
	'conv',
	'block',
	'errors',
	'fat',
	'gid',
	'grpid',
	'rsize',
	'sb', # should only be used with damaged disks; should be unknown?
	'uid',
	'umask',
	'wsize'
)

_dependants = {
	'exec':{'':('user', {'':'', 0:'', 1:'noexec'}),
		0:'noexec',
		1:('user', {'':'', 0:'', 1:'exec'})},
	'suid':{'':('user', {'':'', 0:'', 1:'nosuid'}),
		0:'nosuid',
		1:('user', {'':'', 0:'', 1:'suid'})},
	'dev':{'':('user', {'':'', 0:'', 1:'nodev'}),
		0:'nodev',
		1:('user', {'':'', 0:'', 1:'dev'})}
}

_fieldmap = {
	'device':0,
	'mountpoint':1,
	'fstype':2,
	'dumpfreq':4,
	'fsckpass':5
}

class _fsline_reflector:
    def __init__(self, fstab, mountpoint):
	self.fstab = fstab
	self.mount = mountpoint
	self.option_reflector = _option_reflector(fstab, self)
    def __setattr__(self, name, value):
	if name in ('dev', 'fstab', 'mount', 'option_reflector', 'fsckpass'):
	    self.__dict__[name] = value
	    return None
	elif not self.fstab.has_key(self.mountpoint):
	    raise AttributeError, self.mountpoint+' has been deleted'
	elif name == 'options':
	    raise AttributeError, "'options' field is immutable"
	elif name in _fieldmap.keys():
	    self.fstab.setfield(self.mount, _fieldmap[name], value)
	    # These next lines MUST come AFTER the previous line, and
	    # must be in this precise order to keep the fsline cache coherent.
	    if name == 'device':
		del self.fstab.fsline_cache[self.mountpoint]
		self.dev = value
		self.fstab.fsline_cache[self.mountpoint] = self
	    elif name == 'mountpoint':
		del self.fstab.fsline_cache[self.mountpoint]
		self.mount = value
		self.fstab.fsline_cache[self.mountpoint] = self
	else:
	    raise AttributeError, name+": unknown field name"
    def __getattr__(self, name):
	if not self.fstab.has_key(self.mount):
	    raise AttributeError, self.mount+' has been deleted'
	elif name == 'options':
	    return self.option_reflector
	elif name in _fieldmap.keys():
	    return self.fstab.getfield(self.mount, _fieldmap[name])
	else:
	    raise AttributeError, name+": unknown field name"

class _option_reflector:
    def __init__(self, fstab, fsline):
	self.fstab = fstab
	self.fsline = fsline

	# Create dictionary self.options with options expressed
	# in canonical form
	self.dict = {}
	self.unknown = []

	for option in ['atime', 'auto', 'dev', 'exec', 'rw',
	  'suid', 'sync', 'user', 'case', 'check', 'conv', 'block',
	  'cruft', 'debug', 'errors', 'fat', 'gid', 'grpid',
	  'quiet', 'soft', 'sb', 'uid', 'umask', 'rsize', 'wsize',
	  'fg', 'soft', 'intr']:
	    self.dict[option] = ''

	for optval in regsub.split(fstab.getfield(fsline.mount, 3), ','):
	    optlist = regsub.split(optval, '=')
	    option = optlist[0]

	    # ignore bad options for this fs type -- mount will
	    # barf on them anyway.
	    if _fs_specific.has_key(option) and \
	       not self.fsline.fstype in _fs_specific[option]:
		pass

	    elif _booleans_lookup.has_key(option):
		for element in _booleans_lookup[option]:

		    if type(element) == type(()):
			# just store new value
			self.dict[element[0]] = element[1]

		    elif type(element) == type([]):
			[name, dict] = element
			currentvalue = self.dict[name]
			if dict.has_key(currentvalue):
			    self.dict[dict[currentvalue][0]] = \
				dict[currentvalue][1]

	    elif option in _values:
		self.dict[option] = optlist[1]

	    else:
		# use optval, not optlist, since it will be recorded unchanged
		self.unknown.append(optval)

    def __getattr__(self, name):
	if name == 'exc': return self.dict['exec']
	return self.dict[name]
    def __setattr__(self, name, value):
	if name in ('fstab', 'fsline', 'get_string', 'canon_option',
		    'dict', 'unknown'):
	    self.__dict__[name] = value
	    return
	if name == 'exc': self.dict['exec'] = value
	else: self.dict[name] = value
	# make sure options get canonicalized before being written
	self.fstab.options_to_canon[self] = 1
    def get_string(self):
	"""returns canonical option string"""
	if self.unknown: optstring = string.join(self.unknown, ",")
	else: optstring = ''
	# Note that in the list, the options that might be overriden
	# by other options must come first.  This way, for the rest
	# of the options, we don't need to know which other options
	# depend on them here.
	opts = []
	for option in ['user', 'auto', 'dev', 'exec', 'rw', 'suid']:
	    if not self.dict[option] == '':
		opts.append(option)
	for option in self.dict.keys():
	    if not self.dict[option] == '' and not option in opts:
		opts.append(option)
	for option in opts:
	    canon = self.canon_option(option)
	    if len(canon):
		if optstring:
		    optstring = optstring + ',' + canon
		else:
		    optstring = canon
	if len(optstring) == 0:
	    return 'defaults'
	return optstring
    def canon_option(self, option):
	# returns the canonical string representation of the option.
	# This MUST be called for the options in "canonical order",
	# which I'm defining as:
	# 'user', 'auto', 'dev', 'exec', 'rw', and 'suid' first, then
	# all the rest.

	if _fs_specific_by_fs.has_key(option) and \
	   self.fsline.fstype not in _fs_specific_by_fs[option]:
	    # ignore the option if it does not pertain to this fs type
	    return ''

	# This makes it unnecessary to invert _dependants
	if _booleans_assign.has_key(option) and \
	   _booleans_assign[option].has_key(self.dict[option]):
	    return _booleans_assign[option][self.dict[option]]

	if option in _values:
	    return option + '=' + str(self.dict[option])

	if _dependants.has_key(option):
	    current = _dependants[option][self.dict[option]]
	    while type(current) == type(()):
		current = current[1][self.dict[current[0]]]
	    return current

	# Must be an unrecognized option
	raise AttributeError, option + ': option not recognized'

class ConfFstab(Conf):
    def __init__(self, fstab='/etc/fstab'):
        Conf.__init__(self, fstab)
    def read(self):
        Conf.read(self)
        self.initvars()
    def initvars(self):
	self.fsf()
	self.options_to_canon = {}
	# the fsline cache is kept coherent by getitem and add in this
	# class, and by the fsline objects themselves when they
	# detect their keys changing.
	self.fsline_cache = {}

    def write(self):
	self.canonize()
	Conf.write(self)

    def canonize(self):
	"""canonicalize all option strings before writing"""
	for option in self.options_to_canon.keys():
	    # change third field
	    self.setfield(option.fsline.mount, 3, option.get_string())

    def getfield(self, mount, fieldno):
	self.rewind()
	self.findnextmatch(mount)
	fields = self.getfields()
	if fieldno < len(fields): return fields[fieldno]
	else: return ''

    def setfield(self, mount, fieldno, value):
	self.rewind()
	# set all instances -- should only be one, but you never know
	# what a user will do accidentally.  :-)
	while self.findnextmatch(mount):
	    fields = self.getfields()
	    # if we want to write to a missing field, append a '0'
	    # (only the last two optional fields might be missing)
	    while len(fields) < 6: fields.append('0')
	    fields[fieldno] = value
	    # keep matching up with tabs
	    self.setline("%-23s %-23s %-7s %-15s %s %s" % tuple(fields))
	    self.nextline()

    def __getitem__(self, mount):
	if not self.fsline_cache.has_key(mount):
	    self.fsline_cache[mount] = _fsline_reflector(self, mount)
	return self.fsline_cache[mount]

    def __setitem__(self, mount, value):
	raise AttributeError, 'ConfFstab objects are immutable'

    def __delitem__(self, mount):
	self.rewind()
	# set all instances -- should only be one, but you never know
	# what a user will do accidentally.  :-)
	while self.findnextmatch(mount):
	    self.deleteline()

    def move(self, mount, newmount):
	# change a mountpoint
	if self.fsline_cache.has_key(mount):
	    self.fsline_cache[newmount] = self.fsline_cache[mount]
	    del self.fsline_cache[mount]
	    self.fsline_cache[newmount].mount = newmount
	self.setfield(mount, 1, newmount)

    def add(self, dev, mount, fstype):
	"""add a basic entry that can be modified later; return fsline object"""
	if self.has_key(mount):
	    raise AttributeError, 'mountpoint already exists: '+mount
	self.fsf()
	self.setfields([dev, mount, fstype, 'defaults', '0', '0'])
	return self[mount]

    def has_key(self, mount):
	self.rewind()
	return self.findnextmatch(mount)

    def keys(self):
	keys = []
	self.rewind()
	while self.findnextcodeline():
	    keys.append(self.getfields()[1])
	    self.nextline()
	return keys

    def findnextmatch(self, mount):
	"""only return lines that not only contain the key, but are legal
	lines with at least four fields.  We depend on that constraint
	elsewhere in this class."""
	seps = self.separators
	return self.findnextline('^['+seps+']*[^'+seps+']+['+seps+']+'+mount+
	    '['+seps+']+[^'+seps+']+['+seps+']+[^'+seps+']')


# test code
if __name__ == '__main__':
	f = ConfFstab()
	for mount in f.keys():
	    r = f[mount]
	    # "modify"
	    r.options.soft = r.options.soft
	f.canonize()
	for line in f.lines:
	    print line
