#include "branding_config_server.hpp" #include "branding_bundle.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __SWITCH__ #include #include #include #include #include #include #include #endif namespace { #ifdef __SWITCH__ constexpr std::uint16_t kPort = 8767; constexpr std::size_t kMaxRequest = 8 * 1024 * 1024 + 16 * 1024; std::string portalAppName = "Switchnote Studio"; std::string portalNoteSingular = "Switchnote"; std::string portalNotePlural = "Switchnotes"; struct State { Thread thread{}; std::atomic running{false}; std::atomic connected{false}; std::atomic pending{false}; std::atomic lastSeenSeconds{0}; std::atomic activeClient{-1}; int listenFd = -1; std::string bindAddress, dataDirectory, assetsRoot, code, token; std::uint32_t pairedAddress = 0; unsigned int failedCodeAttempts = 0; }; void logEvent(const State* st, const std::string& message) { std::ofstream log(st->dataDirectory + "/branding-server.log", std::ios::app); if (log) log << static_cast(std::time(nullptr)) << " " << message << '\n'; } bool equalToken(const std::string& a, const std::string& b) { unsigned int diff = static_cast(a.size() ^ b.size()); for (std::size_t i = 0; i < std::max(a.size(), b.size()); ++i) diff |= static_cast((i < a.size() ? a[i] : 0) ^ (i < b.size() ? b[i] : 0)); return diff == 0; } bool sendAll(int fd, const char* data, std::size_t size) { std::size_t sent = 0; while (sent < size) { const ssize_t n = ::send(fd, data + sent, size - sent, 0); if (n <= 0) return false; sent += static_cast(n); } return true; } void reply(int fd, int status, const char* reason, const std::string& type, const std::string& body) { std::ostringstream h; h << "HTTP/1.1 " << status << ' ' << reason << "\r\nContent-Type: " << type << "\r\nContent-Length: " << body.size() << "\r\nConnection: close\r\nCache-Control: no-store\r\n" << "X-Content-Type-Options: nosniff\r\nReferrer-Policy: no-referrer\r\nX-Frame-Options: DENY\r\n" << "Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src 'self' blob:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self'\r\n\r\n"; const std::string headers = h.str(); sendAll(fd, headers.data(), headers.size()); if (!body.empty()) sendAll(fd, body.data(), body.size()); } void textReply(int fd, int status, const char* reason, const std::string& body) { reply(fd, status, reason, "text/plain; charset=utf-8", body); } std::string htmlEscape(const std::string& value) { std::string out; out.reserve(value.size()); for (char c : value) { switch (c) { case '&': out += "&"; break; case '<': out += "<"; break; case '>': out += ">"; break; case '\"': out += """; break; case '\'': out += "'"; break; default: out += c; break; } } return out; } std::string page() { std::string html = R"HTML(Branding Config Portal

Branding Config Portal

Enter the code shown on your Nintendo Switch.

Enter all six digits to connect.
)HTML"; auto replace = [&](const std::string& marker, const std::string& value) { const std::size_t at = html.find(marker); if (at != std::string::npos) html.replace(at, marker.size(), htmlEscape(value)); }; replace("{{APP_NAME}}", portalAppName); replace("{{NOTE_NAME}}", portalNoteSingular); replace("{{NOTES_NAME}}", portalNotePlural); const std::string loaderInput = "Animated images are reduced to eight small frames for the Switch."; const std::size_t formEnd = html.find(""); if (formEnd != std::string::npos) html.insert(formEnd, loaderInput); const std::string bundleControls = "
Branding file

Save or load your branding and title logo as a .config file.

"; const std::size_t bundleFormEnd = html.find(""); if (bundleFormEnd != std::string::npos) html.insert(bundleFormEnd, bundleControls); const std::string uploadScript = R"JS( async function uploadLoading(file){if(!file)return;if(!['image/gif','image/webp'].includes(file.type))throw Error('Choose an animated GIF or WebP.');if(file.size>16*1024*1024)throw Error('Loading animation must be 16 MB or smaller.');const url=URL.createObjectURL(file),im=new Image();try{await new Promise((resolve,reject)=>{im.onload=resolve;im.onerror=()=>reject(Error('Could not decode that animation.'));im.src=url});const c=document.createElement('canvas');c.width=64;c.height=64;const g=c.getContext('2d');for(let i=0;i<8;i++){g.clearRect(0,0,64,64);const s=Math.min(64/im.naturalWidth,64/im.naturalHeight),w=im.naturalWidth*s,h=im.naturalHeight*s;g.drawImage(im,(64-w)/2,(64-h)/2,w,h);const png=await new Promise(resolve=>c.toBlob(resolve,'image/png'));if(!png)throw Error('Could not convert the animation.');const r=await fetch('/api/loading/'+i,{method:'PUT',headers:{Authorization:'Bearer '+token,'Content-Type':'image/png'},body:png});if(!r.ok)throw Error(await r.text()||'Could not upload the loading animation.');await new Promise(resolve=>setTimeout(resolve,125))}}finally{URL.revokeObjectURL(url)}} )JS"; const std::string bundleScript = R"JS( async function exportBrandingBundle(){if(!token||busy)return;busy=true;$('exportBundle').disabled=true;try{msg('Preparing branding file…');const r=await fetch('/api/bundle',{headers:{Authorization:'Bearer '+token},cache:'no-store'});if(!r.ok)throw Error(await r.text()||'Could not export branding.');const blob=await r.blob(),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='branding.config';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);msg('Branding exported as branding.config.','ok')}catch(e){msg(e.message||'Could not export branding.','error')}finally{busy=false;$('exportBundle').disabled=false}} async function importBrandingBundle(file){if(!file||!token||busy)return;busy=true;$('chooseBundle').disabled=true;try{if(file.size>8*1024*1024+16384)throw Error('Branding file is too large.');msg('Importing branding…');const r=await fetch('/api/bundle',{method:'POST',headers:{Authorization:'Bearer '+token,'Content-Type':'application/octet-stream'},body:file});if(!r.ok)throw Error(await r.text()||'Could not import branding.');const current=await fetch('/api/config',{headers:{Authorization:'Bearer '+token},cache:'no-store'});if(current.ok){const saved=await current.json();$('app').value=saved.app;$('singular').value=saved.singular;$('plural').value=saved.plural}$('logo').value='';msg('Branding imported. Restart the app for changes to apply.','ok')}catch(e){msg(e.message||'Could not import branding.','error')}finally{busy=false;$('chooseBundle').disabled=false;$('bundleFile').value=''}} $('exportBundle').addEventListener('click',exportBrandingBundle);$('chooseBundle').addEventListener('click',()=>{if(!busy)$('bundleFile').click()});$('bundleFile').addEventListener('change',()=>importBrandingBundle($('bundleFile').files[0])); )JS"; const std::string submitHook = "$('form').addEventListener('submit',async e=>{e.preventDefault();if(!token||busy)return;busy=true;$('save').disabled=true;try{const f=$('logo').files[0];"; const std::string hookedSubmit = "$('form').addEventListener('submit',async e=>{e.preventDefault();if(!token||busy)return;busy=true;$('save').disabled=true;try{const loadingFile=$('loading').files[0];if(loadingFile)await uploadLoading(loadingFile);const f=$('logo').files[0];"; const std::size_t submitAt = html.find(submitHook); if (submitAt != std::string::npos) html.replace(submitAt, submitHook.size(), hookedSubmit); const std::size_t scriptAt = html.find("setInterval(pollConnection,2000);"); if (scriptAt != std::string::npos) html.insert(scriptAt, uploadScript); const std::size_t bundleScriptAt = html.find("setInterval(pollConnection,2000);"); if (bundleScriptAt != std::string::npos) html.insert(bundleScriptAt, bundleScript); return html; } bool readRequest(int fd, std::string& request, std::size_t& bodyAt, std::size_t& bodySize) { char buffer[8192]; std::size_t headerEnd = std::string::npos; while (headerEnd == std::string::npos) { ssize_t n=::recv(fd,buffer,sizeof(buffer),0); if(n<=0)return false; request.append(buffer,static_cast(n)); if(request.size()>16384)return false; headerEnd=request.find("\r\n\r\n"); } bodyAt=headerEnd+4; std::string headers=request.substr(0,headerEnd); std::size_t pos=headers.find("Content-Length:"); bodySize=0;if(pos!=std::string::npos){pos+=15;while(pos(std::strtoul(headers.c_str()+pos,nullptr,10));} if(bodySize>kMaxRequest)return false; while(request.size()-bodyAt(got));} return true; } std::string decode(const std::string& s) { std::string o; for(std::size_t i=0;i(std::strtoul(h,nullptr,16));i+=2;}else o+=s[i];}return o; } std::string jsonEscape(const std::string& s){std::string o="\"";for(unsigned char c:s){if(c=='\\'||c=='\"'){o+='\\';o+=static_cast(c);}else if(c<32){constexpr char h[]="0123456789abcdef";o+="\\u00";o+=h[c>>4];o+=h[c&15];}else o+=static_cast(c);}o+='\"';return o;} std::map fields(const std::string& b){std::map m;std::size_t p=0;while(p<=b.size()){auto e=b.find('&',p);auto part=b.substr(p,e==std::string::npos?e:e-p);auto q=part.find('=');if(q!=std::string::npos)m[decode(part.substr(0,q))]=decode(part.substr(q+1));if(e==std::string::npos)break;p=e+1;}return m;} bool cleanValue(std::string& s, std::size_t max) { if(s.empty()||s.size()>max)return false;for(unsigned char c:s)if(c<32||c==127)return false;return true; } void handle(State* st,int fd){std::string req;std::size_t bodyAt=0,bodySize=0;if(!readRequest(fd,req,bodyAt,bodySize)){textReply(fd,400,"Bad Request","Invalid request.");return;}auto lineEnd=req.find("\r\n");if(lineEnd==std::string::npos)return;std::istringstream first(req.substr(0,lineEnd));std::string method,path,version;first>>method>>path>>version;std::string headers=req.substr(lineEnd+2,bodyAt-lineEnd-4);auto h=headers.find("Authorization: Bearer ");std::string auth=h==std::string::npos?"":headers.substr(h+22);auto trimEnd=auth.find("\r\n");if(trimEnd!=std::string::npos)auth.resize(trimEnd);auto peer=static_cast(st->pairedAddress);sockaddr_in addr{};socklen_t addrLen=sizeof(addr);if(getpeername(fd,reinterpret_cast(&addr),&addrLen)==0)peer=ntohl(addr.sin_addr.s_addr); if(method=="GET"&&path=="/"){reply(fd,200,"OK","text/html; charset=utf-8",page());return;}if(method=="GET"&&path=="/favicon.ico"){reply(fd,204,"No Content","image/x-icon","");return;}if(method=="GET"&&path=="/api/health"){textReply(fd,200,"OK","Branding portal build: save-diagnostics-1");return;}if(method=="GET"&&path=="/assets/font"){std::ifstream f(st->assetsRoot+"/fonts/FOT-Seurat.otf",std::ios::binary|std::ios::ate);if(!f){textReply(fd,404,"Not Found","Font unavailable.");return;}auto n=f.tellg();if(n<=0||n>4*1024*1024){textReply(fd,404,"Not Found","Font unavailable.");return;}std::string data(static_cast(n),'\0');f.seekg(0);f.read(data.data(),n);reply(fd,200,"OK","font/otf",data);return;} if(method=="POST"&&path=="/api/connect"){if(st->failedCodeAttempts>=8){textReply(fd,429,"Too Many Requests","Close and reopen the portal on Switch to try again.");return;}std::string code=auth;code.erase(std::remove(code.begin(),code.end(),'"'),code.end());if(!equalToken(code,st->code)){++st->failedCodeAttempts;logEvent(st,"pairing rejected");textReply(fd,403,"Forbidden","Incorrect code.");return;}st->pairedAddress=peer;std::array random{};if(R_FAILED(csrngGetRandomBytes(random.data(),random.size()))){textReply(fd,500,"Internal Server Error","Could not create session.");return;}constexpr char hex[]="0123456789abcdef";st->token.clear();for(auto b:random){st->token+=hex[b>>4];st->token+=hex[b&15];}st->lastSeenSeconds.store(static_cast(std::time(nullptr)));st->connected.store(true);logEvent(st,"paired PC session");textReply(fd,200,"OK",st->token);return;} if(!st->connected.load()||peer!=st->pairedAddress||!equalToken(auth,st->token)){textReply(fd,403,"Forbidden","Connect using the code on the Switch first.");return;}st->lastSeenSeconds.store(static_cast(std::time(nullptr))); if(method=="GET"&&path=="/api/status"){textReply(fd,200,"OK","{\"connected\":true}");return;} if(method=="GET"&&path=="/api/config"){std::ifstream in(st->dataDirectory+"/branding.ini",std::ios::binary);std::string app=portalAppName,one=portalNoteSingular,many=portalNotePlural;if(in){std::string v;if(std::getline(in,v)&&!v.empty())app=v;if(std::getline(in,v)&&!v.empty())one=v;if(std::getline(in,v)&&!v.empty())many=v;}textReply(fd,200,"OK","{\"app\":"+jsonEscape(app)+",\"singular\":"+jsonEscape(one)+",\"plural\":"+jsonEscape(many)+"}");return;} if(method=="GET"&&path=="/api/bundle"){ branding::Bundle bundle;std::ifstream config(st->dataDirectory+"/branding.ini",std::ios::binary); if(config){std::string value;if(std::getline(config,value)&&!value.empty())bundle.app=value;else bundle.app=portalAppName;if(std::getline(config,value)&&!value.empty())bundle.singular=value;else bundle.singular=portalNoteSingular;if(std::getline(config,value)&&!value.empty())bundle.plural=value;else bundle.plural=portalNotePlural;} else{bundle.app=portalAppName;bundle.singular=portalNoteSingular;bundle.plural=portalNotePlural;} std::string logoPath=st->dataDirectory+"/branding-logo.png";std::ifstream logo(logoPath,std::ios::binary|std::ios::ate); if(!logo){logoPath=st->assetsRoot+"/art/switchnotedefault.png";logo.open(logoPath,std::ios::binary|std::ios::ate);} if(!logo){logEvent(st,"branding export failed: no logo available");textReply(fd,500,"Internal Server Error","No title logo is available to export.");return;} const std::streamoff logoSize=logo.tellg();if(logoSize<=0||logoSize>8*1024*1024){textReply(fd,500,"Internal Server Error","The title logo has an invalid size.");return;} bundle.logo.resize(static_cast(logoSize));logo.seekg(0);logo.read(reinterpret_cast(bundle.logo.data()),logoSize);if(!logo){textReply(fd,500,"Internal Server Error","Could not read the title logo.");return;}logo.close(); const char* image=reinterpret_cast(bundle.logo.data());const bool png=bundle.logo.size()>=8&&std::memcmp(image,"\x89PNG\r\n\x1a\n",8)==0;const bool jpg=bundle.logo.size()>=3&&static_cast(image[0])==0xff&&static_cast(image[1])==0xd8&&static_cast(image[2])==0xff;if(!png&&!jpg){textReply(fd,415,"Unsupported Media Type","The current logo is not a supported PNG or JPEG.");return;} const std::string temp=st->dataDirectory+"/branding-export.pending.config";std::string error;if(!branding::writeConfig(temp,bundle,error)){logEvent(st,"branding export failed: "+error);textReply(fd,500,"Internal Server Error",error);return;} std::ifstream archive(temp,std::ios::binary|std::ios::ate);const std::streamoff size=archive?static_cast(archive.tellg()):-1;if(size<22||size>9*1024*1024){archive.close();std::remove(temp.c_str());textReply(fd,500,"Internal Server Error","Could not prepare branding.config.");return;}std::string bytes(static_cast(size),'\0');archive.seekg(0);archive.read(bytes.data(),static_cast(size));archive.close();std::remove(temp.c_str());if(!archive){textReply(fd,500,"Internal Server Error","Could not read exported branding.config.");return;}logEvent(st,"branding config exported: "+std::to_string(bytes.size())+" bytes");reply(fd,200,"OK","application/octet-stream",bytes);return; } if(method=="POST"&&path=="/api/bundle"){ if(bodySize<22||bodySize>kMaxRequest){textReply(fd,400,"Bad Request","Branding file size is invalid.");return;} const std::string temp=st->dataDirectory+"/branding-import.pending.config";{std::ofstream file(temp,std::ios::binary|std::ios::trunc);if(!file){textReply(fd,500,"Internal Server Error","Could not open a temporary branding file.");return;}file.write(req.data()+bodyAt,static_cast(bodySize));file.flush();if(!file){textReply(fd,500,"Internal Server Error","Could not store the branding file.");return;}} branding::Bundle bundle;std::string error;const bool parsed=branding::readConfig(temp,bundle,error);std::remove(temp.c_str());if(!parsed){logEvent(st,"branding import rejected: "+error);textReply(fd,400,"Bad Request",error);return;} auto valid=[](const std::string& value){return std::all_of(value.begin(),value.end(),[](unsigned char c){return std::isalnum(c)||c==' '||c=='-'||c=='_'||c=='.'||c>=128;});};if(!cleanValue(bundle.app,40)||!cleanValue(bundle.singular,24)||!cleanValue(bundle.plural,24)||!valid(bundle.app)||!valid(bundle.singular)||!valid(bundle.plural)){textReply(fd,400,"Bad Request","Branding names contain unsupported characters or exceed their limits.");return;} const char* image=reinterpret_cast(bundle.logo.data());const bool png=bundle.logo.size()>=8&&std::memcmp(image,"\x89PNG\r\n\x1a\n",8)==0;const bool jpg=bundle.logo.size()>=3&&static_cast(image[0])==0xff&&static_cast(image[1])==0xd8&&static_cast(image[2])==0xff;if(!png&&!jpg){textReply(fd,415,"Unsupported Media Type","Branding config must contain a PNG or JPEG logo.");return;} const std::string logoPath=st->dataDirectory+"/branding-logo.pending";{std::ofstream logo(logoPath,std::ios::binary|std::ios::trunc);if(!logo){textReply(fd,500,"Internal Server Error","Could not stage the title logo.");return;}logo.write(reinterpret_cast(bundle.logo.data()),static_cast(bundle.logo.size()));logo.flush();if(!logo){textReply(fd,500,"Internal Server Error","Could not write the title logo.");return;}} const std::string configPath=st->dataDirectory+"/branding.ini";{std::ofstream config(configPath,std::ios::binary|std::ios::trunc);if(!config){std::remove(logoPath.c_str());textReply(fd,500,"Internal Server Error","Could not open branding settings on the SD card.");return;}config<pending.store(true,std::memory_order_release);textReply(fd,200,"OK","Imported.");return; } if(method=="PUT"&&path=="/api/logo"){if(bodySize<8||bodySize>8*1024*1024){logEvent(st,"logo upload rejected: invalid size");textReply(fd,400,"Bad Request","Logo must be 8 MB or smaller.");return;}const char* b=req.data()+bodyAt;bool png=bodySize>=8&&std::memcmp(b,"\x89PNG\r\n\x1a\n",8)==0;bool jpg=bodySize>=3&&static_cast(b[0])==0xff&&static_cast(b[1])==0xd8&&static_cast(b[2])==0xff;if(!png&&!jpg){logEvent(st,"logo upload rejected: invalid image signature");textReply(fd,415,"Unsupported Media Type","Choose a valid PNG or JPEG logo.");return;}std::string tmp=st->dataDirectory+"/branding-logo.pending";std::ofstream out(tmp,std::ios::binary|std::ios::trunc);if(!out){logEvent(st,"logo upload failed: pending file open failed");textReply(fd,500,"Internal Server Error","Could not open logo file on the SD card.");return;}out.write(b,static_cast(bodySize));out.flush();out.close();if(!out){logEvent(st,"logo upload failed: pending file write failed");textReply(fd,500,"Internal Server Error","Could not store logo on the SD card.");return;}logEvent(st,"logo uploaded: "+std::to_string(bodySize)+" bytes");textReply(fd,200,"OK","Logo received.");return;} if(method=="PUT"&&path.size()==14&&path.rfind("/api/loading/",0)==0&&path.back()>='0'&&path.back()<='7'){if(bodySize<8||bodySize>128*1024){textReply(fd,400,"Bad Request","Loading frame is too large.");return;}const char*b=req.data()+bodyAt;if(std::memcmp(b,"\x89PNG\r\n\x1a\n",8)!=0){textReply(fd,415,"Unsupported Media Type","Loading frames must be PNG images.");return;}const std::string file=st->dataDirectory+"/branding-loading"+path.substr(path.size()-1)+".pending";std::ofstream out(file,std::ios::binary|std::ios::trunc);if(!out){textReply(fd,500,"Internal Server Error","Could not save loading frame.");return;}out.write(b,static_cast(bodySize));out.flush();if(!out){textReply(fd,500,"Internal Server Error","Could not write loading frame.");return;}logEvent(st,"loading frame uploaded: "+path.substr(path.size()-1));textReply(fd,200,"OK","Frame received.");return;} if(method=="POST"&&path=="/api/config"){auto v=fields(req.substr(bodyAt,bodySize));std::string app=v["app"],one=v["singular"],many=v["plural"];if(!cleanValue(app,40)||!cleanValue(one,24)||!cleanValue(many,24)){textReply(fd,400,"Bad Request","Enter names within the listed limits.");return;}auto valid=[](const std::string&s){return std::all_of(s.begin(),s.end(),[](unsigned char c){return std::isalnum(c)||c==' '||c=='-'||c=='_'||c=='.'||c>=128;});};if(!valid(app)||!valid(one)||!valid(many)){textReply(fd,400,"Bad Request","Names may use letters, numbers, spaces, hyphens, underscores and periods.");return;} // libnx's std::rename over FAT/SD can fail when replacing an existing // file, even after the old destination has been removed. Write the small // config directly instead of reporting a false save failure. const std::string dst=st->dataDirectory+"/branding.ini"; std::ofstream out(dst,std::ios::binary|std::ios::trunc); if(!out){logEvent(st,"branding save failed: destination open failed");textReply(fd,500,"Internal Server Error","Could not open branding file on the SD card.");return;} out<pending.store(true,std::memory_order_release);textReply(fd,200,"OK","Saved.");return;} textReply(fd,404,"Not Found","Not found."); } void serverMain(void* arg){State* st=static_cast(arg);while(st->running.load()){fd_set set;FD_ZERO(&set);FD_SET(st->listenFd,&set);timeval wait{0,200000};if(select(st->listenFd+1,&set,nullptr,nullptr,&wait)<=0)continue;sockaddr_in peer{};socklen_t size=sizeof(peer);int fd=accept(st->listenFd,reinterpret_cast(&peer),&size);if(fd<0)continue;st->activeClient.store(fd);timeval timeout{5,0};setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));handle(st,fd);st->activeClient.store(-1);::close(fd);}if(st->listenFd>=0){::close(st->listenFd);st->listenFd=-1;}st->running.store(false);} #endif } BrandingConfigServer::BrandingConfigServer() = default; BrandingConfigServer::~BrandingConfigServer(){stop();} bool BrandingConfigServer::start(const std::string& ip,const std::string& data,const std::string& assets,const std::string& code,std::string& error,const std::string& appName,const std::string& noteSingular,const std::string& notePlural){ #ifdef __SWITCH__ portalAppName = appName; portalNoteSingular = noteSingular; portalNotePlural = notePlural; stop();State* s=new(std::nothrow) State();if(!s){error="Not enough memory to start the portal.";return false;}s->bindAddress=ip;s->dataDirectory=data;s->assetsRoot=assets;s->code=code;s->listenFd=::socket(AF_INET,SOCK_STREAM,0);if(s->listenFd<0){error="Could not create the local web server.";delete s;return false;}int yes=1;setsockopt(s->listenFd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(yes));sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(kPort);if(inet_pton(AF_INET,ip.c_str(),&a.sin_addr)!=1||bind(s->listenFd,reinterpret_cast(&a),sizeof(a))<0||listen(s->listenFd,4)<0){error="Could not start the portal on port 8767.";::close(s->listenFd);delete s;return false;}s->running.store(true);if(R_FAILED(threadCreate(&s->thread,serverMain,s,nullptr,0x10000,0x3b,-2))){error="Could not create the portal service thread.";s->running.store(false);::close(s->listenFd);delete s;return false;}if(R_FAILED(threadStart(&s->thread))){error="Could not start the portal service.";s->running.store(false);::close(s->listenFd);threadClose(&s->thread);delete s;return false;}impl_=s;return true; #else (void)ip;(void)data;(void)assets;(void)code;(void)appName;(void)noteSingular;(void)notePlural;error="The branding portal is available in the Switch homebrew app.";return false; #endif } void BrandingConfigServer::stop(){ #ifdef __SWITCH__ State*s=static_cast(impl_);if(!s)return;s->running.store(false);int c=s->activeClient.load();if(c>=0)::shutdown(c,SHUT_RDWR);if(s->listenFd>=0)::shutdown(s->listenFd,SHUT_RDWR);threadWaitForExit(&s->thread);threadClose(&s->thread);delete s;impl_=nullptr; #endif } bool BrandingConfigServer::isRunning()const{ #ifdef __SWITCH__ auto*s=static_cast(impl_);return s&&s->running.load(); #else return false; #endif } bool BrandingConfigServer::isConnected()const{ #ifdef __SWITCH__ auto*s=static_cast(impl_);if(!s||!s->connected.load())return false;const auto now=static_cast(std::time(nullptr));const auto last=s->lastSeenSeconds.load();return now>=last&&now-last<=6; #else return false; #endif } bool BrandingConfigServer::takeConfigUpdate(){ #ifdef __SWITCH__ auto*s=static_cast(impl_);return s&&s->pending.exchange(false,std::memory_order_acq_rel); #else return false; #endif }