RSS

Модуль кодирования кириллических доменных имен в PunyCode для Delphi

Что такое PunyCode?

   Punycode – это конвертация символов в кодировке Unicode в формат, поддерживаемый инфраструктурой DNS. Стандарт, в котором описан алгоритм преобразования, находится здесь RFC 3492.  Правда, русскоязычной версии пока не нашел.

   Мысль об использовании в названии домена (человеко-понятный адрес сайта) символов различных алфавитов, появилась еще в 1970-х годах, когда Интернет был еще только в стадии разработки. Но т.к. технология еще не была разработана, допустимые символы для регистрации доменных имен в системе доменных имен (DNS) были ограничены кодировкой ASCII и соответственно символами латинского алфавита (a-z), цифрами (0-9) и дефисом. 

   В 2003 году для решения вопроса с поддержкой национальных алфавитов для доменных имен были подготовлены специальные стандарты, которые обозначаются как IDNA – Internationalizing Domain Names in Applications (Интернационализованные доменные имена для приложений). Этими стандартами описывается технология применения в доменных именах символов, которые не входят в таблицу ASCII. Одним из стандартов – RFC 3492 описывается специальное преобразование символов – Punycode, позволяющее конвертировать набор символов в кодировке Unicode, которая включает все национальные алфавиты в набор символов и поддерживаемый существующей DNS.

   Чтобы IDN-домен после преобразования нельзя было спутать с обычным доменом, все IDN-домены начинаются со специального префикса «XN—».

   К примеру, IDN-домен, записанный с использованием кириллицы как «МОЙСАЙТ.COM», пройдя Punycode-конвертацию, будет иметь вид «XN—80ARBJKTJ.COM», а сочетание символов «МОЙСАЙТ» будет конвертировано в «80ARBJKTJ», зона «COM» остается не тронутой т.к. это латинские символы.

   В конце 2009-го в интернете появилась новая зона «.РФ», в которой все доменные имена регистрируются с использованием кириллицы. Но, такие имена это по факту являются псевдонимом, фактическое имя домена в DNS записано на латинице, путем кодирования кириллицы алгоритмом Punycode. Так же, кодировке подвергается и зона «.РФ», которая представлена как «.XN—P1AI».

   Для оснащения своих приложений в Delphi возможностью преобразования кириллических доменных имен в PunyCode, пришлось «покопаться» на просторах сети. В русскоязычном сегменте мне ничего так и не попалось путного, а вот на англоязычном ресурсе, наткнулся на следующую реализацию модуля для Delphi PunyCode.pas:

unit PunyCode;

interface

type
  {$if (SizeOf(Char) = 1)}
  // for compatibility with versions without Unicodestring (prior Delphi 2009)
  Unicodestring = WideString;
  {$ifend}

  TPunyCodeStatus = (
    pcSuccess,
    pcBadInput,   (* Input is invalid. *)
    pcBigOutput,  (* Output would exceed the space provided. *)
    pcOverflow    (* Input needs wider integers toprocess.  *)
  );

  TPunyCode = Word;
  TPunyCodeArray = array[0..(High(Integer) div SizeOf(TPunyCode)) — 1] of TPunyCode;
  PPunycode = ^TPunyCodeArray;

function PunycodeDecode(inputlen: Cardinal; const input: PByte;
  var outputlen: Cardinal; output: PPunycode = nil;
  caseflags: PByte = nil): TPunyCodeStatus;

function PunycodeEncode(inputlen: Cardinal; const input: PPunycode;
  var outputlen: Cardinal; const output: PByte = nil;
  const caseflags: PByte = nil): TPunyCodeStatus; overload;

function PunycodeDecodeDomain(const str: Ansistring): UnicodeString;
function PunycodeEncodeDomain(const str: Unicodestring): AnsiString;

implementation

uses SysUtils;

type
  PByteArray = ^TByteArray;
  TByteArray = array [0..MaxInt-1] of Byte;

(*** Bootstring parameters for Punycode ***)
const
  PUNY_BASE = 36;
  PUNY_TMIN = 1;
  PUNY_TMAX = 26;
  PUNY_SKEW = 38;
  PUNY_DAMP = 700;
  PUNY_INITIAL_BIAS = 72;
  PUNY_INITIAL_N = $80;
  PUNY_DELIMITER = $2D;

  // typedef unsigned int punycode_uint;
  // /* maxint is the maximum value of a punycode_uint variable: */
  // static const punycode_uint maxint = -1;
  // /* Because maxint is unsigned, -1 becomes the maximum value. */
  PUNY_maxint = High(Cardinal);

(* flagged(bcp) tests whether a basic code point is flagged *)
(* (uppercase).  The behavior is undefined if bcp is not a  *)
(* basic code point.                                        *)

function PUNY_flagged(bcp: Cardinal): Byte; //inline;
begin
  Result := Ord(bcp — 65 < 26);
end;

(* DecodeDigit(cp) returns the numeric value of a basic code *)
(* point (for use in representing integers) in the range 0 to *)
(* BASE-1, or BASE if cp is does not represent a value.       *)

function PUNY_DecodeDigit(cp: Cardinal): Cardinal; //inline;
begin
  if (cp — 48 < 10) then
    Result := cp — 22
  else if (cp — 65 < 26) then
    Result := cp — 65
  else if (cp — 97 < 26) then
    Result := cp — 97
  else
    Result := PUNY_BASE;
end;

(* EncodeDigit(d,flag) returns the basic code point whose value      *)
(* (when used for representing integers) is d, which needs to be in   *)
(* the range 0 to BASE-1.  The lowercase form is used unless flag is  *)
(* nonzero, in which case the uppercase form is used.  The behavior   *)
(* is undefined if flag is nonzero and digit d has no uppercase form. *)

function PUNY_EncodeDigit(d: Cardinal; flag: Boolean): Byte; //inline;
begin
  Result := d + 22 + 75 * Ord(d < 26) — (Ord(flag) shl 5);
  (*  0..25 map to ASCII a..z or A..Z *)
  (* 26..35 map to ASCII 0..9         *)
end;

(* EncodeBasic(bcp,flag) forces a basic code point to lowercase *)
(* if flag is zero, uppercase if flag is nonzero, and returns    *)
(* the resulting code point.  The code point is unchanged if it  *)
(* is caseless.  The behavior is undefined if bcp is not a basic *)
(* code point.                                                   *)

function PUNY_EncodeBasic(bcp: Cardinal; flag: Integer): Byte; //inline;
begin
  Dec(bcp, Ord(bcp — 97 < 26) shl 5);
  Result := bcp + (((not flag) and Ord(bcp — 65 < 26)) shl 5);
end;

(*** Bias adaptation function ***)

function PUNY_Adapt(delta, numpoints: Cardinal; firsttime: Boolean): Cardinal;// inline;
var
  k: TPunyCode;
begin
  if firsttime then
    delta := delta div PUNY_DAMP
  else
    delta := delta shr 1;

  (* delta shr 1 is a faster way of doing delta div 2 *)
  Inc(delta, delta div numpoints);

  k := 0;
  while (delta > ((PUNY_BASE — PUNY_TMIN) * PUNY_TMAX) div 2) do
  begin
    delta := delta div (PUNY_BASE — PUNY_TMIN);
    Inc(k, PUNY_BASE);
  end;

  Result := k + (PUNY_BASE — PUNY_TMIN + 1) * delta div (delta + PUNY_SKEW);
end;

(* PunycodeEncode() converts Unicode to Punycode.  The input     *)
(* is represented as an array of Unicode code points (not code    *)
(* units; surrogate pairs are not allowed), and the output        *)
(* will be represented as an array of ASCII code points.  The     *)
(* output string is *not* null-terminated; it will contain        *)
(* zeros if and only if the input contains zeros.  (Of course     *)
(* the caller can leave room for a terminator and add one if      *)
(* needed.)  The inputlen is the number of code points in         *)
(* the input.  The outputlen is an in/out argument: the           *)
(* caller passes in the maximum number of code points that it     *)
(* can receive, and on successful return it will contain the      *)
(* number of code points actually output.  The case_flags array   *)
(* holds input_length boolean values, where nonzero suggests that *)
(* the corresponding Unicode character be forced to uppercase     *)
(* after being decoded (if possible), and zero suggests that      *)
(* it be forced to lowercase (if possible).  ASCII code points    *)
(* are encoded literally, except that ASCII letters are forced    *)
(* to uppercase or lowercase according to the corresponding       *)
(* uppercase flags.  if case_flags is a null pointer then ASCII   *)
(* letters are left as they are, and other code points are        *)
(* treated as if their uppercase flags were zero.  The return     *)
(* value can be any of the TPunyCodeStatus values defined above   *)
(* except pcBadInput; if not pcSuccess, then       *)
(* output_size and output might contain garbage.                  *)

function PunycodeEncode(inputlen: Cardinal; const input: PPunycode;
  var outputlen: Cardinal; const output: PByte = nil;
  const caseflags: PByte = nil): TPunyCodeStatus;
var
  outidx, maxout, n, delta, h, b, bias, m, q, k, t: Cardinal;
  j: Integer;
  _output: PByteArray absolute output;
  _caseflags: PByteArray absolute caseflags;
begin
  (* Initialize the state: *)

  n := PUNY_INITIAL_N;
  outidx := 0;
  delta := outidx;
  maxout := outputlen;
  bias := PUNY_INITIAL_BIAS;

  (* Handle the basic code points: *)

  for j := 0 to inputlen — 1 do
  begin
    if (input[j] < $80) then
    begin
      if (output <> nil) then
      begin
        if (maxout — outidx < 2) then
        begin
          Result := pcBigOutput;
          Exit;
        end;
        if (caseflags <> nil) then
          _output[outidx] := PUNY_EncodeBasic(input[j], _caseflags[j])
        else
          _output[outidx] := input[j];
      end;

      Inc(outidx);
    end;
    (* else if (input[j] < n) return pcBadInput; *)
    (* (not needed for Punycode with unsigned code points) *)
  end;

  b := outidx;
  h := b;

  (* h is the number of code points that have been handled, b is the *)
  (* number of basic code points, and out is the number of characters *)
  (* that have been output. *)

  if (b > 0) then
  begin
    if (output <> nil) then
      _output[outidx] := PUNY_DELIMITER;
    Inc(outidx);
  end;

  (* Main encoding loop: *)

  while (h < inputlen) do
  begin
    (* All non-basic code points < n have been *)
    (* handled already.  Find the next larger one: *)

    m := PUNY_maxint;
    for j := 0 to inputlen — 1 do
      (* if (basic(input[j])) continue; *)
      (* (not needed for Punycode) *)
      if ((input[j] >= n) and (input[j] < m)) then
        m := input[j];

    (* Increase delta enough to advance the decoder’s *)
    (* <n,i> state to <m,0>, but guard against overflow: *)

    if (m — n > (PUNY_maxint — delta) div (h + 1)) then
    begin
      Result := pcOverflow;
      Exit;
    end;
    Inc(delta, (m — n) * (h + 1));
    n := m;

    for j := 0 to inputlen — 1 do
    begin
      (* Punycode does not need to check whether input[j] is basic: *)
      if (input[j] < n (* or basic(input[j]) *) ) then
      begin
        Inc(delta);
        if (delta = 0) then
        begin
          Result := pcOverflow;
          Exit;
        end;
      end;

      if (input[j] = n) then
      begin
        (* Represent delta as a generalized variable-length integer: *)

        q := delta;
        k := PUNY_BASE;
        while true do
        begin
          if (output <> nil) then
            if (outidx >= maxout) then
            begin
              Result := pcBigOutput;
              Exit;
            end;
          if k <= bias (* + TMIN *) then (* +TMIN not needed *)
            t := PUNY_TMIN
          else if k >= bias + PUNY_TMAX then
            t := PUNY_TMAX
          else
            t := k — bias;
          if (q < t) then
            break;
          if (output <> nil) then
            _output[outidx] := PUNY_EncodeDigit(t + (q — t) mod (PUNY_BASE — t), False);
          Inc(outidx);
          q := (q — t) div (PUNY_BASE — t);
          Inc(k, PUNY_BASE);
        end;
        if (output <> nil) then
          _output[outidx] := PUNY_EncodeDigit(q,
            (caseflags <> nil) and (_caseflags[j] <> 0));
        Inc(outidx);
        bias := PUNY_Adapt(delta, h + 1, h = b);
        delta := 0;
        Inc(h);
      end;
    end;

    Inc(delta);
    Inc(n);
  end;

  outputlen := outidx;
  Result := pcSuccess;
end;

(* PunycodeDecode() converts Punycode to Unicode.  The input is  *)
(* represented as an array of ASCII code points, and the output   *)
(* will be represented as an array of Unicode code points.  The   *)
(* input_length is the number of code points in the input.  The   *)
(* output_length is an in/out argument: the caller passes in      *)
(* the maximum number of code points that it can receive, and     *)
(* on successful return it will contain the actual number of      *)
(* code points output.  The case_flags array needs room for at    *)
(* least output_length values, or it can be a null pointer if the *)
(* case information is not needed.  A nonzero flag suggests that  *)
(* the corresponding Unicode character be forced to uppercase     *)
(* by the caller (if possible), while zero suggests that it be    *)
(* forced to lowercase (if possible).  ASCII code points are      *)
(* output already in the proper case, but their flags will be set *)
(* appropriately so that applying the flags would be harmless.    *)
(* The return value can be any of the TPunyCodeStatus values      *)
(* defined above; if not pcSuccess, then output_length,    *)
(* output, and case_flags might contain garbage.  On success, the *)
(* decoder will never need to write an output_length greater than *)
(* input_length, because of how the encoding is defined.          *)

function PunycodeDecode(inputlen: Cardinal; const input: PByte;
  var outputlen: Cardinal; output: PPunycode;
  caseflags: PByte): TPunyCodeStatus;
var
  outidx, i, maxout, bias, b, inidx, oldi, w, k, digit, t, n : Cardinal;
  j: Integer;
  _input: PByteArray absolute input;
  _caseflags: PByteArray absolute caseflags;
begin

  (* Initialize the state: *)

  n := PUNY_INITIAL_N;
  outidx := 0;
  i := outidx;
  maxout := outputlen;
  bias := PUNY_INITIAL_BIAS;

  (* Handle the basic code points:  Let b be the number of input code *)
  (* points before the last DELIMITER, or 0 if there is none, then *)
  (* copy the first b code points to the output. *)

  b := 0;
  for j := 0 to inputlen — 1 do
    if _input[j] = PUNY_DELIMITER then
      b := j;

  if output <> nil then
    if (b > maxout) then
    begin
      Result := pcBigOutput;
      Exit;
    end;

  for j := 0 to b — 1 do
  begin
    if (caseflags <> nil) then
      _caseflags[outidx] := PUNY_flagged(_input[j]);
    if (_input[j] >= $80) then
    begin
      Result := pcBadInput;
      Exit;
    end;
    if output <> nil then
      output[outidx] := _input[j];
    Inc(outidx);
  end;

  (* Main decoding loop:  Start just after the last DELIMITER if any *)
  (* basic code points were copied; start at the beginning otherwise. *)

  if (b > 0) then
    inidx := b + 1
  else
    inidx := 0;

  while inidx < inputlen do
  begin
    (* in is the index of the next character to be consumed, and *)
    (* out is the number of code points in the output array. *)

    (* Decode a generalized variable-length integer into delta, *)
    (* which gets added to i.  The overflow checking is easier *)
    (* if we increase i as we go, then subtract off its starting *)
    (* value at the end to obtain delta. *)

    oldi := i;
    w := 1;
    k := PUNY_BASE;
    while true do
    begin
      if (inidx >= inputlen) then
      begin
        Result := pcBadInput;
        Exit;
      end;
      digit := PUNY_DecodeDigit(_input[inidx]);
      Inc(inidx);
      if (digit >= PUNY_BASE) then
      begin
        Result := pcBadInput;
        Exit;
      end;
      if (digit > (PUNY_maxint — i) div w) then
      begin
        Result := pcOverflow;
        Exit;
      end;
      Inc(i, digit * w);
      if k <= bias (* + TMIN *) then
        t := PUNY_TMIN
      else (* +TMIN not needed *)
      if k >= bias + PUNY_TMAX then
        t := PUNY_TMAX
      else
        t := k — bias;
      if (digit < t) then
        break;
      if (w > (PUNY_maxint div (PUNY_BASE — t))) then
      begin
        Result := pcOverflow;
        Exit;
      end;
      w := w * (PUNY_BASE — t);
      Inc(k, PUNY_BASE);
    end;

    bias := PUNY_Adapt(i — oldi, outidx + 1, oldi = 0);

    (* i was supposed to wrap around from out+1 to 0, *)
    (* incrementing n each time, so we’ll fix that now: *)

    if (i div (outidx + 1) > PUNY_maxint — n) then
    begin
      Result := pcOverflow;
      Exit;
    end;
    Inc(n, i div (outidx + 1));
    i := i mod (outidx + 1);

    (* Insert n at position i of the output: *)

    (* not needed for Punycode: *)
    (* if (DecodeDigit(n) <= BASE) return punycode_invalid_input; *)
    if output <> nil then
      if (outidx >= maxout) then
      begin
        Result := pcBigOutput;
        Exit;
      end;

    if (caseflags <> nil) then
    begin
      move(_caseflags[i], _caseflags[i + 1], outidx — i);

      (* Case of last character determines uppercase flag: *)
      _caseflags[i] := PUNY_flagged(_input[inidx — 1]);
    end;

    if output <> nil then
    begin
      move(output[i], output[i + 1], (outidx — i) * SizeOf(TPunyCode));
      output[i] := n;
    end;
    Inc(i);

    Inc(outidx);
  end;

  outputlen := outidx;
  Result := pcSuccess;
end;

function PunycodeDecodeDomain(const str: Ansistring): UnicodeString;
var
  p, s: PAnsiChar;

  procedure doIt(dot: Boolean);
  var
    inlen, outlen: Cardinal;
    unicode: Unicodestring;
    u: PWideChar;
  begin
    inlen := p — s;
    if (inlen > 4) and (StrLIComp(s, ‘xn--‘, 4) = 0) and
      (PunycodeDecode(inlen-4, PByte(@s[4]), outlen) = pcSuccess) then
    begin
      if dot then
        SetLength(unicode, outlen + 1)
      else
        SetLength(unicode, outlen);
      u := PWideChar(unicode);
      PunycodeDecode(inlen-4, PByte(@s[4]), outlen, PPunyCode(u));
      if dot then
      begin
        inc(u, outlen);
        u^ := ‘.’;
      end;
    end else
      if dot then
        Setstring(unicode, s, inlen + 1)
      else
        Setstring(unicode, s, inlen);
    Result := Result + unicode;
  end;

begin
  Result := »;
  p := PAnsiChar(str);
  s := p;

  while True do
  case p^ of
    ‘.’:
      begin
        doIt(True);
        Inc(p);
        s := p;
      end;
    #0 :
      begin
        doIt(False);
        Break;
      end;
  else
    Inc(p);
  end;
end;

function PunycodeEncodeDomain(const str: Unicodestring): AnsiString;
var
  p, s: PWideChar;

  procedure doIt(dot: Boolean);
  var
    inlen, outlen: Cardinal;
    ansi: Ansistring;
    a: PAnsiChar;
  begin
    inlen := p — s;
    if (PunycodeEncode(inlen, PPunyCode(s), outlen) = pcSuccess) and (inlen + 1 <> outlen) then
    begin
      if dot then
        SetLength(ansi, outlen + 4 + 1)
      else
        SetLength(ansi, outlen + 4);
      a := PAnsiChar(ansi);
      Move(PAnsiChar(‘xn--‘)^, a^, 4);
      inc(a, 4);
      PunycodeEncode(inlen, PPunyCode(s), outlen, PByte(a));
      if dot then
      begin
        inc(a, outlen);
        a^ := ‘.’;
      end;
    end else
      if dot then
        Setstring(ansi, s, inlen + 1)
      else
        Setstring(ansi, s, inlen);
    Result := Result + ansi;
  end;

begin
  Result := »;
  p := PWideChar(str);
  s := p;

  while True do
  case p^ of
    ‘.’:
      begin
        doIt(True);
        Inc(p);
        s := p;
      end;
    #0 :
      begin
        doIt(False);
        Break;
      end;
  else
    Inc(p);
  end;
end;

end.

Скачать PAS и DCU версии данного модуля, а так же пример его использования, можете здесь.
 
Как использовать этот модуль? Элементарно! Расположите модуль PunyCode DCU в каталоге со своим проектом и в разделе uses своего проекта, пропишите дополнительный модуль «PunyCode». Вот и все. Модуль содержит две ключевых функции для конвертации доменных имен: PunycodeEncodeDomain и PunycodeDecodeDomain. Первая конвертирует из кириллицы в PunyCode, вторая обратно.
 
Пример использования:

unit ExamplePuny;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Punycode, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
private
    { Private declarations }
public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
const urlCyr = ‘МНОГОТОЧЕК.РФ’;
          urlLat = ‘xn--70akdum1a.xn--s0ai’//Яндекс.РФ
begin

  //кодирование в PunyCode
  showmessage(PunycodeEncodeDomain(urlCyr));

  //декодирование из PunyCode в кириллицу
 showmessage(PunycodeDecodeDomain(urlLat));
end;

end.

Все!