Skip to content

Commit

Permalink
Add Base64Decode (#418)
Browse files Browse the repository at this point in the history
* Add Base64Decode

* Remove unnecessary ignore Expr equality context
  • Loading branch information
jdtzmn authored Jun 30, 2022
1 parent 8c3d2a0 commit 839b985
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyteal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ __all__ = [
"AssetHolding",
"AssetParam",
"Balance",
"Base64Decode",
"BinaryExpr",
"BitLen",
"BitwiseAnd",
Expand Down
2 changes: 2 additions & 0 deletions pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
ExtractUint32,
ExtractUint64,
)
from pyteal.ast.base64decode import Base64Decode

# ternary ops
from pyteal.ast.ternaryexpr import Divw, Ed25519Verify, SetBit, SetByte
Expand Down Expand Up @@ -277,6 +278,7 @@
"ExtractUint32",
"ExtractUint64",
"Replace",
"Base64Decode",
"Log",
"While",
"For",
Expand Down
85 changes: 85 additions & 0 deletions pyteal/ast/base64decode.py
Original file line number Diff line number Diff line change
@@ -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"
57 changes: 57 additions & 0 deletions pyteal/ast/base64decode_test.py
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions pyteal/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 839b985

Please sign in to comment.