#!/usr/bin/perl -w
# 
# ***** BEGIN LICENSE BLOCK *****
# Zimbra Collaboration Suite Server
# Copyright (C) 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software Foundation,
# version 2 of the License.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with this program.
# If not, see <https://www.gnu.org/licenses/>.
# ***** END LICENSE BLOCK *****
# 

use strict;
use Getopt::Long;
use POSIX qw(strftime);

my %opt = ();

sub usage {
    my $message = shift;
    if (defined($message)) {
        print "Error: $message\n";
        exit 1;
    } else {
        print<<EOF;
Usage: 
  zminiutil [--help] \\
    --backup=backup-suffix \\
    --(list|isset|get|set|setmin|setmax|unset) \\
    --section=s --key=k [--value=v] inifile

Eg:
  zminiutil --isset  --section=sec --key=lock srv.cnf
  zminiutil --get    --section=sec --key=fds srv.cnf
  zminiutil --backup=.bvx --set    --section=sec --key=lock srv.cnf
  zminiutil --backup=.baz --set    --section=sec --key=fds --value=12 srv.cnf
  zminiutil --backup=.bar --setmin --section=sec --key=foo --value=12 srv.cnf
  zminiutil --backup=.foo --setmax --section=sec --key=foo --value=12 srv.cnf
  zminiutil --backup=.foz --unset  --section=sec --key=bsz srv.cnf
EOF
         exit 0;
    }
}

GetOptions
    (
     'debug'     => \$opt{debug},
     'help'      => \$opt{help},
     'get'       => \$opt{get},
     'isset'     => \$opt{isset},
     'list'      => \$opt{list},
     'set'       => \$opt{set},
     'setmin'    => \$opt{setmin},
     'setmax'    => \$opt{setmax},
     'unset'     => \$opt{unset},
     'backup=s'  => \$opt{backup},
     'section=s' => \$opt{section},
     'key=s'     => \$opt{key},
     'value=s'   => \$opt{value}
     ) || usage("Unknown option!");

usage() if (defined($opt{help}));

my $op = 0;
$op++ if defined $opt{get};
$op++ if defined $opt{isset};
$op++ if defined $opt{list};
$op++ if defined $opt{set};
$op++ if defined $opt{setmin};
$op++ if defined $opt{setmax};
$op++ if defined $opt{unset};

usage("one of [list|isset|get|set|setmin|setmax|unset] must be specified") 
    if $op < 1;

usage("only one of [list|isset|get|set|setmin|setmax|unset] must be specified")
    if $op > 1;

usage("--key not specified") 
    if (!defined($opt{key}) && !defined($opt{list}));

usage("--section not specified") 
    if (!defined($opt{section}) && !defined($opt{list}));

usage("--value can only be specified with [set|setmin|setmax]")
    if (defined($opt{value}) && 
	!(defined($opt{set})||defined($opt{setmin})||defined($opt{setmax})));

usage("--value must be specified with [setmin|setmax]")
    if (!defined($opt{value}) && 
	(defined($opt{setmin}) || defined($opt{setmax})));

usage("--backup must be specified with [set|setmin|setmax|unset]")
    if (!defined($opt{backup}) && 
	(defined($opt{set}) || defined($opt{setmin}) || 
	 defined($opt{setmax}) || defined($opt{set})));

usage("one inifile must be specified") 
    if ($#ARGV != 0);

usage("--value must be a number")
    if ((defined($opt{setmin}) || defined($opt{setmax})) &&
	$opt{value} !~ /^[+-]?\d+$/);

#
# Debug print key/value
#
sub dp($$) {
    return if (!defined($opt{debug}));
    my ($k, $v) = @_; 
    print "parser: $k="; 
    print "'$v'" if (defined($v));
    print "<undef>" if (!defined($v)); 
    print "\n";
}

#
# Parse the INI file and return it as an array, one array element per
# line.
#
sub parseFile($) {
    my $file = shift;
    open(INPUTFILE, "$file") || die "can't open file $file: $!";

    my @lines = ();
    my $currentSection = undef;

  LINE: 
    while (<INPUTFILE>) {
        my %line = ();
        $line{content} = $_;
        push(@lines, \%line);

        # look for a new section definition
        my ($section) = m/^\s*[[](\s*\w*\s*)[]]\s*(|\x23.*)$/; # \x23=#
        if (defined($section)) {
            print "parse: new section=$section\n\n" if ($opt{debug});
            $currentSection = $section;
            $line{newSection} = $section;
            next LINE;
        }
        
        # look for empty or comment lines and avoid parsing those
        if (/^\s*(|\x23.*)$/) {
            next LINE;
        }
        
        my ($keyPre, $key, $keyPost, $equals, $valuePre, $value, $valuePost) =
            m/^(\s*)([^=\s]*)(\s*)(=?)(\s*)([^\s]*)(\s*(|\x23.*))$/;
        dp('==== content', $_);
        $line{section}   = $currentSection; dp('section',   $currentSection);
        $line{keyPre}    = $keyPre;         dp('keyPre',    $keyPre); 
        $line{key}       = $key;            dp('key',       $key);
        $line{keyPost}   = $keyPost;        dp('keyPost',   $keyPost); 
        $line{equals}    = $equals;         dp('equals',    $equals);
        $line{valuePre}  = $valuePre;       dp('valuePre',  $valuePre); 
        $line{value}     = $value;          dp('value',     $value);
        $line{valuePost} = $valuePost;      dp('valuePost', $valuePost);
    }
    close(INPUTFILE);
    return \@lines;
}

#
# main
#        
my $inputFile = $ARGV[0];
my $time = strftime("%Y%m%d%H%M%S", localtime);
my $outputFile = $inputFile . ".modify." . $$ . $time; # created if needed

my $linesRef = parseFile($inputFile);
die "error parsing file $inputFile" if (!defined($linesRef));

my @lines = @{$linesRef};

my $fullKey;
if (defined($opt{section}) && defined($opt{key})) {
    $fullKey = $opt{section} . "->" . $opt{key};
}

my %keys = ();
foreach my $line (@lines) {
    if (defined($line->{section}) && defined($line->{key})) {
	my $compoundKey = $line->{section} . "->" . $line->{key};
	$keys{$compoundKey} = $line->{value};
    }
}

### LIST
if (defined $opt{list}) {
    for my $k (sort keys %keys) {
	print $k, '=', $keys{$k}, "\n";
    }
    exit 0;
}

### GET
if (defined $opt{get}) {
    if (defined $keys{$fullKey}) {
	print $keys{$fullKey}, "\n";
	exit 0;
    } else {
	exit 1;
    }
}

### ISSET
if (defined $opt{isset}) {
    if (defined $keys{$fullKey}) {
	exit 0; 
    } else {
	exit 1;
    }
} 

### SET/SETMIN/SETMAX/UNSET
my $backupFile;
if (defined $opt{backup}) {
    $backupFile = $inputFile . $opt{backup};
} else {
    $backupFile = $inputFile . ".bak";
}

open(BACKUPFILE, ">$backupFile") || die "can't open backup file: $backupFile: $!";
open(OUTPUTFILE, ">$outputFile") || die "can't open temp file: $outputFile: $!";
LINE: for my $line (@lines) {
    print BACKUPFILE $line->{content};
    
    # Set a previously undefined key at start of its section
    if (!defined($keys{$fullKey})
	&& (defined($opt{set})||defined($opt{setmin})||defined($opt{setmax}))
	&& defined($line->{newSection}) 
	&& ($line->{newSection} eq $opt{section}))
    {
	print OUTPUTFILE $line->{content}; # the section header
	print OUTPUTFILE "\n", $opt{key};
	print OUTPUTFILE " = ", $opt{value} if defined $opt{value};
	print OUTPUTFILE "\n";
	next LINE;
    }
    
    # Modify an existing key
    if (defined($line->{key}) && ($line->{key} eq $opt{key}) &&
	defined($line->{section}) && ($line->{section} eq $opt{section}))
    {
	### UNSET
	next LINE if defined $opt{unset};

	### REPLACE WITH NEW
	if (defined($opt{set})) {
	    print OUTPUTFILE $opt{key};
	    print OUTPUTFILE " = ", $opt{value} if defined $opt{value};
	    print OUTPUTFILE "\n";
	    next LINE;
	}
	
	if ($line->{value} !~ /^[+-]?\d+$/) {
	    $line->{value} = 0;
	}

	### REPLACE IF SMALLER
	if (defined($opt{setmin}) && ($opt{value} > $line->{value})) {
	    print OUTPUTFILE $opt{key};
	    print OUTPUTFILE " = ", $opt{value} if defined $opt{value};
	    print OUTPUTFILE "\n";
	    next LINE;
	}
	    
	### REPLACE IF BIGGER
	if (defined($opt{setmax}) && ($opt{value} < $line->{value})) {
	    print OUTPUTFILE $opt{key};
	    print OUTPUTFILE " = ", $opt{value} if defined $opt{value};
	    print OUTPUTFILE "\n";
	    next LINE;
	}
    }
    print OUTPUTFILE $line->{content};
}
close(OUTPUTFILE);
close(BACKUPFILE);
rename($outputFile, $inputFile) || die "can't rename $outputFile, $inputFile: $!";
