You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 23:16:19 +00:00
feat(furnidata): split-tier write to winning tier with path-traversal guard
This commit is contained in:
@@ -11,6 +11,18 @@ class FurnidataWriterTest {
|
||||
" { \"id\": 1, \"classname\": \"01_caterhead\", \"name\": \"old name\", \"description\": \"old desc\" }\n" +
|
||||
"] }, \"wallitemtypes\": { \"furnitype\": [] } }";
|
||||
|
||||
// Tier data: core has the entry with "core name"; custom ALSO has it with "custom old name".
|
||||
// The writer must pick the custom (winning) tier and leave core untouched.
|
||||
private static final String CORE_DATA =
|
||||
"{ \"roomitemtypes\": { \"furnitype\": [\n" +
|
||||
" { \"id\": 1, \"classname\": \"split_chair\", \"name\": \"core name\", \"description\": \"core desc\" }\n" +
|
||||
"] }, \"wallitemtypes\": { \"furnitype\": [] } }";
|
||||
|
||||
private static final String CUSTOM_DATA =
|
||||
"{ \"roomitemtypes\": { \"furnitype\": [\n" +
|
||||
" { \"id\": 1, \"classname\": \"split_chair\", \"name\": \"custom old name\", \"description\": \"custom old desc\" }\n" +
|
||||
"] }, \"wallitemtypes\": { \"furnitype\": [] } }";
|
||||
|
||||
@Test
|
||||
void writesNameAndDescriptionByClassnameSingleFile() throws Exception {
|
||||
Path dir = Files.createTempDirectory("fd");
|
||||
@@ -37,4 +49,82 @@ class FurnidataWriterTest {
|
||||
FurnidataWriter w = new FurnidataWriter(file, false, 64L * 1024 * 1024, 10);
|
||||
assertFalse(w.write("does_not_exist", "x", "y"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split-tier: classname present in both core and custom tiers.
|
||||
* The writer must update the winning (later) tier — custom — and leave core untouched.
|
||||
*/
|
||||
@Test
|
||||
void splitTierWritesWinningTierLeavesEarlierTierUntouched() throws Exception {
|
||||
Path base = Files.createTempDirectory("fd-split");
|
||||
|
||||
// Tier subdirectories
|
||||
Path coreDir = base.resolve("core");
|
||||
Path customDir = base.resolve("custom");
|
||||
Files.createDirectories(coreDir);
|
||||
Files.createDirectories(customDir);
|
||||
|
||||
// Top-level manifest: tiers in override order (core < custom)
|
||||
Files.writeString(base.resolve("manifest.json"),
|
||||
"{ \"tiers\": [ \"core\", \"custom\" ] }");
|
||||
|
||||
// Per-tier manifests listing the data file
|
||||
Files.writeString(coreDir.resolve("manifest.json"),
|
||||
"{ \"files\": [ \"furnidata.json\" ] }");
|
||||
Files.writeString(customDir.resolve("manifest.json"),
|
||||
"{ \"files\": [ \"furnidata.json\" ] }");
|
||||
|
||||
// Data files
|
||||
Path coreFile = coreDir.resolve("furnidata.json");
|
||||
Path customFile = customDir.resolve("furnidata.json");
|
||||
Files.writeString(coreFile, CORE_DATA);
|
||||
Files.writeString(customFile, CUSTOM_DATA);
|
||||
|
||||
FurnidataWriter w = new FurnidataWriter(base, true, 64L * 1024 * 1024, 10);
|
||||
boolean ok = w.write("split_chair", "New Name", "New desc");
|
||||
|
||||
assertTrue(ok, "write must succeed for classname present in split-tier layout");
|
||||
|
||||
// custom (winning tier) must be updated
|
||||
String customAfter = Files.readString(customFile);
|
||||
assertTrue(customAfter.contains("\"New Name\""), "winning tier must contain new name");
|
||||
assertTrue(customAfter.contains("\"New desc\""), "winning tier must contain new desc");
|
||||
assertFalse(customAfter.contains("custom old name"), "old name must be gone from winning tier");
|
||||
|
||||
// core (earlier tier) must be UNTOUCHED
|
||||
String coreAfter = Files.readString(coreFile);
|
||||
assertTrue(coreAfter.contains("core name"), "earlier tier must be left untouched");
|
||||
}
|
||||
|
||||
/**
|
||||
* Split-tier path-traversal guard: a manifest that lists "../escape" as a tier
|
||||
* must be rejected by safeResolve so the writer cannot reach files outside the base dir.
|
||||
*/
|
||||
@Test
|
||||
void splitTierRejectsTraversalTierInManifest() throws Exception {
|
||||
Path base = Files.createTempDirectory("fd-traversal");
|
||||
|
||||
// "Escape" directory sits OUTSIDE base
|
||||
Path escapeDir = base.getParent().resolve("escape_secret");
|
||||
Files.createDirectories(escapeDir);
|
||||
Files.writeString(escapeDir.resolve("manifest.json"),
|
||||
"{ \"files\": [ \"secret.json\" ] }");
|
||||
Files.writeString(escapeDir.resolve("secret.json"),
|
||||
"{ \"roomitemtypes\": { \"furnitype\": [\n" +
|
||||
" { \"id\": 99, \"classname\": \"escape_chair\", \"name\": \"secret old\", \"description\": \"\" }\n" +
|
||||
"] }, \"wallitemtypes\": { \"furnitype\": [] } }");
|
||||
|
||||
// Top-level manifest references the escape dir via traversal
|
||||
Files.writeString(base.resolve("manifest.json"),
|
||||
"{ \"tiers\": [ \"../escape_secret\" ] }");
|
||||
|
||||
FurnidataWriter w = new FurnidataWriter(base, true, 64L * 1024 * 1024, 10);
|
||||
boolean ok = w.write("escape_chair", "Pwned", "desc");
|
||||
|
||||
assertFalse(ok, "classname reachable only via traversal path must not be found/written");
|
||||
|
||||
// The secret file must not have been touched
|
||||
String secretAfter = Files.readString(escapeDir.resolve("secret.json"));
|
||||
assertTrue(secretAfter.contains("secret old"), "traversal target must be untouched");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user