414 lines
15 KiB
C++
414 lines
15 KiB
C++
#include "file_nfs.h"
|
|
|
|
#include <Windows.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <climits>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
struct MountEntry {
|
|
std::string alias;
|
|
std::wstring root;
|
|
bool readOnly;
|
|
};
|
|
|
|
struct FileState {
|
|
HANDLE handle = INVALID_HANDLE_VALUE;
|
|
std::mutex mutex;
|
|
std::wstring nativePath;
|
|
~FileState() {
|
|
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
|
|
}
|
|
};
|
|
|
|
std::mutex mountsMutex;
|
|
std::vector<MountEntry> mounts;
|
|
|
|
std::string Lower(std::string text) {
|
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
|
|
return static_cast<char>(std::tolower(c));
|
|
});
|
|
return text;
|
|
}
|
|
|
|
std::wstring Wide(const char* text) {
|
|
if (text == nullptr || *text == 0) return std::wstring();
|
|
int amount = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
|
text, -1, nullptr, 0);
|
|
UINT codePage = CP_UTF8;
|
|
DWORD flags = MB_ERR_INVALID_CHARS;
|
|
if (amount == 0) {
|
|
codePage = CP_ACP;
|
|
flags = 0;
|
|
amount = MultiByteToWideChar(codePage, flags, text, -1, nullptr, 0);
|
|
}
|
|
if (amount <= 0) return std::wstring();
|
|
std::wstring result(static_cast<std::size_t>(amount), L'\0');
|
|
MultiByteToWideChar(codePage, flags, text, -1, &result[0], amount);
|
|
result.resize(static_cast<std::size_t>(amount - 1));
|
|
return result;
|
|
}
|
|
|
|
std::wstring FullPath(const std::wstring& path) {
|
|
const DWORD required = GetFullPathNameW(path.c_str(), 0, nullptr, nullptr);
|
|
if (required == 0) return std::wstring();
|
|
std::wstring result(static_cast<std::size_t>(required), L'\0');
|
|
const DWORD amount = GetFullPathNameW(path.c_str(), required,
|
|
&result[0], nullptr);
|
|
if (amount == 0 || amount >= required) return std::wstring();
|
|
result.resize(amount);
|
|
while (result.size() > 3
|
|
&& (result.back() == L'\\' || result.back() == L'/')) {
|
|
result.pop_back();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool PathStartsWith(const std::wstring& path, const std::wstring& root) {
|
|
if (path.size() < root.size()) return false;
|
|
if (_wcsnicmp(path.c_str(), root.c_str(), root.size()) != 0) return false;
|
|
return path.size() == root.size() || path[root.size()] == L'\\'
|
|
|| path[root.size()] == L'/';
|
|
}
|
|
|
|
bool Resolve(const char* nfsPath, std::wstring& nativePath,
|
|
bool* readOnly = nullptr, std::string* aliasResult = nullptr) {
|
|
std::string path = nfsPath == nullptr ? "" : nfsPath;
|
|
if (path.size() >= 4 && Lower(path.substr(0, 4)) == "nfs:") {
|
|
path.erase(0, 4);
|
|
}
|
|
std::replace(path.begin(), path.end(), '\\', '/');
|
|
while (!path.empty() && path.front() == '/') path.erase(path.begin());
|
|
const std::size_t slash = path.find('/');
|
|
const std::string alias = Lower(path.substr(0, slash));
|
|
const std::string relative = slash == std::string::npos
|
|
? std::string() : path.substr(slash + 1);
|
|
if (alias.empty()) return false;
|
|
|
|
std::lock_guard<std::mutex> lock(mountsMutex);
|
|
for (const MountEntry& mount : mounts) {
|
|
if (mount.alias != alias) continue;
|
|
std::wstring candidate = mount.root;
|
|
if (!relative.empty()) {
|
|
candidate.push_back(L'\\');
|
|
std::wstring tail = Wide(relative.c_str());
|
|
std::replace(tail.begin(), tail.end(), L'/', L'\\');
|
|
candidate += tail;
|
|
}
|
|
candidate = FullPath(candidate);
|
|
if (candidate.empty() || !PathStartsWith(candidate, mount.root)) {
|
|
return false;
|
|
}
|
|
nativePath = candidate;
|
|
if (readOnly != nullptr) *readOnly = mount.readOnly;
|
|
if (aliasResult != nullptr) *aliasResult = alias;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CreateDirectories(const std::wstring& path, const bool includeLast) {
|
|
std::wstring target = path;
|
|
if (!includeLast) {
|
|
const std::size_t slash = target.find_last_of(L"\\/");
|
|
if (slash == std::wstring::npos) return true;
|
|
target.resize(slash);
|
|
}
|
|
const std::size_t rootEnd = target.size() >= 3 && target[1] == L':' ? 3 : 0;
|
|
for (std::size_t index = rootEnd; index <= target.size(); ++index) {
|
|
if (index != target.size() && target[index] != L'\\'
|
|
&& target[index] != L'/') continue;
|
|
const std::wstring part = target.substr(0, index);
|
|
if (part.empty()) continue;
|
|
if (!CreateDirectoryW(part.c_str(), nullptr)) {
|
|
const DWORD error = GetLastError();
|
|
if (error != ERROR_ALREADY_EXISTS) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool RefreshMetadata(FileState& state, std::uint64_t& size,
|
|
unsigned int& timestamp) {
|
|
LARGE_INTEGER length{};
|
|
FILETIME writeTime{};
|
|
if (!GetFileSizeEx(state.handle, &length)
|
|
|| !GetFileTime(state.handle, nullptr, nullptr, &writeTime)) return false;
|
|
size = static_cast<std::uint64_t>(length.QuadPart);
|
|
ULARGE_INTEGER ticks{};
|
|
ticks.LowPart = writeTime.dwLowDateTime;
|
|
ticks.HighPart = writeTime.dwHighDateTime;
|
|
constexpr std::uint64_t epoch = 116444736000000000ULL;
|
|
timestamp = ticks.QuadPart > epoch
|
|
? static_cast<unsigned int>((ticks.QuadPart - epoch) / 10000000ULL) : 0;
|
|
return true;
|
|
}
|
|
|
|
bool HasExtension(const std::wstring& name, const char* extension) {
|
|
if (extension == nullptr || *extension == 0
|
|
|| std::strcmp(extension, "*") == 0) return true;
|
|
const std::wstring suffix = Wide(extension);
|
|
return name.size() >= suffix.size()
|
|
&& _wcsicmp(name.c_str() + name.size() - suffix.size(),
|
|
suffix.c_str()) == 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
idFile_Nfs::idFile_Nfs()
|
|
: uniqID(0), openRemote(false), openPadding{}, fh{},
|
|
fullPath("invalid"), mode(NFS_FS_READ), position(0), size(0),
|
|
timeStamp(0), demandSeek(false), seekPadding{}, nfsClient(nullptr),
|
|
ro(false) {}
|
|
|
|
idFile_Nfs::~idFile_Nfs() {
|
|
delete static_cast<FileState*>(nfsClient);
|
|
nfsClient = nullptr;
|
|
openRemote = false;
|
|
}
|
|
|
|
bool idFile_Nfs::Mount(const char* alias, const char* windowsRoot,
|
|
const bool readOnly) {
|
|
std::string normalized = Lower(alias == nullptr ? "" : alias);
|
|
while (!normalized.empty() && normalized.front() == '/') {
|
|
normalized.erase(normalized.begin());
|
|
}
|
|
while (!normalized.empty() && normalized.back() == '/') normalized.pop_back();
|
|
if (normalized.empty() || normalized.find('/') != std::string::npos
|
|
|| normalized.find('\\') != std::string::npos) return false;
|
|
const std::wstring root = FullPath(Wide(windowsRoot));
|
|
if (root.empty()) return false;
|
|
const DWORD attributes = GetFileAttributesW(root.c_str());
|
|
if (attributes == INVALID_FILE_ATTRIBUTES
|
|
|| (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) return false;
|
|
|
|
std::lock_guard<std::mutex> lock(mountsMutex);
|
|
for (MountEntry& mount : mounts) {
|
|
if (mount.alias == normalized) {
|
|
mount.root = root;
|
|
mount.readOnly = readOnly;
|
|
return true;
|
|
}
|
|
}
|
|
mounts.push_back({normalized, root, readOnly});
|
|
return true;
|
|
}
|
|
|
|
void idFile_Nfs::UnmountAll() {
|
|
std::lock_guard<std::mutex> lock(mountsMutex);
|
|
mounts.clear();
|
|
}
|
|
|
|
bool idFile_Nfs::Open(const char* path, const nfsFileMode_t openMode,
|
|
bool create, const bool createPath) {
|
|
delete static_cast<FileState*>(nfsClient);
|
|
nfsClient = nullptr;
|
|
openRemote = false;
|
|
|
|
std::wstring native;
|
|
bool mountReadOnly = false;
|
|
if (!Resolve(path, native, &mountReadOnly)) return false;
|
|
const bool wantsWrite = openMode == NFS_FS_WRITE
|
|
|| openMode == NFS_FS_READ_WRITE || openMode == NFS_FS_APPEND;
|
|
if (mountReadOnly && wantsWrite) return false;
|
|
if (openMode == NFS_FS_WRITE || openMode == NFS_FS_APPEND) create = true;
|
|
if (createPath && create && !CreateDirectories(native, false)) return false;
|
|
|
|
DWORD access = GENERIC_READ;
|
|
DWORD disposition = OPEN_EXISTING;
|
|
if (openMode == NFS_FS_WRITE) {
|
|
access = GENERIC_WRITE | GENERIC_READ;
|
|
disposition = CREATE_ALWAYS;
|
|
} else if (openMode == NFS_FS_READ_WRITE) {
|
|
access = GENERIC_WRITE | GENERIC_READ;
|
|
disposition = create ? OPEN_ALWAYS : OPEN_EXISTING;
|
|
} else if (openMode == NFS_FS_APPEND) {
|
|
access = GENERIC_WRITE | GENERIC_READ;
|
|
disposition = OPEN_ALWAYS;
|
|
}
|
|
|
|
FileState* const state = new FileState;
|
|
state->nativePath = native;
|
|
state->handle = CreateFileW(native.c_str(), access,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
|
disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (state->handle == INVALID_HANDLE_VALUE) {
|
|
delete state;
|
|
return false;
|
|
}
|
|
nfsClient = state;
|
|
openRemote = true;
|
|
ro = mountReadOnly;
|
|
mode = openMode;
|
|
fullPath = path == nullptr ? "" : path;
|
|
if (!RefreshMetadata(*state, size, timeStamp)) {
|
|
delete state;
|
|
nfsClient = nullptr;
|
|
openRemote = false;
|
|
return false;
|
|
}
|
|
position = openMode == NFS_FS_APPEND ? size : 0;
|
|
return true;
|
|
}
|
|
|
|
int idFile_Nfs::ReadOfs(const std::int64_t offset, void* buffer,
|
|
const int len) {
|
|
FileState* const state = static_cast<FileState*>(nfsClient);
|
|
if (state == nullptr || buffer == nullptr || len <= 0 || offset < 0) return 0;
|
|
std::lock_guard<std::mutex> lock(state->mutex);
|
|
LARGE_INTEGER saved{}, target{};
|
|
target.QuadPart = offset;
|
|
if (!SetFilePointerEx(state->handle, {}, &saved, FILE_CURRENT)
|
|
|| !SetFilePointerEx(state->handle, target, nullptr, FILE_BEGIN)) return 0;
|
|
DWORD amount = 0;
|
|
const BOOL success = ReadFile(state->handle, buffer,
|
|
static_cast<DWORD>(len), &amount, nullptr);
|
|
SetFilePointerEx(state->handle, saved, nullptr, FILE_BEGIN);
|
|
return success ? static_cast<int>(amount) : 0;
|
|
}
|
|
|
|
int idFile_Nfs::WriteOfs(const std::int64_t offset, const void* buffer,
|
|
const int len) {
|
|
FileState* const state = static_cast<FileState*>(nfsClient);
|
|
if (state == nullptr || ro || buffer == nullptr || len <= 0 || offset < 0) return 0;
|
|
std::lock_guard<std::mutex> lock(state->mutex);
|
|
LARGE_INTEGER saved{}, target{};
|
|
target.QuadPart = offset;
|
|
if (!SetFilePointerEx(state->handle, {}, &saved, FILE_CURRENT)
|
|
|| !SetFilePointerEx(state->handle, target, nullptr, FILE_BEGIN)) return 0;
|
|
DWORD amount = 0;
|
|
const BOOL success = WriteFile(state->handle, buffer,
|
|
static_cast<DWORD>(len), &amount, nullptr);
|
|
SetFilePointerEx(state->handle, saved, nullptr, FILE_BEGIN);
|
|
if (success) {
|
|
size = std::max(size, static_cast<std::uint64_t>(offset) + amount);
|
|
RefreshMetadata(*state, size, timeStamp);
|
|
}
|
|
return success ? static_cast<int>(amount) : 0;
|
|
}
|
|
|
|
int idFile_Nfs::Read(void* buffer, const int len) {
|
|
const int amount = ReadOfs(static_cast<std::int64_t>(position), buffer, len);
|
|
position += static_cast<std::uint64_t>(amount);
|
|
return amount;
|
|
}
|
|
|
|
int idFile_Nfs::Write(const void* buffer, const int len) {
|
|
if (mode == NFS_FS_APPEND) position = size;
|
|
const int amount = WriteOfs(static_cast<std::int64_t>(position), buffer, len);
|
|
position += static_cast<std::uint64_t>(amount);
|
|
return amount;
|
|
}
|
|
|
|
int idFile_Nfs::Length() const {
|
|
return size > static_cast<std::uint64_t>(INT_MAX) ? INT_MAX
|
|
: static_cast<int>(size);
|
|
}
|
|
|
|
int idFile_Nfs::Tell() const {
|
|
return position > static_cast<std::uint64_t>(INT_MAX) ? INT_MAX
|
|
: static_cast<int>(position);
|
|
}
|
|
|
|
int idFile_Nfs::Seek(const long offset, const fsOrigin_t origin) {
|
|
return Seek64(offset, origin);
|
|
}
|
|
|
|
int idFile_Nfs::Seek64(const std::int64_t offset, const fsOrigin_t origin) {
|
|
std::int64_t base = 0;
|
|
if (origin == FS_SEEK_CUR) base = static_cast<std::int64_t>(position);
|
|
else if (origin == FS_SEEK_END) base = static_cast<std::int64_t>(size);
|
|
const std::int64_t next = base + offset;
|
|
if (next < 0) return -1;
|
|
position = static_cast<std::uint64_t>(next);
|
|
demandSeek = true;
|
|
return 0;
|
|
}
|
|
|
|
void idFile_Nfs::SetLength(const unsigned int len) { SetLength64(len); }
|
|
|
|
bool idFile_Nfs::SetLength64(const std::uint64_t len) {
|
|
FileState* const state = static_cast<FileState*>(nfsClient);
|
|
if (state == nullptr || ro || len > static_cast<std::uint64_t>(INT64_MAX)) return false;
|
|
std::lock_guard<std::mutex> lock(state->mutex);
|
|
LARGE_INTEGER saved{}, target{};
|
|
target.QuadPart = static_cast<LONGLONG>(len);
|
|
if (!SetFilePointerEx(state->handle, {}, &saved, FILE_CURRENT)
|
|
|| !SetFilePointerEx(state->handle, target, nullptr, FILE_BEGIN)
|
|
|| !SetEndOfFile(state->handle)) return false;
|
|
SetFilePointerEx(state->handle, saved, nullptr, FILE_BEGIN);
|
|
size = len;
|
|
if (position > size) position = size;
|
|
RefreshMetadata(*state, size, timeStamp);
|
|
return true;
|
|
}
|
|
|
|
void idFile_Nfs::Flush() {
|
|
FileState* const state = static_cast<FileState*>(nfsClient);
|
|
if (state != nullptr && !ro) FlushFileBuffers(state->handle);
|
|
}
|
|
|
|
void idFile_Nfs::ForceFlush() { Flush(); }
|
|
|
|
bool idFile_Nfs::CreateOsPath(const char* path) {
|
|
std::wstring native;
|
|
bool readOnly = false;
|
|
return Resolve(path, native, &readOnly) && !readOnly
|
|
&& CreateDirectories(native, false);
|
|
}
|
|
|
|
int idFile_Nfs::ListFiles(const char* path, const char* extension,
|
|
idList<idStr>& list) {
|
|
list.Clear();
|
|
std::wstring native;
|
|
if (!Resolve(path, native)) return 0;
|
|
std::wstring query = native;
|
|
if (!query.empty() && query.back() != L'\\') query.push_back(L'\\');
|
|
query.push_back(L'*');
|
|
WIN32_FIND_DATAW data{};
|
|
HANDLE find = FindFirstFileW(query.c_str(), &data);
|
|
if (find == INVALID_HANDLE_VALUE) return 0;
|
|
do {
|
|
const std::wstring name = data.cFileName;
|
|
if (name == L"." || name == L".." || !HasExtension(name, extension)) continue;
|
|
const int amount = WideCharToMultiByte(CP_UTF8, 0, name.c_str(), -1,
|
|
nullptr, 0, nullptr, nullptr);
|
|
if (amount <= 1) continue;
|
|
std::string utf8(static_cast<std::size_t>(amount), '\0');
|
|
WideCharToMultiByte(CP_UTF8, 0, name.c_str(), -1, &utf8[0], amount,
|
|
nullptr, nullptr);
|
|
utf8.resize(static_cast<std::size_t>(amount - 1));
|
|
list.Append(idStr(utf8.c_str()));
|
|
} while (FindNextFileW(find, &data));
|
|
FindClose(find);
|
|
return list.Num();
|
|
}
|
|
|
|
bool idFile_Nfs::RemoveFile(const char* path) {
|
|
std::wstring native;
|
|
bool readOnly = false;
|
|
if (!Resolve(path, native, &readOnly) || readOnly) return false;
|
|
const DWORD attributes = GetFileAttributesW(native.c_str());
|
|
if (attributes == INVALID_FILE_ATTRIBUTES) return false;
|
|
return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0
|
|
? RemoveDirectoryW(native.c_str()) != FALSE
|
|
: DeleteFileW(native.c_str()) != FALSE;
|
|
}
|
|
|
|
bool idFile_Nfs::RenameFile(const char* oldPath, const char* newPath) {
|
|
std::wstring oldNative, newNative;
|
|
bool oldReadOnly = false, newReadOnly = false;
|
|
std::string oldAlias, newAlias;
|
|
if (!Resolve(oldPath, oldNative, &oldReadOnly, &oldAlias)
|
|
|| !Resolve(newPath, newNative, &newReadOnly, &newAlias)
|
|
|| oldReadOnly || newReadOnly || oldAlias != newAlias) return false;
|
|
return MoveFileExW(oldNative.c_str(), newNative.c_str(),
|
|
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) != FALSE;
|
|
}
|