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
23 local ffi
, bit
= require('ffi'), require('bit')
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
)
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'
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')"
64 if mode
== 'none' then
65 size
= math
.max(0, size
or 0)
67 size
= math
.max(2, size
or 2) -- Windows requires buffer to be at least 2 bytes
70 local success
= self
._mode
== 'c' or C
.setvbuf(self
._handle
, nil, bufferMode
, size
) == 0
72 self
._bufferMode
, self
._bufferSize
= 'none', 0
73 return false, "Could not set buffer mode"
76 self
._bufferMode
, self
._bufferSize
= mode
, size
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
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()
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
)
110 local data
= container
== 'string' and '' or love
.data
.newFileData('', self
._name
)
114 local data
= love
.data
.newByteData(bytes
)
115 local r
= tonumber(C
.fread(data
:getFFIPointer(), 1, bytes
, self
._handle
))
117 local str
= data
:getString()
119 data
= container
== 'data' and love
.filesystem
.newFileData(str
, self
._name
) or str
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()
133 while bytesRead
> 0 do
134 for i
= bufferPos
, bytesRead
- 1 do
135 if buffer
[i
] == 10 then -- end of line
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
150 if autoclose
then file
:close() end
153 return table.concat(line
)
157 function File
:lines()
158 if self
._mode
~= 'r' then error("File is not opened for reading") end
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
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"
182 function File
:seek(pos
)
183 return self
._handle
and C
.fseek(self
._handle
, pos
, 0) == 0
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
207 function File
:type() return 'File' end
209 function File
:typeOf(t
) return t
== 'File' end
213 -----------------------------------------------------------------------------
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({
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')
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
252 container
, name
, size
= containerOrName
, nameOrSize
, sizeOrNil
253 elseif not nameOrSize
then
254 container
, name
, size
= 'string', containerOrName
, 'all'
256 if type(nameOrSize
) == 'number' or nameOrSize
== 'all' then
257 container
, name
, size
= 'string', containerOrName
, nameOrSize
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
)
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')
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()
307 function nativefs
.setWorkingDirectory(path
)
308 if not chdir(path
) then return false, "Could not set working directory" end
312 function nativefs
.getDriveList()
313 if ffi
.os
~= 'Windows' then return { '/' } end
314 local drives
, bits
= {}, C
.GetLogicalDrives()
316 if bit
.band(bits
, 2 ^ i
) > 0 then
317 table.insert(drives
, string.char(65 + i
) .. ':/')
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
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
340 if not unlink(name
) then return false, "Could not remove file " .. name
end
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
)
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
)
361 local function getDirectoryItemsInfo(path
, filtertype
)
363 local files
= love
.filesystem
.getDirectoryItems(path
)
365 local filepath
= string.format('%s/%s', path
, files
[i
])
366 local info
= love
.filesystem
.getInfo(filepath
, filtertype
)
369 table.insert(items
, info
)
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
)
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('\\', '/')
392 a
= p
:find('/', a
+ 1)
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
)
410 -----------------------------------------------------------------------------
412 MODEMAP
= { r
= 'rb', w
= 'wb', a
= 'ab' }
413 local MAX_PATH
= 4096
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
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
)
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
472 BUFFERMODE
= { full
= 0, line
= 1, none
= 2 }
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
)
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
491 local cwd
= _ptr(C
.getcwd(nameBuffer
, MAX_PATH
))
492 return cwd
and ffi
.string(cwd
) or nil