Files
tdir/parser.ts
Gregor Lohaus 3110eefcbd hardening
2026-05-22 08:55:28 +02:00

112 lines
3.3 KiB
TypeScript

import { readdirSync, statSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { TextDecoder } from "node:util"
export type TemplateVariable = {
path: string
type: string
}
const IF_RE = /<@(?:if|elseif)\((.+?)\)>/g
const VAR_RE = /<@var\(context\.(.+?)(?::(\w+))?\)>/g
const DIRECTIVE_RE = /<@(if|elseif|else|endif)(?:\((.+?)\))?>/g
const EQ_RE = /^eq\(context\.(.+?),\s*"(.*)"\)$/
const PATH_RE = /^context\.(.+)$/
function extractCondition(expr: string | undefined, vars: TemplateVariable[]) {
if (!expr) throw new Error("Missing condition expression")
const eqMatch = expr.match(EQ_RE)
if (eqMatch) {
vars.push({ path: eqMatch[1]!, type: "string" })
return
}
const pathMatch = expr.match(PATH_RE)
if (pathMatch) {
vars.push({ path: pathMatch[1]!, type: "boolean" })
return
}
throw new Error(`Invalid condition expression: ${expr}`)
}
function extractFromString(text: string, vars: TemplateVariable[]) {
for (const match of text.matchAll(IF_RE)) {
extractCondition(match[1]!, vars)
}
for (const match of text.matchAll(VAR_RE)) {
vars.push({ path: match[1]!, type: match[2] ?? "string" })
}
}
function validateIfBlocks(content: string, vars: TemplateVariable[]) {
const stack: { sawElse: boolean }[] = []
for (const match of content.matchAll(DIRECTIVE_RE)) {
const directive = match[1]!
const condition = match[2]
if (directive === "if") {
extractCondition(condition!, vars)
stack.push({ sawElse: false })
} else if (directive === "elseif") {
const frame = stack[stack.length - 1]
if (!frame) throw new Error("Unexpected <@elseif> without <@if>")
if (frame.sawElse) throw new Error("Unexpected <@elseif> after <@else>")
extractCondition(condition!, vars)
} else if (directive === "else") {
const frame = stack[stack.length - 1]
if (!frame) throw new Error("Unexpected <@else> without <@if>")
if (frame.sawElse) throw new Error("Unexpected duplicate <@else>")
frame.sawElse = true
} else if (directive === "endif") {
if (stack.length === 0) throw new Error("Unexpected <@endif> without <@if>")
stack.pop()
}
}
if (stack.length > 0) {
throw new Error("Unmatched <@if> without <@endif>")
}
}
function isUtf8Text(buffer: Buffer): boolean {
if (buffer.indexOf(0) !== -1) return false
try {
new TextDecoder("utf-8", { fatal: true }).decode(buffer)
return true
} catch {
return false
}
}
function walkDir(dirPath: string, vars: TemplateVariable[]) {
const entries = readdirSync(dirPath).sort()
for (const entry of entries) {
const fullPath = join(dirPath, entry)
extractFromString(entry, vars)
const stat = statSync(fullPath)
if (stat.isDirectory()) {
walkDir(fullPath, vars)
} else if (stat.isFile()) {
const content = readFileSync(fullPath)
if (isUtf8Text(content)) {
const text = content.toString("utf-8")
extractFromString(text, vars)
validateIfBlocks(text, vars)
}
}
}
}
export function parse(dirPath: string): TemplateVariable[] {
const vars: TemplateVariable[] = []
walkDir(dirPath, vars)
const seen = new Set<string>()
return vars.filter(v => {
const key = `${v.path}:${v.type}`
if (seen.has(key)) return false
seen.add(key)
return true
})
}