bare-url 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -71,9 +71,9 @@ const URL = exports.URL = class URL {
71
71
 
72
72
  get host () {
73
73
  if (this._url.host === null) return ''
74
- if (this._url.port === null) return this._url.host
74
+ if (this._url.port === null) return serialize.host(this._url.host)
75
75
 
76
- return this._url.host + ':' + this._url.port
76
+ return serialize.host(this._url.host) + ':' + this._url.port.toString(10)
77
77
  }
78
78
 
79
79
  set host (value) {
@@ -87,7 +87,7 @@ const URL = exports.URL = class URL {
87
87
  get hostname () {
88
88
  if (this._url.host === null) return ''
89
89
 
90
- return this._url.host
90
+ return serialize.host(this._url.host)
91
91
  }
92
92
 
93
93
  set hostname (value) {
package/lib/errors.js CHANGED
@@ -44,4 +44,84 @@ module.exports = class URLError extends Error {
44
44
  static PORT_INVALID (msg = 'Invalid URL') {
45
45
  return new URLError(msg, 'PORT_INVALID', URLError.PORT_INVALID)
46
46
  }
47
+
48
+ // https://url.spec.whatwg.org/#validation-error-domain-to-ascii
49
+ static DOMAIN_TO_ASCII (msg = 'Invalid URL') {
50
+ return new URLError(msg, 'DOMAIN_TO_ASCII', URLError.DOMAIN_TO_ASCII)
51
+ }
52
+
53
+ // https://url.spec.whatwg.org/#domain-invalid-code-point
54
+ static DOMAIN_INVALID_CODE_POINT (msg = 'Invalid URL') {
55
+ return new URLError(msg, 'DOMAIN_INVALID_CODE_POINT', URLError.DOMAIN_INVALID_CODE_POINT)
56
+ }
57
+
58
+ // https://url.spec.whatwg.org/#host-invalid-code-point
59
+ static HOST_INVALID_CODE_POINT (msg = 'Invalid URL') {
60
+ return new URLError(msg, 'HOST_INVALID_CODE_POINT', URLError.HOST_INVALID_CODE_POINT)
61
+ }
62
+
63
+ // https://url.spec.whatwg.org/#ipv4-too-many-parts
64
+ static IPV4_TOO_MANY_PARTS (msg = 'Invalid URL') {
65
+ return new URLError(msg, 'IPV4_TOO_MANY_PARTS', URLError.IPV4_TOO_MANY_PARTS)
66
+ }
67
+
68
+ // https://url.spec.whatwg.org/#ipv4-non-numeric-part
69
+ static IPV4_NON_NUMERIC_PART (msg = 'Invalid URL') {
70
+ return new URLError(msg, 'IPV4_NON_NUMERIC_PART', URLError.IPV4_NON_NUMERIC_PART)
71
+ }
72
+
73
+ // https://url.spec.whatwg.org/#ipv4-out-of-range-part
74
+ static IPV4_OUT_OF_RANGE_PART (msg = 'Invalid URL') {
75
+ return new URLError(msg, 'IPV4_OUT_OF_RANGE_PART', URLError.IPV4_OUT_OF_RANGE_PART)
76
+ }
77
+
78
+ // https://url.spec.whatwg.org/#ipv6-unclosed
79
+ static IPV6_UNCLOSED (msg = 'Invalid URL') {
80
+ return new URLError(msg, 'IPV6_UNCLOSED', URLError.IPV6_UNCLOSED)
81
+ }
82
+
83
+ // https://url.spec.whatwg.org/#ipv6-invalid-compression
84
+ static IPV6_INVALID_COMPRESSION (msg = 'Invalid URL') {
85
+ return new URLError(msg, 'IPV6_INVALID_COMPRESSION', URLError.IPV6_INVALID_COMPRESSION)
86
+ }
87
+
88
+ // https://url.spec.whatwg.org/#ipv6-too-many-pieces
89
+ static IPV6_TOO_MANY_PIECES (msg = 'Invalid URL') {
90
+ return new URLError(msg, 'IPV6_TOO_MANY_PIECES', URLError.IPV6_TOO_MANY_PIECES)
91
+ }
92
+
93
+ // https://url.spec.whatwg.org/#ipv6-multiple-compression
94
+ static IPV6_MULTIPLE_COMPRESSION (msg = 'Invalid URL') {
95
+ return new URLError(msg, 'IPV6_MULTIPLE_COMPRESSION', URLError.IPV6_MULTIPLE_COMPRESSION)
96
+ }
97
+
98
+ // https://url.spec.whatwg.org/#ipv6-invalid-code-point
99
+ static IPV6_INVALID_CODE_POINT (msg = 'Invalid URL') {
100
+ return new URLError(msg, 'IPV6_INVALID_CODE_POINT', URLError.IPV6_INVALID_CODE_POINT)
101
+ }
102
+
103
+ // https://url.spec.whatwg.org/#ipv6-too-few-pieces
104
+ static IPV6_TOO_FEW_PIECES (msg = 'Invalid URL') {
105
+ return new URLError(msg, 'IPV6_TOO_FEW_PIECES', URLError.IPV6_TOO_FEW_PIECES)
106
+ }
107
+
108
+ // https://url.spec.whatwg.org/#ipv4-in-ipv6-too-many-pieces
109
+ static IPV4_IN_IPV6_TOO_MANY_PIECES (msg = 'Invalid URL') {
110
+ return new URLError(msg, 'IPV4_IN_IPV6_TOO_MANY_PIECES', URLError.IPV4_IN_IPV6_TOO_MANY_PIECES)
111
+ }
112
+
113
+ // https://url.spec.whatwg.org/#ipv4-in-ipv6-invalid-code-point
114
+ static IPV4_IN_IPV6_INVALID_CODE_POINT (msg = 'Invalid URL') {
115
+ return new URLError(msg, 'IPV4_IN_IPV6_INVALID_CODE_POINT', URLError.IPV4_IN_IPV6_INVALID_CODE_POINT)
116
+ }
117
+
118
+ // https://url.spec.whatwg.org/#ipv4-in-ipv6-out-of-range-part
119
+ static IPV4_IN_IPV6_OUT_OF_RANGE_PART (msg = 'Invalid URL') {
120
+ return new URLError(msg, 'IPV4_IN_IPV6_OUT_OF_RANGE_PART', URLError.IPV4_IN_IPV6_OUT_OF_RANGE_PART)
121
+ }
122
+
123
+ // https://url.spec.whatwg.org/#ipv4-in-ipv6-too-few-parts
124
+ static IPV4_IN_IPV6_TOO_FEW_PARTS (msg = 'Invalid URL') {
125
+ return new URLError(msg, 'IPV4_IN_IPV6_TOO_FEW_PARTS', URLError.IPV4_IN_IPV6_TOO_FEW_PARTS)
126
+ }
47
127
  }
package/lib/parse.js CHANGED
@@ -1,3 +1,4 @@
1
+ const tr46 = require('tr46')
1
2
  const constants = require('./constants')
2
3
  const errors = require('./errors')
3
4
  const infra = require('./infra')
@@ -512,9 +513,241 @@ module.exports = function parse (
512
513
  return url
513
514
  }
514
515
 
515
- // https://url.spec.whatwg.org/#host-parsing
516
- function parseHost (host, isNotSpecial) {
517
- return host
516
+ // https://url.spec.whatwg.org/#concept-host-parser
517
+ function parseHost (input, isOpaque = false) {
518
+ if (input.charCodeAt(0) === 0x5b) {
519
+ if (input.charCodeAt(input.length - 1) !== 0x5d) throw errors.IPV6_UNCLOSED()
520
+
521
+ return parseIPv6(input.substring(1, input.length - 1))
522
+ }
523
+
524
+ if (isOpaque) return parseOpaqueHost(input)
525
+
526
+ const domain = percentDecode(Buffer.from(input, 'utf8'))
527
+
528
+ const asciiDomain = domainToASCII(domain.toString('utf8'), false)
529
+
530
+ for (const codePoint of asciiDomain) {
531
+ if (isForbiddenDomainCodePoint(codePoint.codePointAt(0))) {
532
+ throw errors.DOMAIN_INVALID_CODE_POINT()
533
+ }
534
+ }
535
+
536
+ if (endsInANumber(asciiDomain)) return parseIPv4(asciiDomain)
537
+
538
+ return asciiDomain
539
+ }
540
+
541
+ // https://url.spec.whatwg.org/#ends-in-a-number-checker
542
+ function endsInANumber (input) {
543
+ const parts = input.split('.')
544
+
545
+ const last = parts[parts.length - 1]
546
+
547
+ if (last === '') {
548
+ if (parts.length === 1) return false
549
+ parts.pop()
550
+ }
551
+
552
+ try {
553
+ parseIPv4Number(last)
554
+
555
+ return true
556
+ } catch {
557
+ return false
558
+ }
559
+ }
560
+
561
+ // https://url.spec.whatwg.org/#concept-ipv4-parser
562
+ function parseIPv4 (input) {
563
+ const parts = input.split('.')
564
+
565
+ if (parts[parts.length - 1] === '') {
566
+ if (parts.length > 1) parts.pop()
567
+ }
568
+
569
+ if (parts.length > 4) throw errors.IPV4_TOO_MANY_PARTS()
570
+
571
+ const numbers = []
572
+
573
+ for (const part of parts) {
574
+ numbers.push(parseIPv4Number(part))
575
+ }
576
+
577
+ for (let i = 0, n = numbers.length - 1; i < n; i++) {
578
+ if (numbers[i] > 255) {
579
+ throw errors.IPV4_OUT_OF_RANGE_PART()
580
+ }
581
+ }
582
+
583
+ if (numbers[numbers.length - 1] > 256 ** (5 - numbers.length)) {
584
+ throw errors.IPV4_OUT_OF_RANGE_PART()
585
+ }
586
+
587
+ let ipv4 = numbers.pop()
588
+ let counter = 0
589
+
590
+ for (const n of numbers) ipv4 += n * 256 ** (3 - counter++)
591
+
592
+ return ipv4
593
+ }
594
+
595
+ // https://url.spec.whatwg.org/#ipv4-number-parser
596
+ function parseIPv4Number (input) {
597
+ if (input === '') throw errors.IPV4_NON_NUMERIC_PART()
598
+
599
+ input = input.toLowerCase()
600
+
601
+ let R = 10
602
+
603
+ if (input.length >= 2) {
604
+ if (input[0] === '0') {
605
+ if (input[1] === 'x') {
606
+ input.substring(2)
607
+ R = 16
608
+ } else {
609
+ input.substring(1)
610
+ R = 8
611
+ }
612
+ }
613
+ }
614
+
615
+ if (input === '') return 0
616
+
617
+ const n = parseInt(input, R)
618
+
619
+ if (n.toString(R) !== input) throw errors.IPV4_NON_NUMERIC_PART()
620
+
621
+ return n
622
+ }
623
+
624
+ // https://url.spec.whatwg.org/#concept-ipv6-parser
625
+ function parseIPv6 (input) {
626
+ const address = [0, 0, 0, 0, 0, 0, 0, 0]
627
+ let pieceIndex = 0
628
+ let compress = null
629
+ let pointer = 0
630
+
631
+ if (input.charCodeAt(pointer) === 0x3a) {
632
+ if (input.charCodeAt(pointer + 1) !== 0x3a) {
633
+ throw errors.IPV6_INVALID_COMPRESSION()
634
+ }
635
+
636
+ pointer += 2
637
+ compress = ++pieceIndex
638
+ }
639
+
640
+ while (pointer < input.length) {
641
+ if (pieceIndex === 8) throw errors.IPV6_TOO_MANY_PIECES()
642
+
643
+ if (input.charCodeAt(pointer) === 0x3a) {
644
+ if (compress !== null) throw errors.IPV6_MULTIPLE_COMPRESSION()
645
+
646
+ pointer++
647
+ compress = ++pieceIndex
648
+ continue
649
+ }
650
+
651
+ let value = 0
652
+ let length = 0
653
+
654
+ while (length < 4 && infra.isASCIIHexDigit(input.charCodeAt(pointer))) {
655
+ value = value * 0x10 + parseInt(input[pointer], 16)
656
+
657
+ pointer++
658
+ length++
659
+ }
660
+
661
+ if (input.charCodeAt(pointer) === 0x2e) {
662
+ if (length === 0) throw errors.IPV4_IN_IPV6_INVALID_CODE_POINT()
663
+
664
+ pointer -= length
665
+
666
+ if (pieceIndex > 6) throw errors.IPV4_IN_IPV6_TOO_MANY_PIECES()
667
+
668
+ let numbersSeen = 0
669
+
670
+ while (pointer < input.length) {
671
+ let ipv4Piece = null
672
+
673
+ if (numbersSeen > 0) {
674
+ if (input.charCodeAt(pointer) === 0x2e && numbersSeen < 4) {
675
+ pointer++
676
+ } else {
677
+ throw errors.IPV4_IN_IPV6_INVALID_CODE_POINT()
678
+ }
679
+ }
680
+
681
+ if (!infra.isASCIIDigit(input.charCodeAt(pointer))) {
682
+ throw errors.IPV4_IN_IPV6_INVALID_CODE_POINT()
683
+ }
684
+
685
+ while (infra.isASCIIDigit(input.charCodeAt(pointer))) {
686
+ const number = parseInt(input[pointer], 10)
687
+
688
+ if (ipv4Piece === null) {
689
+ ipv4Piece = number
690
+ } else if (ipv4Piece === 0) {
691
+ throw errors.IPV4_IN_IPV6_INVALID_CODE_POINT()
692
+ } else {
693
+ ipv4Piece = ipv4Piece * 10 + number
694
+ }
695
+
696
+ if (ipv4Piece > 255) throw errors.IPV4_IN_IPV6_OUT_OF_RANGE_PART()
697
+
698
+ pointer++
699
+ }
700
+
701
+ address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece
702
+
703
+ numbersSeen++
704
+
705
+ if (numbersSeen === 2 || numbersSeen === 4) pieceIndex++
706
+ }
707
+
708
+ if (numbersSeen !== 4) throw errors.IPV4_IN_IPV6_TOO_FEW_PARTS()
709
+
710
+ break
711
+ }
712
+
713
+ if (input.charCodeAt(pointer) === 0x3a) {
714
+ pointer++
715
+
716
+ if (pointer === input.length) {
717
+ throw errors.IPV6_INVALID_CODE_POINT()
718
+ }
719
+ } else if (pointer !== input.length) {
720
+ throw errors.IPV6_INVALID_CODE_POINT()
721
+ }
722
+
723
+ address[pieceIndex++] = value
724
+ }
725
+
726
+ if (compress !== null) {
727
+ let swaps = pieceIndex - compress
728
+
729
+ pieceIndex = 7
730
+
731
+ while (pieceIndex !== 0 && swaps > 0) {
732
+ [address[pieceIndex], address[compress + swaps - 1]] = [address[compress + swaps - 1], address[pieceIndex]]
733
+
734
+ pieceIndex--
735
+ swaps--
736
+ }
737
+ } else if (pieceIndex !== 8) {
738
+ throw errors.IPV6_TOO_FEW_PIECES()
739
+ }
740
+
741
+ return address
742
+ }
743
+
744
+ // https://url.spec.whatwg.org/#concept-opaque-host-parser
745
+ function parseOpaqueHost (input) {
746
+ const bytes = Buffer.from(input, 'utf8')
747
+
748
+ if (bytes.some(isForbiddenHostCodePoint)) throw errors.HOST_INVALID_CODE_POINT()
749
+
750
+ return percentEncodeAfterEncoding(bytes, c0ControlPercentEncodeSet)
518
751
  }
519
752
 
520
753
  // https://url.spec.whatwg.org/#special-scheme
@@ -605,6 +838,31 @@ function isDoubleDotPathSegment (segment) {
605
838
  return segment === '..' || decodeURIComponent(segment) === '..'
606
839
  }
607
840
 
841
+ // https://url.spec.whatwg.org/#forbidden-host-code-point
842
+ function isForbiddenHostCodePoint (c) {
843
+ return c === 0x0 || c === 0x9 || c === 0xa || c === 0xd || c === 0x20 || c === 0x23 || c === 0x2f || c === 0x3a || c === 0x3c || c === 0x3e || c === 0x3f || c === 0x40 || c === 0x5b || c === 0x5c || c === 0x5d || c === 0x5e || c === 0x7c
844
+ }
845
+
846
+ // https://url.spec.whatwg.org/#forbidden-domain-code-point
847
+ function isForbiddenDomainCodePoint (c) {
848
+ return isForbiddenHostCodePoint(c) || c <= 0x1f || c === 0x25 || c === 0x7f
849
+ }
850
+
851
+ // https://url.spec.whatwg.org/#concept-domain-to-ascii
852
+ function domainToASCII (domain, beStrict) {
853
+ const result = tr46.toASCII(domain, {
854
+ checkHyphens: false,
855
+ checkBidi: true,
856
+ checkJoiners: true,
857
+ useSTD3ASCIIRules: beStrict,
858
+ verifyDNSLength: beStrict
859
+ })
860
+
861
+ if (result === null || result === '') throw errors.DOMAIN_TO_ASCII()
862
+
863
+ return result
864
+ }
865
+
608
866
  // https://url.spec.whatwg.org/#percent-encode
609
867
  function percentEncode (byte) {
610
868
  return `%${byte.toString(16).toUpperCase().padStart(2, '0')}`
@@ -625,6 +883,28 @@ function percentEncodeAfterEncoding (bytes, percentEncodeSet) {
625
883
  return output
626
884
  }
627
885
 
886
+ // https://url.spec.whatwg.org/#percent-decode
887
+ function percentDecode (input) {
888
+ const output = Buffer.alloc(input.byteLength)
889
+ let outputIndex = 0
890
+
891
+ for (let i = 0, n = input.byteLength; i < n; i++) {
892
+ const byte = input[i]
893
+
894
+ if (byte !== 0x25) {
895
+ output[outputIndex++] = byte
896
+ } else if (byte === 0x25 && (!infra.isASCIIHexDigit(input[i + 1]) || !infra.isASCIIHexDigit(input[i + 2]))) {
897
+ output[outputIndex++] = byte
898
+ } else {
899
+ const bytePoint = parseInt(String.fromCodePoint(input[i + 1], input[i + 2]), 16)
900
+ output[outputIndex++] = bytePoint
901
+ i += 2
902
+ }
903
+ }
904
+
905
+ return output.subarray(0, outputIndex)
906
+ }
907
+
628
908
  // https://url.spec.whatwg.org/#string-utf-8-percent-encode
629
909
  function UTF8PercentEncode (input, percentEncodeSet) {
630
910
  return percentEncodeAfterEncoding(Buffer.from(input, 'utf8'), percentEncodeSet)
package/lib/serialize.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // https://url.spec.whatwg.org/#url-serializing
2
- module.exports = function serialize (url, excludeFragment = false) {
2
+ module.exports = exports = function serialize (url, excludeFragment = false) {
3
3
  let output = url.scheme + ':'
4
4
 
5
5
  if (url.host !== null) {
@@ -15,7 +15,7 @@ module.exports = function serialize (url, excludeFragment = false) {
15
15
  output += '@'
16
16
  }
17
17
 
18
- output += url.host
18
+ output += serializeHost(url.host)
19
19
 
20
20
  if (url.port !== null) {
21
21
  output += ':' + url.port.toString(10)
@@ -47,3 +47,52 @@ module.exports = function serialize (url, excludeFragment = false) {
47
47
 
48
48
  return output
49
49
  }
50
+
51
+ // https://url.spec.whatwg.org/#concept-host-serializer
52
+ const serializeHost = exports.host = function serializeHost (host) {
53
+ if (typeof host === 'number') return serializeIPv4(host)
54
+ if (typeof host !== 'string') return '[' + serializeIPv6(host) + ']'
55
+
56
+ return host
57
+ }
58
+
59
+ // https://url.spec.whatwg.org/#concept-ipv4-serializer
60
+ function serializeIPv4 (address) {
61
+ let output = ''
62
+ let n = address
63
+
64
+ for (let i = 1; i <= 4; i++) {
65
+ output = (n % 256).toString(10) + output
66
+
67
+ if (i !== 4) output = '.' + output
68
+
69
+ n = n / 256 | 0
70
+ }
71
+
72
+ return output
73
+ }
74
+
75
+ // https://url.spec.whatwg.org/#concept-ipv6-serializer
76
+ function serializeIPv6 (address) {
77
+ let output = ''
78
+ const compress = 8 // TODO Find first longest 0 sequence
79
+ let ignore0 = false
80
+
81
+ for (let pieceIndex = 0; pieceIndex <= 7; pieceIndex++) {
82
+ if (ignore0 && address[pieceIndex] === 0) continue
83
+
84
+ ignore0 = false
85
+
86
+ if (compress === pieceIndex) {
87
+ output += pieceIndex === 0 ? '::' : ':'
88
+ ignore0 = true
89
+ continue
90
+ }
91
+
92
+ output += address[pieceIndex].toString(16)
93
+
94
+ if (pieceIndex !== 7) output += ':'
95
+ }
96
+
97
+ return output
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-url",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "URL parser for JavaScript",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -22,7 +22,8 @@
22
22
  "homepage": "https://github.com/holepunchto/bare-url",
23
23
  "dependencies": {
24
24
  "bare-os": "^2.0.0",
25
- "bare-path": "^2.0.0"
25
+ "bare-path": "^2.0.0",
26
+ "tr46": "^5.0.0"
26
27
  },
27
28
  "devDependencies": {
28
29
  "brittle": "^3.3.2",