From d042fc138cc173dbb89263d03e5289bd2ecc5ee5 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 13 Mar 2026 21:06:26 -0400 Subject: [PATCH] refactor(nanoid): replace gonanoid with custom nanoid implementation for ID generation Signed-off-by: Deluan --- core/share.go | 4 +- go.mod | 1 - go.sum | 2 - model/id/id.go | 4 +- utils/nanoid/nanoid.go | 52 +++++++++++++++++++++++ utils/nanoid/nanoid_test.go | 85 +++++++++++++++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 utils/nanoid/nanoid.go create mode 100644 utils/nanoid/nanoid_test.go diff --git a/core/share.go b/core/share.go index fa43a95d..a6d06a01 100644 --- a/core/share.go +++ b/core/share.go @@ -7,11 +7,11 @@ import ( "github.com/Masterminds/squirrel" "github.com/deluan/rest" - gonanoid "github.com/matoous/go-nanoid/v2" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" . "github.com/navidrome/navidrome/utils/gg" + "github.com/navidrome/navidrome/utils/nanoid" "github.com/navidrome/navidrome/utils/slice" "github.com/navidrome/navidrome/utils/str" ) @@ -73,7 +73,7 @@ type shareRepositoryWrapper struct { func (r *shareRepositoryWrapper) newId() (string, error) { for { - id, err := gonanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10) + id, err := nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10) if err != nil { return "", err } diff --git a/go.mod b/go.mod index 424c5242..33333ca3 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,6 @@ require ( github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v3 v3.0.13 github.com/maruel/natural v1.3.0 - github.com/matoous/go-nanoid/v2 v2.1.0 github.com/mattn/go-sqlite3 v1.14.34 github.com/microcosm-cc/bluemonday v1.0.27 github.com/mileusna/useragent v1.3.5 diff --git a/go.sum b/go.sum index a28900a4..30ca76e8 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,6 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= -github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= -github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= diff --git a/model/id/id.go b/model/id/id.go index 93087526..b5454289 100644 --- a/model/id/id.go +++ b/model/id/id.go @@ -6,12 +6,12 @@ import ( "math/big" "strings" - gonanoid "github.com/matoous/go-nanoid/v2" "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/utils/nanoid" ) func NewRandom() string { - id, err := gonanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 22) + id, err := nanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 22) if err != nil { log.Error("Could not generate new ID", err) } diff --git a/utils/nanoid/nanoid.go b/utils/nanoid/nanoid.go new file mode 100644 index 00000000..17d32e72 --- /dev/null +++ b/utils/nanoid/nanoid.go @@ -0,0 +1,52 @@ +package nanoid + +import ( + "crypto/rand" + "errors" + "math" +) + +// Generate returns a cryptographically secure random string of `size` characters +// drawn from `alphabet`. It uses bitmask with rejection sampling to avoid modulo bias. +// The alphabet must be non-empty, contain at most 255 characters, and consist only of +// ASCII characters. Non-ASCII alphabets (e.g., multi-byte UTF-8) are not supported. +func Generate(alphabet string, size int) (string, error) { + if len(alphabet) == 0 || len(alphabet) > 255 { + return "", errors.New("alphabet must be non-empty and at most 255 characters") + } + if size <= 0 { + return "", errors.New("size must be a positive integer") + } + + mask := getMask(len(alphabet)) + step := int(math.Ceil(1.6 * float64(mask) * float64(size) / float64(len(alphabet)))) + + id := make([]byte, size) + bytes := make([]byte, step) + for j := 0; ; { + if _, err := rand.Read(bytes); err != nil { + return "", err + } + for i := range step { + idx := int(bytes[i]) & mask + if idx < len(alphabet) { + id[j] = alphabet[idx] + j++ + if j == size { + return string(id), nil + } + } + } + } +} + +// getMask returns the smallest bitmask >= alphabetSize-1. +func getMask(alphabetSize int) int { + for i := 1; i <= 8; i++ { + mask := (2 << uint(i)) - 1 + if mask >= alphabetSize-1 { + return mask + } + } + return 0 +} diff --git a/utils/nanoid/nanoid_test.go b/utils/nanoid/nanoid_test.go new file mode 100644 index 00000000..99d4e571 --- /dev/null +++ b/utils/nanoid/nanoid_test.go @@ -0,0 +1,85 @@ +package nanoid_test + +import ( + "testing" + + "github.com/navidrome/navidrome/utils/nanoid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestNanoid(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Nanoid Suite") +} + +var _ = Describe("Generate", func() { + const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + It("generates a string of the requested length", func() { + id, err := nanoid.Generate(alphabet, 22) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(HaveLen(22)) + }) + + It("generates a short string of the requested length", func() { + id, err := nanoid.Generate(alphabet, 10) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(HaveLen(10)) + }) + + It("only contains characters from the alphabet", func() { + id, err := nanoid.Generate(alphabet, 100) + Expect(err).ToNot(HaveOccurred()) + for _, c := range id { + Expect(alphabet).To(ContainSubstring(string(c))) + } + }) + + It("generates unique IDs", func() { + seen := make(map[string]bool) + for range 1000 { + id, err := nanoid.Generate(alphabet, 22) + Expect(err).ToNot(HaveOccurred()) + Expect(seen).ToNot(HaveKey(id)) + seen[id] = true + } + }) + + It("works with a single-character alphabet", func() { + id, err := nanoid.Generate("a", 5) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(Equal("aaaaa")) + }) + + It("works with a small alphabet", func() { + id, err := nanoid.Generate("ab", 10) + Expect(err).ToNot(HaveOccurred()) + Expect(id).To(HaveLen(10)) + for _, c := range id { + Expect(string(c)).To(BeElementOf("a", "b")) + } + }) + + It("returns error on empty alphabet", func() { + _, err := nanoid.Generate("", 10) + Expect(err).To(HaveOccurred()) + }) + + It("returns error on alphabet larger than 255 characters", func() { + bigAlphabet := make([]byte, 256) + for i := range bigAlphabet { + bigAlphabet[i] = byte(i) + } + _, err := nanoid.Generate(string(bigAlphabet), 10) + Expect(err).To(HaveOccurred()) + }) + + It("returns error on non-positive size", func() { + _, err := nanoid.Generate(alphabet, 0) + Expect(err).To(HaveOccurred()) + + _, err = nanoid.Generate(alphabet, -1) + Expect(err).To(HaveOccurred()) + }) +})