// Utility implementation included in script.cpp to share the legacy value helpers. // Structured values are immutable; ordinary reads never copy their trees. using Json = nlohmann::json; struct Structured { Json data; explicit Structured(Json j):data(std::move(j)) {} }; static std::size_t jsonSize(const Json& j,const Source& s,std::size_t depth=0,std::size_t* countPtr=nullptr) { std::size_t local=0; auto& nodes=countPtr ? *countPtr : local; if(depth > 64) failLimit(s,"JSON depth limit (64)"); if(++nodes > itemLimit) failLimit(s,"JSON node limit (16384)"); std::size_t bytes=128; // conservative node/map/allocation overhead if(j.is_string()) bytes+=j.get_ref().capacity(); if(j.is_number()) { double n=j.get(); if(!std::isfinite(n)) fail(s,"JSON number must be finite"); if(std::trunc(n)==n && std::abs(n)>9007199254740991.0) fail(s,"JSON integer outside exact safe range"); } if(j.is_array()) bytes+=j.get_ref().capacity()*sizeof(Json); if(j.is_structured()) for(auto it=j.begin();it!=j.end();++it) { if(j.is_object()) bytes+=it.key().capacity(); bytes+=jsonSize(it.value(),s,depth+1,&nodes); if(bytes>valueLimit) failLimit(s,"JSON storage limit (1 MiB)"); } if(bytes>valueLimit) failLimit(s,"JSON storage limit (1 MiB)"); return bytes; } static const Json& jsonValue(Frame& f,const Token& t,const Source& s) { auto& v=lookup(f,t,s); if(!v.structured) fail(s,"expected JSON value"); return v.structured->data; } static void putJson(Frame& f,const Token& t,Json j,const Source& s) { auto bytes=jsonSize(j,s); auto data=std::make_shared(std::move(j)); auto& v=lookup(f,t,s); v=variable{}; v.structured=std::move(data); v.structuredBytes=bytes; } static std::string jsonDiagnostic(const Json::exception& e) { // Library diagnostics may echo invalid UTF-8/control bytes from the input. std::string message=e.what(); for(char& ch:message) if(static_cast(ch)<32 || static_cast(ch)>=127) ch='?'; return message; } static Json parseJson(const std::string& text,const Source& s) { if(text.size()>valueLimit) failLimit(s,"JSON input limit (1 MiB)"); std::vector> keys; std::size_t nodes=0; try { return Json::parse(text,[&](int depth,Json::parse_event_t event,Json& parsed) { if(depth>64) failLimit(s,"JSON depth limit (64)"); if(event==Json::parse_event_t::object_start) keys.emplace_back(); if(event==Json::parse_event_t::object_end) keys.pop_back(); if(event==Json::parse_event_t::key && !keys.back().insert(parsed.get()).second) fail(s,"duplicate JSON key"); if(event==Json::parse_event_t::value || event==Json::parse_event_t::object_start || event==Json::parse_event_t::array_start) if(++nodes>itemLimit) failLimit(s,"JSON node limit (16384)"); return true; }); } catch(const Json::exception& e) { fail(s,std::string("invalid JSON: ")+jsonDiagnostic(e)); } } static std::string jsonDump(const Json& j,const Source& s) { // Encoded output can expand strings by six; check incrementally, without // first allocating an unbounded serialized result. struct Buffer : std::streambuf { std::string text; bool overflowed=false; int_type overflow(int_type ch) override { if(traits_type::eq_int_type(ch,traits_type::eof())) return traits_type::not_eof(ch); if(text.size()>=valueLimit) { overflowed=true; return traits_type::eof(); } text.push_back(traits_type::to_char_type(ch)); return ch; } std::streamsize xsputn(const char* p,std::streamsize n) override { auto size=static_cast(n); if(size>valueLimit-text.size()) { overflowed=true; return 0; } text.append(p,size); return n; } } buffer; try { std::ostream out(&buffer); out< readLine(std::istream& in,std::size_t& total,const Source& s) { std::string line; char ch; bool any=false, newline=false; while(in.get(ch)) { any=true; if(++total>16*valueLimit) failLimit(s,"input byte limit (16 MiB)"); if(ch=='\n') { newline=true; break; } if(line.size()>=65536) failLimit(s,"input line limit (64 KiB)"); line+=ch; } if(in.bad() || (in.fail() && !in.eof())) fail(s,"input read failed"); if(newline && !line.empty() && line.back()=='\r') line.pop_back(); return {std::move(line),!any}; } struct CurlSession { CURL* handle=nullptr; CurlSession() { handle=curl_easy_init(); } ~CurlSession() { if(handle) curl_easy_cleanup(handle); } }; struct CurlGlobal { CURLcode result=curl_global_init(CURL_GLOBAL_DEFAULT); ~CurlGlobal() { if(result==CURLE_OK) curl_global_cleanup(); } }; struct HttpBuffer { std::string body; std::size_t maximum=0, headers=0; bool exceeded=false, allocationFailed=false; }; static std::size_t httpBody(char* p,std::size_t size,std::size_t count,void* data) noexcept { auto& b=*static_cast(data); if(size && count>std::numeric_limits::max()/size) { b.exceeded=true; return 0; } auto n=size*count; if(n>b.maximum-b.body.size()) { b.exceeded=true; return 0; } try { b.body.append(p,n); } catch(...) { b.allocationFailed=true; return 0; } return n; } static std::size_t httpHeader(char*,std::size_t size,std::size_t count,void* data) noexcept { auto& b=*static_cast(data); if(size && count>std::numeric_limits::max()/size) { b.exceeded=true; return 0; } auto n=size*count; if(n>65536-b.headers) { b.exceeded=true; return 0; } b.headers+=n; return n; } static void utilityValidate(const CodeLine& c) { const auto& a=c.arg; const auto& op=a[0].text; auto dest=[&](std::size_t k) { destination(a[k],c.source); }; if(op=="args") { count(c,3,4); if(a[1].text=="count" || a[1].text=="all") count(c,3,3); else if(a[1].text=="get") count(c,4,4); else fail(c.source,"unknown args operation"); dest(2); } else if(op=="input") { count(c,3,3); dest(1); dest(2); } else if(op=="json") { count(c,4,5); auto sub=a[1].text; dest(2); if(sub=="parse" || sub=="stringify" || sub=="type" || sub=="text" || sub=="string") count(c,4,4); else if(sub=="get") count(c,5,5); else fail(c.source,"unknown json operation"); if(sub!="parse" && sub!="string") dest(3); } else if(op=="map") { count(c,3,5); auto sub=a[1].text; dest(2); if(sub=="create") count(c,3,3); else if(sub=="keys" || sub=="remove") { count(c,4,4); if(sub=="keys") dest(3); } else if(sub=="get" || sub=="has" || sub=="set") { count(c,5,5); if(sub!="set") dest(3); } else fail(c.source,"unknown map operation"); } else if(op=="http") { count(c,7,8); dest(2); dest(3); if(a[1].text=="get") count(c,7,7); else if(a[1].text=="post") count(c,8,8); else fail(c.source,"unknown http operation"); } } bool vScript::utility(Frame& frame,const CodeLine& c) { const auto& a=c.arg; const auto& s=c.source; const auto& op=a[0].text; 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); }; if(op=="args") { if(a[1].text=="count") put(2,std::to_string(options.arguments.size())); else if(a[1].text=="all") assignArray(frame,a[2],options.arguments,s); else { auto i=static_cast(integer(val(3),0,1024,s)); if(i>=options.arguments.size()) fail(s,"CLI argument index out of bounds"); put(2,options.arguments[i]); } } else if(op=="input") { lookup(frame,a[1],s); lookup(frame,a[2],s); if(a[1].text==a[2].text) fail(s,"input destinations must differ"); auto line=readLine(std::cin,inputBytes,s); put(1,line.first); put(2,line.second ? "1" : "0"); } else if(op=="json") { auto sub=a[1].text; lookup(frame,a[2],s); if(sub=="parse") putJson(frame,a[2],parseJson(val(3),s),s); else if(sub=="string") putJson(frame,a[2],Json(val(3)),s); else { const auto& j=jsonValue(frame,a[3],s); if(sub=="stringify") put(2,jsonDump(j,s)); else if(sub=="type") put(2,j.is_number() ? "number" : j.type_name()); else if(sub=="text") { if(j.is_structured()) fail(s,"JSON text requires scalar JSON"); auto text=j.is_string() ? j.get() : jsonDump(j,s); put(2,text); } else { auto key=val(4); if(j.is_object()) { auto it=j.find(key); if(it==j.end()) fail(s,"missing JSON key"); putJson(frame,a[2],*it,s); } else if(j.is_array()) { auto index=static_cast(integer(key,0,static_cast(itemLimit),s)); if(index>=j.size()) fail(s,"JSON array index out of bounds"); putJson(frame,a[2],j[index],s); } else fail(s,"JSON get requires object or array"); } } } else if(op=="map") { auto sub=a[1].text; lookup(frame,a[2],s); if(sub=="create") putJson(frame,a[2],Json::object(),s); else { auto index=(sub=="set" || sub=="remove") ? 2U : 3U; const auto& object=jsonValue(frame,a[index],s); if(!object.is_object()) fail(s,"expected map/object"); if(sub=="keys") { std::vector keys; for(auto it=object.begin();it!=object.end();++it) keys.push_back(it.key()); assignArray(frame,a[2],std::move(keys),s); } else { auto key=val(sub=="set" || sub=="remove" ? 3 : 4); if(sub=="has") put(2,object.contains(key) ? "1" : "0"); else if(sub=="get") { auto it=object.find(key); if(it==object.end()) fail(s,"missing map key"); putJson(frame,a[2],*it,s); } else { // Mutations are explicit bounded copies; get/stringify/type never // copy their source. Copy before assignment makes aliasing safe. Json copy=object; if(sub=="remove") copy.erase(key); else if(!a[4].literal && !a[4].text.empty() && a[4].text[0]=='$' && lookup(frame,a[4],s).structured) copy[key]=jsonValue(frame,a[4],s); else copy[key]=val(4); putJson(frame,a[2],std::move(copy),s); } } } } else if(op=="file" && (a[1].text=="open" || a[1].text=="readline" || a[1].text=="close" || a[1].text=="list" || a[1].text=="copy" || a[1].text=="rename")) { if(!options.allowFiles) fail(s,"file operations require --allow-files (not a sandbox)"); const auto& sub=a[1].text; if(sub=="open") { lookup(frame,a[2],s); auto path=checkedPath(val(3),s); std::error_code ec; if(!fs::is_regular_file(path,ec) || ec) fail(s,"file open requires regular file"); if(readers.size()>=32) failLimit(s,"open file handle limit (32)"); auto in=std::make_unique(path,std::ios::binary); if(!*in) fail(s,"file open failed"); auto id=nextReader++; readers.emplace(id,std::move(in)); put(2,std::to_string(id)); } else if(sub=="readline" || sub=="close") { if(sub=="readline") { lookup(frame,a[2],s); lookup(frame,a[3],s); if(a[2].text==a[3].text) fail(s,"readline destinations must differ"); } auto id=static_cast(integer(val(sub=="close" ? 2 : 4),1,100000000,s)); auto it=readers.find(id); if(it==readers.end()) fail(s,"invalid or closed file handle"); if(sub=="close") readers.erase(it); else { auto line=readLine(*it->second,readBytes,s); put(2,line.first); put(3,line.second ? "1" : "0"); } } else if(sub=="list") { lookup(frame,a[2],s); auto path=checkedPath(val(3),s); std::error_code ec; std::vector names; std::size_t bytes=0; fs::directory_iterator it(path,ec),end; if(ec) fail(s,"directory list failed: "+ec.message()); for(;it!=end;it.increment(ec)) { if(ec) fail(s,"directory list failed: "+ec.message()); auto text=it->path().filename().string(); bytes+=text.size()+sizeof(std::string); if(names.size()>=itemLimit || bytes>valueLimit) failLimit(s,"directory listing limit"); names.push_back(std::move(text)); } if(ec) fail(s,"directory list failed: "+ec.message()); std::sort(names.begin(),names.end()); assignArray(frame,a[2],std::move(names),s); } else { auto from=checkedPath(val(2),s),to=checkedPath(val(3),s); // Linux primitives: no destination clobber, including dangling symlinks. if(sub=="rename") { struct stat st{}; if(lstat(from.c_str(),&st)!=0 || !S_ISREG(st.st_mode)) fail(s,"file rename requires regular non-symlink source"); if(syscall(SYS_renameat2,AT_FDCWD,from.c_str(),AT_FDCWD,to.c_str(),RENAME_NOREPLACE)!=0) fail(s,std::string("file rename failed: ")+std::strerror(errno)); } else { struct Fd { int n; ~Fd() { if(n>=0) ::close(n); } } in{::open(from.c_str(),O_RDONLY|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC)}; struct stat st{}; if(in.n<0 || fstat(in.n,&st)!=0 || !S_ISREG(st.st_mode)) fail(s,"file copy requires regular non-symlink source"); if(st.st_size<0 || static_cast(st.st_size)>16*valueLimit-fileBytes) failLimit(s,"file write byte limit (16 MiB)"); Fd out{::open(to.c_str(),O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600)}; if(out.n<0) fail(s,std::string("file copy destination failed: ")+std::strerror(errno)); char buffer[8192]; // On failure a partial new destination remains: never unlink a // pathname another process could have replaced concurrently. for(;;) { auto n=::read(in.n,buffer,sizeof(buffer)); if(n<0) { if(errno==EINTR) continue; fail(s,"file copy read failed"); } if(n==0) break; auto size=static_cast(n); if(size>16*valueLimit-fileBytes) failLimit(s,"file write byte limit (16 MiB)"); fileBytes+=size; std::size_t pos=0; while(pos(written); } } auto fd=out.n; out.n=-1; if(::close(fd)!=0) fail(s,"file copy close failed"); } } } else if(op=="http") { if(!options.allowNetwork) fail(s,"HTTP requires --allow-network (not a sandbox)"); lookup(frame,a[2],s); lookup(frame,a[3],s); if(a[2].text==a[3].text) fail(s,"HTTP destinations must differ"); auto url=val(4); if(url.size()>8192) failLimit(s,"HTTP URL limit (8192 bytes)"); if(url.find('\0')!=std::string::npos || (url.rfind("http://",0)!=0 && url.rfind("https://",0)!=0)) fail(s,"HTTP URL requires http:// or https://"); auto timeout=static_cast(integer(val(5),1,30000,s)); auto maximum=static_cast(integer(val(6),1,static_cast(valueLimit),s)); auto body=a[1].text=="post" ? val(7) : std::string{}; if(++networkCalls>64 || timeout>60000-networkTime) failLimit(s,"HTTP request/time limit (64 requests, 60000 ms reserved)"); networkTime+=timeout; if(body.size()>16*valueLimit-networkBytes) failLimit(s,"HTTP traffic limit (16 MiB)"); networkBytes+=body.size(); static CurlGlobal global; if(global.result!=CURLE_OK) fail(s,"HTTP initialization failed"); if(!(curl_version_info(CURLVERSION_NOW)->features&CURL_VERSION_ASYNCHDNS)) fail(s,"HTTP requires libcurl with asynchronous DNS for bounded timeouts"); CurlSession session; if(!session.handle) failLimit(s,"HTTP allocation limit"); HttpBuffer buffer; buffer.maximum=std::min(maximum,16*valueLimit-networkBytes); auto set=[&](CURLoption option,auto v) { if(curl_easy_setopt(session.handle,option,v)!=CURLE_OK) fail(s,"HTTP configuration failed"); }; set(CURLOPT_URL,url.c_str()); set(CURLOPT_PROTOCOLS_STR,"http,https"); set(CURLOPT_REDIR_PROTOCOLS_STR,"http,https"); set(CURLOPT_FOLLOWLOCATION,0L); set(CURLOPT_SSL_VERIFYPEER,1L); set(CURLOPT_SSL_VERIFYHOST,2L); set(CURLOPT_DISALLOW_USERNAME_IN_URL,1L); set(CURLOPT_PROXY,""); set(CURLOPT_NOSIGNAL,1L); set(CURLOPT_TIMEOUT_MS,timeout); set(CURLOPT_CONNECTTIMEOUT_MS,timeout); set(CURLOPT_WRITEFUNCTION,&httpBody); set(CURLOPT_WRITEDATA,&buffer); set(CURLOPT_HEADERFUNCTION,&httpHeader); set(CURLOPT_HEADERDATA,&buffer); set(CURLOPT_USERAGENT,"Vanilla1/1.1"); if(a[1].text=="post") { set(CURLOPT_POST,1L); set(CURLOPT_POSTFIELDSIZE_LARGE,static_cast(body.size())); set(CURLOPT_POSTFIELDS,body.data()); } auto result=curl_easy_perform(session.handle); networkBytes+=buffer.body.size(); if(buffer.exceeded) failLimit(s,"HTTP response/header limit exceeded"); if(buffer.allocationFailed) failLimit(s,"HTTP allocation limit"); if(result!=CURLE_OK) fail(s,std::string("HTTP transport error: ")+curl_easy_strerror(result)); long status=0; if(curl_easy_getinfo(session.handle,CURLINFO_RESPONSE_CODE,&status)!=CURLE_OK) fail(s,"HTTP status unavailable"); put(2,buffer.body); put(3,std::to_string(status)); } else return false; return true; }