--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c @@ -131,15 +131,26 @@ foreach (string key, string value : a) if (!b.Contains(key) || b.Get(key) != value) return false; return true; } + // A section may have loaded from the backup while the primary remains valid + // JSON. Never promote that damaged document or overwrite its recovery copy. + static bool ContentsValid(map members) + { + foreach (string path : LegacyPaths()) + { + string section = Section(path); + if (members.Contains(section) && !Valid(section, members.Get(section))) return false; + } + return true; + } static bool Commit(map members) { - if (!Replication.IsServer()) return false; + if (!Replication.IsServer() || !ContentsValid(members)) return false; FileHandle file = FileIO.OpenFile(PATH + ".tmp", FileMode.WRITE); if (!file) return false; file.Write(Encode(members)); file.Close(); map checked, previous; if (!Document(PATH + ".tmp", checked) || !Same(members, checked)) return false; - bool hadPrevious = Document(PATH, previous); + bool hadPrevious = Document(PATH, previous) && ContentsValid(previous); if (hadPrevious) { if (!FileIO.CopyFile(PATH, PATH + ".bak") || !Document(PATH + ".bak", checked) || !Same(previous, checked)) return false; --- a/tests/test_profile_config.py +++ b/tests/test_profile_config.py @@ -1,8 +1,10 @@ """Execute the actual shared-file transaction with injected disk failures. Engine JSON/migration/menu checks are recorded separately in native evidence. """ +import os +from pathlib import Path from policy_harness import ROOT, body, run -p = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c' +p = Path(os.environ.get('ERR_PROFILE_CONFIG_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c')) method = body(p, 'static bool Commit(') for before, after in [ ('map', 'Map'), ('FileHandle file', 'FileHandle* file'), @@ -44,6 +46,7 @@ } } struct Store { + static bool ContentsValid(Map){return true;} static string Encode(Map values){string id="doc"+std::to_string(++serial);documents[id]=values;return id;} static bool Document(string path,Map& values){ if(!files.count(path)||!documents.count(files[path]))return false; @@ -81,3 +84,90 @@ } ''') print('PASS: shared config transaction rejects client writes, preserves first-save/prior/fallback state across open, write, backup, partial-copy, readback and mismatch failures') + +# Execute actual shared Save and Commit, with a small token document store. +# JSON lexing and non-terrain section parsers are explicit doubles. The terrain +# rejection runs the actual TerrainPreference.Parse method with a catalog double. +# No native filesystem atomicity or full JsonLoadContext implementation claim. +def translate(method): + for before, after in [ + ('map', 'Map'), ('new Map()', 'Map()'), + ('FileHandle file','FileHandle* file'), ('file.','file->'), + ('FileIO.','FileIO::'), ('FileMode.','FileMode::'), ('Replication.','Replication::'), + ('JsonSaveContext json','JsonSaveContext& json')]: + method=method.replace(before,after) + method=method.replace('foreach (string key, string value : changed)', 'for (auto [key,value] : changed)') + method=method.replace('foreach (string path : LegacyPaths())', 'for (string path : LegacyPaths())') + return method +helper='static bool ContentsValid(Map) {return true;}' +if 'static bool ContentsValid(' in p.read_text(): helper=translate(body(p,'static bool ContentsValid(')) +terrain = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainPreference.c', 'static bool Parse(').replace('JsonLoadContext json', 'JsonLoadContext& json').replace('out ResourceName scenario', 'string& scenario').replace('ERR_TerrainCatalog.', 'ERR_TerrainCatalog::') +run(r""" +#include +#include +#include +#include +#include +struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}void TrimInPlace(){}}; +struct Map:std::map{using std::map::map;bool Contains(string key){return count(key);}string Get(string key){return at(key);}void Set(string key,string value){(*this)[key]=value;}}; +const string PATH="Escapists.json"; +std::map files; +std::map documents; +int serial=0,copies=0,writes=0; +namespace Replication{bool server=true;bool IsServer(){return server;}} +namespace FileMode{int WRITE=1;} +struct FileHandle{string path;void Write(string data){writes++;files[path]=data;}void Close(){}}; +namespace FileIO{ + FileHandle* OpenFile(string path,int){static FileHandle h;h.path=path;return &h;} + bool FileExists(string path){return files.count(path);} + bool DeleteFile(string path){return files.erase(path);} + bool CopyFile(string from,string to){if(!files.count(from))return false;copies++;files[to]=files[from];return true;} +} +struct JsonSaveContext{string token;string SaveToString(){return token;}}; +struct JsonLoadContext{Map fields;bool ReadValue(string key,string&value){if(!fields.Contains(key))return false;value=fields.Get(key);return true;}}; +namespace ERR_TerrainCatalog{string Key(string scenario){return scenario=="valid-saved-terrain"?"known":"";}} +struct Terrain{ +""" + terrain + r""" +}; +struct Store{ + static std::vector LegacyPaths(){return {"mission","mods","factions","terrain","items","voice"};} + static string Section(string path){return path;} + static bool Ensure(){return true;} + static string Encode(Map values){string id="doc"+std::to_string(++serial);documents[id]=values;return id;} + static bool Split(string text,Map& values){if(!documents.count(text))return false;values=documents[text];return true;} + static bool Valid(string section,string text){Map fields;if(!Split(text,fields))return false;if(section!="terrain")return true;JsonLoadContext json{fields};string scenario;return Terrain::Parse(json,scenario);} + static bool Document(string path,Map& values){return files.count(path)&&Split(files[path],values)&&values.Contains("configVersion")&&values.Get("configVersion")=="1";} + static bool Same(Map a,Map b){return a==b;} +""" + helper + translate(body(p,'static bool Commit(')) + translate(body(p,'static bool Save(')) + r""" +}; +int main(){ + auto validTerrain=Store::Encode({{"scenario","valid-saved-terrain"},{"futureTerrainKey","keep"}}); + auto invalidTerrain=Store::Encode({{"scenario","malformed"},{"futureTerrainKey","keep"}}); + auto mission=Store::Encode({{"timer","10"},{"unknownMissionKey","keep"}}); + auto changedMission=Store::Encode({{"timer","20"}}); + Map good={{"configVersion","1"},{"mission",mission},{"terrain",validTerrain},{"unknownRoot","keep"}},current=good,read; + current.Set("terrain",invalidTerrain);string originalGood=Store::Encode(good),originalBad=Store::Encode(current); + files[PATH]=originalBad;files[PATH+".bak"]=originalGood; + // Owner would reject current terrain and accept backup. An unrelated save must + // not destroy the only recovery document or report success with invalid data. + assert(!Store::Valid("terrain",current.Get("terrain"))&&Store::Valid("terrain",good.Get("terrain"))); + JsonSaveContext change{changedMission}; + assert(!Store::Save("mission",change)); + assert(files[PATH]==originalBad&&files[PATH+".bak"]==originalGood&&copies==0&&writes==0&&files.size()==2); + // Repair the section through its recovered menu value. Preserve the only good + // backup; merge unknown fields and allow subsequent ordinary menu persistence. + JsonSaveContext repair{validTerrain};assert(Store::Save("terrain",repair)); + assert(files[PATH+".bak"]==originalGood&&Store::Document(PATH,read)); + assert(read.Get("unknownRoot")=="keep");Map fields;assert(Store::Split(read.Get("terrain"),fields)&&fields.Get("futureTerrainKey")=="keep"); + string repaired=files[PATH];assert(Store::Save("mission",change));assert(files[PATH+".bak"]==repaired); + assert(Store::Document(PATH,read)&&Store::Split(read.Get("mission"),fields));assert(fields.Get("timer")=="20"&&fields.Get("unknownMissionKey")=="keep"); + // Missing optional sections continue to use their owner defaults; unknown roots + // are not subject to this version's validators. + Map sparse={{"configVersion","1"},{"unrecognized","opaque"}};assert(Store::Commit(sparse)); + // Existing defaults and each known section are checked; no invalid first write. + files.clear();copies=0;writes=0;assert(!Store::Commit(current)&&files.empty()&&copies==0&&writes==0); + // Updating an already healthy document retains ordinary last-valid backup. + files[PATH]=originalGood;assert(Store::Save("mission",change));assert(files[PATH+".bak"]==originalGood); +} +""") +print('PASS: actual Save/Commit preserves semantically valid backup across unrelated menu edits and repairs; unknown fields/default sections retained; actual terrain parser; other parsers, catalog and disk doubled')