nativefs.lua 14 KB

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