@amritk/nish-x86_64-linux 0.24.0 → 0.26.0

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.
Files changed (43) hide show
  1. package/bin/nish +0 -0
  2. package/docs/AI.md +45 -4
  3. package/package.json +1 -1
  4. package/runtime/browser.mjs +4 -3
  5. package/runtime/core/host.d.ts +12 -0
  6. package/runtime/core/host.mjs +74 -2
  7. package/runtime/nish.d.ts +148 -0
  8. package/runtime/nish.h +59 -0
  9. package/runtime/nish.mjs +25 -0
  10. package/runtime/node.mjs +136 -6
  11. package/runtime/runtime-net.c +44 -3
  12. package/runtime/runtime-os.c +351 -18
  13. package/scripts/build.sh +13 -3
  14. package/std/README.md +27 -10
  15. package/std/cli-args.ts +1216 -0
  16. package/std/cli-flags.ts +585 -0
  17. package/std/cli.ts +308 -19
  18. package/std/compress/checksum.ts +132 -20
  19. package/std/compress/deflate.ts +638 -248
  20. package/std/compress/gzip.ts +20 -6
  21. package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
  22. package/std/crypto/p256.ts +63 -1
  23. package/std/crypto/p384.ts +6044 -0
  24. package/std/crypto/pki-roots.ts +322 -0
  25. package/std/crypto/pki.ts +1207 -0
  26. package/std/crypto/rsa.ts +663 -0
  27. package/std/crypto/x509.ts +1054 -58
  28. package/std/net/dns-config.ts +734 -0
  29. package/std/net/dns-message.ts +517 -0
  30. package/std/net/dns-resolver.ts +1015 -0
  31. package/std/net/dns.ts +478 -0
  32. package/std/net/http1-client.ts +1227 -0
  33. package/std/net/http1-server.ts +7 -2
  34. package/std/net/http1.ts +9 -9
  35. package/std/net/postgres.ts +259 -26
  36. package/std/net/tls/client.ts +1094 -0
  37. package/std/net/tls/codec.ts +585 -3
  38. package/std/net/tls/record-client.ts +1192 -0
  39. package/std/net/tls/verify.ts +221 -0
  40. package/std/net/tls-tcp.ts +642 -11
  41. package/std/net/tls.ts +2 -2
  42. package/std/net/url.ts +559 -4
  43. package/std/yaml-write.ts +415 -0
package/std/net/tls.ts CHANGED
@@ -410,7 +410,7 @@ export class TlsP256Signer {
410
410
  }
411
411
 
412
412
  /** Whether an x25519 shared secret is all zeros, reading every byte (run inside `expose`). */
413
- const tlsIsZeroSecret = (shared: u8[]): boolean => timingSafeEqual(shared, new Array<u8>(X25519_SIZE))
413
+ export const tlsIsZeroSecret = (shared: u8[]): boolean => timingSafeEqual(shared, new Array<u8>(X25519_SIZE))
414
414
 
415
415
  /**
416
416
  * `Derive-Secret(early, "derived", "")` with no PSK, the salt a full handshake's
@@ -438,7 +438,7 @@ const tlsHandshakeFromEcdhe256 = (ecdhe: u8[], salt: u8[]): u8[] => hmacSha256(s
438
438
  const tlsHandshakeFromEcdhe384 = (ecdhe: u8[], salt: u8[]): u8[] => hmacSha384(salt, ecdhe)
439
439
 
440
440
  /** Wipes the bytes a `Secret` holds, for `exposeWith` to run on one secret of a holder outside its `wipe()`; answers `answer`. */
441
- const tlsZeroHeld = (held: u8[], answer: i32): i32 => {
441
+ export const tlsZeroHeld = (held: u8[], answer: i32): i32 => {
442
442
  wipe(held)
443
443
  return answer
444
444
  }
package/std/net/url.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * `nish/net/url` — the request target's path and query, split and
3
- * percent-decoded in place, and its query string read as key/value pairs.
3
+ * percent-decoded in place, and its query string read as key/value pairs; and
4
+ * whole URLs, parsed into their parts and references resolved against them.
4
5
  *
5
6
  * A handler that routes on a path or reads `?name=…` from a query otherwise
6
7
  * writes its own byte scan, and each such scan decides differently what `%zz`,
@@ -54,9 +55,30 @@
54
55
  * pairs (`a&&b`, a leading or trailing `&`) are skipped and not counted, as
55
56
  * `URLSearchParams` skips them.
56
57
  *
57
- * **A fragment is not part of a request target** (RFC 9112 §3.2), so nothing
58
- * here looks for a `#`: in a target that holds one, it is a byte of the path
59
- * or of the last value.
58
+ * **A fragment is not part of a request target** (RFC 9112 §3.2), so the byte
59
+ * API never looks for a `#`: in a target that holds one, it is a byte of the
60
+ * path or of the last value.
61
+ *
62
+ * **A whole URL is parsed strictly, and nothing in it is decoded.** A client
63
+ * that is handed `http://…` by a user, a redirect or a link needs its host and
64
+ * port before it connects, and the place each reference points to:
65
+ *
66
+ * import { Url, urlOrigin, urlParse, urlResolve, urlSerialize } from "nish/net/url";
67
+ *
68
+ * const base: Url | null = urlParse("https://example.com/a/b?q")
69
+ * if (base !== null) {
70
+ * const next: Url | null = urlResolve(base, "../c#top") // https://example.com/c#top
71
+ * // next.host, next.port (-1: the default), urlOrigin(next) === "https://example.com:443"
72
+ * }
73
+ *
74
+ * `urlParse` and `urlResolve` follow RFC 3986 (§3 for the parts, §5.2 to
75
+ * resolve, dot segments removed) and refuse, with null, what two readers could
76
+ * take for different hosts: a byte outside printable ASCII, a backslash, a
77
+ * malformed `%`, a second `@`, a percent-encoded or otherwise non-unreserved
78
+ * host byte, a host that looks numeric but is not a strict dotted-decimal IPv4
79
+ * address (`127.1`, `0x7f.0.0.1`, `2130706433`), an IPv6 zone or IPvFuture
80
+ * literal, and a port past 65535. The scheme and host are lower-cased; every
81
+ * other part is kept as written, still percent-encoded.
60
82
  *
61
83
  * The record of what this module refuses and why is `docs/security/url.md`.
62
84
  */
@@ -569,3 +591,536 @@ export const urlQueryGet = (target: string, name: string): string | null => {
569
591
  }
570
592
  return urlDecodeString(bytes, query.valueStart, query.valueLength, URL_DECODE_FORM)
571
593
  }
594
+
595
+ const URL_HASH: i32 = 35
596
+ const URL_DOT: i32 = 46
597
+ const URL_COLON: i32 = 58
598
+ const URL_AT: i32 = 64
599
+ const URL_OPEN_BRACKET: i32 = 91
600
+ const URL_CLOSE_BRACKET: i32 = 93
601
+ const URL_BACKSLASH: i32 = 92
602
+ /** The largest port a URL may name (RFC 6335 §6: a port is 16 bits). */
603
+ const URL_MAX_PORT: i32 = 65535
604
+
605
+ /**
606
+ * A URL split into its parts (RFC 3986 §3). Each part is held as it was
607
+ * written, still percent-encoded, except that `scheme` and `host` are
608
+ * lower-cased. `urlParse` and `urlResolve` make one; `urlSerialize` writes one
609
+ * back.
610
+ */
611
+ export class Url {
612
+ /** Lower-cased, without its `:`: `http`. */
613
+ scheme: string = ""
614
+ /** The userinfo before its first `:`, as written; empty when there is none. */
615
+ username: string = ""
616
+ /** The userinfo after its first `:`, as written; empty when there is none. */
617
+ password: string = ""
618
+ /** A lower-cased reg-name, a dotted IPv4 address, or an IPv6 address without its brackets. */
619
+ host: string = ""
620
+ /** With its dot segments removed; empty, or starting with `/` when there is an authority. */
621
+ path: string = ""
622
+ /** Without its `?`, or null when there is no `?`. */
623
+ query: string | null = null
624
+ /** Without its `#`, or null when there is no `#`. */
625
+ fragment: string | null = null
626
+ /** 0 to 65535, or -1 when the URL names none; `urlOrigin` supplies the scheme's default. */
627
+ port: i32 = -1
628
+ /** True when the URL has a `//` authority; `mailto:a@b` has none and `file:///x` has an empty one. */
629
+ hasAuthority: boolean = false
630
+ }
631
+
632
+ /** `bytes[a, b)` as a string with A to Z lower-cased. */
633
+ const urlLowerString = (bytes: u8[], a: i32, b: i32): string => {
634
+ const out: u8[] = new Array<u8>(b - a)
635
+ let i: i32 = a
636
+ while (i >= 0 && i < b && i < toI32(bytes.length) && i - a < toI32(out.length)) {
637
+ const c: i32 = toI32(bytes[i])
638
+ out[i - a] = c >= 65 && c <= 90 ? toU8(c + 32) : toU8(c)
639
+ i += 1
640
+ }
641
+ return String.fromBytes(out, 0, b - a)
642
+ }
643
+
644
+ /** The first `c` in `bytes[a, b)`, or `b`. */
645
+ const urlFind = (bytes: u8[], a: i32, b: i32, c: i32): i32 => {
646
+ let i: i32 = a
647
+ while (i >= 0 && i < b && i < toI32(bytes.length) && toI32(bytes[i]) !== c) {
648
+ i += 1
649
+ }
650
+ return i
651
+ }
652
+
653
+ const urlIsAlpha = (c: i32): boolean => (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
654
+
655
+ const urlIsDigit = (c: i32): boolean => c >= 48 && c <= 57
656
+
657
+ /** RFC 3986's unreserved set: what a host may hold. */
658
+ const urlIsUnreserved = (c: i32): boolean =>
659
+ urlIsAlpha(c) || urlIsDigit(c) || c === 45 || c === URL_DOT || c === 95 || c === 126
660
+
661
+ /**
662
+ * True when `bytes[a, b)` is a dotted-decimal IPv4 address exactly as RFC
663
+ * 3986 §3.2.2 writes one: four parts of one to three digits, each at most 255,
664
+ * with no leading zero.
665
+ */
666
+ const urlIsIpv4 = (bytes: u8[], a: i32, b: i32): boolean => {
667
+ let parts: i32 = 0
668
+ let i: i32 = a
669
+ while (parts < 4) {
670
+ const start: i32 = i
671
+ let value: i32 = 0
672
+ while (i >= 0 && i < b && i < toI32(bytes.length) && i - start < 4 && urlIsDigit(toI32(bytes[i]))) {
673
+ value = value * 10 + toI32(bytes[i]) - 48
674
+ i += 1
675
+ }
676
+ const digits: i32 = i - start
677
+ if (
678
+ digits === 0 ||
679
+ digits > 3 ||
680
+ value > 255 ||
681
+ (digits > 1 && start >= 0 && start < toI32(bytes.length) && toI32(bytes[start]) === 48)
682
+ ) {
683
+ return false
684
+ }
685
+ parts += 1
686
+ if (parts < 4) {
687
+ if (i < 0 || i >= b || i >= toI32(bytes.length) || toI32(bytes[i]) !== URL_DOT) {
688
+ return false
689
+ }
690
+ i += 1
691
+ }
692
+ }
693
+ return i === b
694
+ }
695
+
696
+ /**
697
+ * True when `bytes[a, b)` is an IPv6 address as RFC 4291 §2.2 writes one:
698
+ * eight groups of one to four hex digits, or fewer with one `::` standing for
699
+ * the rest, the last two optionally a dotted IPv4 address. A zone (`%25eth0`)
700
+ * and an IPvFuture literal (`v1.x`) are not.
701
+ */
702
+ const urlIsIpv6 = (bytes: u8[], a: i32, b: i32): boolean => {
703
+ let groups: i32 = 0
704
+ let elided: boolean = false
705
+ let i: i32 = a
706
+ if (b - a >= 2 && toI32(bytes[a]) === URL_COLON && toI32(bytes[a + 1]) === URL_COLON) {
707
+ elided = true
708
+ i = a + 2
709
+ }
710
+ while (i < b) {
711
+ const start: i32 = i
712
+ while (i >= 0 && i < b && i < toI32(bytes.length) && urlHexValue(toI32(bytes[i])) >= 0) {
713
+ i += 1
714
+ }
715
+ if (i === start) {
716
+ return false
717
+ }
718
+ if (i >= 0 && i < b && i < toI32(bytes.length) && toI32(bytes[i]) === URL_DOT) {
719
+ if (!urlIsIpv4(bytes, start, b)) {
720
+ return false
721
+ }
722
+ groups += 2
723
+ i = b
724
+ } else {
725
+ if (i - start > 4) {
726
+ return false
727
+ }
728
+ groups += 1
729
+ if (i >= 0 && i < b && i < toI32(bytes.length)) {
730
+ if (toI32(bytes[i]) !== URL_COLON || i + 1 === b) {
731
+ return false
732
+ }
733
+ i += 1
734
+ if (i < toI32(bytes.length) && toI32(bytes[i]) === URL_COLON) {
735
+ if (elided) {
736
+ return false
737
+ }
738
+ elided = true
739
+ i += 1
740
+ }
741
+ }
742
+ }
743
+ }
744
+ return elided ? groups <= 7 : groups === 8
745
+ }
746
+
747
+ /**
748
+ * True when a reg-name `bytes[a, b)` would be read as a number by a resolver
749
+ * or by the WHATWG host parser: its last label (a trailing `.` aside) is all
750
+ * digits or `0x` and hex. Such a host must be a dotted-decimal IPv4 address
751
+ * exactly, so `127.1`, `0x7f.0.0.1` and `017700000001` never reach a lookup
752
+ * that reads them as 127.0.0.1.
753
+ */
754
+ const urlLooksNumeric = (bytes: u8[], a: i32, b: i32): boolean => {
755
+ let end: i32 = b
756
+ if (end > a && toI32(bytes[end - 1]) === URL_DOT) {
757
+ end -= 1
758
+ }
759
+ let start: i32 = end
760
+ while (start > a && toI32(bytes[start - 1]) !== URL_DOT) {
761
+ start -= 1
762
+ }
763
+ if (start === end) {
764
+ return false
765
+ }
766
+ let i: i32 = start
767
+ if (end - start >= 2 && toI32(bytes[start]) === 48 && (toI32(bytes[start + 1]) | 32) === 120) {
768
+ i = start + 2
769
+ while (i >= 0 && i < end && i < toI32(bytes.length) && urlHexValue(toI32(bytes[i])) >= 0) {
770
+ i += 1
771
+ }
772
+ return i === end
773
+ }
774
+ while (i >= 0 && i < end && i < toI32(bytes.length) && urlIsDigit(toI32(bytes[i]))) {
775
+ i += 1
776
+ }
777
+ return i === end
778
+ }
779
+
780
+ /** Reads the authority `bytes[a, b)` into `u`, or answers false when it breaks a rule `urlParse` names. */
781
+ const urlReadAuthority = (bytes: u8[], a: i32, b: i32, u: Url): boolean => {
782
+ const at: i32 = urlFind(bytes, a, b, URL_AT)
783
+ let h: i32 = a
784
+ if (at < b) {
785
+ if (urlFind(bytes, at + 1, b, URL_AT) < b) {
786
+ return false
787
+ }
788
+ if (urlFind(bytes, a, at, URL_OPEN_BRACKET) < at || urlFind(bytes, a, at, URL_CLOSE_BRACKET) < at) {
789
+ return false
790
+ }
791
+ const colon: i32 = urlFind(bytes, a, at, URL_COLON)
792
+ u.username = String.fromBytes(bytes, a, colon)
793
+ if (colon < at) {
794
+ u.password = String.fromBytes(bytes, colon + 1, at)
795
+ }
796
+ h = at + 1
797
+ }
798
+ let portAt: i32 = b
799
+ if (h < b && toI32(bytes[h]) === URL_OPEN_BRACKET) {
800
+ const close: i32 = urlFind(bytes, h, b, URL_CLOSE_BRACKET)
801
+ if (close === b || !urlIsIpv6(bytes, h + 1, close)) {
802
+ return false
803
+ }
804
+ u.host = urlLowerString(bytes, h + 1, close)
805
+ if (close + 1 < b) {
806
+ if (toI32(bytes[close + 1]) !== URL_COLON) {
807
+ return false
808
+ }
809
+ portAt = close + 1
810
+ }
811
+ } else {
812
+ portAt = urlFind(bytes, h, b, URL_COLON)
813
+ let i: i32 = h
814
+ while (i >= 0 && i < portAt && i < toI32(bytes.length) && urlIsUnreserved(toI32(bytes[i]))) {
815
+ i += 1
816
+ }
817
+ if (i < portAt) {
818
+ return false
819
+ }
820
+ if (urlLooksNumeric(bytes, h, portAt) && !urlIsIpv4(bytes, h, portAt)) {
821
+ return false
822
+ }
823
+ u.host = urlLowerString(bytes, h, portAt)
824
+ }
825
+ if (portAt + 1 < b) {
826
+ let port: i32 = 0
827
+ let i: i32 = portAt + 1
828
+ while (i >= 0 && i < b && i < toI32(bytes.length)) {
829
+ const c: i32 = toI32(bytes[i])
830
+ if (!urlIsDigit(c)) {
831
+ return false
832
+ }
833
+ port = port * 10 + c - 48
834
+ if (port > URL_MAX_PORT) {
835
+ return false
836
+ }
837
+ i += 1
838
+ }
839
+ u.port = port
840
+ }
841
+ return true
842
+ }
843
+
844
+ /**
845
+ * `s` split into a `Url` whose scheme is empty when `s` is a relative
846
+ * reference, with its path as written; null when it breaks one of the rules
847
+ * `urlParse` names, the host a scheme with a default port needs aside, which
848
+ * only a resolved URL is held to.
849
+ */
850
+ const urlReadReference = (s: string): Url | null => {
851
+ const bytes: u8[] | null = urlBytesOf(s)
852
+ if (bytes === null) {
853
+ return null
854
+ }
855
+ const n: i32 = toI32(bytes.length)
856
+ let i: i32 = 0
857
+ while (i < n) {
858
+ const c: i32 = toI32(bytes[i])
859
+ // A WHATWG parser reads `\` as `/` under http(s), so it would end the
860
+ // authority or start one where this reading does not: refused everywhere.
861
+ if (
862
+ c <= URL_SPACE ||
863
+ c >= 127 ||
864
+ c === URL_BACKSLASH ||
865
+ (c === URL_PERCENT && urlEscapeAt(bytes, i, n) < 0)
866
+ ) {
867
+ return null
868
+ }
869
+ i += 1
870
+ }
871
+ const u: Url = new Url()
872
+ const hash: i32 = urlFind(bytes, 0, n, URL_HASH)
873
+ if (hash < n) {
874
+ u.fragment = String.fromBytes(bytes, hash + 1, n)
875
+ }
876
+ const question: i32 = urlFind(bytes, 0, hash, URL_QUESTION)
877
+ if (question < hash) {
878
+ u.query = String.fromBytes(bytes, question + 1, hash)
879
+ }
880
+ let pos: i32 = 0
881
+ if (question > 0 && urlIsAlpha(toI32(bytes[0]))) {
882
+ let j: i32 = 1
883
+ while (j < question) {
884
+ const c: i32 = toI32(bytes[j])
885
+ if (!(urlIsAlpha(c) || urlIsDigit(c) || c === URL_PLUS || c === 45 || c === URL_DOT)) {
886
+ break
887
+ }
888
+ j += 1
889
+ }
890
+ if (j < question && toI32(bytes[j]) === URL_COLON) {
891
+ u.scheme = urlLowerString(bytes, 0, j)
892
+ pos = j + 1
893
+ }
894
+ }
895
+ if (question - pos >= 2 && toI32(bytes[pos]) === URL_SLASH && toI32(bytes[pos + 1]) === URL_SLASH) {
896
+ const end: i32 = urlFind(bytes, pos + 2, question, URL_SLASH)
897
+ u.hasAuthority = true
898
+ if (!urlReadAuthority(bytes, pos + 2, end, u)) {
899
+ return null
900
+ }
901
+ pos = end
902
+ }
903
+ u.path = String.fromBytes(bytes, pos, question)
904
+ return u
905
+ }
906
+
907
+ /** The width of a `.` (1) or `%2e` (3) at `s[i]`, or 0. */
908
+ const urlDotAt = (s: string, i: i32, end: i32): i32 => {
909
+ if (i < end && toI32(s.charCodeAt(i)) === URL_DOT) {
910
+ return 1
911
+ }
912
+ if (
913
+ end - i >= 3 &&
914
+ toI32(s.charCodeAt(i)) === URL_PERCENT &&
915
+ toI32(s.charCodeAt(i + 1)) === 50 &&
916
+ (toI32(s.charCodeAt(i + 2)) | 32) === 101
917
+ ) {
918
+ return 3
919
+ }
920
+ return 0
921
+ }
922
+
923
+ /** 1 when the segment `s[a, b)` is `.`, 2 when it is `..`, either written with `%2e`, else 0. */
924
+ const urlDotSegment = (s: string, a: i32, b: i32): i32 => {
925
+ const first: i32 = urlDotAt(s, a, b)
926
+ if (first === 0) {
927
+ return 0
928
+ }
929
+ if (a + first === b) {
930
+ return 1
931
+ }
932
+ const second: i32 = urlDotAt(s, a + first, b)
933
+ if (second > 0 && a + first + second === b) {
934
+ return 2
935
+ }
936
+ return 0
937
+ }
938
+
939
+ /** The first `/` in `s[i, n)`, or `n`. */
940
+ const urlSlashFrom = (s: string, i: i32, n: i32): i32 => {
941
+ let j: i32 = i
942
+ while (j < n && toI32(s.charCodeAt(j)) !== URL_SLASH) {
943
+ j += 1
944
+ }
945
+ return j
946
+ }
947
+
948
+ /**
949
+ * RFC 3986 §5.2.4 remove_dot_segments, reading `%2e` as `.` the way the WHATWG
950
+ * URL Standard does, so `/a/%2e%2e/b` is `/b` here and at a server that
951
+ * decodes before it resolves. The output is kept as its segments, each with
952
+ * the `/` before it, so "remove the last segment" is a `pop`.
953
+ */
954
+ const urlRemoveDotSegments = (s: string): string => {
955
+ const n: i32 = toI32(s.length)
956
+ const out: string[] = []
957
+ let i: i32 = 0
958
+ while (i >= 0 && i < n) {
959
+ if (toI32(s.charCodeAt(i)) !== URL_SLASH) {
960
+ const j: i32 = urlSlashFrom(s, i, n)
961
+ if (urlDotSegment(s, i, j) > 0) {
962
+ i = j < n ? j + 1 : n
963
+ } else {
964
+ out.push(s.slice(i, j))
965
+ i = j
966
+ }
967
+ } else {
968
+ const j: i32 = urlSlashFrom(s, i + 1, n)
969
+ const dots: i32 = urlDotSegment(s, i + 1, j)
970
+ if (dots === 0) {
971
+ out.push(s.slice(i, j))
972
+ } else if (dots === 2 && out.length > 0) {
973
+ out.pop()
974
+ }
975
+ if (dots > 0 && j === n) {
976
+ out.push("/")
977
+ }
978
+ i = j
979
+ }
980
+ }
981
+ return out.join("")
982
+ }
983
+
984
+ /** `u` when a URL with its scheme may stand as it is, or null: a scheme with a default port needs a host. */
985
+ const urlFinish = (u: Url): Url | null => {
986
+ if (urlDefaultPort(u.scheme) >= 0 && u.host === "") {
987
+ return null
988
+ }
989
+ return u
990
+ }
991
+
992
+ /**
993
+ * The absolute URL `s` split into its parts, or null when it is not one this
994
+ * module accepts. `s` is refused when it has no scheme, holds a byte outside
995
+ * printable ASCII (a control byte, a space, DEL or anything past 0x7F), a
996
+ * backslash or a `%` not followed by two hex digits, or its authority has more
997
+ * than one `@`, a host with a byte outside RFC 3986's unreserved set (so a
998
+ * percent-encoded one), a host whose last label is numeric but that is not a
999
+ * dotted-decimal IPv4 address, a malformed IPv6 literal or a port that is not
1000
+ * digits or is above 65535. An http, https, ws or wss URL needs a non-empty
1001
+ * host. The path's dot segments are removed, as RFC 3986 §5.2.2 removes them
1002
+ * from a reference with a scheme. Nothing is decoded. It allocates its answer.
1003
+ */
1004
+ export const urlParse = (s: string): Url | null => {
1005
+ const u: Url | null = urlReadReference(s)
1006
+ if (u === null || u.scheme === "") {
1007
+ return null
1008
+ }
1009
+ u.path = urlRemoveDotSegments(u.path)
1010
+ return urlFinish(u)
1011
+ }
1012
+
1013
+ /** RFC 3986 §5.2.3: `path` merged onto `base`'s directory. */
1014
+ const urlMerge = (base: Url, path: string): string => {
1015
+ if (base.hasAuthority && base.path === "") {
1016
+ return "/" + path
1017
+ }
1018
+ let k: i32 = toI32(base.path.length)
1019
+ while (k > 0 && toI32(base.path.charCodeAt(k - 1)) !== URL_SLASH) {
1020
+ k -= 1
1021
+ }
1022
+ return base.path.slice(0, k) + path
1023
+ }
1024
+
1025
+ /**
1026
+ * The reference `ref` resolved against the absolute URL `base` by RFC 3986
1027
+ * §5.2.2, strictly (`http:g` is `http:g`, not relative), or null when `ref`
1028
+ * breaks a rule `urlParse` names or what it resolves to is an http, https, ws
1029
+ * or wss URL without a host. `base`'s fragment is never carried over. It
1030
+ * allocates its answer and does not change `base`.
1031
+ */
1032
+ export const urlResolve = (base: Url, ref: string): Url | null => {
1033
+ const r: Url | null = urlReadReference(ref)
1034
+ if (r === null) {
1035
+ return null
1036
+ }
1037
+ if (r.scheme === "") {
1038
+ r.scheme = base.scheme
1039
+ if (!r.hasAuthority) {
1040
+ r.hasAuthority = base.hasAuthority
1041
+ r.username = base.username
1042
+ r.password = base.password
1043
+ r.host = base.host
1044
+ r.port = base.port
1045
+ if (r.path === "") {
1046
+ r.path = base.path
1047
+ if (r.query === null) {
1048
+ r.query = base.query
1049
+ }
1050
+ return urlFinish(r)
1051
+ }
1052
+ if (toI32(r.path.charCodeAt(0)) !== URL_SLASH) {
1053
+ r.path = urlMerge(base, r.path)
1054
+ }
1055
+ }
1056
+ }
1057
+ r.path = urlRemoveDotSegments(r.path)
1058
+ return urlFinish(r)
1059
+ }
1060
+
1061
+ /** `u`'s host as a URL writes it: an IPv6 address, the one host with a `:`, in brackets. */
1062
+ const urlHostText = (u: Url): string => (u.host.indexOf(":") >= 0 ? "[" + u.host + "]" : u.host)
1063
+
1064
+ /**
1065
+ * `u` written back as RFC 3986 §5.3 recomposes it, an IPv6 host in brackets.
1066
+ * The userinfo is written when either half is non-empty. A path that starts
1067
+ * with `//` under no authority is written after `/.`, as the WHATWG URL
1068
+ * Standard writes it, so the string does not read back with an authority.
1069
+ */
1070
+ export const urlSerialize = (u: Url): string => {
1071
+ const parts: string[] = []
1072
+ if (u.scheme !== "") {
1073
+ parts.push(u.scheme + ":")
1074
+ }
1075
+ if (u.hasAuthority) {
1076
+ parts.push("//")
1077
+ if (u.username !== "" || u.password !== "") {
1078
+ parts.push(u.password !== "" ? u.username + ":" + u.password + "@" : u.username + "@")
1079
+ }
1080
+ parts.push(urlHostText(u))
1081
+ if (u.port >= 0) {
1082
+ parts.push(`:${u.port}`)
1083
+ }
1084
+ } else if (u.path.startsWith("//")) {
1085
+ parts.push("/.")
1086
+ }
1087
+ parts.push(u.path)
1088
+ const query: string | null = u.query
1089
+ if (query !== null) {
1090
+ parts.push("?" + query)
1091
+ }
1092
+ const fragment: string | null = u.fragment
1093
+ if (fragment !== null) {
1094
+ parts.push("#" + fragment)
1095
+ }
1096
+ return parts.join("")
1097
+ }
1098
+
1099
+ /** The port a scheme uses when a URL names none: 80 for `http` and `ws`, 443 for `https` and `wss`, else -1. */
1100
+ export const urlDefaultPort = (scheme: string): i32 => {
1101
+ if (scheme === "http" || scheme === "ws") {
1102
+ return 80
1103
+ }
1104
+ if (scheme === "https" || scheme === "wss") {
1105
+ return 443
1106
+ }
1107
+ return -1
1108
+ }
1109
+
1110
+ /**
1111
+ * `u`'s origin as `scheme://host:port`, the port always written (the scheme's
1112
+ * default when `u` names none), so origins compare as plain strings:
1113
+ * `http://A.com/x` and `http://a.com:80/y` are both `http://a.com:80`. Null for
1114
+ * a scheme other than http, https, ws or wss, which has no origin to compare.
1115
+ * Hosts are compared as written, lower-cased, so `[::1]` and `[0::1]` are
1116
+ * different origins, and so are `a.com.` and `a.com`: equal strings are the
1117
+ * same origin, and a split errs toward "different".
1118
+ */
1119
+ export const urlOrigin = (u: Url): string | null => {
1120
+ const fallback: i32 = urlDefaultPort(u.scheme)
1121
+ if (fallback < 0) {
1122
+ return null
1123
+ }
1124
+ const port: i32 = u.port >= 0 ? u.port : fallback
1125
+ return `${u.scheme}://${urlHostText(u)}:${port}`
1126
+ }