new primitives for reading/writing files
[lines.love.git] / nativefs.lua
blob2214bfc4d92f598960b3109d09975a33390a2437
1 --[[
2 Copyright 2020 megagrump@pm.me
4 Permission is hereby granted, free of charge, to any person obtaining a copy of
5 this software and associated documentation files (the "Software"), to deal in
6 the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 of the Software, and to permit persons to whom the Software is furnished to do
9 so, subject to the following conditions:
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 ]]--
23 local ffi, bit = require('ffi'), require('bit')
24 local C = ffi.C
26 local 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,
33 local fopen, getcwd, chdir, unlink, mkdir, rmdir
34 local BUFFERMODE, MODEMAP
35 local ByteArray = ffi.typeof('unsigned char[?]')
36 local function _ptr(p) return p ~= nil and p or nil end -- NULL pointer to nil
38 function 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
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
45 self._handle, self._mode = ffi.gc(handle, C.fclose), mode
46 self:setBuffer(self._bufferMode, self._bufferSize)
48 return true
49 end
51 function 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
56 end
58 function 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
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
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
76 self._bufferMode, self._bufferSize = mode, size
77 return true
78 end
80 function 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
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
96 end
98 function File:read(containerOrBytes, bytes)
99 if self._mode ~= 'r' then return nil, 0 end
101 local container = bytes ~= nil and containerOrBytes or 'string'
102 if container ~= 'string' and container ~= 'data' then
103 error("Invalid container type: " .. container)
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)
109 if bytes <= 0 then
110 local data = container == 'string' and '' or love.data.newFileData('', self._name)
111 return data, 0
114 local data = love.data.newByteData(bytes)
115 local r = tonumber(C.fread(data:getFFIPointer(), 1, bytes, self._handle))
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
123 local 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))
128 local offset = file:tell()
129 return function()
130 file:seek(offset)
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)
140 if buffer[i] ~= 13 then -- ignore CR
141 table.insert(line, string.char(buffer[i]))
145 bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
146 offset, bufferPos = offset + bytesRead, 0
149 if not line[1] then
150 if autoclose then file:close() end
151 return nil
153 return table.concat(line)
157 function File:lines()
158 if self._mode ~= 'r' then error("File is not opened for reading") end
159 return lines(self)
162 function File:write(data, size)
163 if self._mode ~= 'w' and self._mode ~= 'a' then
164 return false, "File " .. self._name .. " not opened for writing"
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()
176 if tonumber(C.fwrite(toWrite, 1, writeSize, self._handle)) ~= writeSize then
177 return false, "Could not write data"
179 return true
182 function File:seek(pos)
183 return self._handle and C.fseek(self._handle, pos, 0) == 0
186 function File:tell()
187 if not self._handle then return nil, "Invalid position" end
188 return tonumber(C.ftell(self._handle))
191 function File:flush()
192 if self._mode ~= 'w' and self._mode ~= 'a' then
193 return nil, "File is not opened for writing"
195 return C.fflush(self._handle) == 0
198 function File:isEOF()
199 return not self:isOpen() or C.feof(self._handle) ~= 0 or self:tell() == self:getSize()
202 function File:release()
203 if self._mode ~= 'c' then self:close() end
204 self._handle = nil
207 function File:type() return 'File' end
209 function File:typeOf(t) return t == 'File' end
211 File.__index = File
213 -----------------------------------------------------------------------------
215 local nativefs = {}
216 local loveC = ffi.os == 'Windows' and ffi.load('love') or C
218 function nativefs.newFile(name)
219 if type(name) ~= 'string' then
220 error("bad argument #1 to 'newFile' (string expected, got " .. type(name) .. ")")
222 return setmetatable({
223 _name = name,
224 _mode = 'c',
225 _handle = nil,
226 _bufferSize = 0,
227 _bufferMode = 'none'
228 }, File)
231 function 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
236 local data, err = f:read('data', 'all')
237 f:close()
238 return data, err
241 function nativefs.mount(archive, mountPoint, appendToPath)
242 return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0
245 function nativefs.unmount(archive)
246 return loveC.PHYSFS_unmount(archive) ~= 0
249 function 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'
263 local file = nativefs.newFile(name)
264 local ok, err = file:open('r')
265 if not ok then return nil, err end
267 local data, size = file:read(container, size)
268 file:close()
269 return data, size
272 local 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
277 ok, err = file:write(data, size or 'all')
278 file:close()
279 return ok, err
282 function nativefs.write(name, data, size)
283 return writeFile('w', name, data, size)
286 function nativefs.append(name, data, size)
287 return writeFile('a', name, data, size)
290 function 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)
297 function nativefs.load(name)
298 local chunk, err = nativefs.read(name)
299 if not chunk then return nil, err end
300 return loadstring(chunk, name)
303 function nativefs.getWorkingDirectory()
304 return getcwd()
307 function nativefs.setWorkingDirectory(path)
308 if not chdir(path) then return false, "Could not set working directory" end
309 return true
312 function 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) .. ':/')
320 return drives
323 function 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
330 return true
333 function 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
340 if not unlink(name) then return false, "Could not remove file " .. name end
341 return true
344 local 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
353 function nativefs.getDirectoryItems(dir)
354 if type(dir) ~= "string" then
355 error("bad argument #1 to 'getDirectoryItems' (string expected, got " .. type(dir) .. ")")
357 local result, err = withTempMount(dir, love.filesystem.getDirectoryItems)
358 return result or {}
361 local 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)
372 return items
375 function nativefs.getDirectoryItemsInfo(path, filtertype)
376 if type(path) ~= "string" then
377 error("bad argument #1 to 'getDirectoryItemsInfo' (string expected, got " .. type(path) .. ")")
379 local result, err = withTempMount(path, getDirectoryItemsInfo, filtertype)
380 return result or {}
383 local function getInfo(path, file, filtertype)
384 local filepath = string.format('%s/%s', path, file)
385 return love.filesystem.getInfo(filepath, filtertype)
388 local 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)
397 return last
400 function nativefs.getInfo(path, filtertype)
401 if type(path) ~= 'string' then
402 error("bad argument #1 to 'getInfo' (string expected, got " .. type(path) .. ")")
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
410 -----------------------------------------------------------------------------
412 MODEMAP = { r = 'rb', w = 'wb', a = 'ab' }
413 local MAX_PATH = 4096
415 ffi.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);
420 typedef struct FILE FILE;
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);
433 if 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);
447 BUFFERMODE = { full = 0, line = 64, none = 4 }
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
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)
463 local nameBuffer = ffi.new('wchar_t[?]', MAX_PATH + 1)
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
471 else
472 BUFFERMODE = { full = 0, line = 1, none = 2 }
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);
482 local nameBuffer = ByteArray(MAX_PATH)
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
490 getcwd = function()
491 local cwd = _ptr(C.getcwd(nameBuffer, MAX_PATH))
492 return cwd and ffi.string(cwd) or nil
496 return nativefs