1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package idna
19
20 import (
21 "fmt"
22 "strings"
23 "unicode/utf8"
24
25 "golang.org/x/text/secure/bidirule"
26 "golang.org/x/text/unicode/norm"
27 )
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 func ToASCII(s string) (string, error) {
46 return Punycode.process(s, true)
47 }
48
49
50 func ToUnicode(s string) (string, error) {
51 return Punycode.process(s, false)
52 }
53
54
55 type Option func(*options)
56
57
58
59
60
61
62 func Transitional(transitional bool) Option {
63 return func(o *options) { o.transitional = transitional }
64 }
65
66
67
68
69
70 func VerifyDNSLength(verify bool) Option {
71 return func(o *options) { o.verifyDNSLength = verify }
72 }
73
74
75
76 func RemoveLeadingDots(remove bool) Option {
77 return func(o *options) { o.removeLeadingDots = remove }
78 }
79
80
81
82
83
84
85 func ValidateLabels(enable bool) Option {
86 return func(o *options) {
87
88
89 if o.mapping == nil && enable {
90 o.mapping = normalize
91 }
92 o.trie = trie
93 o.checkJoiners = enable
94 o.checkHyphens = enable
95 if enable {
96 o.fromPuny = validateFromPunycode
97 } else {
98 o.fromPuny = nil
99 }
100 }
101 }
102
103
104
105
106
107
108 func CheckHyphens(enable bool) Option {
109 return func(o *options) { o.checkHyphens = enable }
110 }
111
112
113
114
115
116 func CheckJoiners(enable bool) Option {
117 return func(o *options) {
118 o.trie = trie
119 o.checkJoiners = enable
120 }
121 }
122
123
124
125
126
127
128
129
130
131
132
133 func StrictDomainName(use bool) Option {
134 return func(o *options) { o.useSTD3Rules = use }
135 }
136
137
138
139
140
141
142
143
144 func BidiRule() Option {
145 return func(o *options) { o.bidirule = bidirule.ValidString }
146 }
147
148
149
150 func ValidateForRegistration() Option {
151 return func(o *options) {
152 o.mapping = validateRegistration
153 StrictDomainName(true)(o)
154 ValidateLabels(true)(o)
155 VerifyDNSLength(true)(o)
156 BidiRule()(o)
157 }
158 }
159
160
161
162
163
164
165
166
167
168 func MapForLookup() Option {
169 return func(o *options) {
170 o.mapping = validateAndMap
171 StrictDomainName(true)(o)
172 ValidateLabels(true)(o)
173 RemoveLeadingDots(true)(o)
174 }
175 }
176
177 type options struct {
178 transitional bool
179 useSTD3Rules bool
180 checkHyphens bool
181 checkJoiners bool
182 verifyDNSLength bool
183 removeLeadingDots bool
184
185 trie *idnaTrie
186
187
188 fromPuny func(p *Profile, s string) error
189
190
191
192 mapping func(p *Profile, s string) (string, error)
193
194
195
196 bidirule func(s string) bool
197 }
198
199
200 type Profile struct {
201 options
202 }
203
204 func apply(o *options, opts []Option) {
205 for _, f := range opts {
206 f(o)
207 }
208 }
209
210
211
212
213
214
215
216
217
218 func New(o ...Option) *Profile {
219 p := &Profile{}
220 apply(&p.options, o)
221 return p
222 }
223
224
225
226
227
228 func (p *Profile) ToASCII(s string) (string, error) {
229 return p.process(s, true)
230 }
231
232
233
234
235
236 func (p *Profile) ToUnicode(s string) (string, error) {
237 pp := *p
238 pp.transitional = false
239 return pp.process(s, false)
240 }
241
242
243
244 func (p *Profile) String() string {
245 s := ""
246 if p.transitional {
247 s = "Transitional"
248 } else {
249 s = "NonTransitional"
250 }
251 if p.useSTD3Rules {
252 s += ":UseSTD3Rules"
253 }
254 if p.checkHyphens {
255 s += ":CheckHyphens"
256 }
257 if p.checkJoiners {
258 s += ":CheckJoiners"
259 }
260 if p.verifyDNSLength {
261 s += ":VerifyDNSLength"
262 }
263 return s
264 }
265
266 var (
267
268
269 Punycode *Profile = punycode
270
271
272
273
274 Lookup *Profile = lookup
275
276
277
278 Display *Profile = display
279
280
281
282 Registration *Profile = registration
283
284 punycode = &Profile{}
285 lookup = &Profile{options{
286 transitional: true,
287 removeLeadingDots: true,
288 useSTD3Rules: true,
289 checkHyphens: true,
290 checkJoiners: true,
291 trie: trie,
292 fromPuny: validateFromPunycode,
293 mapping: validateAndMap,
294 bidirule: bidirule.ValidString,
295 }}
296 display = &Profile{options{
297 useSTD3Rules: true,
298 removeLeadingDots: true,
299 checkHyphens: true,
300 checkJoiners: true,
301 trie: trie,
302 fromPuny: validateFromPunycode,
303 mapping: validateAndMap,
304 bidirule: bidirule.ValidString,
305 }}
306 registration = &Profile{options{
307 useSTD3Rules: true,
308 verifyDNSLength: true,
309 checkHyphens: true,
310 checkJoiners: true,
311 trie: trie,
312 fromPuny: validateFromPunycode,
313 mapping: validateRegistration,
314 bidirule: bidirule.ValidString,
315 }}
316
317
318
319
320 )
321
322 type labelError struct{ label, code_ string }
323
324 func (e labelError) code() string { return e.code_ }
325 func (e labelError) Error() string {
326 return fmt.Sprintf("idna: invalid label %q", e.label)
327 }
328
329 type runeError rune
330
331 func (e runeError) code() string { return "P1" }
332 func (e runeError) Error() string {
333 return fmt.Sprintf("idna: disallowed rune %U", e)
334 }
335
336
337
338 func (p *Profile) process(s string, toASCII bool) (string, error) {
339 var err error
340 if p.mapping != nil {
341 s, err = p.mapping(p, s)
342 }
343
344 if p.removeLeadingDots {
345 for ; len(s) > 0 && s[0] == '.'; s = s[1:] {
346 }
347 }
348
349
350 if err == nil && p.verifyDNSLength && s == "" {
351 err = &labelError{s, "A4"}
352 }
353 labels := labelIter{orig: s}
354 for ; !labels.done(); labels.next() {
355 label := labels.label()
356 if label == "" {
357
358
359 if err == nil && p.verifyDNSLength {
360 err = &labelError{s, "A4"}
361 }
362 continue
363 }
364 if strings.HasPrefix(label, acePrefix) {
365 u, err2 := decode(label[len(acePrefix):])
366 if err2 != nil {
367 if err == nil {
368 err = err2
369 }
370
371 continue
372 }
373 labels.set(u)
374 if err == nil && p.fromPuny != nil {
375 err = p.fromPuny(p, u)
376 }
377 if err == nil {
378
379
380
381 err = p.validateLabel(u)
382 }
383 } else if err == nil {
384 err = p.validateLabel(label)
385 }
386 }
387 if toASCII {
388 for labels.reset(); !labels.done(); labels.next() {
389 label := labels.label()
390 if !ascii(label) {
391 a, err2 := encode(acePrefix, label)
392 if err == nil {
393 err = err2
394 }
395 label = a
396 labels.set(a)
397 }
398 n := len(label)
399 if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
400 err = &labelError{label, "A4"}
401 }
402 }
403 }
404 s = labels.result()
405 if toASCII && p.verifyDNSLength && err == nil {
406
407 n := len(s)
408 if n > 0 && s[n-1] == '.' {
409 n--
410 }
411 if len(s) < 1 || n > 253 {
412 err = &labelError{s, "A4"}
413 }
414 }
415 return s, err
416 }
417
418 func normalize(p *Profile, s string) (string, error) {
419 return norm.NFC.String(s), nil
420 }
421
422 func validateRegistration(p *Profile, s string) (string, error) {
423 if !norm.NFC.IsNormalString(s) {
424 return s, &labelError{s, "V1"}
425 }
426 for i := 0; i < len(s); {
427 v, sz := trie.lookupString(s[i:])
428
429 switch p.simplify(info(v).category()) {
430
431
432 case valid, deviation:
433 case disallowed, mapped, unknown, ignored:
434 r, _ := utf8.DecodeRuneInString(s[i:])
435 return s, runeError(r)
436 }
437 i += sz
438 }
439 return s, nil
440 }
441
442 func validateAndMap(p *Profile, s string) (string, error) {
443 var (
444 err error
445 b []byte
446 k int
447 )
448 for i := 0; i < len(s); {
449 v, sz := trie.lookupString(s[i:])
450 start := i
451 i += sz
452
453 switch p.simplify(info(v).category()) {
454 case valid:
455 continue
456 case disallowed:
457 if err == nil {
458 r, _ := utf8.DecodeRuneInString(s[start:])
459 err = runeError(r)
460 }
461 continue
462 case mapped, deviation:
463 b = append(b, s[k:start]...)
464 b = info(v).appendMapping(b, s[start:i])
465 case ignored:
466 b = append(b, s[k:start]...)
467
468 case unknown:
469 b = append(b, s[k:start]...)
470 b = append(b, "\ufffd"...)
471 }
472 k = i
473 }
474 if k == 0 {
475
476 s = norm.NFC.String(s)
477 } else {
478 b = append(b, s[k:]...)
479 if norm.NFC.QuickSpan(b) != len(b) {
480 b = norm.NFC.Bytes(b)
481 }
482
483 s = string(b)
484 }
485 return s, err
486 }
487
488
489 type labelIter struct {
490 orig string
491 slice []string
492 curStart int
493 curEnd int
494 i int
495 }
496
497 func (l *labelIter) reset() {
498 l.curStart = 0
499 l.curEnd = 0
500 l.i = 0
501 }
502
503 func (l *labelIter) done() bool {
504 return l.curStart >= len(l.orig)
505 }
506
507 func (l *labelIter) result() string {
508 if l.slice != nil {
509 return strings.Join(l.slice, ".")
510 }
511 return l.orig
512 }
513
514 func (l *labelIter) label() string {
515 if l.slice != nil {
516 return l.slice[l.i]
517 }
518 p := strings.IndexByte(l.orig[l.curStart:], '.')
519 l.curEnd = l.curStart + p
520 if p == -1 {
521 l.curEnd = len(l.orig)
522 }
523 return l.orig[l.curStart:l.curEnd]
524 }
525
526
527 func (l *labelIter) next() {
528 l.i++
529 if l.slice != nil {
530 if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" {
531 l.curStart = len(l.orig)
532 }
533 } else {
534 l.curStart = l.curEnd + 1
535 if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' {
536 l.curStart = len(l.orig)
537 }
538 }
539 }
540
541 func (l *labelIter) set(s string) {
542 if l.slice == nil {
543 l.slice = strings.Split(l.orig, ".")
544 }
545 l.slice[l.i] = s
546 }
547
548
549 const acePrefix = "xn--"
550
551 func (p *Profile) simplify(cat category) category {
552 switch cat {
553 case disallowedSTD3Mapped:
554 if p.useSTD3Rules {
555 cat = disallowed
556 } else {
557 cat = mapped
558 }
559 case disallowedSTD3Valid:
560 if p.useSTD3Rules {
561 cat = disallowed
562 } else {
563 cat = valid
564 }
565 case deviation:
566 if !p.transitional {
567 cat = valid
568 }
569 case validNV8, validXV8:
570
571 cat = valid
572 }
573 return cat
574 }
575
576 func validateFromPunycode(p *Profile, s string) error {
577 if !norm.NFC.IsNormalString(s) {
578 return &labelError{s, "V1"}
579 }
580 for i := 0; i < len(s); {
581 v, sz := trie.lookupString(s[i:])
582 if c := p.simplify(info(v).category()); c != valid && c != deviation {
583 return &labelError{s, "V6"}
584 }
585 i += sz
586 }
587 return nil
588 }
589
590 const (
591 zwnj = "\u200c"
592 zwj = "\u200d"
593 )
594
595 type joinState int8
596
597 const (
598 stateStart joinState = iota
599 stateVirama
600 stateBefore
601 stateBeforeVirama
602 stateAfter
603 stateFAIL
604 )
605
606 var joinStates = [][numJoinTypes]joinState{
607 stateStart: {
608 joiningL: stateBefore,
609 joiningD: stateBefore,
610 joinZWNJ: stateFAIL,
611 joinZWJ: stateFAIL,
612 joinVirama: stateVirama,
613 },
614 stateVirama: {
615 joiningL: stateBefore,
616 joiningD: stateBefore,
617 },
618 stateBefore: {
619 joiningL: stateBefore,
620 joiningD: stateBefore,
621 joiningT: stateBefore,
622 joinZWNJ: stateAfter,
623 joinZWJ: stateFAIL,
624 joinVirama: stateBeforeVirama,
625 },
626 stateBeforeVirama: {
627 joiningL: stateBefore,
628 joiningD: stateBefore,
629 joiningT: stateBefore,
630 },
631 stateAfter: {
632 joiningL: stateFAIL,
633 joiningD: stateBefore,
634 joiningT: stateAfter,
635 joiningR: stateStart,
636 joinZWNJ: stateFAIL,
637 joinZWJ: stateFAIL,
638 joinVirama: stateAfter,
639 },
640 stateFAIL: {
641 0: stateFAIL,
642 joiningL: stateFAIL,
643 joiningD: stateFAIL,
644 joiningT: stateFAIL,
645 joiningR: stateFAIL,
646 joinZWNJ: stateFAIL,
647 joinZWJ: stateFAIL,
648 joinVirama: stateFAIL,
649 },
650 }
651
652
653
654 func (p *Profile) validateLabel(s string) error {
655 if s == "" {
656 if p.verifyDNSLength {
657 return &labelError{s, "A4"}
658 }
659 return nil
660 }
661 if p.bidirule != nil && !p.bidirule(s) {
662 return &labelError{s, "B"}
663 }
664 if p.checkHyphens {
665 if len(s) > 4 && s[2] == '-' && s[3] == '-' {
666 return &labelError{s, "V2"}
667 }
668 if s[0] == '-' || s[len(s)-1] == '-' {
669 return &labelError{s, "V3"}
670 }
671 }
672 if !p.checkJoiners {
673 return nil
674 }
675 trie := p.trie
676
677 v, sz := trie.lookupString(s)
678 x := info(v)
679 if x.isModifier() {
680 return &labelError{s, "V5"}
681 }
682
683 if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
684 return nil
685 }
686 st := stateStart
687 for i := 0; ; {
688 jt := x.joinType()
689 if s[i:i+sz] == zwj {
690 jt = joinZWJ
691 } else if s[i:i+sz] == zwnj {
692 jt = joinZWNJ
693 }
694 st = joinStates[st][jt]
695 if x.isViramaModifier() {
696 st = joinStates[st][joinVirama]
697 }
698 if i += sz; i == len(s) {
699 break
700 }
701 v, sz = trie.lookupString(s[i:])
702 x = info(v)
703 }
704 if st == stateFAIL || st == stateAfter {
705 return &labelError{s, "C"}
706 }
707 return nil
708 }
709
710 func ascii(s string) bool {
711 for i := 0; i < len(s); i++ {
712 if s[i] >= utf8.RuneSelf {
713 return false
714 }
715 }
716 return true
717 }
718
View as plain text