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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/remerge/go-tools

go 1.21
go 1.22

require github.com/stretchr/testify v1.3.0

Expand Down
92 changes: 92 additions & 0 deletions uniuri/uniuri.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Written in 2011-2014 by Dmitry Chestnykh
//
// The author(s) have dedicated all copyright and related and
// neighboring rights to this software to the public domain
// worldwide. Distributed without any warranty.
// http://creativecommons.org/publicdomain/zero/1.0/

// Package uniuri generates random strings good for use in URIs to identify
// unique objects.
//
// Example usage:
//
// s := uniuri.New() // s is now "apHCJBl7L1OmC57n"
//
// A standard string created by New() is 16 bytes in length and consists of
// Latin upper and lowercase letters, and numbers (from the set of 62 allowed
// characters), which means that it has ~95 bits of entropy. To get more
// entropy, you can use NewLen(UUIDLen), which returns 20-byte string, giving
// ~119 bits of entropy, or any other desired length.
//
// Functions read from crypto/rand random source, and panic if they fail to
// read from it.
package uniuri

import (
"math/rand/v2"
)

const (
// StdLen is a standard length of uniuri string to achieve ~95 bits of entropy.
StdLen = 16
// UUIDLen is a length of uniuri string to achieve ~119 bits of entropy,
// closest to what can be losslessly converted to UUIDv4 (122 bits).
UUIDLen = 20
)

// StdChars is a set of standard characters allowed in uniuri string.
var StdChars = []byte(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
)

// New returns a new random string of the standard length, consisting of
// standard characters.
func New() string {
return NewLenChars(StdLen, StdChars)
}

// NewLen returns a new random string of the provided length, consisting of
// standard characters.
func NewLen(length int) string {
return NewLenChars(length, StdChars)
}

// NewLenChars returns a new random string of the provided length, consisting
// of the provided byte slice of allowed characters (maximum 256).
func NewLenChars(length int, chars []byte) string {
if length == 0 {
return ""
}
clen := len(chars)
if clen < 2 || clen > 256 {
panic("uniuri: wrong charset length for NewLenChars")
}
maxrb := 255 - (256 % clen)
b := make([]byte, length)
r := make([]byte, length+(length/4)) // storage for random bytes.
i := 0
for {
// Read random bytes
pos := 0
for pos < len(r) {
u := rand.Uint64()
for k := 0; k < 8 && pos < len(r); k++ {
r[pos] = byte(u >> (k * 8))
pos++
}
}

for _, rb := range r {
c := int(rb)
if c > maxrb {
// Skip this number to avoid modulo bias.
continue
}
b[i] = chars[c%clen]
i++
if i == length {
return string(b)
}
}
}
}
102 changes: 102 additions & 0 deletions uniuri/uniuri_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Written in 2011-2014 by Dmitry Chestnykh
//
// The author(s) have dedicated all copyright and related and
// neighboring rights to this software to the public domain
// worldwide. Distributed without any warranty.
// http://creativecommons.org/publicdomain/zero/1.0/

package uniuri

import "testing"

func validateChars(t *testing.T, u string, chars []byte) {
for _, c := range u {
var present bool
for _, a := range chars {
if rune(a) == c {
present = true
}
}
if !present {
t.Fatalf("chars not allowed in %q", u)
}
}
}

func TestNew(t *testing.T) {
u := New()
// Check length
if len(u) != StdLen {
t.Fatalf("wrong length: expected %d, got %d", StdLen, len(u))
}
// Check that only allowed characters are present
validateChars(t, u, StdChars)

// Generate 1000 uniuris and check that they are unique
uris := make([]string, 1000)
for i := range uris {
uris[i] = New()
}
for i, u1 := range uris {
for j, u2 := range uris {
if i != j && u1 == u2 {
t.Fatalf("not unique: %d:%q and %d:%q", i, u1, j, u2)
}
}
}
}

func TestNewLen(t *testing.T) {
for i := 0; i < 100; i++ {
u := NewLen(i)
if len(u) != i {
t.Fatalf("request length %d, got %d", i, len(u))
}
}
}

func TestNewLenChars(t *testing.T) {
length := 10
chars := []byte("01234567")
u := NewLenChars(length, chars)

// Check length
if len(u) != length {
t.Fatalf("wrong length: expected %d, got %d", StdLen, len(u))
}
// Check that only allowed characters are present
validateChars(t, u, chars)

// Check that two generated strings are different
u2 := NewLenChars(length, chars)
if u == u2 {
t.Fatalf("not unique: %q and %q", u, u2)
}
}

func TestNewLenCharsMaxLength(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatal("didn't panic")
}
}()
chars := make([]byte, 257)
NewLenChars(32, chars)
}

func TestBias(t *testing.T) {
chars := []byte("abcdefghijklmnopqrstuvwxyz")
slen := 100000
s := NewLenChars(slen, chars)
counts := make(map[rune]int)
for _, b := range s {
counts[b]++
}
avg := float64(slen) / float64(len(chars))
for k, n := range counts {
diff := float64(n) / avg
if diff < 0.95 || diff > 1.05 {
t.Errorf("Bias on '%c': expected average %f, got %d", k, avg, n)
}
}
}
Loading