diff --git a/dCommon/ZoneFile.cpp b/dCommon/ZoneFile.cpp index 4e9b9ee34..7b4fc7fd4 100644 --- a/dCommon/ZoneFile.cpp +++ b/dCommon/ZoneFile.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include "BinaryIO.h" @@ -60,15 +62,38 @@ void ZoneFile::Read(std::istream& file) { } if (fileFormatVersion >= FileFormatVersion::EarlyAlpha) { + // The paths are a chunk of their own (LuzFile::ReadLUZFile copies it, ReadLUZPaths reads it) BinaryIO::BinaryRead(file, pathDataLength); - BinaryIO::BinaryRead(file, pathChunkVersion); // always should be 1 + std::string chunk(pathDataLength, '\0'); + file.read(chunk.data(), static_cast(chunk.size())); + if (!file) throw std::runtime_error("Failed to read from istream."); + std::istringstream pathChunk(chunk); + if (!ReadPaths(pathChunk)) { + LOG("The zone file's paths are not ones the client reads; the zone has no paths, as in the client"); + paths.clear(); + } + } +} + +bool ZoneFile::ReadPaths(std::istream& file) { + // ReadLUZPaths: a chunk version from 2 on, 10000 paths or more, or any path LevelPath::FromBuffer refuses (it reads + // no further than the chunk's end here) leaves the zone with no paths + try { + BinaryIO::BinaryRead(file, pathChunkVersion); + if (pathChunkVersion >= 2) return false; uint32_t pathCount; BinaryIO::BinaryRead(file, pathCount); + if (pathCount >= 10000) return false; paths.reserve(pathCount); - for (uint32_t i = 0; i < pathCount; ++i) ReadPath(file); + for (uint32_t i = 0; i < pathCount; ++i) { + if (!ReadPath(file)) return false; + } + } catch (const std::runtime_error&) { + return false; } + return true; } void ZoneFile::ReadScene(std::istream& file, uint32_t index) { @@ -187,9 +212,10 @@ SceneTransitionInfo ZoneFile::ReadSceneTransitionInfo(std::istream& file) { return info; } -void ZoneFile::ReadLdfConfig(std::istream& file, PathType pathType, PathWaypoint& waypoint) { +bool ZoneFile::ReadLdfConfig(std::istream& file, PathType pathType, PathWaypoint& waypoint) { uint32_t count; BinaryIO::BinaryRead(file, count); + if (count > 99) return false; // LevelPath::FromBuffer refuses the path for (uint32_t i = 0; i < count; ++i) { std::string parameter; BinaryIO::ReadString(file, parameter, BinaryIO::ReadType::WideString); @@ -211,12 +237,14 @@ void ZoneFile::ReadLdfConfig(std::istream& file, PathType pathType, PathWaypoint waypoint.config.ParseInsert(parameter + "=" + value); } } + return true; } -void ZoneFile::ReadPath(std::istream& file) { +bool ZoneFile::ReadPath(std::istream& file) { Path path = Path(); BinaryIO::BinaryRead(file, path.pathVersion); + if (path.pathVersion >= 19) return false; // LevelPath::FromBuffer refuses it BinaryIO::ReadString(file, path.pathName, BinaryIO::ReadType::WideString); @@ -277,6 +305,7 @@ void ZoneFile::ReadPath(std::istream& file) { // Read waypoints BinaryIO::BinaryRead(file, path.waypointCount); + if (path.waypointCount >= 10000) return false; // LevelPath::FromBuffer refuses it path.pathWaypoints.reserve(path.waypointCount); for (uint32_t i = 0; i < path.waypointCount; ++i) { PathWaypoint waypoint = PathWaypoint(); @@ -294,7 +323,7 @@ void ZoneFile::ReadPath(std::istream& file) { BinaryIO::BinaryRead(file, waypoint.movingPlatform.lockPlayer); BinaryIO::BinaryRead(file, waypoint.speed); BinaryIO::BinaryRead(file, waypoint.movingPlatform.wait); - ReadLdfConfig(file, path.pathType, waypoint); + if (!ReadLdfConfig(file, path.pathType, waypoint)) return false; path.pathWaypoints.push_back(waypoint); continue; } @@ -332,10 +361,11 @@ void ZoneFile::ReadPath(std::istream& file) { // object LDF configs if (path.pathType == PathType::Movement || path.pathType == PathType::Spawner || path.pathType == PathType::Rail) { - ReadLdfConfig(file, path.pathType, waypoint); + if (!ReadLdfConfig(file, path.pathType, waypoint)) return false; } path.pathWaypoints.push_back(waypoint); } paths.push_back(path); + return true; } diff --git a/dCommon/ZoneFile.h b/dCommon/ZoneFile.h index 341501f8f..d864db23e 100644 --- a/dCommon/ZoneFile.h +++ b/dCommon/ZoneFile.h @@ -260,7 +260,8 @@ private: void ReadZoneBoundaries(std::istream& file); void ReadSceneTransition(std::istream& file); SceneTransitionInfo ReadSceneTransitionInfo(std::istream& file); - void ReadPath(std::istream& file); + bool ReadPaths(std::istream& file); + bool ReadPath(std::istream& file); // A waypoint's name/value pairs: waypoint commands on movement and rail paths, LDF config on the others - static void ReadLdfConfig(std::istream& file, PathType pathType, PathWaypoint& waypoint); + static bool ReadLdfConfig(std::istream& file, PathType pathType, PathWaypoint& waypoint); }; diff --git a/tests/dCommonTests/ZoneFileTests.cpp b/tests/dCommonTests/ZoneFileTests.cpp index 3a2df7596..fd368383a 100644 --- a/tests/dCommonTests/ZoneFileTests.cpp +++ b/tests/dCommonTests/ZoneFileTests.cpp @@ -3,7 +3,9 @@ #include #include +#include "Game.h" #include "LevelFile.h" +#include "Logger.h" #include "ZoneFile.h" namespace { @@ -15,6 +17,13 @@ namespace { ZoneBytes& Text(const std::string& text) { Put(static_cast(text.size())); data += text; return *this; } ZoneBytes& Wide(const std::string& text) { Put(static_cast(text.size())); for (char c : text) Put(static_cast(c)); return *this; } ZoneBytes& Point(float x, float y, float z) { return Put(x).Put(y).Put(z); } + // The path chunk's length, filled in by Done (the path chunk ends the file) + size_t pathsAt = std::string::npos; + ZoneBytes& Paths() { pathsAt = data.size(); return Put(0); } + const std::string& Done() { + if (pathsAt != std::string::npos) At(pathsAt, static_cast(data.size() - pathsAt - 4)); + return data; + } void Object(uint32_t version, uint32_t lot, float x, const std::string& settings) { Put(lot * 10).Put(static_cast(lot)); if (version >= 38) Put(0); // node type @@ -35,7 +44,7 @@ namespace { w.Put(0).Text("zone.raw").Text("Name").Text("Description"); // no zone boundaries w.Put(1); for (int i = 0; i < 2; i++) w.Put(1).Point(0, 0, 0); - w.Put(0).Put(1).Put(3); + w.Paths().Put(1).Put(3); // Movement: its config is waypoint commands (spaces dropped from the name) w.Put(18).Wide("Patrol").Put(0).Put(0).Put(0); w.Put(1).Point(5, 0, 5).Put(1).Wide("de lay").Wide(" 2 "); @@ -50,7 +59,7 @@ namespace { w.Put(4); for (char c : std::string("Desc")) w.Put(static_cast(c)); w.Put(0).Put(0).Put(1.0f).Put(0).Put(0).Point(0, 0, 0).Put(128.0f); w.Put(3).Point(0, 0, 0).Point(10, 0, 0).Point(10, 0, 10); - return w.data; + return w.Done(); } } @@ -100,9 +109,9 @@ TEST(ZoneFileTests, ReadsZoneBoundaries) { w.Point(1, 0, 0).Point(10, 20, 30).Put(1100).Put(7).Put(4).Point(5, 6, 7); w.Point(0, 0, -1).Point(-1, -2, -3).Put(1200).Put(0).Put(0).Point(0, 0, 0); w.Text("zone.raw").Text("Name").Text("Description"); - w.Put(0).Put(0).Put(1).Put(0); // no transitions, no paths + w.Put(0).Paths().Put(1).Put(0); // no transitions, no paths - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -126,9 +135,9 @@ TEST(ZoneFileTests, LateAlphaSceneCountIsAU32) { w.Put(1); w.Text("scene.lvl").Put(5).Put(0).Text("Global Scene").Put(0).Put(0).Put(0); w.Put(0).Text("zone.raw").Text("Name").Text("Description"); - w.Put(0).Put(8).Put(1).Put(0); // no transitions, no paths + w.Put(0).Paths().Put(1).Put(0); // no transitions, no paths - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -144,7 +153,7 @@ TEST(ZoneFileTests, ReadsVersionsBeforePrePreAlpha) { w.Put(12).Put(53).Put(4); // version, world, scene count w.Put(9).Put(3).Put(7).Put(3); // SceneTable IDs w.Put(0).Text("zone.raw"); - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -172,7 +181,7 @@ TEST(ZoneFileTests, EarlyScenesAreNumberedInOrder) { ZoneBytes w; w.Put(32).Put(70).Put(3).Text("a.lvl").Text("b.lvl").Text("c.lvl"); w.Put(0).Text("zone.raw").Text("Name").Text("Description").Put(0); - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -200,8 +209,8 @@ TEST(ZoneFileTests, KeepsTheScenesTheClientLoads) { scene("a.lvl", 1, 0); scene("b2.lvl", 2, 0); w.Put(0).Text("zone.raw").Text("Name").Text("Description"); - w.Put(0).Put(0).Put(1).Put(0); - std::istringstream stream(w.data); + w.Put(0).Paths().Put(1).Put(0); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -217,7 +226,7 @@ TEST(ZoneFileTests, PrePreAlphaHasNoZoneName) { ZoneBytes w; w.Put(30).Put(72).Put(1).Text("scale.lvl"); // version, world, one scene: only its file w.Put(0).Text("scale.raw"); - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -235,14 +244,14 @@ TEST(ZoneFileTests, ReadsLegacyPaths) { w.Text("lup.lvl").Put(0).Put(0).Text("Global Scene").Put(0).Put(0).Put(0); w.Put(0).Text("lup.raw").Text("Name").Text("Description"); w.Put(0); // no transitions - w.Put(0).Put(1).Put(2); + w.Paths().Put(1).Put(2); w.Put(2).Wide("LavaPath").Wide("npc").Put(1).Put(0); w.Put(1).Point(1, 2, 3).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(0).Put(3.0f).Put(0.5f); w.Put(1).Wide("delay").Wide("2"); w.Put(2).Wide("Mower").Wide("platform").Put(0).Put(2); w.Put(1).Point(4, 5, 6).Put(0.0f).Put(1.0f).Put(0.0f).Put(0.0f).Put(1).Put(7.0f).Put(1.5f).Put(0); - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -276,7 +285,7 @@ TEST(ZoneFileTests, SpawnerNetActiveFromVersion9) { w.Put(1); w.Text("scene.lvl").Put(0).Put(0).Text("Global").Put(0).Put(0).Put(0); w.Put(0).Text("zone.raw").Text("Name").Text("Description"); - w.Put(0).Put(0).Put(1).Put(2); + w.Put(0).Paths().Put(1).Put(2); for (const uint32_t version : { 8u, 9u }) { w.Put(version).Wide("Spawner").Put(4).Put(0).Put(0); w.Put(6010).Put(10).Put(1).Put(1).Put(123); @@ -284,7 +293,7 @@ TEST(ZoneFileTests, SpawnerNetActiveFromVersion9) { w.Put(1).Point(1, 1, 1).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f).Put(0); } - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); ZoneFile zone; zone.Read(stream); EXPECT_FALSE(stream.fail()); @@ -295,6 +304,39 @@ TEST(ZoneFileTests, SpawnerNetActiveFromVersion9) { EXPECT_EQ(zone.paths[1].spawner.spawnerNetActive, 0); } +// The paths are a chunk the client reads on its own: a path it refuses (here version 19) leaves the zone with no paths, +// and the zone file goes on after the chunk +TEST(ZoneFileTests, RefusedPathsLeaveNoPaths) { + Game::logger = new Logger("./testing.log", false, false); // the reader logs the refusal + for (const uint32_t refused : { 0u, 1u, 2u, 3u }) { + ZoneBytes w; + w.Put(41).Put(3).Put(1150); + w.Point(0, 0, 0).Put(1.0f).Put(0.0f).Put(0.0f).Put(0.0f); + w.Put(1); + w.Text("scene.lvl").Put(0).Put(0).Text("Global").Put(0).Put(0).Put(0); + w.Put(0).Text("zone.raw").Text("Name").Text("Description").Put(0); + const auto size = w.data.size(); + w.Put(0).Put(refused == 1 ? 2 : 1).Put(2); + w.Put(18).Wide("Good").Put(2).Put(0).Put(0); + w.Put(0).Put(0).Put(0).Put(0).Wide("").Put(0); + w.Put(0).Put(0).Put(1.0f).Put(0).Put(0).Point(0, 0, 0).Put(128.0f); + w.Put(1).Point(1, 2, 3); + w.Put(refused == 2 ? 19 : 18).Wide("Patrol").Put(0).Put(0).Put(0); + w.Put(1).Point(5, 0, 5).Put(refused == 3 ? 100 : 0); + for (uint32_t i = 0; refused == 3 && i < 100; i++) w.Wide("delay").Wide("1"); + w.At(size, static_cast(w.data.size() - size - 4)); + + std::istringstream stream(w.Done()); + ZoneFile zone; + zone.Read(stream); + EXPECT_FALSE(stream.fail()) << refused; + EXPECT_EQ(stream.peek(), std::char_traits::eof()) << refused; + EXPECT_EQ(zone.paths.size(), refused == 0 ? 2u : 0u) << refused; + } + delete Game::logger; + Game::logger = nullptr; +} + TEST(ZoneFileTests, ShortFilesThrowOrFail) { const auto zone = SampleZone(); for (const size_t length : { size_t{ 3 }, size_t{ 40 }, zone.size() / 2, zone.size() - 1 }) { @@ -325,7 +367,7 @@ TEST(LevelFileTests, ReadsChunkedObjects) { w.Object(41, 176, -4, "spawntemplate=1:6010"); end(objects); - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); LevelFile level; level.Read(stream); EXPECT_EQ(level.chunkHeaders.at(LevelFile::FileInfo).fileInfo.revision, 9u); @@ -346,7 +388,7 @@ TEST(LevelFileTests, ReadsFilesWithoutChunks) { w.Put(1); w.Object(30, 4945, 3, "respawnname=0:NS_GF"); - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); LevelFile level; level.Read(stream); ASSERT_EQ(level.objects.size(), 1u); @@ -364,7 +406,7 @@ TEST(LevelFileTests, DamagedFilesKeepWhatWasRead) { w.Object(30, 2, 0, "c=0:d"); w.data.resize(w.data.size() - 2); // cut into the last object - std::istringstream stream(w.data); + std::istringstream stream(w.Done()); LevelFile level; EXPECT_THROW(level.Read(stream), std::runtime_error); EXPECT_EQ(level.objects.size(), 1u); diff --git a/tests/dWebTests/ZonePathsTests.cpp b/tests/dWebTests/ZonePathsTests.cpp index 907e55a2e..471ca28cb 100644 --- a/tests/dWebTests/ZonePathsTests.cpp +++ b/tests/dWebTests/ZonePathsTests.cpp @@ -1,5 +1,7 @@ #include +#include + #include "ZonePaths.h" namespace { @@ -21,7 +23,8 @@ namespace { w.Put(0).Text("zone.raw").Text("Name").Text("Description"); // no zone boundaries w.Put(1); // one transition: 2 points in this version for (int i = 0; i < 2; i++) w.Put(1).Point(0, 0, 0); - w.Put(0).Put(1).Put(3); // path data length, chunk version, 3 paths + const auto pathsAt = w.data.size(); + w.Put(0).Put(1).Put(3); // path data length (below), chunk version, 3 paths // A movement path: one waypoint with a command w.Put(18).Wide("Patrol").Put(0).Put(0).Put(0); @@ -39,6 +42,8 @@ namespace { w.Put(4); for (char c : std::string("Desc")) w.Put(static_cast(c)); w.Put(0).Put(0).Put(1.0f).Put(0).Put(0).Point(0, 0, 0).Put(128.0f); w.Put(3).Point(0, 0, 0).Point(10, 0, 0).Point(10, 0, 10); + const auto length = static_cast(w.data.size() - pathsAt - 4); + std::memcpy(w.data.data() + pathsAt, &length, sizeof(length)); return w.data; } }