#include "vfsbindings.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "context.hpp" namespace MWLua { namespace { // Too many arguments may cause stack corruption and crash. constexpr std::size_t sMaximumReadArguments = 20; // Print a message if we read a large chunk of file to string. constexpr std::size_t sFileSizeWarningThreshold = 1024 * 1024; struct FileHandle { public: FileHandle(Files::IStreamPtr stream, std::string_view fileName) { mFilePtr = std::move(stream); mFileName = fileName; } Files::IStreamPtr mFilePtr; std::string mFileName; }; std::ios_base::seekdir getSeekDir(FileHandle& self, std::string_view whence) { if (whence == "cur") return std::ios_base::cur; if (whence == "set") return std::ios_base::beg; if (whence == "end") return std::ios_base::end; throw std::runtime_error( "Error when handling '" + self.mFileName + "': invalid seek direction: '" + std::string(whence) + "'."); } size_t getBytesLeftInStream(Files::IStreamPtr& file) { auto oldPos = file->tellg(); file->seekg(0, std::ios_base::end); auto newPos = file->tellg(); file->seekg(oldPos, std::ios_base::beg); return newPos - oldPos; } void printLargeDataMessage(FileHandle& file, size_t size) { if (!file.mFilePtr || !Settings::lua().mLuaDebug || size < sFileSizeWarningThreshold) return; Log(Debug::Verbose) << "Read a large data chunk (" << size << " bytes) from '" << file.mFileName << "'."; } sol::object readFile(sol::this_state lua, FileHandle& file) { std::ostringstream os; if (file.mFilePtr && file.mFilePtr->peek() != EOF) os << file.mFilePtr->rdbuf(); auto result = os.str(); printLargeDataMessage(file, result.size()); return sol::make_object(lua, std::move(result)); } sol::object readLineFromFile(sol::this_state lua, FileHandle& file) { std::string result; if (file.mFilePtr && std::getline(*file.mFilePtr, result)) { printLargeDataMessage(file, result.size()); return sol::make_object(lua, result); } return sol::nil; } sol::object readNumberFromFile(sol::this_state lua, Files::IStreamPtr& file) { double number = 0; if (file && *file >> number) return sol::make_object(lua, number); return sol::nil; } sol::object readCharactersFromFile(sol::this_state lua, FileHandle& file, size_t count) { if (count <= 0 && file.mFilePtr->peek() != EOF) return sol::make_object(lua, std::string()); auto bytesLeft = getBytesLeftInStream(file.mFilePtr); if (bytesLeft <= 0) return sol::nil; if (count > bytesLeft) count = bytesLeft; std::string result(count, '\0'); if (file.mFilePtr->read(&result[0], count)) { printLargeDataMessage(file, result.size()); return sol::make_object(lua, result); } return sol::nil; } void validateFile(const FileHandle& self) { if (self.mFilePtr) return; throw std::runtime_error("Error when handling '" + self.mFileName + "': attempt to use a closed file."); } sol::variadic_results seek( sol::this_state lua, FileHandle& self, std::ios_base::seekdir dir, std::streamoff off) { sol::variadic_results values; try { self.mFilePtr->seekg(off, dir); if (self.mFilePtr->fail() || self.mFilePtr->bad()) { auto msg = "Failed to seek in file '" + self.mFileName + "'"; values.push_back(sol::nil); values.push_back(sol::make_object(lua, msg)); } else { // tellg returns std::streampos which is not required to be a numeric type. It is required to be // convertible to std::streamoff which is a number values.push_back(sol::make_object(lua, self.mFilePtr->tellg())); } } catch (std::exception& e) { auto msg = "Failed to seek in file '" + self.mFileName + "': " + std::string(e.what()); values.push_back(sol::nil); values.push_back(sol::make_object(lua, msg)); } return values; } } sol::table initVFSPackage(const Context& context) { sol::table api(context.mLua->unsafeState(), sol::create); auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); sol::usertype handle = context.sol().new_usertype("FileHandle"); handle["fileName"] = sol::readonly_property([](const FileHandle& self) -> std::string_view { return self.mFileName; }); handle[sol::meta_function::to_string] = [](const FileHandle& self) { return "FileHandle{'" + self.mFileName + "'" + (!self.mFilePtr ? ", closed" : "") + "}"; }; handle["seek"] = sol::overload( [](sol::this_state lua, FileHandle& self, std::string_view whence, sol::optional offset) { validateFile(self); auto off = static_cast(offset.value_or(0)); auto dir = getSeekDir(self, whence); return seek(lua, self, dir, off); }, [](sol::this_state lua, FileHandle& self, sol::optional offset) { validateFile(self); auto off = static_cast(offset.value_or(0)); return seek(lua, self, std::ios_base::cur, off); }); handle["lines"] = [](sol::this_state lua, FileHandle& self) { return sol::as_function([&lua, &self]() mutable { validateFile(self); return readLineFromFile(lua, self); }); }; api["lines"] = [vfs](sol::this_state lua, std::string_view fileName) { auto normalizedName = VFS::Path::normalizeFilename(fileName); return sol::as_function( [lua, file = FileHandle(vfs->getNormalized(normalizedName), normalizedName)]() mutable { validateFile(file); auto result = readLineFromFile(lua, file); if (result == sol::nil) file.mFilePtr.reset(); return result; }); }; handle["close"] = [](sol::this_state lua, FileHandle& self) { sol::variadic_results values; try { self.mFilePtr.reset(); if (self.mFilePtr) { auto msg = "Can not close file '" + self.mFileName + "': file handle is still opened."; values.push_back(sol::nil); values.push_back(sol::make_object(lua, msg)); } else values.push_back(sol::make_object(lua, true)); } catch (std::exception& e) { auto msg = "Can not close file '" + self.mFileName + "': " + std::string(e.what()); values.push_back(sol::nil); values.push_back(sol::make_object(lua, msg)); } return values; }; handle["read"] = [](sol::this_state lua, FileHandle& self, const sol::variadic_args args) { validateFile(self); if (args.size() > sMaximumReadArguments) throw std::runtime_error( "Error when handling '" + self.mFileName + "': too many arguments for 'read'."); sol::variadic_results values; // If there are no arguments, read a string if (args.size() == 0) { values.push_back(readLineFromFile(lua, self)); return values; } bool success = true; size_t i = 0; for (i = 0; i < args.size() && success; i++) { if (args[i].is()) { auto format = args[i].as(); if (format == "*a" || format == "*all") { values.push_back(readFile(lua, self)); continue; } if (format == "*n" || format == "*number") { auto result = readNumberFromFile(lua, self.mFilePtr); values.push_back(result); if (result == sol::nil) success = false; continue; } if (format == "*l" || format == "*line") { auto result = readLineFromFile(lua, self); values.push_back(result); if (result == sol::nil) success = false; continue; } throw std::runtime_error("Error when handling '" + self.mFileName + "': bad argument #" + std::to_string(i + 1) + " to 'read' (invalid format)"); } else if (args[i].is()) { int number = args[i].as(); auto result = readCharactersFromFile(lua, self, number); values.push_back(result); if (result == sol::nil) success = false; } } // We should return nil if we just reached the end of stream if (!success && self.mFilePtr->eof()) return values; if (!success && (self.mFilePtr->fail() || self.mFilePtr->bad())) { auto msg = "Error when handling '" + self.mFileName + "': can not read data for argument #" + std::to_string(i); values.push_back(sol::make_object(lua, msg)); } return values; }; api["open"] = [vfs](sol::this_state lua, std::string_view fileName) { sol::variadic_results values; try { auto normalizedName = VFS::Path::normalizeFilename(fileName); auto handle = FileHandle(vfs->getNormalized(normalizedName), normalizedName); values.push_back(sol::make_object(lua, std::move(handle))); } catch (std::exception& e) { auto msg = "Can not open file: " + std::string(e.what()); values.push_back(sol::nil); values.push_back(sol::make_object(lua, msg)); } return values; }; api["type"] = sol::overload( [](const FileHandle& handle) -> std::string { if (handle.mFilePtr) return "file"; return "closed file"; }, [](const sol::object&) -> sol::object { return sol::nil; }); api["fileExists"] = [vfs](std::string_view fileName) -> bool { return vfs->exists(VFS::Path::Normalized(fileName)); }; api["pathsWithPrefix"] = [vfs](std::string_view prefix) { auto iterator = vfs->getRecursiveDirectoryIterator(prefix); return sol::as_function([iterator, current = iterator.begin()]() mutable -> sol::optional { if (current != iterator.end()) { const std::string& result = *current; ++current; return result; } return sol::nullopt; }); }; return LuaUtil::makeReadOnly(api); } }