git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/guid/guid.go (about) 1 package guid 2 3 import ( 4 "crypto/rand" 5 "database/sql/driver" 6 "errors" 7 "fmt" 8 "io" 9 "sync" 10 "time" 11 12 "git.sr.ht/~pingoo/stdx/base32" 13 "git.sr.ht/~pingoo/stdx/crypto" 14 ) 15 16 const ( 17 Size = 16 18 19 randPoolSize = 128 * 16 20 ) 21 22 // A GUID is a 128 bit (16 byte) Globally Unique IDentifier 23 type GUID [Size]byte 24 25 var ( 26 ErrGuidIsNotValid = errors.New("GUID is not valid") 27 ErrUuidIsNotValid = errors.New("Not a valid UUID") 28 ) 29 30 var ( 31 Empty GUID // empty GUID, all zeros 32 33 poolEnabled = true 34 randomSource = rand.Reader // random function 35 poolMutex sync.Mutex 36 poolPosition = randPoolSize // protected with poolMutex 37 pool [randPoolSize]byte // protected with poolMutex 38 ) 39 40 func NewRandom() GUID { 41 guid, err := NewRandomWithErr() 42 if err != nil { 43 panic(err) 44 } 45 return guid 46 } 47 48 func NewRandomWithErr() (GUID, error) { 49 if poolEnabled { 50 return newRandomFromPool() 51 } else { 52 return newRandomFromReader(randomSource) 53 } 54 } 55 56 func newRandomFromPool() (GUID, error) { 57 var guid GUID 58 poolMutex.Lock() 59 if poolPosition == randPoolSize { 60 _, err := io.ReadFull(randomSource, pool[:]) 61 if err != nil { 62 poolMutex.Unlock() 63 return Empty, err 64 } 65 poolPosition = 0 66 } 67 copy(guid[:], pool[poolPosition:(poolPosition+16)]) 68 poolPosition += 16 69 poolMutex.Unlock() 70 71 guid[6] = (guid[6] & 0x0f) | (0x04 << 4) // Version 4 72 guid[8] = (guid[8] & 0x3f) | 0x80 // Variant is 10 73 74 return guid, nil 75 } 76 77 func newRandomFromReader(reader io.Reader) (GUID, error) { 78 var guid GUID 79 _, err := io.ReadFull(reader, guid[:]) 80 if err != nil { 81 return Empty, err 82 } 83 84 guid[6] = (guid[6] & 0x0f) | 0x40 // Version 4 85 guid[8] = (guid[8] & 0x3f) | 0x80 // Variant is 10 86 return guid, nil 87 } 88 89 func NewTimeBased() GUID { 90 guid, err := NewTimeBasedWithErr() 91 if err != nil { 92 panic(err) 93 } 94 return guid 95 } 96 97 // TODO: Improve by reading only 10 bytes of random data 98 func NewTimeBasedWithErr() (guid GUID, err error) { 99 guid, err = NewRandomWithErr() 100 if err != nil { 101 return 102 } 103 104 now := time.Now().UTC() 105 // 48 bit timestamp 106 timestamp := uint64(now.UnixNano() / int64(time.Millisecond)) 107 guid[0] = byte(timestamp >> 40) 108 guid[1] = byte(timestamp >> 32) 109 guid[2] = byte(timestamp >> 24) 110 guid[3] = byte(timestamp >> 16) 111 guid[4] = byte(timestamp >> 8) 112 guid[5] = byte(timestamp) 113 114 // var timestamp uint64 115 // timestamp += uint64(now.Unix()) * 1e3 116 // timestamp += uint64(now.Nanosecond()) / 1e6 117 // binary.BigEndian.PutUint64(guid[:8], timestamp<<16) 118 119 // var x, y byte 120 // timestamp := uint64(t.UnixNano() / int64(time.Millisecond)) 121 // // Backups [6] and [7] bytes to override them with their original values later. 122 // x, y, ulid[6], ulid[7] = ulid[6], ulid[7], x, y 123 // binary.LittleEndian.PutUint64(ulid[:], timestamp) 124 // // Truncates at the 6th byte as designed in the original spec (48 bytes). 125 // ulid[6], ulid[7] = x, y 126 127 // guid[6] = (guid[6] & 0x0f) | 0x07 // Version 7 128 guid[6] = (guid[6] & 0x0f) | (0x07 << 4) 129 guid[8] = (guid[8] & 0x3f) | 0x80 // Variant is 10 130 131 return 132 } 133 134 // TODO: parse without allocs 135 func Parse(input string) (guid GUID, err error) { 136 bytes, err := base32.DecodeString(input) 137 if err != nil { 138 err = ErrGuidIsNotValid 139 return 140 } 141 142 if len(bytes) != Size { 143 err = ErrGuidIsNotValid 144 return 145 } 146 147 return GUID(bytes), nil 148 } 149 150 // FromBytes creates a new GUID from a byte slice. Returns an error if the slice 151 // does not have a length of 16. The bytes are copied from the slice. 152 func FromBytes(b []byte) (guid GUID, err error) { 153 err = guid.UnmarshalBinary(b) 154 return guid, err 155 } 156 157 // String returns the string form of guid 158 // TODO: encode without alloc 159 func (guid GUID) String() string { 160 return base32.EncodeToString(guid[:]) 161 } 162 163 func (guid GUID) Equal(other GUID) bool { 164 return crypto.ConstantTimeCompare(guid[:], other[:]) 165 } 166 167 func (guid GUID) Bytes() []byte { 168 return guid[:] 169 } 170 171 // MarshalText implements encoding.TextMarshaler. 172 func (guid GUID) MarshalText() ([]byte, error) { 173 ret := guid.String() 174 return []byte(ret), nil 175 } 176 177 // UnmarshalText implements encoding.TextUnmarshaler. 178 func (guid *GUID) UnmarshalText(data []byte) error { 179 id, err := Parse(string(data)) 180 if err != nil { 181 return err 182 } 183 *guid = id 184 return nil 185 } 186 187 // MarshalBinary implements encoding.BinaryMarshaler. 188 func (guid GUID) MarshalBinary() ([]byte, error) { 189 return guid[:], nil 190 } 191 192 // UnmarshalBinary implements encoding.BinaryUnmarshaler. 193 func (guid *GUID) UnmarshalBinary(data []byte) error { 194 if len(data) != 16 { 195 return fmt.Errorf("invalid GUID (got %d bytes)", len(data)) 196 } 197 copy(guid[:], data) 198 return nil 199 } 200 201 // Scan implements sql.Scanner so GUIDs can be read from databases transparently. 202 // Currently, database types that map to string and []byte are supported. Please 203 // consult database-specific driver documentation for matching types. 204 func (guid *GUID) Scan(src interface{}) error { 205 switch src := src.(type) { 206 case nil: 207 return nil 208 209 case string: 210 // if an empty GUID comes from a table, we return a null GUID 211 if src == "" { 212 return nil 213 } 214 215 // see Parse for required string format 216 u, err := ParseUuidString(src) 217 if err != nil { 218 return fmt.Errorf("Scan: %v", err) 219 } 220 221 *guid = u 222 223 case []byte: 224 // if an empty GUID comes from a table, we return a null GUID 225 if len(src) == 0 { 226 return nil 227 } 228 229 // assumes a simple slice of bytes if 16 bytes 230 // otherwise attempts to parse 231 if len(src) != 16 { 232 return guid.Scan(string(src)) 233 } 234 copy((*guid)[:], src) 235 236 default: 237 return fmt.Errorf("Scan: unable to scan type %T into GUID", src) 238 } 239 240 return nil 241 } 242 243 // Value implements sql.Valuer so that GUIDs can be written to databases 244 // transparently. Currently, GUIDs map to strings. Please consult 245 // database-specific driver documentation for matching types. 246 func (guid GUID) Value() (driver.Value, error) { 247 return guid.ToUuidString(), nil 248 } 249 250 // SetRand sets the random number generator to r, which implements io.Reader. 251 // If r.Read returns an error when the package requests random data then 252 // a panic will be issued. 253 // 254 // Calling SetRand with nil sets the random number generator to the default 255 // generator. 256 func SetRand(r io.Reader) { 257 if r == nil { 258 randomSource = rand.Reader 259 return 260 } 261 randomSource = r 262 } 263 264 func SetPoolEnabled(enabled bool) { 265 poolEnabled = enabled 266 }