#!/usr/bin/perl

=encoding utf8

=begin metadata

Name: base64
Description: encode and decode base64 data
Author: Michael Mikonos
License: artistic2

=end metadata

=cut

use strict;
use warnings;

use constant EX_SUCCESS => 0;
use constant EX_FAILURE => 1;

use File::Basename qw(basename);
use Getopt::Std qw(getopts);
use MIME::Base64 qw(decode_base64 encode_base64);

my $VERSION = '1.0';

my $Program = basename($0);

my (%opt, $bufsz, $in, $out);
getopts('do:v', \%opt) or usage();
if (scalar(@ARGV) > 1) {
    warn "$Program: too many arguments\n";
    usage();
}
if ($opt{'v'}) {
    print "$Program version $VERSION\n";
    exit EX_SUCCESS;
}
$bufsz = $opt{'d'} ? 76 : 57;
$bufsz *= 20;
if (defined $opt{'o'}) {
    unless (open $out, '>', $opt{'o'}) {
        warn "$Program: cannot open '$opt{o}': $!\n";
        exit EX_FAILURE;
    }
} else {
    $out = *STDOUT;
}
if (defined $ARGV[0] && $ARGV[0] ne '-') {
    if (-d $ARGV[0]) {
        warn "$Program: '$ARGV[0]' is a directory\n";
        exit EX_FAILURE;
    }
    unless (open $in, '<', $ARGV[0]) {
        warn "$Program: cannot open '$ARGV[0]': $!\n";
        exit EX_FAILURE;
    }
} else {
    $in = *STDIN;
}

$opt{'d'} ? decode() : encode();
close $in;
close $out;
exit EX_SUCCESS;

sub decode {
    my $buf = '';
    while (readline $in) {
        s/\s//g;
        if (m/[^A-Za-z0-9\+\/\=]/) {
            warn "$Program: bad input\n";
            exit EX_FAILURE;
        }
        $buf .= $_;
        if (length($buf) >= $bufsz) {
            my $chunk = substr($buf, 0, $bufsz);
            print {$out} decode_base64($chunk);
            $buf = substr($buf, $bufsz);
        }
    }
    if (length $buf) {
        print {$out} decode_base64($buf); # end chunk
    }
}

sub encode {
    my $buf;
    while (read $in, $buf, $bufsz) {
        print {$out} encode_base64($buf);
    }
}

sub usage {
    warn "usage: $Program [-dv] [-o FILE] [FILE]\n";
    exit EX_FAILURE;
}

__END__

=pod

=head1 NAME

base64 - encode and decode base64 data

=head1 SYNOPSIS

base64 [-dv] [-o FILE] [FILE]

=head1 DESCRIPTION

When encoding (the default mode), a binary file
is read and a base64 format file is created.
If no input file argument is provided, or file is '-',
stdin will be used.
The base64 output contains 76 characters per line.
Output is written to stdout by default.

When decoding, the input file is expected to contain
only valid base64 characters (alphanumeric, '+', '/' and '=').
Spaces are ignored when decoding. Selecting a binary file
as input will result in an error.

=head2 OPTIONS

The following options are available:

=over 4

=item -d

Decode data

=item -o FILE

Write output to the specified FILE

=item -v

Print version number and exit

=back

=head1 BUGS

No option exists for wrapping encoded base64 output at different
column widths.

It might be desirable to ignore unrecognised input characters when
decoding. This version of base64 has no option for relaxing the
input validation.

=head1 AUTHOR

Written by Michael Mikonos.

=head1 COPYRIGHT

Copyright (c) 2023 Michael Mikonos.

This code is licensed under the Artistic License 2.