import ceylon.buffer { ByteBuffer, CharacterBuffer, Buffer } import ceylon.buffer.codec { ByteToByteCodec, ErrorStrategy, PieceConvert, CharacterToByteCodec, strict, ignore, DecodeException, IncrementalCodec, resetStrategy=reset } abstract class Base64PieceEncoderState() of b64EncodeFirst | b64EncodeSecond | b64EncodeThird {} object b64EncodeFirst extends Base64PieceEncoderState() {} object b64EncodeSecond extends Base64PieceEncoderState() {} object b64EncodeThird extends Base64PieceEncoderState() {} abstract class Base64PieceDecoderState() of b64DecodeFirst | b64DecodeSecond | b64DecodeThird | b64DecodeFourth {} object b64DecodeFirst extends Base64PieceDecoderState() {} object b64DecodeSecond extends Base64PieceDecoderState() {} object b64DecodeThird extends Base64PieceDecoderState() {} object b64DecodeFourth extends Base64PieceDecoderState() {} shared sealed abstract class Base64<ToMutable, ToImmutable, ToSingle>(toMutableOfSize) satisfies IncrementalCodec<ToMutable,ToImmutable,ToSingle,ByteBuffer,List<Byte>,Byte> given ToMutable satisfies Buffer<ToSingle> given ToImmutable satisfies {ToSingle*} given ToSingle satisfies Object { ToMutable(Integer) toMutableOfSize; shared formal ToSingle[] encodeTable; shared formal Integer decodeToIndex(ToSingle input); shared formal Byte[] decodeTable; "The padding character, used where required to terminate discrete blocks of encoded data so they may be concatenated without making the seperation point ambiguous." shared formal ToSingle pad; shared actual Integer averageEncodeSize(Integer inputSize) => ceiling(inputSize, 3.0) * 4; shared actual Integer maximumEncodeSize(Integer inputSize) => averageEncodeSize(inputSize); shared actual Integer averageDecodeSize(Integer inputSize) => ceiling(inputSize * 3, 4.0); shared actual Integer maximumDecodeSize(Integer inputSize) => averageDecodeSize(inputSize); shared actual PieceConvert<ToSingle,Byte> pieceEncoder(ErrorStrategy error) => object satisfies PieceConvert<ToSingle,Byte> { ToMutable output = toMutableOfSize(3); variable Base64PieceEncoderState state = b64EncodeFirst; variable Byte? remainder = null; void reset() { state = b64EncodeFirst; remainder = null; } ToSingle byteToChar(Byte byte) { // Not using ErrorStrategy / EncodeException here since if this // doesn't succeed the implementation is wrong. All input bytes are // valid. "6 bit split is too large. Internal error." assert (byte.signed < 64); "6 bit split is negative. Internal error." assert (byte.signed >= 0); "Base64 table is invalid. Internal error." assert (exists char = encodeTable[byte.signed]); return char; } shared actual {ToSingle*} more(Byte input) { output.clear(); // Three byte repeating quantum, producing 4 characters of 6-bits each switch (state) case (b64EncodeFirst) { // [in 01234567] -> [char 012345][rem 67] remainder = input.and($11.byte); output.put(byteToChar(input.rightLogicalShift(2))); state = b64EncodeSecond; } case (b64EncodeSecond) { // [rem 67][in 01234567] -> [char [rem 67]0123][rem 4567] assert (exists rem = remainder); remainder = input.and($1111.byte); output.put(byteToChar { input.rightLogicalShift(4).or(rem.leftLogicalShift(4)); }); state = b64EncodeThird; } case (b64EncodeThird) { // [rem 4567][in 01234567] -> [char [rem 4567]01][char 234567] assert (exists rem = remainder); output.put(byteToChar { input.rightLogicalShift(6).or(rem.leftLogicalShift(2)); }); output.put(byteToChar { input.and($111111.byte); }); reset(); } output.flip(); return output; } shared actual {ToSingle*} done() { output.clear(); switch (state) case (b64EncodeFirst) { // At quantum boundary, nothing to return } case (b64EncodeSecond) { // [rem 67] -> [char [rem 67][pad 0000]] pad pad assert (exists rem = remainder); output.put(byteToChar(rem.leftLogicalShift(4))); output.put(pad); output.put(pad); } case (b64EncodeThird) { // [rem 4567] -> [char [rem 4567][pad 00]] pad assert (exists rem = remainder); output.put(byteToChar(rem.leftLogicalShift(2))); output.put(pad); } reset(); output.flip(); return output; } }; shared actual PieceConvert<Byte,ToSingle> pieceDecoder(ErrorStrategy error) => object satisfies PieceConvert<Byte,ToSingle> { ByteBuffer output = ByteBuffer.ofSize(1); variable Base64PieceDecoderState state = b64DecodeFirst; variable Boolean padSeen = false; variable Byte? remainder = null; void reset() { state = b64DecodeFirst; padSeen = false; remainder = null; } shared actual {Byte*} more(ToSingle input) { output.clear(); void handleState(nextState, handleInputByte, handlePad = null) { variable Base64PieceDecoderState nextState; Anything(Byte) handleInputByte; Anything()? handlePad; if (input == pad) { if (exists handlePad) { padSeen = true; if (exists rem = remainder, rem != 0.byte) { handlePad(); } } else { switch (error) case (strict) { throw DecodeException { "Pad element ``input`` is not allowed here"; }; } case (ignore) { } case (resetStrategy) { reset(); nextState = state; } } } else if (padSeen) { switch (error) case (strict) { throw DecodeException { "Non-pad character ``input`` is not allowed here"; }; } case (ignore) { } case (resetStrategy) { reset(); nextState = state; } } else { Integer inputIndex = decodeToIndex(input); if (exists inByte = decodeTable[inputIndex], inByte != 255.byte) { handleInputByte(inByte); } else { switch (error) case (strict) { throw DecodeException("``input`` is not a base64 character"); } case (ignore) { } case (resetStrategy) { reset(); nextState = state; } } } state = nextState; } // Repeating quantum of four 6-bit characters, producing 3 bytes switch (state) case (b64DecodeFirst) { handleState { nextState = b64DecodeSecond; // Don't have enough to construct 8 bits yet handleInputByte = (b) => remainder = b; }; } case (b64DecodeSecond) { assert (exists rem = remainder); handleState { nextState = b64DecodeThird; // [rem 012345][in 012345] -> [out [rem 012345][in 01]][rem 2345] handleInputByte = (b) { remainder = b.and($1111.byte); output.put { rem.leftLogicalShift(2).or(b.rightLogicalShift(4)); }; }; // [rem 012345][pad 00] -> [out [rem 012345][pad 00]] handlePad = () { remainder = 0.byte; output.put(rem.leftLogicalShift(2)); }; }; } case (b64DecodeThird) { assert (exists rem = remainder); handleState { nextState = b64DecodeFourth; // [rem 2345][in 012345] -> [out [rem 2345][in 0123]][rem 45] handleInputByte = (b) { remainder = b.and($11.byte); output.put { rem.leftLogicalShift(4).or(b.rightLogicalShift(2)); }; }; // [rem 2345][pad 000000] -> [out [rem 2345][pad 0000]] handlePad = () { remainder = 0.byte; output.put(rem.leftLogicalShift(4)); }; }; } case (b64DecodeFourth) { assert (exists rem = remainder); handleState { nextState = b64DecodeFirst; // [rem 45][in 012345] -> [out [rem 45][in 012345]] handleInputByte = (b) { remainder = 0.byte; output.put { rem.leftLogicalShift(6).or(b); }; }; // [rem 45][pad 000000] -> [out [rem 45][pad 0000]] handlePad = () { remainder = 0.byte; output.put(rem.leftLogicalShift(6)); }; }; reset(); } output.flip(); return output; } shared actual {Byte*} done() { output.clear(); // Pad termination is optional if (!padSeen) { switch (state) case (b64DecodeFirst) { // At quantum boundary, nothing to return } case (b64DecodeSecond) { if (exists rem = remainder, rem != 0.byte) { output.put(rem.leftLogicalShift(2)); } } case (b64DecodeThird) { if (exists rem = remainder, rem != 0.byte) { output.put(rem.leftLogicalShift(4)); } } case (b64DecodeFourth) { if (exists rem = remainder, rem != 0.byte) { output.put(rem.leftLogicalShift(6)); } } } reset(); output.flip(); return output; } }; shared actual Integer decodeBid({ToSingle*} sample) { if (sample.every((s) => s==pad || s in encodeTable)) { return 100; } else { return 0; } } } shared abstract class Base64String() extends Base64<CharacterBuffer,String,Character>(CharacterBuffer.ofSize) satisfies CharacterToByteCodec { shared actual Character pad = '='; } shared abstract class Base64Byte() extends Base64<ByteBuffer,List<Byte>,Byte>(ByteBuffer.ofSize) satisfies ByteToByteCodec { shared actual Byte pad = '='.integer.byte; } Character[] standardBase64CharTable = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' ]; "The Basic type base64 encoding scheme of [RFC 4648][rfc4648]. [rfc4648]: http://tools.ietf.org/html/rfc4648" shared object base64StringStandard extends Base64String() { shared actual Character[] encodeTable = standardBase64CharTable; shared actual Integer decodeToIndex(Character input) => input.integer; shared actual Byte[] decodeTable = toDecodeTable(encodeTable, decodeToIndex); shared actual [String+] aliases = ["base64", "base-64", "base_64"]; shared actual Integer encodeBid({Byte*} sample) => 55; } Byte[] standardBase64ByteTable = standardBase64CharTable*.integer*.byte.sequence(); "The Basic type base64 encoding scheme of [RFC 4648][rfc4648]. [rfc4648]: http://tools.ietf.org/html/rfc4648" shared object base64ByteStandard extends Base64Byte() { shared actual Byte[] encodeTable = standardBase64ByteTable; shared actual Integer decodeToIndex(Byte input) => input.unsigned; shared actual Byte[] decodeTable = toDecodeTable(encodeTable, decodeToIndex); shared actual [String+] aliases = ["base64", "base-64", "base_64"]; shared actual Integer encodeBid({Byte*} sample) => 55; } Character[] urlBase64CharTable = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' ]; "The URL and Filename safe type base64 encoding scheme of [RFC 4648][rfc4648]. [rfc4648]: http://tools.ietf.org/html/rfc4648" shared object base64StringUrl extends Base64String() { shared actual Character[] encodeTable = urlBase64CharTable; shared actual Integer decodeToIndex(Character input) => input.integer; shared actual Byte[] decodeTable = toDecodeTable(encodeTable, decodeToIndex); shared actual [String+] aliases = ["base64url", "base-64-url", "base_64_url"]; shared actual Integer encodeBid({Byte*} sample) => 50; } Byte[] urlBase64ByteTable = urlBase64CharTable*.integer*.byte.sequence(); "The URL and Filename safe type base64 encoding scheme of [RFC 4648][rfc4648]. [rfc4648]: http://tools.ietf.org/html/rfc4648" shared object base64ByteUrl extends Base64Byte() { shared actual Byte[] encodeTable = urlBase64ByteTable; shared actual Integer decodeToIndex(Byte input) => input.unsigned; shared actual Byte[] decodeTable = toDecodeTable(encodeTable, decodeToIndex); shared actual [String+] aliases = ["base64url", "base-64-url", "base_64_url"]; shared actual Integer encodeBid({Byte*} sample) => 50; }