diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi index ec4af54a0..c4f1dd670 100644 --- a/pyteal/__init__.pyi +++ b/pyteal/__init__.pyi @@ -37,6 +37,7 @@ __all__ = [ "AssetHolding", "AssetParam", "Balance", + "Base64Decode", "BinaryExpr", "BitLen", "BitwiseAnd", diff --git a/pyteal/ast/__init__.py b/pyteal/ast/__init__.py index 919e8b6d6..f42ee4ec0 100644 --- a/pyteal/ast/__init__.py +++ b/pyteal/ast/__init__.py @@ -99,6 +99,7 @@ ExtractUint32, ExtractUint64, ) +from pyteal.ast.base64decode import Base64Decode # ternary ops from pyteal.ast.ternaryexpr import Divw, Ed25519Verify, SetBit, SetByte @@ -277,6 +278,7 @@ "ExtractUint32", "ExtractUint64", "Replace", + "Base64Decode", "Log", "While", "For", diff --git a/pyteal/ast/base64decode.py b/pyteal/ast/base64decode.py new file mode 100644 index 000000000..7dcf47c18 --- /dev/null +++ b/pyteal/ast/base64decode.py @@ -0,0 +1,85 @@ +from typing import TYPE_CHECKING +from enum import Enum + +from pyteal.types import TealType, require_type +from pyteal.errors import verifyFieldVersion +from pyteal.ir import TealOp, Op, TealBlock +from pyteal.ast.expr import Expr +from pyteal.ast.leafexpr import LeafExpr + +if TYPE_CHECKING: + from pyteal.compiler import CompileOptions + + +class Base64Encoding(Enum): + # fmt: off + # id | name | min version + url = (0, "URLEncoding", 7) + std = (1, "StdEncoding", 7) + # fmt: on + + def __init__(self, id: int, name: str, min_version: int) -> None: + self.id = id + self.arg_name = name + self.min_version = min_version + + +Base64Encoding.__module__ = "pyteal" + + +class Base64Decode(LeafExpr): + """An expression that decodes a base64-encoded byte string according to a specific encoding. + + See [RFC 4648](https://rfc-editor.org/rfc/rfc4648.html#section-4) (sections 4 and 5) for information on specifications. + + It is assumed that the encoding ends with the exact number of = padding characters as required by the RFC. + When padding occurs, any unused pad bits in the encoding must be set to zero or the decoding will fail. + The special cases of \\n and \\r are allowed but completely ignored. An error will result when attempting + to decode a string with a character that is not in the encoding alphabet or not one of =, \\r, or \\n. + """ + + def __init__(self, encoding: Base64Encoding, base64: Expr) -> None: + super().__init__() + self.encoding = encoding + + require_type(base64, TealType.bytes) + self.base64 = base64 + + def __teal__(self, options: "CompileOptions"): + verifyFieldVersion( + self.encoding.arg_name, self.encoding.min_version, options.version + ) + + op = TealOp(self, Op.base64_decode, self.encoding.arg_name) + return TealBlock.FromOp(options, op, self.base64) + + def __str__(self): + return "(Base64Decode {})".format(self.encoding.arg_name) + + def type_of(self): + return TealType.bytes + + @classmethod + def url(cls, base64: Expr) -> "Base64Decode": + """Decode a base64-encoded byte string according to the URL encoding. + + Refer to the `Base64Decode` class documentation for more information. + + Args: + base64: A base64-encoded byte string. + """ + return cls(Base64Encoding.url, base64) + + @classmethod + def std(cls, base64: Expr) -> "Base64Decode": + """Decode a base64-encoded byte string according to the Standard encoding. + + Refer to the `Base64Decode` class documentation for more information. + + Args: + base64: A base64-encoded byte string. + """ + return cls(Base64Encoding.std, base64) + + +Base64Decode.__module__ = "pyteal" diff --git a/pyteal/ast/base64decode_test.py b/pyteal/ast/base64decode_test.py new file mode 100644 index 000000000..14eb48799 --- /dev/null +++ b/pyteal/ast/base64decode_test.py @@ -0,0 +1,57 @@ +import pytest + +import pyteal as pt + +teal6Options = pt.CompileOptions(version=6) +teal7Options = pt.CompileOptions(version=7) + + +def test_base64decode_std(): + arg = pt.Bytes("aGVsbG8gd29ybGQ=") + expr = pt.Base64Decode.std(arg) + assert expr.type_of() == pt.TealType.bytes + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(arg, pt.Op.byte, '"aGVsbG8gd29ybGQ="'), + pt.TealOp(expr, pt.Op.base64_decode, "StdEncoding"), + ] + ) + + actual, _ = expr.__teal__(teal7Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + with pytest.raises(pt.TealInputError): + expr.__teal__(teal6Options) + + +def test_base64decode_url(): + arg = pt.Bytes("aGVsbG8gd29ybGQ") + expr = pt.Base64Decode.url(arg) + assert expr.type_of() == pt.TealType.bytes + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(arg, pt.Op.byte, '"aGVsbG8gd29ybGQ"'), + pt.TealOp(expr, pt.Op.base64_decode, "URLEncoding"), + ] + ) + + actual, _ = expr.__teal__(teal7Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + with pytest.raises(pt.TealInputError): + expr.__teal__(teal6Options) + + +def test_base64decode_invalid(): + with pytest.raises(pt.TealTypeError): + pt.Base64Decode.std(pt.Int(0)) + with pytest.raises(pt.TealTypeError): + pt.Base64Decode.url(pt.Int(0)) diff --git a/pyteal/ir/ops.py b/pyteal/ir/ops.py index f3ffc1a44..a21e34bc4 100644 --- a/pyteal/ir/ops.py +++ b/pyteal/ir/ops.py @@ -181,6 +181,7 @@ def min_version(self) -> int: acct_params_get = OpType("acct_params_get", Mode.Application, 6) replace2 = OpType("replace2", Mode.Signature | Mode.Application, 7) replace3 = OpType("replace3", Mode.Signature | Mode.Application, 7) + base64_decode = OpType("base64_decode", Mode.Application | Mode.Signature, 7) json_ref = OpType("json_ref", Mode.Signature | Mode.Application, 7) block = OpType("block", Mode.Signature | Mode.Application, 7) # fmt: on