Template repo for tiny cross-platform apps that can be modified on phone, tablet or computer.
at main 496 lines 15 kB view raw
1--[[ 2Copyright 2020 megagrump@pm.me 3 4Permission is hereby granted, free of charge, to any person obtaining a copy of 5this software and associated documentation files (the "Software"), to deal in 6the Software without restriction, including without limitation the rights to 7use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8of the Software, and to permit persons to whom the Software is furnished to do 9so, subject to the following conditions: 10 11The above copyright notice and this permission notice shall be included in all 12copies or substantial portions of the Software. 13 14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20SOFTWARE. 21]]-- 22 23local ffi, bit = require('ffi'), require('bit') 24local C = ffi.C 25 26local File = { 27 getBuffer = function(self) return self._bufferMode, self._bufferSize end, 28 getFilename = function(self) return self._name end, 29 getMode = function(self) return self._mode end, 30 isOpen = function(self) return self._mode ~= 'c' and self._handle ~= nil end, 31} 32 33local fopen, getcwd, chdir, unlink, mkdir, rmdir 34local BUFFERMODE, MODEMAP 35local ByteArray = ffi.typeof('unsigned char[?]') 36local function _ptr(p) return p ~= nil and p or nil end -- NULL pointer to nil 37 38function File:open(mode) 39 if self._mode ~= 'c' then return false, "File " .. self._name .. " is already open" end 40 if not MODEMAP[mode] then return false, "Invalid open mode for " .. self._name .. ": " .. mode end 41 42 local handle = _ptr(fopen(self._name, MODEMAP[mode])) 43 if not handle then return false, "Could not open " .. self._name .. " in mode " .. mode end 44 45 self._handle, self._mode = ffi.gc(handle, C.fclose), mode 46 self:setBuffer(self._bufferMode, self._bufferSize) 47 48 return true 49end 50 51function File:close() 52 if self._mode == 'c' then return false, "File is not open" end 53 C.fclose(ffi.gc(self._handle, nil)) 54 self._handle, self._mode = nil, 'c' 55 return true 56end 57 58function File:setBuffer(mode, size) 59 local bufferMode = BUFFERMODE[mode] 60 if not bufferMode then 61 return false, "Invalid buffer mode " .. mode .. " (expected 'none', 'full', or 'line')" 62 end 63 64 if mode == 'none' then 65 size = math.max(0, size or 0) 66 else 67 size = math.max(2, size or 2) -- Windows requires buffer to be at least 2 bytes 68 end 69 70 local success = self._mode == 'c' or C.setvbuf(self._handle, nil, bufferMode, size) == 0 71 if not success then 72 self._bufferMode, self._bufferSize = 'none', 0 73 return false, "Could not set buffer mode" 74 end 75 76 self._bufferMode, self._bufferSize = mode, size 77 return true 78end 79 80function File:getSize() 81 -- NOTE: The correct way to do this would be a stat() call, which requires a 82 -- lot more (system-specific) code. This is a shortcut that requires the file 83 -- to be readable. 84 local mustOpen = not self:isOpen() 85 if mustOpen and not self:open('r') then return 0 end 86 87 local pos = mustOpen and 0 or self:tell() 88 C.fseek(self._handle, 0, 2) 89 local size = self:tell() 90 if mustOpen then 91 self:close() 92 else 93 self:seek(pos) 94 end 95 return size 96end 97 98function File:read(containerOrBytes, bytes) 99 if self._mode ~= 'r' then return nil, 0 end 100 101 local container = bytes ~= nil and containerOrBytes or 'string' 102 if container ~= 'string' and container ~= 'data' then 103 error("Invalid container type: " .. container) 104 end 105 106 bytes = not bytes and containerOrBytes or 'all' 107 bytes = bytes == 'all' and self:getSize() - self:tell() or math.min(self:getSize() - self:tell(), bytes) 108 109 if bytes <= 0 then 110 local data = container == 'string' and '' or love.data.newFileData('', self._name) 111 return data, 0 112 end 113 114 local data = love.data.newByteData(bytes) 115 local r = tonumber(C.fread(data:getFFIPointer(), 1, bytes, self._handle)) 116 117 local str = data:getString() 118 data:release() 119 data = container == 'data' and love.filesystem.newFileData(str, self._name) or str 120 return data, r 121end 122 123local function lines(file, autoclose) 124 local BUFFERSIZE = 4096 125 local buffer, bufferPos = ByteArray(BUFFERSIZE), 0 126 local bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle)) 127 128 local offset = file:tell() 129 return function() 130 file:seek(offset) 131 132 local line = {} 133 while bytesRead > 0 do 134 for i = bufferPos, bytesRead - 1 do 135 if buffer[i] == 10 then -- end of line 136 bufferPos = i + 1 137 return table.concat(line) 138 end 139 140 if buffer[i] ~= 13 then -- ignore CR 141 table.insert(line, string.char(buffer[i])) 142 end 143 end 144 145 bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle)) 146 offset, bufferPos = offset + bytesRead, 0 147 end 148 149 if not line[1] then 150 if autoclose then file:close() end 151 return nil 152 end 153 return table.concat(line) 154 end 155end 156 157function File:lines() 158 if self._mode ~= 'r' then error("File is not opened for reading") end 159 return lines(self) 160end 161 162function File:write(data, size) 163 if self._mode ~= 'w' and self._mode ~= 'a' then 164 return false, "File " .. self._name .. " not opened for writing" 165 end 166 167 local toWrite, writeSize 168 if type(data) == 'string' then 169 writeSize = (size == nil or size == 'all') and #data or size 170 toWrite = data 171 else 172 writeSize = (size == nil or size == 'all') and data:getSize() or size 173 toWrite = data:getFFIPointer() 174 end 175 176 if tonumber(C.fwrite(toWrite, 1, writeSize, self._handle)) ~= writeSize then 177 return false, "Could not write data" 178 end 179 return true 180end 181 182function File:seek(pos) 183 return self._handle and C.fseek(self._handle, pos, 0) == 0 184end 185 186function File:tell() 187 if not self._handle then return nil, "Invalid position" end 188 return tonumber(C.ftell(self._handle)) 189end 190 191function File:flush() 192 if self._mode ~= 'w' and self._mode ~= 'a' then 193 return nil, "File is not opened for writing" 194 end 195 return C.fflush(self._handle) == 0 196end 197 198function File:isEOF() 199 return not self:isOpen() or C.feof(self._handle) ~= 0 or self:tell() == self:getSize() 200end 201 202function File:release() 203 if self._mode ~= 'c' then self:close() end 204 self._handle = nil 205end 206 207function File:type() return 'File' end 208 209function File:typeOf(t) return t == 'File' end 210 211File.__index = File 212 213----------------------------------------------------------------------------- 214 215local nativefs = {} 216local loveC = ffi.os == 'Windows' and ffi.load('love') or C 217 218function nativefs.newFile(name) 219 if type(name) ~= 'string' then 220 error("bad argument #1 to 'newFile' (string expected, got " .. type(name) .. ")") 221 end 222 return setmetatable({ 223 _name = name, 224 _mode = 'c', 225 _handle = nil, 226 _bufferSize = 0, 227 _bufferMode = 'none' 228 }, File) 229end 230 231function nativefs.newFileData(filepath) 232 local f = nativefs.newFile(filepath) 233 local ok, err = f:open('r') 234 if not ok then return nil, err end 235 236 local data, err = f:read('data', 'all') 237 f:close() 238 return data, err 239end 240 241function nativefs.mount(archive, mountPoint, appendToPath) 242 return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0 243end 244 245function nativefs.unmount(archive) 246 return loveC.PHYSFS_unmount(archive) ~= 0 247end 248 249function nativefs.read(containerOrName, nameOrSize, sizeOrNil) 250 local container, name, size 251 if sizeOrNil then 252 container, name, size = containerOrName, nameOrSize, sizeOrNil 253 elseif not nameOrSize then 254 container, name, size = 'string', containerOrName, 'all' 255 else 256 if type(nameOrSize) == 'number' or nameOrSize == 'all' then 257 container, name, size = 'string', containerOrName, nameOrSize 258 else 259 container, name, size = containerOrName, nameOrSize, 'all' 260 end 261 end 262 263 local file = nativefs.newFile(name) 264 local ok, err = file:open('r') 265 if not ok then return nil, err end 266 267 local data, size = file:read(container, size) 268 file:close() 269 return data, size 270end 271 272local function writeFile(mode, name, data, size) 273 local file = nativefs.newFile(name) 274 local ok, err = file:open(mode) 275 if not ok then return nil, err end 276 277 ok, err = file:write(data, size or 'all') 278 file:close() 279 return ok, err 280end 281 282function nativefs.write(name, data, size) 283 return writeFile('w', name, data, size) 284end 285 286function nativefs.append(name, data, size) 287 return writeFile('a', name, data, size) 288end 289 290function nativefs.lines(name) 291 local f = nativefs.newFile(name) 292 local ok, err = f:open('r') 293 if not ok then return nil, err end 294 return lines(f, true) 295end 296 297function nativefs.load(name) 298 local chunk, err = nativefs.read(name) 299 if not chunk then return nil, err end 300 return loadstring(chunk, name) 301end 302 303function nativefs.getWorkingDirectory() 304 return getcwd() 305end 306 307function nativefs.setWorkingDirectory(path) 308 if not chdir(path) then return false, "Could not set working directory" end 309 return true 310end 311 312function nativefs.getDriveList() 313 if ffi.os ~= 'Windows' then return { '/' } end 314 local drives, bits = {}, C.GetLogicalDrives() 315 for i = 0, 25 do 316 if bit.band(bits, 2 ^ i) > 0 then 317 table.insert(drives, string.char(65 + i) .. ':/') 318 end 319 end 320 return drives 321end 322 323function nativefs.createDirectory(path) 324 local current = path:sub(1, 1) == '/' and '/' or '' 325 for dir in path:gmatch('[^/\\]+') do 326 current = current .. dir .. '/' 327 local info = nativefs.getInfo(current, 'directory') 328 if not info and not mkdir(current) then return false, "Could not create directory " .. current end 329 end 330 return true 331end 332 333function nativefs.remove(name) 334 local info = nativefs.getInfo(name) 335 if not info then return false, "Could not remove " .. name end 336 if info.type == 'directory' then 337 if not rmdir(name) then return false, "Could not remove directory " .. name end 338 return true 339 end 340 if not unlink(name) then return false, "Could not remove file " .. name end 341 return true 342end 343 344local function withTempMount(dir, fn, ...) 345 local mountPoint = _ptr(loveC.PHYSFS_getMountPoint(dir)) 346 if mountPoint then return fn(ffi.string(mountPoint), ...) end 347 if not nativefs.mount(dir, '__nativefs__temp__') then return false, "Could not mount " .. dir end 348 local a, b = fn('__nativefs__temp__', ...) 349 nativefs.unmount(dir) 350 return a, b 351end 352 353function nativefs.getDirectoryItems(dir) 354 if type(dir) ~= "string" then 355 error("bad argument #1 to 'getDirectoryItems' (string expected, got " .. type(dir) .. ")") 356 end 357 local result, err = withTempMount(dir, love.filesystem.getDirectoryItems) 358 return result or {} 359end 360 361local function getDirectoryItemsInfo(path, filtertype) 362 local items = {} 363 local files = love.filesystem.getDirectoryItems(path) 364 for i = 1, #files do 365 local filepath = string.format('%s/%s', path, files[i]) 366 local info = love.filesystem.getInfo(filepath, filtertype) 367 if info then 368 info.name = files[i] 369 table.insert(items, info) 370 end 371 end 372 return items 373end 374 375function nativefs.getDirectoryItemsInfo(path, filtertype) 376 if type(path) ~= "string" then 377 error("bad argument #1 to 'getDirectoryItemsInfo' (string expected, got " .. type(path) .. ")") 378 end 379 local result, err = withTempMount(path, getDirectoryItemsInfo, filtertype) 380 return result or {} 381end 382 383local function getInfo(path, file, filtertype) 384 local filepath = string.format('%s/%s', path, file) 385 return love.filesystem.getInfo(filepath, filtertype) 386end 387 388local function leaf(p) 389 p = p:gsub('\\', '/') 390 local last, a = p, 1 391 while a do 392 a = p:find('/', a + 1) 393 if a then 394 last = p:sub(a + 1) 395 end 396 end 397 return last 398end 399 400function nativefs.getInfo(path, filtertype) 401 if type(path) ~= 'string' then 402 error("bad argument #1 to 'getInfo' (string expected, got " .. type(path) .. ")") 403 end 404 local dir = path:match("(.*[\\/]).*$") or './' 405 local file = leaf(path) 406 local result, err = withTempMount(dir, getInfo, file, filtertype) 407 return result or nil 408end 409 410----------------------------------------------------------------------------- 411 412MODEMAP = { r = 'rb', w = 'wb', a = 'ab' } 413local MAX_PATH = 4096 414 415ffi.cdef([[ 416 int PHYSFS_mount(const char* dir, const char* mountPoint, int appendToPath); 417 int PHYSFS_unmount(const char* dir); 418 const char* PHYSFS_getMountPoint(const char* dir); 419 420 typedef struct FILE FILE; 421 422 FILE* fopen(const char* path, const char* mode); 423 size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream); 424 size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream); 425 int fclose(FILE* stream); 426 int fflush(FILE* stream); 427 size_t fseek(FILE* stream, size_t offset, int whence); 428 size_t ftell(FILE* stream); 429 int setvbuf(FILE* stream, char* buffer, int mode, size_t size); 430 int feof(FILE* stream); 431]]) 432 433if ffi.os == 'Windows' then 434 ffi.cdef([[ 435 int MultiByteToWideChar(unsigned int cp, uint32_t flags, const char* mb, int cmb, const wchar_t* wc, int cwc); 436 int WideCharToMultiByte(unsigned int cp, uint32_t flags, const wchar_t* wc, int cwc, const char* mb, 437 int cmb, const char* def, int* used); 438 int GetLogicalDrives(void); 439 int CreateDirectoryW(const wchar_t* path, void*); 440 int _wchdir(const wchar_t* path); 441 wchar_t* _wgetcwd(wchar_t* buffer, int maxlen); 442 FILE* _wfopen(const wchar_t* path, const wchar_t* mode); 443 int _wunlink(const wchar_t* path); 444 int _wrmdir(const wchar_t* path); 445 ]]) 446 447 BUFFERMODE = { full = 0, line = 64, none = 4 } 448 449 local function towidestring(str) 450 local size = C.MultiByteToWideChar(65001, 0, str, #str, nil, 0) 451 local buf = ffi.new('wchar_t[?]', size + 1) 452 C.MultiByteToWideChar(65001, 0, str, #str, buf, size) 453 return buf 454 end 455 456 local function toutf8string(wstr) 457 local size = C.WideCharToMultiByte(65001, 0, wstr, -1, nil, 0, nil, nil) 458 local buf = ffi.new('char[?]', size + 1) 459 C.WideCharToMultiByte(65001, 0, wstr, -1, buf, size, nil, nil) 460 return ffi.string(buf) 461 end 462 463 local nameBuffer = ffi.new('wchar_t[?]', MAX_PATH + 1) 464 465 fopen = function(path, mode) return C._wfopen(towidestring(path), towidestring(mode)) end 466 getcwd = function() return toutf8string(C._wgetcwd(nameBuffer, MAX_PATH)) end 467 chdir = function(path) return C._wchdir(towidestring(path)) == 0 end 468 unlink = function(path) return C._wunlink(towidestring(path)) == 0 end 469 mkdir = function(path) return C.CreateDirectoryW(towidestring(path), nil) ~= 0 end 470 rmdir = function(path) return C._wrmdir(towidestring(path)) == 0 end 471else 472 BUFFERMODE = { full = 0, line = 1, none = 2 } 473 474 ffi.cdef([[ 475 char* getcwd(char *buffer, int maxlen); 476 int chdir(const char* path); 477 int unlink(const char* path); 478 int mkdir(const char* path, int mode); 479 int rmdir(const char* path); 480 ]]) 481 482 local nameBuffer = ByteArray(MAX_PATH) 483 484 fopen = C.fopen 485 unlink = function(path) return ffi.C.unlink(path) == 0 end 486 chdir = function(path) return ffi.C.chdir(path) == 0 end 487 mkdir = function(path) return ffi.C.mkdir(path, 0x1ed) == 0 end 488 rmdir = function(path) return ffi.C.rmdir(path) == 0 end 489 490 getcwd = function() 491 local cwd = _ptr(C.getcwd(nameBuffer, MAX_PATH)) 492 return cwd and ffi.string(cwd) or nil 493 end 494end 495 496return nativefs