370 lines
12 KiB
JavaScript
370 lines
12 KiB
JavaScript
// Copyright 2021-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 { ScalarType, } from "../descriptors.js";
|
|
/**
|
|
* Create a PathBuilder.
|
|
*/
|
|
export function buildPath(schema) {
|
|
return new PathBuilderImpl(schema, schema, []);
|
|
}
|
|
/**
|
|
* Parse a Path from a string.
|
|
*
|
|
* Throws an InvalidPathError if the path is invalid.
|
|
*
|
|
* Note that a Registry must be provided via the options argument to parse
|
|
* paths that refer to an extension.
|
|
*/
|
|
export function parsePath(schema, path, options) {
|
|
var _a, _b;
|
|
const builder = new PathBuilderImpl(schema, schema, []);
|
|
const err = (message, i) => new InvalidPathError(schema, message + " at column " + (i + 1), path);
|
|
for (let i = 0; i < path.length;) {
|
|
const token = nextToken(i, path);
|
|
const left = builder.getLeft();
|
|
let right = undefined;
|
|
if ("field" in token) {
|
|
right =
|
|
(left === null || left === void 0 ? void 0 : left.kind) != "message"
|
|
? undefined
|
|
: ((_a = left.fields.find((field) => field.name === token.field)) !== null && _a !== void 0 ? _a : left.oneofs.find((oneof) => oneof.name === token.field));
|
|
if (!right) {
|
|
throw err(`Unknown field "${token.field}"`, i);
|
|
}
|
|
}
|
|
else if ("ext" in token) {
|
|
right = (_b = options === null || options === void 0 ? void 0 : options.registry) === null || _b === void 0 ? void 0 : _b.getExtension(token.ext);
|
|
if (!right) {
|
|
throw err(`Unknown extension "${token.ext}"`, i);
|
|
}
|
|
}
|
|
else if ("val" in token) {
|
|
// list or map
|
|
right =
|
|
(left === null || left === void 0 ? void 0 : left.kind) == "field" &&
|
|
left.fieldKind == "list" &&
|
|
typeof token.val == "bigint"
|
|
? { kind: "list_sub", index: Number(token.val) }
|
|
: { kind: "map_sub", key: token.val };
|
|
}
|
|
else if ("err" in token) {
|
|
throw err(token.err, token.i);
|
|
}
|
|
if (right) {
|
|
try {
|
|
builder.add([right]);
|
|
}
|
|
catch (e) {
|
|
throw err(e instanceof InvalidPathError ? e.message : String(e), i);
|
|
}
|
|
}
|
|
i = token.i;
|
|
}
|
|
return builder.toPath();
|
|
}
|
|
/**
|
|
* Stringify a path.
|
|
*/
|
|
export function pathToString(path) {
|
|
const str = [];
|
|
for (const ele of path) {
|
|
switch (ele.kind) {
|
|
case "field":
|
|
case "oneof":
|
|
if (str.length > 0) {
|
|
str.push(".");
|
|
}
|
|
str.push(ele.name);
|
|
break;
|
|
case "extension":
|
|
str.push("[", ele.typeName, "]");
|
|
break;
|
|
case "list_sub":
|
|
str.push("[", ele.index, "]");
|
|
break;
|
|
case "map_sub":
|
|
if (typeof ele.key == "string") {
|
|
str.push('["', ele.key
|
|
.split("\\")
|
|
.join("\\\\")
|
|
.split('"')
|
|
.join('\\"')
|
|
.split("\r")
|
|
.join("\\r")
|
|
.split("\n")
|
|
.join("\\n"), '"]');
|
|
}
|
|
else {
|
|
str.push("[", ele.key, "]");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return str.join("");
|
|
}
|
|
/**
|
|
* InvalidPathError is thrown for invalid Paths, for example during parsing from
|
|
* a string, or when a new Path is built.
|
|
*/
|
|
export class InvalidPathError extends Error {
|
|
constructor(schema, message, path) {
|
|
super(message);
|
|
this.name = "InvalidPathError";
|
|
this.schema = schema;
|
|
this.path = path;
|
|
// see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example
|
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
}
|
|
}
|
|
class PathBuilderImpl {
|
|
constructor(schema, left, path) {
|
|
this.schema = schema;
|
|
this.left = left;
|
|
this.path = path;
|
|
}
|
|
getLeft() {
|
|
return this.left;
|
|
}
|
|
field(field) {
|
|
return this.push(field);
|
|
}
|
|
oneof(oneof) {
|
|
return this.push(oneof);
|
|
}
|
|
extension(extension) {
|
|
return this.push(extension);
|
|
}
|
|
list(index) {
|
|
return this.push({ kind: "list_sub", index });
|
|
}
|
|
map(key) {
|
|
return this.push({ kind: "map_sub", key });
|
|
}
|
|
add(pathOrBuilder) {
|
|
const path = Array.isArray(pathOrBuilder)
|
|
? pathOrBuilder
|
|
: pathOrBuilder.toPath();
|
|
const l = this.path.length;
|
|
try {
|
|
for (const ele of path) {
|
|
this.push(ele);
|
|
}
|
|
}
|
|
catch (e) {
|
|
// undo pushes
|
|
this.path.splice(l);
|
|
throw e;
|
|
}
|
|
return this;
|
|
}
|
|
toPath() {
|
|
return this.path.concat();
|
|
}
|
|
clone() {
|
|
return new PathBuilderImpl(this.schema, this.left, this.path.concat());
|
|
}
|
|
push(ele) {
|
|
switch (ele.kind) {
|
|
case "field":
|
|
if (!this.left ||
|
|
this.left.kind != "message" ||
|
|
this.left.typeName != ele.parent.typeName) {
|
|
throw this.err("field access");
|
|
}
|
|
this.path.push(ele);
|
|
this.left =
|
|
ele.fieldKind == "message"
|
|
? ele.message
|
|
: ele.fieldKind == "list" || ele.fieldKind == "map"
|
|
? ele
|
|
: undefined;
|
|
return this;
|
|
case "oneof":
|
|
if (!this.left ||
|
|
this.left.kind != "message" ||
|
|
this.left.typeName != ele.parent.typeName) {
|
|
throw this.err("oneof access");
|
|
}
|
|
this.path.push(ele);
|
|
this.left = undefined;
|
|
return this;
|
|
case "extension":
|
|
if (!this.left ||
|
|
this.left.kind != "message" ||
|
|
this.left.typeName != ele.extendee.typeName) {
|
|
throw this.err("extension access");
|
|
}
|
|
this.path.push(ele);
|
|
this.left = ele.fieldKind == "message" ? ele.message : undefined;
|
|
return this;
|
|
case "list_sub":
|
|
if (!this.left ||
|
|
this.left.kind != "field" ||
|
|
this.left.fieldKind != "list") {
|
|
throw this.err("list access");
|
|
}
|
|
if (ele.index < 0 || !Number.isInteger(ele.index)) {
|
|
throw this.err("list index");
|
|
}
|
|
this.path.push(ele);
|
|
this.left =
|
|
this.left.listKind == "message" ? this.left.message : undefined;
|
|
return this;
|
|
case "map_sub":
|
|
if (!this.left ||
|
|
this.left.kind != "field" ||
|
|
this.left.fieldKind != "map") {
|
|
throw this.err("map access");
|
|
}
|
|
if (!checkKeyType(ele.key, this.left.mapKey)) {
|
|
throw this.err("map key");
|
|
}
|
|
this.path.push(ele);
|
|
this.left =
|
|
this.left.mapKind == "message" ? this.left.message : undefined;
|
|
return this;
|
|
}
|
|
}
|
|
err(what) {
|
|
return new InvalidPathError(this.schema, "Invalid " + what, this.path);
|
|
}
|
|
}
|
|
function checkKeyType(key, type) {
|
|
switch (type) {
|
|
case ScalarType.STRING:
|
|
return typeof key == "string";
|
|
case ScalarType.INT32:
|
|
case ScalarType.UINT32:
|
|
case ScalarType.SINT32:
|
|
case ScalarType.SFIXED32:
|
|
case ScalarType.FIXED32:
|
|
return typeof key == "number";
|
|
case ScalarType.UINT64:
|
|
case ScalarType.INT64:
|
|
case ScalarType.FIXED64:
|
|
case ScalarType.SFIXED64:
|
|
case ScalarType.SINT64:
|
|
return typeof key == "bigint";
|
|
case ScalarType.BOOL:
|
|
return typeof key == "boolean";
|
|
}
|
|
}
|
|
function nextToken(i, path) {
|
|
const re_extension = /^[A-Za-z_][A-Za-z_0-9]*(?:\.[A-Za-z_][A-Za-z_0-9]*)*$/;
|
|
const re_field = /^[A-Za-z_][A-Za-z_0-9]*$/;
|
|
if (path[i] == "[") {
|
|
i++;
|
|
while (path[i] == " ") {
|
|
// skip leading whitespace
|
|
i++;
|
|
}
|
|
if (i >= path.length) {
|
|
return { err: "Premature end", i: path.length - 1 };
|
|
}
|
|
let token;
|
|
if (path[i] == `"`) {
|
|
// string literal
|
|
i++;
|
|
let val = "";
|
|
for (;;) {
|
|
if (path[i] == `"`) {
|
|
// end of string literal
|
|
i++;
|
|
break;
|
|
}
|
|
if (path[i] == "\\") {
|
|
switch (path[i + 1]) {
|
|
case `"`:
|
|
case "\\":
|
|
val += path[i + 1];
|
|
break;
|
|
case "r":
|
|
val += "\r";
|
|
break;
|
|
case "n":
|
|
val += "\n";
|
|
break;
|
|
default:
|
|
return { err: "Invalid escape sequence", i };
|
|
}
|
|
i++;
|
|
}
|
|
else {
|
|
val += path[i];
|
|
}
|
|
if (i >= path.length) {
|
|
return { err: "Premature end of string", i: path.length - 1 };
|
|
}
|
|
i++;
|
|
}
|
|
token = { val };
|
|
}
|
|
else if (path[i].match(/\d/)) {
|
|
// integer literal
|
|
const start = i;
|
|
while (i < path.length && /\d/.test(path[i])) {
|
|
i++;
|
|
}
|
|
token = { val: BigInt(path.substring(start, i)) };
|
|
}
|
|
else if (path[i] == "]") {
|
|
return { err: "Premature ]", i };
|
|
}
|
|
else {
|
|
// extension identifier or bool literal
|
|
const start = i;
|
|
while (i < path.length && path[i] != " " && path[i] != "]") {
|
|
i++;
|
|
}
|
|
const name = path.substring(start, i);
|
|
if (name === "true") {
|
|
token = { val: true };
|
|
}
|
|
else if (name === "false") {
|
|
token = { val: false };
|
|
}
|
|
else if (re_extension.test(name)) {
|
|
token = { ext: name };
|
|
}
|
|
else {
|
|
return { err: "Invalid ident", i: start };
|
|
}
|
|
}
|
|
while (path[i] == " ") {
|
|
// skip trailing whitespace
|
|
i++;
|
|
}
|
|
if (path[i] != "]") {
|
|
return { err: "Missing ]", i };
|
|
}
|
|
i++;
|
|
return Object.assign(Object.assign({}, token), { i });
|
|
}
|
|
// field identifier
|
|
if (i > 0) {
|
|
if (path[i] != ".") {
|
|
return { err: `Expected "."`, i };
|
|
}
|
|
i++;
|
|
}
|
|
const start = i;
|
|
while (i < path.length && path[i] != "." && path[i] != "[") {
|
|
i++;
|
|
}
|
|
const field = path.substring(start, i);
|
|
return re_field.test(field)
|
|
? { field, i }
|
|
: { err: "Invalid ident", i: start };
|
|
}
|