v. vanilla 2.0

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;
}