Files
zeekay e0d883f38b A node id and a short id read text as text, the way an id already did
UnmarshalText delegated to UnmarshalJSON, so it demanded quotes around a value
that was never JSON. Every caller that keeps the TextUnmarshaler contract met
"first and last characters should be quotes": encoding/json handing over a map
KEY, encoding/xml, flag.Value, and a URL binder reading a query value — where
the failure is silent and the node id arrives as the zero node with a 200 on
the answer.

ID.UnmarshalText was corrected this way already, and its comment records the
same symptom on --chain-aliases-file. This is that correction on the other two,
with the reading in ONE place: UnmarshalText holds it and UnmarshalJSON unquotes
and calls it, so the two spellings cannot drift.
2026-08-29 22:46:03 -07:00

131 lines
3.4 KiB
Go

// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ids
import (
"bytes"
"encoding/hex"
"fmt"
"strings"
"github.com/luxfi/crypto/cb58"
"github.com/luxfi/crypto/hash"
)
const ShortIDLen = 20
// ShortEmpty is a useful all zero value
var (
ShortEmpty = ShortID{}
_ Sortable[ShortID] = ShortID{}
)
// ShortID wraps a 20 byte hash as an identifier
type ShortID [ShortIDLen]byte
// ToShortID attempt to convert a byte slice into an id
func ToShortID(bytes []byte) (ShortID, error) {
return hash.ToHash160(bytes)
}
// ShortFromString is the inverse of ShortID.String()
func ShortFromString(idStr string) (ShortID, error) {
bytes, err := cb58.Decode(idStr)
if err != nil {
return ShortID{}, err
}
return ToShortID(bytes)
}
// ShortFromPrefixedString returns a ShortID assuming the cb58 format is
// prefixed
func ShortFromPrefixedString(idStr, prefix string) (ShortID, error) {
if !strings.HasPrefix(idStr, prefix) {
return ShortID{}, fmt.Errorf("ID: %s is missing the prefix: %s", idStr, prefix)
}
return ShortFromString(strings.TrimPrefix(idStr, prefix))
}
func (id ShortID) MarshalJSON() ([]byte, error) {
str, err := cb58.Encode(id[:])
if err != nil {
return nil, err
}
return []byte(`"` + str + `"`), nil
}
// UnmarshalJSON reads the same cb58 word, quoted. The reading itself is in
// UnmarshalText and is not written twice.
func (id *ShortID) UnmarshalJSON(b []byte) error {
str := string(b)
if str == nullStr { // If "null", do nothing
return nil
}
if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' {
return errMissingQuotes
}
return id.UnmarshalText([]byte(str[1 : len(str)-1]))
}
// UnmarshalText reads the cb58 word ITSELF, unquoted — which is what a
// TextUnmarshaler is handed, by encoding/json for a map key, by encoding/xml,
// by flag.Value and by a URL binder. It delegated to UnmarshalJSON, so every
// one of those callers met "first and last characters should be quotes" for a
// value that was never JSON. [ID.UnmarshalText] was already corrected this way;
// this is the same correction on the other two.
func (id *ShortID) UnmarshalText(text []byte) error {
str := string(text)
if str == nullStr || str == "" {
return nil
}
bytes, err := cb58.Decode(str)
if err != nil {
return fmt.Errorf("couldn't decode ID to bytes: %w", err)
}
*id, err = ToShortID(bytes)
return err
}
// Bytes returns the 20 byte hash as a slice. It is assumed this slice is not
// modified.
func (id ShortID) Bytes() []byte {
return id[:]
}
// Hex returns a hex encoded string of this id.
func (id ShortID) Hex() string {
return hex.EncodeToString(id.Bytes())
}
func (id ShortID) String() string {
// We assume that the maximum size of a byte slice that
// can be stringified is at least the length of an ID
str, _ := cb58.Encode(id.Bytes())
return str
}
// PrefixedString returns the String representation with a prefix added
func (id ShortID) PrefixedString(prefix string) string {
return prefix + id.String()
}
func (id ShortID) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
func (id ShortID) Compare(other ShortID) int {
return bytes.Compare(id[:], other[:])
}
// ShortIDsToStrings converts an array of shortIDs to an array of their string
// representations
func ShortIDsToStrings(ids []ShortID) []string {
idStrs := make([]string, len(ids))
for i, id := range ids {
idStrs[i] = id.String()
}
return idStrs
}