Skip to content

Instantly share code, notes, and snippets.

@ugexe
Created September 2, 2023 23:28
Show Gist options
  • Save ugexe/7cf5d557ef40f21d2e35520d2a440423 to your computer and use it in GitHub Desktop.
Save ugexe/7cf5d557ef40f21d2e35520d2a440423 to your computer and use it in GitHub Desktop.
{
BEGIN my Str:D @chars64base = flat 'A'..'Z','a'..'z','0'..'9';
BEGIN my Str:D @chars64std = flat @chars64base, '+', '/';
class Encoder::Base64 does Encoding::Encoder {
method encode-chars(Str:D $string --> Blob:D) {
my $blob := Blob.new(
map *.ord,
grep *.so,
$string.ords.rotor(3, :partial).map: -> $chunk {
my $padding = 0;
my $n := [+] $chunk.pairs.map: -> $c {
LAST { $padding = do with (3 - ($c.key+1) % 3) { $^a == 3 ?? 0 !! $^a } }
$c.value +< ((state $m = 24) -= 8)
}
my $res := (18, 12, 6, 0).map({ $n +> $_ +& 63 });
(slip(@chars64std[$res[*]][0..*-($padding ?? $padding+1 !! 0)]),
((^$padding).map({"="}).Slip if $padding)).Slip
}
);
return $blob;
}
}
class Decoder::Base64 does Encoding::Decoder {
has Str:D @.alphabet[64];
has Int:D %!lookup{Str:D};
has Blob $!buffer = Blob.new;
has @!line-separators = ("\n", "\r\n");
method TWEAK(:@!alphabet = @chars64std) {
for @!alphabet.kv -> int $key, str $value {
%!lookup{$value} = $key;
}
}
method add-bytes(Blob:D $bytes --> Nil) {
$!buffer ~= $bytes;
}
method decode-chunk($chunk) {
my $n = [+] $chunk.map: { (%!lookup{$_} || 0) +< ((state $m = 24) -= 6) }
my @decoded = (16, 8, 0).map: { $n +> $_ +& 255 }
# Filter out any null bytes and adjust for padding
@decoded = @decoded.head( 3 - ( 4 - $chunk.elems ) ).grep(*.defined && * != 0);
return @decoded;
}
method consume-available-chars(--> Str:D) {
my @buffer;
my $output = '';
for $!buffer.decode('utf-8').comb -> $char {
next if $char eq '=';
@buffer.push($char);
if @buffer.elems == 4 {
$output ~= self.decode-chunk(@buffer).map({ .chr }).join;
@buffer = ();
}
}
# Process any remaining characters in the buffer
if @buffer.elems {
$output ~= self.decode-chunk(@buffer).map({ .chr }).join;
}
$!buffer = Blob.new; # Clear the main buffer
return $output;
}
method consume-all-chars(--> Str:D) {
return self.consume-available-chars;
}
method consume-exactly-chars(int $chars, Bool:D :$eof = False --> Str) {
my $output = self.consume-available-chars;
if $output.chars < $chars {
die "Not enough characters available" unless $eof;
}
$!buffer = $!buffer.subbuf($chars * 4); # 4 bytes for each char in base64
return $output.substr(0, $chars);
}
method set-line-separators(@seps --> Nil) {
@!line-separators = @seps;
}
method consume-line-chars(Bool:D :$chomp = False, Bool:D :$eof = False --> Str) {
# Ensure we have some decoded characters
my $output = self.consume-available-chars;
my $line;
for @!line-separators -> $sep {
if $output ~~ /$sep/ {
$line = $/[0].prematch;
$output = $/[0].postmatch;
last;
}
}
# If no line separator is found and $eof is True, consume all remaining chars
unless $line.defined {
if $eof {
$line = $output;
$output = '';
} else {
die "No complete line available";
}
}
# Update the buffer to remove the consumed bytes
$!buffer = $!buffer.subbuf($line.encode('utf-8').bytes);
return $chomp ?? $line.chomp !! $line;
}
method is-empty(--> Bool) {
return $!buffer.elems == 0;
}
method bytes-available(--> Int:D) {
return $!buffer.elems;
}
method consume-exactly-bytes(int $bytes --> Blob) {
# Ensure we have enough decoded characters
my $output = self.consume-available-chars.encode('utf-8');
if $output.bytes < $bytes {
die "Not enough bytes available";
}
# Update the buffer to remove the consumed bytes
$!buffer = $!buffer.subbuf($bytes);
return $output.subbuf(0, $bytes);
}
}
}
class Encoding::Base64 does Encoding {
method name { 'base64' }
method alternative-names() { Empty }
method encoder(*%options --> Encoding::Encoder) {
my $encoder := Encoder::Base64.new();
return $encoder;
}
method decoder(*%options --> Encoding::Decoder) {
my $decoder := Decoder::Base64.new();
return $decoder;
}
}
use Test;
lives-ok { Encoding::Registry.register(Encoding::Base64) };
subtest 'Encoder' => {
my $encoder = Encoding::Registry.find('base64').encoder();
is $encoder.encode-chars("foo").decode, 'Zm9v';
}
subtest 'Decoder' => {
my $encoder = Encoding::Registry.find('base64').encoder();
my $decoder = Encoding::Registry.find('base64').decoder();
my $source-text = "foo";
$decoder.add-bytes($encoder.encode-chars($source-text));
my $rt-source-text = $decoder.consume-all-chars();
is $rt-source-text, $source-text;
is $decoder.bytes-available, 0, 'Decoder processes all bytes';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment