restored/script.cpp
← All source files · Download this file
37,948 bytes · SHA256 ef5be53bee5b6a5b93ae43078705997fb99a46c2772f717d20bfd9b152b862de
// Linux restoration of the vScript/vFunc/CodeLine/variable model.
// All storage, parsing, validation and execution machinery below is rewritten.
#include "script.h"
#include <algorithm>
#include <charconv>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <sstream>
#include <string_view>
#include <nlohmann/json.hpp>
#include <curl/curl.h>
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <linux/fs.h>
#include <sqlite3.h>
#include <mysql.h>
#include <sql.h>
#include <sqlext.h>
#include <ctime>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#include <array>
#include <ares.h>
#include <openssl/ssl.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
namespace vanilla {
namespace fs = std::filesystem;
static constexpr std::size_t none = static_cast<std::size_t>(-1);
static bool identifier(const std::string& s) {
if(s.empty() || s.size() > 128 || !(std::isalpha(static_cast<unsigned char>(s[0])) || s[0] == '_')) return false;
return std::all_of(s.begin(),s.end(),[](unsigned char c) { return std::isalnum(c) || c == '_'; });
}
static void nameCheck(const std::string& s,const Source& loc) {
if(!identifier(s)) fail(loc,"invalid identifier: " + s.substr(0,128));
}
static void append(std::string& dst,const std::string& src,const Source& s) {
if(src.size() > valueLimit || dst.size() > valueLimit-src.size()) failLimit(s,"value expansion limit (1 MiB)");
dst += src;
}
static std::vector<Token> tokenize(const std::string& line,const Source& loc) {
std::vector<Token> result;
std::size_t p = 0;
while(p < line.size()) {
while(p < line.size() && std::isspace(static_cast<unsigned char>(line[p]))) ++p;
if(p == line.size() || line[p] == '#') break;
Token t;
bool quote = false;
while(p < line.size() && (quote || !std::isspace(static_cast<unsigned char>(line[p])))) {
char c = line[p++];
if(c == '"') { quote = !quote; t.literal = true; continue; }
if(c == '\\') {
t.literal = true;
if(p == line.size()) fail(loc,"incomplete escape");
c = line[p++];
if(c == 'n') c = '\n'; else if(c == 'r') c = '\r'; else if(c == 't') c = '\t';
else if(c != '\\' && c != '"' && c != '$' && c != '%' && c != '&' && c != '#') fail(loc,"unknown escape");
}
if(c == '\0') fail(loc,"NUL byte unavailable in source");
t.text += c;
}
if(quote) fail(loc,"unterminated quote");
if(result.size() >= 1024) failLimit(loc,"token count limit (1024)");
result.push_back(std::move(t));
}
return result;
}
static void count(const CodeLine& c,std::size_t lo,std::size_t hi) {
if(c.arg.size() < lo || c.arg.size() > hi) fail(c.source,"invalid argument count for " + c.arg[0].text);
}
static std::string destination(const Token& t,const Source& loc) {
if(t.literal || t.text.size() < 2 || t.text[0] != '$') fail(loc,"expected $destination");
auto name = t.text.substr(1); nameCheck(name,loc); return name;
}
static variable& lookup(Frame& f,const Token& t,const Source& loc) {
auto name = destination(t,loc);
auto it = f.vars.find(name);
if(it == f.vars.end()) fail(loc,"undefined variable: " + name);
return it->second;
}
static const std::string& textValue(variable& v,const Source& loc) {
if(v.textPending) {
auto text = format(v.numericValue,loc);
// Match assign's fresh-copy capacity; never retain a large old buffer.
std::string fresh(text.data(),text.size());
v.value.swap(fresh);
v.textPending = false;
}
return v.value;
}
static std::string value(Frame& f,const Token& t,const Source& loc) {
if(!t.literal && !t.text.empty() && t.text[0] == '$') {
auto& v = lookup(f,t,loc);
if(v.array) fail(loc,"array used as scalar");
if(v.structured) fail(loc,"JSON used as scalar; use json text/stringify");
return textValue(v,loc);
}
if(!t.literal && !t.text.empty() && t.text[0] == '%') {
auto digits = t.text.substr(1);
if(digits.empty() || digits.size() > 4 || !std::all_of(digits.begin(),digits.end(),[](char c) { return c >= '0' && c <= '9'; }))
fail(loc,"invalid argument index");
auto index = integer(digits,0,9999,loc);
if(index < 1 || static_cast<std::size_t>(index) > f.args.size()) fail(loc,"argument index out of bounds");
auto& v=f.args[static_cast<std::size_t>(index)-1];
if(v.array) fail(loc,"array used as scalar");
if(v.structured) fail(loc,"JSON used as scalar; use named parameter");
return textValue(v,loc);
}
return t.text;
}
static void assign(Frame& f,const Token& dest,const std::string& s,const Source& loc) {
if(s.size() > valueLimit) failLimit(loc,"value size limit");
auto& v = lookup(f,dest,loc);
// Replace storage, not just logical size: repeated large-to-small assignments
// must not retain an unaccounted allocation in every local variable.
v = variable{s,{},false};
}
static double numericValue(Frame& f,const Token& t,const Source& loc) {
if(!t.literal && !t.text.empty() && t.text[0] == '$') {
auto& v = lookup(f,t,loc);
if(v.array) fail(loc,"array used as scalar");
if(!v.numericKnown) {
if(v.structured) fail(loc,"JSON used as number; use json text");
v.numericValue = number(textValue(v,loc),loc);
v.numericKnown = true;
}
return v.numericValue;
}
return number(value(f,t,loc),loc);
}
static void assignNumber(Frame& f,const Token& dest,double n,const Source& loc) {
if(!std::isfinite(n)) fail(loc,"numeric result must be finite");
// strtod may reject rounded subnormals (ERANGE). Preserve the historical
// format-then-parse behavior on the next numeric read, not at assignment.
if(n != 0 && std::abs(n) <= 2*std::numeric_limits<double>::min()) {
assign(f,dest,format(n,loc),loc);
return;
}
auto& v = lookup(f,dest,loc);
// A pending numeric value already owns fresh empty scalar storage. Reusing
// that state avoids repeatedly resetting the utility shared_ptr/vector.
// Every other state still releases its storage (including materialized text,
// arrays and JSON); textPending is set only here, after that reset.
if(!v.textPending) v = variable{};
v.numericValue = n == 0 ? 0 : n; // format(-0) is "0"
v.numericKnown = true;
v.textPending = true;
}
static std::vector<std::string>& array(Frame& f,const Token& t,const Source& loc) {
auto& v = lookup(f,t,loc);
if(!v.array) fail(loc,"expected array");
return v.items;
}
static void assignArray(Frame& f,const Token& t,std::vector<std::string> a,const Source& loc) {
auto& v = lookup(f,t,loc); v = variable{"",std::move(a),true};
}
static void frameCheck(Frame& f,const Source& loc) {
// At most 24 bytes for a finite binary64 formatted to 17 significant
// digits (sign, digits, decimal point, exponent). Reserve 32 in accounting
// while lazy. If close to the limit, materialize and use exact capacities:
// a conservative reservation must not cause a new early-limit failure.
auto bytesUsed = [&]() {
std::size_t bytes = f.extraBytes;
for(const auto& entry:f.vars) {
const auto& v = entry.second;
bytes += entry.first.size() + (v.textPending ? std::max<std::size_t>(32,v.value.capacity()) : v.value.capacity())
+ v.items.capacity()*sizeof(std::string) + v.structuredBytes;
if(v.items.size() > itemLimit) failLimit(loc,"array item limit");
for(const auto& item:v.items) bytes += item.capacity();
}
return bytes;
};
if(bytesUsed() > frameLimit) {
for(auto& entry:f.vars) if(entry.second.textPending) textValue(entry.second,loc);
if(bytesUsed() > frameLimit) failLimit(loc,"frame data limit (2 MiB)");
}
}
static bool compare(Frame& f,const CodeLine& c) {
auto a = value(f,c.arg[1],c.source), b = value(f,c.arg[3],c.source);
double x,y;
int order;
if(numeric(a,x) && numeric(b,y)) order = x < y ? -1 : x > y ? 1 : 0;
else order = a.compare(b);
auto op = c.arg[2].text;
if(op == "==") return order == 0;
if(op == "!=") return order != 0;
if(op == "<") return order < 0;
if(op == ">") return order > 0;
if(op == "<=") return order <= 0;
return order >= 0;
}
#include "utilities.inc"
#include "stdlib.inc"
#include "worklog.inc"
#include "remote_sql.inc"
#include "web_cgi.inc"
#include "sockets.inc"
static const std::set<std::string> builtins = {
"websocket","udp","socket","function","kill","include","dll","dllimport","cgi","var","set","math","print","echo",
"if","else","end","loop","do","while","for","break","continue","point","goto","return","exit",
"call","env","len","substr","find","replace","upper","lower","trim","split","join","array","file","clock","random",
"args","input","json","map","try","catch","http","foreach","returnvalue","assert","date","csv","db","sql","web","text","number"
};
vScript::vScript(Options o):options(o),started(std::chrono::steady_clock::now()) {}
void vScript::Load(const std::string& file) {
loadFile(file,{file,0});
for(auto& item:functions) validate(item.second);
auto main = functions.find("main");
if(main == functions.end() && !options.testMode) fail({file,0},"missing main function");
if(main != functions.end() && main->second.argc != 0) fail(main->second.source,"main argument count must be zero");
}
void vScript::loadFile(const std::string& path,const Source& origin) {
std::error_code ec;
auto full = fs::canonical(path,ec);
if(ec || !fs::is_regular_file(full,ec)) fail(origin,"cannot read regular source file: " + path);
auto filename = full.string();
if(loading.count(filename)) fail(origin,"include cycle: " + filename);
if(loading.size() >= 32) failLimit(origin,"include depth limit (32)");
loading.insert(filename);
std::ifstream in(full,std::ios::binary);
if(!in) fail(origin,"cannot open source: " + filename);
vFunc* current = nullptr;
std::string line;
std::size_t lineNo = 0;
while(true) {
line.clear();
char ch;
bool any = false;
while(in.get(ch)) {
any = true;
if(++sourceBytes > 4*1024*1024) failLimit({filename,lineNo+1},"source byte limit (4 MiB)");
if(ch == '\n') break;
line += ch;
if(line.size() > 65536) failLimit({filename,lineNo+1},"line size limit (64 KiB)");
}
if(!any) break;
Source loc{filename,++lineNo};
if(++sourceLines > 100000) failLimit(loc,"source line limit (100000)");
CodeLine c; c.source = loc; c.arg = tokenize(line,loc);
if(c.arg.empty()) continue;
auto command = c.arg[0].text;
if(command == "dllimport" || command == "dll" || command == "cgi") fail(loc,"DLL/CGI unavailable in Linux restoration");
if(command == "function") {
count(c,2,1024);
if(current) fail(loc,"nested function; missing kill");
nameCheck(c.arg[1].text,loc);
if(builtins.count(c.arg[1].text)) fail(loc,"reserved function name");
if(functions.size() >= 4096) failLimit(loc,"function count limit");
vFunc f; f.name = c.arg[1].text; f.source = loc;
if(c.arg.size()==3 && !identifier(c.arg[2].text))
f.argc = static_cast<std::size_t>(integer(c.arg[2].text,0,1024,loc));
else {
std::set<std::string> names;
for(std::size_t j=2;j<c.arg.size();++j) {
const auto& parameter=c.arg[j]; nameCheck(parameter.text,loc);
if(parameter.literal || builtins.count(parameter.text)) fail(loc,"reserved or quoted parameter name");
if(!names.insert(parameter.text).second) fail(loc,"duplicate parameter name");
f.parameters.push_back(parameter.text);
}
f.argc=f.parameters.size();
}
auto inserted = functions.emplace(f.name,std::move(f));
if(!inserted.second) fail(loc,"duplicate function");
current = &inserted.first->second;
} else if(command == "kill") {
count(c,2,2);
if(!current || c.arg[1].text != current->name) fail(loc,"mismatched kill function");
current = nullptr;
} else if(command == "include") {
count(c,2,2);
if(current) fail(loc,"include only permitted outside functions");
loadFile((full.parent_path()/c.arg[1].text).string(),loc);
} else {
if(!current) fail(loc,"command outside function");
current->line.push_back(std::move(c));
}
}
if(in.bad()) fail({filename,lineNo},"source read failed");
if(current) fail(current->source,"missing kill " + current->name);
loading.erase(filename);
}
void vScript::validate(vFunc& f) {
std::vector<std::size_t> stack;
for(std::size_t i=0;i<f.line.size();++i) {
auto& c = f.line[i]; const auto& a = c.arg;
const auto& op = a[0].text;
c.scope = stack;
for(auto it=stack.rbegin();it!=stack.rend();++it) {
const auto& kind=f.line[*it].arg[0].text;
if(kind=="for" || kind=="foreach" || kind=="loop" || kind=="do") { c.loop = *it; break; }
}
auto dest = [&](std::size_t k) { destination(a[k],c.source); };
if(standardCommand(c)) standardValidate(c);
else if(op=="args" || op=="input" || op=="json" || op=="map" || op=="http") utilityValidate(c);
else if(op=="date" || op=="csv" || op=="db") worklogValidate(c);
else if(op=="sql") remoteSqlValidate(c);
else if(op=="web") webValidate(c);
else if(op=="websocket") websocketValidate(c);
else if(op=="socket") socketValidate(c);
else if(op=="udp") foundationValidate(c);
else if(op=="returnvalue") count(c,2,2);
else if(op=="foreach") { count(c,3,4); dest(1); dest(2); if(a.size()==4) dest(3); }
else if(op=="try") count(c,1,1);
else if(op=="catch") { count(c,2,2); dest(1); }
else if(op == "if" || op == "loop" || op == "while" || op=="assert") {
count(c,4,4);
if(std::set<std::string>{"==","!=","<",">","<=",">="}.count(a[2].text) == 0) fail(c.source,"invalid comparison operator");
} else if(op == "for") { count(c,4,5); dest(1); }
else if(op == "do" || op == "else" || op == "break" || op == "continue" || op == "exit") count(c,1,1);
else if(op == "end") count(c,2,2);
else if(op == "var") { count(c,2,1024); for(std::size_t j=1;j<a.size();++j) nameCheck(a[j].text,c.source); }
else if(op == "set" || op == "math") { count(c,op == "math" ? 3 : 2,1024); dest(1); }
else if(op == "print" || op == "echo" || op == "return") count(c,1,1024);
else if(op == "point" || op == "goto") { count(c,2,2); nameCheck(a[1].text,c.source); }
else if(op == "call") {
count(c,3,1024); dest(1);
auto target = functions.find(a[2].text);
if(target == functions.end()) fail(c.source,"unknown function: " + a[2].text);
if(a.size()-3 != target->second.argc) fail(c.source,"invalid argument count for call");
} else if(op == "env") {
count(c,4,4);
if(a[1].text == "get") dest(2);
else if(a[1].text != "set") fail(c.source,"unknown env operation");
} else if(op == "len" || op == "upper" || op == "lower" || op == "trim") { count(c,3,3); dest(1); }
else if(op == "substr" || op == "replace") { count(c,5,5); dest(1); }
else if(op == "find" || op == "split" || op == "join") { count(c,4,4); dest(1); if(op == "join") dest(2); }
else if(op == "array") {
count(c,3,1024); auto sub = a[1].text;
if(sub == "create") { dest(2); }
else if(sub == "push") { count(c,4,4); dest(2); }
else if(sub == "get" || sub == "set") { count(c,5,5); dest(2); if(sub == "get") dest(3); }
else if(sub == "len" || sub == "pop") { count(c,4,4); dest(2); dest(3); }
else fail(c.source,"unknown array operation");
} else if(op == "file") {
count(c,3,5);
auto sub=a[1].text;
if(sub=="close") count(c,3,3);
else if(sub=="readline") { count(c,5,5); dest(2); dest(3); }
else {
count(c,4,4);
if(sub=="read" || sub=="exists" || sub=="open" || sub=="list") dest(2);
else if(sub!="write" && sub!="append" && sub!="copy" && sub!="rename" && sub!="create") fail(c.source,"unknown file operation");
}
} else if(op == "clock") { count(c,2,2); dest(1); }
else if(op == "random") { count(c,3,4); if(a[1].text == "seed") count(c,3,3); else { count(c,4,4); dest(1); } }
else {
auto target = functions.find(op);
if(target == functions.end()) fail(c.source,"unknown command: " + op);
if(a.size()-1 != target->second.argc) fail(c.source,"invalid argument count for " + op);
}
if(op == "if" || op == "loop" || op == "do" || op == "for" || op=="foreach" || op=="try") {
if(stack.size() >= 128) failLimit(c.source,"block nesting limit (128)");
c.alternate = none; stack.push_back(i);
} else if(op=="catch") {
if(stack.empty() || f.line[stack.back()].arg[0].text!="try" || f.line[stack.back()].alternate!=none) fail(c.source,"unmatched or duplicate catch");
f.line[stack.back()].alternate=i;
} else if(op == "else") {
if(stack.empty() || f.line[stack.back()].arg[0].text != "if" || f.line[stack.back()].alternate != none) fail(c.source,"unmatched or duplicate else");
f.line[stack.back()].alternate = i;
} else if(op == "end" || op == "while") {
auto expected = op == "while" ? "do" : a[1].text;
if(stack.empty() || f.line[stack.back()].arg[0].text != expected || (op == "end" && expected != "if" && expected != "for" && expected != "foreach" && expected != "loop" && expected!="try")) fail(c.source,"mismatched block end");
if(expected=="try" && f.line[stack.back()].alternate==none) fail(c.source,"try requires catch");
auto start = stack.back(); stack.pop_back();
c.match = start; f.line[start].match = i;
if(f.line[start].alternate != none) f.line[f.line[start].alternate].match = i;
} else if((op == "break" || op == "continue") && c.loop == none) fail(c.source,op + " outside loop");
}
if(!stack.empty()) fail(f.line[stack.back()].source,"unclosed block");
}
struct Exit {};
void flushOutput(const Source& source) {
std::cout.flush();
if(!std::cout) fail(source,"output write failed");
}
void vScript::Run() {
try { runFunction("main",{},0,functions.at("main").source); } catch(const Exit&) {}
// Exit and ordinary returns both reach this check before CLI success.
flushOutput(outputSource);
}
bool vScript::Test(const std::string& file) {
std::size_t passed=0, failed=0;
for(const auto& entry:functions) {
if(entry.first.rfind("test_",0)!=0) continue;
try {
if(entry.second.argc) fail(entry.second.source,"test functions must take zero arguments");
vScript isolated(options); isolated.Load(file);
isolated.runFunction(entry.first,{},0,entry.second.source);
flushOutput(isolated.outputSource);
std::cout << "PASS " << entry.first << '\n'; ++passed;
} catch(const Exit&) {
std::cerr << entry.second.source.file << ':' << entry.second.source.line << ": error: exit is not a test pass\n";
std::cout << "FAIL " << entry.first << '\n'; ++failed;
} catch(const Error& e) {
std::cerr << e.source.file << ':' << e.source.line << ": error: " << e.what() << '\n';
std::cout << "FAIL " << entry.first << '\n'; ++failed;
}
}
std::cout << "tests: " << passed << " passed, " << failed << " failed\n";
flushOutput(outputSource);
return passed>0 && failed==0;
}
variable vScript::runFunction(const std::string& name,const std::vector<variable>& args,std::size_t depth,const Source& callSite) {
if(depth >= 64) failLimit(callSite,"recursion limit (64)");
const auto& f = functions.at(name);
Frame frame; frame.args = args;
for(std::size_t i=0;i<f.parameters.size();++i) frame.vars.emplace(f.parameters[i],args[i]);
for(const auto& arg:args) { frame.extraBytes += variableSize(arg); if(frame.extraBytes > frameLimit) failLimit(callSite,"argument data limit"); }
frameCheck(frame,callSite);
struct EachState { variable snapshot; std::size_t index=0; Json::const_iterator it; };
std::map<std::size_t,EachState> each;
auto eraseEach=[&](std::size_t key) { auto it=each.find(key); if(it!=each.end()) { frame.extraBytes-=variableSize(it->second.snapshot); each.erase(it); } };
auto eachPut=[&](std::size_t key) {
auto& state=each.at(key); const auto& header=f.line[key];
const auto& v=state.snapshot; auto n=header.arg.size();
std::string k=std::to_string(state.index);
if(v.structured) {
if(v.structured->data.is_object()) k=state.it.key();
putJson(frame,header.arg[n-2],*state.it,header.source);
} else assign(frame,header.arg[n-2],v.items[state.index],header.source);
if(n==4) assign(frame,header.arg[1],k,header.source);
};
struct ForState { double end,step; };
std::map<std::size_t,ForState> fors;
for(std::size_t pc=0;pc<f.line.size();) {
const auto& c = f.line[pc]; const auto& a = c.arg; const auto& s = c.source;
// Borrow validated command text. string_view comparisons keep literal
// dispatch local even when utility templates enlarge this translation unit.
const std::string_view op = a[0].text;
if(++steps > options.maxSteps) failLimit(s,"step limit exceeded");
auto val = [&](std::size_t k) { return value(frame,a[k],s); };
auto put = [&](std::size_t k,const std::string& text) { assign(frame,a[k],text,s); };
auto concat = [&](std::size_t k) { std::string result; for(;k<a.size();++k) append(result,val(k),s); return result; };
std::size_t next = pc+1;
try {
if(op == "var") {
for(std::size_t i=1;i<a.size();++i) {
if(frame.vars.size() >= 4096 && !frame.vars.count(a[i].text)) failLimit(s,"variable count limit");
frame.vars.emplace(a[i].text,variable{});
}
} else if(op == "set") put(1,concat(2));
else if(op == "math") {
std::string expr;
if(!c.math.ready) for(std::size_t i=2;i<a.size();++i) { if(i>2) append(expr," ",s); append(expr,a[i].text,s); }
assignNumber(frame,a[1],expression(expr,s,[&](const std::string& token) { return numericValue(frame,{token,false},s); },c.math),s);
} else if(op == "print" || op == "echo") {
auto text = concat(1);
emit(text,s);
} else if(op == "return") return variable{concat(1),{},false};
else if(op == "returnvalue") return typedValue(frame,a[1],s);
else if(op == "assert") { if(!compare(frame,c)) fail(s,"assertion failed"); }
else if(op == "exit") throw Exit{};
else if(op == "if") { if(!compare(frame,c)) next = c.alternate == none ? c.match+1 : c.alternate+1; }
else if(op == "else") next = c.match+1;
else if(op == "loop") { if(!compare(frame,c)) next = c.match+1; }
else if(op == "do") {}
else if(op == "while") { if(compare(frame,c)) next = c.match+1; }
else if(op == "foreach") {
lookup(frame,a[1],s); if(a.size()==4) { lookup(frame,a[2],s); if(a[1].text==a[2].text) fail(s,"foreach destinations must differ"); }
auto snapshot=typedValue(frame,a.back(),s);
if(!snapshot.array && (!snapshot.structured || !snapshot.structured->data.is_structured())) fail(s,"foreach requires array or object");
auto size=snapshot.array ? snapshot.items.size() : snapshot.structured->data.size();
eraseEach(pc);
if(!size) next=c.match+1;
else {
frame.extraBytes+=variableSize(snapshot); frameCheck(frame,s);
auto& state=each[pc]; state.snapshot=std::move(snapshot);
if(state.snapshot.structured) state.it=state.snapshot.structured->data.cbegin();
eachPut(pc);
}
}
else if(op == "for") {
double start = numericValue(frame,a[2],s), end = numericValue(frame,a[3],s), step = a.size() == 5 ? numericValue(frame,a[4],s) : 1;
if(step == 0) fail(s,"for step must not be zero");
assignNumber(frame,a[1],start,s);
if(step > 0 ? start >= end : start <= end) next = c.match+1;
else fors[pc] = {end,step};
} else if(op == "end") {
const std::string_view kind = a[1].text;
if(kind == "loop") next = c.match;
else if(kind=="foreach") {
auto& state=each.at(c.match); auto& v=state.snapshot;
auto size=v.array ? v.items.size() : v.structured->data.size();
if(++state.index<size) { if(v.structured) ++state.it; eachPut(c.match); next=c.match+1; }
else eraseEach(c.match);
}
else if(kind == "for") {
auto it = fors.find(c.match);
if(it == fors.end()) fail(s,"inactive for block");
const auto& dest = f.line[c.match].arg[1];
double old = numericValue(frame,dest,s), n = old+it->second.step;
if(!std::isfinite(n)) fail(s,"for result must be finite");
if(n == old) fail(s,"for step makes no numeric progress");
assignNumber(frame,dest,n,s);
if(it->second.step > 0 ? n < it->second.end : n > it->second.end) next = c.match+1;
else fors.erase(it);
}
} else if(op == "break" || op == "continue") {
auto end = f.line[c.loop].match;
if(op == "break") { fors.erase(c.loop); eraseEach(c.loop); next = end+1; }
else next = end;
} else if(op == "point") frame.points[a[1].text] = pc;
else if(op == "goto") {
auto it = frame.points.find(a[1].text);
if(it == frame.points.end()) fail(s,"point has not been visited");
if(f.line[it->second].scope != c.scope) fail(s,"goto cannot cross block boundaries");
for(auto scope:c.scope) {
const auto& block=f.line[scope];
if(block.arg[0].text=="try" && (pc<block.alternate)!=(it->second<block.alternate))
fail(s,"goto cannot cross catch boundary");
}
next = it->second+1;
} else if(op == "env") {
if(a[1].text == "get") {
auto key=val(3);
if(key.empty() || key.find('=') != std::string::npos || key.find('\0') != std::string::npos) fail(s,"invalid environment name");
auto changed=changedEnvironment.find(key);
if(changed != changedEnvironment.end()) put(2,changed->second);
else { const char* p=std::getenv(key.c_str()); put(2,p ? p : ""); }
} else {
auto key=val(2), text=val(3);
if(key.empty() || key.find('=') != std::string::npos || key.find('\0') != std::string::npos || text.find('\0') != std::string::npos) fail(s,"invalid environment name/value");
// Script-local overlay keeps this implementation standard-library only.
if(changedEnvironment.size() >= 4096 && !changedEnvironment.count(key)) failLimit(s,"environment entry limit");
std::size_t bytes=key.size()+text.size();
for(const auto& entry:changedEnvironment) if(entry.first != key) bytes+=entry.first.size()+entry.second.size();
if(bytes > frameLimit) failLimit(s,"environment data limit (2 MiB)");
changedEnvironment[key]=std::move(text);
}
} else if(op == "len") put(1,std::to_string(val(2).size()));
else if(op == "substr") {
auto text=val(2); auto start=static_cast<std::size_t>(integer(val(3),0,static_cast<long long>(text.size()),s));
auto length=static_cast<std::size_t>(integer(val(4),0,static_cast<long long>(valueLimit),s));
put(1,text.substr(start,length));
} else if(op == "find") {
auto at=val(2).find(val(3)); put(1,at == std::string::npos ? "-1" : std::to_string(at));
} else if(op == "replace") {
auto text=val(2), needle=val(3), replacement=val(4); std::string result;
if(needle.empty()) fail(s,"empty replace needle");
std::size_t pos=0,at;
while((at=text.find(needle,pos)) != std::string::npos) {
append(result,text.substr(pos,at-pos),s); append(result,replacement,s); pos=at+needle.size();
}
append(result,text.substr(pos),s); put(1,result);
} else if(op == "upper" || op == "lower" || op == "trim") {
auto text=val(2);
if(op == "trim") {
auto begin=text.find_first_not_of(" \t\r\n\f\v"), end=text.find_last_not_of(" \t\r\n\f\v");
text=begin == std::string::npos ? "" : text.substr(begin,end-begin+1);
} else for(char& ch:text) {
if(op == "upper" && ch >= 'a' && ch <= 'z') ch=static_cast<char>(ch-'a'+'A');
if(op == "lower" && ch >= 'A' && ch <= 'Z') ch=static_cast<char>(ch-'A'+'a');
}
put(1,text);
} else if(op == "split") {
auto text=val(2), delimiter=val(3); std::vector<std::string> parts;
if(delimiter.empty()) fail(s,"empty split delimiter");
std::size_t pos=0,at;
while((at=text.find(delimiter,pos)) != std::string::npos) {
if(parts.size() >= itemLimit-1) failLimit(s,"array item limit");
parts.push_back(text.substr(pos,at-pos)); pos=at+delimiter.size();
}
parts.push_back(text.substr(pos)); assignArray(frame,a[1],std::move(parts),s);
} else if(op == "join") {
const auto& items=array(frame,a[2],s); auto delimiter=val(3); std::string text;
for(std::size_t i=0;i<items.size();++i) { if(i) append(text,delimiter,s); append(text,items[i],s); }
put(1,text);
} else if(standardCommand(c)) { standardExecute(frame,c);
} else if(op == "array") {
auto sub=a[1].text;
if(sub == "create") {
std::vector<std::string> items;
std::size_t bytes=0;
for(std::size_t i=3;i<a.size();++i) {
auto text=val(i); bytes+=text.size()+sizeof(std::string);
if(bytes>frameLimit) failLimit(s,"array data limit (2 MiB)");
items.push_back(std::move(text));
}
assignArray(frame,a[2],std::move(items),s);
} else if(sub == "push") {
auto text=val(3); auto& items=array(frame,a[2],s);
if(items.size() >= itemLimit) failLimit(s,"array item limit");
items.push_back(std::move(text));
} else if(sub == "set") {
auto text=val(4); auto& items=array(frame,a[2],s);
auto index=static_cast<std::size_t>(integer(val(3),0,static_cast<long long>(itemLimit),s));
if(index >= items.size()) fail(s,"array index out of bounds");
items[index]=std::move(text);
} else {
auto& items=array(frame,a[3],s); std::string result;
if(sub == "len") result=std::to_string(items.size());
else if(sub == "pop") {
if(items.empty()) fail(s,"cannot pop empty array");
result=items.back(); items.pop_back();
} else {
auto index=static_cast<std::size_t>(integer(val(4),0,static_cast<long long>(itemLimit),s));
if(index >= items.size()) fail(s,"array index out of bounds");
result=items[index];
}
put(2,result);
}
} else if(op=="try") { lookup(frame,f.line[c.alternate].arg[1],s); }
else if(op=="catch") next=c.match+1;
else if(utility(frame,c)) {}
else if(worklog(frame,c)) {}
else if(remoteSql(frame,c)) {}
else if(webCommand(frame,c)) {}
else if(op=="websocket") { websocketCommand(frame,c); }
else if(op=="socket") { socketCommand(frame,c); }
else if(op=="udp") { foundationCommand(frame,c); }
else if(op == "file") {
if(!options.allowFiles) fail(s,"file operations require --allow-files (not a sandbox)");
auto sub=a[1].text, path=val(sub == "read" || sub == "exists" ? 3 : 2);
if(path.empty() || path.find('\0') != std::string::npos) fail(s,"invalid file path");
std::error_code ec;
bool exists=fs::exists(path,ec);
if(ec) fail(s,"file status failed: " + ec.message());
if(sub == "exists") put(2,exists ? "1" : "0");
else if(sub == "read") {
if(!fs::is_regular_file(path,ec) || ec) fail(s,"file read requires regular file");
std::ifstream in(path,std::ios::binary); if(!in) fail(s,"file read open failed");
std::string text; char buffer[8192];
while(in.read(buffer,sizeof(buffer)) || in.gcount()) append(text,std::string(buffer,static_cast<std::size_t>(in.gcount())),s);
if(in.bad()) fail(s,"file read failed");
put(2,text);
} else {
auto text=val(3);
if(text.size() > 16*1024*1024-fileBytes) failLimit(s,"file write byte limit (16 MiB)");
fileBytes+=text.size();
if(exists && (!fs::is_regular_file(path,ec) || ec)) fail(s,"file write requires regular file");
std::ofstream out(path,std::ios::binary | (sub == "append" ? std::ios::app : std::ios::trunc));
if(!out) fail(s,"file write open failed");
out.write(text.data(),static_cast<std::streamsize>(text.size())); out.close();
if(!out) fail(s,"file write failed");
}
} else if(op == "clock") {
auto ms=std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-started).count(); put(1,format(ms,s));
} else if(op == "random") {
if(a[1].text == "seed") randomEngine.seed(static_cast<std::uint32_t>(integer(val(2),0,4294967295LL,s)));
else {
auto low=integer(val(2),-2147483648LL,2147483647LL,s), high=integer(val(3),-2147483648LL,2147483647LL,s);
if(high < low) fail(s,"invalid random range");
// Explicit rejection mapping gives identical sequences across standard libraries.
auto range=static_cast<std::uint64_t>(high-low)+1;
auto limit=(std::uint64_t{1}<<32)/range*range;
std::uint64_t draw;
do { draw=randomEngine(); } while(draw >= limit);
put(1,std::to_string(low+static_cast<long long>(draw%range)));
}
} else {
bool capture=op == "call";
std::vector<variable> values;
std::size_t bytes=0;
for(std::size_t i=capture ? 3 : 1;i<a.size();++i) {
auto text=typedValue(frame,a[i],s); bytes+=variableSize(text); if(bytes>frameLimit) failLimit(s,"argument data limit"); values.push_back(std::move(text));
}
if(capture) lookup(frame,a[1],s); // validate destination before callee side effects
auto result=runFunction(capture ? a[2].text : a[0].text,values,depth+1,s);
if(capture) lookup(frame,a[1],s)=std::move(result);
}
frameCheck(frame,s);
} catch(const Error& error) {
if(error.fatal) throw;
bool handled=false;
// Lexical handlers need no mutable stack: nonlocal exits cannot
// leave stale handlers behind. A catch never catches itself.
for(auto it=c.scope.rbegin();it!=c.scope.rend();++it) {
const auto& start=f.line[*it];
if(start.arg[0].text!="try" || pc>=start.alternate) continue;
const auto& handler=f.line[start.alternate];
assign(frame,handler.arg[1],error.source.file+":"+std::to_string(error.source.line)+": error: "+error.what(),handler.source);
for(auto loop=fors.begin();loop!=fors.end();) {
if(loop->first>*it && loop->first<start.match) loop=fors.erase(loop); else ++loop;
}
for(auto loop=each.begin();loop!=each.end();) {
auto key=loop->first; ++loop;
if(key>*it && key<start.match) eraseEach(key);
}
frameCheck(frame,handler.source);
next=start.alternate+1; handled=true; break;
}
if(!handled) throw;
}
pc=next;
}
return variable{};
}
} // namespace vanilla