summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/sv_save.c318
1 files changed, 239 insertions, 79 deletions
diff --git a/src/sv_save.c b/src/sv_save.c
index 6ce3985..8bf218f 100644
--- a/src/sv_save.c
+++ b/src/sv_save.c
@@ -20,6 +20,12 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#include "sv_local.h"
+#define SAVE_MAGIC1 (('1'<<24)|('V'<<16)|('A'<<8)|'S')
+#define SAVE_MAGIC2 (('2'<<24)|('V'<<16)|('A'<<8)|'S')
+#define SAVE_VERSION 1
+
+// save to temporary dir and rename only when done
+#define SAVE_CURRENT ".current"
/*
===============================================================================
@@ -29,22 +35,18 @@ SAVEGAME FILES
===============================================================================
*/
-static void write_binary_file( const char *name ) {
- qerror_t ret;
-
- ret = FS_WriteFile( name, msg_write.data, msg_write.cursize );
- if( ret < 0 ) {
- Com_EPrintf( "%s: couldn't write %s: %s\n", __func__, name, Q_ErrorString( ret ) );
- }
-}
-
-static void write_server_file( qboolean autosave ) {
+static qerror_t write_server_file( qboolean autosave ) {
char name[MAX_OSPATH];
cvar_t *var;
+ size_t len;
+ qerror_t ret;
+
+ // write magic
+ MSG_WriteLong( SAVE_MAGIC1 );
+ MSG_WriteLong( SAVE_VERSION );
// write the comment field
MSG_WriteByte( autosave );
- MSG_WriteLong( time( NULL ) );
MSG_WriteString( sv.configstrings[CS_NAME] );
// write the mapcmd
@@ -60,24 +62,38 @@ static void write_server_file( qboolean autosave ) {
}
MSG_WriteString( NULL );
- Q_snprintf (name, sizeof(name), "save/current/server.state");
- write_binary_file( name );
+ // write server state
+ ret = FS_WriteFile( "save/" SAVE_CURRENT "/server.state",
+ msg_write.data, msg_write.cursize );
SZ_Clear( &msg_write );
+ if( ret < 0 ) {
+ return ret;
+ }
+
// write game state
- Q_snprintf (name, sizeof(name), "%s/save/current/game.state", fs_gamedir);
+ len = Q_snprintf( name, sizeof( name ),
+ "%s/save/" SAVE_CURRENT "/game.state", fs_gamedir );
+ if( len >= sizeof( name ) ) {
+ return Q_ERR_NAMETOOLONG;
+ }
+
ge->WriteGame (name, autosave);
+ return Q_ERR_SUCCESS;
}
-static void write_level_file( void ) {
+static qerror_t write_level_file( void ) {
char name[MAX_OSPATH];
int i;
char *s;
size_t len;
byte portalbits[MAX_MAP_PORTAL_BYTES];
+ qerror_t ret;
- Com_DPrintf( "%s()\n", __func__ );
+ // write magic
+ MSG_WriteLong( SAVE_MAGIC2 );
+ MSG_WriteLong( SAVE_VERSION );
// write configstrings
for( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
@@ -100,64 +116,165 @@ static void write_level_file( void ) {
MSG_WriteByte( len );
MSG_WriteData( portalbits, len );
- Q_snprintf (name, sizeof(name), "save/current/server.level");
- write_binary_file( name );
+ ret = FS_WriteFile( "save/" SAVE_CURRENT "/server.level",
+ msg_write.data, msg_write.cursize );
SZ_Clear( &msg_write );
- Q_snprintf( name, sizeof( name ), "%s/save/current/game.level", fs_gamedir );
+ if( ret < 0 ) {
+ return ret;
+ }
+
+ // write game level
+ len = Q_snprintf( name, sizeof( name ),
+ "%s/save/" SAVE_CURRENT "/game.level", fs_gamedir );
+ if( len >= sizeof( name ) ) {
+ return Q_ERR_NAMETOOLONG;
+ }
+
ge->WriteLevel( name );
+ return Q_ERR_SUCCESS;
}
+static qerror_t rename_file( const char *dir, const char *base, const char *suf ) {
+ char from[MAX_QPATH];
+ char to[MAX_QPATH];
+ size_t len;
+
+ len = Q_snprintf( from, sizeof( from ), "save/%s/%s%s", SAVE_CURRENT, base, suf );
+ if( len >= sizeof( from ) )
+ return Q_ERR_NAMETOOLONG;
+
+ len = Q_snprintf( to, sizeof( to ), "save/%s/%s%s", dir, base, suf );
+ if( len >= sizeof( to ) )
+ return Q_ERR_NAMETOOLONG;
-static void read_binary_file( const char *name ) {
+ return FS_RenameFile( from, to );
+}
+
+static qerror_t move_files( const char *dir ) {
+ char name[MAX_OSPATH];
+ size_t len;
+ qerror_t ret;
+
+ len = Q_snprintf( name, sizeof( name ), "%s/save/%s/", fs_gamedir, dir );
+ if( len >= sizeof( name ) )
+ return Q_ERR_NAMETOOLONG;
+
+ ret = FS_CreatePath( name );
+ if( ret )
+ return ret;
+
+ ret = rename_file( dir, "game", ".level" );
+ if( ret )
+ return ret;
+
+ ret = rename_file( dir, "server", ".level" );
+ if( ret )
+ return ret;
+
+ ret = rename_file( dir, "game", ".state" );
+ if( ret )
+ return ret;
+
+ ret = rename_file( dir, "server", ".state" );
+ if( ret )
+ return ret;
+
+ return Q_ERR_SUCCESS;
+}
+
+static qerror_t read_binary_file( const char *name ) {
qhandle_t f;
- ssize_t len;
+ ssize_t len, read;
+ qerror_t ret;
- len = FS_FOpenFile( name, &f, FS_MODE_READ|FS_TYPE_REAL|FS_PATH_GAME );
+ len = FS_FOpenFile( name, &f,
+ FS_MODE_READ | FS_TYPE_REAL | FS_PATH_GAME );
if( !f ) {
- Com_Error( ERR_DROP, "%s: couldn't open %s: %s\n", __func__, name, Q_ErrorString( len ) );
+ return len;
}
if( len > MAX_MSGLEN ) {
- FS_FCloseFile( f );
- Com_Error( ERR_DROP, "%s: %s is too large\n", __func__, name );
+ ret = Q_ERR_FBIG;
+ goto fail;
}
- FS_Read( msg_read_buffer, len, f );
+ read = FS_Read( msg_read_buffer, len, f );
+ if( read != len ) {
+ ret = read < 0 ? read : Q_ERR_UNEXPECTED_EOF;
+ goto fail;
+ }
SZ_Init( &msg_read, msg_read_buffer, len );
msg_read.cursize = len;
- msg_read.allowunderflow = qfalse;
+ ret = Q_ERR_SUCCESS;
+fail:
FS_FCloseFile( f );
+ return ret;
}
-static void read_server_file( void ) {
+static qerror_t read_server_file( const char *dir ) {
char name[MAX_OSPATH], string[MAX_STRING_CHARS];
- char mapcmd[MAX_TOKEN_CHARS];
+ char mapcmd[MAX_QPATH];
+ size_t len;
+ qerror_t ret;
- Com_DPrintf( "%s()\n", __func__ );
+ // errors like missing file, bad version, etc are
+ // non-fatal and just return to the command handler
+ len = Q_snprintf( name, MAX_QPATH, "save/%s/server.state", dir );
+ if( len >= MAX_QPATH ) {
+ return Q_ERR_NAMETOOLONG;
+ }
- Q_snprintf (name, sizeof(name), "save/current/server.state");
- read_binary_file( name );
+ ret = read_binary_file( name );
+ if( ret ) {
+ return ret;
+ }
+
+ if( MSG_ReadLong() != SAVE_MAGIC1 ) {
+ return Q_ERR_UNKNOWN_FORMAT;
+ }
+ if( MSG_ReadLong() != SAVE_VERSION ) {
+ return Q_ERR_INVALID_FORMAT;
+ }
+
+ // any error is fatal from this point
+ SV_Shutdown( "Server restarted\n", ERR_RECONNECT );
+
+ // the rest can't underflow
+ msg_read.allowunderflow = qfalse;
// read the comment field
MSG_ReadByte();
- MSG_ReadLong();
MSG_ReadString( NULL, 0 );
// read the mapcmd
- MSG_ReadString( mapcmd, sizeof( mapcmd ) );
+ len = MSG_ReadString( mapcmd, sizeof( mapcmd ) );
+ if( len >= sizeof( mapcmd ) ) {
+ ret = Q_ERR_STRING_TRUNCATED;
+ goto fail;
+ }
// read all CVAR_LATCH cvars
// these will be things like coop, skill, deathmatch, etc
while( 1 ) {
- MSG_ReadString( name, MAX_QPATH );
- if( !name[0] )
+ len = MSG_ReadString( name, MAX_QPATH );
+ if( !len )
break;
- MSG_ReadString( string, sizeof( string ) );
- Cvar_Set( name, string );
+ if( len >= MAX_QPATH ) {
+ ret = Q_ERR_STRING_TRUNCATED;
+ goto fail;
+ }
+
+ len = MSG_ReadString( string, sizeof( string ) );
+ if( len >= sizeof( string ) ) {
+ ret = Q_ERR_STRING_TRUNCATED;
+ goto fail;
+ }
+
+ Cvar_UserSet( name, string );
}
// start a new game fresh with new cvars
@@ -169,23 +286,51 @@ static void read_server_file( void ) {
}
// read game state
- Q_snprintf (name, sizeof(name), "%s/save/current/game.state", fs_gamedir);
+ len = Q_snprintf( name, sizeof( name ), "%s/save/%s/game.state", fs_gamedir, dir );
+ if( len >= sizeof( name ) ) {
+ ret = Q_ERR_NAMETOOLONG;
+ goto fail;
+ }
+
ge->ReadGame (name);
// go to the map
sv.state = ss_game; // don't save current level when changing
SV_Map( mapcmd, qfalse );
+ return Q_ERR_SUCCESS;
+
+fail:
+ Com_Error( ERR_DROP, "Couldn't load %s: %s", dir, Q_ErrorString( ret ) );
}
-static void read_level_file( void ) {
+static void read_level_file( const char *dir ) {
char name[MAX_OSPATH];
- size_t len;
+ size_t len, maxlen;
+ qerror_t ret;
int index;
- Com_DPrintf( "%s\n", __func__ );
+ len = Q_snprintf( name, MAX_QPATH, "save/%s/server.level", dir );
+ if( len >= MAX_QPATH ) {
+ ret = Q_ERR_NAMETOOLONG;
+ goto fail;
+ }
- Q_snprintf (name, sizeof(name), "save/current/server.level");
- read_binary_file( name );
+ ret = read_binary_file( name );
+ if( ret ) {
+ goto fail;
+ }
+
+ if( MSG_ReadLong() != SAVE_MAGIC2 ) {
+ ret = Q_ERR_UNKNOWN_FORMAT;
+ goto fail;
+ }
+ if( MSG_ReadLong() != SAVE_VERSION ) {
+ ret = Q_ERR_INVALID_FORMAT;
+ goto fail;
+ }
+
+ // the rest can't underflow
+ msg_read.allowunderflow = qfalse;
while( 1 ) {
index = MSG_ReadShort();
@@ -193,25 +338,43 @@ static void read_level_file( void ) {
break;
}
if( index < 0 || index >= MAX_CONFIGSTRINGS ) {
- Com_Error( ERR_DROP, "%s: bad configstring index", __func__ );
+ ret = Q_ERR_BAD_INDEX;
+ goto fail;
+ }
+
+ maxlen = CS_SIZE( index );
+ len = MSG_ReadString( sv.configstrings[index], maxlen );
+ if( len >= maxlen ) {
+ ret = Q_ERR_STRING_TRUNCATED;
+ goto fail;
}
- MSG_ReadString( sv.configstrings[index], MAX_QPATH );
}
len = MSG_ReadByte();
if( len > MAX_MAP_PORTAL_BYTES ) {
- Com_Error( ERR_DROP, "%s: bad portalbits length", __func__ );
+ ret = Q_ERR_INVALID_FORMAT;
+ goto fail;
}
SV_ClearWorld();
CM_SetPortalStates( &sv.cm, MSG_ReadData( len ), len );
- Q_snprintf( name, sizeof( name ), "%s/save/current/game.level", fs_gamedir );
+ // read game level
+ len = Q_snprintf( name, sizeof( name ), "%s/save/%s/game.level", fs_gamedir, dir );
+ if( len >= sizeof( name ) ) {
+ ret = Q_ERR_NAMETOOLONG;
+ goto fail;
+ }
+
ge->ReadLevel( name );
ge->RunFrame();
ge->RunFrame();
+ return;
+
+fail:
+ Com_Error( ERR_DROP, "Couldn't load %s: %s", dir, Q_ErrorString( ret ) );
}
@@ -222,8 +385,8 @@ SV_Loadgame_f
==============
*/
void SV_Loadgame_f (void) {
- char name[MAX_OSPATH];
- char *dir;
+ char *dir;
+ qerror_t ret;
if (Cmd_Argc() != 2) {
Com_Printf ("Usage: %s <directory>\n", Cmd_Argv(0));
@@ -236,25 +399,18 @@ void SV_Loadgame_f (void) {
}
dir = Cmd_Argv(1);
- if (strstr (dir, "..") || strchr (dir, '/') || strchr (dir, '\\') ) {
+ if (!COM_IsPath(dir) ) {
Com_Printf ("Bad savedir.\n");
return;
}
- // make sure the server.ssv file exists
- Q_snprintf (name, sizeof(name), "save/%s/server.state", Cmd_Argv(1));
- if (!FS_FileExists( name ) ) {
- Com_Printf ("No such savegame: %s\n", name);
+ ret = read_server_file( dir );
+ if( ret ) {
+ Com_Printf( "Couldn't load %s: %s\n", dir, Q_ErrorString( ret ) );
return;
}
- Com_Printf ("Loading game...\n");
-
- //SV_CopySaveGame (Cmd_Argv(1), "current");
-
- read_server_file();
-
- read_level_file();
+ read_level_file( dir );
}
@@ -266,17 +422,13 @@ SV_Savegame_f
*/
void SV_Savegame_f( void ) {
char *dir;
+ qerror_t ret;
if (sv.state != ss_game) {
Com_Printf ("You must be in a game to save.\n");
return;
}
- if (Cmd_Argc() != 2) {
- Com_Printf ("Usage: %s <directory>\n", Cmd_Argv(0));
- return;
- }
-
if( dedicated->integer ) {
Com_Printf ("Savegames are for listen servers only\n");
return;
@@ -298,30 +450,38 @@ void SV_Savegame_f( void ) {
return;
}
- dir = Cmd_Argv(1);
- if (strstr (dir, "..") || strchr (dir, '/') || strchr (dir, '\\') ) {
- Com_Printf ("Bad savedir.\n");
+ if (Cmd_Argc() != 2) {
+ Com_Printf ("Usage: %s <directory>\n", Cmd_Argv(0));
return;
}
- if (!strcmp (dir, "current")) {
- Com_Printf ("Can't save to 'current'\n");
+ dir = Cmd_Argv(1);
+ if (!COM_IsPath(dir) ) {
+ Com_Printf ("Bad savedir.\n");
return;
}
- Com_Printf ("Saving game...\n");
-
// archive current level, including all client edicts.
// when the level is reloaded, they will be shells awaiting
// a connecting client
- write_level_file();
+ ret = write_level_file();
+ if( ret )
+ goto fail;
// save server state
- write_server_file( qfalse );
+ ret = write_server_file( qfalse );
+ if( ret )
+ goto fail;
+
+ // rename all stuff
+ ret = move_files( dir );
+ if( ret )
+ goto fail;
- // copy it off
- //SV_CopySaveGame ("current", dir);
+ Com_Printf ("Game saved.\n");
+ return;
- Com_Printf ("Done.\n");
+fail:
+ Com_EPrintf( "Couldn't write %s: %s\n", dir, Q_ErrorString( ret ) );
}