diff --git a/nativefs.lua b/nativefs.lua index 61dab87..6db26c0 100644 --- a/nativefs.lua +++ b/nativefs.lua @@ -8,7 +8,7 @@ ffi.cdef([[ typedef struct FILE FILE; - FILE* fopen(const char* pathname, const char* mode); + 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); @@ -20,9 +20,9 @@ ffi.cdef([[ ]]) local C = ffi.C -local fclose, ftell, fseek, fflush, feof = C.fclose, C.ftell, C.fseek, C.fflush -local fread, fwrite, feof, setvbuf = C.fread, C.fwrite, C.feof, C.setvbuf -local fopen, getcwd, chdir, unlink -- system specific +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 = { @@ -37,10 +37,12 @@ if ffi.os == 'Windows' then 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* name, const wchar_t* mode); - int _wunlink(const wchar_t* name); + 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 @@ -62,37 +64,54 @@ if ffi.os == 'Windows' then local MAX_PATH = 260 local nameBuffer = ffi.new("wchar_t[?]", MAX_PATH + 1) - fopen = function(name, mode) return C._wfopen(towidestring(name), towidestring(mode)) end + 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)) end - unlink = function(name) return C._wunlink(towidestring(name)) 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, unlink, chdir = C.fopen, C.unlink, C.chdir + 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 = {} +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 @@ -111,10 +130,7 @@ function File:open(mode) end function File:close() - if self._handle == nil or self._mode == 'c' then - return false, "File is not open" - end - + 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' @@ -122,29 +138,30 @@ function File:close() end function File:setBuffer(mode, size) - bufferMode = BUFFERMODE[mode] + local bufferMode = BUFFERMODE[mode] if not bufferMode then - return false, "Invalid buffer mode: " .. mode .. " (expected 'none', 'full', or 'line')" + return false, "Invalid buffer mode " .. mode .. " (expected 'none', 'full', or 'line')" end - size = math.max(0, size or 0) - self._bufferMode, self._bufferSize = mode, size - if self._mode == 'c' then return true 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 - return C.setvbuf(self._handle, nil, bufferMode, size) == 0 + 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:getBuffer() - return self._bufferMode, self._bufferSize -end - -function File:getFilename() - return self._name -end - -function File:getMode() - return self._mode -end function File:getSize() -- NOTE: The correct way to do this would be a stat() call, which requires a @@ -166,14 +183,6 @@ function File:getSize() return size; end -function File:isEOF() - return not self:isOpen() or feof(self._handle) ~= 0 -end - -function File:isOpen() - return self._mode ~= 'c' and self._handle ~= nil -end - function File:read(containerOrBytes, bytes) if self._handle == nil or self._mode ~= 'r' then return nil, 0 @@ -208,6 +217,38 @@ function File:read(containerOrBytes, bytes) 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) @@ -241,8 +282,8 @@ function File:tell() end function File:flush() - if self._handle == nil then return end - return fflush(self._handle) + if self._handle == nil then return false, "File is not open" end + return fflush(self._handle) == 0 end function File:release() @@ -366,7 +407,7 @@ function nativefs.getWorkingDirectory() end function nativefs.setWorkingDirectory(path) - if chdir(path) ~= 0 then + if not chdir(path) then return false, "Could not set working directory" end return true @@ -398,11 +439,27 @@ function nativefs.getInfo(path, filtertype) 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) - if unlink(name) ~= 0 then - return false, "Could not remove file " .. 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 diff --git a/test/data/𠆢ßЩ.txt b/test/data/𠆢ßЩ.txt new file mode 100644 index 0000000..3c54fc6 --- /dev/null +++ b/test/data/𠆢ßЩ.txt @@ -0,0 +1,4 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/test/main.lua b/test/main.lua index bb0473c..91df0c0 100644 --- a/test/main.lua +++ b/test/main.lua @@ -6,11 +6,11 @@ local lu = require('luaunit') local fs local equals, notEquals = lu.assertEquals, lu.assertNotEquals -local isError, containsError = lu.assertErrorMsgEquals, lu.assertErrorMsgContains local contains = lu.assertStrContains local function notFailed(ok, err) equals(ok, true) + if err then print("ERROR: " .. err) end equals(err, nil) end @@ -23,6 +23,9 @@ end ----------------------------------------------------------------------------- +local testFile1, testSize1 = 'data/ümläüt.txt', 446 +local testFile2, testSize2 = 'data/𠆢ßЩ.txt', 450 + function test_fs_newFile() local file = fs.newFile('test.file') notEquals(file, nil) @@ -30,37 +33,37 @@ function test_fs_newFile() end function test_fs_newFileData() - local fd = fs.newFileData('data/ümläüt.txt') - local filesize = love.filesystem.newFile('data/ümläüt.txt'):getSize() + local fd = fs.newFileData(testFile1) + local filesize = love.filesystem.newFile(testFile1):getSize() equals(fd:getSize(), filesize) - local d = love.filesystem.read('data/ümläüt.txt') + local d = love.filesystem.read(testFile1) equals(fd:getString(), d) end function test_fs_mount() equals(fs.mount('data', 'test_data'), true) - local data, size = love.filesystem.read('test_data/ümläüt.txt') + local data, size = love.filesystem.read('test_data/' .. love.path.leaf(testFile1)) notEquals(data, nil) notEquals(size, nil) equals(fs.unmount('data'), true) end function test_fs_read() - local text, textSize = love.filesystem.read('data/ümläüt.txt') - local data, size = fs.read('data/ümläüt.txt') + local text, textSize = love.filesystem.read(testFile1) + local data, size = fs.read(testFile1) equals(size, textSize) equals(data, text) - local data, size = fs.read('data/ümläüt.txt', 'all') + local data, size = fs.read(testFile1, 'all') equals(size, textSize) equals(data, text) - local data, size = fs.read('string', 'data/ümläüt.txt') + local data, size = fs.read('string', testFile1) equals(size, textSize) equals(data, text) - local data, size = fs.read('data', 'data/ümläüt.txt', 'all') + local data, size = fs.read('data', testFile1, 'all') equals(size, textSize) equals(data:type(), 'FileData') equals(data:getSize(), size) @@ -72,7 +75,7 @@ function test_fs_read() end function test_fs_write() - local data, size = fs.read('data/ümläüt.txt') + local data, size = fs.read(testFile1) notFailed(fs.write('data/write.test', data)) local data2, size2 = love.filesystem.read('data', 'data/write.test') @@ -84,7 +87,7 @@ function test_fs_write() end function test_fs_append() - local text, textSize = love.filesystem.read('data/ümläüt.txt') + local text, textSize = love.filesystem.read(testFile1) fs.write('data/append.test', text) fs.append('data/append.test', text) @@ -96,6 +99,20 @@ function test_fs_append() end function test_fs_lines() + local count, bytes = 0, 0 + for line in fs.lines(testFile1) do + count = count + 1 + bytes = bytes + #line + end + equals(count, 4) + equals(bytes, testSize1 - count) + + local code = "" + for line in fs.lines('main.lua') do + code = code .. line .. '\n' + end + local r = fs.read('main.lua') + equals(code, r) end function test_fs_load() @@ -155,27 +172,40 @@ function test_fs_getInfo() notEquals(info, nil) equals(info.type, 'file') - local info = fs.getInfo('data/ümläüt.txt') + local info = fs.getInfo(testFile1) notEquals(info, nil) equals(info.type, 'file') - equals(info.size, 446) + equals(info.size, testSize1) notEquals(info.modtime, nil) end function test_fs_createDirectory() + notFailed(fs.createDirectory('data/a/b/c/defg/h')) + fs.remove('data/a/b/c/defg/h') + fs.remove('data/a/b/c/defg') + fs.remove('data/a/b/c') + fs.remove('data/a/b') + fs.remove('data/a') end function test_fs_remove() - local text = love.filesystem.read('data/ümläüt.txt') + local text = love.filesystem.read(testFile1) fs.write('data/remove.test', text) notFailed(fs.remove('data/remove.test')) equals(love.filesystem.getInfo('data/remove.test'), nil) + + fs.createDirectory('data/test1') + fs.createDirectory('data/test1/test2') + notFailed(fs.remove('data/test1/test2')) + equals(love.filesystem.getInfo('data/test1/test2'), nil) + notFailed(fs.remove('data/test1')) + equals(love.filesystem.getInfo('data/test1'), nil) end ----------------------------------------------------------------------------- function test_File_open() - local f = fs.newFile('data/ümläüt.txt') + local f = fs.newFile(testFile1) notEquals(f, nil) equals(f:isOpen(), false) equals(f:getMode(), 'c') @@ -195,16 +225,23 @@ function test_File_open() equals(f:getMode(), 'c') f:close() - local f = fs.newFile('data/𠆢ßЩ.txt') + local f = fs.newFile(testFile2) notFailed(f:open('r')) f:close() end function test_File_setBuffer() + local f = fs.newFile('data/test.test') + f:open('w') + notFailed(f:setBuffer('none', 0)) + notFailed(f:setBuffer('line', 0)) + notFailed(f:setBuffer('full', 0)) + f:close() + fs.remove('data/test.test') end function test_File_isEOF() - local f = love.filesystem.newFile('data/ümläüt.txt') + local f = fs.newFile(testFile1) f:open('r') f:read(f:getSize() - 1) equals(f:isEOF(), false) @@ -214,27 +251,84 @@ function test_File_isEOF() end function test_File_read() + local f = fs.newFile(testFile1) + f:open('r') + local data, size = f:read(5) + equals(data, 'Lorem') + + local data, size = f:read(6) + equals(data, ' ipsum') + f:close() end function test_File_lines() + local text = fs.read(testFile2) + local f = fs.newFile(testFile2) + local lines = "" + f:open('r') + for line in f:lines() do lines = lines .. line .. '\r\n' end + f:close() + equals(lines, text) + + local text = fs.read(testFile1) + local f = fs.newFile(testFile1) + local lines = "" + f:open('r') + for line in f:lines() do lines = lines .. line .. '\n' end + f:close() + equals(lines, text) end function test_File_write() + local f = fs.newFile('data/write.test') + notFailed(f:open('w')) + notFailed(f:write('hello')) + f:close() + f:open('a') + notFailed(f:write('world')) + f:close() + + notFailed(f:open('r')) + local hello, size = f:read() + equals(hello, 'helloworld') + equals(size, #'helloworld') + f:close() + + fs.remove('data/write.test') end function test_File_seek() + local f = fs.newFile(testFile1) + f:open('r') + f:seek(72) + local data, size = f:read(6) + equals(data, 'tempor') + f:close() end function test_File_tell() + local f = fs.newFile(testFile1) + f:open('r') + f:read(172) + equals(f:tell(), 172) + f:close() end function test_File_flush() + local f = fs.newFile('data/write.test') + f:open('w') + f:write('hello') + notFailed(f:flush()) + f:close() end local _globals = {} function test_xxx_globalsCheck() for k, v in pairs(_G) do + if v ~= _globals[k] then + print("LEAKED GLOBAL: " .. k) + end equals(v, _globals[k]) end end