//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // 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 #include "core/crc.h" #include "core/frameAllocator.h" #include "core/util/str.h" #include "core/strings/stringFunctions.h" #include "core/strings/unicode.h" #include "platform/platformVolume.h" #include "platformWin32/winVolume.h" #include "console/console.h" #ifndef NGROUPS_UMAX #define NGROUPS_UMAX 32 #endif namespace Torque { namespace Win32 { // If the file is a Directory, Offline, System or Temporary then FALSE #define S_ISREG(Flags) \ !((Flags) & \ (FILE_ATTRIBUTE_DIRECTORY | \ FILE_ATTRIBUTE_OFFLINE | \ FILE_ATTRIBUTE_SYSTEM | \ FILE_ATTRIBUTE_TEMPORARY)) #define S_ISDIR(Flags) \ ((Flags) & FILE_ATTRIBUTE_DIRECTORY) //----------------------------------------------------------------------------- class Win32FileSystemChangeNotifier : public FileSystemChangeNotifier { public: Win32FileSystemChangeNotifier( FileSystem *fs ) : FileSystemChangeNotifier( fs ) { VECTOR_SET_ASSOCIATION( mHandleList ); VECTOR_SET_ASSOCIATION( mDirs ); } // for use in the thread itself U32 getNumHandles() const { return mHandleList.size(); } HANDLE *getHANDLES() { return mHandleList.address(); } private: virtual void internalProcessOnce(); virtual bool internalAddNotification( const Path &dir ); virtual bool internalRemoveNotification( const Path &dir ); Vector mDirs; Vector mHandleList; }; //----------------------------------------------------------------------------- static String _BuildFileName(const String& prefix,const Path& path) { // Need to join the path (minus the root) with our // internal path name. String file = prefix; file = Path::Join(file, '/', path.getPath()); if (path.getFileName().isEmpty() && path.getExtension().isNotEmpty()) //weird, filename-less file, so handle it slightly special-case file += String("/"); else file = Path::Join(file, '/', path.getFileName()); file = Path::Join(file, '.', path.getExtension()); return file; } /* static bool _IsFile(const String& file) { // Get file info WIN32_FIND_DATA info; HANDLE handle = ::FindFirstFile(PathToOS(file).utf16(), &info); ::FindClose(handle); if (handle == INVALID_HANDLE_VALUE) return false; return S_ISREG(info.dwFileAttributes); } */ static bool _IsDirectory(const String& file) { // Get file info WIN32_FIND_DATAW info; HANDLE handle = ::FindFirstFileW(PathToOS(file).utf16(), &info); ::FindClose(handle); if (handle == INVALID_HANDLE_VALUE) return false; return S_ISDIR(info.dwFileAttributes); } //----------------------------------------------------------------------------- static void _CopyStatAttributes(const WIN32_FIND_DATAW& info, FileNode::Attributes* attr) { // Fill in the return struct. attr->flags = 0; if (S_ISDIR(info.dwFileAttributes)) attr->flags |= FileNode::Directory; if (S_ISREG(info.dwFileAttributes)) attr->flags |= FileNode::File; if (info.dwFileAttributes & FILE_ATTRIBUTE_READONLY) attr->flags |= FileNode::ReadOnly; attr->size = info.nFileSizeLow; attr->mtime = Win32FileTimeToTime( info.ftLastWriteTime.dwLowDateTime, info.ftLastWriteTime.dwHighDateTime); attr->atime = Win32FileTimeToTime( info.ftLastAccessTime.dwLowDateTime, info.ftLastAccessTime.dwHighDateTime); attr->ctime = Win32FileTimeToTime( info.ftCreationTime.dwLowDateTime, info.ftCreationTime.dwHighDateTime); } //----------------------------------------------------------------------------- bool Win32FileSystemChangeNotifier::internalAddNotification( const Path &dir ) { for ( U32 i = 0; i < mDirs.size(); ++i ) { if ( mDirs[i] == dir ) return false; } Path fullFSPath = mFS->mapTo( dir ); String osPath = PathToOS( fullFSPath ); // Con::printf( "[Win32FileSystemChangeNotifier::internalAddNotification] : [%s]", osPath.c_str() ); HANDLE changeHandle = ::FindFirstChangeNotificationW( osPath.utf16(), // directory to watch FALSE, // do not watch subtree FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_ATTRIBUTES); // watch file write changes if (changeHandle == INVALID_HANDLE_VALUE || changeHandle == NULL) { Con::errorf("[Win32FileSystemChangeNotifier::internalAddNotification] : failed on [%s] [%d]", osPath.c_str(), GetLastError()); return false; } mDirs.push_back( dir ); mHandleList.push_back( changeHandle ); return true; } bool Win32FileSystemChangeNotifier::internalRemoveNotification( const Path &dir ) { for ( U32 i = 0; i < mDirs.size(); ++i ) { if ( mDirs[i] != dir ) continue; ::FindCloseChangeNotification( mHandleList[i] ); mDirs.erase( i ); mHandleList.erase( i ); return true; } return false; } void Win32FileSystemChangeNotifier::internalProcessOnce() { // WaitForMultipleObjects has a limit of MAXIMUM_WAIT_OBJECTS (64 at // the moment), so we have to loop till we've handled the entire set. for ( U32 i=0; i < mHandleList.size(); i += MAXIMUM_WAIT_OBJECTS ) { U32 numHandles = getMin( (U32)MAXIMUM_WAIT_OBJECTS, (U32)mHandleList.size() - i ); DWORD dwWaitStatus = WaitForMultipleObjects( numHandles, mHandleList.address()+i, FALSE, 0); if ( dwWaitStatus == WAIT_FAILED || dwWaitStatus == WAIT_TIMEOUT ) continue; if ( dwWaitStatus >= WAIT_OBJECT_0 && dwWaitStatus <= (WAIT_OBJECT_0 + numHandles - 1)) { U32 index = i + dwWaitStatus; // reset our notification // NOTE: we do this before letting the volume system check mod times so we don't miss any. // It is going to loop over the files and check their mod time vs. the saved time. // This may result in extra calls to internalNotifyDirChanged(), but it will simpley check mod times again. ::FindNextChangeNotification( mHandleList[index] ); internalNotifyDirChanged( mDirs[index] ); } } } //----------------------------------------------------------------------------- Win32FileSystem::Win32FileSystem(String volume) { mVolume = volume; mChangeNotifier = new Win32FileSystemChangeNotifier( this ); } Win32FileSystem::~Win32FileSystem() { } void Win32FileSystem::verifyCompatibility(const Path& _path, WIN32_FIND_DATAW _info) { #ifndef TORQUE_POSIX_PATH_CASE_INSENSITIVE if (_path.getFullFileName().isNotEmpty() && _path.getFullFileName().compare(String(_info.cFileName)) != 0) { Con::warnf("Linux Compatibility Warning: %s != %s", String(_info.cFileName).c_str(), _path.getFullFileName().c_str()); } #endif } FileNodeRef Win32FileSystem::resolve(const Path& path) { String file = _BuildFileName(mVolume,path); WIN32_FIND_DATAW info; HANDLE handle = ::FindFirstFileW(PathToOS(file).utf16(), &info); ::FindClose(handle); if (handle != INVALID_HANDLE_VALUE) { #ifdef TORQUE_DEBUG verifyCompatibility(path, info); #endif if (S_ISREG(info.dwFileAttributes)) return new Win32File(path,file); if (S_ISDIR(info.dwFileAttributes)) return new Win32Directory(path,file); } return 0; } FileNodeRef Win32FileSystem::create(const Path& path, FileNode::Mode mode) { // The file will be created on disk when it's opened. if (mode & FileNode::File) return new Win32File(path,_BuildFileName(mVolume,path)); // Create with default permissions. if (mode & FileNode::Directory) { String file = PathToOS(_BuildFileName(mVolume,path)); if (::CreateDirectoryW(file.utf16(), 0)) return new Win32Directory(path, file); } return 0; } bool Win32FileSystem::remove(const Path& path) { // Should probably check for outstanding files or directory objects. String file = PathToOS(_BuildFileName(mVolume,path)); WIN32_FIND_DATAW info; HANDLE handle = ::FindFirstFileW(file.utf16(), &info); ::FindClose(handle); if (handle == INVALID_HANDLE_VALUE) return false; if (S_ISDIR(info.dwFileAttributes)) return ::RemoveDirectoryW(file.utf16()); return ::DeleteFileW(file.utf16()); } bool Win32FileSystem::rename(const Path& from,const Path& to) { String fa = PathToOS(_BuildFileName(mVolume,from)); String fb = PathToOS(_BuildFileName(mVolume,to)); return MoveFile(fa.utf16(),fb.utf16()); } Path Win32FileSystem::mapTo(const Path& path) { return _BuildFileName(mVolume,path); } Path Win32FileSystem::mapFrom(const Path& path) { const String::SizeType volumePathLen = mVolume.length(); String pathStr = path.getFullPath(); if ( mVolume.compare( pathStr, volumePathLen, String::NoCase )) return Path(); return pathStr.substr( volumePathLen, pathStr.length() - volumePathLen ); } //----------------------------------------------------------------------------- Win32File::Win32File(const Path& path,String name) { mPath = path; mName = name; mStatus = Closed; mHandle = 0; } Win32File::~Win32File() { if (mHandle) close(); } Path Win32File::getName() const { return mPath; } FileNode::NodeStatus Win32File::getStatus() const { return mStatus; } bool Win32File::getAttributes(Attributes* attr) { WIN32_FIND_DATAW info; HANDLE handle = ::FindFirstFileW(PathToOS(mName).utf16(), &info); ::FindClose(handle); if (handle == INVALID_HANDLE_VALUE) return false; _CopyStatAttributes(info,attr); attr->name = mPath; return true; } U64 Win32File::getSize() { U64 size; if (mStatus == Open) { // Special case if file is open (handles unflushed buffers) if ( !GetFileSizeEx(mHandle, (PLARGE_INTEGER)&size) ) size = 0; return size; } else { // Fallback to generic function size = File::getSize(); } return size; } U32 Win32File::calculateChecksum() { if (!open( Read )) return 0; U64 fileSize = getSize(); U32 bufSize = 1024 * 1024 * 4; // 4MB FrameTemp buf( bufSize ); U32 crc = CRC::INITIAL_CRC_VALUE; while ( fileSize > 0 ) { U32 bytesRead = getMin( fileSize, bufSize ); if ( read( buf, bytesRead ) != bytesRead ) { close(); return 0; } fileSize -= bytesRead; crc = CRC::calculateCRC(buf, bytesRead, crc); } close(); return crc; } bool Win32File::open(AccessMode mode) { close(); if (mName.isEmpty()) return mStatus; struct Mode { DWORD mode,share,open; } Modes[] = { { GENERIC_READ,FILE_SHARE_READ,OPEN_EXISTING }, // Read { GENERIC_WRITE,0,CREATE_ALWAYS }, // Write { GENERIC_WRITE | GENERIC_READ,0,OPEN_ALWAYS }, // ReadWrite { GENERIC_WRITE,0,OPEN_ALWAYS } // WriteAppend }; Mode& m = (mode == Read)? Modes[0]: (mode == Write)? Modes[1]: (mode == ReadWrite)? Modes[2]: Modes[3]; mHandle = (void*)::CreateFileW(PathToOS(mName).utf16(), m.mode, m.share, NULL, m.open, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if ( mHandle == INVALID_HANDLE_VALUE || mHandle == NULL ) { _updateStatus(); return false; } mStatus = Open; return true; } bool Win32File::close() { if (mHandle) { ::CloseHandle((HANDLE)mHandle); mHandle = 0; } mStatus = Closed; return true; } U32 Win32File::getPosition() { if (mStatus == Open || mStatus == EndOfFile) return ::SetFilePointer((HANDLE)mHandle,0,0,FILE_CURRENT); return 0; } U32 Win32File::setPosition(U32 delta, SeekMode mode) { if (mStatus != Open && mStatus != EndOfFile) return 0; DWORD fmode; switch (mode) { case Begin: fmode = FILE_BEGIN; break; case Current: fmode = FILE_CURRENT; break; case End: fmode = FILE_END; break; default: fmode = 0; break; } DWORD pos = ::SetFilePointer((HANDLE)mHandle,delta,0,fmode); if (pos == INVALID_SET_FILE_POINTER) { mStatus = UnknownError; return 0; } mStatus = Open; return pos; } U32 Win32File::read(void* dst, U32 size) { if (mStatus != Open && mStatus != EndOfFile) return 0; DWORD bytesRead; if (!::ReadFile((HANDLE)mHandle,dst,size,&bytesRead,0)) _updateStatus(); else if (bytesRead != size) mStatus = EndOfFile; return bytesRead; } U32 Win32File::write(const void* src, U32 size) { if ((mStatus != Open && mStatus != EndOfFile) || !size) return 0; DWORD bytesWritten; if (!::WriteFile((HANDLE)mHandle,src,size,&bytesWritten,0)) _updateStatus(); return bytesWritten; } void Win32File::_updateStatus() { switch (::GetLastError()) { case ERROR_INVALID_ACCESS: mStatus = AccessDenied; break; case ERROR_TOO_MANY_OPEN_FILES: mStatus = UnknownError; break; case ERROR_PATH_NOT_FOUND: mStatus = NoSuchFile; break; case ERROR_FILE_NOT_FOUND: mStatus = NoSuchFile; break; case ERROR_SHARING_VIOLATION: mStatus = SharingViolation; break; case ERROR_HANDLE_DISK_FULL: mStatus = FileSystemFull; break; case ERROR_ACCESS_DENIED: mStatus = AccessDenied; break; default: mStatus = UnknownError; break; } } //----------------------------------------------------------------------------- Win32Directory::Win32Directory(const Path& path,String name) { mPath = path; mName = name; mStatus = Closed; mHandle = 0; } Win32Directory::~Win32Directory() { if (mHandle) close(); } Path Win32Directory::getName() const { return mPath; } bool Win32Directory::open() { if (!_IsDirectory(mName)) { mStatus = NoSuchFile; return false; } mStatus = Open; return true; } bool Win32Directory::close() { if (mHandle) { ::FindClose((HANDLE)mHandle); mHandle = 0; return true; } return false; } bool Win32Directory::read(Attributes* entry) { if (mStatus != Open) return false; WIN32_FIND_DATA info; if (!mHandle) { mHandle = ::FindFirstFileW((PathToOS(mName) + "\\*").utf16(), &info); if (mHandle == NULL) { _updateStatus(); return false; } } else if (!::FindNextFileW((HANDLE)mHandle, &info)) { _updateStatus(); return false; } // Skip "." and ".." entries if (info.cFileName[0] == '.' && (info.cFileName[1] == '\0' || (info.cFileName[1] == '.' && info.cFileName[2] == '\0'))) return read(entry); _CopyStatAttributes(info,entry); entry->name = info.cFileName; return true; } U32 Win32Directory::calculateChecksum() { // Return checksum of current entry return 0; } bool Win32Directory::getAttributes(Attributes* attr) { WIN32_FIND_DATA info; HANDLE handle = ::FindFirstFileW(PathToOS(mName).utf16(), &info); ::FindClose(handle); if (handle == INVALID_HANDLE_VALUE) { _updateStatus(); return false; } _CopyStatAttributes(info,attr); attr->name = mPath; return true; } FileNode::NodeStatus Win32Directory::getStatus() const { return mStatus; } void Win32Directory::_updateStatus() { switch (::GetLastError()) { case ERROR_NO_MORE_FILES: mStatus = EndOfFile; break; case ERROR_INVALID_ACCESS: mStatus = AccessDenied; break; case ERROR_PATH_NOT_FOUND: mStatus = NoSuchFile; break; case ERROR_SHARING_VIOLATION: mStatus = SharingViolation; break; case ERROR_ACCESS_DENIED: mStatus = AccessDenied; break; default: mStatus = UnknownError; break; } } } // Namespace Win32 bool FS::VerifyWriteAccess(const Path &path) { // due to UAC's habit of creating "virtual stores" when permission isn't actually available // actually create, write, read, verify, and delete a file to the folder being tested String temp = path.getFullPath(); temp += "\\torque_writetest.tmp"; // first, (try and) delete the file if it exists ::DeleteFileW(temp.utf16()); // now, create the file HANDLE hFile = ::CreateFileW(PathToOS(temp).utf16(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if ( hFile == INVALID_HANDLE_VALUE || hFile == NULL ) return false; U32 t = Platform::getTime(); DWORD bytesWritten; if (!::WriteFile(hFile,&t,sizeof(t),&bytesWritten,0)) { ::CloseHandle(hFile); ::DeleteFileW(temp.utf16()); return false; } // close the file ::CloseHandle(hFile); // open for read hFile = ::CreateFileW(PathToOS(temp).utf16(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); if ( hFile == INVALID_HANDLE_VALUE || hFile == NULL ) return false; U32 t2 = 0; DWORD bytesRead; if (!::ReadFile(hFile,&t2,sizeof(t2),&bytesRead,0)) { ::CloseHandle(hFile); ::DeleteFileW(temp.utf16()); return false; } ::CloseHandle(hFile); ::DeleteFileW(temp.utf16()); return t == t2; } } // Namespace Torque //----------------------------------------------------------------------------- Torque::FS::FileSystemRef Platform::FS::createNativeFS( const String &volume ) { return new Win32::Win32FileSystem( volume ); } String Platform::FS::getAssetDir() { char cen_buf[2048]; #ifdef TORQUE_UNICODE if (!Platform::getWebDeployment()) { TCHAR buf[ 2048 ]; ::GetModuleFileNameW( NULL, buf, sizeof( buf ) ); convertUTF16toUTF8( buf, cen_buf ); } else { TCHAR buf[ 2048 ]; GetCurrentDirectoryW( sizeof( buf ) / sizeof( buf[ 0 ] ), buf ); convertUTF16toUTF8( buf, cen_buf ); return Path::CleanSeparators(cen_buf); } #else ::GetModuleFileNameA( NULL, cen_buf, 2047); #endif char *delimiter = dStrrchr( cen_buf, '\\' ); if( delimiter != NULL ) *delimiter = '\0'; return Path::CleanSeparators(cen_buf); } /// Function invoked by the kernel layer to install OS specific /// file systems. bool Platform::FS::InstallFileSystems() { #ifndef TORQUE_SECURE_VFS WCHAR buffer[1024]; // [8/24/2009 tomb] This stops Windows from complaining about drives that have no disks in SetErrorMode(SEM_FAILCRITICALERRORS); // Load all the Win32 logical drives. DWORD mask = ::GetLogicalDrives(); char drive[] = "A"; char volume[] = "A:/"; while (mask) { if (mask & 1) { volume[0] = drive[0]; Platform::FS::Mount(drive, Platform::FS::createNativeFS(volume)); } mask >>= 1; drive[0]++; } // Set the current working dir. Windows normally returns // upper case driver letters, but the cygwin bash shell // seems to make it return lower case drives. Force upper // to be consistent with the mounts. ::GetCurrentDirectory(sizeof(buffer), buffer); if (buffer[1] == ':') buffer[0] = dToupper(buffer[0]); String wd = buffer; wd += '/'; Platform::FS::SetCwd(wd); #endif return true; }