Switching to new security rules
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
296
src/rules/compile.ts
Normal file
296
src/rules/compile.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import {
|
||||
Node,
|
||||
MatchStatement,
|
||||
Operations,
|
||||
ServiceStatement,
|
||||
Expression,
|
||||
ValueStatement,
|
||||
Operators,
|
||||
} from "./parser";
|
||||
|
||||
export class CompilerError extends Error {
|
||||
node: Node;
|
||||
constructor(message: string, node: Node) {
|
||||
super(message);
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
|
||||
type Variables = { [key: string]: string | Variables };
|
||||
|
||||
class Variable {
|
||||
#name: string;
|
||||
constructor(name: string) {
|
||||
this.#name = name;
|
||||
}
|
||||
|
||||
getValue(variables: Variables) {
|
||||
const parts = this.#name.split(".");
|
||||
let current = variables as any;
|
||||
while (parts.length > 0) {
|
||||
const name = parts.shift();
|
||||
if (current && typeof current == "object") current = current[name];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
class Value {
|
||||
#value: any;
|
||||
constructor(value: any) {
|
||||
this.#value = value;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.#value;
|
||||
}
|
||||
}
|
||||
|
||||
type ConditionParameters = Value | ConditionMatcher | Variable;
|
||||
class ConditionMatcher {
|
||||
#left: ConditionParameters;
|
||||
#right: ConditionParameters;
|
||||
#operator: Operators;
|
||||
constructor(
|
||||
left: ConditionParameters,
|
||||
right: ConditionParameters,
|
||||
operator: Operators
|
||||
) {
|
||||
this.#left = left;
|
||||
this.#right = right;
|
||||
this.#operator = operator;
|
||||
}
|
||||
|
||||
test(variables: Variables): boolean {
|
||||
let leftValue: any;
|
||||
if (this.#left instanceof Value) {
|
||||
leftValue = this.#left.value;
|
||||
} else if (this.#left instanceof Variable) {
|
||||
leftValue = this.#left.getValue(variables);
|
||||
} else {
|
||||
leftValue = this.#left.test(variables);
|
||||
}
|
||||
|
||||
let rightValue: any;
|
||||
if (this.#right instanceof Value) {
|
||||
rightValue = this.#right.value;
|
||||
} else if (this.#right instanceof Variable) {
|
||||
rightValue = this.#right.getValue(variables);
|
||||
} else {
|
||||
rightValue = this.#right.test(variables);
|
||||
}
|
||||
|
||||
switch (this.#operator) {
|
||||
case "==":
|
||||
return leftValue == rightValue;
|
||||
case "!=":
|
||||
return leftValue != rightValue;
|
||||
case ">=":
|
||||
return leftValue >= rightValue;
|
||||
case "<=":
|
||||
return leftValue <= rightValue;
|
||||
case ">":
|
||||
return leftValue > rightValue;
|
||||
case "<":
|
||||
return leftValue < rightValue;
|
||||
case "&&":
|
||||
return leftValue && rightValue;
|
||||
case "||":
|
||||
return leftValue || rightValue;
|
||||
default:
|
||||
throw new Error("Invalid operator " + this.#operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Rule {
|
||||
#operation: Operations;
|
||||
|
||||
#condition: ConditionParameters;
|
||||
|
||||
get operation() {
|
||||
return this.#operation;
|
||||
}
|
||||
|
||||
constructor(operation: Operations, condition: ConditionParameters) {
|
||||
this.#operation = operation;
|
||||
this.#condition = condition;
|
||||
}
|
||||
|
||||
test(variables: Variables): boolean {
|
||||
if (this.#condition instanceof Value) {
|
||||
return Boolean(this.#condition.value);
|
||||
} else if (this.#condition instanceof Variable) {
|
||||
return Boolean(this.#condition.getValue(variables));
|
||||
} else {
|
||||
return this.#condition.test(variables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Segment {
|
||||
#name: string;
|
||||
#variable: boolean;
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
constructor(name: string, variable = false) {
|
||||
this.#name = name;
|
||||
this.#variable = variable;
|
||||
}
|
||||
|
||||
match(segment: string): { match: boolean; variable?: string } {
|
||||
return {
|
||||
match: this.#name === segment || this.#variable,
|
||||
variable: this.#variable && this.#name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Match {
|
||||
#submatches: Match[];
|
||||
#rules: Rule[];
|
||||
|
||||
#segments: Segment[];
|
||||
#wildcard: boolean;
|
||||
|
||||
constructor(
|
||||
segments: Segment[],
|
||||
rules: Rule[],
|
||||
wildcard: boolean,
|
||||
submatches: Match[]
|
||||
) {
|
||||
this.#segments = segments;
|
||||
this.#rules = rules;
|
||||
this.#wildcard = wildcard;
|
||||
this.#submatches = submatches;
|
||||
}
|
||||
|
||||
match(
|
||||
segments: string[],
|
||||
operation: Operations,
|
||||
variables: Variables
|
||||
): boolean {
|
||||
let localVars = { ...variables };
|
||||
if (segments.length >= this.#segments.length) {
|
||||
for (let i = 0; i < this.#segments.length; i++) {
|
||||
const match = this.#segments[i].match(segments[i]);
|
||||
if (match.match) {
|
||||
if (match.variable) {
|
||||
localVars[match.variable] = segments[i];
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let remaining = segments.slice(this.#segments.length);
|
||||
if (remaining.length > 0 && !this.#wildcard) {
|
||||
for (const match of this.#submatches) {
|
||||
const res = match.match(remaining, operation, localVars);
|
||||
if (res) return true;
|
||||
}
|
||||
} else {
|
||||
for (const rule of this.#rules) {
|
||||
console.log(rule.operation, operation);
|
||||
if (rule.operation === operation) {
|
||||
if (rule.test(localVars)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class RuleRunner {
|
||||
#root_matches: Match[];
|
||||
constructor(root_matches: Match[]) {
|
||||
this.#root_matches = root_matches;
|
||||
}
|
||||
|
||||
hasPermission(path: string[], operation: Operations, request: any): boolean {
|
||||
if (request.root) return true;
|
||||
for (const match of this.#root_matches) {
|
||||
const res = match.match(path, operation, { request });
|
||||
if (res) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuleRunner(service: ServiceStatement) {
|
||||
const createMatch = (s_match: MatchStatement) => {
|
||||
let wildcard = false;
|
||||
let segments = s_match.path.segments
|
||||
.map((segment, idx, arr) => {
|
||||
if (typeof segment === "string") {
|
||||
if (segment === "*") {
|
||||
if (idx === arr.length - 1) {
|
||||
wildcard = true;
|
||||
return null;
|
||||
} else {
|
||||
throw new CompilerError("Invalid path wildcard!", s_match);
|
||||
}
|
||||
} else {
|
||||
return new Segment(segment, false);
|
||||
}
|
||||
} else {
|
||||
return new Segment(segment.name, true);
|
||||
}
|
||||
})
|
||||
.filter((e) => e !== null);
|
||||
|
||||
const resolveParameter = (e: Expression | ValueStatement) => {
|
||||
let val: Value | ConditionMatcher | Variable;
|
||||
if (e.type === "value") {
|
||||
const c = e;
|
||||
if (c.isFalse) {
|
||||
val = new Value(false);
|
||||
} else if (c.isTrue) {
|
||||
val = new Value(true);
|
||||
} else if (c.isNull) {
|
||||
val = new Value(null);
|
||||
} else if (c.isNumber) {
|
||||
val = new Value(Number(c.value));
|
||||
} else if (c.isString) {
|
||||
val = new Value(String(c.value));
|
||||
} else if (c.isVariable) {
|
||||
val = new Variable(String(c.value));
|
||||
} else {
|
||||
throw new CompilerError("Invalid value type!", e);
|
||||
}
|
||||
} else {
|
||||
val = createCondition(e);
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
const createCondition = (cond: Expression): ConditionMatcher => {
|
||||
let left: ConditionParameters = resolveParameter(cond.left);
|
||||
let right: ConditionParameters = resolveParameter(cond.right);
|
||||
|
||||
return new ConditionMatcher(left, right, cond.operator);
|
||||
};
|
||||
|
||||
const rules: Rule[] = s_match.rules
|
||||
.map((rule) => {
|
||||
const condition = resolveParameter(rule.condition);
|
||||
return rule.operations.map((op) => new Rule(op, condition));
|
||||
})
|
||||
.flat(1);
|
||||
const submatches = s_match.matches.map((sub) => createMatch(sub));
|
||||
const match = new Match(segments, rules, wildcard, submatches);
|
||||
|
||||
console.log("Adding match", segments, rules, wildcard, submatches);
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
const root_matches = service.matches.map((match) => createMatch(match));
|
||||
|
||||
const runner = new RuleRunner(root_matches);
|
||||
|
||||
return runner;
|
||||
}
|
36
src/rules/error.ts
Normal file
36
src/rules/error.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export interface RuleError {
|
||||
line: number;
|
||||
column: number;
|
||||
message: string;
|
||||
original_err: Error;
|
||||
}
|
||||
|
||||
function indexToLineAndCol(src: string, index: number) {
|
||||
let line = 1;
|
||||
let col = 1;
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (src.charAt(i) === "\n") {
|
||||
line++;
|
||||
col = 1;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
}
|
||||
|
||||
return { line, col };
|
||||
}
|
||||
|
||||
export function transformError(
|
||||
err: Error,
|
||||
data: string,
|
||||
idx: number
|
||||
): RuleError {
|
||||
let loc = indexToLineAndCol(data, idx);
|
||||
|
||||
return {
|
||||
line: loc.line,
|
||||
column: loc.col,
|
||||
message: err.message,
|
||||
original_err: err,
|
||||
};
|
||||
}
|
30
src/rules/index.ts
Normal file
30
src/rules/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { RuleError, transformError } from "./error";
|
||||
import parse, { ParserError } from "./parser";
|
||||
import tokenize, { TokenizerError } from "./tokenise";
|
||||
import { getRuleRunner, RuleRunner } from "./compile";
|
||||
import { inspect } from "util";
|
||||
|
||||
export default function compileRule(rule: string) {
|
||||
let runner: RuleRunner | undefined;
|
||||
let error: RuleError | undefined;
|
||||
try {
|
||||
const tokenised = tokenize(rule);
|
||||
// console.log(tokenised);
|
||||
const parsed = parse(tokenised);
|
||||
const dbservice = parsed.find((e) => e.name === "realtimedb");
|
||||
|
||||
if (!dbservice) throw new Error("No realtimedb service available!");
|
||||
|
||||
runner = getRuleRunner(dbservice);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenizerError) {
|
||||
error = transformError(err, rule, err.index);
|
||||
} else if (err instanceof ParserError) {
|
||||
error = transformError(err, rule, err.token.startIdx);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return { runner, error };
|
||||
}
|
349
src/rules/parser.ts
Normal file
349
src/rules/parser.ts
Normal file
@ -0,0 +1,349 @@
|
||||
import { Token } from "./tokenise";
|
||||
|
||||
export interface Node {
|
||||
type: string;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export interface PathStatement extends Node {
|
||||
type: "path";
|
||||
segments: (string | { type: "variable"; name: string })[];
|
||||
}
|
||||
|
||||
export interface ValueStatement extends Node {
|
||||
type: "value";
|
||||
isNull: boolean;
|
||||
isTrue: boolean;
|
||||
isFalse: boolean;
|
||||
isNumber: boolean;
|
||||
isString: boolean;
|
||||
isVariable: boolean;
|
||||
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export type Operators = "&&" | "||" | "==" | "<=" | ">=" | "!=" | ">" | "<";
|
||||
|
||||
export interface Expression extends Node {
|
||||
type: "expression";
|
||||
|
||||
left: ValueStatement | Expression;
|
||||
operator: Operators;
|
||||
right: ValueStatement | Expression;
|
||||
}
|
||||
|
||||
export type Operations = "read" | "write" | "list"; // | "update" | "create" | "delete" | "list";
|
||||
|
||||
export interface AllowStatement extends Node {
|
||||
type: "permission";
|
||||
operations: Operations[];
|
||||
condition: Expression | ValueStatement;
|
||||
}
|
||||
|
||||
export interface MatchStatement extends Node {
|
||||
type: "match";
|
||||
path: PathStatement;
|
||||
matches: MatchStatement[];
|
||||
rules: AllowStatement[];
|
||||
}
|
||||
|
||||
export interface ServiceStatement extends Node {
|
||||
type: "service";
|
||||
name: string;
|
||||
matches: MatchStatement[];
|
||||
}
|
||||
|
||||
export class ParserError extends Error {
|
||||
token: Token;
|
||||
constructor(message: string, token: Token) {
|
||||
super(message);
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
export default function parse(tokens: Token[]) {
|
||||
const tokenIterator = tokens[Symbol.iterator]();
|
||||
let currentToken: Token = tokenIterator.next().value;
|
||||
let nextToken: Token = tokenIterator.next().value;
|
||||
|
||||
const eatToken = (value?: string) => {
|
||||
if (value && value !== currentToken.value) {
|
||||
throw new ParserError(
|
||||
`Unexpected token value, expected '${value}', received '${currentToken.value}'`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
let idx = currentToken.startIdx;
|
||||
currentToken = nextToken;
|
||||
nextToken = tokenIterator.next().value;
|
||||
return idx;
|
||||
};
|
||||
|
||||
const eatText = (): [string, number] => {
|
||||
checkTypes("text");
|
||||
let val = currentToken.value;
|
||||
let idx = currentToken.startIdx;
|
||||
eatToken();
|
||||
return [val, idx];
|
||||
};
|
||||
const eatNumber = (): number => {
|
||||
checkTypes("number");
|
||||
let val = Number(currentToken.value);
|
||||
if (Number.isNaN(val)) {
|
||||
throw new ParserError(
|
||||
`Value cannot be parsed as number! ${currentToken.value}`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
eatToken();
|
||||
return val;
|
||||
};
|
||||
|
||||
const checkTypes = (...types: string[]) => {
|
||||
if (types.indexOf(currentToken.type) < 0) {
|
||||
throw new ParserError(
|
||||
`Unexpected token value, expected ${types.join(" | ")}, received '${
|
||||
currentToken.value
|
||||
}'`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const parsePathStatement = (): PathStatement => {
|
||||
const segments: (string | { name: string; type: "variable" })[] = [];
|
||||
const idx = currentToken.startIdx;
|
||||
let next = currentToken.type === "slash";
|
||||
while (next) {
|
||||
eatToken("/");
|
||||
if (currentToken.type === "curly_open" && nextToken.type === "text") {
|
||||
eatToken("{");
|
||||
const [name] = eatText();
|
||||
segments.push({
|
||||
type: "variable",
|
||||
name,
|
||||
});
|
||||
eatToken("}");
|
||||
} else if (currentToken.type === "text") {
|
||||
const [name] = eatText();
|
||||
segments.push(name);
|
||||
}
|
||||
next = currentToken.type === "slash";
|
||||
}
|
||||
|
||||
return {
|
||||
type: "path",
|
||||
idx,
|
||||
segments,
|
||||
};
|
||||
};
|
||||
|
||||
const parseValue = (): ValueStatement => {
|
||||
const idx = currentToken.startIdx;
|
||||
|
||||
let isTrue = false;
|
||||
let isFalse = false;
|
||||
let isNull = false;
|
||||
let isVariable = false;
|
||||
let isNumber = false;
|
||||
let isString = false;
|
||||
let value: any = undefined;
|
||||
if (currentToken.type === "keyword") {
|
||||
if (currentToken.value === "true") isTrue = true;
|
||||
else if (currentToken.value === "false") isFalse = true;
|
||||
else if (currentToken.value === "null") isNull = true;
|
||||
else {
|
||||
throw new ParserError(
|
||||
`Invalid keyword at this position ${currentToken.value}`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
eatToken();
|
||||
} else if (currentToken.type === "string") {
|
||||
isString = true;
|
||||
value = currentToken.value.slice(1, currentToken.value.length - 1);
|
||||
eatToken();
|
||||
} else if (currentToken.type === "number") {
|
||||
isNumber = true;
|
||||
value = eatNumber();
|
||||
} else if (currentToken.type === "text") {
|
||||
isVariable = true;
|
||||
[value] = eatText();
|
||||
} else {
|
||||
throw new ParserError(
|
||||
`Expected value got ${currentToken.type}`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "value",
|
||||
isFalse,
|
||||
isNull,
|
||||
isNumber,
|
||||
isString,
|
||||
isTrue,
|
||||
isVariable,
|
||||
value,
|
||||
idx,
|
||||
};
|
||||
};
|
||||
|
||||
const parseCondition = (): Expression | ValueStatement => {
|
||||
// let running = true;
|
||||
let res: Expression | ValueStatement;
|
||||
let left: Expression | ValueStatement | undefined;
|
||||
|
||||
// while (running) {
|
||||
const idx = currentToken.startIdx;
|
||||
|
||||
if (!left) {
|
||||
if (currentToken.type === "bracket_open") {
|
||||
eatToken("(");
|
||||
left = parseCondition();
|
||||
eatToken(")");
|
||||
} else {
|
||||
left = parseValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToken.type === "comparison_operator") {
|
||||
const operator = currentToken.value;
|
||||
|
||||
eatToken();
|
||||
|
||||
let right: Expression | ValueStatement;
|
||||
|
||||
let ct = currentToken; //Quick hack because of TypeScript
|
||||
if (ct.type === "bracket_open") {
|
||||
eatToken("(");
|
||||
right = parseCondition();
|
||||
eatToken(")");
|
||||
} else {
|
||||
right = parseValue();
|
||||
}
|
||||
|
||||
res = {
|
||||
type: "expression",
|
||||
left,
|
||||
right,
|
||||
operator: operator as Operators,
|
||||
idx,
|
||||
};
|
||||
} else if (currentToken.type === "logic_operator") {
|
||||
const operator = currentToken.value;
|
||||
|
||||
eatToken();
|
||||
|
||||
const right = parseCondition();
|
||||
|
||||
res = {
|
||||
type: "expression",
|
||||
left,
|
||||
operator: operator as Operators,
|
||||
right,
|
||||
idx,
|
||||
};
|
||||
} else {
|
||||
res = left;
|
||||
}
|
||||
|
||||
// let ct = currentToken;
|
||||
// if (
|
||||
// ct.type === "comparison_operator" ||
|
||||
// ct.type === "logic_operator"
|
||||
// ) {
|
||||
// left = res;
|
||||
// } else {
|
||||
// running = false;
|
||||
// }
|
||||
// }
|
||||
return res;
|
||||
};
|
||||
|
||||
const parsePermissionStatement = (): AllowStatement => {
|
||||
const idx = eatToken("allow");
|
||||
|
||||
const operations: Operations[] = [];
|
||||
let next = currentToken.type !== "colon";
|
||||
while (next) {
|
||||
const [operation] = eatText();
|
||||
operations.push(operation as Operations);
|
||||
if (currentToken.type === "comma") {
|
||||
next = true;
|
||||
eatToken(",");
|
||||
} else {
|
||||
next = false;
|
||||
}
|
||||
}
|
||||
|
||||
eatToken(":");
|
||||
|
||||
eatToken("if");
|
||||
|
||||
const condition = parseCondition();
|
||||
|
||||
eatToken(";");
|
||||
|
||||
return {
|
||||
type: "permission",
|
||||
idx,
|
||||
operations,
|
||||
condition,
|
||||
};
|
||||
};
|
||||
|
||||
const parseMatchStatement = (): MatchStatement => {
|
||||
const idx = eatToken("match");
|
||||
const path = parsePathStatement();
|
||||
|
||||
eatToken("{");
|
||||
const matches: MatchStatement[] = [];
|
||||
const permissions: AllowStatement[] = [];
|
||||
while (currentToken.type !== "curly_close") {
|
||||
if (currentToken.value === "match") {
|
||||
matches.push(parseMatchStatement());
|
||||
} else if (currentToken.value === "allow") {
|
||||
permissions.push(parsePermissionStatement());
|
||||
} else {
|
||||
throw new ParserError(
|
||||
`Unexpected token value, expected 'match' or 'allow', received '${currentToken.value}'`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
}
|
||||
eatToken("}");
|
||||
|
||||
return {
|
||||
type: "match",
|
||||
path,
|
||||
idx,
|
||||
matches,
|
||||
rules: permissions,
|
||||
};
|
||||
};
|
||||
|
||||
const parseServiceStatement = (): ServiceStatement => {
|
||||
const idx = eatToken("service");
|
||||
let [name] = eatText();
|
||||
eatToken("{");
|
||||
const matches: MatchStatement[] = [];
|
||||
while (currentToken.value === "match") {
|
||||
matches.push(parseMatchStatement());
|
||||
}
|
||||
eatToken("}");
|
||||
|
||||
return {
|
||||
type: "service",
|
||||
name: name,
|
||||
idx,
|
||||
matches,
|
||||
};
|
||||
};
|
||||
|
||||
const nodes: ServiceStatement[] = [];
|
||||
while (currentToken) {
|
||||
nodes.push(parseServiceStatement());
|
||||
}
|
||||
return nodes;
|
||||
}
|
99
src/rules/tokenise.ts
Normal file
99
src/rules/tokenise.ts
Normal file
@ -0,0 +1,99 @@
|
||||
export type TokenTypes =
|
||||
| "space"
|
||||
| "comment"
|
||||
| "string"
|
||||
| "keyword"
|
||||
| "colon"
|
||||
| "semicolon"
|
||||
| "comma"
|
||||
| "comparison_operator"
|
||||
| "logic_operator"
|
||||
| "equals"
|
||||
| "slash"
|
||||
| "bracket_open"
|
||||
| "bracket_close"
|
||||
| "curly_open"
|
||||
| "curly_close"
|
||||
| "array"
|
||||
| "questionmark"
|
||||
| "number"
|
||||
| "text";
|
||||
|
||||
export type Token = {
|
||||
type: TokenTypes;
|
||||
value: string;
|
||||
startIdx: number;
|
||||
endIdx: number;
|
||||
};
|
||||
|
||||
type Matcher = (input: string, index: number) => undefined | Token;
|
||||
|
||||
export class TokenizerError extends Error {
|
||||
index: number;
|
||||
constructor(message: string, index: number) {
|
||||
super(message);
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
function regexMatcher(regex: string | RegExp, type: TokenTypes): Matcher {
|
||||
if (typeof regex === "string") regex = new RegExp(regex);
|
||||
|
||||
return (input: string, index: number) => {
|
||||
let matches = input.substring(index).match(regex as RegExp);
|
||||
if (!matches || matches.length <= 0) return undefined;
|
||||
|
||||
return {
|
||||
type,
|
||||
value: matches[0],
|
||||
startIdx: index,
|
||||
endIdx: index + matches[0].length,
|
||||
} as Token;
|
||||
};
|
||||
}
|
||||
|
||||
const matcher = [
|
||||
regexMatcher(/^\s+/, "space"),
|
||||
regexMatcher(/^(\/\*)(.|\s)*?(\*\/)/g, "comment"),
|
||||
regexMatcher(/^\/\/.+/, "comment"),
|
||||
regexMatcher(/^#.+/, "comment"),
|
||||
regexMatcher(/^".*?"/, "string"),
|
||||
// regexMatcher(/(?<=^")(.*?)(?=")/, "string"),
|
||||
regexMatcher(/^(service|match|allow|if|true|false|null)/, "keyword"),
|
||||
regexMatcher(/^\:/, "colon"),
|
||||
regexMatcher(/^\;/, "semicolon"),
|
||||
regexMatcher(/^\,/, "comma"),
|
||||
regexMatcher(/^(\=\=|\!\=|\<\=|\>\=|\>|\<)/, "comparison_operator"),
|
||||
regexMatcher(/^(&&|\|\|)/, "logic_operator"),
|
||||
regexMatcher(/^\=/, "equals"),
|
||||
regexMatcher(/^\//, "slash"),
|
||||
regexMatcher(/^\(/, "bracket_open"),
|
||||
regexMatcher(/^\)/, "bracket_close"),
|
||||
regexMatcher(/^{/, "curly_open"),
|
||||
regexMatcher(/^}/, "curly_close"),
|
||||
regexMatcher(/^\[\]/, "array"),
|
||||
regexMatcher(/^\?/, "questionmark"),
|
||||
regexMatcher(/^[0-9]+(\.[0-9]+)?/, "number"),
|
||||
regexMatcher(/^[a-zA-Z_\*]([a-zA-Z0-9_\.\*]?)+/, "text"),
|
||||
];
|
||||
|
||||
export default function tokenize(input: string) {
|
||||
let index = 0;
|
||||
let tokens: Token[] = [];
|
||||
while (index < input.length) {
|
||||
const matches = matcher.map((m) => m(input, index)).filter((e) => !!e);
|
||||
let match = matches[0];
|
||||
if (match) {
|
||||
if (match.type !== "space" && match.type !== "comment") {
|
||||
tokens.push(match);
|
||||
}
|
||||
index += match.value.length;
|
||||
} else {
|
||||
throw new TokenizerError(
|
||||
`Unexpected token '${input.substring(index, index + 1)}'`,
|
||||
index
|
||||
);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
Reference in New Issue
Block a user