// Linux restoration of the vScript/vFunc/CodeLine/variable model. // All storage, parsing, validation and execution machinery below is rewritten. #include "script.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace vanilla { namespace fs = std::filesystem; static constexpr std::size_t none = static_cast(-1); static bool identifier(const std::string& s) { if(s.empty() || s.size() > 128 || !(std::isalpha(static_cast(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 tokenize(const std::string& line,const Source& loc) { std::vector result; std::size_t p = 0; while(p < line.size()) { while(p < line.size() && std::isspace(static_cast(line[p]))) ++p; if(p == line.size() || line[p] == '#') break; Token t; bool quote = false; while(p < line.size() && (quote || !std::isspace(static_cast(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(index) > f.args.size()) fail(loc,"argument index out of bounds"); auto& v=f.args[static_cast(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::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& 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 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(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 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(integer(c.arg[2].text,0,1024,loc)); else { std::set names; for(std::size_t j=2;jsecond; } 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 stack; for(std::size_t i=0;i{"==","!=","<",">","<=",">="}.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;jsecond.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& 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 frameLimit) failLimit(callSite,"argument data limit"); } frameCheck(frame,callSite); struct EachState { variable snapshot; std::size_t index=0; Json::const_iterator it; }; std::map 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 fors; for(std::size_t pc=0;pc 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= 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;i2) 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.indexsecond.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" && (pcsecondsecond+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(integer(val(3),0,static_cast(text.size()),s)); auto length=static_cast(integer(val(4),0,static_cast(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(ch-'a'+'A'); if(op == "lower" && ch >= 'A' && ch <= 'Z') ch=static_cast(ch-'A'+'a'); } put(1,text); } else if(op == "split") { auto text=val(2), delimiter=val(3); std::vector 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; std::size_t bytes=0; for(std::size_t i=3;iframeLimit) 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(integer(val(3),0,static_cast(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(integer(val(4),0,static_cast(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(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(text.size())); out.close(); if(!out) fail(s,"file write failed"); } } else if(op == "clock") { auto ms=std::chrono::duration(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(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(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(draw%range))); } } else { bool capture=op == "call"; std::vector values; std::size_t bytes=0; for(std::size_t i=capture ? 3 : 1;iframeLimit) 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->firstfirst; ++loop; if(key>*it && key