Created
September 2, 2023 23:28
-
-
Save ugexe/7cf5d557ef40f21d2e35520d2a440423 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
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