├── .gitignore ├── LICENSE ├── README.md ├── cmd └── word │ └── main.go ├── go.mod ├── randomstring.go └── randomstring_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | cmd/word/word 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Alexander F. Rødseth 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # randomstring 2 | 3 | Generate random strings. 4 | 5 | These are the exported function signatures: 6 | 7 | ```go 8 | func PickLetter() rune 9 | func PickVowel() rune 10 | func PickCons() rune 11 | func Seed() 12 | func String(length int) string 13 | func EnglishFrequencyString(length int) string 14 | func HumanFriendlyString(length int) string 15 | func CookieFriendlyString(length int) string 16 | func CookieFriendlyBytes(length int) []byte 17 | func HumanFriendlyEnglishString(length int) string 18 | ``` 19 | 20 | Used by [cookie](https://github.com/xyproto/cookie) and [alienpdf](https://github.com/xyproto/alienpdf/). 21 | 22 | ### General info 23 | 24 | * Version: 1.2.0 25 | * License: BSD-3 26 | -------------------------------------------------------------------------------- /cmd/word/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/xyproto/randomstring" 6 | ) 7 | 8 | func main() { 9 | randomstring.Seed() 10 | fmt.Println(randomstring.HumanFriendlyString(7)) 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xyproto/randomstring 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /randomstring.go: -------------------------------------------------------------------------------- 1 | // Package randomstring can be used for generating different types of random strings 2 | package randomstring 3 | 4 | import ( 5 | "math/rand" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var random = rand.New(rand.NewSource(1)) 11 | 12 | var freq = map[rune]int{ 13 | 'e': 21912, 14 | 't': 16587, 15 | 'a': 14810, 16 | 'o': 14003, 17 | 'i': 13318, 18 | 'n': 12666, 19 | 's': 11450, 20 | 'r': 10977, 21 | 'h': 10795, 22 | 'd': 7874, 23 | 'l': 7253, 24 | 'u': 5246, 25 | 'c': 4943, 26 | 'm': 4761, 27 | 'f': 4200, 28 | 'y': 3853, 29 | 'w': 3819, 30 | 'g': 3693, 31 | 'p': 3316, 32 | 'b': 2715, 33 | 'v': 2019, 34 | 'k': 1257, 35 | 'x': 315, 36 | 'q': 205, 37 | 'j': 188, 38 | 'z': 128, 39 | } 40 | 41 | var freqVowel = map[rune]int{ 42 | 'e': 21912, 43 | 'a': 14810, 44 | 'o': 14003, 45 | 'i': 13318, 46 | 'u': 5246, 47 | } 48 | 49 | var freqCons = map[rune]int{ 50 | 't': 16587, 51 | 'n': 12666, 52 | 's': 11450, 53 | 'r': 10977, 54 | 'h': 10795, 55 | 'd': 7874, 56 | 'l': 7253, 57 | 'c': 4943, 58 | 'm': 4761, 59 | 'f': 4200, 60 | 'y': 3853, 61 | 'w': 3819, 62 | 'g': 3693, 63 | 'p': 3316, 64 | 'b': 2715, 65 | 'v': 2019, 66 | 'k': 1257, 67 | 'x': 315, 68 | 'q': 205, 69 | 'j': 188, 70 | 'z': 128, 71 | } 72 | 73 | // freqsum is a sum of all the frequencies in the freq map 74 | var freqsum = func() int { 75 | n := 0 76 | for _, v := range freq { 77 | n += v 78 | } 79 | return n 80 | }() 81 | 82 | // freqsumVowel is a sum of all the frequencies in the freqVowel map 83 | var freqsumVowel = func() int { 84 | n := 0 85 | for _, v := range freqVowel { 86 | n += v 87 | } 88 | return n 89 | }() 90 | 91 | // freqsumCons is a sum of all the frequencies in the freqCons map 92 | var freqsumCons = func() int { 93 | n := 0 94 | for _, v := range freqCons { 95 | n += v 96 | } 97 | return n 98 | }() 99 | 100 | // PickLetter will pick a letter, weighted by the frequency table 101 | func PickLetter() rune { 102 | target := random.Intn(freqsum) 103 | selected := 'a' 104 | n := 0 105 | for k, v := range freq { 106 | n += v 107 | if n > target { 108 | selected = k 109 | break 110 | } 111 | } 112 | return selected 113 | } 114 | 115 | // PickVowel will pick a vowel, weighted by the frequency table 116 | func PickVowel() rune { 117 | target := random.Intn(freqsumVowel) 118 | selected := 'a' 119 | n := 0 120 | for k, v := range freqVowel { 121 | n += v 122 | if n > target { 123 | selected = k 124 | break 125 | } 126 | } 127 | return selected 128 | } 129 | 130 | // PickCons will pick a consonant, weighted by the frequency table 131 | func PickCons() rune { 132 | target := random.Intn(freqsumCons) 133 | selected := 't' 134 | n := 0 135 | for k, v := range freqCons { 136 | n += v 137 | if n > target { 138 | selected = k 139 | break 140 | } 141 | } 142 | return selected 143 | } 144 | 145 | // Seed the random number generator in one of many possible ways. 146 | func Seed() { 147 | random = rand.New(rand.NewSource(time.Now().UTC().UnixNano() + 1337)) 148 | } 149 | 150 | // String generates a random string of a given length. 151 | func String(length int) string { 152 | b := make([]byte, length) 153 | for i := 0; i < length; i++ { 154 | b[i] = byte(random.Int63() & 0xff) 155 | } 156 | return string(b) 157 | } 158 | 159 | // StringNoAlloc generates a random string in the given byte slice, 160 | // but does not allocate memory with "make". 161 | func StringNoAlloc(placeholder []byte) { 162 | for i := 0; i < len(placeholder); i++ { 163 | (placeholder)[i] = byte(random.Int63() & 0xff) 164 | } 165 | } 166 | 167 | // EnglishFrequencyString returns a random string that uses the letter frequency of English, 168 | // ref: http://pi.math.cornell.edu/~mec/2003-2004/cryptography/subs/frequencies.html 169 | func EnglishFrequencyString(length int) string { 170 | var sb strings.Builder 171 | for i := 0; i < length; i++ { 172 | sb.WriteRune(PickLetter()) 173 | } 174 | return sb.String() 175 | } 176 | 177 | /*HumanFriendlyString generates a random, but human-friendly, string of 178 | * the given length. It should be possible to read out loud and send in an email 179 | * without problems. The string alternates between vowels and consontants. 180 | * 181 | * Google Translate believes the output is Samoan. 182 | * 183 | * Example output for length 7: rabunor 184 | */ 185 | func HumanFriendlyString(length int) string { 186 | const ( 187 | someVowels = "aeoiu" // a selection of vowels. email+browsers didn't like "æøå" too much 188 | someConsonants = "bdfgklmnoprstv" // a selection of consonants 189 | moreLetters = "chjqwxyz" // the rest of the letters from a-z 190 | ) 191 | vowelOffset := random.Intn(2) 192 | vowelDistribution := 2 193 | b := make([]byte, length) 194 | for i := 0; i < length; i++ { 195 | again: 196 | if (i+vowelOffset)%vowelDistribution == 0 { 197 | b[i] = someVowels[random.Intn(len(someVowels))] 198 | } else if random.Intn(100) > 0 { // 99 of 100 times 199 | b[i] = someConsonants[random.Intn(len(someConsonants))] 200 | // Don't repeat 201 | if i >= 1 && b[i] == b[i-1] { 202 | // Also use more vowels 203 | vowelDistribution = 1 204 | // Then try again 205 | goto again 206 | } 207 | } else { 208 | b[i] = moreLetters[random.Intn(len(moreLetters))] 209 | // Don't repeat 210 | if i >= 1 && b[i] == b[i-1] { 211 | // Also use more vowels 212 | vowelDistribution = 1 213 | // Then try again 214 | goto again 215 | } 216 | } 217 | // Avoid three letters in a row 218 | if i >= 2 && b[i] == b[i-2] { 219 | // Then try again 220 | goto again 221 | } 222 | } 223 | return string(b) 224 | } 225 | 226 | // CookieFriendlyString generates a random, but cookie-friendly, string of the given length. 227 | func CookieFriendlyString(length int) string { 228 | const allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 229 | b := make([]byte, length) 230 | for i := 0; i < length; i++ { 231 | b[i] = allowed[random.Intn(len(allowed))] 232 | } 233 | return string(b) 234 | } 235 | 236 | // CookieFriendlyStringNoAlloc generates a random, but cookie-friendly, string. 237 | // The bytes of the string are stored in the given byte slice. 238 | func CookieFriendlyStringNoAlloc(placeholder []byte) { 239 | const allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 240 | for i := 0; i < len(placeholder); i++ { 241 | (placeholder)[i] = allowed[random.Intn(len(allowed))] 242 | } 243 | } 244 | 245 | // CookieFriendlyBytes generates a random, but cookie-friendly, byte slice of the given length. 246 | func CookieFriendlyBytes(length int) []byte { 247 | const allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 248 | b := make([]byte, length) 249 | for i := 0; i < length; i++ { 250 | b[i] = allowed[random.Intn(len(allowed))] 251 | } 252 | return b 253 | } 254 | 255 | /*HumanFriendlyEnglishString generates a random, but human-friendly, string of 256 | * the given length. It should be possible to read out loud and send in an email 257 | * without problems. The string alternates between vowels and consontants. 258 | * 259 | * The vowels and consontants are wighted by the frequency table 260 | */ 261 | func HumanFriendlyEnglishString(length int) string { 262 | vowelOffset := random.Intn(2) 263 | vowelDistribution := 2 264 | b := make([]byte, length) 265 | for i := 0; i < length; i++ { 266 | again: 267 | if (i+vowelOffset)%vowelDistribution == 0 { 268 | b[i] = byte(PickVowel()) 269 | } else if random.Intn(100) > 0 { // 99 of 100 times 270 | b[i] = byte(PickCons()) 271 | // Don't repeat 272 | if i >= 1 && b[i] == b[i-1] { 273 | // Also use more vowels 274 | vowelDistribution = 1 275 | // Then try again 276 | goto again 277 | } 278 | } else { 279 | b[i] = byte(PickLetter()) 280 | // Don't repeat 281 | if i >= 1 && b[i] == b[i-1] { 282 | // Also use more vowels 283 | vowelDistribution = 1 284 | // Then try again 285 | goto again 286 | } 287 | } 288 | // Avoid three letters in a row 289 | if i >= 2 && b[i] == b[i-2] { 290 | // Then try again 291 | goto again 292 | } 293 | } 294 | return string(b) 295 | } 296 | -------------------------------------------------------------------------------- /randomstring_test.go: -------------------------------------------------------------------------------- 1 | package randomstring 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // Uncomment this function to see that rand.NewSource(1) is the same as the unseeded behavior before Go 1.20 9 | func init() { 10 | Seed() 11 | } 12 | 13 | func TestString(t *testing.T) { 14 | fmt.Printf("%x\n", String(10)) 15 | } 16 | 17 | func TestStringNoAlloc(t *testing.T) { 18 | b := make([]byte, 10) 19 | StringNoAlloc(b) 20 | fmt.Printf("%x\n", string(b)) 21 | } 22 | 23 | func TestHumanFriendlyString(t *testing.T) { 24 | fmt.Printf("%s\n", HumanFriendlyString(7)) 25 | fmt.Printf("%s\n", HumanFriendlyString(20)) 26 | } 27 | 28 | func TestCookieFriendlyString(t *testing.T) { 29 | fmt.Printf("%s\n", CookieFriendlyString(20)) 30 | } 31 | 32 | func TestCookieFriendlyStringNoAlloc(t *testing.T) { 33 | b := make([]byte, 20) 34 | CookieFriendlyStringNoAlloc(b) 35 | fmt.Printf("%s\n", string(b)) 36 | } 37 | 38 | func TestCookieFriendlyBytes(t *testing.T) { 39 | fmt.Printf("%s\n", CookieFriendlyBytes(20)) 40 | } 41 | 42 | func TestEnglishFrequencyString(t *testing.T) { 43 | fmt.Printf("%s\n", EnglishFrequencyString(20)) 44 | } 45 | 46 | func TestHumanFriendlyEnglishString(t *testing.T) { 47 | fmt.Printf("%s\n", HumanFriendlyEnglishString(7)) 48 | fmt.Printf("%s\n", HumanFriendlyEnglishString(20)) 49 | } 50 | --------------------------------------------------------------------------------