@amritk/nish-x86_64-linux 0.24.0 → 0.25.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.
- package/bin/nish +0 -0
- package/docs/AI.md +24 -4
- package/package.json +1 -1
- package/runtime/browser.mjs +4 -3
- package/runtime/core/host.d.ts +8 -0
- package/runtime/core/host.mjs +50 -2
- package/runtime/nish.d.ts +100 -0
- package/runtime/nish.h +34 -0
- package/runtime/nish.mjs +21 -0
- package/runtime/node.mjs +57 -6
- package/runtime/runtime-net.c +44 -3
- package/runtime/runtime-os.c +115 -12
- package/scripts/build.sh +13 -3
- package/std/README.md +25 -8
- package/std/cli-args.ts +1216 -0
- package/std/cli-flags.ts +585 -0
- package/std/cli.ts +308 -19
- package/std/compress/checksum.ts +132 -20
- package/std/compress/deflate.ts +638 -248
- package/std/compress/gzip.ts +20 -6
- package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
- package/std/crypto/p256.ts +63 -1
- package/std/crypto/p384.ts +6044 -0
- package/std/crypto/pki-roots.ts +322 -0
- package/std/crypto/pki.ts +1207 -0
- package/std/crypto/rsa.ts +663 -0
- package/std/crypto/x509.ts +1054 -58
- package/std/net/dns-config.ts +734 -0
- package/std/net/dns-message.ts +517 -0
- package/std/net/dns-resolver.ts +1015 -0
- package/std/net/dns.ts +478 -0
- package/std/net/http1-client.ts +1227 -0
- package/std/net/http1-server.ts +7 -2
- package/std/net/http1.ts +9 -9
- package/std/net/tls/client.ts +1094 -0
- package/std/net/tls/codec.ts +585 -3
- package/std/net/tls/record-client.ts +1192 -0
- package/std/net/tls/verify.ts +221 -0
- package/std/net/tls-tcp.ts +642 -11
- package/std/net/tls.ts +2 -2
- package/std/net/url.ts +559 -4
- 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
|
|
58
|
-
*
|
|
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
|
+
}
|