nativefs.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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 File = {
  22. getBuffer = function(self) return self._bufferMode, self._bufferSize end,
  23. getFilename = function(self) return self._name end,
  24. getMode = function(self) return self._mode end,
  25. isOpen = function(self) return self._mode ~= 'c' and self._handle ~= nil end,
  26. }
  27. local fopen, getcwd, chdir, unlink, mkdir, rmdir
  28. local BUFFERMODE, MODEMAP
  29. local ByteArray = ffi.typeof('unsigned char[?]')
  30. local function _ptr(p) return p ~= nil and p or nil end -- NULL pointer to nil
  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. local str = data:getString()
  95. data:release()
  96. data = container == 'data' and love.filesystem.newFileData(str, self._name) or str
  97. return data, r
  98. end
  99. local function lines(file, autoclose)
  100. local BUFFERSIZE = 4096
  101. local buffer, bufferPos = ByteArray(BUFFERSIZE), 0
  102. local bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
  103. local offset = file:tell()
  104. return function()
  105. file:seek(offset)
  106. local line = {}
  107. while bytesRead > 0 do
  108. for i = bufferPos, bytesRead - 1 do
  109. if buffer[i] == 10 then -- end of line
  110. bufferPos = i + 1
  111. return table.concat(line)
  112. end
  113. if buffer[i] ~= 13 then -- ignore CR
  114. table.insert(line, string.char(buffer[i]))
  115. end
  116. end
  117. bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
  118. offset, bufferPos = offset + bytesRead, 0
  119. end
  120. if not line[1] then
  121. if autoclose then file:close() end
  122. return nil
  123. end
  124. return table.concat(line)
  125. end
  126. end
  127. function File:lines()
  128. if self._mode ~= 'r' then error("File is not opened for reading") end
  129. return lines(self)
  130. end
  131. function File:write(data, size)
  132. if self._mode ~= 'w' and self._mode ~= 'a' then
  133. return false, "File " .. self._name .. " not opened for writing"
  134. end
  135. local toWrite, writeSize
  136. if type(data) == 'string' then
  137. writeSize = (size == nil or size == 'all') and #data or size
  138. toWrite = data
  139. else
  140. writeSize = (size == nil or size == 'all') and data:getSize() or size
  141. toWrite = data:getFFIPointer()
  142. end
  143. if tonumber(C.fwrite(toWrite, 1, writeSize, self._handle)) ~= writeSize then
  144. return false, "Could not write data"
  145. end
  146. return true
  147. end
  148. function File:seek(pos)
  149. return self._handle and C.fseek(self._handle, pos, 0) == 0
  150. end
  151. function File:tell()
  152. if not self._handle then return nil, "Invalid position" end
  153. return tonumber(C.ftell(self._handle))
  154. end
  155. function File:flush()
  156. if self._mode ~= 'w' and self._mode ~= 'a' then
  157. return nil, "File is not opened for writing"
  158. end
  159. return C.fflush(self._handle) == 0
  160. end
  161. function File:isEOF()
  162. return not self:isOpen() or C.feof(self._handle) ~= 0 or self:tell() == self:getSize()
  163. end
  164. function File:release()
  165. if self._mode ~= 'c' then self:close() end
  166. self._handle = nil
  167. end
  168. function File:type() return 'File' end
  169. function File:typeOf(t) return t == 'File' end
  170. File.__index = File
  171. -----------------------------------------------------------------------------
  172. local nativefs = {}
  173. local loveC = ffi.os == 'Windows' and ffi.load('love') or C
  174. function nativefs.newFile(name)
  175. if type(name) ~= 'string' then
  176. error("bad argument #1 to 'newFile' (string expected, got " .. type(name) .. ")")
  177. end
  178. return setmetatable({
  179. _name = name,
  180. _mode = 'c',
  181. _handle = nil,
  182. _bufferSize = 0,
  183. _bufferMode = 'none'
  184. }, File)
  185. end
  186. function nativefs.newFileData(filepath)
  187. local f = nativefs.newFile(filepath)
  188. local ok, err = f:open('r')
  189. if not ok then return nil, err end
  190. local data, err = f:read('data', 'all')
  191. f:close()
  192. return data, err
  193. end
  194. function nativefs.mount(archive, mountPoint, appendToPath)
  195. return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0
  196. end
  197. function nativefs.unmount(archive)
  198. return loveC.PHYSFS_unmount(archive) ~= 0
  199. end
  200. function nativefs.read(containerOrName, nameOrSize, sizeOrNil)
  201. local container, name, size
  202. if sizeOrNil then
  203. container, name, size = containerOrName, nameOrSize, sizeOrNil
  204. elseif not nameOrSize then
  205. container, name, size = 'string', containerOrName, 'all'
  206. else
  207. if type(nameOrSize) == 'number' or nameOrSize == 'all' then
  208. container, name, size = 'string', containerOrName, nameOrSize
  209. else
  210. container, name, size = containerOrName, nameOrSize, 'all'
  211. end
  212. end
  213. local file = nativefs.newFile(name)
  214. local ok, err = file:open('r')
  215. if not ok then return nil, err end
  216. local data, size = file:read(container, size)
  217. file:close()
  218. return data, size
  219. end
  220. local function writeFile(mode, name, data, size)
  221. local file = nativefs.newFile(name)
  222. local ok, err = file:open(mode)
  223. if not ok then return nil, err end
  224. ok, err = file:write(data, size or 'all')
  225. file:close()
  226. return ok, err
  227. end
  228. function nativefs.write(name, data, size)
  229. return writeFile('w', name, data, size)
  230. end
  231. function nativefs.append(name, data, size)
  232. return writeFile('a', name, data, size)
  233. end
  234. function nativefs.lines(name)
  235. local f = nativefs.newFile(name)
  236. local ok, err = f:open('r')
  237. if not ok then return nil, err end
  238. return lines(f, true)
  239. end
  240. function nativefs.load(name)
  241. local chunk, err = nativefs.read(name)
  242. if not chunk then return nil, err end
  243. return loadstring(chunk, name)
  244. end
  245. function nativefs.getWorkingDirectory()
  246. return getcwd()
  247. end
  248. function nativefs.setWorkingDirectory(path)
  249. if not chdir(path) then return false, "Could not set working directory" end
  250. return true
  251. end
  252. function nativefs.getDriveList()
  253. if ffi.os ~= 'Windows' then return { '/' } end
  254. local drives, bits = {}, C.GetLogicalDrives()
  255. for i = 0, 25 do
  256. if bit.band(bits, 2 ^ i) > 0 then
  257. table.insert(drives, string.char(65 + i) .. ':/')
  258. end
  259. end
  260. return drives
  261. end
  262. function nativefs.createDirectory(path)
  263. local current = path:sub(1, 1) == '/' and '/' or ''
  264. for dir in path:gmatch('[^/\\]+') do
  265. current = current .. dir .. '/'
  266. local info = nativefs.getInfo(current, 'directory')
  267. if not info and not mkdir(current) then return false, "Could not create directory " .. current end
  268. end
  269. return true
  270. end
  271. function nativefs.remove(name)
  272. local info = nativefs.getInfo(name)
  273. if not info then return false, "Could not remove " .. name end
  274. if info.type == 'directory' then
  275. if not rmdir(name) then return false, "Could not remove directory " .. name end
  276. return true
  277. end
  278. if not unlink(name) then return false, "Could not remove file " .. name end
  279. return true
  280. end
  281. local function withTempMount(dir, fn, ...)
  282. local mountPoint = _ptr(loveC.PHYSFS_getMountPoint(dir))
  283. if mountPoint then return fn(ffi.string(mountPoint), ...) end
  284. if not nativefs.mount(dir, '__nativefs__temp__') then return false, "Could not mount " .. dir end
  285. local a, b = fn('__nativefs__temp__', ...)
  286. nativefs.unmount(dir)
  287. return a, b
  288. end
  289. function nativefs.getDirectoryItems(dir)
  290. if type(dir) ~= "string" then
  291. error("bad argument #1 to 'getDirectoryItems' (string expected, got " .. type(dir) .. ")")
  292. end
  293. local result, err = withTempMount(dir, love.filesystem.getDirectoryItems)
  294. return result or {}
  295. end
  296. local function getDirectoryItemsInfo(path, filtertype)
  297. local items = {}
  298. local files = love.filesystem.getDirectoryItems(path)
  299. for i = 1, #files do
  300. local filepath = string.format('%s/%s', path, files[i])
  301. local info = love.filesystem.getInfo(filepath, filtertype)
  302. if info then
  303. info.name = files[i]
  304. table.insert(items, info)
  305. end
  306. end
  307. return items
  308. end
  309. function nativefs.getDirectoryItemsInfo(path, filtertype)
  310. if type(path) ~= "string" then
  311. error("bad argument #1 to 'getDirectoryItemsInfo' (string expected, got " .. type(path) .. ")")
  312. end
  313. local result, err = withTempMount(path, getDirectoryItemsInfo, filtertype)
  314. return result or {}
  315. end
  316. local function getInfo(path, file, filtertype)
  317. local filepath = string.format('%s/%s', path, file)
  318. return love.filesystem.getInfo(filepath, filtertype)
  319. end
  320. local function leaf(p)
  321. p = p:gsub('\\', '/')
  322. local last, a = p, 1
  323. while a do
  324. a = p:find('/', a + 1)
  325. if a then
  326. last = p:sub(a + 1)
  327. end
  328. end
  329. return last
  330. end
  331. function nativefs.getInfo(path, filtertype)
  332. if type(path) ~= 'string' then
  333. error("bad argument #1 to 'getInfo' (string expected, got " .. type(path) .. ")")
  334. end
  335. local dir = path:match("(.*[\\/]).*$") or './'
  336. local file = leaf(path)
  337. local result, err = withTempMount(dir, getInfo, file, filtertype)
  338. return result or nil
  339. end
  340. -----------------------------------------------------------------------------
  341. MODEMAP = { r = 'rb', w = 'wb', a = 'ab' }
  342. local MAX_PATH = 4096
  343. ffi.cdef([[
  344. int PHYSFS_mount(const char* dir, const char* mountPoint, int appendToPath);
  345. int PHYSFS_unmount(const char* dir);
  346. const char* PHYSFS_getMountPoint(const char* dir);
  347. typedef struct FILE FILE;
  348. FILE* fopen(const char* path, const char* mode);
  349. size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
  350. size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
  351. int fclose(FILE* stream);
  352. int fflush(FILE* stream);
  353. size_t fseek(FILE* stream, size_t offset, int whence);
  354. size_t ftell(FILE* stream);
  355. int setvbuf(FILE* stream, char* buffer, int mode, size_t size);
  356. int feof(FILE* stream);
  357. ]])
  358. if ffi.os == 'Windows' then
  359. ffi.cdef([[
  360. int MultiByteToWideChar(unsigned int cp, uint32_t flags, const char* mb, int cmb, const wchar_t* wc, int cwc);
  361. int WideCharToMultiByte(unsigned int cp, uint32_t flags, const wchar_t* wc, int cwc, const char* mb,
  362. int cmb, const char* def, int* used);
  363. int GetLogicalDrives(void);
  364. int CreateDirectoryW(const wchar_t* path, void*);
  365. int _wchdir(const wchar_t* path);
  366. wchar_t* _wgetcwd(wchar_t* buffer, int maxlen);
  367. FILE* _wfopen(const wchar_t* path, const wchar_t* mode);
  368. int _wunlink(const wchar_t* path);
  369. int _wrmdir(const wchar_t* path);
  370. ]])
  371. BUFFERMODE = { full = 0, line = 64, none = 4 }
  372. local function towidestring(str)
  373. local size = C.MultiByteToWideChar(65001, 0, str, #str, nil, 0)
  374. local buf = ffi.new('wchar_t[?]', size + 1)
  375. C.MultiByteToWideChar(65001, 0, str, #str, buf, size)
  376. return buf
  377. end
  378. local function toutf8string(wstr)
  379. local size = C.WideCharToMultiByte(65001, 0, wstr, -1, nil, 0, nil, nil)
  380. local buf = ffi.new('char[?]', size + 1)
  381. C.WideCharToMultiByte(65001, 0, wstr, -1, buf, size, nil, nil)
  382. return ffi.string(buf)
  383. end
  384. local nameBuffer = ffi.new('wchar_t[?]', MAX_PATH + 1)
  385. fopen = function(path, mode) return C._wfopen(towidestring(path), towidestring(mode)) end
  386. getcwd = function() return toutf8string(C._wgetcwd(nameBuffer, MAX_PATH)) end
  387. chdir = function(path) return C._wchdir(towidestring(path)) == 0 end
  388. unlink = function(path) return C._wunlink(towidestring(path)) == 0 end
  389. mkdir = function(path) return C.CreateDirectoryW(towidestring(path), nil) ~= 0 end
  390. rmdir = function(path) return C._wrmdir(towidestring(path)) == 0 end
  391. else
  392. BUFFERMODE = { full = 0, line = 1, none = 2 }
  393. ffi.cdef([[
  394. char* getcwd(char *buffer, int maxlen);
  395. int chdir(const char* path);
  396. int unlink(const char* path);
  397. int mkdir(const char* path, int mode);
  398. int rmdir(const char* path);
  399. ]])
  400. local nameBuffer = ByteArray(MAX_PATH)
  401. fopen = C.fopen
  402. unlink = function(path) return ffi.C.unlink(path) == 0 end
  403. chdir = function(path) return ffi.C.chdir(path) == 0 end
  404. mkdir = function(path) return ffi.C.mkdir(path, 0x1ed) == 0 end
  405. rmdir = function(path) return ffi.C.rmdir(path) == 0 end
  406. getcwd = function()
  407. local cwd = _ptr(C.getcwd(nameBuffer, MAX_PATH))
  408. return cwd and ffi.string(cwd) or nil
  409. end
  410. end
  411. return nativefs