@bufbuild/protovalidate
Version:
Protocol Buffer Validation for ECMAScript
1,257 lines (1,256 loc) • 38.2 kB
JavaScript
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CelUint } from "@bufbuild/cel";
import { scalarEquals } from "@bufbuild/protobuf/reflect";
import { ScalarType } from "@bufbuild/protobuf";
/**
* Returns true if the value is infinite, optionally limit to positive or
* negative infinity.
*/
export function isInf(val, sign) {
sign ?? (sign = 0);
return ((sign >= 0 && val === Number.POSITIVE_INFINITY) ||
(sign <= 0 && val === Number.NEGATIVE_INFINITY));
}
/**
* Returns true if the string is an IPv4 or IPv6 address, optionally limited to
* a specific version.
*
* Version 0 means either 4 or 6. Passing a version other than 0, 4, or 6 always
* returns false.
*
* IPv4 addresses are expected in the dotted decimal format, for example "192.168.5.21".
* IPv6 addresses are expected in their text representation, for example "::1",
* or "2001:0DB8:ABCD:0012::0".
*
* Both formats are well-defined in the internet standard RFC 3986. Zone
* identifiers for IPv6 addresses (for example "fe80::a%en1") are supported.
*/
export function isIp(str, version) {
if (version == 6) {
return new Ipv6(str).address();
}
if (version == 4) {
return new Ipv4(str).address();
}
if (version === undefined || version == 0) {
return new Ipv4(str).address() || new Ipv6(str).address();
}
return false;
}
/**
* Returns true if the string is a valid IP with prefix length, optionally
* limited to a specific version (v4 or v6), and optionally requiring the host
* portion to be all zeros.
*
* An address prefix divides an IP address into a network portion, and a host
* portion. The prefix length specifies how many bits the network portion has.
* For example, the IPv6 prefix "2001:db8:abcd:0012::0/64" designates the
* left-most 64 bits as the network prefix. The range of the network is 2**64
* addresses, from 2001:db8:abcd:0012::0 to 2001:db8:abcd:0012:ffff:ffff:ffff:ffff.
*
* An address prefix may include a specific host address, for example
* "2001:db8:abcd:0012::1f/64". With strict = true, this is not permitted. The
* host portion must be all zeros, as in "2001:db8:abcd:0012::0/64".
*
* The same principle applies to IPv4 addresses. "192.168.1.0/24" designates
* the first 24 bits of the 32-bit IPv4 as the network prefix.
*/
export function isIpPrefix(str, version, strict = false) {
if (version == 6) {
const ip = new Ipv6(str);
return ip.addressPrefix() && (!strict || ip.isPrefixOnly());
}
if (version == 4) {
const ip = new Ipv4(str);
return ip.addressPrefix() && (!strict || ip.isPrefixOnly());
}
if (version === undefined || version == 0) {
return isIpPrefix(str, 6, strict) || isIpPrefix(str, 4, strict);
}
return false;
}
export class Ipv4 {
constructor(str) {
this.i = 0;
this.octets = [];
this.prefixLen = 0;
this.str = str;
this.l = str.length;
}
// Return the 32-bit value of an address parsed through address() or addressPrefix().
// Return 0 if no address was parsed successfully.
getBits() {
if (this.octets.length != 4) {
return 0;
}
return (((this.octets[0] << 24) |
(this.octets[1] << 16) |
(this.octets[2] << 8) |
this.octets[3]) >>>
0);
}
// Return true if all bits to the right of the prefix-length are all zeros.
// Behavior is undefined if addressPrefix() has not been called before, or has
// returned false.
isPrefixOnly() {
const bits = this.getBits();
const mask = this.prefixLen == 32
? 0xffffffff
: ~(0xffffffff >>> this.prefixLen) >>> 0;
const masked = (bits & mask) >>> 0;
return bits == masked;
}
// Parse IPv4 Address in dotted decimal notation.
address() {
return this.addressPart() && this.i == this.l;
}
// Parse IPv4 Address prefix.
addressPrefix() {
return (this.addressPart() &&
this.take("/") &&
this.prefixLength() &&
this.i == this.l);
}
// Stores value in `prefixLen`
prefixLength() {
const start = this.i;
while (this.digit()) {
if (this.i - start > 2) {
// max prefix-length is 32 bits, so anything more than 2 digits is invalid
return false;
}
}
const str = this.str.substring(start, this.i);
if (str.length == 0) {
// too short
return false;
}
if (str.length > 1 && str[0] == "0") {
// bad leading 0
return false;
}
const value = parseInt(str);
if (value > 32) {
// max 32 bits
return false;
}
this.prefixLen = value;
return true;
}
addressPart() {
const start = this.i;
if (this.decOctet() &&
this.take(".") &&
this.decOctet() &&
this.take(".") &&
this.decOctet() &&
this.take(".") &&
this.decOctet()) {
return true;
}
this.i = start;
return false;
}
decOctet() {
const start = this.i;
while (this.digit()) {
if (this.i - start > 3) {
// decimal octet can be three characters at most
return false;
}
}
const str = this.str.substring(start, this.i);
if (str.length == 0) {
// too short
return false;
}
if (str.length > 1 && str[0] == "0") {
// bad leading 0
return false;
}
const value = parseInt(str, 10);
if (value > 255) {
return false;
}
this.octets.push(value);
return true;
}
// DIGIT = %x30-39 ; 0-9
digit() {
const c = this.str[this.i];
if ("0" <= c && c <= "9") {
this.i++;
return true;
}
return false;
}
take(char) {
if (this.str[this.i] == char) {
this.i++;
return true;
}
return false;
}
}
export class Ipv6 {
constructor(str) {
this.i = 0;
this.pieces = []; // 16-bit pieces found
this.doubleColonAt = -1; // number of 16-bit pieces found when double colon was found
this.doubleColonSeen = false;
this.dottedRaw = ""; // dotted notation for right-most 32 bits
this.zoneIdFound = false;
this.prefixLen = 0; // 0 - 128
this.str = str;
this.l = str.length;
}
// Return the 128-bit value of an address parsed through address() or addressPrefix(),
// as a 4-tuple of 32-bit values.
// Return [0,0,0,0] if no address was parsed successfully.
getBits() {
const p16 = this.pieces;
// handle dotted decimal, add to p16
if (this.dottedAddr !== undefined) {
const dotted32 = this.dottedAddr.getBits(); // right-most 32 bits
p16.push(dotted32 >>> 16); // high 16 bits
p16.push(dotted32 & (0xffff >>> 0)); // low 16 bits
}
// handle double colon, fill pieces with 0
if (this.doubleColonSeen) {
while (p16.length < 8) {
// delete 0 entries at pos, insert a 0
p16.splice(this.doubleColonAt, 0, 0x00000000);
}
}
if (p16.length != 8) {
return [0, 0, 0, 0];
}
return [
((p16[0] << 16) | p16[1]) >>> 0,
((p16[2] << 16) | p16[3]) >>> 0,
((p16[4] << 16) | p16[5]) >>> 0,
((p16[6] << 16) | p16[7]) >>> 0,
];
}
// Return true if all bits to the right of the prefix-length are all zeros.
// Behavior is undefined if addressPrefix() has not been called before, or has
// returned false.
isPrefixOnly() {
// For each 32-bit piece of the address, require that values to the right of the prefix are zero
for (const [i, p32] of this.getBits().entries()) {
const len = this.prefixLen - 32 * i;
const mask = len >= 32
? 0xffffffff
: len < 0
? 0x00000000
: ~(0xffffffff >>> len) >>> 0;
const masked = (p32 & mask) >>> 0;
if (p32 !== masked) {
return false;
}
}
return true;
}
// Parse IPv6 Address following RFC 4291, with optional zone id following RFC 4007.
address() {
return this.addressPart() && this.i == this.l;
}
// Parse IPv6 Address Prefix following RFC 4291. Zone id is not permitted.
addressPrefix() {
return (this.addressPart() &&
!this.zoneIdFound &&
this.take("/") &&
this.prefixLength() &&
this.i == this.l);
}
// Stores value in `prefixLen`
prefixLength() {
const start = this.i;
while (this.digit()) {
if (this.i - start > 3) {
return false;
}
}
const str = this.str.substring(start, this.i);
if (str.length == 0) {
// too short
return false;
}
if (str.length > 1 && str[0] == "0") {
// bad leading 0
return false;
}
const value = parseInt(str, 10);
if (value > 128) {
// max 128 bits
return false;
}
this.prefixLen = value;
return true;
}
// Stores dotted notation for right-most 32 bits in `dottedRaw` / `dottedAddr` if found.
addressPart() {
while (this.i < this.l) {
// dotted notation for right-most 32 bits, e.g. 0:0:0:0:0:ffff:192.1.56.10
if ((this.doubleColonSeen || this.pieces.length == 6) && this.dotted()) {
const dotted = new Ipv4(this.dottedRaw);
if (dotted.address()) {
this.dottedAddr = dotted;
return true;
}
return false;
}
const result = this.h16();
if (result === "error") {
return false;
}
if (result) {
continue;
}
if (this.take(":")) {
if (this.take(":")) {
if (this.doubleColonSeen) {
return false;
}
this.doubleColonSeen = true;
this.doubleColonAt = this.pieces.length;
if (this.take(":")) {
return false;
}
}
else {
if (this.i === 1 || this.i === this.str.length) {
// invalid - string cannot start or end on single colon
return false;
}
}
continue;
}
if (this.str[this.i] == "%" && !this.zoneId()) {
return false;
}
break;
}
if (this.doubleColonSeen) {
return this.pieces.length < 8;
}
return this.pieces.length == 8;
}
// Parses the rule from RFC 6874:
//
// RFC 6874: ZoneID = 1*( unreserved / pct-encoded )
//
// There is no definition for the character set allowed in the zone
// identifier. RFC 4007 permits basically any non-null string.
zoneId() {
const start = this.i;
if (this.take("%")) {
if (this.l - this.i > 0) {
// permit any non-null string
this.i = this.l;
this.zoneIdFound = true;
return true;
}
}
this.i = start;
this.zoneIdFound = false;
return false;
}
// Parses the rule:
//
// 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
//
// Stores match in `dottedRaw`.
dotted() {
const start = this.i;
this.dottedRaw = "";
for (;;) {
if (this.digit() || this.take(".")) {
continue;
}
break;
}
if (this.i - start >= 7) {
this.dottedRaw = this.str.substring(start, this.i);
return true;
}
this.i = start;
return false;
}
// Parses the rule:
//
// h16 = 1*4HEXDIG
//
// If 1-4 hex digits are found, the parsed 16-bit unsigned integer is stored in `pieces` and true is returned.
// If 0 hex digits are found, returns false.
// If more than 4 hex digits are found, "error" is returned.
//
h16() {
const start = this.i;
while (this.hexdig()) {
// continue
}
const str = this.str.substring(start, this.i);
if (str.length == 0) {
// too short
// this is not an error condition, it just means we didn't find any
// hex digits at the current position.
return false;
}
if (str.length > 4) {
// too long
// this is an error condition, it means we found a string of more than
// four valid hex digits, which is invalid in ipv6 addresses.
return "error";
}
this.pieces.push(parseInt(str, 16));
return true;
}
// Parses the rule:
//
// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
hexdig() {
const c = this.str[this.i];
if (("0" <= c && c <= "9") ||
("a" <= c && c <= "f") ||
("A" <= c && c <= "F") ||
("0" <= c && c <= "9")) {
this.i++;
return true;
}
return false;
}
// Parses the rule:
//
// DIGIT = %x30-39 ; 0-9
digit() {
const c = this.str[this.i];
if ("0" <= c && c <= "9") {
this.i++;
return true;
}
return false;
}
take(char) {
if (this.str[this.i] == char) {
this.i++;
return true;
}
return false;
}
}
/**
* Returns true if the string is a valid hostname, for example "foo.example.com".
*
* A valid hostname follows the rules below:
* - The name consists of one or more labels, separated by a dot (".").
* - Each label can be 1 to 63 alphanumeric characters.
* - A label can contain hyphens ("-"), but must not start or end with a hyphen.
* - The right-most label must not be digits only.
* - The name can have a trailing dot, for example "foo.example.com.".
* - The name can be 253 characters at most, excluding the optional trailing dot.
*/
export function isHostname(str) {
if (str.length > 253) {
return false;
}
const s = str.endsWith(".") ? str.substring(0, str.length - 1) : str;
let allDigits = false;
// split hostname on '.' and validate each part
for (const part of s.split(".")) {
allDigits = true;
// if part is empty, longer than 63 chars, or starts/ends with '-', it is invalid
const l = part.length;
if (l == 0 || l > 63 || part.startsWith("-") || part.endsWith("-")) {
return false;
}
// for each character in part
for (const ch of part.split("")) {
// if the character is not a-z, A-Z, 0-9, or '-', it is invalid
if ((ch < "a" || ch > "z") &&
(ch < "A" || ch > "Z") &&
(ch < "0" || ch > "9") &&
ch != "-") {
return false;
}
allDigits = allDigits && ch >= "0" && ch <= "9";
}
}
// the last part cannot be all numbers
return !allDigits;
}
/**
* Returns true if the string is a valid host/port pair, for example "example.com:8080".
*
* If the argument `portRequired` is true, the port is required. If the argument
* is false, the port is optional.
*
* The host can be one of:
* - An IPv4 address in dotted decimal format, for example "192.168.0.1".
* - An IPv6 address enclosed in square brackets, for example "[::1]".
* - A hostname, for example "example.com".
*
* The port is separated by a colon. It must be non-empty, with a decimal number
* in the range of 0-65535, inclusive.
*/
export function isHostAndPort(str, portRequired) {
if (str.length == 0) {
return false;
}
const splitIdx = str.lastIndexOf(":");
if (str[0] == "[") {
const end = str.lastIndexOf("]");
switch (end + 1) {
case str.length: // no port
return !portRequired && isIp(str.substring(1, end), 6);
case splitIdx: // port
return (isIp(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)));
default: // malformed
return false;
}
}
if (splitIdx < 0) {
return !portRequired && (isHostname(str) || isIp(str, 4));
}
const host = str.substring(0, splitIdx);
const port = str.substring(splitIdx + 1);
return (isHostname(host) || isIp(host, 4)) && isPort(port);
}
function isPort(str) {
if (str.length == 0) {
return false;
}
for (let i = 0; i < str.length; i++) {
const c = str[i];
if ("0" <= c && c <= "9") {
continue;
}
return false;
}
if (str.length > 1 && str[0] === "0") {
return false;
}
return parseInt(str) <= 65535;
}
/**
* Returns true if the string is an email address, for example "foo@example.com".
*
* Conforms to the definition for a valid email address from the HTML standard.
* Note that this standard willfully deviates from RFC 5322, which allows many
* unexpected forms of email addresses and will easily match a typographical
* error.
*/
export function isEmail(str) {
// See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(str);
}
/**
* Returns true if the string is a URI, for example "https://example.com/foo/bar?baz=quux#frag".
*
* URI is defined in the internet standard RFC 3986.
* Zone Identifiers in IPv6 address literals are supported (RFC 6874).
*/
export function isUri(str) {
return new Uri(str).uri();
}
/**
* Returns true if the string is a URI Reference - a URI such as "https://example.com/foo/bar?baz=quux#frag",
* or a Relative Reference such as "./foo/bar?query".
*
* URI, URI Reference, and Relative Reference are defined in the internet
* standard RFC 3986. Zone Identifiers in IPv6 address literals are supported
* (RFC 6874).
*/
export function isUriRef(str) {
return new Uri(str).uriReference();
}
class Uri {
constructor(str) {
this.i = 0;
this.pctEncodedFound = false;
this.str = str;
this.l = str.length;
}
// Parses the rule:
//
// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
uri() {
const start = this.i;
if (!(this.scheme() && this.take(":") && this.hierPart())) {
this.i = start;
return false;
}
if (this.take("?") && !this.query()) {
return false;
}
if (this.take("#") && !this.fragment()) {
return false;
}
if (this.i != this.l) {
this.i = start;
return false;
}
return true;
}
/// Parses the rule:
//
// hier-part = "//" authority path-abempty
// / path-absolute
// / path-rootless
// / path-empty
hierPart() {
const start = this.i;
if (this.take("/") &&
this.take("/") &&
this.authority() &&
this.pathAbempty()) {
return true;
}
this.i = start;
return this.pathAbsolute() || this.pathRootless() || this.pathEmpty();
}
// Parses the rule:
//
// URI-reference = URI / relative-ref
uriReference() {
return this.uri() || this.relativeRef();
}
// Parses the rule:
//
// relative-ref = relative-part [ "?" query ] [ "#" fragment ]
relativeRef() {
const start = this.i;
if (!this.relativePart()) {
return false;
}
if (this.take("?") && !this.query()) {
this.i = start;
return false;
}
if (this.take("#") && !this.fragment()) {
this.i = start;
return false;
}
if (this.i != this.l) {
this.i = start;
return false;
}
return true;
}
// Parses the rule:
//
// relative-part = "//" authority path-abempty
// / path-absolute
// / path-noscheme
// / path-empty
relativePart() {
const start = this.i;
if (this.take("/") &&
this.take("/") &&
this.authority() &&
this.pathAbempty()) {
return true;
}
this.i = start;
return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty();
}
// Parses the rule:
//
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
//
// Terminated by ":".
scheme() {
const start = this.i;
if (this.alpha()) {
while (this.alpha() ||
this.digit() ||
this.take("+") ||
this.take("-") ||
this.take(".")) {
// continue
}
if (this.str[this.i] == ":") {
return true;
}
}
this.i = start;
return false;
}
// Parses the rule:
//
// authority = [ userinfo "@" ] host [ ":" port ]
//
// Lead by double slash ("") and terminated by "/", "?", "#", or end of URI.
authority() {
const start = this.i;
if (this.userinfo()) {
if (!this.take("@")) {
this.i = start;
return false;
}
}
if (!this.host()) {
this.i = start;
return false;
}
if (this.take(":")) {
if (!this.port()) {
this.i = start;
return false;
}
}
if (!this.isAuthorityEnd()) {
this.i = start;
return false;
}
return true;
}
// > The authority component [...] is terminated by the next slash ("/"),
// > question mark ("?"), or number sign ("#") character, or by the
// > end of the URI.
isAuthorityEnd() {
return (this.str[this.i] == "?" ||
this.str[this.i] == "#" ||
this.str[this.i] == "/" ||
this.i >= this.l);
}
// Parses the rule:
//
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
//
// Terminated by "@" in authority.
userinfo() {
const start = this.i;
for (;;) {
if (this.unreserved() ||
this.pctEncoded() ||
this.subDelims() ||
this.take(":")) {
continue;
}
if (this.str[this.i] == "@") {
return true;
}
this.i = start;
return false;
}
}
// Parses the rule:
//
// host = IP-literal / IPv4address / reg-name
host() {
const start = this.i;
this.pctEncodedFound = false;
// Note: IPv4address is a subset of reg-name
if ((this.str[this.i] == "[" && this.ipLiteral()) || this.regName()) {
if (this.pctEncodedFound) {
const rawHost = this.str.substring(start, this.i);
// RFC 3986:
// > URI producing applications must not use percent-encoding in host
// > unless it is used to represent a UTF-8 character sequence.
try {
// decodeURIComponent() throws an error if a pct-encoded escape
// sequence does not encode a valid UTF-8 character.
// Other implementations may have to implement this check themselves.
// For example:
// - Decode pct-encoded rawHost
// - Allocate an octet array
// - For every octet in rawHost
// - For "%", percent-decode the following two hex digits to an
// octet, add it to the octet array
// - For every other octet, add it to the octet array
// - Check that the octet array is valid UTF-8
decodeURIComponent(rawHost);
}
catch (_) {
return false;
}
}
return true;
}
return false;
}
// Parses the rule:
//
// port = *DIGIT
//
// Terminated by end of authority.
port() {
const start = this.i;
for (;;) {
if (this.digit()) {
continue;
}
if (this.isAuthorityEnd()) {
return true;
}
this.i = start;
return false;
}
}
// Parses the rule from RFC 6874:
//
// IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]"
ipLiteral() {
const start = this.i;
if (this.take("[")) {
const j = this.i;
if (this.ipv6Address() && this.take("]")) {
return true;
}
this.i = j;
if (this.ipv6addrz() && this.take("]")) {
return true;
}
this.i = j;
if (this.ipvFuture() && this.take("]")) {
return true;
}
}
this.i = start;
return false;
}
// Parses the rule "IPv6address".
// Relies on the implementation of isIp().
ipv6Address() {
const start = this.i;
while (this.hexdig() || this.take(":")) {
// continue
}
if (isIp(this.str.substring(start, this.i), 6)) {
return true;
}
this.i = start;
return false;
}
// Parses the rule from RFC 6874:
//
// IPv6addrz = IPv6address "%25" ZoneID
ipv6addrz() {
const start = this.i;
if (this.ipv6Address() &&
this.take("%") &&
this.take("2") &&
this.take("5") &&
this.zoneId()) {
return true;
}
this.i = start;
return false;
}
// Parses the rule from RFC 6874:
//
// ZoneID = 1*( unreserved / pct-encoded )
zoneId() {
const start = this.i;
while (this.unreserved() || this.pctEncoded()) {
// continue
}
if (this.i - start > 0) {
return true;
}
this.i = start;
return false;
}
// Parses the rule:
//
// IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
ipvFuture() {
const start = this.i;
if (this.take("v") && this.hexdig()) {
while (this.hexdig()) {
// continue
}
if (this.take(".")) {
let j = 0;
while (this.unreserved() || this.subDelims() || this.take(":")) {
j++;
}
if (j >= 1) {
return true;
}
}
}
this.i = start;
return false;
}
// Parses the rule:
//
// reg-name = *( unreserved / pct-encoded / sub-delims )
//
// Terminates on start of port (":") or end of authority.
regName() {
const start = this.i;
for (;;) {
if (this.unreserved() || this.pctEncoded() || this.subDelims()) {
continue;
}
if (this.str[this.i] == ":") {
return true;
}
if (this.isAuthorityEnd()) {
// End of authority
return true;
}
this.i = start;
return false;
}
}
// > The path is terminated by the first question mark ("?") or
// > number sign ("#") character, or by the end of the URI.
isPathEnd() {
return (this.str[this.i] == "?" || this.str[this.i] == "#" || this.i >= this.l);
}
// Parses the rule:
//
// path-abempty = *( "/" segment )
//
// Terminated by end of path: "?", "#", or end of URI.
pathAbempty() {
const start = this.i;
while (this.take("/") && this.segment()) {
// continue
}
if (this.isPathEnd()) {
return true;
}
this.i = start;
return false;
}
// Parses the rule:
//
// path-absolute = "/" [ segment-nz *( "/" segment ) ]
//
// Terminated by end of path: "?", "#", or end of URI.
pathAbsolute() {
const start = this.i;
if (this.take("/")) {
if (this.segmentNz()) {
while (this.take("/") && this.segment()) {
// continue
}
}
if (this.isPathEnd()) {
return true;
}
}
this.i = start;
return false;
}
// Parses the rule:
//
// path-noscheme = segment-nz-nc *( "/" segment )
//
// Terminated by end of path: "?", "#", or end of URI.
pathNoscheme() {
const start = this.i;
if (this.segmentNzNc()) {
while (this.take("/") && this.segment()) {
// continue
}
if (this.isPathEnd()) {
return true;
}
}
this.i = start;
return false;
}
// Parses the rule:
//
// path-rootless = segment-nz *( "/" segment )
//
// Terminated by end of path: "?", "#", or end of URI.
pathRootless() {
const start = this.i;
if (this.segmentNz()) {
while (this.take("/") && this.segment()) {
// continue
}
if (this.isPathEnd()) {
return true;
}
}
this.i = start;
return false;
}
// Parses the rule:
//
// path-empty = 0<pchar>
//
// Terminated by end of path: "?", "#", or end of URI.
pathEmpty() {
return this.isPathEnd();
}
// Parses the rule:
//
// segment = *pchar
segment() {
while (this.pchar()) {
// continue
}
return true;
}
// Parses the rule:
//
// segment-nz = 1*pchar
segmentNz() {
const start = this.i;
if (this.pchar()) {
while (this.pchar()) {
// continue
}
return true;
}
this.i = start;
return false;
}
// Parses the rule:
//
// segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
// ; non-zero-length segment without any colon ":"
segmentNzNc() {
const start = this.i;
while (this.unreserved() ||
this.pctEncoded() ||
this.subDelims() ||
this.take("@")) {
// continue
}
if (this.i - start > 0) {
return true;
}
this.i = start;
return false;
}
// Parses the rule:
//
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
pchar() {
return (this.unreserved() ||
this.pctEncoded() ||
this.subDelims() ||
this.take(":") ||
this.take("@"));
}
// Parses the rule:
//
// query = *( pchar / "/" / "?" )
//
// Terminated by "#" or end of URI.
query() {
const start = this.i;
for (;;) {
if (this.pchar() || this.take("/") || this.take("?")) {
continue;
}
if (this.str[this.i] == "#" || this.i == this.l) {
return true;
}
this.i = start;
return false;
}
}
// Parses the rule:
//
// fragment = *( pchar / "/" / "?" )
//
// Terminated by end of URI.
fragment() {
const start = this.i;
for (;;) {
if (this.pchar() || this.take("/") || this.take("?")) {
continue;
}
if (this.i == this.l) {
return true;
}
this.i = start;
return false;
}
}
// Parses the rule:
//
// pct-encoded = "%" HEXDIG HEXDIG
//
// Sets `pctEncodedFound` to true if a valid triplet was found
pctEncoded() {
const start = this.i;
if (this.take("%") && this.hexdig() && this.hexdig()) {
this.pctEncodedFound = true;
return true;
}
this.i = start;
return false;
}
// Parses the rule:
//
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
unreserved() {
return (this.alpha() ||
this.digit() ||
this.take("-") ||
this.take("_") ||
this.take(".") ||
this.take("~"));
}
// Parses the rule:
//
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
// / "*" / "+" / "," / ";" / "="
subDelims() {
return (this.take("!") ||
this.take("$") ||
this.take("&") ||
this.take("'") ||
this.take("(") ||
this.take(")") ||
this.take("*") ||
this.take("+") ||
this.take(",") ||
this.take(";") ||
this.take("="));
}
// Parses the rule:
//
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
alpha() {
const c = this.str[this.i];
if (("A" <= c && c <= "Z") || ("a" <= c && c <= "z")) {
this.i++;
return true;
}
return false;
}
// Parses the rule:
//
// DIGIT = %x30-39 ; 0-9
digit() {
const c = this.str[this.i];
if ("0" <= c && c <= "9") {
this.i++;
return true;
}
return false;
}
// Parses the rule:
//
// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
hexdig() {
const c = this.str[this.i];
if (("0" <= c && c <= "9") ||
("a" <= c && c <= "f") ||
("A" <= c && c <= "F") ||
("0" <= c && c <= "9")) {
this.i++;
return true;
}
return false;
}
take(char) {
if (this.str[this.i] == char) {
this.i++;
return true;
}
return false;
}
}
/**
* Returns true if the array only contains values that are distinct from each
* other by strict comparison.
*/
export function unique(list) {
return list.getItems().every((a, index, arr) => {
if (a instanceof CelUint) {
for (let i = 0; i < arr.length; i++) {
if (i == index) {
continue;
}
const b = arr[i];
if (b instanceof CelUint && b.value === a.value) {
return false;
}
}
return true;
}
if (a instanceof Uint8Array) {
for (let i = 0; i < arr.length; i++) {
if (i == index) {
continue;
}
const b = arr[i];
if (b instanceof Uint8Array && scalarEquals(ScalarType.BYTES, b, a)) {
return false;
}
}
return true;
}
return arr.indexOf(a) === index;
});
}
/**
* Returns true if argument bytes contains argument sub.
*/
export function bytesContains(bytes, sub) {
if (sub.length === 0) {
return true;
}
if (sub.length > bytes.length) {
return false;
}
for (let i = 0; i < bytes.length - sub.length + 1; i++) {
let found = true;
for (let j = 0; j < sub.length; j++) {
if (bytes[i + j] != sub[j]) {
found = false;
break;
}
}
if (found) {
return true;
}
}
return false;
}
/**
* Returns true if argument bytes starts with argument sub.
*/
export function bytesStartsWith(bytes, sub) {
if (sub.length > bytes.length) {
return false;
}
for (let i = 0; i < sub.length; i++) {
if (sub[i] != bytes[i]) {
return false;
}
}
return true;
}
/**
* Returns true if argument bytes ends with argument sub.
*/
export function bytesEndsWith(bytes, sub) {
if (sub.length > bytes.length) {
return false;
}
if (bytes.length < sub.length) {
return false;
}
for (let i = 0; i < sub.length; i++) {
if (sub[sub.length - i - 1] != bytes[bytes.length - i - 1]) {
return false;
}
}
return true;
}