#! /usr/bin/env perl
use v5.12;
use warnings;
use Pod::Usage;
use Getopt::Long qw( :config gnu_getopt );
use Crypt::SecretBuffer qw/ secret /;
use Crypt::MultiKey::PKey;
use Crypt::MultiKey::Coffer;

=head1 USAGE

  cmk-coffer-set [OPTIONS] COFFER_FILE [PKEY_FILE...]

Open COFFER_FILE, unlock it by whatever means are required, merge new dictionary
items into it (or completely overwrite it with new content), then save it.

A list of PKey files may follow it on the argument list, each of which will
be loaded and considered for whether it can help unlock the Coffer.
For Coffers with the PKey objects serialized in the same file, use option
C<< --bundled-keys (-b) >>.

This is mainly intended for the case of altering Coffers containing dictionary
content, because for a single-content-blob Coffer you could just as easily use
C<cmk-coffer-new> to create a fresh one rather than decrypting the old one first
and overwritng it.  Overwriting the content might save you the effort of
re-specifying the metadata and access locks, though.

=head1 OPTIONS

=over

=item --bundled-keys (-b)

Scan the remainder of the coffer file for embedded PKey objects

=item --parse=FORMAT

Parse names and values from stdin and store them as dictionary content.

Formats:

=over

=item ini

Parse input as lines of C<NAME=VALUE> in the tradition of INI files, where ';'
and '#' begin comments and whitespace is trimmed from the names and values.
Bare names (lacking an '=') are not supported, nor are multiline values.
INI headers are currently ignored.

=item 0

Parse the input as NUL-delimited strings alternating between Name and Value.

=back

=item --dict-item (-d) NAME=VALUE_FILE

Read a value from VALUE_FILE and store it as dictionary item NAME.
This option can be used multiple times, and applies after items generated by
C<--parse>.  If option C<--parse> is not used, C<--dict-item> implies that no
value will be read from stdin.

=item --content-type MIMETYPE

Optionally specify a MIME-type for the content of the Coffer, stored in the
public headers (not encrypted).
Dictionary storage implies its own content type, so no need to set this if
you specify dictionary storage options.

=back

=cut

GetOptions(
   'bundled-keys|b'        => \my $opt_bundled_keys,
   'parse=s'               => \my $opt_parse,
   'dict-item|d=s'         => \my @opt_dict_item,
   'content-type=s'        => \my $opt_content_type,
   'help'                  => sub { pod2usage(1) },
) and @ARGV >= 1
  or pod2usage(2);

my $path= shift;
pod2usage(-message => "Path '$path' does not exist")
   unless -e $path;

my $in;
if ($opt_parse || !@opt_dict_item) {
   $in= secret;
   while ($in->append_sysread(\*STDIN, 4096) // die "read(stdin): $!\n") {}
}

my $coffer= Crypt::MultiKey::Coffer->load($path, bundled_keys => $opt_bundled_keys);
for (@ARGV) {
   my $pkey= Crypt::MultiKey::PKey->load($_);
   $coffer->insert_keys($pkey);
}
$coffer->interactive_unlock;

$coffer->content_type($opt_content_type)
   if defined $opt_content_type;

if (defined $opt_parse || @opt_dict_item) {
   # Dictionary storage.
   if (defined $opt_parse) {
      my $span= span($in);
      if ($opt_parse eq '0') {
         while ($span->len) {
            my $k= $span->parse(qr/[^\0]/)
               // die "Encountered 0-length dict name in input\n";
            my $v= $span->ltrim("\0")->parse(qr/[^\0]/);
            $span->ltrim("\0");
            $coffer->set($k, $v // '');
         }
      }
      elsif ($opt_parse eq 'ini') {
         require Crypt::SecretBuffer::INI;
         my $ini= Crypt::SecretBuffer::INI->new(inline_comments => 1);
         while ($span->len) {
            my $attrs= $ini->parse_next($span);
            die "Parse error on input: $attrs->{error}\n"
               if defined $attrs->{error};
            die "INI headers are not permitted\n"
               if defined $attrs->{section};
            next unless defined $attrs->{key};
            $coffer->set($attrs->{key}, $attrs->{value} // '');
         }
      }
      else {
         die "Unknown parse format '$opt_parse'\n";
      }
   }
   for (@opt_dict_item) {
      my ($k, $file)= split /=/, $_, 2;
      defined $file && length $file
         or die "--dict-item $k lacks a filename";
      my $val= secret(load_file => $file); # dies on failure
      $coffer->set($k, $val);
   }
} else {
   # stdin is the content of the coffer
   $coffer->content($in);
}

$coffer->save;

exit 0;
