restored/utilities.inc
← All source files · Download this file
18,927 bytes · SHA256 127e23691d43661f771609e8433a4c357be4268abbd04222b83ce54894230a0b
// 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<const std::string&>().capacity();
if(j.is_number()) {
double n=j.get<double>();
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<const Json::array_t&>().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<const Structured>(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<unsigned char>(ch)<32 || static_cast<unsigned char>(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<std::set<std::string>> 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<std::string>()).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<std::size_t>(n);
if(size>valueLimit-text.size()) { overflowed=true; return 0; }
text.append(p,size); return n;
}
} buffer;
try { std::ostream out(&buffer); out<<j; }
catch(const Json::exception& e) { fail(s,std::string("JSON encoding error: ")+jsonDiagnostic(e)); }
if(buffer.overflowed) failLimit(s,"JSON output limit (1 MiB)");
return std::move(buffer.text);
}
static std::string checkedPath(std::string p,const Source& s) {
if(p.empty() || p.find('\0')!=std::string::npos) fail(s,"invalid file path");
return p;
}
static std::pair<std::string,bool> 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<HttpBuffer*>(data);
if(size && count>std::numeric_limits<std::size_t>::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<HttpBuffer*>(data);
if(size && count>std::numeric_limits<std::size_t>::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<std::size_t>(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<std::string>() : 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<std::size_t>(integer(key,0,static_cast<long long>(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<std::string> 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<std::ifstream>(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<unsigned long>(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<std::string> 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<std::uintmax_t>(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<std::size_t>(n);
if(size>16*valueLimit-fileBytes) failLimit(s,"file write byte limit (16 MiB)");
fileBytes+=size;
std::size_t pos=0;
while(pos<size) {
auto written=::write(out.n,buffer+pos,size-pos);
if(written<0 && errno==EINTR) continue;
if(written<=0) fail(s,"file copy write failed");
pos+=static_cast<std::size_t>(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<long>(integer(val(5),1,30000,s));
auto maximum=static_cast<std::size_t>(integer(val(6),1,static_cast<long long>(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<curl_off_t>(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;
}