├── .codecov.yml ├── .github └── workflows │ ├── generated-pr.yml │ ├── go-check.yml │ ├── go-test.yml │ ├── release-check.yml │ ├── releaser.yml │ ├── stale.yml │ └── tagpush.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── base16.go ├── base2.go ├── base256emoji.go ├── base256emoji_test.go ├── base32.go ├── encoder.go ├── encoder_test.go ├── go.mod ├── go.sum ├── multibase-conv └── main.go ├── multibase.go ├── multibase_test.go ├── package.json ├── spec_test.go └── version.json /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /.github/workflows/generated-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Generated PRs 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-generated-pr.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/go-check.yml: -------------------------------------------------------------------------------- 1 | name: Go Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["master"] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | go-check: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v1.0 19 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Go Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ["master"] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | go-test: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/go-test.yml@v1.0 19 | secrets: 20 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/release-check.yml: -------------------------------------------------------------------------------- 1 | name: Release Checker 2 | 3 | on: 4 | pull_request_target: 5 | paths: [ 'version.json' ] 6 | types: [ opened, synchronize, reopened, labeled, unlabeled ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | release-check: 19 | uses: ipdxco/unified-github-workflows/.github/workflows/release-check.yml@v1.0 20 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Releaser 2 | 3 | on: 4 | push: 5 | paths: [ 'version.json' ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.sha }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | releaser: 17 | uses: ipdxco/unified-github-workflows/.github/workflows/releaser.yml@v1.0 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | uses: ipdxco/unified-github-workflows/.github/workflows/reusable-stale-issue.yml@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/tagpush.yml: -------------------------------------------------------------------------------- 1 | name: Tag Push Checker 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: read 10 | issues: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | releaser: 18 | uses: ipdxco/unified-github-workflows/.github/workflows/tagpush.yml@v1.0 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | multibase-conv/multibase-conv 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec"] 2 | path = spec 3 | url = https://github.com/multiformats/multibase.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Protocol Labs Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: deps 2 | go test -count=1 -race -v ./... 3 | 4 | export IPFS_API ?= v04x.ipfs.io 5 | 6 | deps: 7 | go get -t ./... 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-multibase 2 | 3 | [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) 4 | [![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)](https://github.com/multiformats/multiformats) 5 | [![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](https://webchat.freenode.net/?channels=%23ipfs) 6 | [![](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 7 | [![Travis CI](https://img.shields.io/travis/multiformats/go-multibase.svg?style=flat-square&branch=master)](https://travis-ci.org/multiformats/go-multibase) 8 | [![codecov.io](https://img.shields.io/codecov/c/github/multiformats/go-multibase.svg?style=flat-square&branch=master)](https://codecov.io/github/multiformats/go-multibase?branch=master) 9 | 10 | > Implementation of [multibase](https://github.com/multiformats/multibase) -self identifying base encodings- in Go. 11 | 12 | 13 | ## Install 14 | 15 | `go-multibase` is a standard Go module which can be installed with: 16 | 17 | ```sh 18 | go get github.com/multiformats/go-multibase 19 | ``` 20 | 21 | ## Contribute 22 | 23 | Contributions welcome. Please check out [the issues](https://github.com/multiformats/go-multibase/issues). 24 | 25 | Check out our [contributing document](https://github.com/multiformats/multiformats/blob/master/contributing.md) for more information on how we work, and about contributing in general. Please be aware that all interactions related to multiformats are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 26 | 27 | Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 28 | 29 | ## License 30 | 31 | [MIT](LICENSE) © 2016 Protocol Labs Inc. 32 | -------------------------------------------------------------------------------- /base16.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | func hexEncodeToStringUpper(src []byte) string { 4 | dst := make([]byte, len(src)*2) 5 | hexEncodeUpper(dst, src) 6 | return string(dst) 7 | } 8 | 9 | var hexTableUppers = [16]byte{ 10 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 11 | 'A', 'B', 'C', 'D', 'E', 'F', 12 | } 13 | 14 | func hexEncodeUpper(dst, src []byte) int { 15 | for i, v := range src { 16 | dst[i*2] = hexTableUppers[v>>4] 17 | dst[i*2+1] = hexTableUppers[v&0x0f] 18 | } 19 | 20 | return len(src) * 2 21 | } 22 | -------------------------------------------------------------------------------- /base2.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // binaryEncodeToString takes an array of bytes and returns 10 | // multibase binary representation 11 | func binaryEncodeToString(src []byte) string { 12 | dst := make([]byte, len(src)*8) 13 | encodeBinary(dst, src) 14 | return string(dst) 15 | } 16 | 17 | // encodeBinary takes the src and dst bytes and converts each 18 | // byte to their binary rep using power reduction method 19 | func encodeBinary(dst []byte, src []byte) { 20 | for i, b := range src { 21 | for j := 0; j < 8; j++ { 22 | if b&(1<>3) 40 | 41 | for i, dstIndex := 0, 0; i < len(s); i = i + 8 { 42 | value, err := strconv.ParseInt(s[i:i+8], 2, 0) 43 | if err != nil { 44 | return nil, fmt.Errorf("error while conversion: %s", err) 45 | } 46 | 47 | data[dstIndex] = byte(value) 48 | dstIndex++ 49 | } 50 | 51 | return data, nil 52 | } 53 | -------------------------------------------------------------------------------- /base256emoji.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "unicode/utf8" 7 | ) 8 | 9 | var base256emojiTable = [256]rune{ 10 | // Curated list, this is just a list of things that *somwhat* are related to our comunity 11 | '🚀', '🪐', '☄', '🛰', '🌌', // Space 12 | '🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘', // Moon 13 | '🌍', '🌏', '🌎', // Our Home, for now (earth) 14 | '🐉', // Dragon!!! 15 | '☀', // Our Garden, for now (sol) 16 | '💻', '🖥', '💾', '💿', // Computer 17 | // The rest is completed from https://home.unicode.org/emoji/emoji-frequency/ at the time of creation (december 2021) (the data is from 2019), most used first until we reach 256. 18 | // We exclude modifier based emojies (such as flags) as they are bigger than one single codepoint. 19 | // Some other emojies were removed adhoc for various reasons. 20 | '😂', '❤', '😍', '🤣', '😊', '🙏', '💕', '😭', '😘', '👍', 21 | '😅', '👏', '😁', '🔥', '🥰', '💔', '💖', '💙', '😢', '🤔', 22 | '😆', '🙄', '💪', '😉', '☺', '👌', '🤗', '💜', '😔', '😎', 23 | '😇', '🌹', '🤦', '🎉', '💞', '✌', '✨', '🤷', '😱', '😌', 24 | '🌸', '🙌', '😋', '💗', '💚', '😏', '💛', '🙂', '💓', '🤩', 25 | '😄', '😀', '🖤', '😃', '💯', '🙈', '👇', '🎶', '😒', '🤭', 26 | '❣', '😜', '💋', '👀', '😪', '😑', '💥', '🙋', '😞', '😩', 27 | '😡', '🤪', '👊', '🥳', '😥', '🤤', '👉', '💃', '😳', '✋', 28 | '😚', '😝', '😴', '🌟', '😬', '🙃', '🍀', '🌷', '😻', '😓', 29 | '⭐', '✅', '🥺', '🌈', '😈', '🤘', '💦', '✔', '😣', '🏃', 30 | '💐', '☹', '🎊', '💘', '😠', '☝', '😕', '🌺', '🎂', '🌻', 31 | '😐', '🖕', '💝', '🙊', '😹', '🗣', '💫', '💀', '👑', '🎵', 32 | '🤞', '😛', '🔴', '😤', '🌼', '😫', '⚽', '🤙', '☕', '🏆', 33 | '🤫', '👈', '😮', '🙆', '🍻', '🍃', '🐶', '💁', '😲', '🌿', 34 | '🧡', '🎁', '⚡', '🌞', '🎈', '❌', '✊', '👋', '😰', '🤨', 35 | '😶', '🤝', '🚶', '💰', '🍓', '💢', '🤟', '🙁', '🚨', '💨', 36 | '🤬', '✈', '🎀', '🍺', '🤓', '😙', '💟', '🌱', '😖', '👶', 37 | '🥴', '▶', '➡', '❓', '💎', '💸', '⬇', '😨', '🌚', '🦋', 38 | '😷', '🕺', '⚠', '🙅', '😟', '😵', '👎', '🤲', '🤠', '🤧', 39 | '📌', '🔵', '💅', '🧐', '🐾', '🍒', '😗', '🤑', '🌊', '🤯', 40 | '🐷', '☎', '💧', '😯', '💆', '👆', '🎤', '🙇', '🍑', '❄', 41 | '🌴', '💣', '🐸', '💌', '📍', '🥀', '🤢', '👅', '💡', '💩', 42 | '👐', '📸', '👻', '🤐', '🤮', '🎼', '🥵', '🚩', '🍎', '🍊', 43 | '👼', '💍', '📣', '🥂', 44 | } 45 | 46 | var base256emojiReverseTable map[rune]byte 47 | 48 | func init() { 49 | base256emojiReverseTable = make(map[rune]byte, len(base256emojiTable)) 50 | for i, v := range base256emojiTable { 51 | base256emojiReverseTable[v] = byte(i) 52 | } 53 | } 54 | 55 | func base256emojiEncode(in []byte) string { 56 | var l int 57 | for _, v := range in { 58 | l += utf8.RuneLen(base256emojiTable[v]) 59 | } 60 | var out strings.Builder 61 | out.Grow(l) 62 | for _, v := range in { 63 | out.WriteRune(base256emojiTable[v]) 64 | } 65 | return out.String() 66 | } 67 | 68 | type base256emojiCorruptInputError struct { 69 | index int 70 | char rune 71 | } 72 | 73 | func (e base256emojiCorruptInputError) Error() string { 74 | return "illegal base256emoji data at input byte " + strconv.FormatInt(int64(e.index), 10) + ", char: '" + string(e.char) + "'" 75 | } 76 | 77 | func (e base256emojiCorruptInputError) String() string { 78 | return e.Error() 79 | } 80 | 81 | func base256emojiDecode(in string) ([]byte, error) { 82 | out := make([]byte, utf8.RuneCountInString(in)) 83 | var stri int 84 | for i := 0; len(in) > 0; i++ { 85 | r, n := utf8.DecodeRuneInString(in) 86 | in = in[n:] 87 | var ok bool 88 | out[i], ok = base256emojiReverseTable[r] 89 | if !ok { 90 | return nil, base256emojiCorruptInputError{stri, r} 91 | } 92 | stri += n 93 | } 94 | return out, nil 95 | } 96 | -------------------------------------------------------------------------------- /base256emoji_test.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import "testing" 4 | 5 | func TestBase256EmojiAlphabet(t *testing.T) { 6 | var c uint 7 | for _, v := range base256emojiTable { 8 | if v != rune(0) { 9 | c++ 10 | } 11 | } 12 | if c != 256 { 13 | t.Errorf("Base256Emoji count is wrong, expected 256, got %d.", c) 14 | } 15 | } 16 | 17 | func TestBase256EmojiUniq(t *testing.T) { 18 | m := make(map[rune]struct{}, len(base256emojiTable)) 19 | for i, v := range base256emojiTable { 20 | _, ok := m[v] 21 | if ok { 22 | t.Errorf("Base256Emoji duplicate %s at index %d.", string(v), i) 23 | } 24 | m[v] = struct{}{} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /base32.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | b32 "github.com/multiformats/go-base32" 5 | ) 6 | 7 | var base32StdLowerPad = b32.NewEncodingCI("abcdefghijklmnopqrstuvwxyz234567") 8 | var base32StdLowerNoPad = base32StdLowerPad.WithPadding(b32.NoPadding) 9 | 10 | var base32StdUpperPad = b32.NewEncodingCI("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") 11 | var base32StdUpperNoPad = base32StdUpperPad.WithPadding(b32.NoPadding) 12 | 13 | var base32HexLowerPad = b32.NewEncodingCI("0123456789abcdefghijklmnopqrstuv") 14 | var base32HexLowerNoPad = base32HexLowerPad.WithPadding(b32.NoPadding) 15 | 16 | var base32HexUpperPad = b32.NewEncodingCI("0123456789ABCDEFGHIJKLMNOPQRSTUV") 17 | var base32HexUpperNoPad = base32HexUpperPad.WithPadding(b32.NoPadding) 18 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "fmt" 5 | "unicode/utf8" 6 | ) 7 | 8 | // Encoder is a multibase encoding that is verified to be supported and 9 | // supports an Encode method that does not return an error 10 | type Encoder struct { 11 | enc Encoding 12 | } 13 | 14 | // NewEncoder create a new Encoder from an Encoding 15 | func NewEncoder(base Encoding) (Encoder, error) { 16 | _, ok := EncodingToStr[base] 17 | if !ok { 18 | return Encoder{-1}, fmt.Errorf("unsupported multibase encoding: %d", base) 19 | } 20 | return Encoder{base}, nil 21 | } 22 | 23 | // MustNewEncoder is like NewEncoder but will panic if the encoding is 24 | // invalid. 25 | func MustNewEncoder(base Encoding) Encoder { 26 | _, ok := EncodingToStr[base] 27 | if !ok { 28 | panic("Unsupported multibase encoding") 29 | } 30 | return Encoder{base} 31 | } 32 | 33 | // EncoderByName creates an encoder from a string, the string can 34 | // either be the multibase name or single character multibase prefix 35 | func EncoderByName(str string) (Encoder, error) { 36 | var base Encoding 37 | var ok bool 38 | if len(str) == 0 { 39 | return Encoder{-1}, fmt.Errorf("empty multibase encoding") 40 | } else if utf8.RuneCountInString(str) == 1 { 41 | r, _ := utf8.DecodeRuneInString(str) 42 | base = Encoding(r) 43 | _, ok = EncodingToStr[base] 44 | } else { 45 | base, ok = Encodings[str] 46 | } 47 | if !ok { 48 | return Encoder{-1}, fmt.Errorf("unsupported multibase encoding: %s", str) 49 | } 50 | return Encoder{base}, nil 51 | } 52 | 53 | func (p Encoder) Encoding() Encoding { 54 | return p.enc 55 | } 56 | 57 | // Encode encodes the multibase using the given Encoder. 58 | func (p Encoder) Encode(data []byte) string { 59 | str, err := Encode(p.enc, data) 60 | if err != nil { 61 | // should not happen 62 | panic(err) 63 | } 64 | return str 65 | } 66 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "testing" 5 | "unicode/utf8" 6 | ) 7 | 8 | func TestInvalidCode(t *testing.T) { 9 | _, err := NewEncoder('q') 10 | if err == nil { 11 | t.Error("expected failure") 12 | } 13 | } 14 | 15 | func TestInvalidName(t *testing.T) { 16 | values := []string{"invalid", "", "q"} 17 | for _, val := range values { 18 | _, err := EncoderByName(val) 19 | if err == nil { 20 | t.Errorf("EncoderByName(%v) expected failure", val) 21 | } 22 | } 23 | } 24 | 25 | func TestEncoder(t *testing.T) { 26 | for name, code := range Encodings { 27 | encoder, err := NewEncoder(code) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | // Make sure the MustNewEncoder doesn't panic 32 | MustNewEncoder(code) 33 | str, err := Encode(code, sampleBytes) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | str2 := encoder.Encode(sampleBytes) 38 | if str != str2 { 39 | t.Errorf("encoded string mismatch: %s != %s", str, str2) 40 | } 41 | _, err = EncoderByName(name) 42 | if err != nil { 43 | t.Fatalf("EncoderByName(%s) failed: %v", name, err) 44 | } 45 | // Test that an encoder can be created from the single letter 46 | // prefix 47 | r, _ := utf8.DecodeRuneInString(str) 48 | _, err = EncoderByName(string(r)) 49 | if err != nil { 50 | t.Fatalf("EncoderByName(%s) failed: %v", string(r), err) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/multiformats/go-multibase 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/mr-tron/base58 v1.1.0 7 | github.com/multiformats/go-base32 v0.0.3 8 | github.com/multiformats/go-base36 v0.1.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mr-tron/base58 v1.1.0 h1:Y51FGVJ91WBqCEabAi5OPUz38eAx8DakuAm5svLcsfQ= 2 | github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= 3 | github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= 4 | github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= 5 | github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= 6 | github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= 7 | -------------------------------------------------------------------------------- /multibase-conv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | multibase "github.com/multiformats/go-multibase" 8 | ) 9 | 10 | func main() { 11 | if len(os.Args) < 3 { 12 | fmt.Printf("usage: %s ...\n", os.Args[0]) 13 | os.Exit(1) 14 | } 15 | 16 | var newBase multibase.Encoding 17 | if baseParam := os.Args[1]; len(baseParam) != 0 { 18 | newBase = multibase.Encoding(baseParam[0]) 19 | } else { 20 | fmt.Fprintln(os.Stderr, " is empty") 21 | os.Exit(1) 22 | } 23 | 24 | input := os.Args[2:] 25 | 26 | for _, strmbase := range input { 27 | _, data, err := multibase.Decode(strmbase) 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "error while decoding: %s\n", err) 30 | os.Exit(1) 31 | } 32 | 33 | newCid, err := multibase.Encode(newBase, data) 34 | if err != nil { 35 | fmt.Fprintf(os.Stderr, "error while encoding: %s\n", err) 36 | os.Exit(1) 37 | } 38 | fmt.Println(newCid) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /multibase.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "fmt" 7 | "unicode/utf8" 8 | 9 | b58 "github.com/mr-tron/base58/base58" 10 | b32 "github.com/multiformats/go-base32" 11 | b36 "github.com/multiformats/go-base36" 12 | ) 13 | 14 | // Encoding identifies the type of base-encoding that a multibase is carrying. 15 | type Encoding int 16 | 17 | // These are the encodings specified in the standard, not are all 18 | // supported yet 19 | const ( 20 | Identity = 0x00 21 | Base2 = '0' 22 | Base8 = '7' 23 | Base10 = '9' 24 | Base16 = 'f' 25 | Base16Upper = 'F' 26 | Base32 = 'b' 27 | Base32Upper = 'B' 28 | Base32pad = 'c' 29 | Base32padUpper = 'C' 30 | Base32hex = 'v' 31 | Base32hexUpper = 'V' 32 | Base32hexPad = 't' 33 | Base32hexPadUpper = 'T' 34 | Base36 = 'k' 35 | Base36Upper = 'K' 36 | Base58BTC = 'z' 37 | Base58Flickr = 'Z' 38 | Base64 = 'm' 39 | Base64url = 'u' 40 | Base64pad = 'M' 41 | Base64urlPad = 'U' 42 | Base256Emoji = '🚀' 43 | ) 44 | 45 | // EncodingToStr is a map of the supported encoding, unsupported encoding 46 | // specified in standard are left out 47 | var EncodingToStr = map[Encoding]string{ 48 | 0x00: "identity", 49 | '0': "base2", 50 | 'f': "base16", 51 | 'F': "base16upper", 52 | 'b': "base32", 53 | 'B': "base32upper", 54 | 'c': "base32pad", 55 | 'C': "base32padupper", 56 | 'v': "base32hex", 57 | 'V': "base32hexupper", 58 | 't': "base32hexpad", 59 | 'T': "base32hexpadupper", 60 | 'k': "base36", 61 | 'K': "base36upper", 62 | 'z': "base58btc", 63 | 'Z': "base58flickr", 64 | 'm': "base64", 65 | 'u': "base64url", 66 | 'M': "base64pad", 67 | 'U': "base64urlpad", 68 | Base256Emoji: "base256emoji", 69 | } 70 | 71 | var Encodings = map[string]Encoding{} 72 | 73 | func init() { 74 | for e, n := range EncodingToStr { 75 | Encodings[n] = e 76 | } 77 | } 78 | 79 | // ErrUnsupportedEncoding is returned when the selected encoding is not known or 80 | // implemented. 81 | var ErrUnsupportedEncoding = fmt.Errorf("selected encoding not supported") 82 | 83 | // Encode encodes a given byte slice with the selected encoding and returns a 84 | // multibase string (). It will return 85 | // an error if the selected base is not known. 86 | func Encode(base Encoding, data []byte) (string, error) { 87 | switch base { 88 | case Identity: 89 | // 0x00 inside a string is OK in golang and causes no problems with the length calculation. 90 | return string(rune(Identity)) + string(data), nil 91 | case Base2: 92 | return string(Base2) + binaryEncodeToString(data), nil 93 | case Base16: 94 | return string(Base16) + hex.EncodeToString(data), nil 95 | case Base16Upper: 96 | return string(Base16Upper) + hexEncodeToStringUpper(data), nil 97 | case Base32: 98 | return string(Base32) + base32StdLowerNoPad.EncodeToString(data), nil 99 | case Base32Upper: 100 | return string(Base32Upper) + base32StdUpperNoPad.EncodeToString(data), nil 101 | case Base32hex: 102 | return string(Base32hex) + base32HexLowerNoPad.EncodeToString(data), nil 103 | case Base32hexUpper: 104 | return string(Base32hexUpper) + base32HexUpperNoPad.EncodeToString(data), nil 105 | case Base32pad: 106 | return string(Base32pad) + base32StdLowerPad.EncodeToString(data), nil 107 | case Base32padUpper: 108 | return string(Base32padUpper) + base32StdUpperPad.EncodeToString(data), nil 109 | case Base32hexPad: 110 | return string(Base32hexPad) + base32HexLowerPad.EncodeToString(data), nil 111 | case Base32hexPadUpper: 112 | return string(Base32hexPadUpper) + base32HexUpperPad.EncodeToString(data), nil 113 | case Base36: 114 | return string(Base36) + b36.EncodeToStringLc(data), nil 115 | case Base36Upper: 116 | return string(Base36Upper) + b36.EncodeToStringUc(data), nil 117 | case Base58BTC: 118 | return string(Base58BTC) + b58.EncodeAlphabet(data, b58.BTCAlphabet), nil 119 | case Base58Flickr: 120 | return string(Base58Flickr) + b58.EncodeAlphabet(data, b58.FlickrAlphabet), nil 121 | case Base64pad: 122 | return string(Base64pad) + base64.StdEncoding.EncodeToString(data), nil 123 | case Base64urlPad: 124 | return string(Base64urlPad) + base64.URLEncoding.EncodeToString(data), nil 125 | case Base64url: 126 | return string(Base64url) + base64.RawURLEncoding.EncodeToString(data), nil 127 | case Base64: 128 | return string(Base64) + base64.RawStdEncoding.EncodeToString(data), nil 129 | case Base256Emoji: 130 | return string(Base256Emoji) + base256emojiEncode(data), nil 131 | default: 132 | return "", ErrUnsupportedEncoding 133 | } 134 | } 135 | 136 | // Decode takes a multibase string and decodes into a bytes buffer. 137 | // It will return an error if the selected base is not known. 138 | func Decode(data string) (Encoding, []byte, error) { 139 | if len(data) == 0 { 140 | return 0, nil, fmt.Errorf("cannot decode multibase for zero length string") 141 | } 142 | 143 | r, _ := utf8.DecodeRuneInString(data) 144 | enc := Encoding(r) 145 | 146 | switch enc { 147 | case Identity: 148 | return Identity, []byte(data[1:]), nil 149 | case Base2: 150 | bytes, err := decodeBinaryString(data[1:]) 151 | return enc, bytes, err 152 | case Base16, Base16Upper: 153 | bytes, err := hex.DecodeString(data[1:]) 154 | return enc, bytes, err 155 | case Base32, Base32Upper: 156 | bytes, err := b32.RawStdEncoding.DecodeString(data[1:]) 157 | return enc, bytes, err 158 | case Base32hex, Base32hexUpper: 159 | bytes, err := b32.RawHexEncoding.DecodeString(data[1:]) 160 | return enc, bytes, err 161 | case Base32pad, Base32padUpper: 162 | bytes, err := b32.StdEncoding.DecodeString(data[1:]) 163 | return enc, bytes, err 164 | case Base32hexPad, Base32hexPadUpper: 165 | bytes, err := b32.HexEncoding.DecodeString(data[1:]) 166 | return enc, bytes, err 167 | case Base36, Base36Upper: 168 | bytes, err := b36.DecodeString(data[1:]) 169 | return enc, bytes, err 170 | case Base58BTC: 171 | bytes, err := b58.DecodeAlphabet(data[1:], b58.BTCAlphabet) 172 | return Base58BTC, bytes, err 173 | case Base58Flickr: 174 | bytes, err := b58.DecodeAlphabet(data[1:], b58.FlickrAlphabet) 175 | return Base58Flickr, bytes, err 176 | case Base64pad: 177 | bytes, err := base64.StdEncoding.DecodeString(data[1:]) 178 | return Base64pad, bytes, err 179 | case Base64urlPad: 180 | bytes, err := base64.URLEncoding.DecodeString(data[1:]) 181 | return Base64urlPad, bytes, err 182 | case Base64: 183 | bytes, err := base64.RawStdEncoding.DecodeString(data[1:]) 184 | return Base64, bytes, err 185 | case Base64url: 186 | bytes, err := base64.RawURLEncoding.DecodeString(data[1:]) 187 | return Base64url, bytes, err 188 | case Base256Emoji: 189 | bytes, err := base256emojiDecode(data[4:]) 190 | return Base256Emoji, bytes, err 191 | default: 192 | return -1, nil, ErrUnsupportedEncoding 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /multibase_test.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "sort" 7 | "testing" 8 | ) 9 | 10 | func TestMap(t *testing.T) { 11 | for s, e := range Encodings { 12 | s2 := EncodingToStr[e] 13 | if s != s2 { 14 | t.Errorf("round trip failed on encoding map: %s != %s", s, s2) 15 | } 16 | } 17 | for e, s := range EncodingToStr { 18 | e2 := Encodings[s] 19 | if e != e2 { 20 | t.Errorf("round trip failed on encoding map: '%c' != '%c'", e, e2) 21 | } 22 | } 23 | } 24 | 25 | var sampleBytes = []byte("Decentralize everything!!!") 26 | var encodedSamples = map[Encoding]string{ 27 | Identity: string(rune(0x00)) + "Decentralize everything!!!", 28 | Base2: "00100010001100101011000110110010101101110011101000111001001100001011011000110100101111010011001010010000001100101011101100110010101110010011110010111010001101000011010010110111001100111001000010010000100100001", 29 | Base16: "f446563656e7472616c697a652065766572797468696e67212121", 30 | Base16Upper: "F446563656E7472616C697A652065766572797468696E67212121", 31 | Base32: "birswgzloorzgc3djpjssazlwmvzhs5dinfxgoijbee", 32 | Base32Upper: "BIRSWGZLOORZGC3DJPJSSAZLWMVZHS5DINFXGOIJBEE", 33 | Base32pad: "cirswgzloorzgc3djpjssazlwmvzhs5dinfxgoijbee======", 34 | Base32padUpper: "CIRSWGZLOORZGC3DJPJSSAZLWMVZHS5DINFXGOIJBEE======", 35 | Base32hex: "v8him6pbeehp62r39f9ii0pbmclp7it38d5n6e89144", 36 | Base32hexUpper: "V8HIM6PBEEHP62R39F9II0PBMCLP7IT38D5N6E89144", 37 | Base32hexPad: "t8him6pbeehp62r39f9ii0pbmclp7it38d5n6e89144======", 38 | Base32hexPadUpper: "T8HIM6PBEEHP62R39F9II0PBMCLP7IT38D5N6E89144======", 39 | Base36: "km552ng4dabi4neu1oo8l4i5mndwmpc3mkukwtxy9", 40 | Base36Upper: "KM552NG4DABI4NEU1OO8L4I5MNDWMPC3MKUKWTXY9", 41 | Base58BTC: "z36UQrhJq9fNDS7DiAHM9YXqDHMPfr4EMArvt", 42 | Base58Flickr: "Z36tpRGiQ9Endr7dHahm9xwQdhmoER4emaRVT", 43 | Base64: "mRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE", 44 | Base64url: "uRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE", 45 | Base64pad: "MRGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE=", 46 | Base64urlPad: "URGVjZW50cmFsaXplIGV2ZXJ5dGhpbmchISE=", 47 | Base256Emoji: "🚀💛✋💃✋😻😈🥺🤤🍀🌟💐✋😅✋💦✋🥺🏃😈😴🌟😻😝👏👏👏", 48 | } 49 | 50 | func testEncode(t *testing.T, encoding Encoding, bytes []byte, expected string) { 51 | actual, err := Encode(encoding, bytes) 52 | if err != nil { 53 | t.Error(err) 54 | return 55 | } 56 | if actual != expected { 57 | t.Errorf("encoding failed for %c (%d / %s), expected: %s, got: %s", encoding, encoding, EncodingToStr[encoding], expected, actual) 58 | } 59 | } 60 | 61 | func testDecode(t *testing.T, expectedEncoding Encoding, expectedBytes []byte, data string) { 62 | actualEncoding, actualBytes, err := Decode(data) 63 | if err != nil { 64 | t.Error(err) 65 | return 66 | } 67 | if actualEncoding != expectedEncoding { 68 | t.Errorf("wrong encoding code, expected: %c (%d), got %c (%d)", expectedEncoding, expectedEncoding, actualEncoding, actualEncoding) 69 | } 70 | if !bytes.Equal(actualBytes, expectedBytes) { 71 | t.Errorf("decoding failed for %c (%d), expected: %v, got %v", actualEncoding, actualEncoding, expectedBytes, actualBytes) 72 | } 73 | } 74 | 75 | func TestEncode(t *testing.T) { 76 | for encoding := range EncodingToStr { 77 | testEncode(t, encoding, sampleBytes, encodedSamples[encoding]) 78 | } 79 | } 80 | 81 | func TestDecode(t *testing.T) { 82 | for encoding := range EncodingToStr { 83 | testDecode(t, encoding, sampleBytes, encodedSamples[encoding]) 84 | } 85 | } 86 | 87 | func TestRoundTrip(t *testing.T) { 88 | 89 | for base := range EncodingToStr { 90 | if int(base) == 0 { 91 | // skip identity: any byte goes there 92 | continue 93 | } 94 | 95 | _, _, err := Decode(string(rune(base)) + "\u00A0") 96 | if err == nil { 97 | t.Fatal(EncodingToStr[base] + " decode should fail on low-unicode") 98 | } 99 | 100 | _, _, err = Decode(string(rune(base)) + "\u1F4A8") 101 | if err == nil { 102 | t.Fatal(EncodingToStr[base] + " decode should fail on emoji") 103 | } 104 | 105 | _, _, err = Decode(string(rune(base)) + "!") 106 | if err == nil { 107 | t.Fatal(EncodingToStr[base] + " decode should fail on punctuation") 108 | } 109 | 110 | _, _, err = Decode(string(rune(base)) + "\xA0") 111 | if err == nil { 112 | t.Fatal(EncodingToStr[base] + " decode should fail on high-latin1") 113 | } 114 | } 115 | 116 | buf := make([]byte, 137+16) // sufficiently large prime number of bytes + another 16 to test leading 0s 117 | rand.Read(buf[16:]) 118 | 119 | for base := range EncodingToStr { 120 | 121 | // test roundtrip from the full zero-prefixed buffer down to a single byte 122 | for i := 0; i < len(buf); i++ { 123 | 124 | // use a copy to verify we are not overwriting the supplied buffer 125 | newBuf := make([]byte, len(buf)-i) 126 | copy(newBuf, buf[i:]) 127 | 128 | enc, err := Encode(base, newBuf) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | e, out, err := Decode(enc) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | if e != base { 139 | t.Fatal("got wrong encoding out") 140 | } 141 | 142 | if !bytes.Equal(newBuf, buf[i:]) { 143 | t.Fatal("the provided buffer was modified", buf[i:], out) 144 | } 145 | 146 | if !bytes.Equal(buf[i:], out) { 147 | t.Fatal("input wasnt the same as output", buf[i:], out) 148 | } 149 | 150 | // When we have 3 leading zeroes, do a few extra tests 151 | // ( choice of leading zeroes is arbitrary - just cutting down on test permutations ) 152 | 153 | if i == 13 { 154 | 155 | // if this is a case-insensitive codec semi-randomly swap case in enc and try again 156 | name := EncodingToStr[base] 157 | if name[len(name)-5:] == "upper" || Encodings[name+"upper"] > 0 { 158 | caseTamperedEnc := []byte(enc) 159 | 160 | for _, j := range []int{3, 5, 8, 13, 21, 23, 29, 47, 52} { 161 | if caseTamperedEnc[j] >= 65 && caseTamperedEnc[j] <= 90 { 162 | caseTamperedEnc[j] += 32 163 | } else if caseTamperedEnc[j] >= 97 && caseTamperedEnc[j] <= 122 { 164 | caseTamperedEnc[j] -= 32 165 | } 166 | } 167 | 168 | e, out, err := Decode(string(caseTamperedEnc)) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | 173 | if e != base { 174 | t.Fatal("got wrong encoding out") 175 | } 176 | if !bytes.Equal(buf[i:], out) { 177 | t.Fatal("input wasn't the same as output", buf[i:], out) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | // Test that nothing overflows 185 | maxValueBuf := make([]byte, 131) 186 | for i := 0; i < len(maxValueBuf); i++ { 187 | maxValueBuf[i] = 0xFF 188 | } 189 | 190 | for base := range EncodingToStr { 191 | 192 | // test roundtrip from the complete buffer down to a single byte 193 | for i := 0; i < len(maxValueBuf); i++ { 194 | 195 | enc, err := Encode(base, maxValueBuf[i:]) 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | e, out, err := Decode(enc) 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | 205 | if e != base { 206 | t.Fatal("got wrong encoding out") 207 | } 208 | 209 | if !bytes.Equal(maxValueBuf[i:], out) { 210 | t.Fatal("input wasn't the same as output", maxValueBuf[i:], out) 211 | } 212 | } 213 | } 214 | 215 | _, _, err := Decode("") 216 | if err == nil { 217 | t.Fatal("shouldn't be able to decode empty string") 218 | } 219 | } 220 | 221 | var benchmarkBuf [36]byte // typical CID size 222 | var benchmarkCodecs []string 223 | 224 | func init() { 225 | rand.Read(benchmarkBuf[:]) 226 | 227 | benchmarkCodecs = make([]string, 0, len(Encodings)) 228 | for n := range Encodings { 229 | 230 | // // Only bench b36 and b58 231 | // if len(n) < 6 || (n[4:6] != "36" && n[4:6] != "58") { 232 | // continue 233 | // } 234 | 235 | benchmarkCodecs = append(benchmarkCodecs, n) 236 | } 237 | sort.Strings(benchmarkCodecs) 238 | } 239 | 240 | func BenchmarkRoundTrip(b *testing.B) { 241 | b.ResetTimer() 242 | 243 | for _, name := range benchmarkCodecs { 244 | b.Run(name, func(b *testing.B) { 245 | base := Encodings[name] 246 | for i := 0; i < b.N; i++ { 247 | enc, err := Encode(base, benchmarkBuf[:]) 248 | if err != nil { 249 | b.Fatal(err) 250 | } 251 | 252 | e, out, err := Decode(enc) 253 | if err != nil { 254 | b.Fatal(err) 255 | } 256 | 257 | if e != base { 258 | b.Fatal("got wrong encoding out") 259 | } 260 | 261 | if !bytes.Equal(benchmarkBuf[:], out) { 262 | b.Fatal("input wasnt the same as output", benchmarkBuf, out) 263 | } 264 | } 265 | }) 266 | } 267 | } 268 | 269 | func BenchmarkEncode(b *testing.B) { 270 | b.ResetTimer() 271 | 272 | for _, name := range benchmarkCodecs { 273 | b.Run(name, func(b *testing.B) { 274 | base := Encodings[name] 275 | for i := 0; i < b.N; i++ { 276 | _, err := Encode(base, benchmarkBuf[:]) 277 | if err != nil { 278 | b.Fatal(err) 279 | } 280 | } 281 | }) 282 | } 283 | } 284 | 285 | func BenchmarkDecode(b *testing.B) { 286 | b.ResetTimer() 287 | 288 | for _, name := range benchmarkCodecs { 289 | b.Run(name, func(b *testing.B) { 290 | enc, _ := Encode(Encodings[name], benchmarkBuf[:]) 291 | for i := 0; i < b.N; i++ { 292 | _, _, err := Decode(enc) 293 | if err != nil { 294 | b.Fatal(err) 295 | } 296 | } 297 | }) 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "whyrusleeping", 3 | "bugs": { 4 | "url": "https://github.com/multiformats/go-multibase" 5 | }, 6 | "language": "go", 7 | "license": "", 8 | "name": "go-multibase", 9 | "version": "0.3.0" 10 | } 11 | -------------------------------------------------------------------------------- /spec_test.go: -------------------------------------------------------------------------------- 1 | package multibase 2 | 3 | import ( 4 | "encoding/csv" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | "unicode/utf8" 11 | ) 12 | 13 | func TestSpec(t *testing.T) { 14 | file, err := os.Open("spec/multibase.csv") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer file.Close() 19 | 20 | reader := csv.NewReader(file) 21 | reader.LazyQuotes = false 22 | reader.FieldsPerRecord = 4 23 | reader.TrimLeadingSpace = true 24 | 25 | values, err := reader.ReadAll() 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | expectedEncodings := make(map[Encoding]string, len(values)-1) 30 | for _, v := range values[1:] { 31 | encoding := v[0] 32 | codeStr := v[1] 33 | 34 | var code Encoding 35 | if strings.HasPrefix(codeStr, "0x") { 36 | i, err := strconv.ParseUint(codeStr[2:], 16, 64) 37 | if err != nil { 38 | t.Errorf("invalid multibase byte %q", codeStr) 39 | continue 40 | } 41 | code = Encoding(i) 42 | } else { 43 | codeRune, length := utf8.DecodeRuneInString(codeStr) 44 | if code == utf8.RuneError { 45 | t.Errorf("multibase %q wasn't valid utf8", codeStr) 46 | continue 47 | } 48 | if length != len(codeStr) { 49 | t.Errorf("multibase %q wasn't a single character", codeStr) 50 | continue 51 | } 52 | code = Encoding(codeRune) 53 | } 54 | expectedEncodings[code] = encoding 55 | } 56 | 57 | for name, enc := range Encodings { 58 | expectedName, ok := expectedEncodings[enc] 59 | if !ok { 60 | t.Errorf("encoding %q (%c) not defined in the spec", name, enc) 61 | continue 62 | } 63 | if expectedName != name { 64 | t.Errorf("encoding %q (%c) has unexpected name %q", expectedName, enc, name) 65 | } 66 | } 67 | } 68 | func TestSpecVectors(t *testing.T) { 69 | files, err := filepath.Glob("spec/tests/*.csv") 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | for _, fname := range files { 74 | t.Run(fname, func(t *testing.T) { 75 | file, err := os.Open(fname) 76 | if err != nil { 77 | t.Error(err) 78 | return 79 | } 80 | defer file.Close() 81 | reader := csv.NewReader(file) 82 | reader.LazyQuotes = false 83 | reader.FieldsPerRecord = 2 84 | reader.TrimLeadingSpace = true 85 | 86 | values, err := reader.ReadAll() 87 | if err != nil { 88 | t.Error(err) 89 | } 90 | if len(values) == 0 { 91 | t.Error("no test values") 92 | return 93 | } 94 | header := values[0] 95 | 96 | var decodeOnly bool 97 | switch header[0] { 98 | case "encoding": 99 | case "non-canonical encoding": 100 | decodeOnly = true 101 | default: 102 | t.Errorf("invalid test spec %q", fname) 103 | return 104 | } 105 | 106 | testValue, err := strconv.Unquote("\"" + header[1] + "\"") 107 | if err != nil { 108 | t.Error("failed to unquote testcase:", err) 109 | return 110 | } 111 | 112 | for _, testCase := range values[1:] { 113 | encodingName := testCase[0] 114 | expected := testCase[1] 115 | 116 | t.Run(encodingName, func(t *testing.T) { 117 | encoder, err := EncoderByName(encodingName) 118 | if err != nil { 119 | t.Skipf("skipping %s: not supported", encodingName) 120 | return 121 | } 122 | if !decodeOnly { 123 | t.Logf("encoding %q with %s", testValue, encodingName) 124 | actual := encoder.Encode([]byte(testValue)) 125 | if expected != actual { 126 | t.Errorf("expected %q, got %q", expected, actual) 127 | } 128 | } 129 | t.Logf("decoding %q", expected) 130 | encoding, decoded, err := Decode(expected) 131 | if err != nil { 132 | t.Error("failed to decode:", err) 133 | return 134 | } 135 | expectedEncoding := Encodings[encodingName] 136 | if encoding != expectedEncoding { 137 | t.Errorf("expected encoding to be %c, got %c", expectedEncoding, encoding) 138 | } 139 | if string(decoded) != testValue { 140 | t.Errorf("failed to decode %q to %q, got %q", expected, testValue, string(decoded)) 141 | } 142 | }) 143 | 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func FuzzDecode(f *testing.F) { 150 | files, err := filepath.Glob("spec/tests/*.csv") 151 | if err != nil { 152 | f.Fatal(err) 153 | } 154 | for _, fname := range files { 155 | func() { 156 | file, err := os.Open(fname) 157 | if err != nil { 158 | f.Fatal(err) 159 | } 160 | defer file.Close() 161 | reader := csv.NewReader(file) 162 | reader.LazyQuotes = false 163 | reader.FieldsPerRecord = 2 164 | reader.TrimLeadingSpace = true 165 | 166 | values, err := reader.ReadAll() 167 | if err != nil { 168 | f.Fatal(err) 169 | } 170 | 171 | for _, tc := range values[1:] { 172 | f.Add(tc[1]) 173 | } 174 | }() 175 | } 176 | 177 | f.Fuzz(func(_ *testing.T, data string) { 178 | Decode(data) 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v0.2.0" 3 | } 4 | --------------------------------------------------------------------------------