From 9c5126a72da5c216e99f57525a61d35c5552b42b Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Fri, 24 Jun 2022 12:00:12 -0400 Subject: [PATCH 1/2] Add Base64Decode --- pyteal/__init__.pyi | 1 + pyteal/ast/__init__.py | 2 + pyteal/ast/base64decode.py | 85 +++++++++++++++++++++++++++++++++ pyteal/ast/base64decode_test.py | 59 +++++++++++++++++++++++ pyteal/ir/ops.py | 1 + 5 files changed, 148 insertions(+) create mode 100644 pyteal/ast/base64decode.py create mode 100644 pyteal/ast/base64decode_test.py diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi index 4fa32d071..a279281dc 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 613a39480..e56fa7892 100644 --- a/pyteal/ast/__init__.py +++ b/pyteal/ast/__init__.py @@ -98,6 +98,7 @@ ExtractUint32, ExtractUint64, ) +from pyteal.ast.base64decode import Base64Decode # ternary ops from pyteal.ast.ternaryexpr import Divw, Ed25519Verify, SetBit, SetByte @@ -272,6 +273,7 @@ "ExtractUint16", "ExtractUint32", "ExtractUint64", + "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..97bd24795 --- /dev/null +++ b/pyteal/ast/base64decode_test.py @@ -0,0 +1,59 @@ +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) + + with pt.TealComponent.Context.ignoreExprEquality(): + 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) + + with pt.TealComponent.Context.ignoreExprEquality(): + 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 7d91dab42..7b26d113a 100644 --- a/pyteal/ir/ops.py +++ b/pyteal/ir/ops.py @@ -179,6 +179,7 @@ def min_version(self) -> int: gitxnas = OpType("gitxnas", Mode.Application, 6) gloadss = OpType("gloadss", Mode.Application, 6) acct_params_get = OpType("acct_params_get", Mode.Application, 6) + base64_decode = OpType("base64_decode", Mode.Application | Mode.Signature, 7) # fmt: on From 15f9024f90b100780d1c9eb336b6956e974ce1b6 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Mon, 27 Jun 2022 10:04:20 -0400 Subject: [PATCH 2/2] Remove unnecessary ignore Expr equality context --- pyteal/ast/base64decode_test.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyteal/ast/base64decode_test.py b/pyteal/ast/base64decode_test.py index 97bd24795..14eb48799 100644 --- a/pyteal/ast/base64decode_test.py +++ b/pyteal/ast/base64decode_test.py @@ -22,8 +22,7 @@ def test_base64decode_std(): actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) - with pt.TealComponent.Context.ignoreExprEquality(): - assert actual == expected + assert actual == expected with pytest.raises(pt.TealInputError): expr.__teal__(teal6Options) @@ -45,8 +44,7 @@ def test_base64decode_url(): actual.addIncoming() actual = pt.TealBlock.NormalizeBlocks(actual) - with pt.TealComponent.Context.ignoreExprEquality(): - assert actual == expected + assert actual == expected with pytest.raises(pt.TealInputError): expr.__teal__(teal6Options)