Merge pull request #3381 from krytarowski/netbsd-support-20
[mono.git] / mcs / class / corlib / System.Globalization / IdnMapping.cs
1 //
2 // IdnMapping.cs
3 //
4 // Author:
5 //      Atsushi Enomoto  <atsushi@ximian.com>
6 //
7 // Copyright (C) 2007 Novell, Inc (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28
29 /*
30
31 ** related RFCs
32
33         RFC 3490: IDNA
34         RFC 3491: Nameprep
35         RFC 3492: Punycode
36         RFC 3454: STRINGPREP
37
38 Prohibited in [Nameprep]: C.1.2, C.2.2, C.3 - C.9 in [STRINGPREP]
39
40         C.1.2 non-ascii spaces (00A0, 1680, 2000-200B, 202F, 205F, 3000)
41         C.2.2 non-ascii controls (0080-009F, 06DD, 070F, 180E, 200C, 200D,
42               2028, 2029, 2060-2063, 206A-206F, FEFF, FFF9-FFFC, 1D173-1D17A)
43         C.3 private use (E000-F8FF, F0000-FFFFD, 100000-10FFFD)
44         C.4 non-characters (FDD0-FDEF, FFFE-FFFF, nFFFE-nFFFF)
45         C.5 surrogate code (D800-DFFF)
46         C.6 inappropriate for plain text (FFF9-FFFD)
47         C.7 inappropriate for canonical representation (2FF0-2FFB)
48         C.8 change display properties or are deprecated (0340, 0341,
49                 200E, 200F, 202A-202E, 206A-206F)
50         C.9 tagging characters (E0001, E0020-E007F)
51
52 */
53
54 using System;
55 using System.Text;
56
57 namespace System.Globalization
58 {
59         public sealed class IdnMapping
60         {
61                 bool allow_unassigned, use_std3;
62                 Punycode puny = new Punycode ();
63
64                 public IdnMapping ()
65                 {
66                 }
67
68                 public bool AllowUnassigned {
69                         get { return allow_unassigned; }
70                         set { allow_unassigned = value; }
71                 }
72
73                 public bool UseStd3AsciiRules {
74                         get { return use_std3; }
75                         set { use_std3 = value; }
76                 }
77
78                 public override bool Equals (object obj)
79                 {
80                         IdnMapping other = obj as IdnMapping;
81                         return other != null &&
82                                allow_unassigned == other.allow_unassigned &&
83                                use_std3 == other.use_std3;
84                 }
85
86                 public override int GetHashCode ()
87                 {
88                         return (allow_unassigned ? 2 : 0) + (use_std3 ? 1 : 0);
89                 }
90
91                 #region GetAscii
92
93                 public string GetAscii (string unicode)
94                 {
95                         if (unicode == null)
96                                 throw new ArgumentNullException ("unicode");
97                         return GetAscii (unicode, 0, unicode.Length);
98                 }
99
100                 public string GetAscii (string unicode, int index)
101                 {
102                         if (unicode == null)
103                                 throw new ArgumentNullException ("unicode");
104                         return GetAscii (unicode, index, unicode.Length - index);
105                 }
106
107                 public string GetAscii (string unicode, int index, int count)
108                 {
109                         if (unicode == null)
110                                 throw new ArgumentNullException ("unicode");
111                         if (index < 0)
112                                 throw new ArgumentOutOfRangeException ("index must be non-negative value");
113                         if (count < 0 || index + count > unicode.Length)
114                                 throw new ArgumentOutOfRangeException ("index + count must point inside the argument unicode string");
115
116                         return Convert (unicode, index, count, true);
117                 }
118
119                 string Convert (string input, int index, int count, bool toAscii)
120                 {
121                         string s = input.Substring (index, count);
122
123                         // Actually lowering string is done as part of
124                         // Nameprep(), but it is much easier to do it in prior.
125                         for (int i = 0; i < s.Length; i++)
126                                 if (s [i] >= '\x80') {
127                                         s = s.ToLower (CultureInfo.InvariantCulture);
128                                         break;
129                                 }
130
131                         // RFC 3490 section 4. and 4.1
132                         // 1) -> done as AllowUnassigned property
133                         // 2) split the input
134                         string [] labels = s.Split ('.', '\u3002', '\uFF0E', '\uFF61');
135                         int iter = 0;
136                         for (int i = 0; i < labels.Length; iter += labels [i].Length, i++) {
137                                 // 3) -> done as UseStd3AsciiRules property
138                                 // 4) ToAscii
139                                 if (labels [i].Length == 0 && i + 1 == labels.Length)
140                                         // If the input ends with '.', Split()
141                                         // adds another empty string. In that
142                                         // case, we have to ignore it.
143                                         continue;
144                                 if (toAscii)
145                                         labels [i] = ToAscii (labels [i], iter);
146                                 else
147                                         labels [i] = ToUnicode (labels [i], iter);
148                         }
149                         // 5) join them
150                         return String.Join (".", labels);
151                 }
152
153                 string ToAscii (string s, int offset)
154                 {
155                         // 1.
156                         for (int i = 0; i < s.Length; i++) {
157                                 // I wonder if this check is really RFC-conformant
158                                 if (s [i] < '\x20' || s [i] == '\x7F')
159                                         throw new ArgumentException (String.Format ("Not allowed character was found, at {0}", offset + i));
160                                 if (s [i] >= 0x80) {
161                                         // 2.
162                                         s = NamePrep (s, offset);
163                                         break;
164                                 }
165                         }
166
167                         // 3.
168                         if (use_std3)
169                                 VerifyStd3AsciiRules (s, offset);
170
171                         // 4.
172                         for (int i = 0; i < s.Length; i++) {
173                                 if (s [i] >= 0x80) {
174                                         // 5. check ACE.
175                                         if (s.StartsWith ("xn--", StringComparison.OrdinalIgnoreCase))
176                                                 throw new ArgumentException (String.Format ("The input string must not start with ACE (xn--), at {0}", offset + i));
177                                         // 6. Punycode it.
178                                         s = puny.Encode (s, offset);
179                                         // 7. prepend ACE.
180                                         s = "xn--" + s;
181                                         break;
182                                 }
183                         }
184
185                         // 8.
186                         VerifyLength (s, offset);
187
188                         return s;
189                 }
190
191                 void VerifyLength (string s, int offset)
192                 {
193                         if (s.Length == 0)
194                                 throw new ArgumentException (String.Format ("A label in the input string resulted in an invalid zero-length string, at {0}", offset));
195                         if (s.Length > 63)
196                                 throw new ArgumentException (String.Format ("A label in the input string exceeded the length in ASCII representation, at {0}", offset));
197                 }
198
199                 string NamePrep (string s, int offset)
200                 {
201                         s = s.Normalize (NormalizationForm.FormKC);
202                         VerifyProhibitedCharacters (s, offset);
203                         // FIXME: check BIDI
204
205                         if (!allow_unassigned) {
206                                 for (int i = 0; i < s.Length; i++)
207                                         if (Char.GetUnicodeCategory (s, i) == UnicodeCategory.OtherNotAssigned)
208                                                 throw new ArgumentException (String.Format ("Use of unassigned Unicode characer is prohibited in this IdnMapping, at {0}", offset + i));
209                         }
210                         return s;
211                 }
212
213                 void VerifyProhibitedCharacters (string s, int offset)
214                 {
215                         for (int i = 0; i < s.Length; i++) {
216                                 switch (Char.GetUnicodeCategory (s, i)) {
217                                 case UnicodeCategory.SpaceSeparator:
218                                         if (s [i] < '\x80')
219                                                 continue; // valid
220                                         break;
221                                 case UnicodeCategory.Control:
222                                         if (s [i] != '\x0' && s [i] < '\x80')
223                                                 continue; // valid
224                                         break;
225                                 case UnicodeCategory.PrivateUse:
226                                 case UnicodeCategory.Surrogate:
227                                         break;
228                                 default:
229                                         char c = s [i];
230                                         if (// C.4
231                                             '\uFDDF' <= c && c <= '\uFDEF' ||
232                                             ((int) c & 0xFFFF) == 0xFFFE ||
233                                             // C.6
234                                             '\uFFF9' <= c && c <= '\uFFFD' ||
235                                             // C.7
236                                             '\u2FF0' <= c && c <= '\u2FFB' ||
237                                             // C.8
238                                             '\u202A' <= c && c <= '\u202E' ||
239                                             '\u206A' <= c && c <= '\u206F')
240                                                 break;
241                                         switch (c) {
242                                         // C.8
243                                         case '\u0340':
244                                         case '\u0341':
245                                         case '\u200E':
246                                         case '\u200F':
247                                         // C.2.2
248                                         case '\u2028':
249                                         case '\u2029':
250                                                 break;
251                                         default:
252                                                 continue;
253                                         }
254                                         break;
255                                 }
256                                 throw new ArgumentException (String.Format ("Not allowed character was in the input string, at {0}", offset + i));
257                         }
258                 }
259
260                 void VerifyStd3AsciiRules (string s, int offset)
261                 {
262                         if (s.Length > 0 && s [0] == '-')
263                                 throw new ArgumentException (String.Format ("'-' is not allowed at head of a sequence in STD3 mode, found at {0}", offset));
264                         if (s.Length > 0 && s [s.Length - 1] == '-')
265                                 throw new ArgumentException (String.Format ("'-' is not allowed at tail of a sequence in STD3 mode, found at {0}", offset + s.Length - 1));
266
267                         for (int i = 0; i < s.Length; i++) {
268                                 char c = s [i];
269                                 if (c == '-')
270                                         continue;
271                                 if (c <= '\x2F' || '\x3A' <= c && c <= '\x40' || '\x5B' <= c && c <= '\x60' || '\x7B' <= c && c <= '\x7F')
272                                         throw new ArgumentException (String.Format ("Not allowed character in STD3 mode, found at {0}", offset + i));
273                         }
274                 }
275
276                 #endregion
277
278                 public string GetUnicode (string ascii)
279                 {
280                         if (ascii == null)
281                                 throw new ArgumentNullException ("ascii");
282                         return GetUnicode (ascii, 0, ascii.Length);
283                 }
284
285                 public string GetUnicode (string ascii, int index)
286                 {
287                         if (ascii == null)
288                                 throw new ArgumentNullException ("ascii");
289                         return GetUnicode (ascii, index, ascii.Length - index);
290                 }
291
292                 public string GetUnicode (string ascii, int index, int count)
293                 {
294                         if (ascii == null)
295                                 throw new ArgumentNullException ("ascii");
296                         if (index < 0)
297                                 throw new ArgumentOutOfRangeException ("index must be non-negative value");
298                         if (count < 0 || index + count > ascii.Length)
299                                 throw new ArgumentOutOfRangeException ("index + count must point inside the argument ascii string");
300
301                         return Convert (ascii, index, count, false);
302                 }
303
304                 string ToUnicode (string s, int offset)
305                 {
306                         // 1.
307                         for (int i = 0; i < s.Length; i++) {
308                                 if (s [i] >= 0x80) {
309                                         // 2.
310                                         s = NamePrep (s, offset);
311                                         break;
312                                 }
313                         }
314
315                         // 3.
316                         if (!s.StartsWith ("xn--", StringComparison.OrdinalIgnoreCase))
317                                 return s; // failure = return the input string as is.
318                         // Actually lowering string is done as part of
319                         // Nameprep(), but it is much easier to do it in prior.
320                         s = s.ToLower (CultureInfo.InvariantCulture);
321
322                         string at3 = s;
323
324                         // 4.
325                         s = s.Substring (4);
326
327                         // 5.
328                         s = puny.Decode (s, offset);
329                         string at5 = s;
330
331                         // 6.
332                         s = ToAscii (s, offset);
333
334                         // 7.
335                         if (String.Compare (at3, s, StringComparison.OrdinalIgnoreCase) != 0)
336                                 throw new ArgumentException (String.Format ("ToUnicode() failed at verifying the result, at label part from {0}", offset));
337
338                         // 8.
339                         return at5;
340                 }
341         }
342
343         class Bootstring
344         {
345                 readonly char delimiter;
346                 readonly int base_num, tmin, tmax, skew, damp, initial_bias, initial_n;
347                 
348                 public Bootstring (char delimiter,
349                                  int baseNum, int tmin, int tmax,
350                                  int skew, int damp,
351                                  int initialBias, int initialN)
352                 {
353                         this.delimiter = delimiter;
354                         base_num = baseNum;
355                         this.tmin = tmin;
356                         this.tmax = tmax;
357                         this.skew = skew;
358                         this.damp = damp;
359                         initial_bias = initialBias;
360                         initial_n = initialN;
361                 }
362
363                 public string Encode (string s, int offset)
364                 {
365                         int n = initial_n;
366                         int delta = 0;
367                         int bias = initial_bias;
368                         int b = 0, h = 0;
369                         StringBuilder sb = new StringBuilder ();
370                         for (int i = 0; i < s.Length; i++)
371                                 if (s [i] < '\x80')
372                                         sb.Append (s [i]);
373                         b = h = sb.Length;
374                         if (b > 0)
375                                 sb.Append (delimiter);
376
377                         while (h < s.Length) {
378                                 int m = int.MaxValue;
379                                 for (int i = 0; i < s.Length; i++)
380                                         if (s [i] >= n && s [i] < m)
381                                                 m = s [i];
382                                 checked { delta += (m - n) * (h + 1); }
383                                 n = m;
384                                 for (int i = 0; i < s.Length; i++) {
385                                         char c = s [i];
386                                         if (c < n || c < '\x80')
387                                                 checked { delta++; }
388                                         if (c == n) {
389                                                 int q = delta;
390                                                 for (int k = base_num; ;k += base_num) {
391                                                         int t =
392                                                                 k <= bias + tmin ? tmin :
393                                                                 k >= bias + tmax ? tmax :
394                                                                 k - bias;
395                                                         if (q < t)
396                                                                 break;
397                                                         sb.Append (EncodeDigit (t + (q - t) % (base_num - t)));
398                                                         q = (q - t) / (base_num - t);
399                                                 }
400                                                 sb.Append (EncodeDigit (q));
401                                                 bias = Adapt (delta, h + 1, h == b);
402                                                 delta = 0;
403                                                 h++;
404                                         }
405                                 }
406                                 delta++;
407                                 n++;
408                         }
409
410                         return sb.ToString ();
411                 }
412
413                 // 41..5A (A-Z) = 0-25
414                 // 61..7A (a-z) = 0-25
415                 // 30..39 (0-9) = 26-35
416                 char EncodeDigit (int d)
417                 {
418                         return (char) (d < 26 ? d + 'a' : d - 26 + '0');
419                 }
420
421                 int DecodeDigit (char c)
422                 {
423                         return  c - '0' < 10 ? c - 22 :
424                                 c - 'A' < 26 ? c - 'A' :
425                                 c - 'a' < 26 ? c - 'a' : base_num;
426                 }
427
428                 int Adapt (int delta, int numPoints, bool firstTime)
429                 {
430                         if (firstTime)
431                                 delta = delta / damp;
432                         else
433                                 delta = delta / 2;
434                         delta = delta + (delta / numPoints);
435                         int k = 0;
436                         while (delta > ((base_num - tmin) * tmax) / 2) {
437                                 delta = delta / (base_num - tmin);
438                                 k += base_num;
439                         }
440                         return k + (((base_num - tmin + 1) * delta) / (delta + skew));
441                 }
442
443                 public string Decode (string s, int offset)
444                 {
445                         int n = initial_n;
446                         int i = 0;
447                         int bias = initial_bias;
448                         int b = 0;
449                         StringBuilder sb = new StringBuilder ();
450
451                         for (int j = 0; j < s.Length; j++) {
452                                 if (s [j] == delimiter)
453                                         b = j;
454                         }
455                         if (b < 0)
456                                 return s;
457                         sb.Append (s, 0, b);
458
459                         for (int z = b > 0 ? b + 1 : 0; z < s.Length; ) {
460                                 int old_i = i;
461                                 int w = 1;
462                                 for (int k = base_num; ; k += base_num) {
463                                         int digit = DecodeDigit (s [z++]);
464                                         i = i + digit * w;
465                                         int t = k <= bias + tmin ? tmin :
466                                                 k >= bias + tmax ? tmax :
467                                                 k - bias;
468                                         if (digit < t)
469                                                 break;
470                                         w = w * (base_num - t);
471                                 }
472                                 bias = Adapt (i - old_i, sb.Length + 1, old_i == 0);
473                                 n = n + i / (sb.Length + 1);
474                                 i = i % (sb.Length + 1);
475                                 if (n < '\x80')
476                                         throw new ArgumentException (String.Format ("Invalid Bootstring decode result, at {0}", offset + z));
477                                 sb.Insert (i, (char) n);
478                                 i++;
479                         }
480
481                         return sb.ToString ();
482                 }
483         }
484
485         class Punycode : Bootstring
486         {
487                 public Punycode ()
488                         : base ('-', 36, 1, 26, 38, 700, 72, 0x80)
489                 {
490                 }
491         }
492 }