Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ linters:
allow:
- golang.org/x/exp/constraints$
- github.com/cinode/go-common/
- bytes$
- crypto/subtle$
- crypto/sha256$
- embed$
- errors$
- encoding/hex$
- encoding/json$
- fmt$
- math/big$
- reflect$
- regexp$
- strings$
Expand Down
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"go.coverOnSave": true,
"cSpell.words": [
"cinode"
"cinode",
"cutl",
"picotestify"
]
}
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ It has the following goals over the original testify one:

The interface of picotestify is a subset of the original testify one and thus switching to the original testify library should be easily achievable through go.mod's rewrite option.


## curl - Cinode Utilities
## cutl - Cinode Utilities

A set of small utilities that are shared across other modules.

## blob - type-safe wrappers around various blob-related data

- Name - identification of the specific blob instance
- Type - blob type, associated with blob's name
- Key - encryption keys for encrypting and decryption blob's data
- AuthInfo - data allowing blob update after it's created (for dynamic blobs)
104 changes: 104 additions & 0 deletions base58/base58.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
Copyright © 2025 Bartłomiej Święcki (byo)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package base58

import (
"errors"
"fmt"
"math/big"
)

var ErrInvalidBase58Character = errors.New("invalid base58 character")

var bn2btc, btc2bn = func() (bn2btc, btc2bn [256]byte) {
const btcAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
const bitNumDigits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV"

for i := range btcAlphabet {
bn2btc[bitNumDigits[i]] = btcAlphabet[i]
btc2bn[btcAlphabet[i]] = bitNumDigits[i]
}

return bn2btc, btc2bn
}()

func Encode(data []byte) string {
// Count leading zero bytes, those are treated especially
leadingZeros := 0
for _, b := range data {
if b == 0 {
leadingZeros++
} else {
break
}
}

var bi big.Int
bi.SetBytes(data[leadingZeros:])

txt := bi.Text(58)

if txt == "0" {
txt = ""
}

res := make([]byte, leadingZeros+len(txt))

// Add '1' characters for leading zeros
for i := 0; i < leadingZeros; i++ {
res[i] = '1'
}

// Convert the rest
for i, b := range txt {
res[leadingZeros+i] = bn2btc[b]
}

return string(res)
}

func Decode(s string) ([]byte, error) {
// Split into leading bytes and bit.Int-compatible representation
leadingZeros := 0
leadingZerosDone := false
bnText := make([]byte, 0, len(s))
for _, b := range s {
switch {
case !leadingZerosDone && b == '1':
leadingZeros++
case btc2bn[b] != 0:
leadingZerosDone = true
bnText = append(bnText, btc2bn[b])
default:
return nil, fmt.Errorf("%w: '%v'", ErrInvalidBase58Character, b)
}
}

if len(bnText) == 0 {
return make([]byte, leadingZeros), nil
}

var bn big.Int
bn.SetString(string(bnText), 58)

bnBytes := bn.Bytes()

result := make([]byte, leadingZeros+len(bnBytes))
copy(result[leadingZeros:], bnBytes)

return result, nil
}
23 changes: 23 additions & 0 deletions base58/base58_encode_decode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
["", ""],
["61", "2g"],
["626262", "a3gV"],
["636363", "aPEr"],
["73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"],
["00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"],
["516b6fcd0f", "ABnLTmg"],
["bf4f89001e670274dd", "3SEo3LWLoPntC"],
["572e4794", "3EFU7m"],
["ecac89cad93923c02321", "EJDM8drfXA6uyA"],
["10c8511e", "Rt5zm"],
["00000000000000000000", "1111111111"],
["00000000000000000000000000000000000000000000000000000000000000000000000000000000", "1111111111111111111111111111111111111111"],
["00000000000000000000000000000000000000000000000000000000000000000000000000000001", "1111111111111111111111111111111111111112"],
["0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec39d04c37e71e5d591881f6", "111111111111111111111111111111111111111111111111111111111111111111111111111111111111115TYzLYH1udmLdzCLM"],
["000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5", "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"],
["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", "1cWB5HCBdLjAuqGGReWE3R3CguuwSjw6RHn39s2yuDRTS5NsBgNiFpWgAnEx6VQi8csexkgYw3mdYrMHr8x9i7aEwP8kZ7vccXWqKDvGv3u1GxFKPuAkn8JCPPGDMf3vMMnbzm6Nh9zh1gcNsMvH3ZNLmP5fSG6DGbbi2tuwMWPthr4boWwCxf7ewSgNQeacyozhKDDQQ1qL5fQFUW52QKUZDZ5fw3KXNQJMcNTcaB723LchjeKun7MuGW5qyCBZYzA1KjofN1gYBV3NqyhQJ3Ns746GNuf9N2pQPmHz4xpnSrrfCvy6TVVz5d4PdrjeshsWQwpZsZGzvbdAdN8MKV5QsBDY"],
["271F359E", "zzzzy"],
["271F359F", "zzzzz"],
["271F35A0", "211111"],
["271F35A1", "211112"]
]
128 changes: 128 additions & 0 deletions base58/base58_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright © 2025 Bartłomiej Święcki (byo)

Licensed under the Apache License,
Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package base58_test

import (
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"testing"

"github.com/cinode/go-common/base58"
"github.com/cinode/go-common/picotestify/require"
)

// Test cases from (MIT License):
//
// https://github.com/bitcoin/bitcoin/blob/master/src/test/data/base58_encode_decode.json
//
//go:embed base58_encode_decode.json
var testJSONData string

type testCase struct {
expected string
data []byte
}

func validTestCases(t require.TestingT) []testCase {
testCases := []testCase{
{
data: []byte{},
expected: "",
},
{
data: []byte("Hello World!"),
expected: "2NEpo7TZRRrLZSi2U",
},
{
data: []byte("The quick brown fox jumps over the lazy dog."),
expected: "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z",
},
{
data: []byte{0x00, 0x00, 0x28, 0x7f, 0xb4, 0xcd},
expected: "11233QC4",
},
}

jsonTests := [][]string{}
require.NoError(t, json.Unmarshal([]byte(testJSONData), &jsonTests))

for _, tc := range jsonTests {
require.Len(t, tc, 2)

data, err := hex.DecodeString(tc[0])
require.NoError(t, err)
expected := tc[1]

testCases = append(testCases, testCase{
data: data,
expected: expected,
})
}
return testCases
}

func TestEncodeDecode(t *testing.T) {
for _, test := range validTestCases(t) {
t.Run(fmt.Sprintf("data=%v", test.data), func(t *testing.T) {
encoded := base58.Encode(test.data)
require.Equal(t, test.expected, encoded)

decodedBack, err := base58.Decode(encoded)
require.NoError(t, err)

require.Equal(t, test.data, decodedBack)
})
}
}

func TestErrorOnInvalidDecode(t *testing.T) {
for _, test := range []string{
"@",
"3SEo3LWLoPntC@",
"@3SEo3LWLoPntC",
"3SEo3@LWLoPntC",
} {
t.Run(test, func(t *testing.T) {
decoded, err := base58.Decode(test)
require.Nil(t, decoded)
require.ErrorIs(t, err, base58.ErrInvalidBase58Character)
})
}
}

func FuzzEncodeDecode(f *testing.F) {
for _, test := range validTestCases(f) {
f.Add(test.data)
}

f.Fuzz(func(t *testing.T, a []byte) {
if len(a) > 256 {
// No point in testing those
t.SkipNow()
}

str := base58.Encode(a)
back, err := base58.Decode(str)
require.NoError(t, err)
require.Equal(t, a, back)
})
}
32 changes: 32 additions & 0 deletions blob/auth_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright © 2025 Bartłomiej Święcki (byo)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package blob

import (
"bytes"
"crypto/subtle"
)

// AuthInfo is an opaque data that is necessary to perform update of an existing blob.
//
// Currently used only for dynamic links, auth info contains all the necessary information
// to update the content of the blob. The representation is specific to the blob type
type AuthInfo struct{ data []byte }

func AuthInfoFromBytes(ai []byte) *AuthInfo { return &AuthInfo{data: bytes.Clone(ai)} }
func (a *AuthInfo) Bytes() []byte { return bytes.Clone(a.data) }
func (a *AuthInfo) Equal(a2 *AuthInfo) bool { return subtle.ConstantTimeCompare(a.data, a2.data) == 1 }
31 changes: 31 additions & 0 deletions blob/auth_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright © 2025 Bartłomiej Święcki (byo)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package blob

import (
"testing"

"github.com/cinode/go-common/picotestify/require"
)

func TestAuthInfo(t *testing.T) {
authInfoBytes := []byte{1, 2, 3}
authInfo := AuthInfoFromBytes(authInfoBytes)
require.Equal(t, authInfoBytes, authInfo.Bytes())
require.True(t, authInfo.Equal(AuthInfoFromBytes(authInfoBytes)))
require.Nil(t, new(Key).Bytes())
}
Loading
Loading