Module: Philiprehberger::Hex
- Defined in:
- lib/philiprehberger/hex.rb,
lib/philiprehberger/hex/version.rb
Defined Under Namespace
Classes: Error
Constant Summary collapse
- HEX_PATTERN =
/\A[0-9a-fA-F]*\z/- VERSION =
'0.4.0'
Class Method Summary collapse
-
.and(hex1, hex2) ⇒ String
Bitwise AND of two equal-length hex strings.
-
.bytes_from(hex) ⇒ Array<Integer>
Convert a hex string to an array of integer byte values.
-
.chunk(hex, size:) ⇒ Array<String>
Split a hex string into an array of byte-aligned chunks The last chunk may be shorter than size bytes if the input length is not a multiple.
-
.compare(hex1, hex2) ⇒ Array<Hash>
Compare two hex strings and return byte-level differences.
-
.decode(hex) ⇒ String
Decode a hexadecimal string to binary Automatically strips 0x/0X prefix if present.
-
.dump(str) ⇒ String
Produce an xxd-style hex dump.
-
.encode(str, prefix: false, uppercase: false) ⇒ String
Encode a string to hexadecimal.
-
.extract_range(hex, offset:, length:) ⇒ String
Extract a range of bytes from a hex string.
-
.format(str, group: 2) ⇒ String
Format a string as grouped hex.
-
.from_bytes(bytes) ⇒ String
Convert an array of byte integers (0..255) to a hex string.
-
.from_int(int, bytes: nil) ⇒ String
Convert an integer to a hex string.
-
.normalize(hex, uppercase: false) ⇒ String
Normalize a hex string by stripping prefix, whitespace, and separators Validates that the result is even-length and purely hexadecimal.
-
.not(hex) ⇒ String
Bitwise NOT (one’s complement) of a hex string.
-
.or(hex1, hex2) ⇒ String
Bitwise OR of two equal-length hex strings.
-
.pad(hex, length:, side: :left) ⇒ String
Pad a hex string to a target byte length with zeros.
-
.random(n) ⇒ String
Generate a random hex string of n bytes.
-
.secure_equal?(hex1, hex2) ⇒ Boolean
Constant-time hex comparison, safe for MAC/HMAC/signature checks Comparison is case-insensitive; lengths must match (length is not secret).
-
.swap_endian(hex) ⇒ String
Reverse byte order of a hex string.
-
.to_int(hex) ⇒ Integer
Convert a hex string to an integer.
-
.valid?(str) ⇒ Boolean
Check if a string is valid hexadecimal.
-
.xor(hex1, hex2) ⇒ String
XOR two hex strings and return the hex result.
Class Method Details
.and(hex1, hex2) ⇒ String
Bitwise AND of two equal-length hex strings
301 302 303 |
# File 'lib/philiprehberger/hex.rb', line 301 def self.and(hex1, hex2) bitwise_binop(hex1, hex2) { |a, b| a & b } end |
.bytes_from(hex) ⇒ Array<Integer>
Convert a hex string to an array of integer byte values
91 92 93 94 95 96 97 |
# File 'lib/philiprehberger/hex.rb', line 91 def self.bytes_from(hex) validate_string!(hex) raise Error, 'invalid hex string: odd length' if hex.length.odd? raise Error, 'invalid hex string: non-hex characters' unless valid?(hex) [hex].pack('H*').bytes end |
.chunk(hex, size:) ⇒ Array<String>
Split a hex string into an array of byte-aligned chunks The last chunk may be shorter than size bytes if the input length is not a multiple
285 286 287 288 289 290 291 292 293 294 |
# File 'lib/philiprehberger/hex.rb', line 285 def self.chunk(hex, size:) validate_string!(hex) raise Error, 'size must be a positive integer' unless size.is_a?(Integer) && size.positive? stripped = strip_prefix(hex) raise Error, 'invalid hex string: odd length' if stripped.length.odd? raise Error, 'invalid hex string: non-hex characters' unless stripped.empty? || valid?(stripped) stripped.scan(/.{1,#{size * 2}}/) end |
.compare(hex1, hex2) ⇒ Array<Hash>
Compare two hex strings and return byte-level differences
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/philiprehberger/hex.rb', line 104 def self.compare(hex1, hex2) bytes1 = bytes_from(hex1) bytes2 = bytes_from(hex2) max_len = [bytes1.length, bytes2.length].max diffs = [] max_len.times do |i| b1 = bytes1[i] b2 = bytes2[i] next if b1 == b2 diffs << { offset: i, expected: b1 ? Kernel.format('%02x', b1) : nil, actual: b2 ? Kernel.format('%02x', b2) : nil } end diffs end |
.decode(hex) ⇒ String
Decode a hexadecimal string to binary Automatically strips 0x/0X prefix if present
37 38 39 40 41 42 43 44 |
# File 'lib/philiprehberger/hex.rb', line 37 def self.decode(hex) validate_string!(hex) hex = strip_prefix(hex) raise Error, 'invalid hex string: odd length' if hex.length.odd? raise Error, 'invalid hex string: non-hex characters' unless valid?(hex) [hex].pack('H*') end |
.dump(str) ⇒ String
Produce an xxd-style hex dump
50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/philiprehberger/hex.rb', line 50 def self.dump(str) validate_string!(str) lines = [] bytes = str.bytes bytes.each_slice(16).with_index do |chunk, index| offset = Kernel.format('%08x', index * 16) hex_part = chunk.each_slice(2).map { |pair| pair.map { |b| Kernel.format('%02x', b) }.join }.join(' ') ascii_part = chunk.map { |b| b.between?(32, 126) ? b.chr : '.' }.join lines << Kernel.format('%-10s %-40s %s', "#{offset}:", hex_part, ascii_part) end lines.join("\n") end |
.encode(str, prefix: false, uppercase: false) ⇒ String
Encode a string to hexadecimal
25 26 27 28 29 30 |
# File 'lib/philiprehberger/hex.rb', line 25 def self.encode(str, prefix: false, uppercase: false) validate_string!(str) hex = str.unpack1('H*') hex = hex.upcase if uppercase prefix ? "0x#{hex}" : hex end |
.extract_range(hex, offset:, length:) ⇒ String
Extract a range of bytes from a hex string
155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/philiprehberger/hex.rb', line 155 def self.extract_range(hex, offset:, length:) validate_string!(hex) hex = strip_prefix(hex) raise Error, 'invalid hex string: odd length' if hex.length.odd? raise Error, 'invalid hex string: non-hex characters' unless valid?(hex) total_bytes = hex.length / 2 raise Error, 'offset out of range' if offset.negative? || offset >= total_bytes raise Error, 'length out of range' if length.negative? || (offset + length) > total_bytes hex[offset * 2, length * 2] end |
.format(str, group: 2) ⇒ String
Format a string as grouped hex
70 71 72 73 74 |
# File 'lib/philiprehberger/hex.rb', line 70 def self.format(str, group: 2) validate_string!(str) hex = encode(str) hex.scan(/.{1,#{group * 2}}/).join(' ') end |
.from_bytes(bytes) ⇒ String
Convert an array of byte integers (0..255) to a hex string
238 239 240 241 242 243 244 245 |
# File 'lib/philiprehberger/hex.rb', line 238 def self.from_bytes(bytes) raise Error, 'expected an Array' unless bytes.is_a?(Array) unless bytes.all? { |b| b.is_a?(Integer) && b.between?(0, 255) } raise Error, 'all elements must be integers in 0..255' end bytes.pack('C*').unpack1('H*') end |
.from_int(int, bytes: nil) ⇒ String
Convert an integer to a hex string
219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/philiprehberger/hex.rb', line 219 def self.from_int(int, bytes: nil) raise Error, 'expected an Integer' unless int.is_a?(Integer) raise Error, 'integer must be non-negative' if int.negative? hex = int.to_s(16) hex = "0#{hex}" if hex.length.odd? if bytes target = bytes * 2 hex = hex.rjust(target, '0') if hex.length < target end hex end |
.normalize(hex, uppercase: false) ⇒ String
Normalize a hex string by stripping prefix, whitespace, and separators Validates that the result is even-length and purely hexadecimal
253 254 255 256 257 258 259 260 261 |
# File 'lib/philiprehberger/hex.rb', line 253 def self.normalize(hex, uppercase: false) validate_string!(hex) stripped = strip_prefix(hex).gsub(/[\s:\-_]/, '') raise Error, 'invalid hex string: odd length' if stripped.length.odd? raise Error, 'invalid hex string: empty' if stripped.empty? raise Error, 'invalid hex string: non-hex characters' unless valid?(stripped) uppercase ? stripped.upcase : stripped.downcase end |
.not(hex) ⇒ String
Bitwise NOT (one’s complement) of a hex string
318 319 320 |
# File 'lib/philiprehberger/hex.rb', line 318 def self.not(hex) bytes_from(hex).map { |b| Kernel.format('%02x', ~b & 0xFF) }.join end |
.or(hex1, hex2) ⇒ String
Bitwise OR of two equal-length hex strings
310 311 312 |
# File 'lib/philiprehberger/hex.rb', line 310 def self.or(hex1, hex2) bitwise_binop(hex1, hex2) { |a, b| a | b } end |
.pad(hex, length:, side: :left) ⇒ String
Pad a hex string to a target byte length with zeros
187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/philiprehberger/hex.rb', line 187 def self.pad(hex, length:, side: :left) validate_string!(hex) hex = strip_prefix(hex) raise Error, 'invalid hex string: odd length' if hex.length.odd? raise Error, 'invalid hex string: non-hex characters' unless valid?(hex) raise Error, 'side must be :left or :right' unless %i[left right].include?(side) target_chars = length * 2 return hex if hex.length >= target_chars padding = '0' * (target_chars - hex.length) side == :left ? "#{padding}#{hex}" : "#{hex}#{padding}" end |
.random(n) ⇒ String
Generate a random hex string of n bytes
143 144 145 146 147 |
# File 'lib/philiprehberger/hex.rb', line 143 def self.random(n) raise Error, 'byte count must be positive' unless n.is_a?(Integer) && n.positive? SecureRandom.hex(n) end |
.secure_equal?(hex1, hex2) ⇒ Boolean
Constant-time hex comparison, safe for MAC/HMAC/signature checks Comparison is case-insensitive; lengths must match (length is not secret)
269 270 271 272 273 274 275 276 277 |
# File 'lib/philiprehberger/hex.rb', line 269 def self.secure_equal?(hex1, hex2) validate_string!(hex1) validate_string!(hex2) a = strip_prefix(hex1).downcase b = strip_prefix(hex2).downcase return false unless a.length == b.length OpenSSL.fixed_length_secure_compare(a, b) end |
.swap_endian(hex) ⇒ String
Reverse byte order of a hex string
172 173 174 175 176 177 178 179 |
# File 'lib/philiprehberger/hex.rb', line 172 def self.swap_endian(hex) validate_string!(hex) hex = strip_prefix(hex) raise Error, 'invalid hex string: odd length' if hex.length.odd? raise Error, 'invalid hex string: non-hex characters' unless valid?(hex) hex.scan(/../).reverse.join end |
.to_int(hex) ⇒ Integer
Convert a hex string to an integer
205 206 207 208 209 210 211 212 |
# File 'lib/philiprehberger/hex.rb', line 205 def self.to_int(hex) validate_string!(hex) hex = strip_prefix(hex) raise Error, 'invalid hex string: empty' if hex.empty? raise Error, 'invalid hex string: non-hex characters' unless valid?(hex) hex.to_i(16) end |
.valid?(str) ⇒ Boolean
Check if a string is valid hexadecimal
80 81 82 83 84 85 |
# File 'lib/philiprehberger/hex.rb', line 80 def self.valid?(str) return false unless str.is_a?(String) return false if str.empty? HEX_PATTERN.match?(str) end |
.xor(hex1, hex2) ⇒ String
XOR two hex strings and return the hex result
131 132 133 134 135 136 137 |
# File 'lib/philiprehberger/hex.rb', line 131 def self.xor(hex1, hex2) bytes1 = bytes_from(hex1) bytes2 = bytes_from(hex2) raise Error, 'hex strings must be the same length' unless bytes1.length == bytes2.length bytes1.zip(bytes2).map { |a, b| Kernel.format('%02x', a ^ b) }.join end |