#!/usr/bin/perl
##
## pwgen(1) clone: Passwords generator
## Copyright (c) 2007-2011 SATOH Fumiyasu @ OSS Technology, Inc.
##               <http://www.osstech.co.jp/>
##
## License: GNU General Public License version 3
## Date: 2011-06-23, since 2005-05-23
##

use strict;
use warnings;
use Getopt::Long;

use constant TRUE => 1;
use constant FALSE => 0;

sub pdie { print STDERR "$0: ERROR: $_[0]\n"; exit(1); }
sub opt_not_impl { pdie "Option not implemented: -$_[0]"; }

## FIXME: srand(truely random number);

use constant PW_DIGIT =>	0b0001;
use constant PW_UPPER =>	0b0010;
use constant PW_SYMBOL =>	0b0100;

my $seed_digits = '0123456789';
my $re_digits = qr/\d/;
my $seed_uppers = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
my $re_uppers = qr/[A-Z]/;
my $seed_lowers = 'abcdefghijklmnopqrstuvwxyz';
my $re_lowers = qr/[a-z]/;
my $seed_symbols = '#%&\'()*+,-./:;<=>?@[]^_{|}~'; ## Except '"', '$', '\', '`'
my $re_symbols = qr/[\Q$seed_symbols\E]/;
my $seed_ambiguous = '0DOQ1lI2Z5S6G8B9q';
my $re_ambiguous = qr/[\Q$seed_ambiguous\E]/;

my $pw_length = 32;
my $pw_number = 8;
my $pw_flags = PW_DIGIT | PW_UPPER;
my $flag_ambiguous = FALSE;

my $cmd_usage = "Usage: $0 [OPTIONS] [LENGTH [NUMBER]]

Arguments:
  LENGTH
    Password length
  NUMBER
    Number of passwords

Options:
  -A, --no-capitalize
    Don't bother to include any capital letters in the generated passwords
  -c, --capitalize
    Include  at least one capital letter in the password (default)
  -n, --numerals
    Include at least one number in the password (default)
  -0, --no-numerals
    Don't include numbers in the generated passwords
  -y, --symbols
    Include at least one special symbol in the password
  -B, --ambiguous
    Don't include ambiguous characters in the password
";

{
  ## Trap warning messages from Getopt::Long
  local($SIG{'__WARN__'}) = sub {
    my ($msg) = shift(@_);
    chomp($msg);
    print STDERR "$0: ERROR: $msg\n";
  };
  ## I don't like default behavior.
  Getopt::Long::Configure('bundling');
  Getopt::Long::Configure('no_ignore_case');
  Getopt::Long::Configure('no_auto_abbrev');
  GetOptions(
    'h|help' =>			sub { print $cmd_usage; exit(0); },
    'N|num-passwords=i' =>	\$pw_number,
    'C' =>			\&opt_not_impl, ## FIXME
    '1' =>			\&opt_not_impl, ## FIXME
    'a|alt-phonics' =>		\&opt_not_impl, ## FIXME
    'c|capitalize' =>		sub { $pw_flags |= PW_UPPER; },
    'A|no-capitalize'=>		sub { $pw_flags &= ~PW_UPPER; },
    'n|numerals' =>		sub { $pw_flags |= PW_DIGIT; },
    '0|no-numerals' =>		sub { $pw_flags &= ~PW_DIGIT; },
    's|secure' =>		\&opt_not_impl, ## FIXME
    'v|no-vowels' =>		\&opt_not_impl, ## FIXME
    'y|symbols' =>		sub { $pw_flags |= PW_SYMBOL; },
    'no-symbols' =>		sub { $pw_flags &= ~PW_SYMBOL; },
    'H|sha1=s' =>		\&opt_not_impl, ## FIXME
    'B|ambiguous' =>		\$flag_ambiguous,
  ) || exit(1);
}

$pw_length = shift(@ARGV) if (@ARGV);
$pw_number = shift(@ARGV) if (@ARGV);

my $seed = $seed_lowers;
$seed .= $seed_digits if ($pw_flags & PW_DIGIT);
$seed .= $seed_uppers if ($pw_flags & PW_UPPER);
$seed .= $seed_symbols if ($pw_flags & PW_SYMBOL);
if ($flag_ambiguous) {
  $seed =~ s/$re_ambiguous//g;
}

my @seed = split(//, $seed);

PWGEN: for (my $i = 0; $i < $pw_number; $i++) {
  my $pw = '';
  for (my $j = 0; $j < $pw_length; $j++) {
    $pw .= @seed[rand(@seed)];
  }

  my $pw_features = 0;
  $pw_features |= PW_DIGIT if ($pw_flags & PW_DIGIT && $pw =~ $re_digits);
  $pw_features |= PW_UPPER if ($pw_flags & PW_UPPER && $pw =~ $re_uppers);
  $pw_features |= PW_SYMBOL if ($pw_flags & PW_SYMBOL && $pw =~ $re_symbols);
  redo if ($pw_flags != $pw_features);

  print "$pw\n";
}

