From 2c54c2ba2fc34afe67a2fc262ef1ffddc45c924b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=23und=CE=A3f?= Date: Tue, 8 Oct 2024 10:37:18 +0700 Subject: [PATCH] Release Base64 v1.0.0 - Update Base64 Engine V2 (bitwise process) - Add some test case - Add benchmarks - Update document --- .bench/base64.bench.json | 370 +++++++++++++++++++++++++++ LICENSE | 2 +- README.md | 98 ++++++- bench/base64.bench.mo | 110 ++++++++ mops.toml | 3 +- src/{base64.mo => base64v1.mo} | 71 +++-- src/base64v2.mo | 191 ++++++++++++++ src/lib.mo | 78 +++++- src/types.mo | 21 ++ test/{lib.test.mo => base64.test.mo} | 150 ++++++++--- 10 files changed, 1017 insertions(+), 77 deletions(-) create mode 100644 .bench/base64.bench.json create mode 100644 bench/base64.bench.mo rename src/{base64.mo => base64v1.mo} (83%) create mode 100644 src/base64v2.mo create mode 100644 src/types.mo rename test/{lib.test.mo => base64.test.mo} (75%) diff --git a/.bench/base64.bench.json b/.bench/base64.bench.json new file mode 100644 index 0000000..9ca9438 --- /dev/null +++ b/.bench/base64.bench.json @@ -0,0 +1,370 @@ +{ + "version": 1, + "moc": "0.11.2", + "replica": "dfx", + "replicaVersion": "0.22.0", + "gc": "copying", + "forceGc": true, + "results": [ + [ + "EngineV1.Base64.encode:1", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 53185077, + "rts_memory_size": 4259840, + "rts_total_allocation": 4402628, + "rts_collector_instructions": 5586, + "rts_mutator_instructions": -11778, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 244, + "rts_reclaimed": 4402384 + } + ], + [ + "EngineV1.Base64.encode:10", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 527312719, + "rts_memory_size": 39583744, + "rts_total_allocation": 43941320, + "rts_collector_instructions": 5608, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 244, + "rts_reclaimed": 43941076 + } + ], + [ + "EngineV1.Base64.encode:100", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 5268604248, + "rts_memory_size": 395378688, + "rts_total_allocation": 439328288, + "rts_collector_instructions": 5987, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 280, + "rts_reclaimed": 439328008 + } + ], + [ + "EngineV1.Base64.encode:500", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 26339781620, + "rts_memory_size": 1757282304, + "rts_total_allocation": 2196603524, + "rts_collector_instructions": 5987, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 280, + "rts_reclaimed": 2196603208 + } + ], + [ + "EngineV1.Base64.decode:1", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 21661160, + "rts_memory_size": 0, + "rts_total_allocation": 1318800, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 1318448 + } + ], + [ + "EngineV1.Base64.decode:10", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 198741071, + "rts_memory_size": 0, + "rts_total_allocation": 12396252, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 12395900 + } + ], + [ + "EngineV1.Base64.decode:100", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 1969537568, + "rts_memory_size": 0, + "rts_total_allocation": 123170808, + "rts_collector_instructions": 7092, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 388, + "rts_reclaimed": 123170420 + } + ], + [ + "EngineV1.Base64.decode:500", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 9839736894, + "rts_memory_size": 0, + "rts_total_allocation": 615502020, + "rts_collector_instructions": 7092, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 388, + "rts_reclaimed": 615501632 + } + ], + [ + "EngineV1.Base64.isValid:1", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 22465115, + "rts_memory_size": 0, + "rts_total_allocation": 978640, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 978288 + } + ], + [ + "EngineV1.Base64.isValid:10", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 206773229, + "rts_memory_size": 0, + "rts_total_allocation": 8994652, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 8994300 + } + ], + [ + "EngineV1.Base64.isValid:100", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 2049853988, + "rts_memory_size": 0, + "rts_total_allocation": 89154808, + "rts_collector_instructions": 7092, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 388, + "rts_reclaimed": 89154420 + } + ], + [ + "EngineV1.Base64.isValid:500", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 10241327716, + "rts_memory_size": 0, + "rts_total_allocation": 445422020, + "rts_collector_instructions": 7092, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 388, + "rts_reclaimed": 445421632 + } + ], + [ + "EngineV2.Base64.encode:1", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 1820700, + "rts_memory_size": 0, + "rts_total_allocation": 69712, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 69360 + } + ], + [ + "EngineV2.Base64.encode:10", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 13680140, + "rts_memory_size": 0, + "rts_total_allocation": 611188, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 610836 + } + ], + [ + "EngineV2.Base64.encode:100", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 132273539, + "rts_memory_size": 0, + "rts_total_allocation": 6025948, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 6025596 + } + ], + [ + "EngineV2.Base64.encode:500", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 659354371, + "rts_memory_size": 0, + "rts_total_allocation": 30091548, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 30091196 + } + ], + [ + "EngineV2.Base64.decode:1", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 4198463, + "rts_memory_size": 0, + "rts_total_allocation": 154644, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 154292 + } + ], + [ + "EngineV2.Base64.decode:10", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 24102850, + "rts_memory_size": 0, + "rts_total_allocation": 754692, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 754340 + } + ], + [ + "EngineV2.Base64.decode:100", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 223145595, + "rts_memory_size": 0, + "rts_total_allocation": 6755172, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 6754820 + } + ], + [ + "EngineV2.Base64.decode:500", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 1107779937, + "rts_memory_size": 0, + "rts_total_allocation": 33424008, + "rts_collector_instructions": 7092, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 388, + "rts_reclaimed": 33423620 + } + ], + [ + "EngineV2.Base64.isValid:1", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 4014300, + "rts_memory_size": 0, + "rts_total_allocation": 159040, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 158688 + } + ], + [ + "EngineV2.Base64.isValid:10", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 22255564, + "rts_memory_size": 0, + "rts_total_allocation": 798652, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 798300 + } + ], + [ + "EngineV2.Base64.isValid:100", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 204666459, + "rts_memory_size": 0, + "rts_total_allocation": 7194772, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 7194420 + } + ], + [ + "EngineV2.Base64.isValid:500", + { + "rts_stable_memory_size": 0, + "stable_memory_size": 0, + "instructions": 1015381137, + "rts_memory_size": 0, + "rts_total_allocation": 35621972, + "rts_collector_instructions": 6725, + "rts_mutator_instructions": -7830, + "rts_logical_stable_memory_size": 0, + "rts_heap_size": 352, + "rts_reclaimed": 35621620 + } + ] + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index c5b4fe5..b403419 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 #undΣf +Copyright (c) 2024 nirvana369 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 08b3186..5f7f2dc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ ### Base64 implementation for Motoko ## Documentation +* [Installation](###installation) +* [Usage](#usage) + * [Encode](#base64-encode) + * [Decode](#base64-decode) + * [Is valid](#base64-isvalid) + * [Set is support URI](#base64-setissupporturi) +* [Testing](#testing) +* [Benchmarks](#benchmarks) +* [License](#license) ### Installation @@ -12,29 +21,106 @@ You need mops installed. In your project directory run [Mops](https://mops.one/) ```sh mops add base64 ``` -### Testing -You need mops installed. In your project directory run [Mops](https://mops.one/): +Usage ```sh -mops test +import {Base64 = Base64Engine, V2} "mo:base64"; + +let isSupportURI : Bool = false; +let base64 = Base64Engine(#v V2, ?isSupportURI); ``` -### Usage +Or ```sh import Base64 "mo:base64"; + +let isSupportURI : Bool = false; +let base64 = Base64.Base64(#version (Base64.V2), ?isSupportURI); ``` + +### Base64 encode + +Base64.encode(data : FormatType) : Text + Takes a data by format type (text/ byte array) and returns a base64 string ```sh -Base64.encode(data : FormatType, isSupportURI : Bool) : Text +import {Base64 = Base64Engine, V2} "mo:base64"; + +let isSupportURI : Bool = false; +let base64 = Base64Engine(#v V2, ?isSupportURI); + +let bytesData : [Nat8] = [100, 97, 110, 107, 111, 103, 97, 105]; +base64.encode(#bytes bytesData); + +let textData : Text = "𠮷野家"; +let base64EncodeString : Text = base64.encode(#text textData); ``` +### Base64 decode + +Base64.decode(base64String : Text) : [Nat8] + Takes a base64 string and returns length of byte array ```sh -Base64.decode(base64 : Text, isSupportURI : Bool) : [Nat8] +import {Base64 = Base64Engine, V2} "mo:base64"; + +let isSupportURI : Bool = false; +let base64 = Base64Engine(#v V2, ?isSupportURI); + +let base64String : Text = "ZA=="; +let bytesArrayDecode : [Nat8] = base64.decode(base64String); +``` + +### Base64 is valid + +Base64.isValid(base64String : Text) : [Nat8] + +Takes a base64 string and returns a boolean value + +```sh +import {Base64 = Base64Engine, V2} "mo:base64"; + +let isSupportURI : Bool = false; +let base64 = Base64Engine(#v V2, ?isSupportURI); + +let base64String : Text = "8KCut+mHjuWutg=="; +let isSupportURI : Bool = base64.isValid(base64String); +``` + +### Base64 set is support uri + +Base64.setSupportURI(isSupportURI : Bool) + +Base64 uri support ([RFC 4648 §5: base64url (URL- and filename-safe standard)](https://en.wikipedia.org/wiki/Base64#URL_applications)) + +```sh +import {Base64 = Base64Engine, V2} "mo:base64"; + +let isSupportURI : Bool = false; +let base64 = Base64Engine(#v V2, ?isSupportURI); + +base64.setSupportURI(true); +``` + + +### Testing + +You need mops installed. In your project directory run [Mops](https://mops.one/): + +```sh +mops test +``` + +### Benchmarks + +You need mops installed. In your project directory run [Mops](https://mops.one/): + +```sh +mops bench ``` ## License diff --git a/bench/base64.bench.mo b/bench/base64.bench.mo new file mode 100644 index 0000000..d150848 --- /dev/null +++ b/bench/base64.bench.mo @@ -0,0 +1,110 @@ +/******************************************************************* +* Copyright : 2024 nirvana369 +* File Name : base64.bench.mo +* Description : Benchmark Base64 engine v1 and v2. +* +* Revision History : +* Date Author Comments +* --------------------------------------------------------------------------- +* 10/08/2024 nirvana369 Add benchmarks. +******************************************************************/ + +import Bench "mo:bench"; +import Nat "mo:base/Nat"; +import Buffer "mo:base/Buffer"; +import Iter "mo:base/Iter"; +import Nat8 "mo:base/Nat8"; +import Text "mo:base/Text"; +import {Base64 = Base64Engine; V1; V2} "../src/lib"; + + +module { + + func getBase64EncodeBytes() : [Nat8] { + let MAX_LENGTH = 1024; + let big = Buffer.Buffer(MAX_LENGTH); + for (i in Iter.range(1, MAX_LENGTH)) { + big.add(Nat8.fromNat(i % 256)); + }; + Buffer.toArray(big); + }; + + func getBase64Decode() : Text { + let MAX_LENGTH = 1024; + let big = Buffer.Buffer(MAX_LENGTH); + for (i in Iter.range(1, MAX_LENGTH)) { + big.add(Nat8.fromNat(i % 256)); + }; + let bytes = Buffer.toArray(big); + + let base64 = Base64Engine(#v V2, null); + base64.encode(#bytes bytes); + }; + + public func init() : Bench.Bench { + let bench = Bench.Bench(); + + bench.name("Base64"); + bench.description("Base64 module benchmark"); + + bench.rows(["EngineV1.Base64.encode", + "EngineV1.Base64.decode", + "EngineV1.Base64.isValid", + "EngineV2.Base64.encode", + "EngineV2.Base64.decode", + "EngineV2.Base64.isValid" + ]); + bench.cols(["1", "10", "100", "500"/*, "1000"*/]); + + let base64 = Base64Engine(#v V1, null); + let base64v2 = Base64Engine(#v V2, null); + + bench.runner(func(row, col) { + let ?n = Nat.fromText(col); + + switch (row) { + // Engine V1 + case ("EngineV1.Base64.encode") { + let bytes = getBase64EncodeBytes(); + for (i in Iter.range(1, n)) { + ignore base64.encode(#bytes bytes); + }; + }; + case ("EngineV1.Base64.decode") { + let b64 = getBase64Decode(); + for (i in Iter.range(1, n)) { + ignore base64.decode(b64); + }; + }; + case ("EngineV1.Base64.isValid") { + let b64 = getBase64Decode(); + for (i in Iter.range(1, n)) { + ignore base64.isValid(b64); + }; + }; + // Engine V2 + case ("EngineV2.Base64.encode") { + let bytes = getBase64EncodeBytes(); + for (i in Iter.range(1, n)) { + ignore base64v2.encode(#bytes bytes); + }; + }; + case ("EngineV2.Base64.decode") { + let b64 = getBase64Decode(); + for (i in Iter.range(1, n)) { + ignore base64v2.decode(b64); + }; + }; + case ("EngineV2.Base64.isValid") { + let b64 = getBase64Decode(); + for (i in Iter.range(1, n)) { + ignore base64v2.isValid(b64); + }; + }; + case _ {}; + }; + }); + + bench; + }; +}; \ No newline at end of file diff --git a/mops.toml b/mops.toml index 3fe703e..c747b5a 100644 --- a/mops.toml +++ b/mops.toml @@ -7,4 +7,5 @@ keywords = [ "base64", "encoding", "decoding", "urldecode" ] license = "MIT" [dependencies] -test = "2.0.0" \ No newline at end of file +test = "2.0.0" +bench = "1.0.0" \ No newline at end of file diff --git a/src/base64.mo b/src/base64v1.mo similarity index 83% rename from src/base64.mo rename to src/base64v1.mo index 4978f35..759d170 100644 --- a/src/base64.mo +++ b/src/base64v1.mo @@ -1,3 +1,14 @@ +/******************************************************************* +* Copyright : 2024 nirvana369 +* File Name : v1engine.mo +* Description : Base64 version 1. +* +* Revision History : +* Date Author Comments +* --------------------------------------------------------------------------- +* 10/07/2024 nirvana369 Implement. +******************************************************************/ + import Text "mo:base/Text"; import Option "mo:base/Option"; import Array "mo:base/Array"; @@ -11,14 +22,11 @@ import Buffer "mo:base/Buffer"; import Int "mo:base/Int"; import Blob "mo:base/Blob"; import Bool "mo:base/Bool"; -import Debug "mo:base/Debug"; +import Types "./types"; module { - public type FormatType = { - #text : Text; - #bytes : [Nat8]; - }; + type FormatType = Types.FormatType; public class Base64(isSupportURI : ?Bool) { @@ -29,13 +37,38 @@ module { let code = Text.toArray("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); - private func _init(supportURI : () -> ()) { + var _isSupportURI = false; + + private func _initSupportURI(isSupportURI : Bool) { + if (_isSupportURI != isSupportURI) { + _isSupportURI := isSupportURI; + + let b62 = byte2bits(62, func z = z, 6); + let b63 = byte2bits(63, func z = z, 6); + let bits62 = Text.join("", Array.map(b62, func z = if (z) "1" else "0").vals()); + let bits63 = Text.join("", Array.map(b63, func z = if (z) "1" else "0").vals()); + + if (_isSupportURI) { + revLookup.put('-', b62); + revLookup.put('_', b63); + lookup.put(bits62, '-'); + lookup.put(bits63, '_'); + } else { + revLookup.put('+', b62); + revLookup.put('/', b63); + lookup.put(bits62, '+'); + lookup.put(bits63, '/'); + }; + }; + }; + + private func _init(isSupportURI : Bool) { for (i in Iter.range(0, code.size() - 1)) { let b = byte2bits(i, func z = z, 6); lookup.put(Text.join("", Array.map(b, func z = if (z) "1" else "0").vals()), code[i]); revLookup.put(code[i], b); }; - supportURI(); + _initSupportURI(isSupportURI); }; /** @@ -45,22 +78,10 @@ module { **/ switch (isSupportURI) { case(?sp) { - let f = func () = if (sp) { - let b62 = byte2bits(62, func z = z, 6); - let b63 = byte2bits(63, func z = z, 6); - revLookup.put('-', b62); - revLookup.put('_', b63); - - let bits62 = Text.join("", Array.map(b62, func z = if (z) "1" else "0").vals()); - let bits63 = Text.join("", Array.map(b63, func z = if (z) "1" else "0").vals()); - lookup.put(bits62, '-'); - lookup.put(bits63, '_'); - - } else (); - _init(f); + _init(sp); }; case (null) { - _init(func () = ()); + _init(_isSupportURI); }; }; @@ -82,7 +103,7 @@ module { buf := Buffer.subBuffer(buf, 0, 8 * (buf.size() / 8)); }; // check special case - if (isSupportURI == ?true) { + if (_isSupportURI == true) { if (buf.size() % 8 != 0) { buf := Buffer.subBuffer(buf, 0, 8 * (buf.size() / 8)); }; @@ -124,7 +145,7 @@ module { i += 1; }; // check special case - if (isSupportURI == ?false) { + if (_isSupportURI == false) { let extraBytes = bits.size() % 3; if (extraBytes > 0) { @@ -162,6 +183,10 @@ module { }; true; }; + + public func setSupportURI(isSupportURI : Bool) { + _initSupportURI(isSupportURI); + }; }; diff --git a/src/base64v2.mo b/src/base64v2.mo new file mode 100644 index 0000000..229233d --- /dev/null +++ b/src/base64v2.mo @@ -0,0 +1,191 @@ +/******************************************************************* +* Copyright : 2024 nirvana369 +* File Name : v2engine.mo +* Description : Base64 version 2 - bitwise processing. +* +* Revision History : +* Date Author Comments +* --------------------------------------------------------------------------- +* 10/08/2024 nirvana369 Implement. +******************************************************************/ + +import Text "mo:base/Text"; +import Option "mo:base/Option"; +import Hash "mo:base/Hash"; +import Char "mo:base/Char"; +import HashMap "mo:base/HashMap"; +import Iter "mo:base/Iter"; +import Nat8 "mo:base/Nat8"; +import Buffer "mo:base/Buffer"; +import Int "mo:base/Int"; +import Blob "mo:base/Blob"; +import Bool "mo:base/Bool"; +import Nat32 "mo:base/Nat32"; +import Types "./types"; + +module { + + type FormatType = Types.FormatType; + + public class Base64(isSupportURI : ?Bool) { + + let lookup = HashMap.HashMap(0, Nat32.equal, func (x) : Hash.Hash = x); + var revLookup = HashMap.HashMap(0, Char.equal, func x : Hash.Hash = Char.toNat32(x)); + + let code = Text.toArray("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); + + var _isSupportURI = false; + + private func _initSupportURI(isSupportURI : Bool) { + if (_isSupportURI != isSupportURI) { + _isSupportURI := isSupportURI; + + if (_isSupportURI) { + revLookup.put('-', 62); + revLookup.put('_', 63); + lookup.put(62, '-'); + lookup.put(63, '_'); + } else { + revLookup.put('+', 62); + revLookup.put('/', 63); + lookup.put(62, '+'); + lookup.put(63, '/'); + }; + }; + }; + + private func _init(isSupportURI : Bool) { + for (i in Iter.range(0, code.size() - 1)) { + lookup.put(Nat32.fromNat(i), code[i]); + revLookup.put(code[i], Nat32.fromNat(i)); + }; + _initSupportURI(isSupportURI); + }; + + /** + * Support decoding URL-safe base64 strings. + * RFC 4648 §5: base64url (URL- and filename-safe standard)[a] + * See: https://en.wikipedia.org/wiki/Base64#URL_applications + **/ + switch (isSupportURI) { + case(?sp) { + _init(sp); + }; + case (null) { + _init(_isSupportURI); + }; + }; + + public func decode (b64 : Text) : [Nat8] { + let base64 = Text.toArray(b64); + var padding = 0; + + var result = Buffer.fromArray([]); + var bitcount : Nat32 = 0; + var char : Nat32 = 0; + for (c in base64.vals()) { + if (Char.toNat32(c) != 10 and Char.toNat32(c) != 32) { + if (c == '=') { + padding += 1; + } else { + let val = Option.get(revLookup.get(c), 0); + if (bitcount + 6 >= 8) { + let ret = (char << (8 - bitcount) | (val >> (6 - (8 - bitcount)))) & 0xff; + result.add(Nat8.fromNat(Nat32.toNat(ret))); + bitcount := bitcount + 6 - 8; + if (bitcount == 0) { + char := 0; + } else { + char := val; + } + } else { + char := val; + bitcount := 6; + }; + }; + }; + }; + (Buffer.toArray(result)) + }; + + public func encode (data : FormatType) : Text { + let bytes = switch(data) { + case (#text(t)) { + Blob.toArray(Text.encodeUtf8(t)); + }; + case (#bytes(arr)) { + arr; + }; + }; + var ret = ""; + var remain : Nat32 = 0; + var bits : Nat32 = 0; + var bitcount = 0; + for (byte in bytes.vals()) { + bitcount += 8; + let b = Nat32.fromNat(Nat8.toNat(byte)); + remain += 8; + bits <<= 8; + bits |= b; + while (remain >= 6) { + let c = (bits >> (remain - 6)) & 0x3f; + let char = Option.get(lookup.get(c), ' '); + ret #= Char.toText(char); + remain -= 6; + }; + bits := b & (2 ** remain - 1); + }; + + if (remain != 0) { + bits <<= (6 - remain); + bits &= 0x3f; + let char = Option.get(lookup.get(bits), ' '); + ret #= Char.toText(char); + }; + + // check special case + if (_isSupportURI == false) { + let extraBytes = bitcount % 3; + + if (extraBytes > 0) { + for (i in Iter.range(1, extraBytes)) ret #= "="; + }; + }; + (ret) + }; + + public func isValid(b64 : Text) : Bool { + let base64 = Text.toArray(b64); + let size : Int = if (base64.size() > 0) {base64.size() - 1} else 0; + var i = 0; + for (c in base64.vals()) { + switch (revLookup.get(c)) { + case (?v) { + let charBits = Option.get(lookup.get(v), ' '); + if (c != charBits) { + return false; + }; + }; + case null { + if (Char.toNat32(c) == 10 or Char.toNat32(c) == 32) { + () + } else if (c == '=') { + if (i != size and i != size - 1) { + return false; + }; + } else { + return false; + }; + }; + }; + i += 1; + }; + true; + }; + + public func setSupportURI(isSupportURI : Bool) { + _initSupportURI(isSupportURI); + }; + }; + +} \ No newline at end of file diff --git a/src/lib.mo b/src/lib.mo index fc31520..98aa030 100644 --- a/src/lib.mo +++ b/src/lib.mo @@ -1,21 +1,73 @@ -import Base64 "./base64"; +/******************************************************************* +* Copyright : 2024 nirvana369 +* File Name : v2engine.mo +* Description : Base64 library interface +* +* Revision History : +* Date Author Comments +* --------------------------------------------------------------------------- +* 10/07/2024 nirvana369 Implement. +* 10/08/2024 nirvana369 Implement base64 factory +******************************************************************/ + +import Types "./types"; +import EngineV1 "./base64v1"; +import EngineV2 "./base64v2"; +import Text "mo:base/Text"; module { - public type FormatType = Base64.FormatType; - - public func encode(data : FormatType, isSupportURI : Bool) : Text { - let base64 = Base64.Base64(?isSupportURI); - base64.encode(data); - }; + public type FormatType = Types.FormatType; - public func decode(b64 : Text, isSupportURI : Bool) : [Nat8] { - let base64 = Base64.Base64(?isSupportURI); - base64.decode(b64); + public type Version = { + #version : Text; + #ver : Text; + #v : Text; }; - public func isValid(b64 : Text, isSupportURI : Bool) : Bool { - let base64 = Base64.Base64(?isSupportURI); - base64.isValid(b64); + public let V1 = Types.V1; + public let V2 = Types.V2; + + public class Base64(version : Version, isSupportURI : ?Bool) { + + let engine = switch (version) { + case (#version(index)) { + if (index == V1) { + EngineV1.Base64(isSupportURI); + } else { + EngineV2.Base64(isSupportURI); + }; + }; + case (#ver(index)) { + if (index == V1) { + EngineV1.Base64(isSupportURI); + } else { + EngineV2.Base64(isSupportURI); + }; + }; + case (#v(index)) { + if (index == V1) { + EngineV1.Base64(isSupportURI); + } else { + EngineV2.Base64(isSupportURI); + }; + }; + }; + + public func decode (b64 : Text) : [Nat8] { + engine.decode(b64); + }; + + public func encode (data : Types.FormatType) : Text { + engine.encode(data); + }; + + public func isValid(b64 : Text) : Bool { + engine.isValid(b64); + }; + + public func setSupportURI(isSupportURI : Bool) { + engine.setSupportURI(isSupportURI); + }; }; } \ No newline at end of file diff --git a/src/types.mo b/src/types.mo new file mode 100644 index 0000000..c481a86 --- /dev/null +++ b/src/types.mo @@ -0,0 +1,21 @@ +/******************************************************************* +* Copyright : 2024 nirvana369 +* File Name : types.mo +* Description : Base64 types. +* +* Revision History : +* Date Author Comments +* --------------------------------------------------------------------------- +* 10/07/2024 nirvana369 Implement. +******************************************************************/ + +module { + + public type FormatType = { + #text : Text; + #bytes : [Nat8]; + }; + + public let V1 = "1"; + public let V2 = "2"; +} \ No newline at end of file diff --git a/test/lib.test.mo b/test/base64.test.mo similarity index 75% rename from test/lib.test.mo rename to test/base64.test.mo index a12f251..2d242ef 100644 --- a/test/lib.test.mo +++ b/test/base64.test.mo @@ -1,3 +1,14 @@ +/******************************************************************* +* Copyright : 2024 nirvana369 +* File Name : base64.test.mo +* Description : Base64 version 1. +* +* Revision History : +* Date Author Comments +* --------------------------------------------------------------------------- +* 10/07/2024 nirvana369 Add test case +******************************************************************/ + import {test; suite} "mo:test"; import Blob "mo:base/Blob"; import Text "mo:base/Text"; @@ -7,11 +18,14 @@ import Array "mo:base/Array"; import Nat8 "mo:base/Nat8"; import Buffer "mo:base/Buffer"; import Iter "mo:base/Iter"; -import Base64 "../src"; +import {Base64 = Base64Engine; V1; V2} "../src"; actor { public func runTests() : async () { + + let version = V2; + let Base64 = Base64Engine(#v version, ?false); suite("Base64", func() { let encodeTestVectors = [ @@ -37,7 +51,8 @@ actor { test("encode", func() { for (x in encodeTestVectors.vals()) { - assert(Base64.encode(#text (x.input), x.isSupportURI) == x.output); + Base64.setSupportURI(x.isSupportURI); + assert(Base64.encode(#text (x.input)) == x.output); }; }); @@ -82,13 +97,15 @@ actor { test("decode", func() { for (x in decodeTestVectors.vals()) { - assert(Option.get(Text.decodeUtf8(Blob.fromArray(Base64.decode(x.input, x.isSupportURI))), "") == x.output); + Base64.setSupportURI(x.isSupportURI); + assert(Option.get(Text.decodeUtf8(Blob.fromArray(Base64.decode(x.input))), "") == x.output); }; }); test("isValid", func() { for (x in encodeTestVectors.vals()) { - assert(Base64.isValid(x.output, x.isSupportURI)); + Base64.setSupportURI(x.isSupportURI); + assert(Base64.isValid(x.output)); }; }); }); @@ -118,7 +135,8 @@ actor { test("encode", func() { for (x in encodeTestVectors.vals()) { - assert(Base64.encode(#bytes (x.input), x.isSupportURI) == x.output); + Base64.setSupportURI(x.isSupportURI); + assert(Base64.encode(#bytes (x.input)) == x.output); }; }); @@ -145,13 +163,15 @@ actor { test("decode", func() { for (x in decodeTestVectors.vals()) { - assert(Array.equal(Base64.decode(x.input, x.isSupportURI), x.output, Nat8.equal)); + Base64.setSupportURI(x.isSupportURI); + assert(Array.equal(Base64.decode(x.input), x.output, Nat8.equal)); }; }); test("isValid", func() { for (x in encodeTestVectors.vals()) { - assert(Base64.isValid(x.output, x.isSupportURI)); + Base64.setSupportURI(x.isSupportURI); + assert(Base64.isValid(x.output)); }; }); }); @@ -174,7 +194,8 @@ actor { test("encode", func() { for (x in encodeTestVectors.vals()) { - let encode = Base64.encode(#text (x.input), x.isSupportURI); + Base64.setSupportURI(x.isSupportURI); + let encode = Base64.encode(#text (x.input)); Debug.print(encode); assert(encode == x.output); }; @@ -197,7 +218,8 @@ actor { test("decode", func() { for (x in decodeTestVectors.vals()) { - let decode = Option.get(Text.decodeUtf8(Blob.fromArray(Base64.decode(x.input, x.isSupportURI))), ""); + Base64.setSupportURI(x.isSupportURI); + let decode = Option.get(Text.decodeUtf8(Blob.fromArray(Base64.decode(x.input))), ""); Debug.print(decode); assert(decode == x.output); }; @@ -205,7 +227,8 @@ actor { test("isValid", func() { for (x in encodeTestVectors.vals()) { - assert(Base64.isValid(x.output, x.isSupportURI)); + Base64.setSupportURI(x.isSupportURI); + assert(Base64.isValid(x.output)); }; }); }); @@ -271,8 +294,9 @@ actor { test("encode", func() { for (x in encodeTestVectors.vals()) { - let encodeNat8 = Base64.encode(#bytes (x.nat8arr), x.isSupportURI); - let encodeText = Base64.encode(#text (x.text), x.isSupportURI); + Base64.setSupportURI(x.isSupportURI); + let encodeNat8 = Base64.encode(#bytes (x.nat8arr)); + let encodeText = Base64.encode(#text (x.text)); Debug.print(encodeNat8); Debug.print(encodeText); assert(encodeNat8 == encodeText); @@ -338,24 +362,79 @@ actor { test("decode", func() { for (x in decodeTestVectors.vals()) { - let decode = Base64.decode(x.input, false); + Base64.setSupportURI(x.isSupportURI); + let decode = Base64.decode(x.input); assert(Array.equal(decode, x.output, Nat8.equal)); }; }); + let isValidTestVector = [ + { + input = ""; + output = true; + isSupportURI = false; + }, + { + input = "Z"; + output = true; + isSupportURI = false; + }, + { + input = "ZA"; + output = true; + isSupportURI = false; + }, + { + input = "ZA="; + output = true; + isSupportURI = false; + }, + { + input = "ZA=="; + output = true; + isSupportURI = false; + }, + { + input = "++"; + output = false; + isSupportURI = true; + }, + { + input = "+-"; + output = false; + isSupportURI = true; + }, + { + input = "+-"; + output = false; + isSupportURI = false; + }, + { + input = "--"; + output = true; + isSupportURI = true; + }, + { + input = "//"; + output = true; + isSupportURI = false; + }, + { + input = "__"; + output = true; + isSupportURI = true; + }, + { + input = "/_"; + output = false; + isSupportURI = false; + } + ]; test("isValid", func() { - assert(Base64.isValid("", false) == true); - assert(Base64.isValid("Z", false) == true); - assert(Base64.isValid("ZA", false) == true); - assert(Base64.isValid("ZA=", false) == true); - assert(Base64.isValid("ZA==", false) == true); - assert(Base64.isValid("++", false) == true); - assert(Base64.isValid("+-", true) == false); - assert(Base64.isValid("+-", false) == false); - assert(Base64.isValid("--", true) == true); - assert(Base64.isValid("//", false) == true); - assert(Base64.isValid("__", true) == true); - assert(Base64.isValid("/_", false) == false); + for (x in isValidTestVector.vals()) { + Base64.setSupportURI(x.isSupportURI); + assert(Base64.isValid(x.input) == x.output); + }; }); }); @@ -378,7 +457,8 @@ actor { test("encode", func() { for (x in encodeTestVectors.vals()) { - let encodeText = Base64.encode(#text (x.input), x.isSupportURI); + Base64.setSupportURI(x.isSupportURI); + let encodeText = Base64.encode(#text (x.input)); Debug.print(encodeText); assert(x.output == encodeText); }; @@ -407,7 +487,8 @@ actor { test("decode", func() { for (x in decodeTestVectors.vals()) { - let decode = Base64.decode(x.input, x.isSupportURI); + Base64.setSupportURI(x.isSupportURI); + let decode = Base64.decode(x.input); Debug.print(Text.join(", ", Array.map(decode, func c = Nat8.toText(c)).vals())); Debug.print(Text.join(", ", Array.map(Blob.toArray(Text.encodeUtf8(x.output)), func c = Nat8.toText(c)).vals())); assert(Array.equal(decode, Blob.toArray(Text.encodeUtf8(x.output)), Nat8.equal)); @@ -416,7 +497,8 @@ actor { test("isValid", func() { for (x in decodeTestVectors.vals()) { - assert(Base64.isValid(x.name, x.isSupportURI)); + Base64.setSupportURI(x.isSupportURI); + assert(Base64.isValid(x.name)); }; }); @@ -424,11 +506,13 @@ actor { test("url-safe", func() { let strA = "//++/++/++//"; - let notSupportURI = Base64.decode(strA, false); + Base64.setSupportURI(false); + let notSupportURI = Base64.decode(strA); assert(Array.equal(notSupportURI, expected, Nat8.equal)); let strB = "__--_--_--__"; - let supportURI = Base64.decode(strB, true); + Base64.setSupportURI(true); + let supportURI = Base64.decode(strB); assert(Array.equal(supportURI, expected, Nat8.equal)); }); @@ -438,9 +522,9 @@ actor { for (i in Iter.range(1, MAX_LENGTH)) { big.add(Nat8.fromNat(i % 256)); }; - - let base64 = Base64.encode(#bytes (Buffer.toArray(big)), false); - let base64Decode = Base64.decode(base64, false); + Base64.setSupportURI(false); + let base64 = Base64.encode(#bytes (Buffer.toArray(big))); + let base64Decode = Base64.decode(base64); assert(Array.equal(Buffer.toArray(big), base64Decode, Nat8.equal)); }); });