Files
EscapistsReintroduced/docs/evidence/config-supply-recovery-2026-10-03/config/fix.patch
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

145 lines
8.9 KiB
Diff

--- 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<string, string> 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<string, string> 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<string, string> 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<string, string>', '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<string, string>', '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 <cassert>
+#include <map>
+#include <string>
+#include <vector>
+#include <algorithm>
+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<string,string>{using std::map<string,string>::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<string,string> files;
+std::map<string,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<string> 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')