Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Core/GameEngine/Include/GameNetwork/LANAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ struct LANMessage
{
char options[m_lanMaxOptionsLength+1];
} GameOptions;
static_assert(ARRAY_SIZE(GameOptions.options) > m_lanMaxOptionsLength, "GameOptions.options buffer must be larger than m_lanMaxOptionsLength");

};
};
Expand Down
142 changes: 119 additions & 23 deletions Core/GameEngine/Source/GameNetwork/GameInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "GameNetwork/LANAPI.h" // for testing packet size
#include "GameNetwork/LANAPICallbacks.h" // for testing packet size
#include "WWLib/strtok_r.h"
#include "WWLib/utf8.h"



Expand Down Expand Up @@ -891,12 +892,75 @@ Bool GameInfo::isSandbox()

static const char slotListID = 'S';

AsciiString GameInfoToAsciiString( const GameInfo *game )
// Shorten player names without cutting a UTF-8 character in half.
static void truncatePlayerNameToByteCount(AsciiString& name, Int maxByteCount)
{
if (!game)
return AsciiString::TheEmptyString;
const size_t truncatedLength = Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount);
name.truncateTo(static_cast<Int>(truncatedLength));
}

static Int getMinPlayerNameLength(const AsciiString& name)
{
for (Int maxByteCount = 1; maxByteCount <= name.getLength(); ++maxByteCount)
{
const size_t truncatedLength = Utf8_Truncate_Len(name.str(), name.getLength(), maxByteCount);
if (truncatedLength > 0)
{
return static_cast<Int>(truncatedLength);
}
}

return 0;
}

static Bool truncatePlayerNames(const GameInfo& game, AsciiString playerNames[MAX_SLOTS], Int maxPlayerNamesLength)
{
Int minLengths[MAX_SLOTS] = { 0 };
Int minTotalLength = 0;
Int playerCount = 0;
Int i;

for (i = 0; i < MAX_SLOTS; ++i)
{
const GameSlot *slot = game.getConstSlot(i);
if (slot && slot->isHuman())
{
minLengths[i] = getMinPlayerNameLength(playerNames[i]);
if (minLengths[i] == 0)
{
// Every serialized human must retain at least one complete UTF-8 character.
return false;
Comment thread
xezon marked this conversation as resolved.
}
minTotalLength += minLengths[i];
++playerCount;
}
}

if (playerCount == 0 || maxPlayerNamesLength < minTotalLength)
{
return false;
}

Int remainingLength = maxPlayerNamesLength;
for (i = 0; i < MAX_SLOTS; ++i)
{
const GameSlot *slot = game.getConstSlot(i);
if (slot && slot->isHuman())
{
const Int extraLength = (remainingLength - minTotalLength) / playerCount;
truncatePlayerNameToByteCount(playerNames[i], minLengths[i] + extraLength);
remainingLength -= playerNames[i].getLength();
minTotalLength -= minLengths[i];
--playerCount;
}
}

AsciiString mapName = game->getMap();
return true;
}

static AsciiString buildGameInfoAsciiString(const GameInfo& game, const AsciiString playerNames[MAX_SLOTS])
{
AsciiString mapName = game.getMap();
mapName = TheGameState->realMapPathToPortableMapPath(mapName);
AsciiString newMapName;
if (!mapName.isEmpty())
Expand All @@ -922,20 +986,20 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )

AsciiString optionsString;
#if RTS_GENERALS
optionsString.format("M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;", game->getMapContentsMask(), newMapName.str(),
game->getMapCRC(), game->getMapSize(), game->getSeed(), game->getCRCInterval());
optionsString.format("M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;", game.getMapContentsMask(), newMapName.str(),
game.getMapCRC(), game.getMapSize(), game.getSeed(), game.getCRCInterval());
#else
optionsString.format("US=%d;M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;SR=%u;SC=%u;O=%c;", game->getUseStats(), game->getMapContentsMask(), newMapName.str(),
game->getMapCRC(), game->getMapSize(), game->getSeed(), game->getCRCInterval(), game->getSuperweaponRestriction(),
game->getStartingCash().countMoney(), game->oldFactionsOnly() ? 'Y' : 'N' );
optionsString.format("US=%d;M=%2.2x%s;MC=%X;MS=%d;SD=%d;C=%d;SR=%u;SC=%u;O=%c;", game.getUseStats(), game.getMapContentsMask(), newMapName.str(),
game.getMapCRC(), game.getMapSize(), game.getSeed(), game.getCRCInterval(), game.getSuperweaponRestriction(),
game.getStartingCash().countMoney(), game.oldFactionsOnly() ? 'Y' : 'N' );
#endif

//add player info for each slot
optionsString.concat(slotListID);
optionsString.concat('=');
for (Int i=0; i<MAX_SLOTS; ++i)
{
const GameSlot *slot = game->getConstSlot(i);
const GameSlot *slot = game.getConstSlot(i);

AsciiString str;
if (slot && slot->isHuman())
Expand All @@ -948,15 +1012,8 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
slot->getColor(), slot->getPlayerTemplate(),
slot->getStartPos(), slot->getTeamNumber(),
slot->getNATBehavior() );
//make sure name doesn't cause overflow of m_lanMaxOptionsLength
int lenCur = tmp.getLength() + optionsString.getLength() + 2; //+2 for H and trailing ;
int lenRem = m_lanMaxOptionsLength - lenCur; //length remaining before overflowing
int lenMax = lenRem / (MAX_SLOTS-i); //share lenRem with all remaining slots
AsciiString name = WideCharStringToMultiByte(slot->getName().str()).c_str();
while( name.getLength() > lenMax )
name.removeLastChar(); //what a horrible way to truncate. I hate AsciiString.

str.format( "H%s%s", name.str(), tmp.str() );

str.format( "H%s%s", playerNames[i].str(), tmp.str() );
}
else if (slot && slot->isAI())
{
Expand Down Expand Up @@ -988,9 +1045,49 @@ AsciiString GameInfoToAsciiString( const GameInfo *game )
}
optionsString.concat(';');

DEBUG_ASSERTCRASH(!TheLAN || (optionsString.getLength() < m_lanMaxOptionsLength),
("WARNING: options string is longer than expected! Length is %d, but max is %d!",
optionsString.getLength(), m_lanMaxOptionsLength));
return optionsString;
}

AsciiString GameInfoToAsciiString( const GameInfo *game )
{
if (!game)
{
return AsciiString::TheEmptyString;
}

AsciiString playerNames[MAX_SLOTS];
Int playerNamesLength = 0;
for (Int i = 0; i < MAX_SLOTS; ++i)
{
const GameSlot *slot = game->getConstSlot(i);
if (slot && slot->isHuman())
{
playerNames[i] = WideCharStringToMultiByte(slot->getName().str()).c_str();
playerNamesLength += playerNames[i].getLength();
}
}

// TheSuperHackers @bugfix bobtista 23/08/2026 Prevent an infinite loop when player names exceed
// the LAN options limit by rebuilding the payload with bounded UTF-8 names.
AsciiString optionsString = buildGameInfoAsciiString(*game, playerNames);
Bool optionsFit = TheLAN == nullptr || optionsString.getLength() <= m_lanMaxOptionsLength;
if (!optionsFit)
{
const Int fixedLength = optionsString.getLength() - playerNamesLength;
const Int maxPlayerNamesLength = m_lanMaxOptionsLength - fixedLength;
if (truncatePlayerNames(*game, playerNames, maxPlayerNamesLength))
{
optionsString = buildGameInfoAsciiString(*game, playerNames);
optionsFit = optionsString.getLength() <= m_lanMaxOptionsLength;
}
}

if (!optionsFit)
{
DEBUG_CRASH(("WARNING: options string cannot fit within the expected length! Length is %d, but max is %d!",
optionsString.getLength(), m_lanMaxOptionsLength));
return AsciiString::TheEmptyString;
}
Comment on lines +1067 to +1090

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '870,1110p' Core/GameEngine/Source/GameNetwork/GameInfo.cpp
rg -n 'GameInfoToAsciiString|RequestGameOptions|GameOptions|options\[' Core/GameEngine/Source/GameNetwork Core/GameEngine/Include/GameNetwork
sed -n '240,280p' Core/GameEngine/Include/GameNetwork/LANAPI.h
sed -n '800,860p' Core/GameEngine/Source/GameNetwork/LANAPI.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 17398


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- LANGameInfo.cpp ---'
sed -n '230,315p' Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp
printf '%s\n' '--- LANAPI.cpp publication path ---'
sed -n '690,735p' Core/GameEngine/Source/GameNetwork/LANAPI.cpp
sed -n '825,875p' Core/GameEngine/Source/GameNetwork/LANAPI.cpp
printf '%s\n' '--- LANAPIhandlers.cpp relevant callers/consumer ---'
sed -n '45,160p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '180,210p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '420,455p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '515,545p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '660,690p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
sed -n '710,730p' Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
printf '%s\n' '--- LANAPICallbacks.cpp relevant callers ---'
sed -n '110,140p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
sed -n '450,475p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
sed -n '500,540p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
sed -n '580,600p' Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 15987


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- GameInfo parser ---'
sed -n '1090,1325p' Core/GameEngine/Source/GameNetwork/GameInfo.cpp
printf '%s\n' '--- LANGameInfo parser return ---'
sed -n '245,330p' Core/GameEngine/Source/GameNetwork/LANGameInfo.cpp
printf '%s\n' '--- all direct publication call sites ---'
rg -n -C 4 'GameInfoToAsciiString\\(|GenerateGameOptionsString\\(' Core/GameEngine/Source/GameNetwork

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 9908


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- parser return and required fields ---'
rg -n -C 8 'sawMap|sawSlotlist|optionsOk|return ' Core/GameEngine/Source/GameNetwork/GameInfo.cpp | tail -n 180
printf '%s\n' '--- direct serialization callers ---'
rg -n -F -C 3 'GameInfoToAsciiString(' Core/GameEngine/Source/GameNetwork
printf '%s\n' '--- generated-options callers ---'
rg -n -F -C 3 'GenerateGameOptionsString(' Core/GameEngine/Source/GameNetwork

Repository: TheSuperHackers/GeneralsGameCode

Length of output: 18314


Reject empty game-options payloads before LAN publication. When GameInfoToAsciiString cannot fit the payload, RequestGameAnnounce and handleRequestGameInfo copy the empty result into MSG_GAME_ANNOUNCE and send it. handleGameAnnounce rejects the empty string because the required fields are missing, then removes the discovered game. If that game already exists, this removes valid lobby state. Guard the empty result before sending the announcement or game-options update.


return optionsString;
}
Expand Down Expand Up @@ -1645,4 +1742,3 @@ void SkirmishGameInfo::loadPostProcess()
{
}


13 changes: 10 additions & 3 deletions Core/GameEngine/Source/GameNetwork/LANAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -710,11 +710,16 @@ void LANAPI::RequestGameAnnounce()
{
if (m_currentGame->getIP(0) == m_localIP || (m_currentGame->isGameInProgress() && TheNetwork && TheNetwork->isPacketRouter())) // if we're in game we should reply if we're the packet router
{
AsciiString gameOpts = GameInfoToAsciiString(m_currentGame);
if (gameOpts.isEmpty())
{
return;
}

LANMessage reply;
fillInLANMessage( &reply );
reply.messageType = LANMessage::MSG_GAME_ANNOUNCE;

AsciiString gameOpts = GameInfoToAsciiString(m_currentGame);
strlcpy(reply.GameInfo.options,gameOpts.str(), ARRAY_SIZE(reply.GameInfo.options));
wcslcpy(reply.GameInfo.gameName, m_currentGame->getName().str(), ARRAY_SIZE(reply.GameInfo.gameName));
reply.GameInfo.inProgress = m_currentGame->isGameInProgress();
Expand Down Expand Up @@ -834,10 +839,12 @@ void LANAPI::RequestGameStartTimer( Int seconds )

void LANAPI::RequestGameOptions( AsciiString gameOptions, Bool isPublic, UnsignedInt ip /* = 0 */ )
{
DEBUG_ASSERTCRASH(gameOptions.getLength() < m_lanMaxOptionsLength, ("Game options string is too long!"));
DEBUG_ASSERTCRASH(gameOptions.getLength() <= m_lanMaxOptionsLength, ("Game options string is too long!"));

if (!m_currentGame)
if (!m_currentGame || gameOptions.isEmpty())
{
return;
}

LANMessage msg;
fillInLANMessage( &msg );
Expand Down
28 changes: 18 additions & 10 deletions Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,19 @@ void LANAPI::handleRequestLocations( LANMessage *msg, UnsignedInt senderIP )
{
if (m_currentGame->getIP(0) == m_localIP)
{
LANMessage reply;
fillInLANMessage( &reply );
reply.messageType = LANMessage::MSG_GAME_ANNOUNCE;
AsciiString gameOpts = GenerateGameOptionsString();
strlcpy(reply.GameInfo.options, gameOpts.str(), ARRAY_SIZE(reply.GameInfo.options));
wcslcpy(reply.GameInfo.gameName, m_currentGame->getName().str(), ARRAY_SIZE(reply.GameInfo.gameName));
reply.GameInfo.inProgress = m_currentGame->isGameInProgress();
reply.GameInfo.isDirectConnect = m_currentGame->getIsDirectConnect();

sendMessage(&reply);
if (!gameOpts.isEmpty())
{
LANMessage reply;
fillInLANMessage( &reply );
reply.messageType = LANMessage::MSG_GAME_ANNOUNCE;
strlcpy(reply.GameInfo.options, gameOpts.str(), ARRAY_SIZE(reply.GameInfo.options));
wcslcpy(reply.GameInfo.gameName, m_currentGame->getName().str(), ARRAY_SIZE(reply.GameInfo.gameName));
reply.GameInfo.inProgress = m_currentGame->isGameInProgress();
reply.GameInfo.isDirectConnect = m_currentGame->getIsDirectConnect();

sendMessage(&reply);
}
}
else
{
Expand Down Expand Up @@ -188,11 +191,16 @@ void LANAPI::handleRequestGameInfo( LANMessage *msg, UnsignedInt senderIP )
{
if (m_currentGame->getIP(0) == m_localIP || (m_currentGame->isGameInProgress() && TheNetwork && TheNetwork->isPacketRouter())) // if we're in game we should reply if we're the packet router
{
AsciiString gameOpts = GameInfoToAsciiString(m_currentGame);
if (gameOpts.isEmpty())
{
return;
}

LANMessage reply;
fillInLANMessage( &reply );
reply.messageType = LANMessage::MSG_GAME_ANNOUNCE;

AsciiString gameOpts = GameInfoToAsciiString(m_currentGame);
strlcpy(reply.GameInfo.options,gameOpts.str(), ARRAY_SIZE(reply.GameInfo.options));
wcslcpy(reply.GameInfo.gameName, m_currentGame->getName().str(), ARRAY_SIZE(reply.GameInfo.gameName));
reply.GameInfo.inProgress = m_currentGame->isGameInProgress();
Expand Down
21 changes: 21 additions & 0 deletions Core/Libraries/Source/WWVegas/WWLib/utf8.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,24 @@ size_t Utf8_To_Wide(wchar_t* dest, size_t destLen, const char* src, size_t srcLe
}
return needed;
}

// A UTF-8 continuation byte matches 10xxxxxx, so it can never start a sequence.
static bool Utf8_Is_Continuation_Byte(char c)
{
return ((unsigned char)c & 0xC0) == 0x80;
}

size_t Utf8_Truncate_Len(const char* src, size_t srcLen, size_t maxLen)
{
if (srcLen <= maxLen)
{
return srcLen;
}

size_t len = maxLen;
while (len > 0 && Utf8_Is_Continuation_Byte(src[len]))
{
--len;
}
return len;
}
6 changes: 6 additions & 0 deletions Core/Libraries/Source/WWVegas/WWLib/utf8.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ size_t Wide_To_Utf8(char* dest, size_t destLen, const wchar_t* src, size_t srcLe
// that many wide characters plus one for the terminator. Pass destLen 0 to measure without writing.
// Returns UTF8_INVALID if src is not well-formed UTF-8, setting dest[0] to L'\0' if destLen > 0.
size_t Utf8_To_Wide(wchar_t* dest, size_t destLen, const char* src, size_t srcLen);

// Returns the largest length not greater than maxLen at which the srcLen bytes of the UTF-8 string
// src can be cut without splitting a multibyte sequence, by backing off the continuation bytes at
// the cut point. Returns srcLen when the string already fits in maxLen. Returns 0 when no whole
// sequence fits, which is also what malformed UTF-8 yields once it has no lead byte to back off to.
size_t Utf8_Truncate_Len(const char* src, size_t srcLen, size_t maxLen);
Loading