Module: BSV::Primitives::Hex

Defined in:
lib/bsv/primitives/hex.rb

Overview

Strict hex encoding/decoding utilities.

Ruby’s Array#pack(‘H*’) silently drops non-hex characters and truncates odd-length strings. This module rejects both, raising ArgumentError on invalid input so consumer-facing parse paths fail loudly rather than producing garbage.

Internal paths that serialise/deserialise trusted hex (e.g. round-tripping our own unpack1(‘H*’) output) can continue using pack(‘H*’) directly — the validation overhead isn’t warranted when the hex is known-good.

Examples:

BSV::Primitives::Hex.decode('deadbeef')          #=> "\xDE\xAD\xBE\xEF"
BSV::Primitives::Hex.decode('nope')              #=> ArgumentError
BSV::Primitives::Hex.decode('abc')               #=> ArgumentError (odd length)
BSV::Primitives::Hex.encode("\xDE\xAD")          #=> "dead"

Class Method Summary collapse

Class Method Details

.decode(str, name: 'hex value') ⇒ String

Decode a hex string to binary bytes.

Parameters:

  • str (String)

    hex string (must be even-length, hex-only)

  • name (String) (defaults to: 'hex value')

    label for the error message

Returns:

  • (String)

    binary string (ASCII-8BIT encoding)

Raises:

  • (ArgumentError)

    if str is not valid hex



63
64
65
66
# File 'lib/bsv/primitives/hex.rb', line 63

def self.decode(str, name: 'hex value')
  validate!(str, name: name)
  [str].pack('H*')
end

.encode(bytes) ⇒ String

Encode binary bytes as lowercase hex.

Parameters:

  • bytes (String)

    binary data

Returns:

  • (String)

    lowercase hex string (UTF-8 encoding)



72
73
74
# File 'lib/bsv/primitives/hex.rb', line 72

def self.encode(bytes)
  bytes.unpack1('H*')
end

.valid?(str) ⇒ Boolean

Test whether str is valid hex (even-length, hex-only).

Parameters:

  • str (String)

Returns:

  • (Boolean)


32
33
34
35
36
# File 'lib/bsv/primitives/hex.rb', line 32

def self.valid?(str)
  str.is_a?(String) && str.match?(HEX_RE)
rescue Encoding::CompatibilityError
  false
end

.validate!(str, name: 'hex value') ⇒ String

Validate str as hex, raising on failure.

Parameters:

  • str (String)
  • name (String) (defaults to: 'hex value')

    label for the error message (e.g. ‘txid’)

Returns:

  • (String)

    the input string (pass-through for chaining)

Raises:

  • (ArgumentError)

    if str is not valid hex



44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/bsv/primitives/hex.rb', line 44

def self.validate!(str, name: 'hex value')
  return str if valid?(str)

  reason = if !str.is_a?(String)
             "expected String, got #{str.class}"
           elsif str.length.odd?
             'odd length'
           else
             'contains non-hex characters'
           end
  raise ArgumentError, "invalid #{name}: #{reason} (#{str.inspect})"
end

.validate_dtxid_hex!(value, name: 'dtxid_hex') ⇒ String

Validate that value is a 64-character display-order hex transaction ID.

Parameters:

  • value (String)

    expected 64-char hex string

  • name (String) (defaults to: 'dtxid_hex')

    label for the error message (e.g. ‘dtxid_hex’)

Returns:

  • (String)

    the input value (pass-through for chaining)

Raises:

  • (ArgumentError)

    if value is not a 64-char hex string



126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/bsv/primitives/hex.rb', line 126

def self.validate_dtxid_hex!(value, name: 'dtxid_hex')
  unless value.is_a?(String) && value.length == 64 && value.match?(HEX_RE)
    hint = if value.is_a?(String) && value.bytesize == 32 && !value.match?(HEX_RE)
             ' (looks like binary bytes — use dtxid_hex or unpack to convert)'
           else
             ''
           end
    size = value.is_a?(String) ? "#{value.length}-char string" : value.class.to_s
    raise ArgumentError,
          "expected 64-char display-order hex for #{name}, got #{size}#{hint}"
  end
  value
end

.validate_hash32!(value, name: 'hash') ⇒ String

Validate that value is a 32-byte binary hash.

General-purpose validator for any 32-byte hash (merkle nodes, roots, etc.) — not specific to transaction IDs. For txid-specific validation use validate_wtxid! or validate_dtxid_hex! instead.

Parameters:

  • value (String)

    expected 32-byte binary string

  • name (String) (defaults to: 'hash')

    label for the error message

Returns:

  • (String)

    the input value (pass-through for chaining)

Raises:

  • (ArgumentError)

    if value is not a 32-byte binary string



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/bsv/primitives/hex.rb', line 106

def self.validate_hash32!(value, name: 'hash')
  unless value.is_a?(String) && value.bytesize == 32
    hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
             ' (looks like hex — decode it first)'
           else
             ''
           end
    size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
    raise ArgumentError,
          "expected 32-byte hash for #{name}, got #{size}#{hint}"
  end
  value
end

.validate_wtxid!(value, name: 'wtxid') ⇒ String

Validate that value is a 32-byte wire-order transaction ID.

Parameters:

  • value (String)

    expected 32-byte binary string

  • name (String) (defaults to: 'wtxid')

    label for the error message (e.g. ‘prev_wtxid’)

Returns:

  • (String)

    the input value (pass-through for chaining)

Raises:

  • (ArgumentError)

    if value is not a 32-byte binary string



82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/bsv/primitives/hex.rb', line 82

def self.validate_wtxid!(value, name: 'wtxid')
  unless value.is_a?(String) && value.bytesize == 32
    hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
             ' (looks like a hex txid — use wtxid_from_hex to convert)'
           else
             ''
           end
    size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
    raise ArgumentError,
          "expected 32-byte wire-order wtxid for #{name}, got #{size}#{hint}"
  end
  value
end