diff --git a/CHANGELOG/CHANGELOG-1.x.md b/CHANGELOG/CHANGELOG-1.x.md index 5cc533a..0f423ed 100644 --- a/CHANGELOG/CHANGELOG-1.x.md +++ b/CHANGELOG/CHANGELOG-1.x.md @@ -17,6 +17,24 @@ Date format: `YYYY-MM-DD` ### Fixed ### Security +--- +## [1.19.0] - 2024-11-16 + +### Added +- **FEATURE:** Added [README.md](../x/crypto/prng/README.md) to provide detailed information about the PRNG implementation. + +### Changed +- **DEBT:** Updated [PRNG](../x/crypto/prng) benchmark tests to test the standard size of 21 characters for Nano ID generation. +- **DEBT:** The runtime configuration (`Config`) now uses pointer receivers for all methods to ensure consistent behavior and interface compliance. +- **DEBT:** Refactored Codebase: Split the `nanoid.go` file into multiple modular files within the `nanoid` package to enhance code organization, readability, and maintainability +- +### Deprecated +### Removed +### Fixed +- **DEFECT:** Fixed receiver types by updating all `ID` methods to use pointer receivers consistently, ensuring proper functionality and interface compliance. + +### Security + --- ## [1.18.1] - 2024-11-15 @@ -444,7 +462,8 @@ Date format: `YYYY-MM-DD` ### Fixed ### Security -[Unreleased]: https://github.com/sixafter/nanoid/compare/v1.18.1..HEAD +[Unreleased]: https://github.com/sixafter/nanoid/compare/v1.19.0..HEAD +[1.19.0]: https://github.com/sixafter/nanoid/compare/v1.18.1...v1.19.0 [1.18.1]: https://github.com/sixafter/nanoid/compare/v1.18.0...v1.18.1 [1.18.0]: https://github.com/sixafter/nanoid/compare/v1.17.3...v1.18.0 [1.17.3]: https://github.com/sixafter/nanoid/compare/v1.17.2...v1.17.3 diff --git a/config.go b/config.go new file mode 100644 index 0000000..333985f --- /dev/null +++ b/config.go @@ -0,0 +1,456 @@ +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the Apache 2.0 License found in the +// LICENSE file in the root directory of this source tree. + +package nanoid + +import ( + "io" + "math" + "math/bits" + "unicode" + "unicode/utf8" +) + +// ConfigOptions holds the configurable options for the Generator. +// It is used with the Function Options pattern. +type ConfigOptions struct { + // RandReader is the source of randomness used for generating IDs. + // By default, it uses x/crypto/prng/Reader, which provides cryptographically secure random bytes. + RandReader io.Reader + + // Alphabet is the set of characters used to generate the Nano ID. + // It must be a valid UTF-8 string containing between 2 and 256 unique characters. + // Using a diverse and appropriately sized alphabet ensures the uniqueness and randomness of the generated IDs. + Alphabet string + + // LengthHint specifies a typical or default length for generated IDs. + LengthHint uint16 +} + +// Config holds the runtime configuration for the Nano ID generator. +// +// It is immutable after initialization and provides all the necessary +// parameters for generating unique IDs efficiently and securely. +type Config interface { + // AlphabetLen returns the number of unique characters in the provided alphabet. + // + // This length determines the range of indices for selecting characters during ID generation. + // Using uint16 allows for alphabets up to 65,535 characters. + AlphabetLen() uint16 + + // BaseMultiplier returns the foundational multiplier used in buffer size calculations. + // + // It is based on the logarithm of the intended ID length (LengthHint) plus 2. + // This helps scale the buffer size appropriately with different ID lengths. + BaseMultiplier() int + + // BitsNeeded returns the minimum number of bits required to represent all possible indices of the alphabet. + // + // This value is crucial for generating random numbers that map uniformly to the alphabet indices without bias. + BitsNeeded() uint + + // BufferMultiplier returns the combined multiplier used in the buffer size calculation. + // + // It adds a fraction of the scaling factor to the base multiplier to fine-tune the buffer size, + // considering both the ID length and the alphabet size. + BufferMultiplier() int + + // BufferSize returns the total size of the buffer (in bytes) used for generating random data. + // + // The buffer size is calculated to balance efficiency and performance, + // minimizing calls to the random number generator by reading larger chunks of random data at once. + BufferSize() int + + // ByteAlphabet returns the slice of bytes representing the alphabet, + // used when the alphabet consists solely of ASCII characters. + // + // For non-ASCII alphabets, this returns nil, and RuneAlphabet is used instead. + ByteAlphabet() []byte + + // BytesNeeded returns the number of bytes required to store the BitsNeeded for each character in the ID. + // + // It rounds up BitsNeeded to the nearest byte, ensuring sufficient space for random data generation. + BytesNeeded() uint + + // IsASCII returns true if the alphabet consists solely of ASCII characters. + // + // This allows for optimization in processing, using bytes instead of runes for ID generation. + IsASCII() bool + + // IsPowerOfTwo returns true if the length of the alphabet is a power of two. + // + // When true, random index selection can be optimized using bitwise operations, + // such as bitwise AND with the mask, improving performance. + IsPowerOfTwo() bool + + // LengthHint returns the intended length of the IDs to be generated. + // + // This hint is used in calculations to adjust buffer sizes and scaling factors accordingly. + LengthHint() uint16 + + // MaxBytesPerRune represents the maximum number of bytes required to encode + // any rune in the alphabet using UTF-8 encoding. + // + // This value is computed during + // configuration based on the provided alphabet and is used to preallocate the + // buffer size in the newUnicode function. By accurately estimating the buffer size, + // we ensure efficient string building without unnecessary memory allocations + // or buffer resizing. + // + // For example, if the alphabet includes only ASCII and Latin-1 characters, each rune + // requires at most 2 bytes. However, if the alphabet includes emojis or other + // multibyte characters, this value could be up to 4 bytes. + MaxBytesPerRune() int + + // Mask returns the bitmask used to extract the necessary bits from randomly generated bytes. + // + // The mask is essential for efficiently mapping random values to valid alphabet indices, + // ensuring uniform distribution and preventing bias. + Mask() uint + + // RandReader returns the source of randomness used for generating IDs. + // + // It is typically a cryptographically secure random number generator (e.g., crypto/rand.Reader). + RandReader() io.Reader + + // RuneAlphabet returns the slice of runes representing the alphabet. + // + // This is used for ID generation when the alphabet includes non-ASCII (multibyte) characters, + // allowing support for a wider range of characters. + RuneAlphabet() []rune + + // ScalingFactor returns the scaling factor used to adjust the buffer size. + // + // It balances the influence of the alphabet size and the intended ID length, + // ensuring efficient random data generation without excessive memory usage. + ScalingFactor() int +} + +// Configuration defines the interface for retrieving generator configuration. +type Configuration interface { + // Config returns the runtime configuration of the generator. + Config() Config +} + +// Option defines a function type for configuring the Generator. +// It allows for flexible and extensible configuration by applying +// various settings to the ConfigOptions during Generator initialization. +type Option func(*ConfigOptions) + +// WithAlphabet sets a custom alphabet for the Generator. +// The provided alphabet string defines the set of characters that will be +// used to generate Nano IDs. This allows users to customize the character set +// according to their specific requirements, such as using only alphanumeric +// characters, including symbols, or supporting non-ASCII characters. +// +// Parameters: +// - alphabet string: A string representing the desired set of characters for ID generation. +// +// Returns: +// - Option: A configuration option that applies the custom alphabet to ConfigOptions. +// +// Usage: +// +// generator, err := nanoid.NewGenerator(nanoid.WithAlphabet("abcdef123456")) +func WithAlphabet(alphabet string) Option { + return func(c *ConfigOptions) { + c.Alphabet = alphabet + } +} + +// WithRandReader sets a custom random reader for the Generator. +// By default, the Generator uses a cryptographically secure random number +// generator (e.g., crypto/rand.Reader). However, in some cases, users might +// want to provide their own source of randomness, such as for testing purposes +// or to integrate with a different entropy source. +// +// Parameters: +// - reader io.Reader: An implementation of io.Reader that supplies random data. +// +// Returns: +// - Option: A configuration option that applies the custom random reader to ConfigOptions. +// +// Usage Example: +// +// customReader := myCustomRandomReader() +// generator, err := nanoid.NewGenerator( +// nanoid.WithRandReader(customReader)) +func WithRandReader(reader io.Reader) Option { + return func(c *ConfigOptions) { + c.RandReader = reader + } +} + +// WithLengthHint sets the hint of the intended length of the IDs to be generated. +// Providing a length hint allows the Generator to optimize internal configurations, +// such as buffer sizes and scaling factors, based on the expected ID length. This +// can enhance performance and efficiency, especially when generating a large number +// of IDs with similar lengths. +// +// Parameters: +// - hint uint16: A non-zero unsigned integer representing the anticipated length of the Nano IDs. +// +// Returns: +// - Option: A configuration option that applies the length hint to ConfigOptions. +// +// Usage Example: +// +// generator, err := nanoid.NewGenerator(nanoid.WithLengthHint(21)) +func WithLengthHint(hint uint16) Option { + return func(c *ConfigOptions) { + c.LengthHint = hint + } +} + +// runtimeConfig holds the runtime configuration for the Nano ID generator. +// It is immutable after initialization. +type runtimeConfig struct { + randReader io.Reader // 16 bytes + byteAlphabet []byte // 24 bytes + runeAlphabet []rune // 24 bytes + mask uint // 8 bytes + bitsNeeded uint // 8 bytes + bytesNeeded uint // 8 bytes + bufferSize int // 8 bytes + bufferMultiplier int // 8 bytes + scalingFactor int // 8 bytes + baseMultiplier int // 8 bytes + maxBytesPerRune int // 8 bytes + alphabetLen uint16 // 2 bytes + lengthHint uint16 // 2 bytes + isASCII bool // 1 byte + isPowerOfTwo bool // 1 byte +} + +func buildRuntimeConfig(opts *ConfigOptions) (*runtimeConfig, error) { + if len(opts.Alphabet) == 0 { + return nil, ErrInvalidAlphabet + } + + // Check if the alphabet is valid UTF-8 + if !utf8.ValidString(opts.Alphabet) { + return nil, ErrNonUTF8Alphabet + } + + alphabetRunes := []rune(opts.Alphabet) + isASCII := true + byteAlphabet := make([]byte, len(alphabetRunes)) + maxBytesPerRune := 1 // Initialize to 1 for ASCII + + for i, r := range alphabetRunes { + if r > unicode.MaxASCII { + isASCII = false + // Compute the number of bytes needed to encode this rune + runeBytes := utf8.RuneLen(r) + if runeBytes < 0 { + return nil, ErrInvalidAlphabet + } + if runeBytes > maxBytesPerRune { + maxBytesPerRune = runeBytes + } + } else { + byteAlphabet[i] = byte(r) + } + } + + if !isASCII { + // Convert to rune alphabet if non-ASCII characters are present + byteAlphabet = nil // Clear byteAlphabet as it's not used + } + + // Check for duplicate characters + seenRunes := make(map[rune]bool) + for _, r := range alphabetRunes { + if seenRunes[r] { + return nil, ErrDuplicateCharacters + } + seenRunes[r] = true + } + + // The length of the alphabet, representing the number of unique characters available for ID generation. + alphabetLen := uint16(len(alphabetRunes)) + + // Ensure the alphabet length adheres to predefined constraints. + if alphabetLen > MaxAlphabetLength { + return nil, ErrAlphabetTooLong + } + + if alphabetLen < MinAlphabetLength { + return nil, ErrAlphabetTooShort + } + + // Calculate the minimum number of bits needed to represent all indices of the alphabet. + // This is essential for generating random numbers that map uniformly to the alphabet indices. + // The calculation uses bits.Len to find the position of the highest set bit in alphabetLen - 1. + bitsNeeded := uint(bits.Len(uint(alphabetLen - 1))) + if bitsNeeded == 0 { + return nil, ErrInvalidAlphabet + } + + // Create a bitmask that isolates the bits needed to represent the alphabet indices. + // The mask is used to efficiently extract valid bits from randomly generated bytes. + mask := uint((1 << bitsNeeded) - 1) + + // TODO: Scale bitsNeeded based on length hint (???) + //adjustedBitsNeeded := bitsNeeded + uint(math.Log2(float64(opts.LengthHint))) + + // Determine the number of bytes required to store 'bitsNeeded' bits, rounding up to the nearest byte. + bytesNeeded := (bitsNeeded + 7) / 8 + + // Check if the alphabet length is a power of two, allowing optimization of modulus operations using bitwise AND. + // This optimization improves performance during random index generation. + isPowerOfTwo := (alphabetLen & (alphabetLen - 1)) == 0 + + // Calculate a base multiplier for buffer size based on the length hint. + // The length hint indicates the desired length of the generated IDs. + // Using logarithm ensures the buffer scales appropriately with the ID length. + baseMultiplier := int(math.Ceil(math.Log2(float64(opts.LengthHint) + 2.0))) + + // Determine a scaling factor to adjust the buffer size. + // This factor ensures the buffer is sufficiently large to accommodate the randomness needed, + // balancing between performance (less frequent random reads) and memory usage. + scalingFactor := int(math.Max(3.0, float64(alphabetLen)/math.Pow(float64(opts.LengthHint), 0.6))) + + // Compute the buffer multiplier by adding the base multiplier and a fraction of the scaling factor. + // This combination fine-tunes the buffer size, considering both the ID length and the alphabet size. + bufferMultiplier := baseMultiplier + int(math.Ceil(float64(scalingFactor)/1.5)) + + // Calculate the total buffer size in bytes for generating random data. + // The buffer size is influenced by the buffer multiplier, bytes needed per character, + // and a factor that scales with the length hint. + // A larger buffer reduces the number of calls to the random number generator, improving efficiency. + bufferSize := bufferMultiplier * int(bytesNeeded) * int(math.Max(1.5, float64(opts.LengthHint)/10.0)) + + return &runtimeConfig{ + randReader: opts.RandReader, + byteAlphabet: byteAlphabet, + runeAlphabet: alphabetRunes, + mask: mask, + bitsNeeded: bitsNeeded, + bytesNeeded: bytesNeeded, + bufferSize: bufferSize, + bufferMultiplier: bufferMultiplier, + scalingFactor: scalingFactor, + baseMultiplier: baseMultiplier, + alphabetLen: alphabetLen, + isASCII: isASCII, + isPowerOfTwo: isPowerOfTwo, + lengthHint: opts.LengthHint, + maxBytesPerRune: maxBytesPerRune, + }, nil +} + +// AlphabetLen returns the number of unique characters in the provided alphabet. +// +// This length determines the range of indices for selecting characters during ID generation. +// Using uint16 allows for alphabets up to 65,535 characters. +func (r *runtimeConfig) AlphabetLen() uint16 { + return r.alphabetLen +} + +// BaseMultiplier returns the foundational multiplier used in buffer size calculations. +// +// It is based on the logarithm of the intended ID length (LengthHint) plus 2. +// This helps scale the buffer size appropriately with different ID lengths. +func (r *runtimeConfig) BaseMultiplier() int { + return r.baseMultiplier +} + +// BitsNeeded returns the minimum number of bits required to represent all possible indices of the alphabet. +// +// This value is crucial for generating random numbers that map uniformly to the alphabet indices without bias. +func (r *runtimeConfig) BitsNeeded() uint { + return r.bitsNeeded +} + +// BufferMultiplier returns the combined multiplier used in the buffer size calculation. +// +// It adds a fraction of the scaling factor to the base multiplier to fine-tune the buffer size, +// considering both the ID length and the alphabet size. +func (r *runtimeConfig) BufferMultiplier() int { + return r.bufferMultiplier +} + +// BufferSize returns the total size of the buffer (in bytes) used for generating random data. +// +// The buffer size is calculated to balance efficiency and performance, +// minimizing calls to the random number generator by reading larger chunks of random data at once. +func (r *runtimeConfig) BufferSize() int { + return r.bufferSize +} + +// ByteAlphabet returns the slice of bytes representing the alphabet, +// used when the alphabet consists solely of ASCII characters. +// +// For non-ASCII alphabets, this returns nil, and RuneAlphabet is used instead. +func (r *runtimeConfig) ByteAlphabet() []byte { + return r.byteAlphabet +} + +// BytesNeeded returns the number of bytes required to store the BitsNeeded for each character in the ID. +// +// It rounds up BitsNeeded to the nearest byte, ensuring sufficient space for random data generation. +func (r *runtimeConfig) BytesNeeded() uint { + return r.bytesNeeded +} + +// IsASCII returns true if the alphabet consists solely of ASCII characters. +// +// This allows for optimization in processing, using bytes instead of runes for ID generation. +func (r *runtimeConfig) IsASCII() bool { + return r.isASCII +} + +// IsPowerOfTwo returns true if the length of the alphabet is a power of two. +// +// When true, random index selection can be optimized using bitwise operations, +// such as bitwise AND with the mask, improving performance. +func (r *runtimeConfig) IsPowerOfTwo() bool { + return r.isPowerOfTwo +} + +// LengthHint returns the intended length of the IDs to be generated. +// +// This hint is used in calculations to adjust buffer sizes and scaling factors accordingly. +func (r *runtimeConfig) LengthHint() uint16 { + return r.lengthHint +} + +// Mask returns the bitmask used to extract the necessary bits from randomly generated bytes. +// +// The mask is essential for efficiently mapping random values to valid alphabet indices, +// ensuring uniform distribution and preventing bias. +func (r *runtimeConfig) Mask() uint { + return r.mask +} + +// RandReader returns the source of randomness used for generating IDs. +// +// It is typically a cryptographically secure random number generator (e.g., crypto/rand.Reader). +func (r *runtimeConfig) RandReader() io.Reader { + return r.randReader +} + +// RuneAlphabet returns the slice of runes representing the alphabet. +// +// This is used for ID generation when the alphabet includes non-ASCII (multibyte) characters, +// allowing support for a wider range of characters. +func (r *runtimeConfig) RuneAlphabet() []rune { + return r.runeAlphabet +} + +// ScalingFactor returns the scaling factor used to adjust the buffer size. +// +// It balances the influence of the alphabet size and the intended ID length, +// ensuring efficient random data generation without excessive memory usage. +func (r *runtimeConfig) ScalingFactor() int { + return r.scalingFactor +} + +// MaxBytesPerRune represents the maximum number of bytes required to encode +// any rune in the alphabet using UTF-8 encoding. +func (r *runtimeConfig) MaxBytesPerRune() int { + return r.maxBytesPerRune +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..9a3ae79 --- /dev/null +++ b/config_test.go @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the Apache 2.0 License found in the +// LICENSE file in the root directory of this source tree. + +package nanoid + +import ( + "math/bits" + "testing" + + "github.com/sixafter/nanoid/x/crypto/prng" + "github.com/stretchr/testify/assert" +) + +// TestGetConfig tests the Config() method of the generator. +func TestGetConfig(t *testing.T) { + t.Parallel() + is := assert.New(t) + + gen, err := NewGenerator() + is.NoError(err, "NewGenerator() should not return an error with the default alphabet") + + // Assert that generator implements Configuration interface + config, ok := gen.(Configuration) + is.True(ok, "Generator should implement Configuration interface") + + runtimeConfig := config.Config() + + is.Equal(DefaultAlphabet, string(runtimeConfig.RuneAlphabet()), "Config.RuneAlphabet should match the default alphabet") + is.Equal(uint16(len([]rune(DefaultAlphabet))), runtimeConfig.AlphabetLen(), "Config.AlphabetLen should match the default alphabet length") + + // Update expectedMask calculation based on RuntimeConfig + expectedMask := uint((1 << bits.Len(uint(runtimeConfig.AlphabetLen()-1))) - 1) + is.Equal(expectedMask, runtimeConfig.Mask(), "Config.Mask should be correctly calculated") + + is.Equal((runtimeConfig.AlphabetLen()&(runtimeConfig.AlphabetLen()-1)) == 0, runtimeConfig.IsPowerOfTwo(), "Config.IsPowerOfTwo should be correct") + is.Positive(runtimeConfig.BaseMultiplier(), "Config.BaseMultiplier should be a positive integer") + is.Positive(runtimeConfig.BitsNeeded(), "Config.BitsNeeded should be a positive integer") + is.Positive(runtimeConfig.BufferMultiplier(), "Config.BufferMultiplier should be a positive integer") + is.Positive(runtimeConfig.BufferSize(), "Config.BufferSize should be a positive integer") + is.NotNil(runtimeConfig.ByteAlphabet(), "Config.ByteAlphabet should not be nil") + is.Positive(runtimeConfig.BytesNeeded(), "Config.BytesNeeded should be a positive integer") + is.Equal(true, runtimeConfig.IsASCII(), "Config.IsASCII should be true by default") + is.Equal(true, runtimeConfig.IsPowerOfTwo(), "Config.IsPowerOfTwo should be true by default") + is.Positive(runtimeConfig.LengthHint(), "Config.LengthHint should be a positive integer") + is.Equal(1, runtimeConfig.MaxBytesPerRune(), "Config.MaxBytesPerRune should be 1 by default") + is.Equal(prng.Reader, runtimeConfig.RandReader(), "Config.RandReader should be rand.Reader by default") + is.NotNil(runtimeConfig.RuneAlphabet(), "Config.RuneAlphabet should not be nil") + is.Positive(runtimeConfig.ScalingFactor(), "Config.ScalingFactor should be a positive integer") +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..2b3fa1b --- /dev/null +++ b/errors.go @@ -0,0 +1,41 @@ +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the Apache 2.0 License found in the +// LICENSE file in the root directory of this source tree. + +package nanoid + +import ( + "errors" +) + +var ( + // ErrDuplicateCharacters is returned when the provided alphabet contains duplicate characters. + ErrDuplicateCharacters = errors.New("duplicate characters in alphabet") + + // ErrExceededMaxAttempts is returned when the maximum number of attempts to perform + // an operation, such as generating a unique ID, has been exceeded. + ErrExceededMaxAttempts = errors.New("exceeded maximum attempts") + + // ErrInvalidLength is returned when a specified length value for an operation is invalid. + ErrInvalidLength = errors.New("invalid length") + + // ErrInvalidAlphabet is returned when the provided alphabet for generating IDs is invalid. + ErrInvalidAlphabet = errors.New("invalid alphabet") + + // ErrNonUTF8Alphabet is returned when the provided alphabet contains non-UTF-8 characters. + ErrNonUTF8Alphabet = errors.New("alphabet contains invalid UTF-8 characters") + + // ErrAlphabetTooShort is returned when the provided alphabet has fewer than 2 characters. + ErrAlphabetTooShort = errors.New("alphabet length is less than 2") + + // ErrAlphabetTooLong is returned when the provided alphabet exceeds 256 characters. + ErrAlphabetTooLong = errors.New("alphabet length exceeds 256") + + // ErrNilRandReader is returned when the random number generator (rand.Reader) is nil, + // preventing the generation of random values. + ErrNilRandReader = errors.New("nil random reader") + + // ErrNilPointer is returned when a nil pointer is passed to a function that does not accept nil pointers. + ErrNilPointer = errors.New("nil pointer") +) diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..d366bcb --- /dev/null +++ b/errors_test.go @@ -0,0 +1,197 @@ +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the Apache 2.0 License found in the +// LICENSE file in the root directory of this source tree. + +package nanoid + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestErrDuplicateCharacters ensures that the generator returns ErrDuplicateCharacters +// when the provided alphabet contains duplicate characters. +func TestErrDuplicateCharacters(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // Alphabet with duplicate characters + alphabet := "abcabc" + + _, err := NewGenerator(WithAlphabet(alphabet)) + is.Equal(ErrDuplicateCharacters, err) +} + +// TestErrInvalidLength verifies that the generator returns ErrInvalidLength +// when the LengthHint is set to zero, which is invalid. +func TestErrInvalidLength(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // LengthHint set to 0, which is invalid + _, err := NewGenerator(WithLengthHint(0)) + is.Equal(ErrInvalidLength, err) +} + +// TestErrInvalidAlphabet checks that the generator returns ErrInvalidAlphabet +// when an empty alphabet is provided. +func TestErrInvalidAlphabet(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // Empty alphabet string, which is invalid + alphabet := "" + + _, err := NewGenerator(WithAlphabet(alphabet)) + is.Equal(ErrInvalidAlphabet, err) +} + +// TestErrNonUTF8Alphabet ensures that the generator returns ErrNonUTF8Alphabet +// when the alphabet contains invalid UTF-8 characters. +func TestErrNonUTF8Alphabet(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // Invalid UTF-8 string (e.g., invalid byte sequences) + // For example, byte 0xFF is invalid in UTF-8 + alphabet := string([]byte{0xFF, 0xFE, 0xFD}) + + _, err := NewGenerator(WithAlphabet(alphabet)) + is.Equal(ErrNonUTF8Alphabet, err) +} + +// TestErrAlphabetTooShort verifies that the generator returns ErrAlphabetTooShort +// when the alphabet has fewer than 2 characters. +func TestErrAlphabetTooShort(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // Alphabet with only 1 character, which is too short + alphabet := "A" + + _, err := NewGenerator(WithAlphabet(alphabet)) + is.Equal(ErrAlphabetTooShort, err) +} + +// TestErrAlphabetTooLong checks that the generator returns ErrAlphabetTooLong +// when the alphabet exceeds 256 characters. +func TestErrAlphabetTooLong(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // Create a unique alphabet with 257 unique characters + alphabet := makeUnicodeAlphabet(257) + + // Verify the length. We expect 514 because each character is represented by 2 bytes. + is.Equal(514, len(alphabet)) + + _, err := NewGenerator(WithAlphabet(alphabet)) + is.Equal(ErrAlphabetTooLong, err) +} + +// TestErrNilRandReader ensures that the generator returns ErrNilRandReader +// when the random reader (RandReader) is set to nil. +func TestErrNilRandReader(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // RandReader set to nil, which is invalid + _, err := NewGenerator(WithRandReader(nil)) + is.Equal(ErrNilRandReader, err) +} + +// alwaysInvalidRandReader is a mock implementation of io.Reader that always returns invalid indices. +// For an alphabet of "ABC" (length 3), it returns bytes with value 3, which are invalid indices. +type alwaysInvalidRandReader struct{} + +// Read fills the provided byte slice with the invalid byte (3) and never returns an error. +func (a *alwaysInvalidRandReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 3 // Invalid index for alphabet "ABC" + } + return len(p), nil +} + +// TestErrExceededMaxAttempts verifies that the generator returns ErrExceededMaxAttempts +// when it cannot produce a valid ID within the maximum number of attempts. +func TestErrExceededMaxAttempts(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + // Initialize the mockRandReader that always returns invalid indices. + mockReader := &alwaysInvalidRandReader{} + + const length = 5 + generator, err := NewGenerator( + WithAlphabet("ABC"), // Non-power-of-two alphabet (length 3) + WithRandReader(mockReader), // Mocked RandReader returning invalid indices (3) + WithLengthHint(length), + ) + is.NoError(err, "Expected no error when initializing generator with valid configuration") + + // Attempt to generate an ID; expect ErrExceededMaxAttempts + _, err = generator.New(length) + is.Equal(ErrExceededMaxAttempts, err, "Expected ErrExceededMaxAttempts when generator cannot find valid indices") +} + +// TestErrNilPointer_MarshalText ensures that MarshalText returns ErrNilPointer +// when called on a nil *ID. +func TestErrNilPointer_MarshalText(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + var id *ID = nil + + _, err := id.MarshalText() + is.Equal(ErrNilPointer, err) +} + +// TestErrNilPointer_UnmarshalText ensures that UnmarshalText returns ErrNilPointer +// when called on a nil *ID. +func TestErrNilPointer_UnmarshalText(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + var id *ID = nil + + err := id.UnmarshalText([]byte("test")) + is.Equal(ErrNilPointer, err) +} + +// TestErrNilPointer_MarshalBinary ensures that MarshalBinary returns ErrNilPointer +// when called on a nil *ID. +func TestErrNilPointer_MarshalBinary(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + var id *ID = nil + + _, err := id.MarshalBinary() + is.Equal(ErrNilPointer, err) +} + +// TestErrNilPointer_UnmarshalBinary ensures that UnmarshalBinary returns ErrNilPointer +// when called on a nil *ID. +func TestErrNilPointer_UnmarshalBinary(t *testing.T) { + t.Parallel() + + is := assert.New(t) + + var id *ID = nil + + err := id.UnmarshalBinary([]byte("test")) + is.Equal(ErrNilPointer, err) +} diff --git a/id.go b/id.go new file mode 100644 index 0000000..6d5df42 --- /dev/null +++ b/id.go @@ -0,0 +1,154 @@ +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the Apache 2.0 License found in the +// LICENSE file in the root directory of this source tree. + +package nanoid + +import ( + "strings" +) + +// ID represents a Nano ID as a string. +type ID string + +// EmptyID represents an empty Nano ID. +var EmptyID = ID("") + +// IsEmpty returns true if the ID is an empty ID (EmptyID) +func (id *ID) IsEmpty() bool { + return id.Compare(EmptyID) == 0 +} + +// Compare compares two IDs lexicographically and returns an integer. +// The result will be 0 if id==other, -1 if id < other, and +1 if id > other. +// +// Parameters: +// - other ID: The ID to compare against. +// +// Returns: +// - int: An integer indicating the comparison result. +// +// Usage: +// +// id1 := ID("V1StGXR8_Z5jdHi6B-myT") +// id2 := ID("V1StGXR8_Z5jdHi6B-myT") +// result := id1.Compare(id2) +// fmt.Println(result) // Output: 0 +func (id *ID) Compare(other ID) int { + return strings.Compare(string(*id), string(other)) +} + +// String returns the string representation of the ID. +// It implements the fmt.Stringer interface, allowing the ID to be +// used seamlessly with fmt package functions like fmt.Println and fmt.Printf. +// +// Example: +// +// id := Must() +// fmt.Println(id) // Output: V1StGXR8_Z5jdHi6B-myT +func (id *ID) String() string { + return string(*id) +} + +// MarshalText converts the ID to a byte slice. +// It implements the encoding.TextMarshaler interface, enabling the ID +// to be marshaled into text-based formats such as XML and YAML. +// +// Returns: +// - A byte slice containing the ID. +// - An error if the marshaling fails. +// +// Example: +// +// id := Must() +// text, err := id.MarshalText() +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(string(text)) // Output: V1StGXR8_Z5jdHi6B-myT +func (id *ID) MarshalText() ([]byte, error) { + if id == nil { + return nil, ErrNilPointer + } + + return []byte(*id), nil +} + +// UnmarshalText parses a byte slice and assigns the result to the ID. +// It implements the encoding.TextUnmarshaler interface, allowing the ID +// to be unmarshaled from text-based formats. +// +// Parameters: +// - text: A byte slice containing the ID data. +// +// Returns: +// - An error if the unmarshaling fails. +// +// Example: +// +// var id ID +// err := id.UnmarshalText([]byte("new-id")) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(id) // Output: new-id +func (id *ID) UnmarshalText(text []byte) error { + if id == nil { + return ErrNilPointer + } + + *id = ID(text) + return nil +} + +// MarshalBinary converts the ID to a byte slice. +// It implements the encoding.BinaryMarshaler interface, enabling the ID +// to be marshaled into binary formats for efficient storage or transmission. +// +// Returns: +// - A byte slice containing the ID. +// - An error if the marshaling fails. +// +// Example: +// +// id := Must() +// binaryData, err := id.MarshalBinary() +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(binaryData) // Output: [86 49 83 116 71 88 82 56 95 90 ...] +func (id *ID) MarshalBinary() ([]byte, error) { + if id == nil { + return nil, ErrNilPointer + } + + return []byte(*id), nil +} + +// UnmarshalBinary parses a byte slice and assigns the result to the ID. +// It implements the encoding.BinaryUnmarshaler interface, allowing the ID +// to be unmarshaled from binary formats. +// +// Parameters: +// - data: A byte slice containing the binary ID data. +// +// Returns: +// - An error if the unmarshaling fails. +// +// Example: +// +// var id ID +// err := id.UnmarshalBinary([]byte{86, 49, 83, 116, 71, 88, 82, 56, 95, 90}) // "V1StGXR8_Z5jdHi6B-myT" +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(id) // Output: V1StGXR8_Z5jdHi6B-myT +func (id *ID) UnmarshalBinary(data []byte) error { + if id == nil { + return ErrNilPointer + } + + *id = ID(data) + return nil +} diff --git a/id_test.go b/id_test.go new file mode 100644 index 0000000..a42f1bb --- /dev/null +++ b/id_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2024 Six After, Inc +// +// This source code is licensed under the Apache 2.0 License found in the +// LICENSE file in the root directory of this source tree. + +package nanoid + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestID_String tests the String() method of the ID type. +// It verifies that the String() method returns the underlying string value. +func TestID_String(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // Initialize expected using Must() + expectedID := Must() + expected := expectedID.String() + + // Actual is obtained by calling String() on the ID + actual := expectedID.String() + + // Assert that actual equals expected + is.Equal(expected, actual, "ID.String() should return the underlying string") +} + +// TestID_MarshalText tests the MarshalText() method of the ID type. +// It verifies that MarshalText() returns the correct byte slice representation of the ID. +func TestID_MarshalText(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // Initialize expected using Must() + expectedID := Must() + expectedBytes := []byte(expectedID.String()) + + // Actual is obtained by calling MarshalText() + actualBytes, err := expectedID.MarshalText() + + // Assert no error occurred + is.NoError(err, "MarshalText() should not return an error") + + // Assert that actual bytes match expected bytes + is.Equal(expectedBytes, actualBytes, "MarshalText() should return the correct byte slice") +} + +// TestID_UnmarshalText tests the UnmarshalText() method of the ID type. +// It verifies that UnmarshalText() correctly parses the byte slice and assigns the value to the ID. +func TestID_UnmarshalText(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // Initialize expected using Must() + expectedID := Must() + inputBytes := []byte(expectedID.String()) + + // Initialize a zero-valued ID + var actualID ID + + // Call UnmarshalText with inputBytes + err := actualID.UnmarshalText(inputBytes) + + // Assert no error occurred + is.NoError(err, "UnmarshalText() should not return an error") + + // Assert that actualID matches expectedID + is.Equal(expectedID, actualID, "UnmarshalText() should correctly assign the input value to ID") +} + +// TestID_MarshalBinary tests the MarshalBinary() method of the ID type. +// It verifies that MarshalBinary() returns the correct byte slice representation of the ID. +func TestID_MarshalBinary(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // Initialize expected using Must() + expectedID := Must() + expectedBytes := []byte(expectedID.String()) + + // Actual is obtained by calling MarshalBinary() + actualBytes, err := expectedID.MarshalBinary() + + // Assert no error occurred + is.NoError(err, "MarshalBinary() should not return an error") + + // Assert that actual bytes match expected bytes + is.Equal(expectedBytes, actualBytes, "MarshalBinary() should return the correct byte slice") +} + +// TestID_UnmarshalBinary tests the UnmarshalBinary() method of the ID type. +// It verifies that UnmarshalBinary() correctly parses the byte slice and assigns the value to the ID. +func TestID_UnmarshalBinary(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // Initialize expected using Must() + expectedID := Must() + inputBytes := []byte(expectedID.String()) + + // Initialize a zero-valued ID + var actualID ID + + // Call UnmarshalBinary with inputBytes + err := actualID.UnmarshalBinary(inputBytes) + + // Assert no error occurred + is.NoError(err, "UnmarshalBinary() should not return an error") + + // Assert that actualID matches expectedID + is.Equal(expectedID, actualID, "UnmarshalBinary() should correctly assign the input value to ID") +} + +// TestID_Compare tests the Compare() method of the ID type. +// It verifies that Compare() correctly compares two IDs and returns the expected result. +func TestID_Compare(t *testing.T) { + t.Parallel() + is := assert.New(t) + + id1 := ID("FgEVN8QMTrnKGvBxFjtjw") + id2 := ID("zTxG5Nl21ZAoM8Fabqk3H") + + // Case 1: id1 < id2 + is.Equal(-1, id1.Compare(id2), "id1 should be less than id2") + + // Case 2: id1 = id2 + is.Equal(0, id1.Compare(id1), "id1 should be equal to id1") + + // Case 3: id1 > id2 + is.Equal(1, id2.Compare(id1), "id2 should be greater than id1") +} + +// TestID_IsEmpty tests the IsEmpty() method of the ID type. +// It verifies that IsEmpty() correctly returns true for an empty ID and false for a non-empty ID. +func TestID_IsEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // Initialize two IDs using Must() + id1 := Must() + id2 := EmptyID + + // Case 1: id1 is not empty + is.False(id1.IsEmpty(), "id1 should not be empty") + + // Case 2: id2 is empty + is.True(id2.IsEmpty(), "id2 should be empty") +} diff --git a/nanoid.go b/nanoid.go index 5200d28..4f00f86 100644 --- a/nanoid.go +++ b/nanoid.go @@ -7,15 +7,9 @@ package nanoid import ( "encoding/binary" - "errors" "fmt" - "io" - "math" - "math/bits" "strings" "sync" - "unicode" - "unicode/utf8" "github.com/sixafter/nanoid/x/crypto/prng" ) @@ -26,32 +20,6 @@ var ( // DefaultRandReader is the default random number generator used for generating IDs. DefaultRandReader = prng.Reader - - // ErrDuplicateCharacters is returned when the provided alphabet contains duplicate characters. - ErrDuplicateCharacters = errors.New("duplicate characters in alphabet") - - // ErrExceededMaxAttempts is returned when the maximum number of attempts to perform - // an operation, such as generating a unique ID, has been exceeded. - ErrExceededMaxAttempts = errors.New("exceeded maximum attempts") - - // ErrInvalidLength is returned when a specified length value for an operation is invalid. - ErrInvalidLength = errors.New("invalid length") - - // ErrInvalidAlphabet is returned when the provided alphabet for generating IDs is invalid. - ErrInvalidAlphabet = errors.New("invalid alphabet") - - // ErrNonUTF8Alphabet is returned when the provided alphabet contains non-UTF-8 characters. - ErrNonUTF8Alphabet = errors.New("alphabet contains invalid UTF-8 characters") - - // ErrAlphabetTooShort is returned when the provided alphabet has fewer than 2 characters. - ErrAlphabetTooShort = errors.New("alphabet length is less than 2") - - // ErrAlphabetTooLong is returned when the provided alphabet exceeds 256 characters. - ErrAlphabetTooLong = errors.New("alphabet length exceeds 256") - - // ErrNilRandReader is returned when the random number generator (rand.Reader) is nil, - // preventing the generation of random values. - ErrNilRandReader = errors.New("nil random reader") ) const ( @@ -91,12 +59,6 @@ const ( MaxAlphabetLength = 256 ) -// ID represents a Nano ID as a string. -type ID string - -// EmptyID represents an empty Nano ID. -var EmptyID = ID("") - func init() { var err error DefaultGenerator, err = NewGenerator() @@ -105,127 +67,6 @@ func init() { } } -// ConfigOptions holds the configurable options for the Generator. -// It is used with the Function Options pattern. -type ConfigOptions struct { - // RandReader is the source of randomness used for generating IDs. - // By default, it uses x/crypto/prng/Reader, which provides cryptographically secure random bytes. - RandReader io.Reader - - // Alphabet is the set of characters used to generate the Nano ID. - // It must be a valid UTF-8 string containing between 2 and 256 unique characters. - // Using a diverse and appropriately sized alphabet ensures the uniqueness and randomness of the generated IDs. - Alphabet string - - // LengthHint specifies a typical or default length for generated IDs. - LengthHint uint16 -} - -// Config holds the runtime configuration for the Nano ID generator. -// -// It is immutable after initialization and provides all the necessary -// parameters for generating unique IDs efficiently and securely. -type Config interface { - // AlphabetLen returns the number of unique characters in the provided alphabet. - // - // This length determines the range of indices for selecting characters during ID generation. - // Using uint16 allows for alphabets up to 65,535 characters. - AlphabetLen() uint16 - - // BaseMultiplier returns the foundational multiplier used in buffer size calculations. - // - // It is based on the logarithm of the intended ID length (LengthHint) plus 2. - // This helps scale the buffer size appropriately with different ID lengths. - BaseMultiplier() int - - // BitsNeeded returns the minimum number of bits required to represent all possible indices of the alphabet. - // - // This value is crucial for generating random numbers that map uniformly to the alphabet indices without bias. - BitsNeeded() uint - - // BufferMultiplier returns the combined multiplier used in the buffer size calculation. - // - // It adds a fraction of the scaling factor to the base multiplier to fine-tune the buffer size, - // considering both the ID length and the alphabet size. - BufferMultiplier() int - - // BufferSize returns the total size of the buffer (in bytes) used for generating random data. - // - // The buffer size is calculated to balance efficiency and performance, - // minimizing calls to the random number generator by reading larger chunks of random data at once. - BufferSize() int - - // ByteAlphabet returns the slice of bytes representing the alphabet, - // used when the alphabet consists solely of ASCII characters. - // - // For non-ASCII alphabets, this returns nil, and RuneAlphabet is used instead. - ByteAlphabet() []byte - - // BytesNeeded returns the number of bytes required to store the BitsNeeded for each character in the ID. - // - // It rounds up BitsNeeded to the nearest byte, ensuring sufficient space for random data generation. - BytesNeeded() uint - - // IsASCII returns true if the alphabet consists solely of ASCII characters. - // - // This allows for optimization in processing, using bytes instead of runes for ID generation. - IsASCII() bool - - // IsPowerOfTwo returns true if the length of the alphabet is a power of two. - // - // When true, random index selection can be optimized using bitwise operations, - // such as bitwise AND with the mask, improving performance. - IsPowerOfTwo() bool - - // LengthHint returns the intended length of the IDs to be generated. - // - // This hint is used in calculations to adjust buffer sizes and scaling factors accordingly. - LengthHint() uint16 - - // MaxBytesPerRune represents the maximum number of bytes required to encode - // any rune in the alphabet using UTF-8 encoding. - // - // This value is computed during - // configuration based on the provided alphabet and is used to preallocate the - // buffer size in the newUnicode function. By accurately estimating the buffer size, - // we ensure efficient string building without unnecessary memory allocations - // or buffer resizing. - // - // For example, if the alphabet includes only ASCII and Latin-1 characters, each rune - // requires at most 2 bytes. However, if the alphabet includes emojis or other - // multibyte characters, this value could be up to 4 bytes. - MaxBytesPerRune() int - - // Mask returns the bitmask used to extract the necessary bits from randomly generated bytes. - // - // The mask is essential for efficiently mapping random values to valid alphabet indices, - // ensuring uniform distribution and preventing bias. - Mask() uint - - // RandReader returns the source of randomness used for generating IDs. - // - // It is typically a cryptographically secure random number generator (e.g., crypto/rand.Reader). - RandReader() io.Reader - - // RuneAlphabet returns the slice of runes representing the alphabet. - // - // This is used for ID generation when the alphabet includes non-ASCII (multibyte) characters, - // allowing support for a wider range of characters. - RuneAlphabet() []rune - - // ScalingFactor returns the scaling factor used to adjust the buffer size. - // - // It balances the influence of the alphabet size and the intended ID length, - // ensuring efficient random data generation without excessive memory usage. - ScalingFactor() int -} - -// Configuration defines the interface for retrieving generator configuration. -type Configuration interface { - // Config returns the runtime configuration of the generator. - Config() Config -} - // Generator defines the interface for generating Nano IDs. // Implementations of this interface provide methods to create new IDs // and to read random data, supporting both ID generation and direct random byte access. @@ -258,27 +99,6 @@ type Generator interface { Read(p []byte) (n int, err error) } -// runtimeConfig holds the runtime configuration for the Nano ID generator. -// It is immutable after initialization. -type runtimeConfig struct { - // RandReader is the source of randomness used for generating IDs. - randReader io.Reader - byteAlphabet []byte - runeAlphabet []rune - mask uint - bitsNeeded uint - bytesNeeded uint - bufferSize int - bufferMultiplier int - scalingFactor int - baseMultiplier int - maxBytesPerRune int - alphabetLen uint16 - lengthHint uint16 - isASCII bool - isPowerOfTwo bool -} - type generator struct { config *runtimeConfig entropyPool *sync.Pool @@ -386,76 +206,6 @@ func Read(p []byte) (n int, err error) { return DefaultGenerator.Read(p) } -// Option defines a function type for configuring the Generator. -// It allows for flexible and extensible configuration by applying -// various settings to the ConfigOptions during Generator initialization. -type Option func(*ConfigOptions) - -// WithAlphabet sets a custom alphabet for the Generator. -// The provided alphabet string defines the set of characters that will be -// used to generate Nano IDs. This allows users to customize the character set -// according to their specific requirements, such as using only alphanumeric -// characters, including symbols, or supporting non-ASCII characters. -// -// Parameters: -// - alphabet string: A string representing the desired set of characters for ID generation. -// -// Returns: -// - Option: A configuration option that applies the custom alphabet to ConfigOptions. -// -// Usage: -// -// generator, err := nanoid.NewGenerator(nanoid.WithAlphabet("abcdef123456")) -func WithAlphabet(alphabet string) Option { - return func(c *ConfigOptions) { - c.Alphabet = alphabet - } -} - -// WithRandReader sets a custom random reader for the Generator. -// By default, the Generator uses a cryptographically secure random number -// generator (e.g., crypto/rand.Reader). However, in some cases, users might -// want to provide their own source of randomness, such as for testing purposes -// or to integrate with a different entropy source. -// -// Parameters: -// - reader io.Reader: An implementation of io.Reader that supplies random data. -// -// Returns: -// - Option: A configuration option that applies the custom random reader to ConfigOptions. -// -// Usage Example: -// -// customReader := myCustomRandomReader() -// generator, err := nanoid.NewGenerator( -// nanoid.WithRandReader(customReader)) -func WithRandReader(reader io.Reader) Option { - return func(c *ConfigOptions) { - c.RandReader = reader - } -} - -// WithLengthHint sets the hint of the intended length of the IDs to be generated. -// Providing a length hint allows the Generator to optimize internal configurations, -// such as buffer sizes and scaling factors, based on the expected ID length. This -// can enhance performance and efficiency, especially when generating a large number -// of IDs with similar lengths. -// -// Parameters: -// - hint uint16: A non-zero unsigned integer representing the anticipated length of the Nano IDs. -// -// Returns: -// - Option: A configuration option that applies the length hint to ConfigOptions. -// -// Usage Example: -// -// generator, err := nanoid.NewGenerator(nanoid.WithLengthHint(21)) -func WithLengthHint(hint uint16) Option { - return func(c *ConfigOptions) { - c.LengthHint = hint - } -} - // NewGenerator creates a new Generator with buffer pooling enabled. // It accepts variadic Option parameters to configure the Generator's behavior. // The function initializes the configuration with default values, applies any provided options, @@ -547,161 +297,6 @@ func NewGenerator(options ...Option) (Generator, error) { }, nil } -func buildRuntimeConfig(opts *ConfigOptions) (*runtimeConfig, error) { - if len(opts.Alphabet) == 0 { - return nil, ErrInvalidAlphabet - } - - // Check if the alphabet is valid UTF-8 - if !utf8.ValidString(opts.Alphabet) { - return nil, ErrNonUTF8Alphabet - } - - alphabetRunes := []rune(opts.Alphabet) - isASCII := true - byteAlphabet := make([]byte, len(alphabetRunes)) - maxBytesPerRune := 1 // Initialize to 1 for ASCII - - for i, r := range alphabetRunes { - if r > unicode.MaxASCII { - isASCII = false - // Compute the number of bytes needed to encode this rune - runeBytes := utf8.RuneLen(r) - if runeBytes < 0 { - return nil, ErrInvalidAlphabet - } - if runeBytes > maxBytesPerRune { - maxBytesPerRune = runeBytes - } - } else { - byteAlphabet[i] = byte(r) - } - } - - if !isASCII { - // Convert to rune alphabet if non-ASCII characters are present - byteAlphabet = nil // Clear byteAlphabet as it's not used - } - - // Check for duplicate characters - seenRunes := make(map[rune]bool) - for _, r := range alphabetRunes { - if seenRunes[r] { - return nil, ErrDuplicateCharacters - } - seenRunes[r] = true - } - - // The length of the alphabet, representing the number of unique characters available for ID generation. - alphabetLen := uint16(len(alphabetRunes)) - - // Ensure the alphabet length adheres to predefined constraints. - if alphabetLen > MaxAlphabetLength { - return nil, ErrAlphabetTooLong - } - - if alphabetLen < MinAlphabetLength { - return nil, ErrAlphabetTooShort - } - - // Calculate the minimum number of bits needed to represent all indices of the alphabet. - // This is essential for generating random numbers that map uniformly to the alphabet indices. - // The calculation uses bits.Len to find the position of the highest set bit in alphabetLen - 1. - bitsNeeded := uint(bits.Len(uint(alphabetLen - 1))) - if bitsNeeded == 0 { - return nil, ErrInvalidAlphabet - } - - // Create a bitmask that isolates the bits needed to represent the alphabet indices. - // The mask is used to efficiently extract valid bits from randomly generated bytes. - mask := uint((1 << bitsNeeded) - 1) - - // TODO: Scale bitsNeeded based on length hint (???) - //adjustedBitsNeeded := bitsNeeded + uint(math.Log2(float64(opts.LengthHint))) - - // Determine the number of bytes required to store 'bitsNeeded' bits, rounding up to the nearest byte. - bytesNeeded := (bitsNeeded + 7) / 8 - - // Check if the alphabet length is a power of two, allowing optimization of modulus operations using bitwise AND. - // This optimization improves performance during random index generation. - isPowerOfTwo := (alphabetLen & (alphabetLen - 1)) == 0 - - // Calculate a base multiplier for buffer size based on the length hint. - // The length hint indicates the desired length of the generated IDs. - // Using logarithm ensures the buffer scales appropriately with the ID length. - baseMultiplier := int(math.Ceil(math.Log2(float64(opts.LengthHint) + 2.0))) - - // Determine a scaling factor to adjust the buffer size. - // This factor ensures the buffer is sufficiently large to accommodate the randomness needed, - // balancing between performance (less frequent random reads) and memory usage. - scalingFactor := int(math.Max(3.0, float64(alphabetLen)/math.Pow(float64(opts.LengthHint), 0.6))) - - // Compute the buffer multiplier by adding the base multiplier and a fraction of the scaling factor. - // This combination fine-tunes the buffer size, considering both the ID length and the alphabet size. - bufferMultiplier := baseMultiplier + int(math.Ceil(float64(scalingFactor)/1.5)) - - // Calculate the total buffer size in bytes for generating random data. - // The buffer size is influenced by the buffer multiplier, bytes needed per character, - // and a factor that scales with the length hint. - // A larger buffer reduces the number of calls to the random number generator, improving efficiency. - bufferSize := bufferMultiplier * int(bytesNeeded) * int(math.Max(1.5, float64(opts.LengthHint)/10.0)) - - return &runtimeConfig{ - randReader: opts.RandReader, - byteAlphabet: byteAlphabet, - runeAlphabet: alphabetRunes, - mask: mask, - bitsNeeded: bitsNeeded, - bytesNeeded: bytesNeeded, - bufferSize: bufferSize, - bufferMultiplier: bufferMultiplier, - scalingFactor: scalingFactor, - baseMultiplier: baseMultiplier, - alphabetLen: alphabetLen, - isASCII: isASCII, - isPowerOfTwo: isPowerOfTwo, - lengthHint: opts.LengthHint, - maxBytesPerRune: maxBytesPerRune, - }, nil -} - -// processRandomBytes extracts and returns an unsigned integer from the given randomBytes slice, -// starting at the specified index 'i'. The size of the returned value is determined by the -// g.config.bytesNeeded field. -// -// Parameters: -// - randomBytes: A byte slice containing random data. -// - i: The starting index from which to extract the required bytes from the randomBytes slice. -// -// Returns: -// - uint: An unsigned integer constructed from the bytes, with a size defined by g.config.bytesNeeded. -// -// Behavior: -// - If bytesNeeded is 1, a single byte is returned as an unsigned integer. -// - If bytesNeeded is 2, the function returns a 16-bit unsigned integer (2 bytes) in Big Endian order. -// - If bytesNeeded is 4, the function returns a 32-bit unsigned integer (4 bytes) in Big Endian order. -// - For other values of bytesNeeded, it constructs an unsigned integer by shifting and combining each byte. -// -// This function is kept small to encourage inlining by the compiler. -// -//go:inline -func (g *generator) processRandomBytes(randomBytes []byte, i int) uint { - switch g.config.bytesNeeded { - case 1: - return uint(randomBytes[i]) - case 2: - return uint(binary.BigEndian.Uint16(randomBytes[i : i+2])) - case 4: - return uint(binary.BigEndian.Uint32(randomBytes[i : i+4])) - default: - var rnd uint - for j := 0; j < int(g.config.bytesNeeded); j++ { - rnd = (rnd << 8) | uint(randomBytes[i+j]) - } - return rnd - } -} - // New generates a new Nano ID string of the specified length. // // It validates the provided length to ensure it is a positive integer. @@ -735,6 +330,14 @@ func (g *generator) New(length int) (ID, error) { return g.newUnicode(length) } +// Config holds the runtime configuration for the Nano ID generator. +// +// It is immutable after initialization and provides all the necessary +// parameters for generating unique IDs efficiently and securely. +func (g *generator) Config() Config { + return g.config +} + // newASCII generates a new Nano ID using the ASCII alphabet. func (g *generator) newASCII(length int) (ID, error) { randomBytesPtr := g.entropyPool.Get().(*[]byte) @@ -887,245 +490,37 @@ func (g *generator) Read(p []byte) (n int, err error) { return length, nil } -// IsEmpty returns true if the ID is an empty ID (EmptyID) -func (id ID) IsEmpty() bool { - return id.Compare(EmptyID) == 0 -} - -// Compare compares two IDs lexicographically and returns an integer. -// The result will be 0 if id==other, -1 if id < other, and +1 if id > other. -// -// Parameters: -// - other ID: The ID to compare against. -// -// Returns: -// - int: An integer indicating the comparison result. -// -// Usage: -// -// id1 := ID("V1StGXR8_Z5jdHi6B-myT") -// id2 := ID("V1StGXR8_Z5jdHi6B-myT") -// result := id1.Compare(id2) -// fmt.Println(result) // Output: 0 -func (id ID) Compare(other ID) int { - return strings.Compare(string(id), string(other)) -} - -// String returns the string representation of the ID. -// It implements the fmt.Stringer interface, allowing the ID to be -// used seamlessly with fmt package functions like fmt.Println and fmt.Printf. -// -// Example: -// -// id := Must() -// fmt.Println(id) // Output: V1StGXR8_Z5jdHi6B-myT -func (id ID) String() string { - return string(id) -} - -// MarshalText converts the ID to a byte slice. -// It implements the encoding.TextMarshaler interface, enabling the ID -// to be marshaled into text-based formats such as XML and YAML. -// -// Returns: -// - A byte slice containing the ID. -// - An error if the marshaling fails. -// -// Example: -// -// id := Must() -// text, err := id.MarshalText() -// if err != nil { -// log.Fatal(err) -// } -// fmt.Println(string(text)) // Output: V1StGXR8_Z5jdHi6B-myT -func (id ID) MarshalText() ([]byte, error) { - return []byte(id), nil -} - -// UnmarshalText parses a byte slice and assigns the result to the ID. -// It implements the encoding.TextUnmarshaler interface, allowing the ID -// to be unmarshaled from text-based formats. -// -// Parameters: -// - text: A byte slice containing the ID data. -// -// Returns: -// - An error if the unmarshaling fails. -// -// Example: -// -// var id ID -// err := id.UnmarshalText([]byte("new-id")) -// if err != nil { -// log.Fatal(err) -// } -// fmt.Println(id) // Output: new-id -func (id *ID) UnmarshalText(text []byte) error { - *id = ID(text) - return nil -} - -// MarshalBinary converts the ID to a byte slice. -// It implements the encoding.BinaryMarshaler interface, enabling the ID -// to be marshaled into binary formats for efficient storage or transmission. -// -// Returns: -// - A byte slice containing the ID. -// - An error if the marshaling fails. -// -// Example: -// -// id := Must() -// binaryData, err := id.MarshalBinary() -// if err != nil { -// log.Fatal(err) -// } -// fmt.Println(binaryData) // Output: [86 49 83 116 71 88 82 56 95 90 ...] -func (id ID) MarshalBinary() ([]byte, error) { - return []byte(id), nil -} - -// UnmarshalBinary parses a byte slice and assigns the result to the ID. -// It implements the encoding.BinaryUnmarshaler interface, allowing the ID -// to be unmarshaled from binary formats. +// processRandomBytes extracts and returns an unsigned integer from the given randomBytes slice, +// starting at the specified index 'i'. The size of the returned value is determined by the +// g.config.bytesNeeded field. // // Parameters: -// - data: A byte slice containing the binary ID data. +// - randomBytes: A byte slice containing random data. +// - i: The starting index from which to extract the required bytes from the randomBytes slice. // // Returns: -// - An error if the unmarshaling fails. -// -// Example: -// -// var id ID -// err := id.UnmarshalBinary([]byte{86, 49, 83, 116, 71, 88, 82, 56, 95, 90}) // "V1StGXR8_Z5jdHi6B-myT" -// if err != nil { -// log.Fatal(err) -// } -// fmt.Println(id) // Output: V1StGXR8_Z5jdHi6B-myT -func (id *ID) UnmarshalBinary(data []byte) error { - *id = ID(data) - return nil -} - -// Config holds the runtime configuration for the Nano ID generator. -// -// It is immutable after initialization and provides all the necessary -// parameters for generating unique IDs efficiently and securely. -func (g *generator) Config() Config { - return g.config -} - -// AlphabetLen returns the number of unique characters in the provided alphabet. -// -// This length determines the range of indices for selecting characters during ID generation. -// Using uint16 allows for alphabets up to 65,535 characters. -func (r runtimeConfig) AlphabetLen() uint16 { - return r.alphabetLen -} - -// BaseMultiplier returns the foundational multiplier used in buffer size calculations. -// -// It is based on the logarithm of the intended ID length (LengthHint) plus 2. -// This helps scale the buffer size appropriately with different ID lengths. -func (r runtimeConfig) BaseMultiplier() int { - return r.baseMultiplier -} - -// BitsNeeded returns the minimum number of bits required to represent all possible indices of the alphabet. -// -// This value is crucial for generating random numbers that map uniformly to the alphabet indices without bias. -func (r runtimeConfig) BitsNeeded() uint { - return r.bitsNeeded -} - -// BufferMultiplier returns the combined multiplier used in the buffer size calculation. -// -// It adds a fraction of the scaling factor to the base multiplier to fine-tune the buffer size, -// considering both the ID length and the alphabet size. -func (r runtimeConfig) BufferMultiplier() int { - return r.bufferMultiplier -} - -// BufferSize returns the total size of the buffer (in bytes) used for generating random data. -// -// The buffer size is calculated to balance efficiency and performance, -// minimizing calls to the random number generator by reading larger chunks of random data at once. -func (r runtimeConfig) BufferSize() int { - return r.bufferSize -} - -// ByteAlphabet returns the slice of bytes representing the alphabet, -// used when the alphabet consists solely of ASCII characters. -// -// For non-ASCII alphabets, this returns nil, and RuneAlphabet is used instead. -func (r runtimeConfig) ByteAlphabet() []byte { - return r.byteAlphabet -} - -// BytesNeeded returns the number of bytes required to store the BitsNeeded for each character in the ID. -// -// It rounds up BitsNeeded to the nearest byte, ensuring sufficient space for random data generation. -func (r runtimeConfig) BytesNeeded() uint { - return r.bytesNeeded -} - -// IsASCII returns true if the alphabet consists solely of ASCII characters. -// -// This allows for optimization in processing, using bytes instead of runes for ID generation. -func (r runtimeConfig) IsASCII() bool { - return r.isASCII -} - -// IsPowerOfTwo returns true if the length of the alphabet is a power of two. -// -// When true, random index selection can be optimized using bitwise operations, -// such as bitwise AND with the mask, improving performance. -func (r runtimeConfig) IsPowerOfTwo() bool { - return r.isPowerOfTwo -} - -// LengthHint returns the intended length of the IDs to be generated. -// -// This hint is used in calculations to adjust buffer sizes and scaling factors accordingly. -func (r runtimeConfig) LengthHint() uint16 { - return r.lengthHint -} - -// Mask returns the bitmask used to extract the necessary bits from randomly generated bytes. -// -// The mask is essential for efficiently mapping random values to valid alphabet indices, -// ensuring uniform distribution and preventing bias. -func (r runtimeConfig) Mask() uint { - return r.mask -} - -// RandReader returns the source of randomness used for generating IDs. -// -// It is typically a cryptographically secure random number generator (e.g., crypto/rand.Reader). -func (r runtimeConfig) RandReader() io.Reader { - return r.randReader -} - -// RuneAlphabet returns the slice of runes representing the alphabet. +// - uint: An unsigned integer constructed from the bytes, with a size defined by g.config.bytesNeeded. // -// This is used for ID generation when the alphabet includes non-ASCII (multibyte) characters, -// allowing support for a wider range of characters. -func (r runtimeConfig) RuneAlphabet() []rune { - return r.runeAlphabet -} - -// ScalingFactor returns the scaling factor used to adjust the buffer size. +// Behavior: +// - If bytesNeeded is 1, a single byte is returned as an unsigned integer. +// - If bytesNeeded is 2, the function returns a 16-bit unsigned integer (2 bytes) in Big Endian order. +// - If bytesNeeded is 4, the function returns a 32-bit unsigned integer (4 bytes) in Big Endian order. +// - For other values of bytesNeeded, it constructs an unsigned integer by shifting and combining each byte. // -// It balances the influence of the alphabet size and the intended ID length, -// ensuring efficient random data generation without excessive memory usage. -func (r runtimeConfig) ScalingFactor() int { - return r.scalingFactor -} - -// MaxBytesPerRune represents the maximum number of bytes required to encode -// any rune in the alphabet using UTF-8 encoding. -func (r runtimeConfig) MaxBytesPerRune() int { - return r.maxBytesPerRune +// This function is kept small to encourage inlining by the compiler. +func (g *generator) processRandomBytes(randomBytes []byte, i int) uint { + switch g.config.bytesNeeded { + case 1: + return uint(randomBytes[i]) + case 2: + return uint(binary.BigEndian.Uint16(randomBytes[i : i+2])) + case 4: + return uint(binary.BigEndian.Uint32(randomBytes[i : i+4])) + default: + var rnd uint + for j := 0; j < int(g.config.bytesNeeded); j++ { + rnd = (rnd << 8) | uint(randomBytes[i+j]) + } + return rnd + } } diff --git a/nanoid_test.go b/nanoid_test.go index 15ee7fa..ad58ba2 100644 --- a/nanoid_test.go +++ b/nanoid_test.go @@ -10,27 +10,25 @@ import ( "errors" "fmt" "io" - "math/bits" "strconv" "sync" "testing" - "github.com/sixafter/nanoid/x/crypto/prng" "github.com/stretchr/testify/assert" ) var ( // Ensure ID implements the fmt.Stringer interface - _ = fmt.Stringer(EmptyID) + _ = fmt.Stringer(&EmptyID) // Ensure ID implements the encoding.BinaryMarshaler interface - _ = encoding.BinaryMarshaler(EmptyID) + _ = encoding.BinaryMarshaler(&EmptyID) // Ensure ID implements the encoding.BinaryUnmarshaler interface _ = encoding.BinaryUnmarshaler(&EmptyID) // Ensure ID implements the encoding.TextMarshaler interface - _ = encoding.TextMarshaler(EmptyID) + _ = encoding.TextMarshaler(&EmptyID) // Ensure ID implements the encoding.TextUnmarshaler interface _ = encoding.TextUnmarshaler(&EmptyID) @@ -106,10 +104,10 @@ func TestGenerateWithCustomAlphabet(t *testing.T) { is := assert.New(t) // Include Unicode characters in the custom alphabet - customAlphabet := "abcπŸ˜ŠπŸš€πŸŒŸ" + alphabet := "abcπŸ˜ŠπŸš€πŸŒŸ" const idLength = 8 gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a valid custom alphabet") @@ -118,7 +116,7 @@ func TestGenerateWithCustomAlphabet(t *testing.T) { is.NoError(err, "New(10) should not return an error") is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, customAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithDuplicateAlphabet tests that the generator returns an error with duplicate characters. @@ -126,9 +124,9 @@ func TestGenerateWithDuplicateAlphabet(t *testing.T) { t.Parallel() is := assert.New(t) - duplicateAlphabet := "aabbcc😊😊" + alphabet := "aabbcc😊😊" gen, err := NewGenerator( - WithAlphabet(duplicateAlphabet), + WithAlphabet(alphabet), ) is.Error(err, "NewGenerator() should return an error with duplicate characters in the alphabet") is.Nil(gen, "Generator should be nil when initialization fails") @@ -145,9 +143,9 @@ func TestNewGeneratorWithInvalidAlphabet(t *testing.T) { mean := mean(lengths) // Define the alphabet types to test - alphabetTypes := []string{"ASCII", "Unicode"} + types := []string{"ASCII", "Unicode"} - for _, alphabetType := range alphabetTypes { + for _, alphabetType := range types { for _, length := range lengths { // New the appropriate alphabet var alphabet string @@ -186,10 +184,7 @@ func TestInvalidUTF8Alphabet(t *testing.T) { // Create a byte slice with an invalid UTF-8 sequence. // Here, 0x80 is a continuation byte, which by itself is not valid UTF-8. - invalidUTF8 := []byte{0x80} - - // Convert the byte slice to a string. - alphabet := string(invalidUTF8) + alphabet := string([]byte{0x80}) gen, err := NewGenerator( WithAlphabet(alphabet), @@ -200,42 +195,6 @@ func TestInvalidUTF8Alphabet(t *testing.T) { is.Equal(ErrNonUTF8Alphabet, err, "Expected ErrNonUTF8Alphabet") } -// TestGetConfig tests the Config() method of the generator. -func TestGetConfig(t *testing.T) { - t.Parallel() - is := assert.New(t) - - gen, err := NewGenerator() - is.NoError(err, "NewGenerator() should not return an error with the default alphabet") - - // Assert that generator implements Configuration interface - config, ok := gen.(Configuration) - is.True(ok, "Generator should implement Configuration interface") - - runtimeConfig := config.Config() - - is.Equal(DefaultAlphabet, string(runtimeConfig.RuneAlphabet()), "Config.RuneAlphabet should match the default alphabet") - is.Equal(uint16(len([]rune(DefaultAlphabet))), runtimeConfig.AlphabetLen(), "Config.AlphabetLen should match the default alphabet length") - - // Update expectedMask calculation based on RuntimeConfig - expectedMask := uint((1 << bits.Len(uint(runtimeConfig.AlphabetLen()-1))) - 1) - is.Equal(expectedMask, runtimeConfig.Mask(), "Config.Mask should be correctly calculated") - - is.Equal((runtimeConfig.AlphabetLen()&(runtimeConfig.AlphabetLen()-1)) == 0, runtimeConfig.IsPowerOfTwo(), "Config.IsPowerOfTwo should be correct") - is.Positive(runtimeConfig.BaseMultiplier(), "Config.BaseMultiplier should be a positive integer") - is.Positive(runtimeConfig.BitsNeeded(), "Config.BitsNeeded should be a positive integer") - is.Positive(runtimeConfig.BufferMultiplier(), "Config.BufferMultiplier should be a positive integer") - is.Positive(runtimeConfig.BufferSize(), "Config.BufferSize should be a positive integer") - is.NotNil(runtimeConfig.ByteAlphabet(), "Config.ByteAlphabet should not be nil") - is.Positive(runtimeConfig.BytesNeeded(), "Config.BytesNeeded should be a positive integer") - is.Equal(true, runtimeConfig.IsASCII(), "Config.IsASCII should be true by default") - is.Positive(runtimeConfig.LengthHint(), "Config.LengthHint should be a positive integer") - is.Equal(prng.Reader, runtimeConfig.RandReader(), "Config.RandReader should be rand.Reader by default") - is.NotNil(runtimeConfig.RuneAlphabet(), "Config.RuneAlphabet should not be nil") - is.Positive(runtimeConfig.ScalingFactor(), "Config.ScalingFactor should be a positive integer") - is.Positive(runtimeConfig.MaxBytesPerRune(), "Config.MaxBytesPerRune should be a positive integer") -} - // TestUniqueness tests that multiple generated IDs are unique. func TestUniqueness(t *testing.T) { t.Parallel() @@ -355,7 +314,7 @@ func TestWithRandReader(t *testing.T) { is := assert.New(t) // Define a custom alphabet - customAlphabet := "ABCD" + alphabet := "ABCD" // Define a custom random source with known bytes // For example, bytes [0,1,2,3] should map to 'A','B','C','D' @@ -364,7 +323,7 @@ func TestWithRandReader(t *testing.T) { // Initialize the generator with custom alphabet and custom random reader gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(customReader), ) is.NoError(err, "NewGenerator() should not return an error with valid custom alphabet and random reader") @@ -391,7 +350,7 @@ func TestWithRandReaderDifferentSequence(t *testing.T) { is := assert.New(t) // Define a different custom alphabet - customAlphabet := "WXYZ" + alphabet := "WXYZ" // Define a different custom random source with known bytes // For example, bytes [3,2,1,0] should map to 'Z','Y','X','W' @@ -400,7 +359,7 @@ func TestWithRandReaderDifferentSequence(t *testing.T) { // Initialize the generator with custom alphabet and custom random reader gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(customReader), ) is.NoError(err, "NewGenerator() should not return an error with valid custom alphabet and random reader") @@ -428,7 +387,7 @@ func TestWithRandReaderInsufficientBytes(t *testing.T) { is := assert.New(t) // Define a custom alphabet - customAlphabet := "EFGH" + alphabet := "EFGH" // Define a custom random source with a single byte customBytes := []byte{1} // Should map to 'F' repeatedly @@ -436,7 +395,7 @@ func TestWithRandReaderInsufficientBytes(t *testing.T) { // Initialize the generator with custom alphabet and custom random reader gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(customReader), ) is.NoError(err, "NewGenerator() should not return an error with valid custom alphabet and random reader") @@ -458,10 +417,10 @@ func TestGenerateWithNonPowerOfTwoAlphabetLength(t *testing.T) { is := assert.New(t) // Alphabet length is 10, which is not a power of two - customAlphabet := "ABCDEFGHIJ" // Length = 10 + alphabet := "ABCDEFGHIJ" // Length = 10 const idLength = 16 gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a valid non-power-of-two alphabet length") @@ -470,7 +429,7 @@ func TestGenerateWithNonPowerOfTwoAlphabetLength(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, customAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithMinimalAlphabet tests ID generation with the minimal valid alphabet size. @@ -479,10 +438,10 @@ func TestGenerateWithMinimalAlphabet(t *testing.T) { is := assert.New(t) // Minimal valid alphabet length is 2 - minimalAlphabet := "01" + alphabet := "01" const idLength = 32 gen, err := NewGenerator( - WithAlphabet(minimalAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with the minimal alphabet length") @@ -491,7 +450,7 @@ func TestGenerateWithMinimalAlphabet(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, minimalAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithMaximalAlphabet tests the generation of IDs with a large alphabet size. @@ -500,15 +459,15 @@ func TestGenerateWithMaximalAlphabet(t *testing.T) { is := assert.New(t) // Generate a maximal alphabet of 256 unique runes that form a valid UTF-8 string - var maximalAlphabet string + var alphabet string for i := 0; i < MaxAlphabetLength; i++ { // Ensure each rune is a valid UTF-8 character // Runes from 0x0000 to 0x00FF are valid and can be represented in UTF-8 - maximalAlphabet += string(rune(i)) + alphabet += string(rune(i)) } const idLength = 128 gen, err := NewGenerator( - WithAlphabet(maximalAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a maximal alphabet length") @@ -517,7 +476,7 @@ func TestGenerateWithMaximalAlphabet(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, maximalAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithCustomRandReaderReturningError tests generator behavior when the custom random reader returns an error. @@ -530,9 +489,9 @@ func TestGenerateWithCustomRandReaderReturningError(t *testing.T) { const idLength = 8 // Initialize the generator with a valid alphabet and the failing random reader - customAlphabet := "ABCDEFGH" + alphabet := "ABCDEFGH" gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(failingReader), WithLengthHint(idLength), ) @@ -559,10 +518,10 @@ func TestGenerateWithNonASCIIAlphabet(t *testing.T) { is := assert.New(t) // Define a Unicode alphabet with emojis and special characters - unicodeAlphabet := "Ξ±Ξ²Ξ³Ξ΄Ξ΅πŸ˜ŠπŸš€πŸŒŸ" + alphabet := "Ξ±Ξ²Ξ³Ξ΄Ξ΅πŸ˜ŠπŸš€πŸŒŸ" const idLength = 10 gen, err := NewGenerator( - WithAlphabet(unicodeAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a valid Unicode alphabet and isASCII=false") @@ -571,7 +530,7 @@ func TestGenerateWithNonASCIIAlphabet(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, unicodeAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithSpecialCharactersInAlphabet tests ID generation with an alphabet containing special characters and emojis. @@ -580,10 +539,10 @@ func TestGenerateWithSpecialCharactersInAlphabet(t *testing.T) { is := assert.New(t) // Alphabet with special characters and emojis - specialAlphabet := "!@#$%^&*()_+πŸ˜ŠπŸš€" + alphabet := "!@#$%^&*()_+πŸ˜ŠπŸš€" const idLength = 12 gen, err := NewGenerator( - WithAlphabet(specialAlphabet), + WithAlphabet(alphabet), ) is.NoError(err, "NewGenerator() should not return an error with a special characters alphabet") @@ -591,7 +550,7 @@ func TestGenerateWithSpecialCharactersInAlphabet(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, specialAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithVeryLargeLength tests ID generation with a very large length. @@ -600,10 +559,10 @@ func TestGenerateWithVeryLargeLength(t *testing.T) { is := assert.New(t) // Define a standard alphabet - standardAlphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" const idLength = 1000 // Very large length gen, err := NewGenerator( - WithAlphabet(standardAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a valid alphabet") @@ -612,7 +571,7 @@ func TestGenerateWithVeryLargeLength(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, standardAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGeneratorBufferReuse tests that buffers are correctly reused from the pool without residual data. @@ -620,10 +579,10 @@ func TestGeneratorBufferReuse(t *testing.T) { t.Parallel() is := assert.New(t) - customAlphabet := "XYZ123" + alphabet := "XYZ123" const idLength = 6 gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a valid custom alphabet") @@ -632,13 +591,13 @@ func TestGeneratorBufferReuse(t *testing.T) { id1, err := gen.New(idLength) is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id1.String())), "Generated ID should have the specified length") - is.True(isValidID(id1, customAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id1, alphabet), "Generated ID contains invalid characters") // Generate second ID id2, err := gen.New(idLength) is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id2.String())), "Generated ID should have the specified length") - is.True(isValidID(id2, customAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id2, alphabet), "Generated ID contains invalid characters") // Ensure that IDs are different if possible if id1 == id2 { @@ -652,13 +611,13 @@ func TestGenerateWithMaxAttempts(t *testing.T) { is := assert.New(t) // Define a small alphabet - customAlphabet := "ABC" // len=3, bitsNeeded=2, mask=3 + alphabet := "ABC" // len=3, bitsNeeded=2, mask=3 // Define a random reader that always returns rnd=3 (>= len(alphabet)=3) failReader := &alwaysFailRandReader{} gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(failReader), WithLengthHint(10), ) @@ -676,11 +635,11 @@ func TestGeneratorWithZeroLengthHint(t *testing.T) { t.Parallel() is := assert.New(t) - customAlphabet := "ABCDEFGHijklmnopQR" + alphabet := "ABCDEFGHijklmnopQR" lengthHint := uint16(0) _, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithLengthHint(lengthHint), ) is.Error(err, "NewGenerator() should return an error with LengthHint=0") @@ -692,14 +651,14 @@ func TestNewWithZeroLengthHintAndMaxAlphabet(t *testing.T) { is := assert.New(t) // Define the maximum valid alphabet size - maxAlphabet := make([]rune, MaxAlphabetLength) + alphabet := make([]rune, MaxAlphabetLength) for i := 0; i < MaxAlphabetLength; i++ { - maxAlphabet[i] = rune(i) + alphabet[i] = rune(i) } lengthHint := uint16(0) gen, err := NewGenerator( - WithAlphabet(string(maxAlphabet)), + WithAlphabet(string(alphabet)), WithLengthHint(lengthHint), ) is.Error(err, "NewGenerator() should return an error with LengthHint=0 and maximum alphabet size") @@ -716,9 +675,9 @@ func TestGenerateWithCustomRandReaderReturningNoBytes(t *testing.T) { const idLength = 8 // Initialize the generator with a valid alphabet and the empty random reader - customAlphabet := "ABCDEFGH" + alphabet := "ABCDEFGH" gen, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(emptyReader), WithLengthHint(idLength), ) @@ -798,13 +757,13 @@ func TestGenerateWithAllPrintableASCII(t *testing.T) { is := assert.New(t) // Define an alphabet with all printable ASCII characters - var asciiAlphabet string + var alphabet string for i := 32; i <= 126; i++ { - asciiAlphabet += string(rune(i)) + alphabet += string(rune(i)) } const idLength = 20 gen, err := NewGenerator( - WithAlphabet(asciiAlphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with all printable ASCII characters") @@ -813,7 +772,7 @@ func TestGenerateWithAllPrintableASCII(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, asciiAlphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGenerateWithSpecialUTF8Characters tests the generation of IDs with an alphabet containing special UTF-8 characters. @@ -822,10 +781,10 @@ func TestGenerateWithSpecialUTF8Characters(t *testing.T) { is := assert.New(t) // Alphabet with special UTF-8 characters - specialUTF8Alphabet := "Γ€ΓΆΓΌΓŸπŸ˜Šβœ¨πŸ’–" + alphabet := "Γ€ΓΆΓΌΓŸπŸ˜Šβœ¨πŸ’–" const idLength = 15 gen, err := NewGenerator( - WithAlphabet(specialUTF8Alphabet), + WithAlphabet(alphabet), WithLengthHint(idLength), ) is.NoError(err, "NewGenerator() should not return an error with a special UTF-8 characters alphabet") @@ -834,7 +793,7 @@ func TestGenerateWithSpecialUTF8Characters(t *testing.T) { is.NoError(err, "gen.New(%d) should not return an error", idLength) is.Equal(idLength, len([]rune(id)), "Generated ID should have the specified length") - is.True(isValidID(id, specialUTF8Alphabet), "Generated ID contains invalid characters") + is.True(isValidID(id, alphabet), "Generated ID contains invalid characters") } // TestGeneratorWithInvalidLengthHint tests that the generator returns an error when LengthHint is invalid. @@ -842,11 +801,11 @@ func TestGeneratorWithInvalidLengthHint(t *testing.T) { t.Parallel() is := assert.New(t) - customAlphabet := "ABCDEFG" + alphabet := "ABCDEFG" lengthHint := uint16(0) _, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithLengthHint(lengthHint), ) is.Error(err, "NewGenerator() should return an error when LengthHint is zero") @@ -859,12 +818,12 @@ func TestGenerateWithMaxAttemptsExceeded(t *testing.T) { is := assert.New(t) // Define a small alphabet - smallAlphabet := "AB" + alphabet := "AB" const idLength = 100 failReader := &alwaysFailRandReader{} gen, err := NewGenerator( - WithAlphabet(smallAlphabet), + WithAlphabet(alphabet), WithRandReader(failReader), WithLengthHint(idLength), ) @@ -892,9 +851,9 @@ func TestGenerateWithEmptyAlphabet(t *testing.T) { t.Parallel() is := assert.New(t) - customAlphabet := "" + alphabet := "" _, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), ) is.Error(err, "NewGenerator() should return an error when alphabet is empty") is.Equal(ErrInvalidAlphabet, err, "Expected ErrInvalidAlphabet when the alphabet is empty") @@ -905,9 +864,9 @@ func TestGenerateWithNilRandReader(t *testing.T) { t.Parallel() is := assert.New(t) - customAlphabet := "ABCDEFG" + alphabet := "ABCDEFG" _, err := NewGenerator( - WithAlphabet(customAlphabet), + WithAlphabet(alphabet), WithRandReader(io.Reader(nil)), ) is.Error(err, "NewGenerator() should return an error when RandReader is nil") @@ -1077,142 +1036,3 @@ type errorReader struct{} func (e *errorReader) Read(_ []byte) (int, error) { return 0, errors.New("simulated read error") } - -// TestID_String tests the String() method of the ID type. -// It verifies that the String() method returns the underlying string value. -func TestID_String(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // Initialize expected using Must() - expectedID := Must() - expected := expectedID.String() - - // Actual is obtained by calling String() on the ID - actual := expectedID.String() - - // Assert that actual equals expected - is.Equal(expected, actual, "ID.String() should return the underlying string") -} - -// TestID_MarshalText tests the MarshalText() method of the ID type. -// It verifies that MarshalText() returns the correct byte slice representation of the ID. -func TestID_MarshalText(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // Initialize expected using Must() - expectedID := Must() - expectedBytes := []byte(expectedID.String()) - - // Actual is obtained by calling MarshalText() - actualBytes, err := expectedID.MarshalText() - - // Assert no error occurred - is.NoError(err, "MarshalText() should not return an error") - - // Assert that actual bytes match expected bytes - is.Equal(expectedBytes, actualBytes, "MarshalText() should return the correct byte slice") -} - -// TestID_UnmarshalText tests the UnmarshalText() method of the ID type. -// It verifies that UnmarshalText() correctly parses the byte slice and assigns the value to the ID. -func TestID_UnmarshalText(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // Initialize expected using Must() - expectedID := Must() - inputBytes := []byte(expectedID.String()) - - // Initialize a zero-valued ID - var actualID ID - - // Call UnmarshalText with inputBytes - err := actualID.UnmarshalText(inputBytes) - - // Assert no error occurred - is.NoError(err, "UnmarshalText() should not return an error") - - // Assert that actualID matches expectedID - is.Equal(expectedID, actualID, "UnmarshalText() should correctly assign the input value to ID") -} - -// TestID_MarshalBinary tests the MarshalBinary() method of the ID type. -// It verifies that MarshalBinary() returns the correct byte slice representation of the ID. -func TestID_MarshalBinary(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // Initialize expected using Must() - expectedID := Must() - expectedBytes := []byte(expectedID.String()) - - // Actual is obtained by calling MarshalBinary() - actualBytes, err := expectedID.MarshalBinary() - - // Assert no error occurred - is.NoError(err, "MarshalBinary() should not return an error") - - // Assert that actual bytes match expected bytes - is.Equal(expectedBytes, actualBytes, "MarshalBinary() should return the correct byte slice") -} - -// TestID_UnmarshalBinary tests the UnmarshalBinary() method of the ID type. -// It verifies that UnmarshalBinary() correctly parses the byte slice and assigns the value to the ID. -func TestID_UnmarshalBinary(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // Initialize expected using Must() - expectedID := Must() - inputBytes := []byte(expectedID.String()) - - // Initialize a zero-valued ID - var actualID ID - - // Call UnmarshalBinary with inputBytes - err := actualID.UnmarshalBinary(inputBytes) - - // Assert no error occurred - is.NoError(err, "UnmarshalBinary() should not return an error") - - // Assert that actualID matches expectedID - is.Equal(expectedID, actualID, "UnmarshalBinary() should correctly assign the input value to ID") -} - -// TestID_Compare tests the Compare() method of the ID type. -// It verifies that Compare() correctly compares two IDs and returns the expected result. -func TestID_Compare(t *testing.T) { - t.Parallel() - is := assert.New(t) - - id1 := ID("FgEVN8QMTrnKGvBxFjtjw") - id2 := ID("zTxG5Nl21ZAoM8Fabqk3H") - - // Case 1: id1 < id2 - is.Equal(-1, id1.Compare(id2), "id1 should be less than id2") - - // Case 2: id1 = id2 - is.Equal(0, id1.Compare(id1), "id1 should be equal to id1") - - // Case 3: id1 > id2 - is.Equal(1, id2.Compare(id1), "id2 should be greater than id1") -} - -// TestID_IsEmpty tests the IsEmpty() method of the ID type. -// It verifies that IsEmpty() correctly returns true for an empty ID and false for a non-empty ID. -func TestID_IsEmpty(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // Initialize two IDs using Must() - id1 := Must() - id2 := EmptyID - - // Case 1: id1 is not empty - is.False(id1.IsEmpty(), "id1 should not be empty") - - // Case 2: id2 is empty - is.True(id2.IsEmpty(), "id2 should be empty") -} diff --git a/x/crypto/prng/README.md b/x/crypto/prng/README.md new file mode 100644 index 0000000..a468cca --- /dev/null +++ b/x/crypto/prng/README.md @@ -0,0 +1,211 @@ +# prng: Cryptographically Secure Pseudo-Random Number Generator (PRNG) + +## Overview + +The prng package provides a high-performance, cryptographically secure pseudo-random number generator (PRNG) +that implements the io.Reader interface. Designed for concurrent use, it leverages the ChaCha20 cipher stream +to efficiently generate random bytes. + +The package includes a global Reader and a sync.Pool to manage PRNG instances, ensuring low contention and +optimized performance. + +Performance Benchmarks for concurrent reads of standard size Nano ID generation of 21 bytes: + +* Throughput: ~3.16 `ns/op` +* Memory Usage: 0 `B/op` +* Allocations: 0 `allocs/op` + +These benchmarks demonstrate the package's focus on minimizing latency, memory usage, and allocation overhead, making it suitable for high-performance applications. + +
+ Expand to see results + +```shell +go test -bench=. -benchmem -memprofile=mem.out -cpuprofile=cpu.out +goos: darwin +goarch: arm64 +pkg: github.com/sixafter/nanoid/x/crypto/prng +cpu: Apple M4 Max +BenchmarkPRNG_ReadSerial/Serial_Read_8Bytes-16 61095649 16.73 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_16Bytes-16 50855985 23.37 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_21Bytes-16 42470296 27.94 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_32Bytes-16 33680940 35.87 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_64Bytes-16 20185567 58.66 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_100Bytes-16 13742863 86.86 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_256Bytes-16 7850715 151.8 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_512Bytes-16 4226336 283.3 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_1000Bytes-16 2120786 568.3 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_4096Bytes-16 570459 2125 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSerial/Serial_Read_16384Bytes-16 142891 8472 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_16Bytes_10Goroutines-16 327010598 3.914 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_16Bytes_100Goroutines-16 625220077 2.589 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_16Bytes_1000Goroutines-16 615430639 2.799 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_21Bytes_10Goroutines-16 460389994 3.196 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_21Bytes_100Goroutines-16 404379598 4.376 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_21Bytes_1000Goroutines-16 490523025 3.641 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_32Bytes_10Goroutines-16 385261396 4.748 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_32Bytes_100Goroutines-16 390760681 3.960 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_32Bytes_1000Goroutines-16 413073429 3.278 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_64Bytes_10Goroutines-16 245182754 7.568 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_64Bytes_100Goroutines-16 252511812 6.157 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_64Bytes_1000Goroutines-16 238363909 7.617 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_100Bytes_10Goroutines-16 149754214 9.942 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_100Bytes_100Goroutines-16 142018248 10.49 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_100Bytes_1000Goroutines-16 151676746 10.31 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_256Bytes_10Goroutines-16 100000000 12.23 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_256Bytes_100Goroutines-16 100000000 11.58 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_256Bytes_1000Goroutines-16 100000000 12.70 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_512Bytes_10Goroutines-16 54788371 21.60 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_512Bytes_100Goroutines-16 52693305 21.97 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_512Bytes_1000Goroutines-16 54657770 23.91 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_1000Bytes_10Goroutines-16 27138499 43.44 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_1000Bytes_100Goroutines-16 26837056 45.54 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_1000Bytes_1000Goroutines-16 27158050 44.98 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_4096Bytes_10Goroutines-16 7433740 163.9 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_4096Bytes_100Goroutines-16 7371459 163.6 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_4096Bytes_1000Goroutines-16 7314062 164.2 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_16384Bytes_10Goroutines-16 1876988 640.2 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_16384Bytes_100Goroutines-16 1880425 642.6 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrent/Concurrent_Read_16384Bytes_1000Goroutines-16 1891022 637.6 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSequentialLargeSizes/Serial_Read_Large_4096Bytes-16 533180 2205 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSequentialLargeSizes/Serial_Read_Large_10000Bytes-16 222049 5388 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSequentialLargeSizes/Serial_Read_Large_16384Bytes-16 136656 8759 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadSequentialLargeSizes/Serial_Read_Large_65536Bytes-16 34308 35107 ns/op 1 B/op 0 allocs/op +BenchmarkPRNG_ReadSequentialLargeSizes/Serial_Read_Large_1048576Bytes-16 2116 560392 ns/op 496 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_4096Bytes_10Goroutines-16 7386999 165.0 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_4096Bytes_100Goroutines-16 7367784 162.0 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_4096Bytes_1000Goroutines-16 7452956 162.9 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_10000Bytes_10Goroutines-16 3050674 397.3 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_10000Bytes_100Goroutines-16 3032727 395.9 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_10000Bytes_1000Goroutines-16 3044510 394.2 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_16384Bytes_10Goroutines-16 1883161 642.1 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_16384Bytes_100Goroutines-16 1853803 646.7 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_16384Bytes_1000Goroutines-16 1856721 638.6 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_65536Bytes_10Goroutines-16 458271 2534 ns/op 2 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_65536Bytes_100Goroutines-16 461078 2536 ns/op 2 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_65536Bytes_1000Goroutines-16 463308 2538 ns/op 2 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_1048576Bytes_10Goroutines-16 29392 40742 ns/op 713 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_1048576Bytes_100Goroutines-16 29422 40746 ns/op 784 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentLargeSizes/Concurrent_Read_Large_1048576Bytes_1000Goroutines-16 29251 40800 ns/op 753 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_8Bytes-16 68608347 16.61 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_16Bytes-16 51152076 22.99 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_21Bytes-16 42930866 28.03 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_24Bytes-16 39859881 29.86 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_32Bytes-16 33484634 35.35 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_48Bytes-16 24490773 48.32 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_64Bytes-16 20496118 59.26 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_128Bytes-16 12214537 98.06 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_256Bytes-16 7697662 155.9 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_512Bytes-16 4106547 291.4 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_1024Bytes-16 2129442 559.6 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_2048Bytes-16 1000000 1100 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadVariableSizes/Serial_Read_Variable_4096Bytes-16 553623 2171 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_8Bytes_10Goroutines-16 733742068 1.985 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_8Bytes_100Goroutines-16 808259511 2.588 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_8Bytes_1000Goroutines-16 686577902 3.023 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_16Bytes_10Goroutines-16 561635169 2.615 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_16Bytes_100Goroutines-16 544451948 3.381 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_16Bytes_1000Goroutines-16 555864582 3.580 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_21Bytes_10Goroutines-16 455553841 4.740 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_21Bytes_100Goroutines-16 423823033 3.833 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_21Bytes_1000Goroutines-16 450182042 4.029 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_24Bytes_10Goroutines-16 389610598 4.173 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_24Bytes_100Goroutines-16 431140887 3.930 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_24Bytes_1000Goroutines-16 440629598 3.393 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_32Bytes_10Goroutines-16 389705337 3.308 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_32Bytes_100Goroutines-16 320857830 4.472 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_32Bytes_1000Goroutines-16 326843707 4.072 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_48Bytes_10Goroutines-16 198504434 8.365 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_48Bytes_100Goroutines-16 190762611 8.312 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_48Bytes_1000Goroutines-16 199009430 7.753 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_64Bytes_10Goroutines-16 235403240 5.751 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_64Bytes_100Goroutines-16 242589153 5.827 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_64Bytes_1000Goroutines-16 244511232 5.508 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_128Bytes_10Goroutines-16 138769131 8.816 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_128Bytes_100Goroutines-16 145349035 8.788 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_128Bytes_1000Goroutines-16 151517790 8.132 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_256Bytes_10Goroutines-16 99757879 11.57 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_256Bytes_100Goroutines-16 94553646 11.95 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_256Bytes_1000Goroutines-16 100000000 11.82 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_512Bytes_10Goroutines-16 53568338 22.13 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_512Bytes_100Goroutines-16 54571398 22.10 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_512Bytes_1000Goroutines-16 53821010 21.66 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_1024Bytes_10Goroutines-16 28318333 43.82 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_1024Bytes_100Goroutines-16 27987386 42.62 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_1024Bytes_1000Goroutines-16 28246702 42.21 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_2048Bytes_10Goroutines-16 14485872 82.88 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_2048Bytes_100Goroutines-16 14364454 82.24 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_2048Bytes_1000Goroutines-16 14617455 83.71 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_4096Bytes_10Goroutines-16 7475578 162.5 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_4096Bytes_100Goroutines-16 7485626 163.3 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadConcurrentVariableSizes/Concurrent_Read_Variable_4096Bytes_1000Goroutines-16 7430336 166.8 ns/op 0 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Serial_Read_Extreme_10485760Bytes-16 213 5617983 ns/op 49291 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Concurrent_Read_Extreme_10485760Bytes_10Goroutines-16 2856 417124 ns/op 117495 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Concurrent_Read_Extreme_10485760Bytes_100Goroutines-16 2845 429635 ns/op 117946 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Serial_Read_Extreme_52428800Bytes-16 42 28135572 ns/op 1248426 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Concurrent_Read_Extreme_52428800Bytes_10Goroutines-16 561 2184203 ns/op 2990615 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Concurrent_Read_Extreme_52428800Bytes_100Goroutines-16 510 2253316 ns/op 2261651 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Serial_Read_Extreme_104857600Bytes-16 20 56963298 ns/op 5243134 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Concurrent_Read_Extreme_104857600Bytes_10Goroutines-16 219 4637253 ns/op 11012484 B/op 0 allocs/op +BenchmarkPRNG_ReadExtremeSizes/Concurrent_Read_Extreme_104857600Bytes_100Goroutines-16 235 4988574 ns/op 10708902 B/op 0 allocs/op +PASS +ok github.com/sixafter/nanoid/x/crypto/prng 195.948s +``` +
+ +--- + +## Features + +* Cryptographic Security: Utilizes the [ChaCha20](https://pkg.go.dev/golang.org/x/crypto/chacha20) cipher for secure random number generation. +* Concurrent Support: Includes a thread-safe global `Reader` for concurrent access. +* Efficient Resource Management: Uses a `sync.Pool` to manage PRNG instances, reducing the overhead on `crypto/rand.Reader`. +* Extensible API: Allows users to create and manage custom PRNG instances via `NewReader`. + +--- + +## Installation + +To install the package, run the following command: + +```bash +go get -u github.com/sixafter/nanoid/x/crypto/prng +``` + +## Usage + +Global Reader: + +```go +package main + +import ( + "fmt" + + "github.com/sixafter/nanoid/x/crypto/prng" +) + +func main() { + buffer := make([]byte, 64) + n, err := prng.Reader.Read(buffer) + if err != nil { + // Handle error + } + fmt.Printf("Read %d bytes of random data: %x\n", n, buffer) +} +``` + +--- + +## Architecture + +* Global Reader: A pre-configured io.Reader (`prng.Reader`) manages a pool of PRNG instances for concurrent use. +* PRNG Instances: Each instance uses ChaCha20, initialized with a unique key and nonce sourced from `crypto/rand.Reader`. +* Error Handling: The `errorPRNG` ensures safe failure when initialization errors occur. +* Resource Efficiency: A `sync.Pool` optimizes resource reuse and reduces contention on `crypto/rand.Reader`. + +--- + +## License + +This project is licensed under the [Apache 2.0 License](https://choosealicense.com/licenses/apache-2.0/). See [LICENSE](../../../LICENSE) file. diff --git a/x/crypto/prng/prng_benchmark_test.go b/x/crypto/prng/prng_benchmark_test.go index 31429d9..87cd518 100644 --- a/x/crypto/prng/prng_benchmark_test.go +++ b/x/crypto/prng/prng_benchmark_test.go @@ -13,7 +13,7 @@ import ( // BenchmarkPRNG_ReadSerial benchmarks the Read method of prng.Reader with various buffer sizes in serial. func BenchmarkPRNG_ReadSerial(b *testing.B) { // Define the buffer sizes to benchmark. - bufferSizes := []int{8, 16, 32, 64, 100, 256, 512, 1000, 4096, 16384} + bufferSizes := []int{8, 16, 21, 32, 64, 100, 256, 512, 1000, 4096, 16384} for _, size := range bufferSizes { size := size // Capture range variable @@ -38,7 +38,7 @@ func BenchmarkPRNG_ReadSerial(b *testing.B) { // BenchmarkPRNG_ReadConcurrent benchmarks the Read method of prng.Reader under concurrent access with varying buffer sizes and goroutine counts. func BenchmarkPRNG_ReadConcurrent(b *testing.B) { // Define the buffer sizes and goroutine counts to benchmark concurrently. - bufferSizes := []int{16, 32, 64, 100, 256, 512, 1000, 4096, 16384} + bufferSizes := []int{16, 21, 32, 64, 100, 256, 512, 1000, 4096, 16384} goroutineCounts := []int{10, 100, 1000} // Varying goroutine counts for _, size := range bufferSizes { @@ -119,7 +119,7 @@ func BenchmarkPRNG_ReadConcurrentLargeSizes(b *testing.B) { // BenchmarkPRNG_ReadVariableSizes benchmarks the Read method with variable buffer sizes in serial. func BenchmarkPRNG_ReadVariableSizes(b *testing.B) { // Define a range of buffer sizes to benchmark in serial. - variableBufferSizes := []int{8, 16, 24, 32, 48, 64, 128, 256, 512, 1024, 2048, 4096} + variableBufferSizes := []int{8, 16, 21, 24, 32, 48, 64, 128, 256, 512, 1024, 2048, 4096} for _, size := range variableBufferSizes { size := size // Capture range variable @@ -144,7 +144,7 @@ func BenchmarkPRNG_ReadVariableSizes(b *testing.B) { // BenchmarkPRNG_ReadConcurrentVariableSizes benchmarks the Read method with variable buffer sizes under concurrent access. func BenchmarkPRNG_ReadConcurrentVariableSizes(b *testing.B) { // Define a range of buffer sizes and goroutine counts to benchmark concurrently. - variableBufferSizes := []int{8, 16, 24, 32, 48, 64, 128, 256, 512, 1024, 2048, 4096} + variableBufferSizes := []int{8, 16, 21, 24, 32, 48, 64, 128, 256, 512, 1024, 2048, 4096} goroutineCounts := []int{10, 100, 1000} for _, size := range variableBufferSizes {