github.com/creachadair/ffs@v0.17.3/storage/hexkey/hexkey.go (about) 1 // Copyright 2024 Michael J. Fromberger. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package hexkey implements utilities for hexadecimal encoding of blob store keys. 16 package hexkey 17 18 import ( 19 "cmp" 20 "encoding/hex" 21 "errors" 22 "path" 23 "strings" 24 ) 25 26 // Config carries settings for the encoding and decoding of hex keys. The zero 27 // value is ready for use and encodes keys as plain hexadecimal strings. 28 type Config struct { 29 // Prefix, if set, is prepended to all keys, separated from the remainder of 30 // the key by "/". 31 Prefix string 32 33 // Shard, if positive, specifies a prefix length for each hex-encoded key, 34 // that will be separated from the key by an intervening "/". 35 // For example, if Shard is 2, a key "012345" becomes "01/012345". 36 // If Shard ≤ 0, keys are not partitioned. 37 Shard int 38 } 39 40 // ErrNotMyKey is a sentinel error reported by Decode when given a key that 41 // does not match the parameters of the config. 42 var ErrNotMyKey = errors.New("key does not match config") 43 44 // Encode encodes the specified key as hexadecimal according to c. 45 func (c Config) Encode(key string) string { 46 if c.Shard <= 0 { 47 return path.Join(c.Prefix, hex.EncodeToString([]byte(key))) 48 } 49 tail := hex.EncodeToString([]byte(key)) 50 51 // Pad out the shard label to the desired length. Use "-" as the pad so it 52 // orders prior to any hexadecimal digit. 53 shard := tail[:min(c.Shard, len(tail))] 54 for len(shard) < c.Shard { 55 shard += "-" 56 } 57 58 // Special case: an empty key is encoded as "-", which sorts before all 59 // hexadecimal values, but is non-empty. 60 return path.Join(c.Prefix, shard, cmp.Or(tail, "-")) 61 } 62 63 // Decode decodes the specified hex-encoded key according to c. 64 // If ekey does not match the expected format, it reports ErrNotMyKey. 65 // Otherwise, any error results from decoding the hexadecimal digits. 66 func (c Config) Decode(ekey string) (string, error) { 67 if c.Prefix != "" { 68 tail, ok := strings.CutPrefix(ekey, c.Prefix+"/") 69 if !ok { 70 return "", ErrNotMyKey 71 } 72 ekey = tail 73 } 74 75 // If no shard prefix is expected, the key is complete. 76 if c.Shard <= 0 { 77 key, err := hex.DecodeString(ekey) 78 return string(key), err 79 } 80 81 // Otherwise, make sure we have a matching shard prefix and non-empty suffix. 82 pre, post, ok := strings.Cut(ekey, "/") 83 if !ok || len(pre) != c.Shard || post == "" { 84 return "", ErrNotMyKey 85 } 86 87 // Special case: "-" is the encoding of an empty key. 88 if post == "-" { 89 return "", nil 90 } 91 key, err := hex.DecodeString(post) 92 return string(key), err 93 } 94 95 // Start returns the hex encoding of a "start" key, a point in the lexiographic 96 // sequence of keys. 97 func (c Config) Start(key string) string { 98 tail := hex.EncodeToString([]byte(key)) 99 if c.Shard <= 0 || len(tail) <= c.Shard { 100 return path.Join(c.Prefix, tail) 101 } 102 return path.Join(c.Prefix, tail[:c.Shard], tail) 103 } 104 105 // WithPrefix returns a copy of c with its prefix set to pfx. 106 func (c Config) WithPrefix(pfx string) Config { c.Prefix = pfx; return c }