Module: MonoVM::Whois::Punycode

Defined in:
lib/monovm/whois/punycode.rb

Overview

Punycode (RFC 3492) encoder and decoder.

Ruby has no Punycode or IDNA support in its standard library, and pulling in a gem for ~120 lines of well-specified arithmetic would cost this library its zero-dependency guarantee. So it lives here, transcribed from the reference pseudocode in RFC 3492 section 6 and checked against the test vectors in section 7.1.

This module deals in single labels. Whole-name conversion, the xn-- prefix and validation belong to DomainName.

Defined Under Namespace

Classes: Error

Constant Summary collapse

BASE =
36
TMIN =
1
TMAX =
26
SKEW =
38
DAMP =
700
INITIAL_BIAS =
72
INITIAL_N =
128
DELIMITER =
"-"
MAX_CODE_POINT =

Highest code point that survives a round trip through the bias arithmetic without overflowing what a DNS label could ever hold.

0x10FFFF

Class Method Summary collapse

Class Method Details

.decode(input) ⇒ String

Decode a Punycode label. The xn-- prefix must already be stripped.

Punycode.decode("mnchen-3ya") # => "münchen"

Parameters:

  • input (String)

Returns:

  • (String)

Raises:

  • (Error)

    if the input is not valid Punycode



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/monovm/whois/punycode.rb', line 84

def decode(input)
  delimiter_at = input.rindex(DELIMITER)

  if delimiter_at
    basic = input[0...delimiter_at]
    raise Error, "non-basic code point before the delimiter" unless basic.ascii_only?

    output = basic.codepoints
    cursor = delimiter_at + 1
  else
    output = []
    cursor = 0
  end

  n = INITIAL_N
  i = 0
  bias = INITIAL_BIAS
  digits = input.codepoints

  while cursor < digits.length
    old_i = i
    weight = 1
    k = BASE

    loop do
      raise Error, "truncated punycode sequence" if cursor >= digits.length

      digit = digit_value(digits[cursor])
      cursor += 1

      i += digit * weight
      raise Error, "punycode overflow" if i > MAX_CODE_POINT * (output.length + 1)

      t = threshold(k, bias)
      break if digit < t

      weight *= (BASE - t)
      k += BASE
    end

    bias = adapt(i - old_i, output.length + 1, old_i.zero?)
    n += i / (output.length + 1)
    i %= (output.length + 1)

    raise Error, "punycode decoded to a basic code point" if basic?(n)
    raise Error, "punycode decoded outside Unicode" if n > MAX_CODE_POINT

    output.insert(i, n)
    i += 1
  end

  output.pack("U*")
end

.encode(input) ⇒ String

Encode a Unicode label as Punycode, without the xn-- prefix.

Punycode.encode("münchen") # => "mnchen-3ya"

Parameters:

  • input (String)

    one label, already normalised and downcased

Returns:

  • (String)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/monovm/whois/punycode.rb', line 40

def encode(input)
  code_points = input.codepoints
  basic = code_points.select { |cp| basic?(cp) }

  output = basic.pack("U*")
  output += DELIMITER unless basic.empty?

  n = INITIAL_N
  delta = 0
  bias = INITIAL_BIAS
  handled = basic.length
  basic_length = basic.length

  while handled < code_points.length
    # The next code point to deal with is the smallest one we have not
    # reached yet; that ordering is what makes the encoding reversible.
    m = code_points.select { |cp| cp >= n }.min
    delta += (m - n) * (handled + 1)
    n = m

    code_points.each do |cp|
      delta += 1 if cp < n
      next unless cp == n

      output += encode_delta(delta, bias)
      bias = adapt(delta, handled + 1, handled == basic_length)
      delta = 0
      handled += 1
    end

    delta += 1
    n += 1
  end

  output
end