Index: ps/trunk/source/lib/sysdep/filesystem.h =================================================================== --- ps/trunk/source/lib/sysdep/filesystem.h (revision 25906) +++ ps/trunk/source/lib/sysdep/filesystem.h (revision 25907) @@ -1,123 +1,116 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * wchar_t versions of POSIX filesystem functions */ #ifndef INCLUDED_SYSDEP_FILESYSTEM #define INCLUDED_SYSDEP_FILESYSTEM #include "lib/os_path.h" #include "lib/posix/posix_filesystem.h" // mode_t // // dirent.h // struct WDIR; struct wdirent { // note: SUSv3 describes this as a "char array" but of unspecified size. // we declare as a pointer to avoid having to copy the string. wchar_t* d_name; }; extern WDIR* wopendir(const OsPath& path); extern wdirent* wreaddir(WDIR*); // return status for the file returned by the last successful // wreaddir call from the given directory stream. // currently sets st_size, st_mode, and st_mtime; the rest are zeroed. // non-portable, but considerably faster than stat(). used by dir_ForEachSortedEntry. extern int wreaddir_stat_np(WDIR*, struct stat*); extern int wclosedir(WDIR*); // // fcntl.h // // transfer directly to/from user's buffer. // treated as a request to enable aio. #ifndef O_DIRECT // i.e. Windows or OS X #define O_DIRECT 0x10000000 // (value does not conflict with any current Win32 _O_* flags.) #endif // Win32 _wsopen_s does not open files in a manner compatible with waio. // if its aio_read/aio_write are to be used, waio_open must (also) be called. // calling both is possible but wasteful and unsafe, since it prevents // file sharing restrictions, which are the only way to prevent // exposing previous data as a side effect of waio_Preallocate. // applications shouldn't mix aio and synchronous I/O anyway, so we // want wopen to call either waio_open or _wsopen_s. // since waio requires callers to respect the FILE_FLAG_NO_BUFFERING // restrictions (sector alignment), and IRIX/BSD/Linux O_DIRECT imposes // similar semantics, we treat that flag as a request to enable aio. extern int wopen(const OsPath& pathname, int oflag); extern int wopen(const OsPath& pathname, int oflag, mode_t mode); extern int wclose(int fd); // // unistd.h // // waio requires offsets and sizes to be multiples of the sector size. // to allow arbitrarily sized files, we truncate them after I/O. // however, ftruncate cannot be used since it is also subject to the // sector-alignment requirement. instead, the file must be closed and // this function called. LIB_API int wtruncate(const OsPath& pathname, off_t length); LIB_API int wunlink(const OsPath& pathname); LIB_API int wrmdir(const OsPath& path); // -// stdio.h -// - -LIB_API int wrename(const OsPath& pathnameOld, const OsPath& pathnameNew); - - -// // stdlib.h // LIB_API OsPath wrealpath(const OsPath& pathname); // // sys/stat.h // LIB_API int wstat(const OsPath& pathname, struct stat* buf); LIB_API int wmkdir(const OsPath& path, mode_t mode); #endif // #ifndef INCLUDED_SYSDEP_FILESYSTEM Index: ps/trunk/source/lib/sysdep/os/win/wposix/wfilesystem.cpp =================================================================== --- ps/trunk/source/lib/sysdep/os/win/wposix/wfilesystem.cpp (revision 25906) +++ ps/trunk/source/lib/sysdep/os/win/wposix/wfilesystem.cpp (revision 25907) @@ -1,355 +1,349 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "precompiled.h" #include "lib/sysdep/filesystem.h" #include "lib/sysdep/cpu.h" // cpu_CAS #include "lib/sysdep/os/win/wutil.h" // StatusFromWin #include "lib/sysdep/os/win/wposix/waio.h" // waio_reopen #include "lib/sysdep/os/win/wposix/wtime_internal.h" // wtime_utc_filetime_to_time_t #include "lib/sysdep/os/win/wposix/crt_posix.h" // _close, _lseeki64 etc. //----------------------------------------------------------------------------- // WDIR suballocator //----------------------------------------------------------------------------- // most applications only need a single WDIR at a time. we avoid expensive // heap allocations by reusing a single static instance. if it is already // in use, we allocate further instances dynamically. // NB: this is thread-safe due to CAS. struct WDIR // POD { HANDLE hFind; WIN32_FIND_DATAW findData; // indeterminate if hFind == INVALID_HANDLE_VALUE // wreaddir will return the address of this member. // (must be stored in WDIR to allow multiple independent // wopendir/wreaddir sequences). struct wdirent ent; // used by wreaddir to skip the first FindNextFileW. (a counter is // easy to test/update and also provides useful information.) size_t numCalls; }; static WDIR wdir_storage; static volatile intptr_t wdir_in_use; static inline WDIR* wdir_alloc() { if(cpu_CAS(&wdir_in_use, 0, 1)) // gained ownership return &wdir_storage; // already in use (rare) - allocate from heap return new WDIR; } static inline void wdir_free(WDIR* d) { if(d == &wdir_storage) { const bool ok = cpu_CAS(&wdir_in_use, 1, 0); // relinquish ownership ENSURE(ok); // ensure it wasn't double-freed } else // allocated from heap delete d; } //----------------------------------------------------------------------------- // dirent.h //----------------------------------------------------------------------------- static bool IsValidDirectory(const OsPath& path) { const DWORD fileAttributes = GetFileAttributesW(OsString(path).c_str()); // path not found if(fileAttributes == INVALID_FILE_ATTRIBUTES) return false; // not a directory if((fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) return false; // NB: no longer reject hidden or system attributes since // wsnd's add_oal_dlls_in_dir opens the Windows system directory, // which sometimes has these attributes set. return true; } WDIR* wopendir(const OsPath& path) { WinScopedPreserveLastError s; if(!IsValidDirectory(path)) { errno = ENOENT; return 0; } WDIR* d = wdir_alloc(); d->numCalls = 0; // NB: "c:\\path" only returns information about that directory; // trailing slashes aren't allowed. append "\\*" to retrieve its entries. OsPath searchPath = path/"*"; // (we don't defer FindFirstFileW until wreaddir because callers // expect us to return 0 if directory reading will/did fail.) d->hFind = FindFirstFileW(OsString(searchPath).c_str(), &d->findData); if(d->hFind != INVALID_HANDLE_VALUE) return d; // success if(GetLastError() == ERROR_NO_MORE_FILES) return d; // success, but directory is empty Status status = StatusFromWin(); // release the WDIR allocated above (this is preferable to // always copying the large WDIR or findData from a temporary) wdir_free(d); WARN_IF_ERR(status); errno = ErrnoFromStatus(status); return 0; } struct wdirent* wreaddir(WDIR* d) { // directory is empty and d->findData is indeterminate if(d->hFind == INVALID_HANDLE_VALUE) return 0; WinScopedPreserveLastError s; // until end of directory or a valid entry was found: for(;;) { if(d->numCalls++ != 0) // (skip first call to FindNextFileW - see wopendir) { if(!FindNextFileW(d->hFind, &d->findData)) { if(GetLastError() == ERROR_NO_MORE_FILES) SetLastError(0); else // unexpected error DEBUG_WARN_ERR(StatusFromWin()); return 0; // end of directory or error } } // only accept non-hidden and non-system entries - otherwise, // callers might encounter errors when attempting to open them. if((d->findData.dwFileAttributes & (FILE_ATTRIBUTE_HIDDEN|FILE_ATTRIBUTE_SYSTEM)) == 0) { d->ent.d_name = d->findData.cFileName; // (NB: d_name is a pointer) return &d->ent; } } } int wreaddir_stat_np(WDIR* d, struct stat* s) { // NTFS stores UTC but FAT stores local times, which are incorrectly // translated to UTC based on the _current_ DST settings. we no longer // bother checking the filesystem, since that's either unreliable or // expensive. timestamps may therefore be off after a DST transition, // which means our cached files would be regenerated. FILETIME* filetime = &d->findData.ftLastWriteTime; memset(s, 0, sizeof(*s)); s->st_size = (off_t)u64_from_u32(d->findData.nFileSizeHigh, d->findData.nFileSizeLow); s->st_mode = (unsigned short)((d->findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)? S_IFDIR : S_IFREG); s->st_mtime = wtime_utc_filetime_to_time_t(filetime); return 0; } int wclosedir(WDIR* d) { FindClose(d->hFind); wdir_free(d); return 0; } //----------------------------------------------------------------------------- // fcntl.h //----------------------------------------------------------------------------- int wopen(const OsPath& pathname, int oflag) { ENSURE(!(oflag & O_CREAT)); // must specify mode_arg if O_CREAT return wopen(OsString(pathname).c_str(), oflag, _S_IREAD|_S_IWRITE); } int wopen(const OsPath& pathname, int oflag, mode_t mode) { if(oflag & O_DIRECT) { Status ret = waio_open(pathname, oflag); if(ret < 0) { errno = ErrnoFromStatus(ret); return -1; } return (int)ret; // file descriptor } else { WinScopedPreserveLastError s; // _wsopen_s's CreateFileW int fd; oflag |= _O_BINARY; if(oflag & O_WRONLY) oflag |= O_CREAT|O_TRUNC; // NB: _wsopen_s ignores mode unless oflag & O_CREAT errno_t ret = _wsopen_s(&fd, OsString(pathname).c_str(), oflag, _SH_DENYRD, mode); if(ret != 0) { errno = ret; return -1; // NOWARN } return fd; } } int wclose(int fd) { ENSURE(fd >= 3); // not invalid nor stdin/out/err if(waio_close(fd) != 0) return _close(fd); return 0; } //----------------------------------------------------------------------------- // unistd.h //----------------------------------------------------------------------------- // we don't want to #define read to _read, since that's a fairly common // identifier. therefore, translate from MS CRT names via thunk functions. // efficiency is less important, and the overhead could be optimized away. int read(int fd, void* buf, size_t nbytes) { return _read(fd, buf, (int)nbytes); } int write(int fd, void* buf, size_t nbytes) { return _write(fd, buf, (int)nbytes); } off_t lseek(int fd, off_t ofs, int whence) { return _lseeki64(fd, ofs, whence); } int wtruncate(const OsPath& pathname, off_t length) { // (re-open the file to avoid the FILE_FLAG_NO_BUFFERING // sector-alignment restriction) HANDLE hFile = CreateFileW(OsString(pathname).c_str(), GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0); ENSURE(hFile != INVALID_HANDLE_VALUE); LARGE_INTEGER ofs; ofs.QuadPart = length; WARN_IF_FALSE(SetFilePointerEx(hFile, ofs, 0, FILE_BEGIN)); WARN_IF_FALSE(SetEndOfFile(hFile)); WARN_IF_FALSE(CloseHandle(hFile)); return 0; } int wunlink(const OsPath& pathname) { return _wunlink(OsString(pathname).c_str()); } int wrmdir(const OsPath& path) { return _wrmdir(OsString(path).c_str()); } -int wrename(const OsPath& pathnameOld, const OsPath& pathnameNew) -{ - return _wrename(OsString(pathnameOld).c_str(), OsString(pathnameNew).c_str()); -} - - OsPath wrealpath(const OsPath& pathname) { wchar_t resolved[PATH_MAX]; if(!GetFullPathNameW(OsString(pathname).c_str(), PATH_MAX, resolved, 0)) return OsPath(); return resolved; } static int ErrnoFromCreateDirectory() { switch(GetLastError()) { case ERROR_ALREADY_EXISTS: return EEXIST; case ERROR_PATH_NOT_FOUND: return ENOENT; case ERROR_ACCESS_DENIED: return EACCES; case ERROR_WRITE_PROTECT: return EROFS; case ERROR_DIRECTORY: return ENOTDIR; default: return 0; } } int wmkdir(const OsPath& path, mode_t UNUSED(mode)) { if(!CreateDirectoryW(OsString(path).c_str(), (LPSECURITY_ATTRIBUTES)NULL)) { errno = ErrnoFromCreateDirectory(); return -1; } return 0; } int wstat(const OsPath& pathname, struct stat* buf) { return _wstat64(OsString(pathname).c_str(), buf); } Index: ps/trunk/source/ps/VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/VisualReplay.cpp (revision 25906) +++ ps/trunk/source/ps/VisualReplay.cpp (revision 25907) @@ -1,508 +1,509 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "VisualReplay.h" #include "graphics/GameView.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/allocators/shared_ptr.h" +#include "lib/file/file_system.h" #include "lib/external_libraries/libsdl.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/Util.h" #include "scriptinterface/JSON.h" #include /** * Filter too short replays (value in seconds). */ const u8 minimumReplayDuration = 3; OsPath VisualReplay::GetDirectoryPath() { return Paths(g_CmdLineArgs).UserData() / "replays" / engine_version; } OsPath VisualReplay::GetCacheFilePath() { return GetDirectoryPath() / L"replayCache.json"; } OsPath VisualReplay::GetTempCacheFilePath() { return GetDirectoryPath() / L"replayCache_temp.json"; } bool VisualReplay::StartVisualReplay(const OsPath& directory) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); const OsPath replayFile = VisualReplay::GetDirectoryPath() / directory / L"commands.txt"; if (!FileExists(replayFile)) return false; g_Game = new CGame(false); return g_Game->StartVisualReplay(replayFile); } bool VisualReplay::ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject) { if (!FileExists(GetCacheFilePath())) return false; std::ifstream cacheStream(OsString(GetCacheFilePath()).c_str()); CStr cacheStr((std::istreambuf_iterator(cacheStream)), std::istreambuf_iterator()); cacheStream.close(); ScriptRequest rq(scriptInterface); JS::RootedValue cachedReplays(rq.cx); if (Script::ParseJSON(rq, cacheStr, &cachedReplays)) { cachedReplaysObject.set(&cachedReplays.toObject()); bool isArray; if (JS::IsArrayObject(rq.cx, cachedReplaysObject, &isArray) && isArray) return true; } LOGWARNING("The replay cache file is corrupted, it will be deleted"); wunlink(GetCacheFilePath()); return false; } void VisualReplay::StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays) { ScriptRequest rq(scriptInterface); JS::RootedValue replaysRooted(rq.cx, JS::ObjectValue(*replays)); std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc); cacheStream << Script::StringifyJSON(rq, &replaysRooted); cacheStream.close(); wunlink(GetCacheFilePath()); - if (wrename(GetTempCacheFilePath(), GetCacheFilePath())) + if (RenameFile(GetTempCacheFilePath(), GetCacheFilePath())) LOGERROR("Could not store the replay cache"); } JS::HandleObject VisualReplay::ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles) { TIMER(L"ReloadReplayCache"); ScriptRequest rq(scriptInterface); // Maps the filename onto the index, mtime and size using replayCacheMap = std::map>; replayCacheMap fileList; JS::RootedObject cachedReplaysObject(rq.cx); if (ReadCacheFile(scriptInterface, &cachedReplaysObject)) { // Create list of files included in the cache u32 cacheLength = 0; JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength); for (u32 j = 0; j < cacheLength; ++j) { JS::RootedValue replay(rq.cx); JS_GetElement(rq.cx, cachedReplaysObject, j, &replay); JS::RootedValue file(rq.cx); OsPath fileName; double fileSize; double fileMtime; Script::GetProperty(rq, replay, "directory", fileName); Script::GetProperty(rq, replay, "fileSize", fileSize); Script::GetProperty(rq, replay, "fileMTime", fileMtime); fileList[fileName] = std::make_tuple(j, fileMtime, fileSize); } } JS::RootedObject replays(rq.cx, JS::NewArrayObject(rq.cx, 0)); DirectoryNames directories; if (GetDirectoryEntries(GetDirectoryPath(), nullptr, &directories) != INFO::OK) return replays; bool newReplays = false; std::vector copyFromOldCache; // Specifies where the next replay should be kept u32 i = 0; for (const OsPath& directory : directories) { // This cannot use IsQuitRequested(), because the current loop and that function both run in the main thread. // So SDL events are not processed unless called explicitly here. if (SDL_QuitRequested()) // Don't return, because we want to save our progress break; const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt"; bool isNew = true; replayCacheMap::iterator it = fileList.find(directory); if (it != fileList.end()) { if (compareFiles) { if (!FileExists(replayFile)) continue; CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); if ((u64)fileInfo.MTime() == std::get<1>(it->second) && (off_t)fileInfo.Size() == std::get<2>(it->second)) isNew = false; } else isNew = false; } if (isNew) { JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, directory)); if (replayData.isNull()) { if (!FileExists(replayFile)) continue; CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); Script::CreateObject( rq, &replayData, "directory", directory.string(), "fileMTime", static_cast(fileInfo.MTime()), "fileSize", static_cast(fileInfo.Size())); } JS_SetElement(rq.cx, replays, i++, replayData); newReplays = true; } else copyFromOldCache.push_back(std::get<0>(it->second)); } debug_printf( "Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n", (unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i); if (!newReplays && fileList.empty()) return replays; // No replay was changed, so just return the cache if (!newReplays && fileList.size() == copyFromOldCache.size()) return cachedReplaysObject; { // Copy the replays from the old cache that are not deleted if (!copyFromOldCache.empty()) for (u32 j : copyFromOldCache) { JS::RootedValue replay(rq.cx); JS_GetElement(rq.cx, cachedReplaysObject, j, &replay); JS_SetElement(rq.cx, replays, i++, replay); } } StoreCacheFile(scriptInterface, replays); return replays; } JS::Value VisualReplay::GetReplays(const ScriptInterface& scriptInterface, bool compareFiles) { TIMER(L"GetReplays"); ScriptRequest rq(scriptInterface); JS::RootedObject replays(rq.cx, ReloadReplayCache(scriptInterface, compareFiles)); // Only take entries with data JS::RootedValue replaysWithoutNullEntries(rq.cx); Script::CreateArray(rq, &replaysWithoutNullEntries); u32 replaysLength = 0; JS::GetArrayLength(rq.cx, replays, &replaysLength); for (u32 j = 0, i = 0; j < replaysLength; ++j) { JS::RootedValue replay(rq.cx); JS_GetElement(rq.cx, replays, j, &replay); if (Script::HasProperty(rq, replay, "attribs")) Script::SetPropertyInt(rq, replaysWithoutNullEntries, i++, replay); } return replaysWithoutNullEntries; } /** * Move the cursor backwards until a newline was read or the beginning of the file was found. * Either way the cursor points to the beginning of a newline. * * @return The current cursor position or -1 on error. */ inline off_t goBackToLineBeginning(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { int currentPos; char character; for (int characters = 0; characters < 10000; ++characters) { currentPos = (int) replayStream->tellg(); // Stop when reached the beginning of the file if (currentPos == 0) return currentPos; if (!replayStream->good()) { LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.string8().c_str()); return -1; } // Stop when reached newline replayStream->get(character); if (character == '\n') return currentPos; // Otherwise go back one character. // Notice: -1 will set the cursor back to the most recently read character. replayStream->seekg(-2, std::ios_base::cur); } LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.string8().c_str()); return -1; } /** * Compute game duration in seconds. Assume constant turn length. * Find the last line that starts with "turn" by reading the file backwards. * * @return seconds or -1 on error */ inline int getReplayDuration(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { CStr type; // Move one character before the file-end replayStream->seekg(-2, std::ios_base::end); // Infinite loop protection, should never occur. // There should be about 5 lines to read until a turn is found. for (int linesRead = 1; linesRead < 1000; ++linesRead) { off_t currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); // Read error or reached file beginning. No turns exist. if (currentPosition < 1) return -1; if (!replayStream->good()) { LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.string8().c_str()); return -1; } // Found last turn, compute duration. if (currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") { u32 turn = 0, turnLength = 0; *replayStream >> turn >> turnLength; return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0 } // Otherwise move cursor back to the character before the last newline replayStream->seekg(currentPosition - 2, std::ios_base::beg); } LOGERROR("Infinite loop when determining replay duration for %s", fileName.string8().c_str()); return -1; } JS::Value VisualReplay::LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory) { // The directory argument must not be constant, otherwise concatenating will fail const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt"; if (!FileExists(replayFile)) return JS::NullValue(); // Get file size and modification date CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); const off_t fileSize = fileInfo.Size(); if (fileSize == 0) return JS::NullValue(); std::ifstream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type; if (!(*replayStream >> type).good()) { LOGERROR("Couldn't open %s.", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } if (type != "start") { LOGWARNING("The replay %s doesn't begin with 'start'!", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } // Parse header / first line CStr header; std::getline(*replayStream, header); ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); if (!Script::ParseJSON(rq, header, &attribs)) { LOGERROR("Couldn't parse replay header of %s", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } // Ensure "turn" after header if (!(*replayStream >> type).good() || type != "turn") { SAFE_DELETE(replayStream); return JS::NullValue(); // there are no turns at all } // Don't process files of rejoined clients u32 turn = 1; *replayStream >> turn; if (turn != 0) { SAFE_DELETE(replayStream); return JS::NullValue(); } int duration = getReplayDuration(replayStream, replayFile, fileSize); SAFE_DELETE(replayStream); // Ensure minimum duration if (duration < minimumReplayDuration) return JS::NullValue(); // Return the actual data JS::RootedValue replayData(rq.cx); Script::CreateObject( rq, &replayData, "directory", directory.string(), "fileSize", static_cast(fileSize), "fileMTime", static_cast(fileInfo.MTime()), "duration", duration); Script::SetProperty(rq, replayData, "attribs", attribs); return replayData; } bool VisualReplay::DeleteReplay(const OsPath& replayDirectory) { if (replayDirectory.empty()) return false; const OsPath directory = GetDirectoryPath() / replayDirectory; return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; } JS::Value VisualReplay::GetReplayAttributes(const ScriptInterface& scriptInterface, const OsPath& directoryName) { // Create empty JS object ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); Script::CreateObject(rq, &attribs); // Return empty object if file doesn't exist const OsPath replayFile = GetDirectoryPath() / directoryName / L"commands.txt"; if (!FileExists(replayFile)) return attribs; // Open file std::istream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type, line; ENSURE((*replayStream >> type).good() && type == "start"); // Read and return first line std::getline(*replayStream, line); Script::ParseJSON(rq, line, &attribs); SAFE_DELETE(replayStream);; return attribs; } void VisualReplay::AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName) { TIMER(L"AddReplayToCache"); ScriptRequest rq(scriptInterface); JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, OsPath(directoryName))); if (replayData.isNull()) return; JS::RootedObject cachedReplaysObject(rq.cx); if (!ReadCacheFile(scriptInterface, &cachedReplaysObject)) cachedReplaysObject = JS::NewArrayObject(rq.cx, 0); u32 cacheLength = 0; JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength); JS_SetElement(rq.cx, cachedReplaysObject, cacheLength, replayData); StoreCacheFile(scriptInterface, cachedReplaysObject); } bool VisualReplay::HasReplayMetadata(const OsPath& directoryName) { const OsPath filePath(GetDirectoryPath() / directoryName / L"metadata.json"); if (!FileExists(filePath)) return false; CFileInfo fileInfo; GetFileInfo(filePath, &fileInfo); return fileInfo.Size() > 0; } JS::Value VisualReplay::GetReplayMetadata(const ScriptInterface& scriptInterface, const OsPath& directoryName) { if (!HasReplayMetadata(directoryName)) return JS::NullValue(); ScriptRequest rq(scriptInterface); JS::RootedValue metadata(rq.cx); std::ifstream* stream = new std::ifstream(OsString(GetDirectoryPath() / directoryName / L"metadata.json").c_str()); ENSURE(stream->good()); CStr line; std::getline(*stream, line); stream->close(); SAFE_DELETE(stream); Script::ParseJSON(rq, line, &metadata); return metadata; }