### ----------------------------------------------------
### -- Util::Uuencode
### -- Licenses: Artistic-2.0
### -- Authors: Jonathan Stowe <jns+gh@gellyfish.co.uk>
### -- File: lib/Util/Uuencode.rakumod
### ----------------------------------------------------
use v6;

=begin pod


=head1 NAME

Util::Uuencode - uuencode/uudecode for Raku

=head1 SYNOPSIS

   use Util::Uuencode;

   my $image = "some-image.jpg".IO.slurp(:bin);

   my $encoded = uuencode($image);

   # now $encoded can be sent or stored as text safely

   $image = uudecode($encoded);

   # now have the image back as binary

=head1 DESCRIPTION

L<uuencode|https://en.wikipedia.org/wiki/Uuencoding> is a binary to text
encoding mechanism designed for sending binary files across computer
boundaries where the transport mechanism may not be 8 bit clean, such
as e-mail, usenet or uucp. It has largely been obsoleted by MIME (and
Base64 encoding,) and the advent of ubiquitous 8 bit clean networking.

This module provides routines for uuencoding (C<uuencode>) and decoding
(C<uudecode>) which will round trip data between binary and text encoded
representations. C<uuencode> will quite happily encode plain text but as
the effect is to make it a third larger there isn't much point doing that
( I guess at a push it could be used to preserve some unicode encoding
across some boundary that doesn't deal with that well.)

The  POSIX commands L<uuencode and
uudecode|https://pubs.opengroup.org/onlinepubs/9699919799/utilities/uuencode.html>
expect some additional header and trailer information which this module
doesn't deal with, so if you expect your data to be processed by these
tools you will need to add this to your encoded output and remove it
from the input for correct processing.

=head2 uuencode

    multi sub uuencode(Str $in, Bool :$strict = False --> Str)
    multi sub uuencode(buf8 $in, Bool :$strict = False --> Str)

This takes the data to be encoded and returns the encoded text representation, if the input data is
a C<Blob> other than a C<buf8> (C<Buf[uint8]>) you will need to coerce it as appropriate before
passing it.

By default this uses the modified encoding, that appears to be used by the majority of
implementations, whereby the backtick ("`", ASCII 96) replaces space ( " ", ASCII 32)
in the encoded output.  If you need to interoperate with code that strictly implements
the POSIX description then you should supply the C<:strict> adverb to the call.


=head2 uudecode

    sub uudecode(Str $in --> buf8)

This takes the uuencoded text as produced by C<uuencode> and returns the original data as a C<buf8>.
If you are expecting C<Str> data then you will need to call C<.decode> on the C<buf8>.

As noted above this will only deal with the encoded block of text and the C<begin> and C<end> lines
from output generated by the classic C<uuencode> command will need to be removed prior to calling
this.

This will deal with either the strict or de-facto encoding as described in L<uuencode|#uuencode>.

=end pod

module Util::Uuencode {

    multi sub uuencode(Str $in, :$strict --> Str) is export(:DEFAULT) {
        uuencode(buf8.new($in.encode), :$strict);
    }

    multi sub uuencode(buf8 $in, :$strict --> Str) is export(:DEFAULT) {
        my $elems = $in.elems;
        my  buf8 $out = buf8.new;

        my Int $pos = 0;

        while ( $pos < $elems ) {
            my $buf = $in.subbuf($pos, 45);

            my Int @line;
            @line.append: $buf.elems + 32;


            if $buf.elems < 45 {
                my $pad = (3 - ($buf.elems % 3));
                $buf.append: (0) xx $pad;
            }

            for (^(($buf.elems * 8) / 6 )).map( * * 6 ) -> $start {
                my $sextet = $buf.read-ubits($start, 6);
                @line.append: ( !$strict && $sextet == 0 ?? 96 !! $sextet + 32 );
            }
            @line.append: "\n".ord;
            $out.append: @line;
            $pos += 45;
        }
        # the specific encoding is not strictly required as the algorithm
        # will always result in 7-bit characters
        $out.decode('ascii');
    }

    my sub map-in(uint8 $in --> uint8) {
        given $in {
            when 96 {
                32
            }
            default {
                $in
            }
        }
    }

    sub uudecode(Str $in --> buf8) is export(:DEFAULT) {
        my buf8 $out = buf8.new;

        for $in.lines.map( -> $v { buf8.new($v.encode.list.map(&map-in) ) } ) -> $line {
            my Int $out-chars = $line.shift - 32;
            my $pos = 0;
            my buf8 $out-line = buf8.new;
            for $line.list.map( * - 32 ) -> $char {
                $out-line.write-bits($pos, 6, $char);
                $pos += 6;
            }
            $out.append: $out-line.subbuf(0, $out-chars);
        }
        $out;
    }
}

# vim: ft=raku


