#!/usr/bin/perl -w
# LdapJ.pm -- Generic functions for UCLA-Mathnet LDAP administration extensions.
# Copyright (c) 2010 by The Regents of the University of California
# Author: Jim Carter <jimc@math.ucla.edu>, 2010-05-27, perl-5.10.0

# Command line options are shown with the Opt package, the first one below.

# Test command lines: 
# ./ldaputil -x -h localhost -d 0174 -c ./networks.ldif |& tee $j/errs | less
# ./ldaputil -y $j/root.secret -h localhost -d 0174 -i ./networks.ldif | & tee $j/errs | less
# ./ldaputil -Y GSSAPI -h localhost -d 0174 -i ./networks.ldif -n | & tee $j/errs | less
# slapcat -l $j/all.ldif 

# When you try to STARTTLS and the client library reports "unsupported extended
# operation", this means that the server has not been configured with a host
# certificate and secret key, or for some other reason doesn't intend to do
# TLS.

# Problems:
#   X	OU's of nisMapName=netgroup.byuser and nisMapName=netgroup.byhost
#	can't be done by the generic subroutines.  Is this even relevant?
#	I don't think we're using the list of NIS maps.
#   %?	Have we got /etc/hosts working right?  What does it really expect
#	for multiple IP per host?  [Supposedly correct now]  [Presently there
#	are no hosts with multiple IPs on Mathnet, but IPv6 changes that.]
#   %	Need to deal with the whole issue of udata.  Need to add it to schema.
#   %	Also need to add hostgroup table.
#   %	Migration tools include OUs for mounts automount fstab.  We need
#	auto.home and auto.master.  Are we going to have automount tables?
#	Yes, we need to add these.
#   X	-O is used here for Organizational Unit, but also for SASL security
#	properties.  And -U is for SASL authentication ID.  Straighten this
#	out.  [Punt, not using Security Properties.]
#   .	Comparing password in shadow: LDAP has (or had) {SSHA}QWERTY... while
#	flat file has $2a$10$2345678...  Avoid comparing apples and oranges.
#	It wanted to modify every entry, prob. identity transformation.
#	But root and testacct had $2a$10$ in both flat and LDAP.
#   .	GSSAPI has become broken.  Message: "Failed to bind to LDAP server 
#	as...  Unknown parameter to auth callback."  Specifying -U doesn't
#	help (default is $ENV{USER}).  Omitting auth callback makes it revert
#	to "unknown parameter to user callback".  Omitting both produces
#	"invalid connection parameter".  (Presumably no user ID at all.)
#   .	SASL/GSSAPI sets up 56 bit security, but the (hashed) password is only 
#	revealed with ssf >= 128 bits.  Web info suggests (but I don't take it
#	as authoritative) that SSL/GSSAPI can't set higher than 56 bits, and
#	if you want more, you need to do TLS before GSSAPI.  
#   X	A user is present in passwd but not shadow.  Updating shadow, kills
#	the whole record.  Users: rtkit testacct .  Disposition: don't do that.

# Test Matrix -- Output, or for -c, 2nd input 
# Input		flat		LDIF		LDAP(empty)	LDAP(full)
# flat		--		OK (incl -a)	*
# LDIF		OK		--
# LDAP(full)
# -c flat					OK
# -c LDIF					OK
# -c LDAP (impossible)

# $Header: /src/math/etc/ldap/RCS/ldaputil,v 1.2 2011/03/05 05:49:04 jimc Exp jimc $

# $Log: ldaputil,v $
# Revision 1.2  2011/03/05 05:49:04  jimc
# Added format presets to handle udata, hostgroup, netgroup, auto_home.
# Input lines ending with backslash newline (as in netgroup) are joined.
# Added dynamic field type selection (as in netgroup).
#
# Revision 1.1  2010/08/16 18:06:03  jimc
# Initial revision
#


use strict;

package Barf;		# Forward reference
package Attrconv;	# Forward reference

# ======
package Opt;
# This package is used by LDAP utility scripts that all use a common set of
# command line arguments.  Some defaults are taken from ldap.conf and friends;
# see Opt::default() for all the anally retentive override possibilities.
#   -i file     Input data is read from a UNIX directory file of this name, 
#		such as /etc/passwd or /etc/master/passwd.new .
#		'-' designates standard input (or output, for -o).  If the 
#		filename has an extension of .ldif then it is interpreted as
#		LDIF format (but see -L).  If -i is absent then input data 
#		comes from the LDAP table.  
#   -o file     Similar to -i except this file is written on.  
#   -c file	Similar to -i except the activity is to read the -c and -i
#		datastreams and report discrepancies.  When -c is absent the
#		activity is to read -i and write the content onto -o.  With
#		-c and not -i, the -c file is compared with the LDAP table. 
#   -L ioc	The argument is a set of bytes i, o or c.  These are 
#		interpreted as LDIF whatever the filename's extension.  This is
#		required if standard input/output is involved since the 
#		filename of '-' lacks the extension.  
#   -a		Include the realm containers (otherwise they are assumed to
#		exist but are not checked).
#   -k		When modifying the LDAP table, remove all entries not in the
#		input file (otherwise extra entries are left alone).  Do not
#		specify if output is to a file.  
#   -B		Bail out after the first error (failure to update an entry).
#		For debugging.
#   -f filetype Semantics of the UNIX directory file involved, in the form of
#		the file's basename.  The defalt is inferred from the actual
#		filename (from -i -o or -c), e.g. "/etc/master/passwd.new"
#		would turn into "passwd".  Many of the following arguments have
#		defaults that are a function of the -f value.
#   -F fmt      The flat file being handled has this format.  The value is a
#		sequence of LDAP attribute (field) names separated by the
#		authentic separators in the plain file (and flags).  See
#		package FlatFormat for the flags.  Default per -f.
#   -t class	A space separated list of objectClass value(s) to go into the
#		entries.  Default per -f.
#   -O ou	Pseudo-organizational-unit used in the Distinguished Names of
#		the table entries.  Default per -f.  
#   -v		Verbose output.  Mainly, prints statistics at the end.
#   -q		Quiet (no output except on errors).  Without -q it prints
#		a 1-line summary of statistics.  
#   -n		LDAP tables are not changed.  But the -o file (if any) is
#		still written.  
#   -d N	Turn on debugging messages.  N is an integer (sum of bits)
#		specifying which messages are wanted; see below for values.  
# The following options are generally similar to those in the LDAP utilities:
#   -b DN       This Distinguished Name (e.g. ou=People,dc=example,dc=com) is a
#		container for the LDAP table per -f, to be matched with records
#		in the flat file.  The default is the -O value followed by DC's
#		from the BASE parameter in ldap.conf.
# The defaults for the host and port are found in /etc/openldap/ldap.conf.
#   -H URI	Space separated list of LDAP URI(s) for the server(s) to use. 
#   -h host	Host where the server is located.  (Use only one of -H or -h.)
#		In the likely case that TLS is used (-Z) the hostname in -h or
#		-H must match that in the server's certificate (normally the
#		FQDN, sometimes a particular CNAME) or it will be rejected.
#   -p port	Port that the server is on.
#   -Z		Require TLS (using StartTLS command).  We don't use -ZZ; TLS
#		either is not used or is required.  -Z is on by default 
#		whenever there is a password for simple authentication.  
#   -x		Use simple authentication (vs. SASL).
# The following are only for simple authentication.
#   -D DN       Bind using this Distinguished Name. If -D is omitted, the
#		program will bind anonymously.  Default is BINDDN in ldap.conf
#		or (more normally) ~/.ldaprc .
#   -W		Prompt on /dev/tty for the password.
#   -w passwd	Put your password on the command line so everyone can see it.
#		Extension: default from BINDPW in ldap.conf. 
#   -y file	The password is read from this file.  Extension: default from
#		BINDPWFILE in ldap.conf.  Use only one of -w or -y.  
# The following are only for SASL authentication.  
#   -O secprop	SASL security properties.  (Not actually honored, see -O OU).
#   -Y mech	SASL mechanism.
#   -R realm	Realm (Not actually honored).
#   -U user	Authentication ID (default from USER environment variable).
#   -X user	Authorization ID (default is same as authentication ID).

# The configuration file(s) have key value pairs which are saved in $opt.
# Keys are case insensitive and are converted to lower case in this program. 
# See Opt::default for the names of the various configuration file 
# possibilities.

# $opt->{errcode} is a global error code which ends up as the exit code of
# the program; it is a sum of these bits:
#	0	no error was found
#	1	some entries were unequal (only set with -c)
#	2	write failures, e.g. server rejected an update
#	4	bad input data
#	8	other errors that prevent startup

# Debug bits:  Add these to give the value for the -d option.
#   1		Program forces this to 0 (kill bit)
#   2		Program forces this to 1 (for non-debug output)
#   4		Whatever we're currently debugging
#   8	010	Important intermediate values; show options being used
#   16	020	Entry and exit from major subroutines
#   32	040	Show each Entry affected by the program (bypass irrelevant ones)
#   64	0100	Show each Entry read and written including irrelevant ones

# The Opt object is a hash keyed by the switch letters.  There is only one
# per program execution.  Other packages often import it into their address
# space with this motif ("our" is not needed):
#   BEGIN { *opt = \$Opt::opt; }
# Other packages may preset options with a motif like:
#   BEGIN { $Opt::opt->{x} = "value"; }
our $opt; BEGIN { $opt = bless({ }, "Opt"); }

# Other packages may want to monkey with default arguments.  They should
# append to @defaults a callback using this motif:
#   BEGIN { push(@Opt::defaults, "Class", \&subrt); }
# $class->$subrt() will be called (just the class argument, static member
# function).  If OK it should return 1; or print a message (not die) and return
# 0.  
our @defaults;

use Getopt::Std;

BEGIN { *barf = \&Barf::barf; }
our $conv; BEGIN {
    #$conv = Attrconv->new();
    $conv = bless({ }, 'Attrconv');	# Gross perversion of modularity
    %$conv = ( d => { 
	in  => \&Barf::dbconv,
	out => sub { sprintf "0%o", $_[0]; },
	} );
}

# "Constructor".  Arguments:
#   $class	Name of class (Opt)
#   K=>val	0 or more key-value pairs used to preset default option values.
#   Returns	Ref. to the Opt object hash.
sub new {	#Opt
    my($class) = splice(@_, 0, 1);
		# Copy preset defaults into the pre-existing Opt object.
    my($k, $v);
    while (($k, $v) = splice(@_, 0, 2)) {
	$opt->{$k} = $v;
    }
		# Analyse the command line options.
    getopts("i:o:c:L:akBf:F:t:vqnd:b:H:h:p:ZxD:Ww:y:O:Y:R:U:X:", $opt) 
	or die "Invalid command line option(s)\n";
    $conv->convhash($opt, 'in');	# Immediately convert the format.
		# Set default arguments of interest to various packages,
		# including reading configuration options into $opt.
    my $ok = 1;
    my ($dclass, $subrt);
    while (($dclass, $subrt) = splice(@defaults, 0, 2)) {
	barf(020, "Opt->new calls %s initialization ( -d 0%o )\n", 
	    $dclass, ($opt->{d} || 0));
	$ok &&= $dclass->$subrt();
    }
    barf(8, sub { "Opt->new is using these options:\n%s", $opt->as_string() });
    barf(3, "Initialization error, exiting.\n") unless $ok;
    $opt;
}

# Produces a string showing the options, for debugging.  Args:
#   $this	Class object ref.
#   Returns	A string.  
sub as_string {	#Opt
    my($this) = @_;
    my $str = '';
    foreach my $k (sort keys %$this) {
	my $v = $this->{$k};
	$v = '[' . join(' ', @$v) . ']' if ref($v);
	$v = $conv->conv($k, 'out', $v);
	$str .= sprintf("%-4s %s\n", $k, $v);
    }
    $str;
}

# Call this static member function to read ldap.conf and friends.  
sub default {	#Opt
    my($class) = @_;
    my $rc = 1;				# The eventual return value
    $opt->{errcode} = 0;		# Program return code
    my $cmdline = { %$opt };		# Save explicit command line options
		# Read the client configuration files.  Technically, all are
		# optional.  A value in later files overrides earlier.  See
		# the man page for ldap.conf for all the anally retentive
		# variables and overrides.  Command line options override
		# all others.  
		# File format: key value, # begins a comment.  Keys are case
		# insensitive and are stored in $opt in lower case.  (Command
		# line switch letters are case sensitive.)
    my @fnames = ("/etc/openldap/ldap.conf", "$ENV{HOME}/.ldaprc", 
	"$ENV{HOME}/ldaprc", "./ldaprc");
    my($key, $val);
    push(@fnames, $ENV{LDAPCONF}) if $ENV{LDAPCONF};
    push(@fnames, "$ENV{HOME}/.$ENV{LDAPRC}", 
	    "$ENV{HOME}/$ENV{LDAPRC}", "./$ENV{LDAPRC}") if $ENV{LDAPRC};
    undef @fnames if $ENV{LDAPNOINIT};	# You can bypass all client conf files
    my @ldapconf;			# Names of config files actually read
    foreach my $fname (@fnames) {
	my $FD = FileHandle->new($fname) or next;
	push(@ldapconf, $fname);
	while (<$FD>) {
	    next if /^\s*(:?\#|$)/;	# Toss comments and blank lines.
	    chomp;
	    ($key, $val) = split(' ', $_, 2);
	    $key = lc($key);
	    $opt->{$key} = $conv->conv($key, 'in', $val);
	}
    }
    $opt->{conf} = \@ldapconf;
		# An environment variable LDAP$PARM sets that parameter.
		# Keys are lower case (case insensitive).  
    while (($key, $val) = each %ENV) {
	next unless substr($key,0,4) eq 'LDAP';
	$key = lc($key);
	$opt->{substr($key,4)} = $conv->conv($key, 'in', $val) 
    }
		# Command line options override the configuration file.
		# (Format already converted.)  (Command line switch letters
		# are case sensitive.)
    foreach $key (keys %$cmdline) {
	$opt->{$key} = $cmdline->{$key};
    }
		# Infer the BASE from other parameters if not explicit.  
    BASE: {
	last if $opt->{base};		# Already know BASE
	if ($opt->{b}) {		# Infer BASE from -b if given.
	    ($opt->{base} = $opt->{b}) =~ s/ou=[^,]*,//;
	    last;
	}
	$opt->{base} = "dc=missing";
	barf(2, "Realm (query BASE) can't be determined from either -b or %s, (not) using %s\n",
	    join(' ', @ldapconf), $opt->{base});
	$rc = 0;
    }
    $rc;
}
BEGIN { push(@Opt::defaults, 'Opt', \&default); }

# ======
package Barf;

BEGIN { *opt = \$Opt::opt; }

# Prints an error message on stderr.  Call as: 
#   &Barf::barf(3, "Error code %d (fatal)\n", $code);
# You can import barf into your namespace like this:
#   BEGIN { *barf = \&Barf::barf; }
# Args:
#   $flags	This is "anded" with the debug option ($opt->{d}) and if the
#		result is nonzero the message is printed.  Special cases:
#		1   Exit (die) after showing the error message.  Also set bit
#		    2 if you set bit 1.
#		2   Provide this bit for non-debug messages to be printed
#		    unconditionally.
#   $format	A format string for sprintf (or could be the whole message).
#		Alternatively it can be a code ref. which produces (in list
#		context) the format and values.  This is called only if the
#		message is to be printed, i.e. the debug switch is on.  This
#		saves the work of creating and throwing away the values when
#		not debugging.  Example: 
#		    barf(4, sub { "%d: %s = %s\n", $lno, $key, $value });
#   @values	Values to be substituted by sprintf, if any.
#   Returns	Always undef, so you could do:
#		return barf(2, "Message\n") if $bad;
sub barf {	#Barf
    my $flags = shift @_;
    return undef unless ($flags & $opt->{d});
    my ($format, @vals) = @_;	# Format must be a scalar separate from values
		# For debug messages, you can send in a subroutine that emits
		# the format and values.
    if (ref($format)) {
	($format, @vals) = &{$format}();
    }
    printf STDERR $format, @vals;
    exit 8 if $flags & 1;
    undef;
}

# Easy debug printout of variables.  Args:
#   $flags	First argument of barf()
#   $msg	Initial part of message.  Or it can be a code ref. which 
#		returns the initial message and the label-value pairs. 
#   label => $value	Key-value pairs.  Labels are printed, then their value.
#   Returns	What barf returns.
sub barflabel {	#Barf
    my($flags, $msg, @keyval) = @_;
    return undef unless ($flags & $opt->{d});
    if (ref($msg)) {
	($msg, @keyval) = &{$msg}();
    }
    my (@vals, $label, $v);
    while (($label, $v) = splice(@keyval, 0, 2)) {
	$msg .= "  $label %s";
	push(@vals, (!defined($v) ? 'undef' : ($v eq '') ? "''" : $v));
    }
    $msg .= "\n";
    barf($flags, $msg, @vals);
}

# Conversion subroutine for the debug option, for use with Attrconv.
# Arg: external representation of -d value.  Returns internal representation.
sub dbconv {	#Barf
    my($d) = @_;
    $d = '0' unless $d;
    $d = oct($d) if substr($d,0,1) eq '0';
    $d &= ~1;			# Idiotproof bogus setting of "die" bit
    $d |= 2;			# Bit is always on for non-debug messages
    $d;				# This is the internal representation (integer).
}

# Initialization features:
sub default {	#Barf
    my($class) = @_;
    $| = 1 if $opt->{d} & ~3;	# Line buffer stdout if debugging
    barf(020, "Barf initialization ( -d %0o)\n", $opt->{d});
    1;				# This subroutine always succeeds
}
BEGIN {
    $opt->{d} = 0;		# Make sure debug value isn't undef
		# Best to initialize Barf first.
    unshift(@Opt::defaults, 'Barf', \&default);
}

# ======
package Attrconv;
# For converting attributes according to special formats.  The class object
# is a hash whose keys are attribute names; each value is a hash ref. whose
# keys signify the direction of conversion (unique for each client package),
# and their values are code refs called with one argument, the value of the
# attribute.  It should return the modified value suitable for output in the 
# direction indicated.  

# Since package Opt needs to use Attrconv, its conversion object can't be
# initialized in a BEGIN block.  Client packages should construct a hash
# ref as described above, and then push (a copy of) this ref. onto @initme
# using this kind of motif:
#	BEGIN { 
#	    $conv = { userPassword => { flat => sub {...}, ldap => sub {...}} };
#	    push(@Attrconv::initme, $conv);
#	}
# In principle the client package could just bless the hash itself, but that
# radically violates modularity.  
our @initme;

# Constructor.  This is actually pro forma since Attrconv objects are only
# constructed through the default() subroutine.
sub new {	#Attrconv
    bless({ }, $_[0]);
}

# Converts an attribute.  Args:
#   $this	Class object ref.
#   $attr	Name of the attribute
#   $dir	Direction key (varies in each client package)
#   $val	The value to be converted
#   Returns	The converted value; in the usual case that no conversion
#		subroutine is found in $this, $val is returned unchanged.
sub conv {	#Attrconv
    my($this, $attr, $dir, $val) = @_;
    my $atrh = $this->{$attr};		# Hash keyed by attribute name
    my $sub = ($atrh ? $atrh->{$dir} : undef) || sub { $_[0]; };
    &{$sub}($val);
}

# Converts a list of attributes.  Args:
#   $this	Class object ref.
#   $attr	Name of the attribute
#   $dir	Direction key (varies in each client package)
#   \@val	Ref. to a list of values to be converted
#   Returns	List of converted values; in the usual case that no conversion
#		subroutine is found in $this, @$val is returned unchanged.
sub convlist {	#Attrconv
    my($this, $attr, $dir, $val) = @_;
    my $atrh = $this->{$attr};
    my $sub = ($atrh ? $atrh->{$dir} : undef) || sub { $_[0]; };
    map {&{$sub}($_)} @$val;
}

# Converts all attributes in a hash.  Args:
#   $this	Class object ref.
#   \%hash	Hash whose values are to be converted.
#   $dir	Direction key (varies in each client package).
#   Returns	Nothing (%hash values are converted).
sub convhash {	#Attrconv
    my($this, $hash, $dir) = @_;
    foreach my $k (keys %$hash) {
	my $atrh = $this->{$k} or next;		# Hash keyed by attribute name
	my $sub = $atrh->{$dir} or next;
	$hash->{$k} = &{$sub}($hash->{$k});
    }
}

# The client packages have made a list of Attrconv objects on @initme.
# This subroutine blesses them into the package.
sub default {	#Attrconv
    my($class) = @_;
    foreach my $obj (@initme) {
	bless($obj, $class);
    }
    1;
}
# Has to init the Attrconv objects before Opt::default tries to use it.
BEGIN { unshift(@Opt::defaults, 'Attrconv', \&default); }

# ======
package FlatFormat;
# This object describes the transformation between a flat file and a LDAP
# Entry.  It is a hash with these members:
#   format	The format string (for debugging).
#   fields	Ref. to an array of attribute names in the order they occur in 
#		the flat file.  In one bizarre case the same field occurs 
#		twice in the flat file, and in another, one flat field feeds
#		into two Entry attributes.  In the latter case an arbitrary
#		one is chosen as the "real" field name.  
#   attrs	Ref. to list of attributes in the Entry, with objectClass
#		added synthetically.  
#   regexp	A regexp object which will split a line into these fields.
#   splitre	Present only if the last field is repeated.  It extracts the
#		field values.  
#   prikey	The name of the primary key field.
#   PF          Hash keyed by attribute names.  Each attribute name has a
#		member, including multiple attributes per field and including
#		pseudo-attributes excluded by the 'x' flag.  Do not use "keys
#		%{$this->{PF}}" to get a list of field names, since something
#		looks for domain container fields and creates extraneous empty
#		hashes.  Values are hashes with these members:
#		sep	The separator for output, to follow the field, as text
#		rsep	Trailing separator as regexp
#		psep	The preceeding separator, as text
#		rpsep	Preceeding separator as regexp
#		index	Subscript in field list of this field (0 origin).
#		attrs	Ref. to list of attribute names (usually exactly 1)
#			whose values come from this field.  
#		dflt	Value to be used if the field is null.  Usually ''.
#		wid	Field width, pad with whitespace on output.
#		<flag>	Key is a flag byte (e.g. 'r') and value is 1 if that
#			flag is present.
#		split	Only if the field is split, this is another PF hash for
#			the properties of the second instance.  
#   base	Distinguished Name of the container within which the Entry
#		lives.  
#   oclass	Ref. to array of objectClass attributes to set in the entry.
#   fclass	Name of format (normally, basename of UNIX directory file)

# Format of the format string: It is a sequence of units, each of which is a
# slash-separated list of field names (attributes as used in the Entry), then
# (flags) in parens (optional), then =default (optional), then a
# non-alphanumeric separator.
#   .	The first field must always start at the beginning of the record.
#	No leading whitespace.
#   .   The first section of the unit is a list of attribute names, usually
#	only one, separated by '|'.  The first is the official name of the
#	field, but the same value is stuffed in the resulting Entry under each
#	of the attribute names.
#   .	The separator after the last existing field is not written and is
#	not required on input.  Nonetheless it must appear in the format.
#   .	Before the separator could be a set of letters in (parens), which are
#	processing flags.  These are removed, that is, not written, not 
#	expected on input.  Flag letter interpretations:
#	r   Field may be repeated more than once.  Last field only.  This
#	    field's separator comes after each instance except the last. 
#	o   Field is optional, i.e. no content and preceeding separator may 
#	    or may not be missing.  Put 'o' on contiguous trailing fields. 
#	N   Field must not be '' (and must not be missing).  If it is, the 
#	    record is tossed.  
#	q   Field may include quoted strings (quotes are preserved).  For 
#	    mail aliases.
#	x   Field doesn't go in the Entry and on output its value is ''.
#	s   Field is split: first value goes in the nonrepeated field and the
#	    rest is in the repeated instance.  Put on both instances.  Used
#	    in /etc/rpc.  
#	k   This is the primary key (RDN).  In case there are multiple 
#	    attributes in the field name, the first is the primary key 
#	    attribute name, e.g. uid/cn results in a DN of uid=joeluser.
#   	U   Take the union of all the multiple rows with the same primary key.
#	    Put this flag on the primary key field.
#	u   When taking the union, fields with this flag are allowed to have
#	    multiple values; others are just replaced.  
#   	E   Multiple tables share the same DN, don't freak out.  Example:
#	    passwd and shadow.
#   	\d+ The last "flag" may be a number which is the field width.  On 
#	    output the field is padded to this width with blanks or tabs. 
#	    Value is 0 if not specified.  
#   .   After (flags) is (optionally) '=' followed by alphanumerics or
#	hyphen, which gives a default value.  Just '=' is legal (value is '')
#	but useless.
#   .	On output the separators are emitted as written.
#   .   On input, wherever whitespace appears in the separator, any number
#	(including 0) of any kind of whitespace bytes may appear in the record,
#	except if the first separator byte is whitespace, at least one such
#	byte is required.

use Net::LDAP::Entry;

# Presets for the various standard file types.  Key is the -f value, e.g.
# passwd or group, which normally would be the basename of the flat file.  
# Value hashes are arguments for FlatFormat->new() and have these members:
#	f	Format class, copy of the key (-f value)
#	O	Pseudo-organizational-unit, last component of realm containers
#	t	Space separated list of objectClass values to go in the Entry.
#		If an object comes from multiple sources (e.g. passwd, shadow
#		-> People), include all its objectClasses with each source.
#	F	Format string, see above for its format
#	C	Cleanup subroutine, a code ref.  If present it sanitizes the 
#		input line before it is split for fields.  /etc/rpc has a
#		trailing comment feature which is very troublesome. 
#		If the return value is undef, the line should be tossed 
#		silently.
#	A	Type subroutine, maps the sequential index (0 origin) of a
#		field to the index in FlatFormat->{fields}.  Args: Ref to 
#		FlatFormat object, ref. to array of field values, index of 
#		field.  The default is like this but simplified:
#		sub { my($this, $data, $i) = @_; $i; }
#		Netgroup needs this for its bizarre polymorphic format. 
#		
our %preset; BEGIN {
    %preset = (
	passwd => {
		f => 'passwd',
		O => 'People',
		t => 'top account posixAccount shadowAccount mathUdata',
		F => 'uid|cn(kE):pw(x):uidNumber(N):gidNumber(N):gecos:homeDirectory(o):loginShell(o) ',
	    },
		    # The auto_home map is derived from passwd but specially.
		    # Set -f auto_home -i /etc/passwd
	auto_home => {
		f => 'auto_home',
		O => 'Auto_home',
		t => 'top automount',
		F => 'automountKey(k) automountInformation(N) ',
		    # Expects the homedir to be like /u/$HOST/rest/of/path
		    # and returns $USER (tab) $HOST:/rest/of/path
		    # or undef if it doesn't see the /u/, killing the line.
		C => sub {
			my @line = split(/:/, $_[0]);
			if ($line[5]  =~ m|^/u/(\w+)/(.+)|) {
			    $_[0] = "$line[0]\t${1}:/$2";
			    return 1;
			}
			undef;
		    },
	    },
	shadow => {
		f => 'shadow',
		O => 'People',
		t => 'top account posixAccount shadowAccount mathUdata',
		F => 'uid(kE):userPassword:shadowLastChange:shadowMin:shadowMax:shadowWarning:shadowInactive:shadowExpire:shadowFlag ',
	    },
	udata => {
		f => 'udata',
		O => 'People',
		t => 'top account posixAccount shadowAccount mathUdata',
		F => 'uid(kE):uidNumber(N):homeSite:mailSite:sponsor:siteGroup:discq:paperq:eDate ',
	    },
	group => {
		f => 'group',
		O => 'Group',
		t => 'top posixGroup',
		F => 'cn(k):userPassword:gidNumber(N):memberUid(ro),'
	    },
	aliases => {
		f => 'aliases',
		O => 'Aliases',
		t => 'top nisMailAlias',
		F => 'cn(k): rfc822MailMember(rNq), '
	    },
	rpc => {
		f => 'rpc',
		O => 'Rpc',
		t => 'top oncRpc',
			# The 3rd "field" contains additional cn's that are
			# less equal than the first one, i.e. they don't
			# get a separate DN, just being multiple values of the
			# cn attribute.  Following those is optionally
			# "# description". The program can't handle 2 optional 
			# fields, so it gets tossed.
		F => "cn(ks16)\toncRpcNumber(N) cn(rso) ",
		C => sub { $_[0] =~ s/\s*#.*$//, 1 },
	    },
			# Example: whosockami 2009/udp; whosockami 2019/tcp
			# whosockami is the RDN.  How are we supposed to deal
			# with that?  Normally the cn and port are the same
			# for multiple protocols, which are put out on separate
			# lines in /etc/services.  
			# More bad news: "whois++ 63/tcp" invalid chars in a DN.
			# "sunrpc 111/tcp rpcbind": split primary key!
			# We'll need to jettison the description, same as for
			# /etc/rpc.  
	services => {
		f => 'services',
		O => 'Services',
		t => 'top ipService',
		F => 'cn(kUus17) ipServicePort(N)/ipServiceProtocol(Nu) cn(ruso) ',
		C => sub { $_[0] =~ s/\s*#.*$//, 1 },
	    },
	protocols => {
		f => 'protocols',
		O => 'Protocols',
		t => 'top ipProtocol',
		F => "cn(k16) ipProtocolNumber(N3) alias(x14) # description(o)#",
	    },
	networks => {
		f => 'networks',
		O => 'Networks',
		t => 'top ipNetwork',
			# According to the schema, the cn is optional and
			# there are optional trailing fields possibly in this 
			# order: netmask, l (whatever that is), description,
			# manager.  The netmask comes from /etc/netmasks.
		F => "cn(k16)\tipNetworkNumber(N) ",
	    },
			# A comment in the file says /etc/netmasks is a
			# Solaris-ism, not existing on Linux.  Since the RDN
			# does not appear, how are we supposed to stuff in
			# the information?
	netmasks => {
		f => 'netmasks',
		O => 'Networks',
		t => 'top ipNetwork',
		F => "ipNetworkNumber(N16)\tipNetmaskNumber(N) ",
	    },
	hosts => {
		f => 'hosts',
		O => 'Hosts',
		t => 'top ipHost device',
			# The first cn is the host's canonical name and is the
			# RDN.  Other names don't have their own Entry and DN.
			# It's normal for a host to have more than one 
			# ipHostNumber (e.g. IPv4 and IPv6); each belongs on
			# its own row in /etc/hosts.  
		F => "ipHostNumber(Nu40)\tcn(kUur)\t",
	    },
			# Netgroup is a real nightmare: 3 OU's (ou=Netgroup,
			# nisMapName=netgroup.byuser,nisMapName=netgroup.byhost)
			# and a totally wacko format of the flat file.
			# (I've lost track of where the OU's come from.)
			# Flat file format has fields whitespace separated and
			# long lines (which are common) joined by backslash
			# newline.  First field is the Common Name (cn).
			# After that you can have an identifier (sub-netgroup)
			# or a triplet in parens.  The 1st subfield is the host,
			# which is the only one ever used.
	netgroup => {
		f => 'netgroup',
		O => 'Netgroup',
		t => 'top nisNetgroup',
		F => 'cn(k) nisNetgroupTriple(ro) memberNisNetgroup(ro) ',
		A => sub {
			my($this, $data, $i) = @_;
			($i == 0) ? 0 : (substr($data,0,1) eq '(') ? 1 : 2; #\)
		    },
	    },
			# Special table in mathnet.schema.  
	hostgroup => {
		f => 'hostgroup',
		O => 'Hostgroup',
		t => 'top mathHostGroup',
		F => 'cn(kU) hostGroup(rou) ',
	    },
			# The following are container classes which may contain
			# a table or levels of the realm, keyed by their main
			# objectClass name.  From core.schema objectclass 
			# definitions.  
	domain => {
		f => 'domain',
		k => 'dc',
		t => 'top domain',
		F => 'dc(kN) ',
	    },
	organizationalUnit => {
		f => 'organizationalUnit',
		k => 'ou',
		t => 'top organizationalUnit',
		F => 'ou(kN) ',
	    },
	country => {
		f => 'country',
		k => 'c',
		t => 'top country',
		F => 'c(kN) ',
	    },
			# Somehow these two use the same objectClass for 
			# different external semantics and different attributes.
	locality => {
		f => 'locality',
		k => 'l',
		t => 'top locality',
		F => 'l(k) ',
	    },
	state => {
		f => 'state',
		k => 'st',
		t => 'top locality',
		F => 'st(k) ',
	    },
	);
}

# $ctnrs->{$attr}, e.g. $ctnrs{'dc'} is an Entry object stuffed with attributes
# implementing the kind of toplevel container object which has that kind of
# attribute, except the key attribute (e.g. 'dc') is not stuffed.  The 
# Distinguished Name is also not stuffed.
our(%ctnrs, %attr2obj, %obj2attr, %cache);
BEGIN {
    %attr2obj = qw(
	dc	domain
	ou	organizationalUnit
	c	country
	l	locality
	st	state
	);
    %obj2attr = reverse %attr2obj;
    my($attr, $ctnt);
    while (($attr, $ctnt) = each %attr2obj) {
	my $entry = $ctnrs{$attr} = Net::LDAP::Entry->new();	# Chgtype = add
	$entry->add(objectClass => [ split(' ', $preset{$ctnt}->{t}) ]);
    }
}

# A few attributes need special handling.  The attrconv hash keys are attribute
# names, and the value is a sub-hash with keys of 'flat' or 'ldap'.  Their
# value is a code ref (subroutine) which returns the attribute value as that
# kind of file needs it, from an argument found in the other kind of file.  
# "ldap' includes LDIF.
our $conv; BEGIN {
    $conv = {
	userPassword	=> {
	    flat	=> sub { my($v) = @_; $v =~ s/\{crypt\}//; $v; },
	    ldap	=> sub { my($v) = @_; substr($v,0,0) = '{crypt}' if length($v) > 1; $v; },
	},
    };
    push(@Attrconv::initme, $conv);	# Blesses $conv into class Attrconv
}

BEGIN { *barf = \&Barf::barf; }

# Constructor.  Actually assembles the FlatFormat object to be put in the
# cache.  Arguments:
#   $class	Class name (FlatFormat).
#   \%PS	Preset hash ref, with these members, defaults from 
#		corresponding command line options or their defaults.
#		F	Format as a string
#		t	Space separated list of objectClass to go in the Entry
#		f	Name of format class (basename of UNIX directory file)
#		k	Primary key attribute (not used in this subrt)
#		C	Cleanup subroutine (optional)
#		A	Field index subroutine (optional)
#   $base	The base Distinguished Name, as a string.
#   Returns	New FlatFormat object
sub new {	#Flatformat
    my($class, $PS, $base) = @_;
    my $fmt = $PS->{F};
    my $this = bless({
	format		=> $fmt,
	base		=> $base,
	fields		=> [ ],
	#attrs		=> [ ],			# This is stored later
	PF		=> { },
	oclass		=> [ split(' ', $PS->{t}) ],
	fclass		=> $PS->{f},
	cleanup		=> $PS->{C} || sub { 1 }, # Does nothing, successfully
	ixsub		=> $PS->{A} || sub { $_[2]; },
	#regexp		=> qr(whatever),	# This is stored later
	}, $class);
    my $fields = $this->{fields};
    my %attrs = qw(objectClass 1);
    my $PF = $this->{PF};
		# The description says the last separator is required, but
		# idiotproof a missing separator anyway.
    if ($fmt !~ /[^\w=()]$/) {
	barf(2, "Warning, format for %s lacks ending separator, using one blank.\n", $opt->{f});
	$fmt .= ' ';
    }
                # @units = field/name flags dflt separator, with an empty field
		# name after the last separator.  The regexp in "split"
		# produces these items: optional parenthesized flag letters,
		# optional =default value, and required non-word chars, which
		# are the separator.  The value of missing optional items is
		# ''.
    my $NSU = 4;			# Number of list items per field
    my @units = split(/((?:\(\w*\))?)((?:=[-\w]*)?)([^\w|-]+)/, $fmt);
		# Can't handle a separator at the start of the format.  Also 
		# fails if the format is "".
    barf(3, "Flatformat->new(%s): separator not allowed to start the format.\n%s\n", 
	$this->{fclass}, $this->{format}) unless $units[0];
    my $rpsep = my $psep = '';		# Separator from the preceeding field
    my $regexp = "^";			# Must match at start of line
    for(my $i = 0; $i < @units ; $i += $NSU) {
	my ($name, $flags, $dflt, $sep) = @units[$i .. $i+$NSU-1];
		# $name is a slash separated list of attribute names.  Use the
		# first of these as the official field name.
	my $names = [ split(/\|/, $name) ];
	my $k;
	foreach $k (@$names) {		# Make a list of all attrs in entry
	    $attrs{$k}++;
	}
	push(@$names, 'no-name') unless @$names;	# Should never happen
	$name = $names->[0];
	$flags =~ /\(?([[:alpha:]]*)([\d]*)\)?/;
	my $wid = $2 || 0;
	$flags = $1;
	$dflt =~ s/^=//;
	my $rsep = $sep;	# $rsep is a regexp recognizing the separator
	$rsep =~ s/^\s+/\\s+/g;		# Leading whitespace is required
	$rsep =~ s/\s+/\\s*/g;		# Other whitespace is optional
		# %pf becomes the per-field hash with flags and info about
		# that field.
	my %pf = (sep => $sep, rsep => $rsep, psep => $psep, rpsep => $rpsep, 
	    wid => $wid, attrs => $names, dflt => $dflt, index => int($i/$NSU));
	$flags =~ s/\d//g;		# Remove the field width from the flags
	foreach $k (split(//, $flags)) {	# Turn on flag letters
	    $pf{$k}++;
	}
	foreach $k (@$names) {		# Create a unit for each field name
	    if (exists($PF->{$k})) {
		$PF->{$k}{split} = \%pf;
	    } else {
		$PF->{$k} = \%pf;
	    }
	}
	if ($pf{k}) {			# Capture the name of the primary key
	    $pf{N}++;			# Primary key may not be null.
	    $this->{prikey} = $name;
	}
		# This is the fragment of the regular expression that captures
		# the field content.  
	(my $notrsep = $sep) =~ s/\s+/\\s/g;	# Character class of separator
		# Regexp explanation:  the whole regexp's output is captured.
		# 1st alternative: a literal quote followed by 
		#    either non-quote or backslash (don't backtrack) or 
		#    backslash and 1 char, the pair repeated * times (don't
		#    backtrack), up to an ending literal quote.
                # 2nd alternative: Complement of the following separator *
		#    times.
		# It's legal for the payload to have 0 characters; null fields
		# are checked for after being split up.
	my $re = $pf{q} ? "(\"(?:[^\\\"]++|\\\\.)*+\"|[^$notrsep]*)" : 
			"([^$notrsep]*)";
	if ($pf{r}) {
		# Flag (r) means the field is repeated.  A separate regexp
		# is used to split it.
	   $this->{splitre} = $rsep;
		# Change to a regexp which captures the entire trailing 
		# portion, which will be split by {splitre}.
	   $re = "(.*)";
	}
		# If the field is missing, the preceeding separator may or
		# may not appear.  Only the last field can be optional.  
	if ($pf{o}) {
	    $re = "(?:$rpsep(?:$re)?|(?:$rpsep)?)";
	} else {
		# Non-optional field, prepend the previous separator
	    $re = "$rpsep$re";
	}
	$regexp .= $re;			# Build up the regexp for the whole line
	push(@$fields, $name);		# Save the field name.  
	$psep = $sep;			# Propagate the previous separator
	$rpsep = $rsep;			# Propagate the previous separator
    }
    $regexp .= '$';			# Regexp must match to the end of line
    $this->{regexp} = qr{$regexp};
    barf(010, sub {"FlatFormat->new(%s) regexp = '%s' = '%s'\n", $this->{fclass}, $regexp, $this->{regexp} });
    $this->{attrs} = [ keys %attrs ];
    barf(3, "FlatFormat->new(%s): no primary key field (flagged with (k)).  Format:\n    %s\n", $this->{fclass}, $this->{format})
	unless $this->{prikey};
    $this;
}

# Clones the preset format info for this file/object type.
# Override presets with explicitly specified options.
# Make sure required options are known (for nonstandard format).
# Arg (ordinary subroutine): 
#   $fclass	Format class (key in %preset).  Can handle a nonstandard format.
#   $onerror	Argument to barf (0, 2, 3) for the error message if required
#		members were not specified.
#   Returns	Preset hash.  It will have a member "BAD" whose value is the
#		error message if the result is incomplete.
sub pclone {	#FlatFormat
    my($fclass, $onerror) = @_;
    my $ps = $preset{$fclass} ? { %{$preset{$fclass}} } : { };	#Return value
		# Override preset values with command line arguments, for these
		# members.
    foreach my $k (qw(F t O)) {
	$ps->{$k} = $opt->{$k} if $opt->{$k}
    }
    $ps->{f} ||= $fclass;	# Provide class, only for nonstandard format
    my @missing = grep {!exists($ps->{$_})} qw(F t O f);
    if (@missing) {
	my $ermsg = $ps->{BAD} = 
	    "FlatFormat->pclone(%s): incomplete format, you must specify -"
	    . join(', -', @missing);
	barf($onerror, "FlatFormat->pclone(%s): %s\n", $fclass, $ermsg);
    }
    $ps;
}

# Retrieve the correct FlatFormat.  The format class is inferred from the
# Distinguished Name, and there is a cache for the preset formats since
# the {base} member is altered.  Args:
#   $class	Name of class.  (Static member function.)
#   $subrt	Subroutine to be called: $format->{$subrt}($entry)
#   $entry	Argument for $subrt, must be an Entry ref. (actually, anything
#		with a $entry->dn() method) or a Distinguished Name string.
#   Returns	What $subrt returns.
sub auto {	#FlatFormat
    my($class, $subrt, $entry) = @_;
    my $dn = ref($entry) ? $entry->dn() : $entry;	# Extract Dist. Name
    my $fullen = length($dn || '') > length($opt->{b});	# False for containers
    if (!$dn) {
	$dn = 'dc=nonexistent';
	barf(2, "FlatFormat->auto(%s, undef), using %s as the DN\n", 
	    $subrt, $dn);
    }
    my ($fclass, $attr);		# File type, key in %preset
    if ($fullen) {
	$fclass = $opt->{f};
    } else {
		# Get the attribute name that begins the DN and convert.
	my $i = index($dn, '=');
	$fclass = $attr2obj{substr($dn, 0, $i)} || 'unknown';
    }
		# If the format is not cached, do that now.
    if (!$cache{$fclass}) {
	my $ps = &pclone($fclass, 3);	# Clone the preset format hash
	my ($leaf, $stem) = split(/,/, $dn, 2);
	$cache{$fclass} = $class->new($ps, ($stem || ''));
    }
    my $fmt = $cache{$fclass};
    {
	no strict;			# Allow symbolic reference to subrt
	$fmt->$subrt($entry);		# Will return what $subrt returns
    }
}

# Returns the relevant FlatFormat object ref for a Distinguished Name or an 
# Entry ref.  Call as FlatFormat->auto('getfmt', $entry) (or $dn as string).
sub getfmt {	#FlatFormat
    $_[0];				# All he wants is the FlatFormat object
}

# Returns a list of attributes relevant to this format.  'objectClass' is
# prepended implicitly.  
sub attrs {	#FlatFormat
    @{$_[0]->{attrs}};
}


# Splits a line to become an Entry.  Args:
#   $this	FlatFormat object ref.
#   $line       A record from the flat file, as a string possibly with an
#               ending newline.  Has to be the right format, realm containers
#               not allowed.
#   $lineno	Line number where this entry was found.  
#   Returns     Either a Net::LDAP::Entry ref. or a scalar error message, which
#		could be '' if the line is neither content nor an error, e.g.
#		a comment or empty line.  Modification type is 'add'.
# Rules for rejecting lines: blank lines and comments (starting with #) are
# rejected with a message of "".  On other lines that do not match the format,
# the message has a description.
# (Debug this carefully for /etc/hosts: RDN is the FQDN, multiple cn's are
# the hostnames and include the FQDN.  Yes, comes out right.) #DEBUG
sub split {	#FlatFormat
    my($this, $line, $lineno) = @_;
    chomp $line;
		# If the cleanup routine returns false (undef), the input line
		# is supposed to be tossed silently.  
    &{$this->{cleanup}}($line) or return '';
		# Reject a comment or blank line.
    return '' if $line =~ /^\s*(?:#|$)/;
		# Split the line according to its format.  If capturing parens
		# don't capture anything (e.g. an optional field) then their
		# value is undef, not a zero-length list, so toss the undefs.
    my @line = grep {defined($_)} reverse ($line  =~ $this->{regexp});
    my $nin = scalar(@line);
    if ($nin <= 0) {
	$opt->{errcode} |= 4;
	return "Line does not match the expected format";
    }
		# Capture a trailing repeated field.  The entire repetition
		# is in $line[0].  
    if (exists($this->{splitre})) {
	my @extra = split($this->{splitre}, $line[0]);
	splice(@line, 0, 1, reverse @extra);
    }
    my $ermsg = '';			# Return value in case of error
    my $entry = Net::LDAP::Entry->new(); # Return value if there's no error
					# Changetype is 'add' by default.
    $entry->{util_lineno} = $lineno;	# Cowboy programming, save line number
		# Stuff the line's fields into the new Entry.
    my $i = 0;				# Subscript in @fields
    my $j = 1;				# Actual field number, 1 origin, for msg
    my $ixsub = $this->{ixsub};		# Subrt. to translate $i to field index.
    my $nflds = @{$this->{fields}};	# Only the last field can be repeated
    my %split;				# Recognize if a field is split
    my $repeat;				# True if the last field is repeated
    my $rdn;				# First instance of primary key
    my $prikey = $this->{prikey};
    my($f, $PF, $v, $ix, $sub, $ac);
    STUFF: {
	$v = pop @line;			# The value of this field
	$ix = &{$ixsub}($this, $v, $i);	# What kind of field it is (array index)
	$f = $this->{fields}[$ix];	# Name of field
	$PF = $this->{PF}{$f};		# Per field data and flags
		# Recognize the second part of a split field.
	$PF = $PF->{split} if exists($PF->{split}) && $split{$f}++;
		# Convert from flat to LDAP format (if needed).
	$v = $conv->conv($f, 'ldap', $v);
		# Is this field allowed to be repeated?  
	$repeat++ if $PF->{r};
	if ($i > $PF->{index} && !$PF->{r} && $nin > $nflds) {
	    $ermsg .= sprintf("; too many fields (has %d, max %d)", 
		$nin, $nflds);
	    last;
	}
		# Certain fields in the flat file do not get into LDAP table.
	next if $PF->{x};
		# Is the field illegally null?
	if ($PF->{N} && $v eq '') {
	    $ermsg .= sprintf("; field %d (%s) must not be null", $j, $f);
	    $v = "_NULL_";
	}
		# Fill in the default value for a null field, usually ''.
	$v = $PF->{dflt} if $v eq '';
		# Add the value to the new Entry.
	foreach my $a (@{$PF->{attrs}}) {
	    $entry->add($a, $v);
	}
		# Capture the primary key, only the first value counts.
	$rdn = $v if $f eq $prikey && !defined($rdn);
    } continue {
	$i++ unless $repeat;
	$j++;
	redo STUFF if @line;
    }
		# Check that we got all required fields.
    while (($f, $PF) = each %{$this->{PF}}) {
	if ($PF->{N} && !$entry->exists($f)) {
	    $ermsg .= sprintf("; field %d (%s) must not be omitted", 
		$PF->{index}+1, $f);
	    $entry->add($f, '_MISSING_');
	}
    }
		# Stuff the objectClass attributes.
    $entry->add(objectClass => $this->{oclass});
		# Set up the Entry's Distinguished Name.  In case of multiple
		# values of the primary key, the first one is used.
    if (defined($rdn)) {
	$entry->dn($prikey . '=' . $rdn . ',' . $this->{base});
    } else {
	$ermsg .= sprintf("; missing primary key (%s)", $prikey);
    }
    substr($ermsg, 0, 2) = '' if $ermsg;	# Remove unused leading '; '
    $ermsg ? $ermsg : $entry;
}

# Joins an entry, giving a flat string to be written in the UNIX directory 
# file.  This is the inverse of sub split().
#   $this	Class object ref. (FlatFormat).
#   $entry	The Net::LDAP::Entry object to be emitted.  If its objectClass
#		differs from what $this has the format for, this subroutine 
#		will find and use the correct format (for realm containers). 
#		Non-Entries (typically undef) are handled neatly.
#   Returns     A list, usually just one member, each element of which is a
#		string representation of the Entry, with an ending newline.
#		/etc/hosts needs a separate line for each IP address,
#		duplicating the hostnames.
sub join {	#FlatFormat
    my($this, $entry) = @_;
    unless (eval { $entry->isa('Net::LDAP::Entry') } ) {
	return !defined($entry) ? "undefined entry\n" :
	    !ref($entry) ? "entry is scalar: $entry\n" :
	    sprintf("non-entry, type %s\n", ref($entry));
    }
    my $dn = $entry->dn();
		# Auto-switch formats for realm containers.
    $this = FlatFormat->auto('getfmt', $dn) if length($dn) <= length($opt->{b});
    my @lines;				# The eventual return value
    my $L;				# Subscript in per-line arrays
    my $sub;				# Attribute conversion subroutine
    NLINES: {
	my $line = '';			# The line that's being put out
	my $nlines = 0;			# True if additional lines must come out
	$L = scalar(@lines);		# Subscript corresponding to output line
	my %split;			# To recognize a split field
	my($f, $PF, $v, $j);
	foreach $f (@{$this->{fields}}) {
	    $PF = $this->{PF}{$f};	# Per field data and flags
	    $PF = $PF->{split} if $split{$f}++;
			# In the case of multiple values, if the field has the
			# 'r' flag they are joined; otherwise one line is
			# put out for each, and for those fields with only one
			# value (or fewer than the maximum), that value is
			# repeated in each line.  (This is for /etc/hosts,
			# a separate line for each IP address.)  Fields that
			# were omitted from the Entry are put out as ''.
	    my $vals = $entry->get_value($f, asref => 1) || 
		($PF->{N} ? ['(missing)'] : ['']);
	    last if $vals->[0] eq '' && $PF->{o}; # Missing trailing fields
			# Convert the value from LDAP to flat format, if needed.
			# This also makes a local copy so we aren't modifying
			# the array in the Entry.  Also sort to canonical order.
	    $vals = [ sort $conv->convlist($f, 'flat', $vals) ];
			# On the primary key, the instance which goes into the
			# Distinguished Name has to come first.
	    DNFR: {
		last DNFR unless $PF->{k} && @$vals > 1 && $dn =~ /=([^,]+)/;
			# $1 is the primary key in the DN.  $j = its subscript
			# in @$vals.  
		($j) = grep { $vals->[$_] eq $1 } 0..$#{$vals};
		last DNFR if !defined($j) || $j == 0;	# DN key already first
		$v = splice(@$vals, $j, 1);
		unshift(@$vals, $v);
	    }
			# If a width was specified, pad the value(s) with
			# blanks or tabs -- tabs if the following separator
			# is a tab and the width is a multiple of 8.
	    my $w = $PF->{wid};
	    if ($w) {
		foreach $v (@$vals) {
		    if (substr($PF->{sep},0,1) eq "\t") {
			if (length($v) < $w-8 && $w >= 8 && $w % 8 == 0) {
					# Pad with tabs, counting separator tab
			    $v .= "\t" x (int(($w - length($v))/8));
			}
		    } elsif (length($v) < $w) {
			$v .= ' ' x ($w - length($v));	# Pad with blanks
		    }
		}
	    }
	    if ($PF->{r}) {
			# Bizarre kludge in /etc/rpc: field 1 is called "cn"
			# and is the primary key; field 3 is also called "cn"
			# and is multi-repeated.  On output the first value 
			# goes in field 1 and the rest in 3.  
		if (!$PF->{s}) {
		    $v = join($PF->{sep}, @$vals);
		} elsif ($split{$f} <= 1) {
		    $v = $vals->[0];
		} else {
		    $v = join($PF->{sep}, @{$vals}[1..$#$vals]);
		}
	    } else {
		my $k = $#{$vals};		# If 1 value, use in each line
		if ($PF->{s}) {
		    $k = 0;			# Split field, first value here
		} elsif ($k > $L) {		# Repeated, one in each line
		    $k = $L;
		    $nlines++;			# More values -> need more lines
		}
		$v = $vals->[$k];		# This value will be used.
	    }
			# Add the field preceeded by the separator of the
			# previous field.
	    $line .= $PF->{psep} . $v;
	}
	$line .= "\n";
	push(@lines, $line);
	redo NLINES if $nlines;
    }
    @lines;
}

# Joins an entry into a canonical form, suitable for checking if Entries are
# equal.  
#   $class	Name of class (static member function).
#   $entry	The Net::LDAP::Entry object to be mashed, could be undef.
#   Returns	A string of alternating attribute names and value lists.
#		No ending newline.
sub mash {	#FlatFormat
    my($class, $e) = @_;
    return '(undef)' unless defined($e);
    my $res = "DN " . $e->dn();		# The return value
    foreach my $attr ($e->attributes(nooptions => 1)) {
	$res .= ' ' . $attr;
	my $val = $e->get_value($attr, asref => 1);
	$res .= ' [' . CORE::join(' ', @$val) . ']';
    }
    $res;
}

# Merges one Entry into another.  Args:
#   $class	Name of class or ref. to class object (static member function)
#   $eexist	The "more equal" entry; in theory merging is commutative, but
#		this entry is the one that existed before, and that will remain
#		afterward.
#   $entry	Attributes from here are merged into $eexist.  Their DN's
#		should be the same.
#   Returns	Nothing.
sub merge {	#FlatFormat
    my($class, $eexist, $entry) = @_;
    foreach my $f ($entry->attributes(nooptions => 1)) {
	$eexist->add($f => $entry->get_value($f, asref => 1));
    }
}

# Removes extraneous attributes from an Entry, i.e. attributes which are not
# expected for this format.  The People table gets content from both 
# /etc/passwd and /etc/shadow, and we need to not delete one's attributes
# when updating the other.  Also, attributes whose value is '' are removed,
# since this syntax is rejected by the server.  Args:
#   $this	Class object reference
#   $e		Entry to be purged, could be undef.
#   Returns	Entry with only format-relevant attributes.  In most cases 
#		there is no change, and $e is returned, but if any attributes
#		are actually purged then $e is cloned and the clone is 
#		returned.  If $e is undef, then undef is returned.
# WARNING: this subroutine does not actually remove the attributes, it 
# configures the Entry so if Entry->update is called, the server's version will
# lose those attributes.  
sub purge {	#FlatFormat
    my($this, $e) = @_;
    my $cloned;				# True if $e has been cloned.
    return $e unless $e;
    foreach my $f ($e->attributes(nooptions => 1)) {
		# Remove values of ''
	my @toss = grep { $_ eq '' } @{$e->get_value($f, asref => 1)};
	if (@toss) {
	    $e = $e->clone() unless ($cloned++);
	    $e->delete($f, \@toss);
	}
		# Remove the entire attribute if it is irrelevant.
	next if $this->{PF}{$f};	# This attribute goes with this format
	next if $f eq "objectClass";	# objectClass is always required.
	$e = $e->clone() unless ($cloned++);
	$e->delete($f);			# Toss irrelevant attribute
    }
    $e->changetype('modify') if $cloned;
    $e;
}

# Call this static member function to infer the filetype from flat filenames
# and set up the corresponding defaults.
sub default {	#FlatFormat
    my($class) = @_;
    my $rc = 1;			# Eventual return value
	    # If -b was provided, extract the OU.
    if (!$opt->{O} && $opt->{b} && $opt->{b} =~ /ou=(\w+)/i) {
	$opt->{O} = $1;
    }
	    # Infers the filetype from the -c, -i or -o values.  
    my($k, $v);
    $opt->{f} ||= '';			# Avoid errors in error printouts.
    foreach $k (qw(c i o)) {
	last if $opt->{f};		# Exit if we already know the filetype
	my $fname = $opt->{$k} or next;
	$fname =~ s/\.[.\w]*$//;	# Chop off extension(s) and directories
	$fname =~ s/.*\///;
	next unless $preset{$fname};	# Bypass if useless for picking a format
	$opt->{f} = $fname;
    }
    my $ps = $opt->{f} ? &pclone($opt->{f}, 0) : 
	{ BAD => "Can't determine -f (table format)" };
	    # Set -O from the preset, if not already known.
    $opt->{O} ||= $ps->{O};
    if ($ps->{BAD}) {
	$rc = 0;
	barf(2, "FlatFormat->default: %s, using -f '%s'\nStandard -f values: %s\n",
	    $ps->{BAD}, ($opt->{f} || '(unknown)'),
	    CORE::join(' ', sort keys %preset) );
    }
	    # Lacking -b, create it from the OU and BASE from ldap.conf
	    # "eval" because if -f is wrong then -O isn't set giving an 
	    # annoying error message.  
    eval {$opt->{b} = 'ou=' . $opt->{O} . ',' . $opt->{base} } if (!$opt->{b});
    $rc;
}
BEGIN { push(@Opt::defaults, 'FlatFormat', \&default); }

# ======
package CiEntry;
# A table of the values of one attribute of an Entry, indexed for use in
# CommonFile::diffentry().  The main issue here is that matches are case
# insensitive, and we need to detect and work around case conflicts, e.g.
# an attribute has the value 'deKuyper' and also 'Dekuyper'.  According to
# LDAP these are equal and it's an error to try to put both on the value
# list.  Also a value of '' is poisonous.  

BEGIN { *barf = \&Barf::barf; }

use Net::LDAP::Entry;

# Creates a new CiEntry object.  Args:
#   $class	Name of class (CiEntry)
#   $ent	Entry whose attribute is to be indexed.
#   $attr	Name of attribute to be indexed.  If $ent does not have this
#		attribute, zero values will be in the index. 
#   Returns	New object with indices of the attribute's values.
sub new {	#CiEntry
    my($class, $ent, $attr) = @_;
    my $this = bless( {
		attr	=> $attr,	# For debug messages
		vals	=> { },		# Keys are values, case sensitive
		vci	=> { },		# Keys are lower case values, values 
					# are nbr of vals with this lower case
		nci	=> 0,		# Number of case conflicts found
		ndup	=> 0,		# Number of duplicate values found
		nval	=> 0,		# Number of values (incl. duplicates)
	}, $class);
    my $vs = $ent->get_value($attr, asref => 1) || [];
    $this->{nval} = @$vs;
    foreach my $v (@$vs) {
		# Make an index of the values.  
		# Recognize a duplicate value, and bypass case insensitive
		# test which would wrongly trigger on every duplicate.  
	$this->{ndup}++, next if $this->{vals}{$v}++;
		# If 2 values have the same lower case, that's a case conflict.
	$this->{nci}++ if $this->{vci}{lc($v)}++;
    }
    $this;
}

# Extracts the values, working around case conflicts.  Args:
#   $this	Class object ref.
#   \@prev	Ref. to another list of values as returned by vals().  If this
#		is not undef, the union (case insensitive) of it and the values
#		from $this is taken.  
#   Returns	Ref. to list of values.
sub vals {	#CiEntry
    my($this, $prev) = @_;
    my %vci;
    my %add;
    if ($prev) {
	foreach my $v (@$prev) {
	    my $L = lc($v);
	    $add{ $this->{vci}{$L} ? $L : $v }++;
	}
    }
    foreach my $v (keys(%{$this->{vals}})) {
	my $L = lc($v);
	$add{ ($this->{vci}{$L} > 1) ? $L : $v }++ unless $add{$L};
    }
		# At one point a value of '' was showing up, probably due to
		# a bug.  It's an error to create this; cryptic error message
		# is "no values for attribute type".  Toss.  
    delete $add{''};
    [ keys %add ];
}

# Produces differences between two CiEntry objects.  Args:
#   $this	Class object ref. for the "master" entry.
#   $othr	Class object ref. for the "output" entry.
#   $union	If this is true, the objective is to produce the union of the
#		two sets of values; if false, we're trying to change $othr
#		to be equal to $this in its set of values.
#   Returns	A hash ref. with these members:
#		strategy	1 = add individual values 
#				2 = toss individual values
#				4 = total replacement is required. (*)
#				010 = $othr doesn't have attribute so add it.(*)
#				020 = $this lacks attribute so toss it in $othr.
#				040 = neither has the attribute, skip it.
#		add		Hash whose keys are values to be added, or in
#				a total replacement, all the new values.  
#				(indicated by * on the strategy bits).
#		toss		Hash whose keys are values to be removed 
our @diffstr; BEGIN { @diffstr = (040, 010, 020, 0); }
sub diff {	#CiEntry
    my($this, $othr, $union) = @_;
    my %res = ( add => { }, toss => { }, );	# Result
    my ($v, $L, %vci);
		# First handle the cases of adding or tossing everything.
    my $strategy = 
	$diffstr[($this->{nval} ? 1 : 0) + ($othr->{nval} ? 2 : 0)];
		# Total replacement needed if either entry has a case conflict
		# or if the destination has duplicate values.  
		# A duplicate in $this does not prevent differential strategy.
		# Presence of '' as a value in $this prevents differential. 
		# If either entry lacks the attribute ($strategy != 0), bypass 
		# setting up the differential values.
    my $nci = $this->{nci} + $othr->{nci} + $othr->{ndup} + 
	(exists($res{add}{''}) || 0) + $strategy;
		# Assuming a differential strategy will work, identify the
		# values that need to be added or removed.  
    my($e1, $e2) = @_;
    my $list = 'add';
    my $istr = 1;			# Strategy: incremental add or toss
    LISTS: while (!$nci) {
	foreach $v (keys %{$e1->{vals}}) {
	    next if exists($e2->{vals}{$v});	# $e2 already has, don't add it
	    $res{$list}{$v}++;			# Need to add/toss this value.
	    $strategy |= $istr;
			# Bypass if no case conflict in the destination.
			# If we're here, $v isn't in $e2, but if lc($v) is
			# in {vci} then some other case combination is in $e2,
			# which is a case conflict.  
	    next unless exists($e2->{vci}{lc($v)});
	    $nci++;				# Oops, need total replacement.
	    $strategy &= ~3;
	    $strategy |= 4;
	    last LISTS;
	}
	last LISTS if $list ne 'add' || $union;
	$v = $e1; $e1 = $e2; $e2 = $v;		# Exchange $e1 and $e2
	$list = 'toss';
	$istr++;
    }
		# If a total replacement is needed, fill in {add}.  Keep case,
		# except if $this has a case conflict, fold to lower case. 
    if ($strategy & 014) {
	$v = $this->vals();
	$v = $othr->vals($v) if $union;
	if (@$v) {
	    $res{add} = { map {$_, 1} @$v };
	} else {
                # If '' is the only value (as when fields are omitted, which is
		# common), we end up with no values.  Toss the attribute if
		# $othr has it, or skip if it doesn't.
	    $strategy = ($strategy & 4) ? 020 : 0;
	    $res{add} = { };
	}
    }
    $res{strategy} = $strategy;
    \%res;
}

# Turns a CiEntry into a multi-line string with ending newline(s).
# For debugging.
sub as_string {	#CiEntry
    my($this) = @_;
    my $res = sprintf(
	"%s: %d values, %d duplicates, %d case conflicts, values:\n    ", 
	@{$this}{qw(attr nval ndup nci)});
    $res .= join(' ', map {$_, $this->{vci}{lc($_)}} sort keys %{$this->{vals}});
    $res .= "\n";
    $res;
}

# Convers a diff output to a string (static member fcn) (for debugging).
sub printdiff {	#CiEntry
    my($class, $diff) = @_;
    sprintf("str=0%o ADD %s  TOSS %s", $diff->{strategy}, 
	join(' ', keys %{$diff->{add}}), join(' ', keys %{$diff->{toss}}));
}

# ======
package CommonFile;
# Common data members and member functions shared by the I/O channel classes
# The CommonFile object is a hash with these members:
#   type    Textual file type: 'flat', 'ldif', 'ldap'.  
#   fname   Name of the file as given in the -i -c or -o option, or "LDAP table
#	    whatever" for LdapIO.
#   mode    PERL mode string used to open the file: 'r' = read, 'w' = write.
#   FH	    FileHandle reference of the file.  Absent in LdapIO.  
#   eof	    True when all data has been read.
#   stuff   Which parts were already prestuffed: 1 = realm containers, 
#	    2 = content.
#   lineno  Line number in the file which produced the most recent Entry.
#	    1 origin, i.e. the first line is designated "1".
#   fmt	    Ref. to FlatFormat object describing the file format.
#   uniq    True if the primary key is supposed to be unique (as it almost
#	    always is).
#   E       A hash ref, keys are Distinguished Names in the various Entries.
#	    Values are Entry objects, either prestuffed or written out.
#   keys    Ref. to array of DN's in {E}.  It gets added to or cleared.  The
#	    outermost (biggest) container comes last, and leaf objects are
#	    in reverse lexical order, so you can pop the array.

BEGIN { *opt = \$Opt::opt; *barf = \&Barf::barf; }

use Net::LDAP::Entry;
use FileHandle;

# Constructor.  Args:
#   $class	Name of class (CommonFile)
#   $fmt	Ref. to FlatFormat object giving the format of the file.
#   $mode	Mode string to open the file, 'r' or 'w'.
#   $fname	Filename to be opened.  Undef to not open (for LdapIO), in 
#		which case the caller will set $this->{fname} later.
#   Returns	New class object
sub new {	#CommonFile
    my($class, $fmt, $mode, $fname) = @_;
    barf(020, sub { "CommonFile->new (for %s) opening %s (%s)\n", $class, ($fname || '(none)'), $mode });
    my $this = bless({
		fname	=> $fname,
		mode	=> $mode,
		fmt	=> $fmt,
		E	=> { },
		keys	=> [ ],
		FH	=> myopen($mode, $fname),
		lineno	=> 0,
		eof	=> 0,
		stuff	=> 0,
		uniq	=> $fmt->{PF}{$fmt->{prikey}}{U} ? 0 : 1,
	}, $class);
    $this;
}

use IO::Handle;
use IO::File;

# Open a file, with some special contingencies.  (Ordinary subroutine.)  Args:
#   $mode	'r' or 'w' for the direction of I/O.
#   $fname	Name of file to open, with these special cases:
#		'-' replicates (fdopen) STDOUT or STDIN.
#		undef causes nothing to be opened, silently.
#   Returns	IO::Handle open on the file, or undef if $fname is undef.
#		If there was an error opening, this subroutine barfs fatally. 
sub myopen {	#CommonFile
    my($mode, $fname) = @_;
    my $FH;
    if (!defined($fname)) {
	# Not opening any file, return undef
    } elsif ($fname ne '-') {
	$FH = IO::File->new($fname, $mode);
    } elsif ($mode eq 'w') {
	$FH = IO::Handle->new();
	$FH->fdopen(\*STDOUT, 'w');
    } else { # $mode eq 'r'
	$FH = IO::Handle->new();
	$FH->fdopen(\*STDIN, 'r');
    } 
    barf(3, "Failed to open(%s) %s: %s\n", $mode, $fname, $!)
	if $fname && !$FH;
    $FH;
}

# Each derived class has its own methods for read(), write(), wdiff().

# Takes the union of two entries.  Args:
#   $this	Class object ref.
#   $e1		This Net::LDAP::Entry object is merged into $e2
#   $e2		This one becomes the union, or is modified to become a copy
#		of $e1.
#   $union	If true, $e2 becomes the union of both entries.  If false,
#		it becomes a copy of $e1.  This happens using attribute-setting
#		methods so when $e2->update() is called, only altered 
#		attribute values will be sent to the server, not replacing the
#		whole entry.  
#   Returns	Nonzero if any values were changed, otherwise 0.
sub union {	#CommonFile
    my($this, $e1, $e2, $union) = @_;
    my $rc = 0;				# Return value, count of changes
    my $PF = $this->{fmt}{PF};
    my @attrs = FlatFormat->auto('attrs', $e1);
                # Compare each attribute of $e1 with $e2.  Only attributes
		# relevant to the current format (plus objectClass).  OK to
		# retrieve an attribute that the Entry doesn't have.
    foreach my $attr (@attrs) {
		# Compare the values of this attribute.
	my $cie1 = CiEntry->new($e1, $attr);
	my $cie2 = CiEntry->new($e2, $attr);
	my $cdiff = $cie1->diff($cie2, $union && $PF->{$attr}{u});
	my $s = $cdiff->{strategy};
		# Execute the strategy.
		# If neither entry has the attr, skip it.  If their values are
		# identical, skip it.
	next if $s == 0 || $s & 040;
	$rc++;					# Yes a difference was found.
	    # $e1 lacks it, toss on $e2
	$e2->delete($attr), next if ($s & 020);
	    # Total replacement required
	$e2->replace($attr, [ sort keys %{$cdiff->{add}} ]), next if ($s & 4);
	    # Remove unwanted values differentially.
	$e2->delete($attr, [ sort keys %{$cdiff->{toss}} ]) if ($s & 2);
	    # $e2 lacks $attr, add all values.  Or, differential addition.
	$e2->add($attr, [ sort keys %{$cdiff->{add}} ]), next if ($s & 011);
    }
    $rc;
}

# Produces a list of names of the realm containers (from $opt->{b}).
# Bugfix: you need the realm container corresponding to $opt->{b}, and the OU,
# but not more general containers, because there is no database that could
# contain
# them.
#   $class	Class name or object ref (static member function)
#   Returns	Ref. to a list of Distinguished Names, outermost (most 
#		general) first.
sub containerdn {	#CommonFile
    my($class) = @_;
    my @dns;				# The return value(s)
		# First get the realm containers including the OU.
    my $dn = '';
    my @dn = reverse split(/,/, $opt->{b});
    foreach my $key (@dn) {
	$key .= ',' if $dn;
	substr($dn, 0, 0) = $key;
	push(@dns, $dn);
    }
    splice(@dns, 0, scalar(($dns[-1] =~ /ou=/) ? @dns - 2 : @dns - 1));
    \@dns;
}

# Produces a list of synthetic Entries for realm containers (from $opt->{b}).
#   $class	Class name or object ref (static member function)
#   Returns	Ref. to a list of Entries, outermost (most general) first.
sub containerent {	#CommonFile
    my($class) = @_;
    my @entries;			# Return value
    my $lineno = -1;
    my $dns = $class->containerdn();
    foreach my $dn (@$dns) {
	my($attr, $val) = split('[=,]', $dn, 3);
	my $oclass = $FlatFormat::attr2obj{$attr};
	my $classes = [ split(' ', $preset{$oclass}{t}) ];
	my $e = Net::LDAP::Entry->new($dn, 
	    $attr => $val, objectClass => $classes);
	$e->{util_lineno} = $lineno--;	# Cowboy programming, save line number
	push(@entries, $e);
    }
    \@entries;
}

# Common code for pre-stuffing entries.  Args:
#   $this	Class object ref.
#   $replace	0 = a duplicate entry is an error and is skipped.
#		1 = take union if U flag is present, error and skip if not.  
#		2 = take union if U flag, otherwise new entry replaces old one.
#		3 = new entry replaces old, whether or not U flag is there.
#   \@entries	Each of these in order is filed in the {E} member and its
#		Distinguished Name is prepended to {keys}.  It's more efficient
#		to do a lot of Entries at once.
#   Returns	1 if everything worked, 0 if error (duplicate entries without
#		U flag on primary key field in the format).
sub stuffcom {	#CommonFile
    my($this, $replace, $ents) = @_;
    my $rc = 1;				# Eventual return code
    my $E = $this->{E};
    my $PF = $this->{fmt}{PF}{$this->{fmt}{prikey}};	# Primary key info
    my $eflg = $PF->{E};				# Extract E and U flags
    my $uflg = (qw(0 1 1 0))[$replace] && !$this->{uniq}; # From $PF->{U};
    my @keys;
    foreach my $entry (@$ents) {
	my $dn = $entry->dn();
	my $ex = $E->{$dn};		# If an entry already exists
	if (!defined($ex)) {
	    $E->{$dn} = $entry;		# First time we've seen this entry
	    push(@keys, $dn);
	} elsif ($uflg) {
	    $this->union($entry, $ex, 1); # Take union with existing entry
	} elsif ($replace >= 2) {
	    $E->{$dn} = $entry;		# Incoming entry replaces existing one
	    # and don't add it a 2nd time to the key list.
	} else {
	    $rc = 0;
	    $opt->{errcode} |= 4;
	    barf(2, "Duplicate DN %s (skipped) at line %d, keeping line %d\n", 
		$dn, ($entry->{util_lineno} || -1), ($ex->{util_lineno} || -1));
	    LdifFile->print(\*STDERR, $entry, "Failing entry:\n");
	}
    }
    unshift(@{$this->{keys}}, reverse @keys) if @keys;
    $rc;
}

# Reads the entire table and saves Entries in the {E} member.  This version
# is shared between FlatFile and LdifFile; LdapIO has its own.  Args:
#   $this	Class object ref.
#   $which	Sum of bits: 1 = include realm containers, 2 = read the
#		content (leaf nodes).  If read from a file, the containers
#		are always synthetic.  Interlocked to be idempotent.
#   Returns	Nothing.
sub prestuff {	#CommonFile
    my($this, $which0) = @_;
    my $which = $which0 & ~$this->{stuff};	# Each set is only done once
    $this->{stuff} |= $which;
    barf(0160, sub {"CommonFile(%s) prestuff(%d) (will do %d)\n    tree %s\n", 
	$this->{fname}, $which0, $which, $opt->{b}});
    my $nkey = scalar(@{$this->{keys}});
    my $dk;
		# Stuff the realm containers (if requested).
    if ($which & 1) {
	$this->stuffcom(0, $this->containerent());
	$dk = scalar(@{$this->{keys}}) - $nkey;
	$nkey += $dk;
	barf(0160, sub {"    Prestuffed %d realm containers, %d total\n", 
	    $dk, $nkey});
    }
		# Read the input file and save its content.
    if ($which & 2) {
	my($entry, @entries);
	while ($entry = $this->read(1)) {
	    push(@entries, $entry) if ref($entry);	# Bypass error returns
	    barf(2, "Giving up due to -B switch (CommonFile::prestuff)\n"), 
		last if $opt->{errcode} && $opt->{B};
	}
	$this->stuffcom(1, \@entries);
	$dk = scalar(@{$this->{keys}}) - $nkey;
	$nkey += $dk;
	barf(0160, sub {"    Prestuffed %d content Entries, %d total\n", 
	    $dk, $nkey});
    }
    barf(0160, sub {"    Already prestuffed everything, %d Entries\n", 
	    $nkey} ) unless $which;
}

# Returns the prestuffed entry with a particular Distinguished Name.  Args:
#   $this	Class object reference.
#   $dn		Targeted Distinguished Name as a string, or Entry ref from
#		which the DN is obtained.
#   Returns	Entry from the LDAP table, or undef if DN is not found.
sub getent {	#CommonFile
    my($this, $dn) = @_;
    my $dnsub = eval {$dn->can('dn')};
    $dn = &$dnsub($dn) if $dnsub;	# Skip this if it's already a string.
    $this->{E}{$dn};
}

# Creates an Entry containing the differences between $e1 and $e2 which, when
# its update method is called, will change $e2 to be a copy of $e1.  (Or if the
# U flag is on an attribute, $e2 and $e1 are unionized.)  Only
# attributes relevant to this output channel's format are considered.  If
# $e1 has an attribute with a value of '', it is deleted from $e2 if present
# (and is not added or replaced).  If an attribute has multiple values, 
# duplicates are removed (case insensitive comparison).  Args:
#   $this	Class object ref. for the output channel.  
#   $e1		Ref. to "input" Entry.
#   $e2		Ref. to "pre-existing" Entry.  Either could be undef if not
#		present in the respective file. Bizarrely, both could be undef.
#   $union	If false, the result turns (a clone of) $e2 into a copy of $e1.
#		If true, the clone becomes the union of $e2 and $e1.  Take this
#		from the primary key's {PF}{U} flag.  
#   Returns	Difference Entry object, or undef if $e1 is the same as $e2.
sub diffentry {	#CommonFile
    my($this, $e1, $e2, $union) = @_;
    my ($attr, $e);
    my $eany = $e1 || $e2;			# Whichever entry is not undef
    my $fmt = $eany ? FlatFormat->auto('getfmt', $eany) : $this->{fmt};
    my $rc = 0;					# Nonzero if any diffs found
    my $dn = $eany ? $eany->dn() : 'both_entries_undef';
    my @attrs = $eany ? FlatFormat->auto('attrs', $eany) : ();
    if (!$eany) {
	# Both Entries are undef, let it return undef.  Sometimes this happens.
    } elsif (!$e1) {
		# No $e1, yes $e2 -> toss Entry on server.  
	$e = Net::LDAP::Entry->new($dn);
	$e->changetype('delete');
	$rc++;
    } elsif (!$e2) {
                # No $e2 -> add $e1.  But in some cases (/etc/rpc) there are
		# duplicate values that we can't change in the flat file, case
		# conflicts, and illegal values of ''.  Get rid of them now.
	$e = Net::LDAP::Entry->new($dn);
	$e->changetype('add');
	foreach $attr (@attrs) {
	    my $cie1 = CiEntry->new($e1, $attr);
	    my $vals = $cie1->vals();
	    $e->add($attr, $vals) if @$vals;	# Could end up with 0 values
	}
	$rc++;
    } else {
		# Both entries exist.  Need to modify $e2 to be like $e1.
		# Or if the primary key has the U flag, merge $e1 into $e2
		# taking their union (if requested).  
	$e = $e2->clone();
	$e->changetype('modify');
	$rc += $this->union($e1, $e, $union);
    }
    $rc ? $e : undef;
}

# ======
package FlatFile;
use base qw(CommonFile);
# This package handles translation between LDAP tables and flat files. 
# See CommonFile for object hash members.  

BEGIN { *opt = \$Opt::opt; *barf = \&Barf::barf; }

# Constructor.  Args:
#   $class	Name of class (FlatFile)
#   $fmt	Ref. to FlatFormat object giving the format of the file.
#   $mode	Mode string to open the file, 'r' or 'w'.
#   $fname	Filename to be opened.
#   Returns	New class object
sub new {	#FlatFile
    my($class) = shift @_;
    my $this = $class->SUPER::new(@_);
    $this->{type} = 'flat';
    $this;
}

# Reads the flat file.  Args:
#   $this	Class object reference.
#   $nocache	Do not return any cached entries (for prestuff).
#   Returns	An Entry object, or undef at end of file.  This subroutine
#		takes care of error messages that prevent production of a
#		valid Entry, and complaints about a primary key that is not
#		unique in the flat file, but conflicts with the LDAP table
#		can't be recognized.  
sub read {	#FlatFile
    my($this, $nocache) = @_;
		# If there are remaining prestuffed entries, return them first.
    my $entry = (!$nocache && @{$this->{keys}}) ? 
				$this->{E}{pop(@{$this->{keys}})} : undef;
    my $dbmsg = "EOF\n";
    my $line = '';
    my $line1;
    while (!$this->{eof}) {
	last if $entry;
	$this->{lineno}++;
	$line1 = $this->{FH}->getline();
	if (defined($line1)) {
	    $line .= $line1;
	} else {
	    ++$this->{eof};
	    last if $line eq '';
	}
			# A line ending in backslash newline means that the
			# next line should be joined to it, replacing the \\\n
			# with one blank.  (Only used for netgroup.)
	if (substr($line, -2) eq "\\\n") {
	    substr($line, -2) = ' ';
	    redo;
	}
			# Turn the line into a LDAP entry.
	$entry = $this->{fmt}->split($line, $this->{lineno});
	$line = '';			# Lose line, if it gets tossed.
	if (ref($entry)) {
	    barf(0100, "FlatFile->read: %s", $line);
	    my $rdn = $entry->get_value($this->{fmt}{prikey});
	    if ($this->{E}{$rdn} && $this->{uniq}) {
		barf(2,"line %d: duplicate key '%s' skipped, earlier at line %d\n", 
		    $this->{lineno}, $rdn, $this->{E}{$rdn});
	    } else {
		$this->{E}{$rdn} = $this->{lineno};
		last;		# Accept this entry.
	    }
	} elsif ($entry ne '') {
	    barf(2, "line %d: %s\n", $this->{lineno}, $entry); #(show error msg)
	    $opt->{errcode} |= 4;
	    $dbmsg = "(input error)\n";
	    last if $opt->{B};
	    # (return undef instead of an Entry object)
	}
    }
    barf(0100, sub { "FlatFile->read: %s", 
	($entry ? $this->{fmt}->join($entry) : $dbmsg)});
    return $entry;
}

# Returns the line number that produced the most recent Entry.  Args:
#   $this	Class object reference.
#   Returns	An integer.
sub lineno {	#FlatFile
    $_[0]->{lineno};
}

# Writes out an entry.  Args:
#   $this	Class object reference.
#   $e1		Ref. to Entry object to be written out.
#   $e2		Existing content (should be impossible on a flat file).
#   $ediff	Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical.  (Not used here.)
#   Returns	0 on success, 2 in the unlikely event of failure.
sub write {	#FlatFile
    my($this, $e1, $ediff) = @_;
    my $rc = 0;				# Eventual return code
    $this->{lineno}++;
    my @line = $this->{fmt}->join($e1);
    barf(0140, "FlatFile->write, line %3d: %s", $this->{lineno}, join('    ', @line));
    $this->{FH}->print(@line) or $opt->{errcode} |= ($rc = 2);
    $rc;
}

# Writes out (on $this->{FH}) the difference between 2 entries.  The output is
# like from the "diff" utility: 0, 1 or 2 flat file lines with prefixes telling
# which file they came from.  Args:
#   $this	Class object reference.
#   $e1		Ref. to "input" Entry.
#   $e2		Ref. to "pre-existing" Entry.  Either could be undef if not
#		present in the respective file.
#   $ediff	Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical.  (Not used here.)
#   Returns	0 if entries are identical, 1 if not, 2 for an error.  
our @prefixes; BEGIN { 
    @prefixes = ('X  ', 'X  ', '<< ', 'X  ', '>> ', 'X  ', '<  ', '>  ');
}
sub wdiff {	#FlatFile
    my($this, $e1, $e2, $ediff) = @_;
    my @es = ($e1, $e2);
    my (@eouts, @canons, @exist);
    foreach my $e (@es) {
	push(@exist, $e ? 1 : 0);
	my @eout = $e ? $this->{fmt}->join($e) : ("(undefined entry)\n");
	push(@eouts, \@eout);
	my @canon = @eout;
	foreach $_ (@canon) {
	    $_ =~ s/\s+/ /g;
	}
	push(@canons, join(' ', sort @canon));
    }
    my $rc = ($canons[0] eq $canons[1]) ? 0 : 1;	#0 = identical
    return $rc if !$rc && !($opt->{d} & 0100);
		# Unequal lines need to be printed.  Also if debug flag is on.
    my $ipr = 2*$exist[0] + 4*$exist[1];
    foreach my $eo (@eouts) {
	my $pfx = $prefixes[$ipr++];
	next if substr($pfx,0,1) eq 'X';
	foreach my $line (@$eo) {
	    $this->{FH}->print($pfx . $line) or $opt->{errcode} |= ($rc = 2);
	}
    }
    $rc;
}

# ======
package LdifFile;
use base qw(CommonFile);
# This package reads/writes LDAP Entries in LDIF format.  
# The LdifFile object is a hash with these members:
#   fname   Name of the LDIF file as given in the -i or -o option.
#   mode    PERL mode string used to open the file: 'r' = read, 'w' = write.
#   fmt	    FlatFormat object describing the (non-LDIF) file format.
#   FH	    FileHandle open on {fname} (not to be used independent of {FD}).
#   FD	    LDIF object reference, connected to {FH} which is open on {fname}.
#   eof	    True if file is at the end (input only).  
#   E	    Keys are RDNs seen in the returned Entries (for uniqueness
#	    checking), values are 1.  
#   lineno  Line number in the file which produced the most recent Entry. 1
#	    origin, i.e. the first line is designated "1".  Sorry,
#	    current_lines() etc. are documented in Net::LDAP::LDIF but not
#	    actually implemented, so we can't keep track of the line number.
#   linedelta  Number of lines in the most recent entry (to be added to
#	    {lineno} on the next I/O operation).  

BEGIN { *opt = \$Opt::opt; *barf = \&Barf::barf; }

use Net::LDAP::LDIF;
use FileHandle;

# Constructor.  Args:
#   $class	Name of class (LdifFile)
#   $fmt	Ref. to FlatFormat object 
#   $mode	Mode string to open the file, 'r' or 'w'.
#   $fname	Filename to be opened.
#   Returns	New class object
sub new {	#LdifFile
    my($class, $fmt, $mode, $fname) = @_;
    my $this = $class->SUPER::new($fmt, $mode, $fname);
    $this->{type} = 'ldif';
    my @fdargs = (qw(encode base64  sort 1  onerror), undef);
    push(@fdargs, qw(change 1)) if $opt->{c} && $mode eq 'w';
			# (Not sure if we need raw => qr/regexp/ )
    $this->{FD}	= Net::LDAP::LDIF->new($this->{FH}, $mode, @fdargs);
    barf(3, "Failed to initialize LDIF object from %s: %s\n",
	$fname, $this->{FD}->error()) if $this->{FD}->error();
		# It could happen that the file starts out at EOF, e.g. a file
		# with only blank lines and comments, or 0 length.
    $this->{eof} = $this->{FD}->eof();
    $this;
}

# Reads the LDIF file.  Args:
#   $this	Class object reference.
#   $nocache	Do not return any cached entries (for prestuff).
#   Returns	An Entry object, or undef at end of file.  This subroutine
#		takes care of error messages that prevent production of a
#		valid Entry, and complaints about a primary key that is not
#		unique in the LDIF file.
sub read {	#LdifFile
    my($this, $nocache) = @_;
		# If there are remaining prestuffed entries, return them first.
    my $entry = (!$nocache && @{$this->{keys}}) ?
				$this->{E}{pop(@{$this->{keys}})} : undef;
    my $FD = $this->{FD};
    my $dbmsg = "EOF\n";
    while (!$this->{eof}) {
	last if $entry;
	$this->{lineno}++;			# Actually a count of entries
	$entry = $FD->read_entry();		# Read the next Entry.
	$this->{eof} = $FD->eof();
	if ($FD->error()) {
	    barf(2, "LDIF input error in entry nbr %d: %s\n", 
		$this->{lineno}, $FD->error());
	    $opt->{errcode} |= 4;
	    $dbmsg = '(input error)';
	    undef $entry;
	    last if $opt->{B};
	} elsif (defined($entry)) {
	    $entry->{util_lineno} = $this->{lineno};	# Cowboy pgmg, save line number
	    barf(0100, sub { "LdifFile->read (entry %2d): %s", $this->{lineno}, $this->{fmt}->join($entry) });
	    my $dn = $entry->dn();		# Distinguished Name of Entry
	    my $oldent = $this->{E}{$dn};	# Seen this one before?
	    if ($oldent) {
		barf(2, 
		    "LDIF duplicate DN (%s) (skipped) at entry %d, keeping %d\n", 
		    $dn, $this->{lineno}, $oldent->{util_lineno});
		$opt->{errcode} |= 4;
		$dbmsg = "(duplicate entry)\n";
		undef $entry;
	    } else {
		$this->{E}{$dn} = $entry;	# Remember where we saw it
		last;				# Accept this Entry
	    }
	}
    }
    barf(0100, sub { "LdifFile->read: %s", ($entry ? $this->{fmt}->join($entry) : $dbmsg)});
    $entry;
}

# Returns the line number that produced the most recent Entry.  Args:
#   $this	Class object reference.
#   Returns	An integer.
sub lineno {	#LdifFile
    $_[0]->{lineno};
}

# Writes out an entry.  Args:
#   $this	Class object reference.
#   $e1		Ref. to Entry object to be written out.
#   $e2		Existing content (should be impossible on a LDIF file).
#   $ediff	Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical.  (Not used here.)
#   Returns	0 on success, 2 in the unlikely event of failure.
# (write_entry() does not store the output lines so current_lines can return
# them, so no line numbers on output.)
sub write {	#LdifFile
    my($this, $e1, $ediff) = @_;
    my $FD = $this->{FD};
    $this->{lineno}++;
    barf(0140, sub { "LdifFile->write (entry %3d): %s", $this->{lineno}, $this->{fmt}->join($e1) });
    $FD->write_entry($e1);	# Error return if any is not documented
    my $rc = 0;
    barf(2, "LDIF output error (entry %d): %s\n", $this->{lineno}, 
				$FD->error()), ($opt->{errcode} |= ($rc = 2))
						if $FD->error();
    $rc;
}

# Writes out (on $this->{FH} through $this->{FD}) the difference between 2
# entries so $e2 could be updated to become a copy of $e1.  The output is LDIF
# of a difference Entry, which could be fed to ldapmodify.  Args:
#   $this	Class object reference.
#   $e1		Ref. to "input" Entry.
#   $e2		Ref. to "pre-existing" Entry.  Either could be undef if not
#		present in the respective file.
#   $ediff	Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical.
#   Returns	0 if entries are identical, 1 if not, 2 for an error.  
sub wdiff {	#LdifFile
    my($this, $e1, $e2, $e) = @_;
    my $FD = $this->{FD};
    my $rc;
    if (!defined($e)) {
	$rc = 0;
	if ($opt->{d} & 0100) {
	    barf(0100, "# DN = %s identical entries\n", $e1->dn());
	    $this->{FH}->printf("# DN = %s identical entries\n", $e1->dn());
	}
    } else {
	barf(0140, sub { "LdifFile->wdiff:\n  < %s  > %s", 
	    $this->{fmt}->join($e1), $this->{fmt}->join($e1)} );
	$FD->write_entry($e);		# Error return if any is not documented
	$rc = 1;
	barf(2, "LDIF output error (entry %d): %s\n", $this->{lineno}, 
				$FD->error()), ($opt->{errcode} |= ($rc = 2))
						if $FD->error();
    }
    $rc;
}

# Prints an entry as LDIF, for debugging.  Args:
#   $class	Name of class (static member function)
#   \*FH	Open filehandle or type glob for the output file.
#   $entry	Ref. to the Net::LDAP::Entry object to be printed.
#   $title	A line to come before the entry (optional).
sub print {	#LdifFile
    my($class, $FH, $entry, $title) = @_;
    $FH->print($title) if $title;
    $FH->print("Attrs: '", join("', '", sort $entry->attributes()), "'\n");
    my $FD = Net::LDAP::LDIF->new($FH, 'a', onerror => 'warn', sort => 1);
    $FD->write_entry($entry);
    # (Automatically detaches from $FH when finished)
}

# ======
package LdapIO;
use base qw(CommonFile);
# This package obtains LDAP Entries from a LDAP table, or updates the table
# with the new Entry content.  Package object data members:
#   LDAP	LDAP package object ref.
#   ermsg	Error message from last failing operation
#   search	Message object representing the search results.
#   E		Ref. to hash keyed by Distinguished Name containing the
#		Entries returned by the search.  
#   keys	Ref. to array of Distinguished Names; the last one must be
#		created first in an empty table.  

BEGIN { *opt = \$Opt::opt; *barf = \&Barf::barf; }

use Net::LDAP;
use Net::LDAP::Entry;
use FileHandle;
use Authen::SASL;

our %openopt;					# Options for LDAP connection
our %bindopt;					# Options for bind operation
our %tlsopt;					# Options for starting TLS

# Opens a connection to the LDAP server.  Args:
#   $class	Name of class (LdapIO).
#   $fmt	Ref. to FlatFormat object 
#   $mode	Mode string to open the file, 'r' or 'w'.
#   $fname	Filename to be opened (always undef for LdapIO)
#   Returns	LdapIO object with an open connection.
sub new {	#LdapIO
    my($class) = shift @_;
    my $this = $class->SUPER::new(@_);
    $this->{type} = 'ldap';
    $this->{fname} = "LDAP table ${$opt}{f}";
    $this->{ermsg} = '';
		# Connect to the LDAP server(s).
    $openopt{onerror} = undef;		# Failing method will return undef
    my $HOST = delete $openopt{HOST};
    barf(020, sub { "LdapIO->new: connect to %s\n", join(' ', @$HOST) });
    $this->{LDAP} = Net::LDAP->new($HOST, %openopt);
    $openopt{HOST} = $HOST;
    barf(3, "Can't connect to LDAP server (%s): %s\n", 
	$opt->{H}, $this->{ermsg}) unless $this->{LDAP};
		# Start TLS when needed for security (which it usually is).
    my $mesg;
    TLS: {
	barf(030, "Not doing StartTLS\n"), last unless $tlsopt{verify};
	barf(030, "LdapIO->new starting TLS\n");
	$mesg = $this->{LDAP}->start_tls(%tlsopt);
	last unless $mesg->is_error();
	barf(3, "Failed to start TLS: %s\n", $mesg->error());
    }
		# Bind (authenticate) to the server.
    my $BINDDN = delete $bindopt{BINDDN};
    barf(020, "Bind to server as %s\n", ($BINDDN || 'anonymous'));
    $mesg = $BINDDN ? $this->{LDAP}->bind($BINDDN, %bindopt) : 
			$this->{LDAP}->bind();
    barf(3, "Failed to bind to LDAP server as %s: %s\n",
	    ($BINDDN || 'anonymous'), $mesg->error())
		if $mesg->is_error();
    $bindopt{BINDDN} = $BINDDN if $BINDDN;
    barf(020, "LdapIO->new finished\n");
    $this;
}

# Reads the next Entry and returns it, or undef when all have been read.
sub read {	#LdapIO
    my($this) = @_;
    my $dn = pop(@{$this->{keys}});
    return undef unless $dn;
    my $entry = $this->{E}{$dn};
    barf(0100, sub { "LdapIO->read: %s", ($entry ? $this->{fmt}->join($entry) : "EOF\n")});
    $entry;
}

# Reads the entire table and saves Entries in the {E} member.  Args:
#   $this	Class object ref.
#   $which	Sum of bits: 1 = include realm containers, 2 = read the
#		content (leaf nodes).  Interlocked so each is only done once.
#   Returns	Nothing.
sub prestuff {	#LdapIO
    my($this, $which0) = @_;
    my $which = $which0 & ~$this->{stuff};	# Each set is only done once
    $this->{stuff} |= $which;
    barf(0160, sub {"LdapIO(%s) prestuff(%d) (will do %d)\n    tree %s\n", 
	$this->{fname}, $which0, $which, $opt->{b}});
    my $nkey = scalar(@{$this->{keys}});
    my $dk;
                # For reading, there has to be something to read.  For writing,
		# we have to know what's already in the table (if anything) in
		# order to update it.  Do two searches: First retrieve the
		# containers representing the search base, then the subtree
		# under the base.  It's not an error if the realm container
		# hasn't been created yet.
    my ($mesg, @entries);
    SEARCH: {
		# First get the realm containers.  (If requested.)
	my($dn, $dns);
	if ($which & 1) {
	    $dns = $this->containerdn();
	    pop @$dns;			# Lose the OU container.
	} else {
	    $dns = [ ];
	}
	foreach my $dn (@$dns) {
	    $mesg = $this->{LDAP}->search(base => $dn, scope => 'base',
			filter => '(objectClass=*)');
	    barf(2, "Search failed for %s: %s (ignored)\n", 
		$opt->{b}, $mesg->error()), last if $mesg->is_error();
	    my @entries = $mesg->entries();
	    barf(2, "Search for DN=%s returned %d entries, must be 1\n",
		$dn, scalar(@entries)), last SEARCH unless @entries == 1;
	    barf(2, "Sought DN=%s, got DN=%s, this is wacko\n"), last SEARCH
		if $entries[0]->dn() ne $dn;
	    my $lineno = 0;
	    foreach my $e (@entries) {
		$e->{util_lineno} = ++$lineno;	# Cowboy pgmg, save line number
	    }
	    $this->stuffcom(0, \@entries);
	    $dk = scalar(@{$this->{keys}}) - $nkey;
	    $nkey += $dk;
	    barf(0160, sub {"    Prestuffed %d realm containers, %d total\n", 
		$dk, $nkey});
	}
		# Now search for the content, including the OU container.
	last SEARCH unless $which & 2;
	$mesg = $this->{search} =
		$this->{LDAP}->search(base => $opt->{b}, scope => 'sub',
		    filter => '(objectClass=*)');
	barf(2, "Search failed for OU = %s: %s (ignored)\n", 
	    $opt->{b}, $mesg->error()), last
		if $mesg->is_error();
	my $entries = [ $mesg->entries() ];
		# Toss the pseudo-OU container unless full tree is being done
		# in this execution.
	if (!($which & 1)) {
	    foreach my $i (0..$#{$entries}) {
		next if $entries->[$i]->dn() ne $opt->{b};
		splice(@$entries, $i, 1);
		last;
	    }
	}
	$this->stuffcom(1, $entries);
	$dk = scalar(@{$this->{keys}}) - $nkey;
	$nkey += $dk;
	barf(0160, sub {"    Prestuffed %d content Entries, %d total\n", 
	    $dk, $nkey});
    }
    barf(0160, sub {"    Already prestuffed everything, %d Entries\n", 
	    $nkey} ) unless $which;
}

# Sends out an entry for updating.  Args:
#   $this	Class object reference.
#   $e1		The entry to be updated.  Undef means remove $e2 from the
#		server.  
#   $e2		Existing content for this Distinguished Name, or undef if none.
#   $ediff      Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical, in which case writing is
#		skipped.  This subroutine saves $ediff in $this->{E}{$dn}.
#   \@rcs       What to return in these cases, array of 3 items: [0] if entries
#		are equal, [1] if unequal, [2] on a write failure.  [3] is the
#		name (text) of the caller, for error messages.
#   Returns	The value from \@rcs according to the outcome.
sub writecore {	#LdapIO
    my($this, $e1, $e2, $e, $rcs) = @_;
    my $rc = 0;				# Subscript in @$rcs
    UPDATE: {
	barf(2, sub {("%s%s %s\n", ($opt->{n} ? "would " : ''), 
		($e ? $e->changetype() : 'skip'), 
		(($e1 || $e2) ? ($e1 || $e2)->dn() : 'nonexistent entry'))})
			if (($opt->{n} || $opt->{d} & 040) && $e) 
				|| $opt->{d} & 0100;
	last if !$e;				# If entries already identical
	$rc = 1;
	last if $opt->{n};			# Bypass updating if -n
	my $mesg = $e->update($this->{LDAP});
	if ($mesg->is_error()) {
	    $opt->{errcode} |= ($rc = 2);
	    barf(2, "Failed to %s %s: %s\n", 
		$e->changetype(), $e->dn(), $mesg->error());
	    LdifFile->print(\*STDERR, $e, "Failing entry:\n");
	} else {
		# Seems to be OK.  Update prestuff stuff.  Example: the flat
		# file has multiple rows for the same primary key, and they
		# produce multiple values for some attribute.
	    $this->stuffcom(2, [$e]);
	}
    }
    $rcs->[$rc];
}

# Writing an Entry to the table.  (Use wdiff to remove an Entry.)  Args:
#   $this	Class object reference.
#   $e1		The entry to be updated.  
#   $e2		Existing content of the table (not actually used).
#   $ediff	Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical.
#   Returns	0 if write succeeded, 2 if failed.
my @writerc; BEGIN { @writerc = (0, 0, 2, 'write'); }
sub write {	#LdapIO
    my($this, $e1, $e2, $ediff) = @_;
    $this->writecore($e1, $e2, $ediff, \@writerc);
}

# Update the table.  Specifically this is called to remove Entries.  Args:
#   $this	Class object reference.
#   $e1		The entry to be updated.  Undef means remove $e2 from the
#		server.  
#   $e2		Existing content for this Distinguished Name, or undef if none.
#   $ediff	Difference between $e1 and $e2 (result of diffentry($e1,$e2)).
#		Could be undef if they are identical.
#   Returns	0 for equal Entries, 1 if unequal, 2 for an error.
my @wdiffrc; BEGIN { @wdiffrc = (0, 1, 2, 'wdiff'); }
sub wdiff {	#LdapIO
    my($this, $e1, $e2, $ediff) = @_;
    $this->writecore($e1, $e2, $ediff, \@wdiffrc);
}

# Stuff an option in override order.  Args:
#   \%hash	A hash of options
#   $member	Key in that hash where the value goes
#   @values	The first of these which is defined is stored in that member.
#		Except if the member is already set, it is not changed.
#   Returns	The value in the hash (undef if it didn't get set)
sub oropt {	#LdapIO
    my($hash, $member) = splice(@_, 0, 2);
    foreach my $val (@_) {
	last if defined($hash->{$member});
	$hash->{$member} = $val;
    }
    $hash->{$member};
}

# Initializes the various connection parameters.
sub default {	#LdapIO
    my($class) = @_;
    my $rc = 1;				# The eventual return value
		# Decide which host(s) to connect to and convert to a list.
    HOST: {
	last if $opt->{H};		# -H URI list was given, use it.
	if ($opt->{h}) {		# -h host was given, convert to URI
	    $opt->{H} = "ldap://" . $opt->{h};
	    $opt->{H} .= ':' . $opt->{p} if $opt->{p};
	    last;
	}
	&oropt($opt, 'H', $opt->{uri}, $opt->{host}) and last;
	barf(2, "Can't discover server's URI, fix %s (HOST or URI parm) or specify -H\n", 
	    join(' ', @{$opt->{conf}}));
	$rc = 0;
	$opt->{H} = "ldap://unknown-server"; # Avoid messages about undef string
    }
    $openopt{HOST} = [ split(' ', $opt->{H}) ];
		# Extract generic open options from LDAP configuration.
    &oropt(\%openopt, 'timeout', $opt->{timeout});
		# Extract bind options from the LDAP configuration.  If 
		# there are no options, binding is anonymous.
		# Password extensions: BINDPW gives the password, and 
		# BINDPWFILE gives the name of a file to read it from.
    PASSWD: {
	last unless $opt->{x};		# Password is only for simple auth.
	my $FD;
		# If we're reading the password from the TTY.
		# The newline at the end is not part of the password. 
	FROMTTY: {
	    last unless ($opt->{W});
	    $FD = FileHandle->new("/dev/tty", '+>') or 
		barf(3, "Can't read password from /dev/tty: %s\n", $!);
	    $FD->print("Password: ");
	    $FD->flush();
	    system "stty -F /dev/tty -echo";	# Turn off echo
	    chomp ($bindopt{password} = <$FD>);
	    system "stty -F /dev/tty echo";	# Turn on echo
	    barf(3, "Error reading password from /dev/tty: %s\n", 
		$! || 'empty password') unless $bindopt{password};
	    $FD->close();
	    last PASSWD;
	}
		# If the password is on the command line:
	&oropt(\%bindopt, 'password', $opt->{w}, $opt->{bindpw}) and last;
                # Reading the password from a file.  The ending newline (if
		# present) is included as part of the password.  The file could
		# have multiple lines.
	my $pwfile = $opt->{y} || $opt->{bindpwfile};
	last unless $pwfile;
	$FD = FileHandle->new($pwfile) or do {
		barf(2, "Can't read %s: %s\n", $pwfile, $!);
		$rc = 0;
		last;
	    };
	$bindopt{password} = join('', <$FD>);
	$FD->close();
    }
    ++$bindopt{anonymous} if $opt->{x} && !exists($bindopt{password});
		# For a SASL bind, BINDDN is syntactically required but is
		# ignored by SASL.  For a simple bind, if there is a password
		# then BINDDN is required.  -W counts as having a password. 
		# For a simple bind with no password, BINDDN must be omitted.
    if (!$opt->{x} || $bindopt{password}) {
	&oropt($opt, 'D', $opt->{binddn}, 
	    "uid=$ENV{USER},ou=People,${$opt}{base}");
	$bindopt{BINDDN} = $opt->{D};
    }
		# Handle SASL authentication.  
		# The following advertised options are not actually honored:
		#   -O secprop	SASL security properties
		#   -R realm	SASL realm
                # Check if GSSAPI works -- yes it does.
    if (!$opt->{x}) {
	my $class = "Authen::SASL";
	my (%saslargs, %callbacks);
	&oropt(\%callbacks, 'user', $opt->{U}, $ENV{USER});
	&oropt(\%callbacks, 'auth', $opt->{X}, $callbacks{user});
#OBSOLETE	&oropt(\%callbacks, 'authname', $callbacks{auth}); #For GSSAPI
	&oropt(\%saslargs, 'mechanism', $opt->{Y});
	$saslargs{callback} = \%callbacks;
	$bindopt{sasl} = $class->new(%saslargs);
    }
		# StartTLS option setup.  LdapIO::new should detect if TLS is 
		# required by looking at $tlsopt{verify}; if missing, TLS is 
		# not needed.  TLS is required if requested by the user or
		# if a password was or will be provided.  Basically, required
		# except for anonymous access.  
    if ($opt->{Z} || !$opt->{x} || $opt->{W} || $opt->{y} || $opt->{Y} || 
				$bindopt{password} || $bindopt{binddn}) {
	$tlsopt{verify} = 'require';
	# $tlsopt{checkcrl} = 0;	# Hiss, boo, need to set this up
	# $tlsopt{ciphers} = 'ALL';	# How do we demand strength > 0?
	&oropt(\%tlsopt, 'cafile', $opt->{tls_cacert}) or
	    &oropt(\%tlsopt, 'capath', $opt->{tls_cacertdir}) 
	    or do {
	    barf(2, "Missing TLS parameters (TLS_CACERT or TLS_CACERTDIR)\nneeded in %s\n",
		join(' ', @{$opt->{conf}}));
	    $rc = 0;
	};
    }
    $rc;
}
BEGIN { push(@Opt::defaults, 'LdapIO', \&default); }

# ======
package Activity;
# Represents a LDAP table upon which various activities can be done.  
# Package object data members:
#   base	Distinguished Name elements to be appended to the RDN.
#   format	FlatFormat object describing the flat format file.
#   infd	Ref. to I/O channel object for the main input.
#   cmpfd	Ref. to I/O channel object for the comparison input (-c only).
#   outfd	Ref. to I/O channel object for the output. 
#   stat	Hash of statistics, number of records in these categories:
#		in	Read from infd
#		cmp	Read from cmpfd
#		out	Read from outfd (prestuff)
#		unequal	Number of unequal records incl. missing ones
#		modify	Number of unequal records excluding missing ones
#		add	Records in "in" that aren't in cmp or out
#		delete	Records in cmp or out that aren't in "in"
#		equal	Number of equal records
#		error	Number of write errors (doesn't count read errors)

use Net::LDAP::Entry;

BEGIN { *opt = \$Opt::opt; *barf = \&Barf::barf; }

# Logic for opening files:
#   If we have -c, open {i} and {c} for reading.  {o} if present is a file 
#	opened for output, otherwise output goes to stdout.  Output involves 
#	generating diffs.  
#   Otherwise, open {i} for reading and {o} for writing.  Either one missing
#	means to read or write the LDAP table.  
#   -k means to go through the table and delete/report entries that aren't in
#	the input.  

# Constructor.  Most of the stuff is interpreted from the options.  Args:
#   $class	Name of class (Activity).
#   Returns	New Activity object.
sub new {	#Activity
    my($class) = @_;
    barf(020, "Activity->new starting, table %s\n", 
	($opt->{f} || $opt->{F}));
    my $this = bless({
	    base	=> $opt->{b},
	    format	=> FlatFormat->auto('getfmt', 'xx=xx,' . $opt->{b}),
	    stat	=> { qw(in 0 cmp 0 out 0 unequal 0 modify 0 add 0 delete 0 equal 0 error 0) },
	}, $class);
    my $fmt= $this->{format};
		# Overriding decision whether a file is LDIF format.
    my %opt_L = map {$_, 1} split(//, ($opt->{L} || ''));
		# Open the various I/O channels.
    my %ktl = qw(i infd  o outfd  c cmpfd);
    my($m, $fd);
    while (($m, $fd) = each %ktl) {
	my $M = ($m eq 'o') ? 'w' : 'r';
	$this->{$fd} = $opt->{$m} ?
			# If the option is specified, open the file, as LDIF
			# if the extension so indicates.
	    (($opt->{$m} =~ /\.ldif$/ || $opt_L{$m}) ? 
		LdifFile->new($this->{format}, $M, $opt->{$m}) :
		FlatFile->new($this->{format}, $M, $opt->{$m})) :
			# Option is not specified.  If not comparing, do not
			# open comparison input.
	    ($m eq 'c' && !$opt->{c}) ? undef :
			# If comparing, output goes to stdout by default.
	    ($m eq 'o' && $opt->{c}) ? FlatFile->new($this->{format}, $M, '-') :
			# Otherwise, look at the LDAP table.  
	    LdapIO->new($this->{format}, $M, undef);
		# Bypass further setup (prestuff) for unused channels.
	next unless $this->{$fd};
		# Always prestuff input channel(s), and also the output channel
		# if it's LdapIO.  Reason: for those tables which have multiple
		# flat file records per LDAP Entry, we need to see all the 
		# records before comparing/updating.  On LDAP output we don't
		# replace entire entries, we send diffs to the server, so we
		# need to know what's there already.  
	$this->{$fd}->prestuff(2 + $opt->{a})
	    if $m ne 'o' || (!$opt->{c} && !$opt->{$m});
    }
    barf(020, "Activity->new finished\n");
    $this;
}

# The main thread should create the Opt object, create the Activity object, 
# then call this method.  Return value is 0 for a successful copy, 1 if copy
# failed; or 0 if all entries compare equal, 1 if they don't.  The activities
# are very similar for comparing and copying.  
sub activity {	#Activity
#    return barf(2, "Error reading input data, Giving up due to -B switch\n") 
#	|| 0 if $opt->{errcode} && $opt->{B};
    my($this) = @_;
    my($in1, $in2) = $opt->{c} ? qw(cmpfd infd) : qw(infd outfd);
    my($st1, $st2) = $opt->{c} ? qw(cmp in) : qw(in out);
    my $wdiff = $opt->{c} ? sub {$this->{outfd}->wdiff(@_)} : 
			sub {$this->{outfd}->write(@_)};
    my $rc = 0;			# Return code: number of differences or errors
    my %checkoff;		# Key = DN, value (1 = in cmpfd) + (2 = in infd)
    my $halt;			# For bailing out in case of errors.
    my($e1, $e2, $ediff);
		# Do the activity on records from input #1.
    while ($e1 = $this->{$in1}->read()) {
	next unless eval { $e1->isa('Net::LDAP::Entry') };
	my $dn = $e1->dn();
	$checkoff{$dn} = 1;
	$e2 = $this->{$in2}->getent($dn);
	$checkoff{$dn} += 2 if $e2;
		# Difference between the records on the two channels.
		# Never take their union (take unions during prestuff()).
	$ediff = $this->{$in1}->diffentry($e1, $e2, 0);
		# Count the records in each category.
	$this->{stat}{$st1}++;			# Records in file 1
	$this->{stat}{$st2}++ if $e2;		# Records in file 2
	$this->{stat}{unequal}++ if $ediff;	# All kinds of unequal recs
	my $ct = defined($ediff) ? $ediff->changetype() : 'equal';
	$this->{stat}{$ct}++;			# Mutually exclusive disposition
	if ($opt->{d} & 040 &&	# (0100 printout is handled by read())
			FlatFormat->mash($e1) ne FlatFormat->mash($e2)) {
	    barf(040, "Activity input:\n  < %s  > %s",
		$this->{$in1}{fmt}->join($e1),
		$this->{$in2}{fmt}->join($e2));
	}
	my $r = &{$wdiff}($e1, $e2, $ediff);
	$rc++ if $r;
	$this->{stat}{error}++ if $r >= 2; 
    } continue {
	$halt = $opt->{errcode} && $opt->{B}; 
	barf(2, "Giving up due to -B switch\n"), last if $halt;
    }
    if ($opt->{k} && !$halt) {
	while ($e2 = $this->{$in2}->read()) {
	    my $dn = $e2->dn();
	    next if $checkoff{$dn};
	    $this->{stat}{$st2}++;	# Recs not read in previous phase
	    if ($opt->{d} & 040) {	# (0100 printout is handled by read())
		barf(040, "Activity input (kill):\n >> %s",
		    $this->{$in2}{fmt}->join($e2));
	    }
			# write() can't kill entries, always use wdiff()
	    $ediff = $this->{$in1}->diffentry(undef, $e2);
	    my $r = $this->{outfd}->wdiff(undef, $e2, $ediff);
	    $rc++;			# Killed entries always cause a change
	    $this->{stat}{delete}++;
	    $this->{stat}{error}++ if $r >= 2; 
	} continue {
	    $halt = $opt->{errcode} && $opt->{B}; 
	    barf(2, "Giving up (-k) due to -B switch\n"), last if $halt;
	}
    }
		# Result code:	-c		not -c
		# 0	All records equal	All records written OK
		# 1	Some records unequal	Some I/O errors
    ($this->{stat}{$opt->{c} ? 'unequal' : 'error'} ? 1 : 0) | $opt->{errcode};
}

# Print statistics.  Args:
#   $this	Class object ref.
#   $flag	First argument to barf().
#   Returns	Nothing (it prints the stats itself).
sub printstat {	#Activity
    my($this, $flag) = @_;
    my($in1, $in2) = $opt->{c} ? qw(cmpfd infd) : qw(infd outfd);
    my($st1, $st2) = $opt->{c} ? qw(cmp in) : qw(in out);
    my %descr = (
	equal	=> 'equal records',
	unequal	=> 'unequal records including missing ones',
	modify	=> 'unequal records excluding missing ones',
	add	=> "records in '$st1' and not in '$st2'",
	delete	=> "records not in '$st1' and in '$st2'",
	error	=> "write failures",
	);
    unless ($opt->{c}) {
	$descr{modify}	.= " (modified)";
	$descr{add}	.= " (added)";
	$descr{delete}	.= " (tossed)";
    }
    my %fd = qw(in infd  out outfd  cmp cmpfd);
    my $f;
    foreach $f ($st1, $st2) {
	$descr{$f} = "records in '$f' = " . $this->{$fd{$f}}{fname};
    }
    foreach $f ($st1, $st2, qw(equal unequal modify add delete error)) {
	barf($flag, "%5d  %s\n", $this->{stat}{$f}, $descr{$f});
    }
}

# Print one-line stat summary.  Args:
#   $this	Class object ref.
#   $flag	First argument to barf().
#   Returns	Nothing (it prints the stats itself).
sub print1line {	#Activity
    my($this, $flag) = @_;
    my($in1, $in2) = $opt->{c} ? qw(cmpfd infd) : qw(infd outfd);
    my($st1, $st2) = $opt->{c} ? qw(cmp in) : qw(in out);
    my @descr = qw(
	error	write_failures
	delete	tossed
	add	added
	modify	changed
	equal	equal
	);
    my %fd = qw(in infd  out outfd  cmp cmpfd);
    my ($f, $d, $n);
    my $msg = "updating ${$opt}{f}: ";
    my @msgs;
    while (@descr && (($f, $d) = splice(@descr, -2))) {
	$n = $this->{stat}{$f};
	push(@msgs, "$n $d") if $n;
    }
    push(@msgs, "no changes") if @msgs <= 1;
    $msg .= join(', ', @msgs) . "\n";
    barf($flag, $msg);
}

# Initialization, check consistency of file/table options.
sub default {	#Activity
    my($class) = @_;
    my $rc = 1;
    $opt->{a} ||= 0;			# Make sure $opt->{a} is defined.
    unless ($opt->{i} || $opt->{o} || $opt->{c}) {
	barf(2, "At least one of -i -o -c is required.\n") ;
	$rc = 0;
    }
    if ($opt->{k} && $opt->{o} && !$opt->{c}) {
	barf(2, "-k only works when comparing, or when updating LDAP (no -o file).\n");
	$rc = 0;
    }
    $rc;
}
BEGIN { push(@Opt::defaults, 'Activity', \&default); }

# ======
package main;
# The main thread.

{
    my $opt = Opt->new();
    my $A = Activity->new();
    my $rc = $A->activity();
    $opt->{v} ? $A->printstat(2) : $opt->{q} ? undef : $A->print1line(2);
    exit($rc);
}


1;	#Yes the module loaded.
