Thanks to physfs and nativefs.lua
nativefs still introduces some inconsistencies with love.filesystem with relative paths:
love.fs.read: reads from save dir if it exists, falls back to source dir if not
nativefs.read: reads from save dir if it exists, falls back to source dir if not ✓
love.fs.write: always writes to save dir
nativefs.write: always writes to source dir (since no restrictions)
love.fs.newFile followed by file:open('r'): reads from save dir if it exists, source dir if not
nativefs.newFile followed by file:open('r'): always reads from working dir
love.fs.newFile followed by file:open('w'): always writes to save dir
nativefs.newFile followed by file:open('w'): always writes to working dir
So avoid using relative paths with App primitives.
ED4Z6ORCADLWJPSZNKQVUF63NBKLQVE7UFMBTQAQA5O47X4NLFIAC FPY4LO2WZNXUYCZ6MOQBSPGCC3M3PE7YX2TJIFRNEQLJ4ACWUPVAC KKMFQDR43ZWVCDRHQLWWX3FCWCFA3ZSXYOBRJNPHUQZR2XPKWULAC N2NUGNN4E37TNLC6JZE4H4HKJZPNVBV4VYTLXW57AP5BOU4ZK7LQC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC AD34IX2ZSGYGU3LGY2IZOZNKD4HRQOYJVG5UWMWLXJZJSM62FFOAC 2CK5QI7WA7M4IVSACFGOJYAIDKRUTZVMMPSFWEJTUNMWTN7AX4NAC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC CUFW4EJL75OAA5BS5EXGTM5RMRNJOBBPAXUJADGZ3VLP2ZMKFOTAC ORRSP7FVCHI2TF5GXBRGQYYJAA3JFYXZBM3T663BKSBV22FCZVCAC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC R3KXFRZNL4CAT5OSKIIGWR3CHL2YJ5S4TKQDIPEHIJ2HW2WS46BQC HKV72RZVJEOF5GCHCRKEBGC3FQN7AYETY7LKEJUXVIQAB4QPEPYQC * `love.filesystem.getDirectoryItems(dir)` -- returns an unsorted array of thefiles and directories available under `dir`. `dir` must be relative to[LÖVE's save directory](https://love2d.org/wiki/love.filesystem.getSaveDirectory).There is no easy, portable way in Lua/LÖVE to list directories outside thesave dir.
* `App.files(dir)` -- returns an unsorted array of the files and directoriesavailable under `dir`.
--[[Copyright 2020 megagrump@pm.mePermission is hereby granted, free of charge, to any person obtaining a copy ofthis software and associated documentation files (the "Software"), to deal inthe Software without restriction, including without limitation the rights touse, copy, modify, merge, publish, distribute, sublicense, and/or sell copiesof the Software, and to permit persons to whom the Software is furnished to doso, subject to the following conditions:The above copyright notice and this permission notice shall be included in allcopies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, 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 THESOFTWARE.]]--local ffi, bit = require('ffi'), require('bit')local C = ffi.Clocal File = {getBuffer = function(self) return self._bufferMode, self._bufferSize end,getFilename = function(self) return self._name end,getMode = function(self) return self._mode end,isOpen = function(self) return self._mode ~= 'c' and self._handle ~= nil end,}local fopen, getcwd, chdir, unlink, mkdir, rmdirlocal BUFFERMODE, MODEMAPlocal ByteArray = ffi.typeof('unsigned char[?]')local function _ptr(p) return p ~= nil and p or nil end -- NULL pointer to nilfunction File:open(mode)if self._mode ~= 'c' then return false, "File " .. self._name .. " is already open" endif not MODEMAP[mode] then return false, "Invalid open mode for " .. self._name .. ": " .. mode endlocal handle = _ptr(fopen(self._name, MODEMAP[mode]))if not handle then return false, "Could not open " .. self._name .. " in mode " .. mode endself._handle, self._mode = ffi.gc(handle, C.fclose), modeself:setBuffer(self._bufferMode, self._bufferSize)return trueendfunction File:close()if self._mode == 'c' then return false, "File is not open" endC.fclose(ffi.gc(self._handle, nil))self._handle, self._mode = nil, 'c'return trueendfunction File:setBuffer(mode, size)local bufferMode = BUFFERMODE[mode]if not bufferMode thenreturn false, "Invalid buffer mode " .. mode .. " (expected 'none', 'full', or 'line')"endif mode == 'none' thensize = math.max(0, size or 0)elsesize = math.max(2, size or 2) -- Windows requires buffer to be at least 2 bytesendlocal success = self._mode == 'c' or C.setvbuf(self._handle, nil, bufferMode, size) == 0if not success thenself._bufferMode, self._bufferSize = 'none', 0return false, "Could not set buffer mode"endself._bufferMode, self._bufferSize = mode, sizereturn trueendfunction File:getSize()-- NOTE: The correct way to do this would be a stat() call, which requires a-- lot more (system-specific) code. This is a shortcut that requires the file-- to be readable.local mustOpen = not self:isOpen()if mustOpen and not self:open('r') then return 0 endlocal pos = mustOpen and 0 or self:tell()C.fseek(self._handle, 0, 2)local size = self:tell()if mustOpen thenself:close()elseself:seek(pos)endreturn sizeendfunction File:read(containerOrBytes, bytes)if self._mode ~= 'r' then return nil, 0 endlocal container = bytes ~= nil and containerOrBytes or 'string'if container ~= 'string' and container ~= 'data' thenerror("Invalid container type: " .. container)endbytes = not bytes and containerOrBytes or 'all'bytes = bytes == 'all' and self:getSize() - self:tell() or math.min(self:getSize() - self:tell(), bytes)if bytes <= 0 thenlocal data = container == 'string' and '' or love.data.newFileData('', self._name)return data, 0endlocal data = love.data.newByteData(bytes)local r = tonumber(C.fread(data:getFFIPointer(), 1, bytes, self._handle))local str = data:getString()data:release()data = container == 'data' and love.filesystem.newFileData(str, self._name) or strreturn data, rendlocal function lines(file, autoclose)local BUFFERSIZE = 4096local buffer, bufferPos = ByteArray(BUFFERSIZE), 0local bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))local offset = file:tell()return function()file:seek(offset)local line = {}while bytesRead > 0 dofor i = bufferPos, bytesRead - 1 doif buffer[i] == 10 then -- end of linebufferPos = i + 1return table.concat(line)endif buffer[i] ~= 13 then -- ignore CRtable.insert(line, string.char(buffer[i]))endendbytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))offset, bufferPos = offset + bytesRead, 0endif not line[1] thenif autoclose then file:close() endreturn nilendreturn table.concat(line)endendfunction File:lines()if self._mode ~= 'r' then error("File is not opened for reading") endreturn lines(self)endfunction File:write(data, size)if self._mode ~= 'w' and self._mode ~= 'a' thenreturn false, "File " .. self._name .. " not opened for writing"endlocal toWrite, writeSizeif type(data) == 'string' thenwriteSize = (size == nil or size == 'all') and #data or sizetoWrite = dataelsewriteSize = (size == nil or size == 'all') and data:getSize() or sizetoWrite = data:getFFIPointer()endif tonumber(C.fwrite(toWrite, 1, writeSize, self._handle)) ~= writeSize thenreturn false, "Could not write data"endreturn trueendfunction File:seek(pos)return self._handle and C.fseek(self._handle, pos, 0) == 0endfunction File:tell()if not self._handle then return nil, "Invalid position" endreturn tonumber(C.ftell(self._handle))endfunction File:flush()if self._mode ~= 'w' and self._mode ~= 'a' thenreturn nil, "File is not opened for writing"endreturn C.fflush(self._handle) == 0endfunction File:isEOF()return not self:isOpen() or C.feof(self._handle) ~= 0 or self:tell() == self:getSize()endfunction File:release()if self._mode ~= 'c' then self:close() endself._handle = nilendfunction File:type() return 'File' endfunction File:typeOf(t) return t == 'File' endFile.__index = File-----------------------------------------------------------------------------local nativefs = {}local loveC = ffi.os == 'Windows' and ffi.load('love') or Cfunction nativefs.newFile(name)if type(name) ~= 'string' thenerror("bad argument #1 to 'newFile' (string expected, got " .. type(name) .. ")")endreturn setmetatable({_name = name,_mode = 'c',_handle = nil,_bufferSize = 0,_bufferMode = 'none'}, File)endfunction nativefs.newFileData(filepath)local f = nativefs.newFile(filepath)local ok, err = f:open('r')if not ok then return nil, err endlocal data, err = f:read('data', 'all')f:close()return data, errendfunction nativefs.mount(archive, mountPoint, appendToPath)return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0endfunction nativefs.unmount(archive)return loveC.PHYSFS_unmount(archive) ~= 0endfunction nativefs.read(containerOrName, nameOrSize, sizeOrNil)local container, name, sizeif sizeOrNil thencontainer, name, size = containerOrName, nameOrSize, sizeOrNilelseif not nameOrSize thencontainer, name, size = 'string', containerOrName, 'all'elseif type(nameOrSize) == 'number' or nameOrSize == 'all' thencontainer, name, size = 'string', containerOrName, nameOrSizeelsecontainer, name, size = containerOrName, nameOrSize, 'all'endendlocal file = nativefs.newFile(name)local ok, err = file:open('r')if not ok then return nil, err endlocal data, size = file:read(container, size)file:close()return data, sizeendlocal function writeFile(mode, name, data, size)local file = nativefs.newFile(name)local ok, err = file:open(mode)if not ok then return nil, err endok, err = file:write(data, size or 'all')file:close()return ok, errendfunction nativefs.write(name, data, size)return writeFile('w', name, data, size)endfunction nativefs.append(name, data, size)return writeFile('a', name, data, size)endfunction nativefs.lines(name)local f = nativefs.newFile(name)local ok, err = f:open('r')if not ok then return nil, err endreturn lines(f, true)endfunction nativefs.load(name)local chunk, err = nativefs.read(name)if not chunk then return nil, err endreturn loadstring(chunk, name)endfunction nativefs.getWorkingDirectory()return getcwd()endfunction nativefs.setWorkingDirectory(path)if not chdir(path) then return false, "Could not set working directory" endreturn trueendfunction nativefs.getDriveList()if ffi.os ~= 'Windows' then return { '/' } endlocal drives, bits = {}, C.GetLogicalDrives()for i = 0, 25 doif bit.band(bits, 2 ^ i) > 0 thentable.insert(drives, string.char(65 + i) .. ':/')endendreturn drivesendfunction nativefs.createDirectory(path)local current = path:sub(1, 1) == '/' and '/' or ''for dir in path:gmatch('[^/\\]+') docurrent = current .. dir .. '/'local info = nativefs.getInfo(current, 'directory')if not info and not mkdir(current) then return false, "Could not create directory " .. current endendreturn trueendfunction nativefs.remove(name)local info = nativefs.getInfo(name)if not info then return false, "Could not remove " .. name endif info.type == 'directory' thenif not rmdir(name) then return false, "Could not remove directory " .. name endreturn trueendif not unlink(name) then return false, "Could not remove file " .. name endreturn trueendlocal function withTempMount(dir, fn, ...)local mountPoint = _ptr(loveC.PHYSFS_getMountPoint(dir))if mountPoint then return fn(ffi.string(mountPoint), ...) endif not nativefs.mount(dir, '__nativefs__temp__') then return false, "Could not mount " .. dir endlocal a, b = fn('__nativefs__temp__', ...)nativefs.unmount(dir)return a, bendfunction nativefs.getDirectoryItems(dir)if type(dir) ~= "string" thenerror("bad argument #1 to 'getDirectoryItems' (string expected, got " .. type(dir) .. ")")endlocal result, err = withTempMount(dir, love.filesystem.getDirectoryItems)return result or {}endlocal function getDirectoryItemsInfo(path, filtertype)local items = {}local files = love.filesystem.getDirectoryItems(path)for i = 1, #files dolocal filepath = string.format('%s/%s', path, files[i])local info = love.filesystem.getInfo(filepath, filtertype)if info theninfo.name = files[i]table.insert(items, info)endendreturn itemsendfunction nativefs.getDirectoryItemsInfo(path, filtertype)if type(path) ~= "string" thenerror("bad argument #1 to 'getDirectoryItemsInfo' (string expected, got " .. type(path) .. ")")endlocal result, err = withTempMount(path, getDirectoryItemsInfo, filtertype)return result or {}endlocal function getInfo(path, file, filtertype)local filepath = string.format('%s/%s', path, file)return love.filesystem.getInfo(filepath, filtertype)endlocal function leaf(p)p = p:gsub('\\', '/')local last, a = p, 1while a doa = p:find('/', a + 1)if a thenlast = p:sub(a + 1)endendreturn lastendfunction nativefs.getInfo(path, filtertype)if type(path) ~= 'string' thenerror("bad argument #1 to 'getInfo' (string expected, got " .. type(path) .. ")")endlocal dir = path:match("(.*[\\/]).*$") or './'local file = leaf(path)local result, err = withTempMount(dir, getInfo, file, filtertype)return result or nilend-----------------------------------------------------------------------------MODEMAP = { r = 'rb', w = 'wb', a = 'ab' }local MAX_PATH = 4096ffi.cdef([[int PHYSFS_mount(const char* dir, const char* mountPoint, int appendToPath);int PHYSFS_unmount(const char* dir);const char* PHYSFS_getMountPoint(const char* dir);typedef struct FILE FILE;FILE* fopen(const char* path, const char* mode);size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);int fclose(FILE* stream);int fflush(FILE* stream);size_t fseek(FILE* stream, size_t offset, int whence);size_t ftell(FILE* stream);int setvbuf(FILE* stream, char* buffer, int mode, size_t size);int feof(FILE* stream);]])if ffi.os == 'Windows' thenffi.cdef([[int MultiByteToWideChar(unsigned int cp, uint32_t flags, const char* mb, int cmb, const wchar_t* wc, int cwc);int WideCharToMultiByte(unsigned int cp, uint32_t flags, const wchar_t* wc, int cwc, const char* mb,int cmb, const char* def, int* used);int GetLogicalDrives(void);int CreateDirectoryW(const wchar_t* path, void*);int _wchdir(const wchar_t* path);wchar_t* _wgetcwd(wchar_t* buffer, int maxlen);FILE* _wfopen(const wchar_t* path, const wchar_t* mode);int _wunlink(const wchar_t* path);int _wrmdir(const wchar_t* path);]])BUFFERMODE = { full = 0, line = 64, none = 4 }local function towidestring(str)local size = C.MultiByteToWideChar(65001, 0, str, #str, nil, 0)local buf = ffi.new('wchar_t[?]', size + 1)C.MultiByteToWideChar(65001, 0, str, #str, buf, size)return bufendlocal function toutf8string(wstr)local size = C.WideCharToMultiByte(65001, 0, wstr, -1, nil, 0, nil, nil)local buf = ffi.new('char[?]', size + 1)C.WideCharToMultiByte(65001, 0, wstr, -1, buf, size, nil, nil)return ffi.string(buf)endlocal nameBuffer = ffi.new('wchar_t[?]', MAX_PATH + 1)fopen = function(path, mode) return C._wfopen(towidestring(path), towidestring(mode)) endgetcwd = function() return toutf8string(C._wgetcwd(nameBuffer, MAX_PATH)) endchdir = function(path) return C._wchdir(towidestring(path)) == 0 endunlink = function(path) return C._wunlink(towidestring(path)) == 0 endmkdir = function(path) return C.CreateDirectoryW(towidestring(path), nil) ~= 0 endrmdir = function(path) return C._wrmdir(towidestring(path)) == 0 endelseBUFFERMODE = { full = 0, line = 1, none = 2 }ffi.cdef([[char* getcwd(char *buffer, int maxlen);int chdir(const char* path);int unlink(const char* path);int mkdir(const char* path, int mode);int rmdir(const char* path);]])local nameBuffer = ByteArray(MAX_PATH)fopen = C.fopenunlink = function(path) return ffi.C.unlink(path) == 0 endchdir = function(path) return ffi.C.chdir(path) == 0 endmkdir = function(path) return ffi.C.mkdir(path, 0x1ed) == 0 endrmdir = function(path) return ffi.C.rmdir(path) == 0 endgetcwd = function()local cwd = _ptr(C.getcwd(nameBuffer, MAX_PATH))return cwd and ffi.string(cwd) or nilendendreturn nativefs
if Current_app == nil or Current_app == 'run' thenreturn {write = function(self, ...)local args = {...}for i,s in ipairs(args) doApp.filesystem[filename] = App.filesystem[filename]..sendend,close = function(self)end,}elseif Current_app == 'source' thenreturn {write = function(self, s)App.filesystem[filename] = App.filesystem[filename]..send,close = function(self)end,}end
return {write = function(self, s)App.filesystem[filename] = App.filesystem[filename]..send,close = function(self)end,}
if Current_app == nil or Current_app == 'run' thenApp.open_for_reading = function(filename) return io.open(filename, 'r') endApp.open_for_writing = function(filename) return io.open(filename, 'w') endelseif Current_app == 'source' then-- HACK: source editor requires a couple of different foundational definitionsApp.open_for_reading =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('r')if ok thenreturn resultelsereturn ok, errend
App.open_for_reading =function(filename)local result = nativefs.newFile(filename)local ok, err = result:open('r')if ok thenreturn resultelsereturn ok, err
App.open_for_writing =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('w')if ok thenreturn resultelsereturn ok, errend
endApp.open_for_writing =function(filename)local result = nativefs.newFile(filename)local ok, err = result:open('w')if ok thenreturn resultelsereturn ok, err