mirror of
https://github.com/EngineerSmith/nativefs.git
synced 2025-11-08 23:15:02 +00:00
468 lines
13 KiB
Lua
468 lines
13 KiB
Lua
local ffi, bit = require('ffi'), require('bit')
|
|
local nativefs = {}
|
|
|
|
ffi.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);
|
|
]])
|
|
|
|
local C = ffi.C
|
|
local fclose, ftell, fseek, fflush = C.fclose, C.ftell, C.fseek, C.fflush
|
|
local fread, fwrite, setvbuf = C.fread, C.fwrite, C.setvbuf
|
|
local fopen, getcwd, chdir, unlink, mkdir, rmdir, feof -- system specific
|
|
local loveC = ffi.os == 'Windows' and ffi.load('love') or C
|
|
|
|
local BUFFERMODE = {
|
|
full = 0,
|
|
line = 1,
|
|
none = 2,
|
|
}
|
|
|
|
if ffi.os == 'Windows' then
|
|
ffi.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.line, BUFFERMODE.none = 64, 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 buf
|
|
end
|
|
|
|
local function toutf8string(wstr)
|
|
local size = C.WideCharToMultiByte(65001, 0, wstr, -1, nil, 0, nil, nil)
|
|
local buf = ffi.new("char[?]", size + 1)
|
|
size = C.WideCharToMultiByte(65001, 0, wstr, -1, buf, size, nil, nil)
|
|
return ffi.string(buf)
|
|
end
|
|
|
|
local MAX_PATH = 260
|
|
local nameBuffer = ffi.new("wchar_t[?]", MAX_PATH + 1)
|
|
|
|
fopen = function(path, mode) return C._wfopen(towidestring(path), towidestring(mode)) end
|
|
getcwd = function() return toutf8string(C._wgetcwd(nameBuffer, MAX_PATH)) end
|
|
chdir = function(path) return C._wchdir(towidestring(path)) == 0 end
|
|
unlink = function(path) return C._wunlink(towidestring(path)) == 0 end
|
|
mkdir = function(path) return C.CreateDirectoryW(towidestring(path), nil) ~= 0 end
|
|
rmdir = function(path) return C._wrmdir(towidestring(path)) == 0 end
|
|
else
|
|
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 MAX_PATH = 4096
|
|
local nameBuffer = ffi.new("char[?]", MAX_PATH)
|
|
|
|
fopen = C.fopen
|
|
unlink = function(path) return ffi.C.unlink(path) == 0 end
|
|
chdir = function(path) return ffi.C.chdir(path) == 0 end
|
|
mkdir = function(path) return ffi.C.mkdir(path, 0x1ed) == 0 end
|
|
rmdir = function(path) return ffi.C.rmdir(path) == 0 end
|
|
|
|
getcwd = function()
|
|
local cwd = C.getcwd(nameBuffer, MAX_PATH)
|
|
return cwd ~= nil and ffi.string(cwd) or nil
|
|
end
|
|
end
|
|
|
|
feof = function(stream) return C.feof(stream) ~= 0 end
|
|
|
|
-----------------------------------------------------------------------------
|
|
-- NOTE: nil checks on file handles MUST be explicit (_handle == nil)
|
|
-- due to ffi's NULL semantics!
|
|
|
|
local File = {
|
|
getBuffer = function(self) return self._bufferMode, self._bufferSize end,
|
|
getFilename = function(self) return self._name end,
|
|
getMode = function(self) return self._mode end,
|
|
isEOF = function(self) return not self:isOpen() or feof(self._handle) or self:tell() == self:getSize() end,
|
|
isOpen = function(self) return self._mode ~= 'c' and self._handle ~= nil end,
|
|
}
|
|
|
|
File.__index = File
|
|
|
|
function File:open(mode)
|
|
if self._mode ~= 'c' then return false, "File is already open" end
|
|
if mode ~= 'r' and mode ~= 'w' and mode ~= 'a' then
|
|
return false, "Invalid file open mode: " .. mode
|
|
end
|
|
|
|
local handle = fopen(self._name, mode .. 'b')
|
|
if handle == nil then
|
|
return false, "Could not open " .. self._name .. " in mode " .. mode
|
|
end
|
|
|
|
if C.setvbuf(handle, nil, BUFFERMODE[self._bufferMode], self._bufferSize) ~= 0 then
|
|
self._bufferMode, self._bufferSize = 'none', 0
|
|
end
|
|
|
|
self._handle, self._mode = ffi.gc(handle, C.fclose), mode
|
|
return true
|
|
end
|
|
|
|
function File:close()
|
|
if self._handle == nil or self._mode == 'c' then return false, "File is not open" end
|
|
ffi.gc(self._handle, nil)
|
|
fclose(self._handle)
|
|
self._handle, self._mode = nil, 'c'
|
|
return true
|
|
end
|
|
|
|
function File:setBuffer(mode, size)
|
|
local bufferMode = BUFFERMODE[mode]
|
|
if not bufferMode then
|
|
return false, "Invalid buffer mode " .. mode .. " (expected 'none', 'full', or 'line')"
|
|
end
|
|
|
|
if mode == 'line' or mode == 'full' then
|
|
size = math.max(2, size or 2) -- Windows requires buffer to be at least 2 bytes
|
|
else
|
|
size = math.max(0, size or 0)
|
|
end
|
|
if self._mode == 'c' then
|
|
self._bufferMode, self._bufferSize = mode, size
|
|
return true
|
|
end
|
|
|
|
local success = C.setvbuf(self._handle, nil, bufferMode, size) == 0
|
|
if success then
|
|
self._bufferMode, self._bufferSize = mode, size
|
|
return true
|
|
end
|
|
|
|
return false, "Could not set buffer mode"
|
|
end
|
|
|
|
|
|
function 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
|
|
end
|
|
|
|
local pos = mustOpen and 0 or self:tell()
|
|
fseek(self._handle, 0, 2)
|
|
local size = tonumber(self:tell())
|
|
if mustOpen then
|
|
self:close()
|
|
else
|
|
self:seek(pos)
|
|
end
|
|
return size;
|
|
end
|
|
|
|
function File:read(containerOrBytes, bytes)
|
|
if self._handle == nil or self._mode ~= 'r' then
|
|
return nil, 0
|
|
end
|
|
|
|
local container = bytes ~= nil and containerOrBytes or 'string'
|
|
if container ~= 'string' and container ~= 'data' then
|
|
error("Invalid container type: " .. container)
|
|
end
|
|
|
|
bytes = 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 then
|
|
local data = container == 'string' and '' or love.data.newFileData('', self._name)
|
|
return data, 0
|
|
end
|
|
|
|
local data = love.data.newByteData(bytes)
|
|
local r = tonumber(fread(data:getFFIPointer(), 1, bytes, self._handle))
|
|
|
|
if container == 'string' then
|
|
local str = data:getString()
|
|
data:release()
|
|
data = str
|
|
else
|
|
local fd = love.filesystem.newFileData(data:getString(), self._name)
|
|
data:release()
|
|
data = fd
|
|
end
|
|
return data, r
|
|
end
|
|
|
|
function File:lines()
|
|
if self._mode ~= 'r' then
|
|
error("File is not opened for reading")
|
|
end
|
|
|
|
local BUFFERSIZE = 4096
|
|
local buffer = ffi.new('unsigned char[?]', BUFFERSIZE)
|
|
local bytesRead = tonumber(fread(buffer, 1, BUFFERSIZE, self._handle))
|
|
|
|
local bufferPos = 0
|
|
local offset = self:tell()
|
|
return function()
|
|
self:seek(offset)
|
|
local line = {}
|
|
data = data
|
|
while true do
|
|
for i = bufferPos, bytesRead - 1 do
|
|
if buffer[i] ~= 10 and buffer[i] ~= 13 then
|
|
table.insert(line, string.char(buffer[i]))
|
|
elseif buffer[i] == 10 then
|
|
bufferPos = i + 1
|
|
return table.concat(line)
|
|
end
|
|
end
|
|
|
|
bytesRead = tonumber(fread(buffer, 1, BUFFERSIZE, self._handle))
|
|
if bytesRead == 0 then break end
|
|
offset, bufferPos = offset + bytesRead, 0
|
|
end
|
|
if #line > 0 then
|
|
return table.concat(line)
|
|
end
|
|
end
|
|
end
|
|
|
|
function File:write(data, size)
|
|
if self._mode ~= 'w' and self._mode ~= 'a' then
|
|
return false, "File " .. self._name .. " not opened for writing"
|
|
end
|
|
if type(data) == 'string' then
|
|
size = (size == nil or size == 'all') and #data or size
|
|
local success = tonumber(fwrite(data, 1, size, self._handle)) == size
|
|
if not success then
|
|
return false, "Could not write data"
|
|
end
|
|
else
|
|
size = (size == nil or size == 'all') and data:getSize() or size
|
|
local success = tonumber(fwrite(data:getFFIPointer(), 1, size, self._handle)) == size
|
|
if not success then
|
|
return false, "Could not write data"
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function File:seek(pos)
|
|
if self._handle == nil then return false end
|
|
return fseek(self._handle, pos, 0) == 0
|
|
end
|
|
|
|
function File:tell()
|
|
if self._handle == nil then return -1 end
|
|
return tonumber(ftell(self._handle))
|
|
end
|
|
|
|
function File:flush()
|
|
if self._handle == nil then return false, "File is not open" end
|
|
return fflush(self._handle) == 0
|
|
end
|
|
|
|
function File:release()
|
|
if self._mode ~= 'c' then
|
|
self:close()
|
|
end
|
|
self._handle = nil
|
|
end
|
|
|
|
-----------------------------------------------------------------------------
|
|
|
|
function nativefs.newFile(name)
|
|
return setmetatable({
|
|
_name = name,
|
|
_mode = 'c',
|
|
_handle = nil,
|
|
_bufferSize = 0,
|
|
_bufferMode = 'none'
|
|
}, File)
|
|
end
|
|
|
|
function nativefs.newFileData(filepath)
|
|
local f = nativefs.newFile(filepath)
|
|
local ok, err = f:open('r')
|
|
if not ok then return nil, err end
|
|
|
|
local data = f:read()
|
|
f:close()
|
|
return love.filesystem.newFileData(data, filepath)
|
|
end
|
|
|
|
function nativefs.mount(archive, mountPoint, appendToPath)
|
|
return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0
|
|
end
|
|
|
|
function nativefs.unmount(archive)
|
|
return loveC.PHYSFS_unmount(archive) ~= 0
|
|
end
|
|
|
|
function nativefs.read(containerOrName, nameOrSize, sizeOrNil)
|
|
local container, name, size
|
|
if sizeOrNil then
|
|
container, name, size = containerOrName, nameOrSize, sizeOrNil
|
|
elseif not nameOrSize then
|
|
container, name, size = 'string', containerOrName, 'all'
|
|
else
|
|
if type(nameOrSize) == 'number' or nameOrSize == 'all' then
|
|
container, name, size = 'string', containerOrName, nameOrSize
|
|
else
|
|
container, name, size = containerOrName, nameOrSize, 'all'
|
|
end
|
|
end
|
|
|
|
local file = nativefs.newFile(name)
|
|
if not file:open('r') then
|
|
return nil, "Could not open file for reading: " .. name
|
|
end
|
|
|
|
local data, size = file:read(container, size)
|
|
file:close()
|
|
return data, size
|
|
end
|
|
|
|
function nativefs.write(name, data, size)
|
|
local file = nativefs.newFile(name)
|
|
if not file:open('w') then
|
|
return nil, "Could not open file for writing: " .. name
|
|
end
|
|
|
|
local ok, err = file:write(data, size or 'all')
|
|
file:close()
|
|
return ok, err
|
|
end
|
|
|
|
function nativefs.append(name, data, size)
|
|
local file = nativefs.newFile(name)
|
|
if not file:open('a') then
|
|
return nil, "Could not open file for writing: " .. name
|
|
end
|
|
|
|
local ok, err = file:write(data, size or 'all')
|
|
file:close()
|
|
return ok, err
|
|
end
|
|
|
|
function nativefs.lines(name)
|
|
local f = nativefs.newFile(name)
|
|
local ok, err = f:open('r')
|
|
if not ok then return nil, err end
|
|
return f:lines()
|
|
end
|
|
|
|
function nativefs.load(name)
|
|
local chunk, err = nativefs.read(name)
|
|
if not chunk then return nil, err end
|
|
return loadstring(chunk, name)
|
|
end
|
|
|
|
local function withTempMount(dir, fn)
|
|
local mountPoint = loveC.PHYSFS_getMountPoint(dir)
|
|
if mountPoint ~= nil then
|
|
return fn(ffi.string(mountPoint))
|
|
end
|
|
|
|
if not nativefs.mount(dir, '__nativefs__temp__') then
|
|
return false, "Could not mount " .. dir
|
|
end
|
|
local a, b, c, d = fn('__nativefs__temp__')
|
|
nativefs.unmount(dir)
|
|
return a, b, c, d
|
|
end
|
|
|
|
function nativefs.getDirectoryItems(dir, callback)
|
|
return withTempMount(dir, function(mount)
|
|
return love.filesystem.getDirectoryItems(mount, callback)
|
|
end)
|
|
end
|
|
|
|
function nativefs.getWorkingDirectory()
|
|
return getcwd()
|
|
end
|
|
|
|
function nativefs.setWorkingDirectory(path)
|
|
if not chdir(path) then
|
|
return false, "Could not set working directory"
|
|
end
|
|
return true
|
|
end
|
|
|
|
function nativefs.getDriveList()
|
|
if ffi.os ~= 'Windows' then
|
|
return { '/' }
|
|
end
|
|
|
|
local drives = {}
|
|
local bits = C.GetLogicalDrives()
|
|
for i = 0, 25 do
|
|
if bit.band(bits, 2 ^ i) > 0 then
|
|
table.insert(drives, string.char(65 + i) .. ':/')
|
|
end
|
|
end
|
|
|
|
return drives
|
|
end
|
|
|
|
function nativefs.getInfo(path, filtertype)
|
|
local dir = path:match("(.*[\\/]).*$") or './'
|
|
local file = love.path.leaf(path)
|
|
return withTempMount(dir, function(mount)
|
|
local filepath = string.format("%s/%s", mount, file)
|
|
return love.filesystem.getInfo(filepath, filtertype)
|
|
end)
|
|
end
|
|
|
|
function nativefs.createDirectory(path)
|
|
local current = ''
|
|
for dir in path:gmatch('[^/\\]+') do
|
|
current = (current == '' and current or current .. '/') .. dir
|
|
local info = nativefs.getInfo(current, 'directory')
|
|
if not info and not mkdir(current) then
|
|
return false, "Could not create directory " .. current
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function nativefs.remove(name)
|
|
local info = nativefs.getInfo(name)
|
|
if info.type == 'directory' then
|
|
if not rmdir(name) then
|
|
return false, "Could not remove directory " .. name
|
|
end
|
|
else
|
|
if not unlink(name) then
|
|
return false, "Could not remove file " .. name
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
return nativefs
|