├── .travis.yml ├── LICENSE ├── nanoid_test.go ├── nanoid.go └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | - 1.7 6 | - 1.8 7 | - 1.9 8 | - 1.10 9 | - 1.11 10 | - 1.12 11 | 12 | script: go test -v -bench=. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alberto Schiabel 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nanoid_test.go: -------------------------------------------------------------------------------- 1 | package nanoid 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var urlLength = len(defaults.Alphabet) 10 | 11 | // Test that nanoid generates URL friendly IDs 12 | // it ('generates URL-friendly IDs') 13 | func TestGeneratesURLFriendlyIDs(t *testing.T) { 14 | for i := 0; i < 10; i++ { 15 | id, err := Nanoid() 16 | if err != nil { 17 | t.Errorf("Nanoid error: %v", err) 18 | } 19 | if len(id) != defaults.Size { 20 | t.Errorf( 21 | "TestGeneratesURLFriendlyIDs error: length of id %v should be %v, got %v", 22 | id, 23 | defaults.Size, 24 | id, 25 | ) 26 | } 27 | 28 | runeID := []rune(id) 29 | 30 | for j := 0; j < len(runeID); j++ { 31 | res := strings.Contains(defaults.Alphabet, string(runeID[j])) 32 | if !res { 33 | t.Errorf( 34 | "GeneratesURLFriendlyIds error: char %v should be contained in %v", 35 | string(runeID[j]), 36 | defaults.Alphabet, 37 | ) 38 | } 39 | } 40 | } 41 | } 42 | 43 | // Test that nanoid has no collisions 44 | // it ('has no collisions') 45 | func TestHasNoCollisions(t *testing.T) { 46 | COUNT := 100 * 1000 47 | used := make(map[string]bool) 48 | for i := 0; i < COUNT; i++ { 49 | id, err := Nanoid() 50 | if err != nil { 51 | t.Errorf("Nanoid error: %v", err) 52 | } 53 | if used[id] { 54 | t.Errorf("Collision error! Id %v found for test arr %v", id, used) 55 | } 56 | used[id] = true 57 | } 58 | } 59 | 60 | // Test that Nanoid has flat distribution 61 | // it ('has flat distribution') 62 | func TestFlatDistribution(t *testing.T) { 63 | COUNT := 100 * 1000 64 | instance, err := Nanoid() 65 | if err != nil { 66 | t.Errorf("Nanoid error: %v", err) 67 | } 68 | LENGTH := len(instance) 69 | 70 | chars := make(map[byte]int) 71 | 72 | for i := 0; i < COUNT; i++ { 73 | id, _ := Nanoid() 74 | for j := 0; j < LENGTH; j++ { 75 | // https://github.com/ai/nanoid/blob/d6ad3412147fa4c2b0d404841ade245a00c2009f/test/index.test.js#L33 76 | // if (!chars[char]) chars[char] = 0 is useless since it 77 | // is initialized by default to 0 from Golang 78 | chars[id[j]]++ 79 | } 80 | } 81 | 82 | for char, k := range chars { 83 | distribution := float64(k) * float64(urlLength) / float64(COUNT*LENGTH) 84 | if !toBeCloseTo(distribution, 1, 1) { 85 | t.Errorf("Distribution error! Distribution %v found for char %v", distribution, char) 86 | } 87 | } 88 | } 89 | 90 | // utility that replicates jest.toBeCloseTo 91 | func toBeCloseTo(value, actual, expected float64) bool { 92 | precision := 2 93 | // https://github.com/facebook/jest/blob/a397abaf9f08e691f8739899819fc4da41c1e476/packages/expect/src/matchers.js#L83 94 | pass := math.Abs(expected-actual) < math.Pow10(-precision)/2 95 | return pass 96 | } 97 | 98 | // Benchmark nanoid generator 99 | func BenchmarkNanoid(b *testing.B) { 100 | for n := 0; n < b.N; n++ { 101 | Nanoid() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /nanoid.go: -------------------------------------------------------------------------------- 1 | package nanoid 2 | 3 | import ( 4 | "crypto/rand" 5 | "math" 6 | ) 7 | 8 | // RandomType is the type that the custom random generator has to be 9 | type RandomType func(int) ([]byte, error) 10 | 11 | // DefaultsType is the type of the default configuration for Nanoid 12 | type DefaultsType struct { 13 | Alphabet string 14 | Size int 15 | MaskSize int 16 | } 17 | 18 | // GetDefaults returns the default configuration for Nanoid 19 | func GetDefaults() *DefaultsType { 20 | return &DefaultsType{ 21 | Alphabet: "_~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", // len=64 22 | Size: 22, 23 | MaskSize: 5, 24 | } 25 | } 26 | 27 | var defaults = GetDefaults() 28 | 29 | func initMasks(params ...int) []uint { 30 | var size int 31 | if len(params) == 0 { 32 | size = defaults.MaskSize 33 | } else { 34 | size = params[0] 35 | } 36 | /* 37 | https://github.com/ai/nanoid/blob/d6ad3412147fa4c2b0d404841ade245a00c2009f/format.js#L1 38 | As per 'var masks = [15, 31, 63, 127, 255]' 39 | 40 | The next block initializes an array of size elements, from 2^4-1 to 2^(3 + size)-1 41 | */ 42 | masks := make([]uint, size) 43 | for i := 0; i < size; i++ { 44 | shift := 3 + i 45 | masks[i] = (2 << uint(shift)) - 1 46 | } 47 | return masks 48 | } 49 | 50 | /* 51 | https://github.com/ai/nanoid/blob/d6ad3412147fa4c2b0d404841ade245a00c2009f/format.js#L29-L31 52 | var mask = masks.find(function (i) { 53 | return i >= alphabet.length - 1 54 | }) 55 | */ 56 | func getMask(alphabet string, masks []uint) int { 57 | for i := 0; i < len(masks); i++ { 58 | curr := int(masks[i]) 59 | if curr >= len(alphabet)-1 { 60 | return curr 61 | } 62 | } 63 | return 0 64 | } 65 | 66 | // Random generates cryptographically strong pseudo-random data. 67 | // The size argument is a number indicating the number of bytes to generate. 68 | func Random(size int) ([]byte, error) { 69 | var randomBytes = make([]byte, size) 70 | _, err := rand.Read(randomBytes) 71 | return randomBytes, err 72 | } 73 | 74 | // Format returns a secure random string with custom random generator and alphabet. 75 | // `size` is the number of symbols in new random string 76 | func Format(random RandomType, alphabet string, size int) (string, error) { 77 | masks := initMasks(size) 78 | mask := getMask(alphabet, masks) 79 | ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet)) 80 | step := int(math.Ceil(ceilArg)) 81 | 82 | id := make([]byte, size) 83 | for j := 0; ; { 84 | bytes, err := random(step) 85 | if err != nil { 86 | return "", err 87 | } 88 | 89 | for i := 0; i < step; i++ { 90 | currByte := bytes[i] & byte(mask) 91 | if currByte < byte(len(alphabet)) { 92 | id[j] = alphabet[currByte] 93 | j++ 94 | if j == size { 95 | return string(id[:size]), nil 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | // Generate is a low-level function to change alphabet and ID size. 103 | func Generate(alphabet string, size int) (string, error) { 104 | return Format(Random, alphabet, size) 105 | } 106 | 107 | // Nanoid generates secure URL-friendly unique ID. 108 | func Nanoid(param ...int) (string, error) { 109 | var size int 110 | if len(param) == 0 { 111 | size = defaults.Size 112 | } else { 113 | size = param[0] 114 | } 115 | bytes, err := Random(size) 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | id := make([]byte, size) 121 | for i := 0; i < size; i++ { 122 | id[i] = defaults.Alphabet[bytes[i]&63] 123 | } 124 | return string(id[:size]), nil 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanoid 2 | Golang port of [ai/nanoid](https://github.com/ai/nanoid) (originally written in JavaScript). 3 | 4 | [![Build Status](https://travis-ci.org/jkomyno/nanoid.svg?branch=master)](https://travis-ci.org/jkomyno/nanoid) [![GoDoc](https://godoc.org/github.com/jkomyno/nanoid?status.svg)](https://godoc.org/github.com/jkomyno/nanoid) [![Go Report Card](https://goreportcard.com/badge/github.com/jkomyno/nanoid)](https://goreportcard.com/report/github.com/jkomyno/nanoid) 5 | 6 | # Description 7 | A tiny, secure URL-friendly unique string ID generator for Golang. 8 | 9 | **Safe.** It uses cryptographically strong random APIs 10 | and guarantees a proper distribution of symbols. 11 | 12 | **Compact.** It uses more symbols than UUID (`A-Za-z0-9_~`) 13 | and has the same number of unique options in just 22 symbols instead of 36. 14 | 15 | **No third party dependencies** No need to pollute your $GOPATH 16 | 17 | ## Install 18 | 19 | ```bash 20 | $ go get github.com/jkomyno/nanoid 21 | ``` 22 | 23 | ## Testing 24 | 25 | ``` bash 26 | $ go test -v -bench=. 27 | ``` 28 | 29 | You should be able to see a log similar to the following one: 30 | ``` 31 | === RUN TestGeneratesURLFriendlyIDs 32 | --- PASS: TestGeneratesURLFriendlyIDs (0.00s) 33 | === RUN TestHasNoCollisions 34 | --- PASS: TestHasNoCollisions (0.21s) 35 | === RUN TestFlatDistribution 36 | --- PASS: TestFlatDistribution (0.33s) 37 | goos: linux 38 | goarch: amd64 39 | pkg: github.com/jkomyno/nanoid 40 | BenchmarkNanoid-4 1000000 1704 ns/op 41 | PASS 42 | ok github.com/jkomyno/nanoid 2.265s 43 | ``` 44 | 45 | ## Usage 46 | **This packages tries to offer an API as close as possible to the original JS module.** 47 | 48 | ### Normal 49 | 50 | The Nanoid() function uses URL-friendly symbols (`A-Za-z0-9_~`) and returns an ID 51 | with 22 characters (to have the same collisions probability as UUID v4). 52 | Please note that it also returns an error, which (hopefully) will be `nil`. 53 | 54 | ```go 55 | import "github.com/jkomyno/nanoid" 56 | 57 | id, err := nanoid.Nanoid() //=> "Uakgb_J5m9g~0JDMbcJqLJ" 58 | ``` 59 | 60 | Symbols `-,.()` are not encoded in URL, but in the end of a link 61 | they could be identified as a punctuation symbol. 62 | 63 | If you want to reduce ID length (and increase collisions probability), 64 | you can pass length as argument: 65 | 66 | ```go 67 | import "github.com/jkomyno/nanoid" 68 | 69 | id, err := nanoid.Nanoid(10) //=> "IRFa~VaY2b" 70 | ``` 71 | 72 | ### Custom Alphabet or Length 73 | 74 | If you want to change the ID alphabet or the length 75 | you can use low-level `Generate` function. 76 | 77 | ```go 78 | import "github.com/jkomyno/nanoid" 79 | 80 | id, err := nanoid.Generate("1234567890abcdef", 10) //=> "4f90d13a42" 81 | ``` 82 | 83 | Alphabet must contain less than 256 symbols. 84 | 85 | ### Custom Random Bytes Generator 86 | 87 | You can replace the default safe random generator using the `Format` function. 88 | 89 | ```go 90 | import ( 91 | "crypto/rand" 92 | 93 | "github.com/jkomyno/nanoid" 94 | ) 95 | 96 | func random(size int) ([]byte, error) { 97 | var randomBytes = make([]byte, size) 98 | _, err := rand.Read(randomBytes) 99 | return randomBytes, err 100 | } 101 | 102 | id, err := nanoid.Format(random, "abcdef", 10) //=> "fbaefaadeb" 103 | ``` 104 | 105 | Note that `random` function must follow this spec: 106 | ```go 107 | type RandomType func(int) ([]byte, error) 108 | ``` 109 | 110 | If you want to use the same URL-friendly symbols with `format`, 111 | or take a look at the other defaults value, you can use `GetDefaults`. 112 | 113 | ```go 114 | import "github.com/jkomyno/nanoid" 115 | 116 | var defaults *nanoid.DefaultsType 117 | defaults = nanoid.GetDefaults() 118 | /* 119 | &DefaultsType{ 120 | Alphabet: "_~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 121 | Size: 22, 122 | MaskSize: 5, 123 | } 124 | */ 125 | ``` 126 | 127 | ## Credits 128 | 129 | [ai](https://github.com/ai) - [nanoid](https://github.com/ai/nanoid) 130 | 131 | ## License 132 | 133 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 134 | --------------------------------------------------------------------------------