cleaner API for file-system access
[?]
Aug 31, 2023, 2:04 AM
ED4Z6ORCADLWJPSZNKQVUF63NBKLQVE7UFMBTQAQA5O47X4NLFIACDependencies
- [2]
FPY4LO2Wmake a few names consistent with snake_case - [3]
R3KXFRZNget rid of to_text - [4]
HKV72RZVbugfix: save modified files in save directory - [5]
OI4FPFINsupport drawings in the source editor - [6]
7IDHIAYIrename modifier_down to key_down - [7]
ISOFHXB2App.width can no longer take a Text - [8]
JFFUF5ALoverride mouse state lookups in tests - [9]
7CLGG7J2test: autosave after any shape - [10]
AVTNUQYRbasic test-enabled framework - [11]
AD34IX2Zcouple more tests - [12]
N2NUGNN4include a brief reference enabling many useful apps - [13]
JF5L2BBStest harness now supports copy/paste - [14]
KKMFQDR4editing source code from within the app - [15]
PX7DDEMOautosave slightly less aggressively - [16]
CUFW4EJLreorganize app.lua and its comments - [*]
R5QXEHUIsomebody stop me - [*]
3QNOKBFMbeginnings of a test harness - [*]
2CK5QI7Wmake love event names consistent - [*]
ORRSP7FVdeduce test names on failures
Change contents
- edit in source_file.lua at line 13
-- the source editor supports only files in the save dir, not even subdirectories - replacement in source_file.lua at line 15
local infile = App.open_for_reading(State.filename)local infile = App.open_for_reading(App.save_dir..State.filename) - replacement in source_file.lua at line 41
local outfile = App.open_for_writing(State.filename)local outfile = App.open_for_writing(App.save_dir..State.filename) - replacement in reference.md at line 361
* `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`. - edit in reference.md at line 367
`filename` must be relative to [LÖVE's save directory](https://love2d.org/wiki/love.filesystem.getSaveDirectory). - file addition: nativefs.lua[18.2]
--[[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 - edit in app.lua at line 100
App.source_dir = ''App.current_dir = ''App.save_dir = '' - replacement in app.lua at line 263
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,}endreturn {write = function(self, s)App.filesystem[filename] = App.filesystem[filename]..send,close = function(self)end,} - edit in app.lua at line 349[21.61193][3.7569]
nativefs = require 'nativefs' - replacement in app.lua at line 386
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, errendApp.open_for_reading =function(filename)local result = nativefs.newFile(filename)local ok, err = result:open('r')if ok thenreturn resultelsereturn ok, err - replacement in app.lua at line 395
App.open_for_writing =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('w')if ok thenreturn resultelsereturn ok, errendendApp.open_for_writing =function(filename)local result = nativefs.newFile(filename)local ok, err = result:open('w')if ok thenreturn resultelsereturn ok, err - replacement in app.lua at line 405
endendApp.files = nativefs.getDirectoryItemsApp.source_dir = love.filesystem.getSource()..'/'App.current_dir = nativefs.getWorkingDirectory()..'/'App.save_dir = love.filesystem.getSaveDirectory()..'/'