#!/usr/bin/perl

=head1 NAME

dh_installnss - enable NSS services

=cut

use strict;
use warnings;
use Debian::Debhelper::Dh_Lib;

our $VERSION = "1.5";

=head1 SYNOPSIS

B<dh_installnss> [S<I<debhelper options>>]

=head1 DESCRIPTION

B<dh_installnss> is a debhelper program that is responsible for injecting
NSS (Name Service Switch) services into B</etc/nsswitch.conf>.

=head1 FILES

=over 4

=item debian/I<package>.nss

Lists the services to inject into B</etc/nsswitch.conf> when a package is
configured and to remove when a package is removed or purged.

Each line in that file should be of the form

C<I<db> I<position> I<service> I<action> I<condition>>

where the fields contain the following pieces of information:

=over

=item C<I<db>>: the NSS database in which the service will be added.
Usually C<hosts>.

=item C<I<position>>: where to add the NSS service.
Possible values are C<first>, C<last>, C<before=I<service>>,
C<after=I<service>>.
The pseudo-position C<remove-only> is used to mark services that are not
going to be added during the installation of the package, but that will
be removed during its removal (e.g., legacy services).

=item C<I<service>>: the name of the NSS service to add.

=item C<I<action>>: optional action specification C<[STATE=ACTION]>.

=item C<I<condition>>: optional set of conditions to better define when
a service should (or should not) be installed.
Only one kind of condition is currently defined:
C<skip-if-present=I<service,service,...>>.

=back

Additionally, text between a C<#> character and the end of line is ignored.

=back

=head1 EXAMPLES

An example F<debian/nss> file could look like this:

    hosts before=dns mdns4
    hosts before=mdns4 mdns4_minimal [NOTFOUND=return]
    hosts remove-only mdns    # In case the user manually added it

After the installation of this package, the original configuration of
the B<hosts> database in B</etc/nsswitch.conf> will change from:

    hosts:    files dns

to:

    hosts:    files mdns4_minimal [NOTFOUND=return] mdns4 dns

=cut

init();

# PROMISE: DH NOOP WITHOUT nss cli-options()

sub process {
	foreach my $package (getpackages()) {
		my @service_names = ();
		my @inst_lines = ();
		my %rm_info = ();

		my $nss = pkgfile($package, "nss") or next;
		open(my $fd, $nss) or die("open($nss): $!");
		foreach my $line (<$fd>) {
			chomp($line);
			$line =~ s/#.*$//; # Remove comments.
			next if ($line eq "");

			my ($db, $service_name, $position, $inst_line) = process_line($line, $package);

			# Collect the names of NSS services installed by this package,
			# as well as their installation command lines.
			if (! grep { $_ eq $service_name } @service_names) {
				push(@service_names, $service_name);
			}
			push(@inst_lines, $inst_line) unless ($position eq "remove-only");

			# Collect the names of the NSS services that will be removed
			# when this package will be uninstalled.
			# The services are grouped by NSS DB.
			if (! exists $rm_info{$db}) { @{$rm_info{$db}} = (); }
			if (! grep { $_ eq $service_name } @{$rm_info{$db}}) {
				push(@{$rm_info{$db}}, $service_name);
			}
		}
		close($fd);

		# Turn the lists of service names into regular expressions.
		my $service_names_expr = join("|", @service_names);
		my @dbs = sort(keys %rm_info);
		foreach my $db (@dbs) {
			my @db_services = @{$rm_info{$db}};
			my $db_services_expr = join("|", @db_services);
			$rm_info{$db} = $db_services_expr;
		}

		# Generate the required debhelper snippets.
		output_autoscripts($package, $service_names_expr, \@inst_lines, \%rm_info);
	}
}

sub process_line {
	my ($line, $package) = @_;

	my ($db, $position, $service, $action, $condition) = split(" ", $line);
	$action //= "";
	$condition //= "";

	# Turn before=service into pos=before, anchor=service.
	($position, my $anchors) = split("=", $position);
	$anchors //= "";

	# Use $action as condition if it does not look like a proper action.
	if (substr($action, 0, 1) ne "[") {
		$condition = $action;
		$action = "";
	}

	my $comment = comment_for_line($package, $db, $service, $position, $anchors, $action, $condition);
	my $inst_operation = inst_operation_expr($package, $db, $service, $position, $anchors, $action, $condition);
	my $inst_line = "$comment\n\t\t$inst_operation";

	return $db, $service, $position, $inst_line;
}

sub comment_for_line {
	my ($package, $db, $service, $position, $anchors, $action, $condition) = @_;

	my $comment = "# Installing $db/$service$action from $package in position $position";
	if ($anchors ne "") { $comment .= "=$anchors"; }
	if ($condition ne "") { $comment .= " ($condition)"; }

	return $comment;
}

sub inst_operation_expr {
	my ($package, $db, $service, $position, $anchors, $action, $condition) = @_;

	my $anchors_expr = anchors_expr($anchors);

	my $condition_expr = cond_expr($db, $condition);

	# Prepare a sed-ready string with the service name.
	my $service_complete = $service;
	$service_complete .= " $action" if ($action ne "");
	$service_complete =~ s/(\[|\])/\\$1/g; # Escape `[` and `]`.

	# Choose the right sed invocation for the required position.
	my $sed_cmd = 'sed -E -i "${DPKG_ROOT}/etc/nsswitch.conf"';
	if ($position eq "first") {
		$sed_cmd .= " -e '/^$db:\\s/ s/(:\\s+)/\\1$service_complete /'";
	} elsif ($position eq "last") {
		$sed_cmd .= " -e '/^$db:\\s[^#]*\$/ s/\$/ $service_complete/'";
		$sed_cmd .= " -e '/^$db:\\s.*#/ s/#/ $service_complete #/'";
	} elsif ($position eq "before") {
		$sed_cmd .= " -e '/^$db:\\s[^#]*\$/ s/(\\s)($anchors_expr)(\\s|\$)/\\1$service_complete \\2 /'" ;
		$sed_cmd .= " -e '/^$db:\\s.*#/ s/(\\s)($anchors_expr)(\\s|#)/\\1$service_complete \\2 /'" ;
		$sed_cmd .= " -e 's/ \$//'";
	} elsif ($position eq "after") {
		$sed_cmd .= " -e '/^$db:\\s[^#]*\$/ s/(\\s)($anchors_expr)(\\s|\$)/\\1\\2 $service_complete /'";
		$sed_cmd .= " -e '/^$db:\\s.*#/ s/(\\s)($anchors_expr)(\\s|#)/\\1\\2 $service_complete \\3/'";
		$sed_cmd .= " -e 's/ \$//'";
	} elsif ($position eq "remove-only") {
		$sed_cmd = "";
	}

	if ($condition_expr ne "") {
		$sed_cmd = "if $condition_expr ; then\n\t\t\t$sed_cmd\n\t\tfi";
	}

	return $sed_cmd;
}

sub anchors_expr {
	my ($anchors_str) = @_;
	if (! defined($anchors_str)) { return "" };

	my @anchors_res;
	foreach my $anchor (split(",", $anchors_str)) {
		my $anchor_re = "$anchor(\\s+\\[[^]]+\\])?";
		push(@anchors_res, $anchor_re)	;
	}

	my $anchors_expr = join("|", @anchors_res);

	return $anchors_expr;
}

sub cond_expr {
	my ($db, $cond_str) = @_;
	if ($cond_str eq "") { return "" };

	my $cond_expr = "grep -q -E ";
	if ($cond_str =~ /^skip-if-present=/ ) {
		my $services = $cond_str =~ s/^skip-if-present=//r;
		$services =~ s/,/|/g;
		$cond_expr = "! $cond_expr";
		$cond_expr .= "'^$db:.*\\s($services)(\\s|\$)'"
	} else {
		error("Cannot parse condition $cond_str");
	}

	$cond_expr .= ' "${DPKG_ROOT}/etc/nsswitch.conf"';
	return $cond_expr
}

sub output_autoscripts {
	my ($package, $service_names_expr, $inst_lines, $rm_info) = @_;
	my @inst_lines = @{$inst_lines};
	my %rm_info = %{$rm_info};

	# Generate a single snippet with a sequence of installation instructions.
	my $inst_lines_expr = join("\n\t\t", @inst_lines);
	autoscript($package, "postinst", "postinst-nss", {
		"SERVICE_NAMES" => $service_names_expr,
		"OPERATIONS" => $inst_lines_expr,
	});

	# Generate one snipped for each NSS DB, removing only the services
	# related to that DB.
	my @dbs = sort(keys %rm_info);
	foreach my $db (@dbs) {
		my $db_services_expr = $rm_info{$db};
		autoscript($package, "postrm", "postrm-nss", {
			"DB"            => $db,
			"SERVICE_NAMES" => $db_services_expr,
		});
	}
}

process();

=head1 SEE ALSO

L<debhelper(7)>

This program is a debhelper addon.

=head1 AUTHOR

Gioele Barabucci <gioele@svario.it>

=cut
