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 }