#!/bin/ksh93

FPROG=${.sh.file}
PROG=${FPROG##*/}

function fixGroups {
	typeset -n OLDGID2NEW=$1 GID2GROUP=$2 OLDGID2NAME=$3

	# Official groups overall platforms. Werden _alle_ präventiv in /etc/group
	# eingetragen, damit der abgefuckte Ubuntu-Schrott die nicht später mal
	# überschreibt
	typeset -A GROUPS=(
		# Solaris was first
		[root]='root:0'				# same on linux
		[other]='other:1:root'
		[bin]='bin:2:root,daemon'	# same on linux
		[sys]='sys:3:root,bin,adm'	# same on linux
		[adm]='adm:4:root,daemon'	# same on linux
		[uucp]='uucp:5:root'
		[mail]='mail:6:root'		# chgrp -R mail /var/mail
		[tty]='tty:7:root,adm'
		[lp]='lp:8:root,adm'
		[nuucp]='nuucp:9:root'		# alias news
		[staff]='staff:10'			# chown root:staff /var/log/sulog
		[daemon]='daemon:12:root'	# chown -R daemon:daemon /var/cron/atjobs
		[sysadmin]='sysadmin:14'
		[games]='games:20'
		[${SSH}]="${SSH}:22"			# openssh /usr/bin/ssh-agent sgid
		[smmsp]='smmsp:25'
		[dovecot]='dovecot:43'		# LNF
		[dovenull]='dovenull:44'	# LNF
		[gdm]='gdm:50'
		[upnp]='upnp:52'
#		[xvm]='xvm:60'
		[aiuser]='aiuser:61'
		[netadm]='netadm:65'
		[mysql]='mysql:70'
		[openldap]='openldap:75'
		[webservd]='webservd:80'
#		[cinder]='cinder:81'
#		[glance]='glance:82'
#		[keystone]='keystone:83'
#		[neutron]='neutron:84'
#		[nova]='nova:85'
#		[swift]='swift:86'
#		[heat]='heat:87'
#		[evsgroup]='evsgroup:89'
		[postgres]='postgres:90'	# chown -R postgres:postgres {/var/lib,/var/log,/etc}/postgresql
		[ironic]='ironic:91'
		[mlocate]='mlocate:95'
		[unknown]='unknown:96'
		[pkg5srv]='pkg5srv:97'
		[vboxuser]='vboxuser:499'
		[nobody]='nobody:60001'
		[noaccess]='noaccess:60002'
		[nogroup]='nogroup:65534'	# same on linux

		# Linux - just to have the more or less important groups stable: crazy
		# ubuntu assigns GIDs even to system accounts in an unstable manner!
		[kmem]='kmem:15'		# /dev/{kmem,mem,port}
		[messagebus]='messagebus:31'	# /usr/lib/dbus-1.0/dbus-daemon-launch-helper
		[ntpsec]='ntpsec:23'			# Ubuntu 24+
		[ulog]='ulog:32'			# ulogd2 kernel logger
		[ssl-cert]='ssl-cert:33'	# postgres.postinst, ssl-cert.postinst
		[man]='man:37'				# wird seit bionic gebraucht
		[lxd]='lxd:38'				# wird seit bionic/lxd gebraucht
		[sasl]='sasl:41'	# sasl2-bin: avoid sabotages of further pkg installs
		[shadow]='shadow:42'
		[postfix]='postfix:45'	# chown -R postfix:postfix /var/{lib,spool}/postfix
		[postdrop]='postdrop:46'
		[syslog]='syslog:47'	# chown syslog:staff /var/{log,log/*}
		[crontab]='crontab:48'	# chgrp -R crontab /var/spool/cron/crontabs
		[fuse]='fuse:49'
		[disk]='disk:51'		# login.defs
		[floppy]='floppy:53'	# login.defs
		[cdrom]='cdrom:54'		# login.defs
		[audio]='audio:55'		# login.defs
		[video]='video:56'		# login.defs
		[uuid]='uuid:57'		# libuuid
		[utmp]='utmp:58'		# chown root:utmp /var/log/{lastlog,btmp*,wtmp*}
		# systemd-* ist alles /run/* - unkritisch
		[systemd-journal]='systemd-journal:59'
		[systemd-oom]='systemd-oom:60'
		[systemd-resolve]='systemd-resolve:62'
		[systemd-network]='systemd-network:63'
		[systemd-bus-proxy]='systemd-bus-proxy:64'
		[sssd]='sssd:69'			# stupid deb pkgs fail on bionic w/o
		#[ntp]="ntp:32"
		#[scanner]="scanner:32"	# libsane
		#[www-data]="www-data:33"	# apache2
		#[backup]="backup:34"
		#[man]="man:37"
	)
	# local policy groups. Sind ebenfalls fix, werden aber nur eingetragen, wenn
	# es dafür einen Eintrag gibt
	typeset -A LOCAL_GROUPS=(
		[zope]='zope:102'		# EduComponents - WebServer
		[zeo]='zeo:103'			#     --"--     - zope DB server
		[zopeadm]='zopeadm:104'	# Zope Admin
		[ecsd]='ecsd:105'		# EduComponents ???
		[ecsbd]='ecsbd:106'		#     --"--     - Backend
		[ecsadm]='ecsadm:107'	#     --"--     - Admin
		[opengrok]='opengrok:108'	# OpenGrok Admin
		[ldapd]='ldapd:109'			# Ldap-Server (OpenDJ)
		[majordomo]='majordomo:110'	# Mailing list server
		[oscpd]='oscpd:115'			# OSCP daemon
		[redis]='redis:117'			# chown redis:redis /var/lib/redis
		[ruby]='ruby:118'			# ruby install admin
		[ontohub]='ontohub:119'		# Ontohub users
		[git]='git:120'				# git, ehemals utwc:sunray Windows Connector
		[esearch]='esearch:121'		# elastic search daemon
		[jenkins]='jenkins:122'		# jenkins daemon
		[mongodb]='mongodb:127'		# mongoDB daemon
		[slurm]='slurm:128'			# slurm services
		[info]='info:999'			# test accounts
	)
	# Die auf jeden Fall rausschmeißen - dieser unbenutzte Schrott scheißt uns
	# nur den 0-100er Block zu ...
	typeset -A DROP_GROUPS=(
		# bullshit from /var/lib/dpkg/info/base-passwd.preinst
		[news]='useless default'
		[man]='useless default'
		[proxy]='useless default'
		[dialout]='useless default'
		[fax]='useless default'
		[voice]='useless default'
		[tape]='useless default'
		[sudo]='useless default'
		[dip]='useless default'
		[backup]='useless default'
		[operator]='useless default'
		[list]='useless default'
		[irc]='useless default'
		[src]='useless default'
		[gnats]='useless default'
		#[sasl]='useless default'	# non-sense but to make its postinst happy
		[plugdev]='useless default'
		[users]='useless default'
		# misc unused stuff - wird ggf. dann mit GID >= 400 generiert, blockt
		# aber nicht < 100
		[input]='pkg udev blindness'
		[netdev]='pkg ifupdown relicts'
		[systemd-timesync]='useless - use ntp in GZ'
		[mlocate]='pkg locate - useless'
		[sgx]='no SW using SGX'
		[kvm]='no KVM here'
	)

	typeset -A POST
	typeset OLDIFS="${IFS}" GN X GGID GUSER F G DROP
	IFS=':'

	for X in ${!GROUPS[@]} ; do
		F=( ${GROUPS[$X]} )
		GID2GROUP[${F[1]}]="${F[0]} ${F[2]}"
	done

	# adjust gid of gidnames existing
	while read GN X GGID GUSER ; do
		OLDGID2NAME[${GGID}]="${GN}"
		# these groups are not needed - just record remapping
		if [[ ${GN} =~ ^admin(1|istrator)?$ ]]; then
			OLDGID2NEW[${GGID}]=10		# to staff
			continue
		elif [[ ${GN} =~ ^(ulog|proxy)$ ]]; then
			OLDGID2NEW[${GGID}]=12		# to daemon
			continue
		elif [[ ${GN} =~ ^(www-data)$ ]]; then
			OLDGID2NEW[${GGID}]=80		# to webservd
			continue
		elif [[ ${GN} =~ ^(ntp)$ ]]; then
			OLDGID2NEW[${GGID}]=3		# to sys
			continue
		elif [[ -n ${DROP_GROUPS[${GN}]} ]]; then
			GDROPPED+="${GN}:${GGID}  (${DROP_GROUPS[${GN}]})\n"
			continue
		fi

		X=${GROUPS[${GN}]}
		[[ -z $X ]] && X=${LOCAL_GROUPS[${GN}]}
		if [[ -n $X ]]; then
			# same grp name
			G=( ${X} )
			if [[ ${GGID} != ${G[1]} ]]; then
				# different GID
				OLDGID2NEW[${GGID}]=${G[1]}
				GGID=${G[1]}
			fi
			GID2GROUP[${GGID}]="${GN} ${GUSER}"	# overwrite possible users
			continue	# is already  in GID2GROUP
		fi
		# a unknown/local group
		if [[ -n ${GID2GROUP[${GGID}]} ]] || (( GGID > 100 && GGID < 60001 ))
		then
			# with an already reserved GID or a GID out of vendor range
			# -> postpone
			POST[$GGID]="${GN} ${GUSER}"
		else
			# add as is: causes no collision
			GID2GROUP[${GGID}]="${GN} ${GUSER}"
		fi
	done < "${SOPTS[group]}"
	IFS="${OLDIFS}"

	# post: handle collisions
	integer I 								# find 1st free gid above 50
	for (( I=50; I < 100; I++ )); do
		[[ -z ${GID2GROUP[$I]} ]] && break
	done

	# to get a stable result we sort
	F="${!POST[@]}"
	[[ -n ${SORT} ]] && F=${ print "${F// /$'\n'}" | ${SORT} -k1,1n ; }

	for X in ${F} ; do
		GID2GROUP[$I]="${POST[$X]}"
		OLDGID2NEW[$X]=$I
		for (( I++ ; I < 100; I++ )); do
			[[ -z ${GID2GROUP[$I]} ]] && break	# find next free gid
		done
		(( I == 100 )) && print -u2 "WARNING: no GIDs left - skipping $X"
	done

	# finally print the "merged" set
	GGID="${!GID2GROUP[@]}"
	[[ -n ${SORT} ]] && GGID=${ print "${GGID// /$'\n'}" |${SORT} -k1,1n ; }
	F=
	for X in ${GGID} ; do
		G=( ${GID2GROUP[$X]} )
		F+="${G[0]}:x:${X}:${G[1]}\n"
	done
	if [[ ${SOPTS[ogroup]} == '-' || -z ${SOPTS[ogroup]} ]]; then
		print "\n# ${SOPTS[group]}\n$F"
	else
		[[ -e ${SOPTS[ogroup]}.orig ]] || \
			cp -p "${SOPTS[ogroup]}" "${SOPTS[ogroup]}.orig"
		print -n "$F" >"${SOPTS[ogroup]}"
	fi

	if (( IOPTS[verbose] )); then
		if [[ -n ${GDROPPED} ]]; then
			print -u2 '\nGIDs dropped:\n'
			print "${GDROPPED}"
		fi
		print -u2 '\nGID remap:\n'
		F=
		for X in ${!OLDGID2NEW[@]} ; do
			I=${OLDGID2NEW[$X]}
			F+="$X -> $I  (${OLDGID2NAME[$X]// *} -> ${GID2GROUP[$I]// *})\n"
		done
		[[ -n ${SORT} ]] && X=${ print -n "$F" | ${SORT} -k1,1n ; } || X="$F"
		print -u2 "$X"
	fi
}

function fixAccounts {
	# here we need to do similar things as in groups
	typeset -n OLDGID2NEW=$1 UID2NAME=$2 NEWGID2GROUP=$3

	typeset -A ACCOUNTS=(
[root]='root:x:0:0:Super-User:/root:/usr/bin/bash'
[daemon]='daemon:x:1:1::/:'
[bin]='bin:x:2:2::/usr/bin:'
[sys]='sys:x:3:3::/:'
[adm]='adm:x:4:4:Admin:/var/adm:'
[uucp]='uucp:x:5:5:uucp Admin:/usr/lib/uucp:'
[nuucp]='nuucp:x:9:9:uucp Admin:/var/spool/uucppublic:/usr/lib/uucp/uucico'
[dladm]='dladm:x:15:65:Datalink Admin:/:'
[netadm]='netadm:x:16:65:Network Admin:/:'
[netcfg]='netcfg:x:17:65:Network Configuration Admin:/:'
[dhcpserv]='dhcpserv:x:18:65:DHCP Configuration Admin:/:'
[sshd]='sshd:x:22:22:sshd privsep:/var/run/sshd:/usr/sbin/nologin'
[smmsp]='smmsp:x:25:25:SendMail Message Submission Program:/:'
[dovecot]='dovecot:x:43:43:Dovecot Internal:/:'
[dovenull]='dovenull:x:44:44:Dovecot Auth Helpers:/:'
[rabbitmq]='rabbitmq:x:48:12:RabbitMQ:/var/lib/rabbitmq:'
[gdm]='gdm:x:50:50:GDM Reserved UID:/var/lib/gdm:'
[zfssnap]='zfssnap:x:51:12:ZFS Automatic Snapshots Reserved UID:/:/usr/bin/pfsh'
[upnp]='upnp:x:52:52:UPnP Server Reserved UID:/var/coherence:/bin/ksh'
[secadm]='secadm:x:53:14:Security Administrator:/export/home/secadm:/usr/bin/pfbash'
[useradm]='useradm:x:54:14:User Administrator:/export/home/useradm:/usr/bin/pfbash'
[fsadm]='fsadm:x:55:14:File System Administrator:/export/home/fsadm:/usr/bin/pfbash'
[svcadm]='svcadm:x:56:14:Service Administrator:/export/home/svcadm:/usr/bin/pfbash'
[auditadm]='auditadm:x:57:14:Audit Administrator:/export/home/auditadm:/usr/bin/pfbash'
[pkgadm]='pkgadm:x:58:14:Software Package Administrator:/export/home/pkgadm:/usr/bin/pfbash'
[sysop]='sysop:x:59:14:System Operator:/export/home/sysop:/usr/bin/pfbash'
#[xvm]='xvm:x:60:60:xVM User:/:'
[aiuser]='aiuser:x:61:61:AI User:/:'
[ikeuser]='ikeuser:x:67:12:IKE Admin:/:'
[mysql]='mysql:x:70:70:MySQL Reserved UID:/:'
[lp]='lp:x:71:8:Line Printer Admin:/:'
[openldap]='openldap:x:75:75:OpenLDAP User:/:'
[webservd]='webservd:x:80:80:WebServer Reserved UID:/:'
#[cinder]='cinder:x:81:81:OpenStack Cinder:/var/lib/cinder:'
#[glance]='glance:x:82:82:Openstack Glance:/var/lib/glance:'
#[keystone]='keystone:x:83:83:OpenStack Keystone:/var/lib/keystone:'
#[neutron]='neutron:x:84:84:OpenStack Neutron:/var/lib/neutron:'
#[nova]='nova:x:85:85:OpenStack Nova:/var/lib/nova:'
#[swift]='swift:x:86:86:Openstack Swift:/var/lib/swift:'
#[evsuser]='evsuser:x:89:89:Elastic Virtual Switch:/var/user/evsuser:'
[clamav]='clamav:x:89:12:Clam AntiVirus:/var/clamav:'
[postgres]='postgres:x:90:90:PostgreSQL Reserved UID:/:/usr/bin/pfksh'
[svctag]='svctag:x:95:12:Service Tag UID:/:'
[unknown]='unknown:x:96:96:Unknown Remote UID:/:'
[pkg5srv]='pkg5srv:x:97:97:pkg(5) server UID:/:'
# standard Accounts
[nobody]='nobody:x:60001:60001:NFS Anonymous Access User:/:'
[noaccess]='noaccess:x:60002:60002:No Access User:/:'
#	eigentlich 'nobody4'. Damit das schema username == groupname gewahrt bleibt
#	und die Gruppe 'nogroup' noch benutzt wird:
[nogroup]='nogroup:x:65534:65534:SunOS 4.x NFS Anonymous Access User:/:'

	# Linux
# proxy:*:13:13
[messagebus]='messagebus:x:31:31::/var/run/dbus:/bin/false'			# dbus
[ulog]='ulog:x:32:32::/var/log/ulog:/bin/false'
[ntpsec]='ntpsec:x:23:23::/nonexistent:/bin/sh'	# seit Ubuntu 24.04
# www-data:*:33:33
#[backup]='backup:x:34:34:backup:/var/backups:/usr/sbin/nologin'
[ntp]='ntp:x:35:3::/home/ntp:/bin/false'
[statd]='statd:x:36:65534::/var/lib/nfs:/bin/false'
[man]='man:x:37:37:man:/var/cache/man:/usr/sbin/nologin'
[lxd]='lxd:x:38:38:Linux Container Daemon:/var/lib/lxd:/bin/false'
[postfix]='postfix:x:45:45::/var/spool/postfix:/bin/false'
[syslog]='syslog:x:47:47::/home/syslog:/bin/false'
['systemd-oom']='systemd-oom:x:60:60:systemd Userspace OOM Killer,,,:/:/usr/sbin/nologin'
['systemd-resolve']='systemd-resolve:x:62:62:systemd Resolver,,,:/run/systemd/resolve:/bin/false'
['systemd-network']='systemd-network:x:63:63:systemd Network Management,,,:/run/systemd/netif:/bin/false'
['systemd-bus-proxy']='systemd-bus-proxy:x:64:64:systemd Bus Proxy,,,:/run/systemd:/bin/false'
['sssd']='sssd:x:69:69:System Security Services Daemon,,,:/var/lib/sss:/bin/false'
)

	typeset -A LOCAL_ACCOUNTS=(
# lokale administrative accounts (100..499)
[admin]='admin:x:101:10:Administrator:/local/home/admin:/usr/bin/tcsh'
[zope]='zope:x:102:102:Zope App Server:/:/bin/true'
[zeo]='zeo:x:103:103:Zeo DB Server:/:/bin/true'
[zopeadm]='zopeadm:x:104:104:Zeo DB Server:/local/home/zopeadm:/usr/bin/tcsh'
[ecsd]='ecsd:x:105:105:EC backend Server:/tmp:/bin/true'
[ecsbd]='ecsbd:x:106:106:EC Backend Server:/:/bin/true'
[ecsadm]='ecsadm:x:107:107:EC Backend Server Admin:/local/home/ecsadm:/usr/bin/tcsh'
[opengrok]='opengrok:x:108:108:OpenGrok Server:/:'
[ldapd]='ldapd:x:109:109:LDAP Server:/var/share/ldap:/usr/bin/ksh93'
[majordomo]='majordomo:x:110:110:Majordomo:/:'
[winadm]='winadm:x:111:1502:Windows Admin:/home/winadm:/usr/bin/tcsh'
[svn]='svn:x:112:12:Subversion Repository:/data/svn:/usr/bin/tcsh'
[sfw]='sfw:x:113:12:Software Verwalter:/home/sfw:/usr/bin/tcsh'
[webadm]='webadm:x:114:10:Webmaster:/local/home/webadm:/usr/bin/tcsh'
[ocspd]='ocspd:x:115:115:Online Certificate Status Protocol daemon:/:'
[spam]='spam:x:116:1502:Spam catcher:/local/home/spam:'
[redis]='redis:x:117:117:Redis DB daemon:/local/home/redis:/bin/false'
[ruby]='ruby:x:118:118Ruby Admin:/local/home/ruby:/bin/bash'
[ontohub]='ontohub:x:119:80:Ontohub Admin:/local/home/ontohub:/bin/bash'
[git]='git:x:120:80:Git Repository:/local/home/git:/bin/bash'
[esearch]='esearch:x:121:121:Elastic Search:/usr/share/elasticsearch:/bin/false'       # elastic search (ontohub)
[jenkins]='jenkins:x:122:80:Jenkins server:/data/jenkins:/bin/bash'
[arpwatchd]='arpwatchd:x:123:12:Arpwatch:/var/arpwatch:'
# lokale Instituts-Accounts (500..999)
[info]='info:x:999:1510:Info Templates:/home/info:/bin/tcsh'
[demo]='demo:x:124:999:AiLab Demo,,,:/local/home/demo:/bin/bash'	# demo user
[freeswitch]='freeswitch:x:125:12:Freeswitch daemon:/var/freeswitch:/bin/bash'
[dbadm]='dbadm:x:126:10:DB Administrator:/home/dbadm:/bin/bash'
[mongodb]='mongodb:x:127:127:MongoDB daemon:/var/lib/mongodb:/bin/false'
[slurm]='slurm:x:128:128::/nonexistent:/bin/sh'
[demoadm]='demoadm:x:129:1521:AiLab Demo Admin,,,:/local/home/demoadm:/bin/bash'
)
	unset POST
	typeset -A POST CHANGED SEEN
	typeset OLDIFS="${IFS}" X F G ANAME AUID AGID AGCOS AHOME ASHELL
	IFS=':'

	for X in ${!ACCOUNTS[@]} ; do
		F=( ${ACCOUNTS[$X]} )
		if [[ -z ${F[6]} && $X != 'mysql' ]]; then
			F[6]='/usr/sbin/nologin'
		elif [[ ${F[6]:0:11} == '/usr/bin/pf' ]]; then
			F[6]="/bin/${F[6]:11}"
		elif [[ ${F[6]:0:8} == '/usr/bin/' ]]; then
			F[6]="${F[6]:4}"	
		fi
		#		UID			 NAME    GID     HOME   SHELL	GCOS
		UID2NAME[${F[2]}]="${F[0]}:x:${F[2]}:${F[3]}:${F[4]}:${F[5]}:${F[6]}"
	done
	
	while read ANAME X AUID AGID AGCOS AHOME ASHELL ; do
		[[ ${ANAME} == 'admin1' || ${ANAME} == 'administrator' ]] && \
			ANAME='admin'
		X="${ACCOUNTS[${ANAME}]}"
		[[ -z $X ]] && X="${LOCAL_ACCOUNTS[${ANAME}]}"
		[[ -n $X ]] && G=( $X )

		# admin may have the right GID  already (e.g. by zone install scripts)
		[[ ${ANAME} == 'admin' && ${AGID} == ${G[3]} ]] && NEWGID= || \
			NEWGID=${OLDGID2NEW[${AGID}]}
		[[ -n ${NEWGID} ]] && AGID=${NEWGID}  && CHANGED[${ANAME}]=1

		if [[ -n $X ]]; then
			# predefined name
			if [[ ${AUID} != ${G[2]} ]]; then
				AUID=${G[2]}
				CHANGED[${ANAME}]=1
			fi
			SEEN[${ANAME}]=1		# for shadow later
			UID2NAME[${AUID}]="${ANAME}:x:${AUID}:${AGID}:${AGCOS}:${AHOME}:${ASHELL}"
			continue
		fi
		# unknown/local - DROP account
		if [[ ${ANAME} =~ ^(sync|proxy|www-data|backup|list|irc|gnats|systemd-timesync)$ ]]; then
			continue	# bullshit
		elif [[ -n ${UID2NAME[${AUID}]} ]] || (( AUID > 100 && AUID < 60000 ))
		then
			# there is already an account with the same UID -> postpone
			POST[${AUID}]="${ANAME}:x:@@:${AGID}:${AGCOS}:${AHOME}:${ASHELL}"
		else
			UID2NAME[${AUID}]="${ANAME}:x:${AUID}:${AGID}:${AGCOS}:${AHOME}:${ASHELL}"
		fi
	done < "${SOPTS[passwd]}"
	IFS="${OLDIFS}"

	# postdo collisioning accounts
	integer I
	for (( I=50; I < 100; I++ )); do
		[[ -z ${UID2NAME[$I]} ]] && break
	done
	# to get a stable result we sort
	F="${!POST[@]}"
	[[ -n ${SORT} ]] && F=${ print -n "${F// /$'\n'}" | ${SORT} -k1,1n ; }
	for X in ${F} ; do
		F="${POST[$X]}"
		UID2NAME[$I]="${F//@@/$I}"
		CHANGED[${F%%:*}]=1
		for (( I++ ; I < 100; I++ )); do
			[[ -z ${UID2NAME[$I]} ]] && break
		done
	done

	# finally print the "merged" set
	F=
	for X in ${!UID2NAME[@]} ; do
		[[ -z ${UID2NAME[$X]} ]] && print "# '$X' is empty" && continue
		F+="${UID2NAME[$X]}\n"
	done
	
	[[ -n ${SORT} ]] && F="${ print -n "$F" | ${SORT} -k3,4n -t: ; }"
	if [[ ${SOPTS[opasswd]} == '-' || -z ${SOPTS[opasswd]} ]]; then
		print "\n# ${SOPTS[passwd]}\n$F"
	else
		[[ -e ${SOPTS[opasswd]}.orig ]] || \
			cp -p "${SOPTS[opasswd]}" "${SOPTS[opasswd]}.orig"
		print "$F" >"${SOPTS[opasswd]}"
	fi

	F=''
	for X in ${!ACCOUNTS[@]} ; do
		[[ -n ${SEEN[$X]} ]] && continue
		G="${ACCOUNTS[$X]}"
		F+="${G%%:*}:*:16314:0:99999:7:::\n"
	done
	if [[ -n $F ]]; then
		[[ -n ${SORT} ]] && F="${ print -n "$F" | ${SORT} -k1,1 -t: ; }"
		if [[ ${SOPTS[shadow]} == '-' || -z ${SOPTS[shadow]} ]]; then
			print "\n# /etc/shadow  additions\n$F"
		else
			[[ -e ${SOPTS[shadow]}.orig ]] || \
				cp -p "${SOPTS[shadow]}" "${SOPTS[shadow]}.orig"
			print "$F" >>"${SOPTS[shadow]}"
		fi
	fi

	G="${SOPTS[rpath]}"
	for X in ${OLDGID2NEW[@]} ; do
		F="${NEWGID2GROUP[$X]}"
		[[ -n $F ]] && CHANGED["g_${F%% *}"]=1
	done
	# TBD: mapping to num
	F=''
	for X in ${!CHANGED[@]} ; do
		# we use UID/GID here since the "mangled" zone may not be running yet
		# and dueto fixGroups() and the stuff above we can use them safely
		case $X in
			g_staff)
				F+='chown root:staff /var/log/sulog\n'
				F+='find /usr/local -xdev -type d -exec chown root:sys {} +\n'
				;;
			g_daemon) F+='chown -R daemon:daemon /var/spool/cron/at*\n'
					F+='chgrp daemon /etc/at.deny\n'
					F+='chown daemon:daemon /usr/bin/at\n'
					F+='chmod 6755 /usr/bin/at\n'
					;;
			postfix)
				F+='chown -R postfix:postfix /var/lib/postfix /var/spool/postfix\n'
				F+='chown -R postfix:postdrop /var/spool/postfix/maildrop\n'
				F+='chmod 1730 /var/spool/postfix/maildrop\n'
				;;
			g_syslog) F+='chown -R root:syslog /var/log /var/log/* /var/spool/rsyslog\n' ;;
			g_crontab)
				F+='chgrp -R crontab /var/spool/cron/crontabs /usr/bin/crontab\n'
				F+='chmod 2755 /usr/bin/crontab\n'
				;;
			g_utmp)
				F+='chown root:utmp /var/log/lastlog /var/log/btmp* /var/log/wtmp* /usr/bin/screen /usr/lib/utempter/utempter\n'
				F+='chmod 2755 /usr/bin/screen /usr/lib/utempter/utempter\n'
				;;
			g_mail)	# useless stuff ...
				F+='chown root:mail /usr/bin/mail-{lock,touchlock,unlock} /usr/bin/{dotlockfile,procmail,lockfile} /usr/lib/mail /usr/lib/emacs/24.3/*/movemail /etc/mail /etc/mail/{README,trusted-users,local-host-names,sendmail.mc}\n'
				F+='chgrp -R mail /var/mail\n'
				F+='chmod 2755 /usr/bin/mail-{lock,touchlock,unlock} /usr/bin/dotlockfile /usr/bin/{dotlockfile,lockfile} /var/mail /usr/lib/emacs/24.3/*/movemail\n'
				F+='chmod 6755 /usr/bin/procmail\n'
				;;
			g_tty) F+="[ -e /etc/default/devpts ] && sed -e '/^TTYGRP=/ s,=.*,=7,' -i /etc/default/devpts\n"
				F+='chown root:tty /usr/bin/{bsd-write,wall}\n'
				F+='chmod 4755 /usr/bin/{bsd-write,wall}\n'
				;;
			g_sys) F+='chgrp -R sys /etc/ssl/private\n' ;;
			redis) F+='chown -R redis:redis /var/lib/redis\n' ;;
			admin) F+='chown -R admin:staff /local/home/admin\n' ;;
			g_messagebus)
				F+='chgrp messagebus /usr/lib/dbus-1.0/dbus-daemon-launch-helper\n'
				F+='chmod 4754 /usr/lib/dbus-1.0/dbus-daemon-launch-helper\n'
				;;
			g_${SSH})
				F+="chgrp ${SSH} /usr/bin/ssh-agent\n"
				F+='chmod 2755 /usr/bin/ssh-agent\n'
				;;
			g_ntp)
				F+='chown -R ntp:sys /var/lib/ntp /var/log/ntpstats\n'
				;;
			g_webservd)
				F+='chown -R webservd:webservd /var/cache/apache2/mod_cache_disk\n'
				;;
			g_shadow)
				F+='chgrp shadow /etc/{gshadow,shadow} /sbin/unix_chkpwd /usr/bin/{chage,expiry} /var/backups/{gshadow,shadow}.bak\n'
				F+='chmod 2755 /sbin/unix_chkpwd /usr/bin/{chage,expiry}\n'
				;;
			g_fuse)
				F+='chgrp fuse /etc/fuse.conf\n'
				;;
			g_postdrop)
				F+='chown root:postdrop /usr/sbin/post{drop,queue}\n'
				F+='chmod 02755 /usr/sbin/post{drop,queue}\n'
				;;
			statd)
				F+='chown -R statd:nogroup /var/lib/nfs\n'
				;;
			g_uuid)
				F+='chown uuid:uuid /usr/sbin/uuidd\n'
				F+='chmod 6755 /usr/sbin/uuidd\n'
				;;
			g_ssl-cert)
				F+='chgrp -R ssl-cert /etc/ssl/private\n'
				;;
			g_systemd-network)
				# weil wir noch nicht gebootet haben, müssen wir das auch fixen
				# ansonsten gibt's nameservice probleme
				F+="rm -rf /run/systemd/netif\n"
				F+="systemctl restart systemd-networkd\n"
				;;
			rabbitmq|g_rabbitmq)
				F+='chown -R rabbitmq:rabbitmq /var/{lib,log}/rabbitmq\n'
				F+='systemctl restart rabbitmq\n'
				;;
			mongodb|g_mongodb)
				F+='chown -R mongodb:mongodb /var/{lib,log}/mongodb\n'
				F+='systemctl restart mongodb\n'
				;;
			mysql|g_mysql)
				# @since noble
				F+='chown -R mysql:staff /var/{lib,log}/mysql\n'
				F+='chown mysql:staff /var/lib/mysql-{files,keyring}\n'
				F+='chmod g+rx /var/lib/mysql\n'
				# no restart since we use our own instance anyway
				;;
			postgres)
				F+='chown -R postgres:staff /var/{lib,log}/postgresql /etc/postgresql/*\n'
					;;
			g_postgres)
				# @since noble
				# private key file "/etc/ssl/private/ssl-cert-snakeoil.key"
				# must be owned by the database user or root and readable
				F+='chown -R root:postgres /etc/ssl/private\n'
				F+='chmod g+rx /var/lib/postgresql\n'
				# no restart since we use our own instance anyway
				;;
			g_sasl)
				# basically dpkg-statoverride is debian bullshit. We remove it
				# so that post.inst may re-register it with the right gid
				dpkg-statoverride --remove /var/run/saslauthd	# total stupid
				dpkg-statoverride --remove /etc/sasldb2		# not used anyway
				;;
			clamav)
				F+='[ -d /etc/clamav ] && chown -R clamav:staff /etc/clamav/freshclam.conf'
				F+=' clamav:clamav /var/lib/clamav\n'
				;;
			dovecot)
				F+='chgrp dovecot /etc/dovecot/*.ext\n'
				;;
		esac
	done
	[[ -z $F ]] && return
	if [[ -z ${SOPTS[script]} ]]; then
		X=${ mktemp -qt fix-uidgid.XXXXXX ; }
		if [[ -z $X ]]; then
			print -u2 'Unable to create tempfile - writing to stdout'
			SOPTS[script]='-'
		else
			SOPTS[script]="$X"
		fi
	fi	
	if [[ ${SOPTS[script]} == '-' ]]; then
		print -n "\n# fix path permissions wrt. changed UID/GIDs\n$F"
	else
		print -u2 "\nWriting fix path permissions script to '${SOPTS[script]}'"
		print -n "# fix path permissions wrt. changed UID/GIDs\n$F" \
			>"${SOPTS[script]}"
	fi
}

function showUsage {
	getopts -a ${PROG} "${ print ${USAGE}; }" OPT --man
}

unset REL; integer REL
# Debian-Spinner haben Gruppe das wieder mal umbenannt in 24.04
typeset SSH='_ssh'

function fixOpts {
	typeset X Y
	integer RES=0
	if (( IOPTS[dry] )); then
		SOPTS[opasswd]='-'
		SOPTS[shadow]='-'
		SOPTS[ogroup]='-'
		SOPTS[script]='-'
	fi
	X="${SOPTS[rpath]}"
	Y="$X/etc/lsb-release"
	if [[ ! -f $Y ]]; then
		print -u2 "$Y not found - not Ubuntu?"
		if [[ /etc/lsb-release ]]; then
			print -u2 "Using /etc/lsb-release instead."
			Y=/etc/lsb-release
		fi
	fi
	[[ -f $Y ]] && . "$Y"
	[[ -z ${DISTRIB_RELEASE} ]] && \
		DISTRIB_RELEASE=${ lsb_release -sr 2>/dev/null; }
	# if we can't get the distro version, assume 20.04
	[[ -n ${DISTRIB_RELEASE} ]] && REL=${DISTRIB_RELEASE//.} || REL=2004

	# Eigentlich sshd, aber Debian-Bratzen nennen's ssh und haben's in 24.x
	# schon wieder wieder umbenannt ... Vollpfosten ...
	(( REL < 2404 )) && SSH='ssh'
	print "Using REL=${REL} => SSH=${SSH}"

	Y="${SOPTS[passwd]}"
	Y="$X/$Y"
	if [[ ! -f "$Y" ]]; then
		print -u2 "$Y is not a file or unreadable."
		(( RES++ ))
	else
		SOPTS[passwd]="$Y"
	fi
	Y="${SOPTS[group]}"
	Y="$X/$Y"
	if [[ ! -f "$Y" ]]; then
		print -u2 "$Y is not a file or unreadable."
		(( RES++ ))
	else
		SOPTS[group]="$Y"
	fi
	Y="${SOPTS[opasswd]}"
	if [[ -z $Y ]]; then
		SOPTS[opasswd]="${SOPTS[passwd]}"
	elif [[ $Y != '-' ]]; then
		SOPTS[opasswd]="$X/$Y"
	fi
	Y="${SOPTS[ogroup]}"
	if [[ -z $Y ]]; then
		SOPTS[ogroup]="${SOPTS[group]}"
	elif [[ $Y != '-' ]]; then
		SOPTS[ogroup]="$X/$Y"
	fi
	Y="${SOPTS[shadow]}"
	[[ -n $Y && $Y != '-' ]] && SOPTS[shadow]="$X/$Y"
	Y="${SOPTS[script]}"
	[[ -n $Y && $Y != '-' ]] && SOPTS[script]="$X/$Y"
	Y=${ whence sort ; }
	if [[ -z $Y ]]; then
		if [[ -x /bin/sort ]]; then
			Y='/bin/sort'
		elif [[ -x /usr/bin/sort ]]; then
			Y='/usr/bin/sort'
		elif [[ -n $X ]]; then
			if [[ -x $X/bin/sort ]]; then
				Y="$X/bin/sort"
			elif [[ -x $X/usr/bin/sort ]]; then
				Y="$X/usr/bin/sort"
			fi	
		fi
	fi
	SORT="$Y"
	if [[ -z ${SORT} ]]; then
		print -u2 "WARNING: 'sort' not found! Emitted lists as well as UIDs/GIDs for lokal/unknown entries are likely to be unstable!"
	fi
	return ${RES}
}

USAGE='[-?1.1 ]
[-copyright?Copyright (c) 2014 Jens Elkner. All rights reserved.]
[-license?CDDL 1.0]
[+NAME?'"${PROG}"' - adjust local account and group database.]
[+DESCRIPTION?Adjusts the local account database \b/etc/passwd\b incl. \b/etc/shadow\b as well as the local group database \b/etc/group\b wrt. to our common schema for UIDs and GIDs. If this requires permission changes for known directories or files, a script gets generated, which can be used to fix them.]
[h:help?Print this help and exit.]
[F:functions?Print out a list of all defined functions. Just invokes the \btypeset +f\b builtin.]
[T:trace]:[fname_list?A comma or whitspace separated list of function names, which should be traced during execution.]
[R:rootpath]:[path?Prefix all mentioned files and directories with the given \apath\a. Default: ""]
[p:passwd]:[file?The passwd file to read. Default: \b/etc/passwd\b]
[P:opasswd]:[file?The new passwd file to write. Default: The value of option \b-p\b \a...\a]
[g:group]:[file?The group file to read. Default: \b/etc/group\b]
[G:ogroup]:[file?The new group file to write. Default: The value of option \b-g\b \a...\a]
[s:script]:[file?Where to write the commands, which can be used to fix permissions possibly needed to reflect changed UID/GIDs. Default: A temporary file created on demand.]
[S:shadow]:[file?The file where to append changes wrt. to new accounts added to the local account database.]
[v:verbose?Print out some additional information to stderr if appropriate.]
[n:dry?Dry run, i.e. does not change any files but prints out all new database files and scripts to stdout.]
'
unset OLDGID2NEW GID2GROUP UID2NAME IOPTS SOPTS
typeset -A OLDGID2NEW GID2GROUP OLDGID2NAME UID2NAME SOPTS
typeset -A -i IOPTS
typeset SORT=''

SOPTS=(
	[rpath]='' [script]=''
	[passwd]='etc/passwd' [group]='etc/group' [shadow]='etc/shadow'
)

X="${ print ${USAGE} ; }"
while getopts "${X}" OPT ; do
	case "${OPT}" in
		h) showUsage ; exit 0 ;;
		F) typeset +f ; exit 0 ;;
		T) [[ ${OPTARG} == 'ALL' ]] &&  typeset -ft ${ typeset +f ; } || \
				typeset -ft ${OPTARG//,/ } ;;
		R) [[ -n ${OPTARG} ]] && SOPTS[rpath]="${OPTARG%%/}" ;;
		p) [[ -n ${OPTARG} ]] && SOPTS[passwd]="${OPTARG##/}" ;;
		P) [[ -n ${OPTARG} ]] && SOPTS[opasswd]="${OPTARG##/}" ;;
		g) [[ -n ${OPTARG} ]] && SOPTS[group]="${OPTARG##/}" ;;
		G) [[ -n ${OPTARG} ]] && SOPTS[ogroup]="${OPTARG##/}" ;;
		s) [[ -n ${OPTARG} ]] && SOPTS[script]="${OPTARG##/}" ;;
		S) [[ -n ${OPTARG} ]] && SOPTS[shadow]="${OPTARG##/}" ;;
		v) IOPTS[verbose]=1 ;;
		n) IOPTS[dry]=1 ;;
	esac
done
X=$((OPTIND-1))
shift $X

fixOpts || exit 1
fixGroups OLDGID2NEW GID2GROUP OLDGID2NAME
fixAccounts OLDGID2NEW UID2NAME GID2GROUP
