Skip to content

Add IPAddr#to_hex - #126

Merged
taketo1113 merged 1 commit into
ruby:masterfrom
datibbaw:add-to-hex
Oct 17, 2026
Merged

taketo1113 merged 1 commit into
ruby:masterfrom
datibbaw:add-to-hex

Conversation

@datibbaw

@datibbaw datibbaw commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Adds IPAddr#to_hex, which returns the hexadecimal representation of the address value, right adjusted to the address size — 8 characters for IPv4, 32 for IPv6:

IPAddr.new("192.168.1.1").to_hex   #=> "c0a80101"
IPAddr.new("3ffe:505:2::1").to_hex #=> "3ffe0505000200000000000000000001"

Motivation: consumers that need the padded hex form of an address — most notably SPF macro expansions (RFC 7208 %{ir}), but also DNS reverse-name construction and protocol encodings — currently have to reimplement the family-dependent padding themselves:

sprintf('%08x', ip.to_i)   # only correct for IPv4
sprintf('%032x', ip.to_i)  # only correct for IPv6

Having this in the library would help reduce the external branching in other projects.

I decided to make this raise an AddressFamilyError despite it being impossible to create an instance of a family type outside of IPv4 or IPv6.

@datibbaw
datibbaw marked this pull request as ready for review October 2, 2026 02:14
@datibbaw
datibbaw requested a review from knu as a code owner October 2, 2026 02:14
@taketo1113

Copy link
Copy Markdown
Collaborator

Motivation: consumers that need the padded hex form of an address — most notably SPF macro expansions (RFC 7208 %{ir}), but also DNS reverse-name construction and protocol encodings — currently have to reimplement the family-dependent padding themselves:

@datibbaw I think IPAddr#to_hex would be useful.
However, for the motivation mentioned here, such as SPF macro expansions (RFC 7208 %{ir}), would what is needed here be a reversed representation like the following, rather than hexadecimal notation?

192.0.2.3
=> 3.2.0.192

2001:db8::cb01
=> 1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2

@datibbaw

datibbaw commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Motivation: consumers that need the padded hex form of an address — most notably SPF macro expansions (RFC 7208 %{ir}), but also DNS reverse-name construction and protocol encodings — currently have to reimplement the family-dependent padding themselves:

@datibbaw I think IPAddr#to_hex would be useful. However, for the motivation mentioned here, such as SPF macro expansions (RFC 7208 %{ir}), would what is needed here be a reversed representation like the following, rather than hexadecimal notation?

192.0.2.3
=> 3.2.0.192

2001:db8::cb01
=> 1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2

Thanks @taketo1113. The #to_hex is a building block and can be used to simplify #reverse, which is already %{ir}.%{v}.arpa. So perhaps exposing #_reverse could be a consideration? Or have a to_dotted which implements %{i}?

@taketo1113 taketo1113 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So perhaps exposing #_reverse could be a consideration? Or have a to_dotted which implements %{i}?

@datibbaw I agree that if we make IPAddr#_reverse a public method, it would be better to give it a different name.
However, I would not accept the name to_dotted at this point. There have been several proposals in the IETF in the past to represent IPv6 addresses using dotted notation, so I think we should choose the name carefully, considering that dotted notation might be used for a different purpose in the future.

I think adding IPAddr#to_hex and using it in hton and _reverse would be fine. So, how about adding only to_hex in this Pull Request, without implementing the RFC 7208 %{i} macro?

Comment thread lib/ipaddr.rb Outdated
Comment thread test/test_ipaddr.rb Outdated
@taketo1113

Copy link
Copy Markdown
Collaborator

@datibbaw Thank you for the update.
Could you also revise the commit message as follows?

Add IPAddr#to_hex
Returns the hexadecimal representation of the address value, right
adjusted to the address size (8 characters for IPv4, 32 for IPv6).
- This is used for e.g. SPF macro expansions.

Returns the hexadecimal representation of the address value, right
adjusted to the address size (8 characters for IPv4, 32 for IPv6).
@datibbaw

Copy link
Copy Markdown
Contributor Author

@datibbaw Thank you for the update. Could you also revise the commit message as follows?

Add IPAddr#to_hex
Returns the hexadecimal representation of the address value, right
adjusted to the address size (8 characters for IPv4, 32 for IPv6).
- This is used for e.g. SPF macro expansions.

Derp, thanks for spotting that. Removed!

@taketo1113
taketo1113 merged commit a49f6fa into ruby:master Oct 17, 2026
29 checks passed
@taketo1113

Copy link
Copy Markdown
Collaborator

@datibbaw Merged. Thank you!

@taketo1113

Copy link
Copy Markdown
Collaborator

As a follow-up, I benchmarked the changes and confirmed significant performance improvements for both IPAddr#hton and IPAddr#reverse. The execution time was reduced by approximately 37% for IPAddr#hton and 70% for IPAddr#reverse.

Benchmark results

$ ruby --yjit benchmark_to_hex.rb 
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [arm64-darwin25]

# IPAddr#hton
Rehearsal ---------------------------------------------------
IPAddr#hton_old   0.944692   0.007539   0.952231 (  0.954063)
IPAddr#hton_new   0.599881   0.005763   0.605644 (  0.605648)
------------------------------------------ total: 1.557875sec

                      user     system      total        real
IPAddr#hton_old   0.928315   0.009167   0.937482 (  0.937473)
IPAddr#hton_new   0.580288   0.005752   0.586040 (  0.586102)

# IPAddr#reverse
Rehearsal ------------------------------------------------------
IPAddr#reverse_old   6.996867   0.030497   7.027364 (  7.027258)
IPAddr#reverse_new   2.219429   0.013321   2.232750 (  2.232718)
--------------------------------------------- total: 9.260114sec

                         user     system      total        real
IPAddr#reverse_old   7.132512   0.034575   7.167087 (  7.167466)
IPAddr#reverse_new   2.173082   0.009145   2.182227 (  2.182194)
Benchmark code
require "./lib/ipaddr"
require "benchmark"

puts RUBY_DESCRIPTION
puts

ipv4 = IPAddr.new("192.0.2.1")
ipv6 = IPAddr.new("2001:db8::1")

n = 1_000_000

puts "# IPAddr#hton"

Benchmark.bmbm do |x|
  x.report("IPAddr#hton_old") do
    n.times { ipv4.hton_old }
    n.times { ipv6.hton_old }
  end
  x.report("IPAddr#hton_new") do
    n.times { ipv4.hton_new }
    n.times { ipv6.hton_new }
  end
end


puts
puts "# IPAddr#reverse"

Benchmark.bmbm do |x|
  x.report("IPAddr#reverse_old") do
    n.times { ipv4.reverse_old }
    n.times { ipv6.reverse_old }
  end
  x.report("IPAddr#reverse_new") do
    n.times { ipv4.reverse_new }
    n.times { ipv6.reverse_new }
  end
end
# lib/ipaddr.rb

  def hton_old
    case @family
    when Socket::AF_INET
      return [@addr].pack('N')
    when Socket::AF_INET6
      return (0..7).map { |i|
        (@addr >> (112 - 16 * i)) & 0xffff
      }.pack('n8')
    else
      raise AddressFamilyError, "unsupported address family"
    end
  end

  def hton_new
    case @family
    when Socket::AF_INET, Socket::AF_INET6
      value = to_hex
      return [value].pack("H#{value.length}")
    else
      raise AddressFamilyError, "unsupported address family"
    end
  end
...
  def reverse_old
    case @family
    when Socket::AF_INET
      return _reverse_old + ".in-addr.arpa"
    when Socket::AF_INET6
      return ip6_arpa_old
    else
      raise AddressFamilyError, "unsupported address family"
    end
  end

  def reverse_new
    case @family
    when Socket::AF_INET
      return _reverse_new + ".in-addr.arpa"
    when Socket::AF_INET6
      return ip6_arpa_new
    else
      raise AddressFamilyError, "unsupported address family"
    end
  end

  def ip6_arpa_old
    if !ipv6?
      raise InvalidAddressError, "not an IPv6 address: #{to_s}"
    end
    return _reverse_old + ".ip6.arpa"
  end

  def ip6_arpa_new
    if !ipv6?
      raise InvalidAddressError, "not an IPv6 address: #{to_s}"
    end
    return _reverse_new + ".ip6.arpa"
  end
...
  def _reverse_old
    case @family
    when Socket::AF_INET
      return (0..3).map { |i|
        (@addr >> (8 * i)) & 0xff
      }.join('.')
    when Socket::AF_INET6
      return ("%.32x" % @addr).reverse!.gsub!(/.(?!$)/, '\&.')
    else
      raise AddressFamilyError, "unsupported address family"
    end
  end

  def _reverse_new
    case @family
    when Socket::AF_INET
      return (0..3).map { |i|
        (@addr >> (8 * i)) & 0xff
      }.join('.')
    when Socket::AF_INET6
      return to_hex.reverse.chars.join('.')
    else
      raise AddressFamilyError, "unsupported address family"
    end
  end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants