From 0c7f290e54e8f694a5bb347510824030dd3dc52a Mon Sep 17 00:00:00 2001 From: Adrian Schubek Date: Sun, 2 Jun 2024 08:00:12 +0200 Subject: [PATCH] dev: preprocessor --- package.json | 2 +- src/analyzer.ts | 16 + src/ast.ts | 26 ++ src/common.ts | 86 ++++- src/lexer.ts | 40 +-- src/main.ts | 65 ++-- src/parser.ts | 14 +- src/preprocessor.ts | 46 +++ src/visitor.ts | 31 +- tests/t1.txt | 48 ++- tests/t3.php | 760 ++++++++++++++++++++++++++++++++++++++++++++ tests/t4.txt | 1 + 12 files changed, 1051 insertions(+), 84 deletions(-) create mode 100644 src/analyzer.ts create mode 100644 src/preprocessor.ts create mode 100644 tests/t3.php create mode 100644 tests/t4.txt diff --git a/package.json b/package.json index bd07f20..fe44d24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "utpp", - "version": "0.6.0", + "version": "1.0.0", "description": "Universal Text Pre-Processor", "main": "bin/index.js", "bin": "bin/npx.js", diff --git a/src/analyzer.ts b/src/analyzer.ts new file mode 100644 index 0000000..d7db22d --- /dev/null +++ b/src/analyzer.ts @@ -0,0 +1,16 @@ +import { ASTNode } from "./ast"; +import { Config } from "./common"; +import { Visitor } from "./visitor"; + +export function analyze(node: ASTNode, config: Config): ASTNode { + // Analyzing Visitor (imports, allow/disallow features) + // imports only for \use{\file...} or \use{\url...} or \use{stdlib} -> scan full ast -> add imports to config + // other \url,\file IGNORE. they are on demand in-place reads as-is without eval. do not eval + // scan for node types: use>raw, use>url, use>file + + return node; +} + +export class Analyzer implements Visitor { + +} \ No newline at end of file diff --git a/src/ast.ts b/src/ast.ts index 89d9ab7..07daf04 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -96,6 +96,32 @@ export class IfStatement extends ASTNode { this.falseBranch = falseBranch; } } +export class MatchStatement extends ASTNode { + accept(visitor: Visitor): T { + return visitor.visitMatchStatement(this); + } + value: ASTNode; + cases: CaseStatement[]; // eval from first to last + // defaultCase: ASTNode; // nicht nötig einfach \case{true}{...} + constructor(value: ASTNode, cases: CaseStatement[], row: number, col: number) { + super(ASTNodeType._MATCH, row, col); + this.value = value; + this.cases = cases; + } +} +export class CaseStatement extends ASTNode { + accept(visitor: Visitor): T { + return visitor.visitCaseStatement(this); + } + value: ASTNode; + body: ASTNode; + constructor(value: ASTNode, body: ASTNode, row: number, col: number) { + super(ASTNodeType._CASE, row, col); + this.value = value; + this.body = body; + } +} + export class LoopStatement extends ASTNode { accept(visitor: Visitor): T { return visitor.visitLoopStatement(this); diff --git a/src/common.ts b/src/common.ts index 873532e..96facbc 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { ASTNode, Params } from "./ast"; +import { Params } from "./ast"; export enum TokenType { T_RAW = "T_RAW", @@ -29,7 +29,9 @@ export enum ASTNodeType { _LOOP = "_LOOP", _URL = "_URL", _FILE = "_FILE", - _USE = "_USE" /* imports. must be top level. parse file then interpret */, + _USE = "_USE" /* imports. nein: must be top level. parse file then interpret */, + _MATCH = "_MATCH", + _CASE = "_CASE", } // built in cannot be overriden @@ -49,12 +51,72 @@ export enum BuiltInFunction { FILE = "file", USE = "use", VAR = "var", - TRUE = "$true", - FALSE = "$false", + TRUE = "true", + FALSE = "false", HALT = "halt" /* stop exec immediate */, ASSERT = "assert" /* assert function */, + MATCH = "match", + CASE = "case", } +// export interface Config { +// /* Tokens */ +// prefix: string; +// argStart: string; +// argEnd: string; +// paramStart: string; +// paramAssign: string; +// paramSep: string; +// paramEnd: string; +// evalStart: string; +// evalEnd: string; +// /* Visitor */ +// readUrls: boolean; +// readFiles: boolean; +// readEnv: boolean; +// eval: boolean; +// } + +export type Config = { [key in ConfigKey]: string }; + +export type ConfigKey = + /* Lexer */ + | "prefix" + | "argStart" + | "argEnd" + | "paramStart" + | "paramAssign" + | "paramSep" + | "paramEnd" + | "evalStart" + | "evalEnd" + /* Visitor */ + | "readUrls" + | "readFiles" + | "readEnv" + | "eval"; +// | "allowBuiltinOverride"; +// | string /* custom config key */; + +export const DefaultConfig: Config = { + /* Tokens */ + prefix: "\\", + argStart: "{", + argEnd: "}", + paramStart: "[", + paramAssign: "=", + paramSep: ",", + paramEnd: "]", + evalStart: "`", + evalEnd: "`", + /* Visitor */ + readUrls: "true", + readFiles: "true", + readEnv: "true", + eval: "true", + // allowBuiltinOverride: "false", +}; + export interface Token extends Indexable { type: TokenType; value: string; @@ -67,10 +129,6 @@ export interface Indexable { col: number; } -export interface Visitor { - visit(node: ASTNode): void; -} - /** * Check RAW for truthy values * @@ -80,13 +138,18 @@ export function truthy(value: string): boolean { return /* val !== "false" && val !== "0" && */ val !== "$false"; } +export function info(msg: string, row?: number, col?: number): void { + // can be silenced with "-q" + console.log("ℹ️ " + chalk.cyanBright(` ${msg} ${row !== undefined && col !== undefined ? `on line ${row}:${col}.` : ""}`)); +} + export function warn(msg: string, row?: number, col?: number): void { // treat warning as errors config? - console.log("⚠️ " + chalk.yellow(` ${msg} ${row !== undefined && col !== undefined ? `on line ${row}:${col}.` : ""}\n`)); + console.log("⚠️ " + chalk.yellow(` ${msg} ${row !== undefined && col !== undefined ? `on line ${row}:${col}.` : ""}`)); } export function err(msg: string, row?: number, col?: number): never { - throw new Error("🔥 " + chalk.red(`${msg} ${row !== undefined && col !== undefined ? `on line ${row}:${col}.` : ""}\n`)); + throw new Error("🔥 " + chalk.red(`${msg} ${row !== undefined && col !== undefined ? `on line ${row}:${col}.` : ""}`)); } export function assertCount(text: string, details: string, thisToken: Indexable, count: number, args?: T[]) { @@ -119,6 +182,9 @@ export function assertFnArgRange(thisToken: Indexable, fnName: string, min: n export function assertParamCount(thisToken: Indexable, fnName: string, count: number, params: Params | null) { assertCount("parameters", `in function \\${fnName}`, thisToken, count, params?.kv ? Object.values(params.kv) : undefined); } +export function assertParamRange(thisToken: Indexable, fnName: string, min: number, max: number, params: Params | null) { + assertRange("parameters", `in function \\${fnName}`, thisToken, min, max, params?.kv ? Object.values(params.kv) : undefined); +} export function assertType(details: string, thisToken: Indexable, expected: T, actual: T) { if (actual !== expected) { diff --git a/src/lexer.ts b/src/lexer.ts index 4816120..d4a3908 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -1,45 +1,9 @@ -import { log } from "console"; import { TokenType, err } from "./common"; -import type { Token } from "./common"; +import type { Token, Config } from "./common"; const ROW_OFFSET = 0; -const INDEX_OFFSET = 0; -const PREFIX = "\\"; -const ARG_START = "{"; -const ARG_END = "}"; -const PARAM_START = "["; -const PARAM_ASSIGN = "="; -const PARAM_SEP = ","; -const PARAM_END = "]"; -const EVAL_START = "`"; -const EVAL_END = "`"; - -export interface TokenizerConfig { - prefix: string; - argStart: string; - argEnd: string; - paramStart: string; - paramAssign: string; - paramSep: string; - paramEnd: string; - evalStart: string; - evalEnd: string; -} -export function tokenize( - input: string, - config: TokenizerConfig = { - prefix: PREFIX, - argStart: ARG_START, - argEnd: ARG_END, - paramStart: PARAM_START, - paramAssign: PARAM_ASSIGN, - paramSep: PARAM_SEP, - paramEnd: PARAM_END, - evalStart: EVAL_START, - evalEnd: EVAL_END, - } -): Token[] { +export function tokenize(input: string, config: Config): Token[] { let row = 0 - ROW_OFFSET; // offset for inserted statements let col = 0; let index = 0; diff --git a/src/main.ts b/src/main.ts index 7add37d..de6f0fe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,45 +1,66 @@ import fs from "fs"; import { tokenize } from "./lexer"; import { log } from "console"; -import { TokenType } from "./common"; -import assert from "node:assert/strict"; import { parse } from "./parser"; -import { Interpreter } from "./visitor"; -import { Program } from "./ast"; +import { interpret } from "./visitor"; +import { preprocess } from "./preprocessor"; +import chalk from "chalk"; +import { analyze } from "./analyzer"; let original = ""; // original = fs.readFileSync(__dirname + "/../tests/loop.txt", "utf8"); - original = fs.readFileSync(__dirname + "/../tests/t1.txt", "utf8"); +// original = fs.readFileSync(__dirname + "/../tests/t1.txt", "utf8"); +// original = fs.readFileSync(__dirname + "/../tests/t3.php", "utf8"); +original = fs.readFileSync(__dirname + "/../tests/t4.txt", "utf8"); -// 1. add default stuff -// original = "\\version{1}\n\\prefix{\\}\n\\config{files}{true}\n\\config{net}{true}\n\\config{env}{true}\n\\config{js}{true}\n" + original; +// \rawfile{path} imports the file, just copo/pastes the file content, no pipeline +// allow file importsw: \file{name} just copo/pastes the file content +// \use{realtivePathOrURL} imports the file, usign this pipeline and executes it +// pro: own \\\utppp[] block +// no easy. In Interperter for \file{} just do the pipeline (except interpret) preprocess>tokenize>parse -// original += "\n\\halt"; +// read cli: -q quiet? (print debug) -log("===== content: ====="); -log(original); +const pj = require("../package.json"); +console.log(chalk.yellowBright(chalk.bold(`🚀 utpp ${pj.version} `))); + +// log("===== content: ====="); +// log(original); + +// 1. preprocessor (Meta Config) +log("===== preprocess: ====="); +const [input, config] = preprocess(original); + +console.log("with config: ", config); // 2. lexer (Tokens) log("===== tokenize: ====="); -const tokenized = tokenize(original); +const tokenized = tokenize(input, config); log(tokenized); -const reconstructedFromLexer = tokenized.map((t) => t.value).join(""); -assert.strictEqual(original, reconstructedFromLexer); -log("lexer verified ✅"); +// const reconstructedFromLexer = tokenized.map((t) => t.value).join(""); +// assert.strictEqual(original, reconstructedFromLexer); +// log("lexer verified ✅"); // doesnt work when metaconfig used + +// TODO: allow export/import (serialize) AST for faster executing // 3. parser (AST) log("===== parse: ====="); -const ast = parse(tokenized) as Program; -const treeify = require("./utils/treeify"); -treeify.asLines(ast, true, false, log); - -// 4. interpreter (Execute) +const ast = parse(tokenized); +const treeify = require("./utils/treeify"); // debug +treeify.asLines(ast, true, false, log); // debug + +// 4. AST Analyzer (imports, verify enabled/disabled features) +log("===== analyze: ====="); +const program = analyze(ast, config); + +// 5. interpreter (Execute on complete resolved AST) log("===== interpret: ====="); -const visitor = new Interpreter(); -const generated = ast.accept(visitor); +const generated = interpret(program); log(generated); -log("----------------------") +log("----------------------"); // ?. Validator/Rewriter (check enabled/disabled features) // ?. Optimizer (dead code elimination, constant folding, etc.) + +// maybe IR-Representation SSA form optimize... diff --git a/src/parser.ts b/src/parser.ts index f1c87df..cf6340d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,3 +1,4 @@ +import { log } from "console"; import { ASTNode, Raw, @@ -13,7 +14,7 @@ import { FileStatement, UseStatement, } from "./ast"; -import { BuiltInFunction, Token, TokenType, assertCount, assertRange, assertType, err } from "./common"; +import { BuiltInFunction, Token, TokenType, assertCount, assertRange, err } from "./common"; // ne Build CST from tokens like \if{}{} --> Command(name="if",args=2...) // ne Build AST from CST like Command(name="if",args=2...) --> IfStatement(condition=...,trueBranch=...,falseBranch=...) @@ -116,25 +117,24 @@ export function parse(input: Token[], isRecursiveCall: boolean = false): ASTNode } function parseArguments(alwaysEvalNArgs?: number): ASTNode[] { - const as = consume(TokenType.T_ARG_START); const args: ASTNode[] = []; // FIXME may break if JS contains { }. - while (alwaysEvalNArgs && alwaysEvalNArgs > 0) { + while (alwaysEvalNArgs && alwaysEvalNArgs > 0 && hasMore() && option(TokenType.T_ARG_START)) { + const as = consume(TokenType.T_ARG_START); alwaysEvalNArgs--; let evalCode = ""; + log("uhhhhhhhhhhhhhhhhhhhhhhhhhhh", alwaysEvalNArgs); while (!option(TokenType.T_ARG_END)) evalCode += consumeAny().value; consume(TokenType.T_ARG_END); - consume(TokenType.T_ARG_START); args.push(new EvalStatement(evalCode, ...rowcol(as))); } - while (hasMore()) { + while (hasMore() && option(TokenType.T_ARG_START)) { + consume(TokenType.T_ARG_START); const arg = parse(input, true); args.push(arg); consume(TokenType.T_ARG_END); - if (!option(TokenType.T_ARG_START)) break; - consume(TokenType.T_ARG_START); } return args; diff --git a/src/preprocessor.ts b/src/preprocessor.ts new file mode 100644 index 0000000..dc2e5a4 --- /dev/null +++ b/src/preprocessor.ts @@ -0,0 +1,46 @@ +import { DefaultConfig, Config, err, info, warn } from "./common"; + +// 1. add default stuff (meta config) + +const metaConfigRegex = /\\\\\\utpp\[([\s\S]*?)\]/g; + +export function preprocess(input: string): [string, Readonly] { + // scan code for meta config + const configs = input.matchAll(metaConfigRegex); + let metaConfig: Config = DefaultConfig; + let errors: string[] = []; + let len = 0; + + for (const match of configs) { + const [_, config] = match; + const options = config.split(",").map((x) => x.trimStart()); + const kv = options.map((x) => x.split("=")).filter((k, v) => k !== undefined && k[0] !== ""); + for (const [key, value] of kv) { + if (value === undefined) { + err(`Invalid meta config key-value pair (${key},${value}). ${key === undefined ? "key" : "value"} cannot be undefined.`); + } + + if (!Object.hasOwn(metaConfig, key)) { + // error or warn ? + // errors.push(`Unknown meta config key '${key}'.`); + info(`Unknown meta config key '${key}' defined.`); + } + + metaConfig[key as keyof Config] = value; + len++; + } + } + + if (len > 1) { + warn("Multiple meta config blocks found."); + } + + if (errors.length > 0) { + err(errors.join("\n")); + } + + input = input.replaceAll(metaConfigRegex, ""); + console.log("input", input); + + return [input, metaConfig]; +} diff --git a/src/visitor.ts b/src/visitor.ts index ef4cdeb..c85a396 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -1,5 +1,6 @@ import { ASTNode, + CaseStatement, Comment, EvalStatement, FileStatement, @@ -7,13 +8,14 @@ import { FunctionDefinition, IfStatement, LoopStatement, + MatchStatement, Params, Program, Raw, URLStatement, UseStatement, } from "./ast"; -import { BuiltInFunction, assertCount, assertFnArgCount, assertParamCount, err as trueErr, warn } from "./common"; +import { BuiltInFunction, assertCount, assertFnArgCount, assertFnArgRange, assertParamCount, err as trueErr, truthy, warn } from "./common"; function err(msg: string, node: ASTNode): never { return trueErr(msg, ...rowcol(node)); @@ -26,6 +28,8 @@ export interface Visitor { visitFunctionDefinition(node: FunctionDefinition): T; visitParams(node: Params): T; visitIfStatement(node: IfStatement): T; + visitMatchStatement(node: MatchStatement): T; + visitCaseStatement(node: CaseStatement): T; visitLoopStatement(node: LoopStatement): T; visitURLStatement(node: URLStatement): T; visitFileStatement(node: FileStatement): T; @@ -38,10 +42,21 @@ interface DeclaredFunction { fnArgs: string[]; fnBody: ASTNode; } +interface InterpreterOptions { + ignoreOverride: boolean; +} + +export function interpret(program: ASTNode /* , config: cfg */): string { + const intp = new Interpreter(); + return program.accept(intp); +} export class Interpreter implements Visitor { parent: Interpreter | null = null; functions = new Map(); + options: InterpreterOptions = { + ignoreOverride: false, + }; visitProgram(node: Program): string { let output = ""; @@ -61,11 +76,12 @@ export class Interpreter implements Visitor { const fnParams = decode<{ [key: string]: string | null }>(node.params.accept(this)); // params are available as {foo: 123} => $PARAM_FOO -> 123 - const isVar = fnName.startsWith("$"); - const isEval = fnName.endsWith("!"); + // const isVar = fnName.startsWith("$"); + // const isEval = fnName.endsWith("!"); // if isEval convert arguments from Raw to EvalStatement: // -> do not evaluate arguments // -> before: args.0.prog.raw --> args.0.prog.eval assert args.0.prog.length === 1 !!! + // nei ngehört hier garnicht rein sondern in parser!! // check for built-in functions (except if,f,loop,..). cannot be overridden let declaredFn: DeclaredFunction | undefined; @@ -144,7 +160,9 @@ export class Interpreter implements Visitor { } return ""; case BuiltInFunction.HALT: - err(`Execution halted`, node); + assertFnArgRange(node, fnName, 0, 1, args); + assertParamCount(node, fnName, 0, node.params); + err("Execution halted" + (args[0] ? ". " + args[0] : ""), node); default: // check for declared functions // args.map((arg) => arg.accept(this)); @@ -180,6 +198,9 @@ export class Interpreter implements Visitor { newScope.functions.set(`\$p_${key}`, { fnArgs: [], fnBody: new Raw(fnParams[key] ?? "", ...rowcol(node)) }); } + // also create positional arguments $1, $2, ... + // and do with pareamete \f[minarg=0,maxargs=5]... + return declaredFn.fnBody.accept(newScope); } visitFunctionDefinition(node: FunctionDefinition): string { @@ -207,7 +228,7 @@ export class Interpreter implements Visitor { } visitIfStatement(node: IfStatement): string { const cond = node.condition.accept(this); - if (cond) return node.trueBranch.accept(this); + if (truthy(cond)) return node.trueBranch.accept(this); if (node.falseBranch !== undefined) return node.falseBranch.accept(this); return ""; } diff --git a/tests/t1.txt b/tests/t1.txt index ade5d7e..be6da19 100644 --- a/tests/t1.txt +++ b/tests/t1.txt @@ -7,6 +7,21 @@ Die Zeit ist \${new Date()}. \toparent{$\$name} } +\f{true}{\true} +\f{false}{\false} + +wie latex mit placehodlers +\f[2]{and}{\and{#1}{#2}} + +\true +\false +\not{} +\and{}{}{}... +\or{}{} + +allow dynamic number of arguments + + \var!{b}{" => " + 0.2+0.3 } \var{a}{0.2+0.3} @@ -21,5 +36,36 @@ Die Zeit ist \${new Date()}. \add{42}{69} +\not{} + +\if{\false}{T}{F} +\if{123}{TT} + + + + + + + +\halt{fertig} + +Match: +\match{value}{ + Dies + \case{foo}{123} + Ist + \case{bar}{456} + Ein + \case{true}{999} \# default + Test +} + + -\if{$false}{true}{false} \ No newline at end of file +\\\utpp[ + uuu=9, + prefix=a, +] +\\\utpp[ + prefix=\, +] diff --git a/tests/t3.php b/tests/t3.php new file mode 100644 index 0000000..52afaa8 --- /dev/null +++ b/tests/t3.php @@ -0,0 +1,760 @@ + $part !== ''); + +// get real path and check if accessible (open_basedir) +$local_path = realpath(PUBLIC_FOLDER . $request_uri); + +// check if path is dir +$path_is_dir = is_dir($local_path); + +class File +{ + public string $name; + public string $url; + public string $size; + public bool $is_dir; + public string $modified_date; + public int $dl_count; + public ?object $meta; + + public function __toString(): string + { + return $this->name; + } +} + +/* @var array */ +$sorted = []; + +$total_items = 0; +$total_size = 0; + +// local path exists +if ($path_is_dir) { + $[if `!process.env.NO_DL_COUNT`]$ + $redis = new Redis(); + $redis->connect('127.0.0.1', 6379); + $[end]$ + // TODO: refactor use MGET instead of loop GET + + $sorted_files = []; + $sorted_folders = []; + foreach (($files = scandir($local_path)) as $file) { + // always skip current folder '.' or parent folder '..' if current path is root or file should be ignored or .dbmeta.json + if ($file === '.' || $file === '..' && count($url_parts) === 0 $[if `process.env.IGNORE`]$|| $file !== '..' && fnmatch("${{`process.env.IGNORE ?? ""`}}$", $file)$[end]$ || str_contains($file, ".dbmeta.json")) continue; + + $[if `process.env.IGNORE`]$ + foreach ($url_parts as $int_path) { /* check if parent folders are hidden */ + if (fnmatch("${{`process.env.IGNORE ?? ""`}}$", $int_path)) { + $path_is_dir = false; + goto skip; /* Folder should be ignored so skip to 404 */ + } + } + $[end]$ + + // remove '/var/www/public' from path + $url = substr($local_path, strlen(PUBLIC_FOLDER)) . '/' . $file; + + $file_size = filesize($local_path . '/' . $file); + + $is_dir = is_dir($local_path . '/' . $file); + + $file_modified_date = gmdate('Y-m-d\TH:i:s\Z', filemtime($local_path . '/' . $file)); + + // load metadata if file exists + $meta_file = realpath($local_path . '/' . $file . '.dbmeta.json'); + if ($meta_file !== false) { + $meta = json_decode(file_get_contents($meta_file)); + if ($meta !== null && $meta->hidden === true) continue; + } else { + // Variables stay alive in php so we need to reset it explicitly + $meta = null; + } + + $item = new File(); + $item->name = $file; + $item->url = $url; + $item->size = $file_size; + $item->is_dir = $is_dir; + $item->modified_date = $file_modified_date; + $item->dl_count = $[if `!process.env.NO_DL_COUNT`]$!$is_dir ? $redis->get($url) :$[end]$ 0; + $item->meta = $meta ?? null; + if ($is_dir) { + array_push($sorted_folders, $item); + } else { + array_push($sorted_files, $item); + } + + // don't count parent folder + if ($file !== "..") $total_items++; + $total_size += $file_size; + } + + natcasesort($sorted_folders); + natcasesort($sorted_files); + $[if `process.env.REVERSE_SORT`]$ + $sorted_folders = array_reverse($sorted_folders); + $sorted_files = array_reverse($sorted_files); + $[end]$ + $sorted = array_merge($sorted_folders, $sorted_files); + + // if list request return json + if(isset($_REQUEST["ls"])) { + $info = []; + foreach ($sorted as $file) { + $info[] = [ + "url" => $file->url, + "name" => $file->name, + "type" => $file->is_dir ? "dir" : "file", + "size" => $file->size, + "modified" => $file->modified_date, + "downloads" => $[if `!process.env.NO_DL_COUNT`]$ intval($redis->get($file->url))$[else]$0$[end]$ + ]; + } + header("Content-Type: application/json"); + die(json_encode($info)); + } +} elseif (file_exists($local_path)) { + // local path is file. serve it directly using nginx + + $relative_path = substr($local_path, strlen(PUBLIC_FOLDER)); + + $[if `process.env.IGNORE`]$ + foreach ($url_parts as $int_path) { /* check if parent folders are hidden */ + if (fnmatch("${{`process.env.IGNORE ?? ""`}}$", $int_path)) { + goto skip; /* File should be ignored so skip to 404 */ + } + } + $[end]$ + + // skip if file is .dbmeta.json + if (str_contains($local_path, ".dbmeta.json")) goto skip; + + // check if password proteced + if (file_exists($local_path . '.dbmeta.json')) { + $meta = json_decode(file_get_contents($local_path . '.dbmeta.json')); + if (isset($meta->password)) { + if (!isset($_REQUEST["key"]) || $_REQUEST["key"] !== $meta->password) { // allows get and post reqeusts + http_response_code(401); + define('AUTH_REQUIRED', true); + goto end; + } + } + } + + $[if `process.env.HASH`]$ + // only allow download if requested hash matches actual hash + if (isset($_REQUEST["hash"]) || isset($meta) && $meta->hash_required === true) { + if ($_REQUEST["hash"] !== hash_file('sha256', $local_path)) { + http_response_code(403); + die("Access denied. Supplied hash does not match actual file hash."); + } + } + $[end]$ + + // increment redis view counter + $[if `!process.env.NO_DL_COUNT`]$ + $redis = new Redis(); + $redis->connect('127.0.0.1', 6379); + $redis->incr($relative_path); + $[end]$ + + if(isset($_REQUEST["info"])) { + $info = [ + "path" => $relative_path, + "name" => basename($local_path), + "mime" => mime_content_type($local_path) ?? "application/octet-stream", + "size" => filesize($local_path), + "modified" => filemtime($local_path), + "downloads" => $[if `!process.env.NO_DL_COUNT`]$ intval($redis->get($relative_path))$[else]$0$[end]$, + "hash_sha256" => $[if `process.env.HASH`]$hash_file('sha256', $local_path)$[else]$null$[end]$ + ]; + header("Content-Type: application/json"); + die(json_encode($info)); + } + + // let nginx guess content type + header("Content-Type: "); + // let nginx handle file serving + header("X-Accel-Redirect: /__internal_public__" . $relative_path); + die(); +} else { + // local path does not exist +skip: + http_response_code(404); +end: +} +?> + + + + + + + + Dir Browser - <?= '/' . implode(separator: '/', array: $url_parts) ?> + $[ifeq env:THEME cerulean]$ + + $[ifeq env:THEME materia]$ + + $[ifeq env:THEME quartz]$ + + $[ifeq env:THEME sandstone]$ + + $[ifeq env:THEME sketchy]$ + + $[ifeq env:THEME united]$ + + $[ifeq env:THEME yeti]$ + + $[ifeq env:THEME litera]$ + + $[else]$ + + $[end]$ + + $[if `process.env.ICONS !== "false"`]$ + + $[end]$ + + + + + + + $[if `!process.env.NO_README_RENDER`]$ + name) === "readme.md") { + $readme = $file; + break; + } + } + + require __DIR__ . "/vendor/autoload.php"; + use League\CommonMark\Environment\Environment; + use League\CommonMark\Extension\Autolink\AutolinkExtension; + use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; + use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension; + use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; + use League\CommonMark\Extension\Table\TableExtension; + use League\CommonMark\Extension\TaskList\TaskListExtension; + use League\CommonMark\MarkdownConverter; + + if ($readme) { + // Define your configuration, if needed + $config = []; + + // Configure the Environment with all the CommonMark parsers/renderers + $environment = new Environment($config); + $environment->addExtension(new CommonMarkCoreExtension()); + + // Remove any of the lines below if you don't want a particular feature + $environment->addExtension(new AutolinkExtension()); + ${{`!process.env.ALLOW_RAW_HTML ? "$environment->addExtension(new DisallowedRawHtmlExtension());" : ""`}}$ + $environment->addExtension(new StrikethroughExtension()); + $environment->addExtension(new TableExtension()); + $environment->addExtension(new TaskListExtension()); + $converter = new MarkdownConverter($environment); + + $readme_render = $converter->convert(file_get_contents(PUBLIC_FOLDER . $readme->url)); + ?> +
+
+ +
+
+ + $[end]$ + +
+ +
+ + $[ifeq env:LAYOUT popup]$ + + $[end]$ + + + + $[if `process.env.ICONS !== "false"`]$ + + + $[end]$ + + + + + + \ No newline at end of file diff --git a/tests/t4.txt b/tests/t4.txt new file mode 100644 index 0000000..fc3370b --- /dev/null +++ b/tests/t4.txt @@ -0,0 +1 @@ +\file!{1+2} \ No newline at end of file