--portable filesystem API for LuaJIT
|
--Written by Cosmin Apreutesei. Public Domain.
|
|
local ffi = require'ffi'
|
|
-- begin fs/common.lua
|
|
local bit = require'bit'
|
|
--implement path module to minimize the dependencies (Paul Kulchenko)
|
local path = {
|
sep = package.config:sub(1,1),
|
seppat = "[/"..package.config:sub(1,1).."]",
|
notseppat = "[^/"..package.config:sub(1,1).."]",
|
}
|
path.endsep = function(s) return s:find(path.seppat.."$") ~= nil end
|
path.combine = function(s1, s2) return path.isabs(s2) and s2 or path.endsep(s1) and s1..s2 or s1..path.sep..s2 end
|
path.dir = function(s) return s:match("(.+)"..path.seppat..path.notseppat.."+$") end
|
path.isabs = function(s) return s:find("^"..path.seppat) ~= nil or path.sep == "\\" and s:sub(2,2) == ":" end
|
|
local min, max, floor = math.min, math.max, math.floor
|
|
local C = ffi.C
|
|
local cdef = ffi.cdef
|
local x64 = ffi.arch == 'x64' or nil
|
local osx = ffi.os == 'OSX' or nil
|
local linux = ffi.os == 'Linux' or nil
|
local win = ffi.abi'win' or nil
|
|
--namespaces in which backends can add methods directly.
|
local fs = {} --fs module namespace
|
local file = {} --file object methods
|
local stream = {} --FILE methods
|
|
--assert() with string formatting.
|
local function assert(v, err, ...)
|
if v then return v end
|
err = err or 'assertion failed!'
|
if select('#',...) > 0 then
|
err = string.format(err,...)
|
end
|
error(err, 2)
|
end
|
|
--return a function which reuses and returns an ever-increasing buffer.
|
local function mkbuf(ctype, min_sz)
|
ctype = ffi.typeof('$[?]', ffi.typeof(ctype))
|
min_sz = min_sz or 256
|
assert(min_sz > 0)
|
local buf, bufsz
|
return function(sz)
|
sz = sz or bufsz or min_sz
|
assert(sz > 0)
|
if not bufsz or sz > bufsz then
|
buf, bufsz = ctype(sz), sz
|
end
|
return buf, bufsz
|
end
|
end
|
|
--error reporting ------------------------------------------------------------
|
|
cdef'char *strerror(int errnum);'
|
|
local error_classes = {
|
[2] = 'not_found', --ENOENT, _open_osfhandle(), _fdopen(), open(), mkdir(),
|
--rmdir(), opendir(), rename(), unlink()
|
[5] = 'io_error', --EIO, readlink()
|
[17] = 'already_exists', --EEXIST, open(), mkdir()
|
[20] = 'not_found', --ENOTDIR, opendir()
|
--[21] = 'access_denied', --EISDIR, unlink()
|
[linux and 39 or osx and 66 or ''] = 'not_empty',
|
--ENOTEMPTY, rmdir()
|
[28] = 'disk_full', --ENOSPC: fallocate()
|
[linux and 95 or ''] = 'not_supported', --EOPNOTSUPP: fallocate()
|
|
--[[ --TODO: mmap
|
local ENOENT = 2
|
local ENOMEM = 12
|
local EINVAL = 22
|
local EFBIG = 27
|
local ENOSPC = 28
|
local EDQUOT = osx and 69 or 122
|
|
local errcodes = {
|
[ENOENT] = 'not_found',
|
[ENOMEM] = 'out_of_mem',
|
[EINVAL] = 'file_too_short',
|
[EFBIG] = 'disk_full',
|
[ENOSPC] = 'disk_full',
|
[EDQUOT] = 'disk_full',
|
}
|
]]
|
|
--[[
|
[12] = 'out_of_mem', --TODO: ENOMEM: mmap
|
[22] = 'file_too_short', --TODO: EINVAL: mmap
|
[27] = 'disk_full', --TODO: EFBIG
|
[osx and 69 or 122] = 'disk_full', --TODO: EDQUOT
|
]]
|
}
|
|
local function check_errno(ret, errno)
|
if ret then return ret end
|
errno = errno or ffi.errno()
|
local err = error_classes[errno]
|
if not err then
|
local s = C.strerror(errno)
|
err = s ~= nil and ffi.string(s) or 'Error '..errno
|
end
|
return ret, err, errno
|
end
|
|
--flags arg parsing ----------------------------------------------------------
|
|
--turn a table of boolean options into a bit mask.
|
local function table_flags(t, masks, strict)
|
local bits = 0
|
local mask = 0
|
for k,v in pairs(t) do
|
local flag
|
if type(k) == 'string' and v then --flags as table keys: {flag->true}
|
flag = k
|
elseif type(k) == 'number'
|
and floor(k) == k
|
and type(v) == 'string'
|
then --flags as array: {flag1,...}
|
flag = v
|
end
|
local bitmask = masks[flag]
|
if strict then
|
assert(bitmask, 'invalid flag: "%s"', tostring(flag))
|
elseif bitmask then
|
mask = bit.bor(mask, bitmask)
|
if flag then
|
bits = bit.bor(bits, bitmask)
|
end
|
end
|
end
|
return bits, mask
|
end
|
|
--turn 'opt1 +opt2 -opt3' -> {opt1=true, opt2=true, opt3=false}
|
local function string_flags(s, masks, strict)
|
local t = {}
|
for s in s:gmatch'[^ ,]+' do
|
local m,s = s:match'^([%+%-]?)(.*)$'
|
t[s] = m ~= '-'
|
end
|
return table_flags(t, masks, strict)
|
end
|
|
--set one or more bits of a value without affecting other bits.
|
local function setbits(bits, mask, over)
|
return over and bit.bor(bits, bit.band(over, bit.bnot(mask))) or bits
|
end
|
|
--cache tuple(options_string, masks_table) -> bits, mask
|
local cache = {}
|
local function getcache(s, masks)
|
cache[masks] = cache[masks] or {}
|
local t = cache[masks][s]
|
if not t then return end
|
return t[1], t[2]
|
end
|
local function setcache(s, masks, bits, mask)
|
cache[masks][s] = {bits, mask}
|
end
|
|
local function flags(arg, masks, cur_bits, strict)
|
if type(arg) == 'string' then
|
local bits, mask = getcache(arg, masks)
|
if not bits then
|
bits, mask = string_flags(arg, masks, strict)
|
setcache(arg, masks, bits, mask)
|
end
|
return setbits(bits, mask, cur_bits)
|
elseif type(arg) == 'table' then
|
local bits, mask = table_flags(arg, masks, strict)
|
return setbits(bits, mask, cur_bits)
|
elseif type(arg) == 'number' then
|
return arg
|
elseif arg == nil then
|
return 0
|
else
|
assert(false, 'flags expected but "%s" given', type(arg))
|
end
|
end
|
|
--file objects ---------------------------------------------------------------
|
|
--returns a read(buf, sz) -> sz function which reads ahead from file
|
function file.buffered_read(f, ctype, bufsize)
|
local elem_ct = ffi.typeof(ctype or 'char')
|
local ptr_ct = ffi.typeof('$*', elem_ct)
|
assert(ffi.sizeof(elem_ct) == 1)
|
local buf_ct = ffi.typeof('$[?]', elem_ct)
|
local bufsize = bufsize or 4096
|
local buf = buf_ct(bufsize)
|
local ofs, len = 0, 0
|
local eof = false
|
return function(dst, sz)
|
if not dst then --skip bytes (libjpeg semantics)
|
local pos0, err, errcode = f:seek'cur'
|
if not pos0 then return nil, err, errcode end
|
local pos, err, errcode = f:seek('cur', sz)
|
if not pos then return nil, err, errcode end
|
return pos - pos0
|
end
|
local rsz = 0
|
while sz > 0 do
|
if len == 0 then
|
if eof then
|
return 0
|
end
|
ofs = 0
|
local len1, err, errcode = f:read(buf, bufsize)
|
if not len1 then return nil, err, errcode end
|
len = len1
|
if len == 0 then
|
eof = true
|
return rsz
|
end
|
end
|
local n = min(sz, len)
|
ffi.copy(ffi.cast(ptr_ct, dst) + rsz, buf + ofs, n)
|
ofs = ofs + n
|
len = len - n
|
rsz = rsz + n
|
sz = sz - n
|
end
|
return rsz
|
end
|
end
|
|
--stdio streams --------------------------------------------------------------
|
|
cdef[[
|
typedef struct FILE FILE;
|
int fclose(FILE*);
|
]]
|
|
local stream_ct = ffi.typeof'struct FILE'
|
|
function stream.close(fs)
|
local ok = C.fclose(fs) == 0
|
if not ok then return check_errno() end
|
ffi.gc(fs, nil)
|
return true
|
end
|
|
--i/o ------------------------------------------------------------------------
|
|
local whences = {set = 0, cur = 1, ['end'] = 2} --FILE_*
|
function file.seek(f, whence, offset)
|
if tonumber(whence) and not offset then --middle arg missing
|
whence, offset = 'cur', tonumber(whence)
|
end
|
whence = whence or 'cur'
|
offset = tonumber(offset or 0)
|
whence = assert(whences[whence], 'invalid whence: "%s"', whence)
|
return f._seek(f, whence, offset)
|
end
|
|
-- end fs/common.lua
|
|
if win then
|
|
--types, consts, utils -------------------------------------------------------
|
|
if x64 then
|
cdef'typedef int64_t ULONG_PTR;'
|
else
|
cdef'typedef int32_t ULONG_PTR;'
|
end
|
|
cdef[[
|
typedef void VOID, *PVOID, *LPVOID;
|
typedef VOID* HANDLE;
|
typedef unsigned short WORD;
|
typedef unsigned long DWORD, *PDWORD, *LPDWORD;
|
typedef unsigned int UINT;
|
typedef int BOOL;
|
typedef ULONG_PTR SIZE_T;
|
typedef const void* LPCVOID;
|
typedef char* LPSTR;
|
typedef const char* LPCSTR;
|
typedef wchar_t WCHAR;
|
typedef WCHAR* LPWSTR;
|
typedef const WCHAR* LPCWSTR;
|
typedef BOOL *LPBOOL;
|
typedef void* HMODULE;
|
typedef unsigned char UCHAR;
|
typedef unsigned short USHORT;
|
typedef long LONG;
|
typedef unsigned long ULONG;
|
typedef long long LONGLONG;
|
|
typedef union {
|
struct {
|
DWORD LowPart;
|
LONG HighPart;
|
};
|
struct {
|
DWORD LowPart;
|
LONG HighPart;
|
} u;
|
LONGLONG QuadPart;
|
} LARGE_INTEGER, *PLARGE_INTEGER;
|
|
typedef struct {
|
DWORD nLength;
|
LPVOID lpSecurityDescriptor;
|
BOOL bInheritHandle;
|
} SECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
|
]]
|
|
local INVALID_HANDLE_VALUE = ffi.cast('HANDLE', -1)
|
|
local wbuf = mkbuf'WCHAR'
|
local libuf = ffi.new'LARGE_INTEGER[1]'
|
|
--error reporting ------------------------------------------------------------
|
|
cdef[[
|
DWORD GetLastError(void);
|
|
DWORD FormatMessageA(
|
DWORD dwFlags,
|
LPCVOID lpSource,
|
DWORD dwMessageId,
|
DWORD dwLanguageId,
|
LPSTR lpBuffer,
|
DWORD nSize,
|
va_list *Arguments
|
);
|
]]
|
|
local FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
|
local errbuf = mkbuf'char'
|
|
local error_classes = {
|
[0x002] = 'File not found', --ERROR_FILE_NOT_FOUND, CreateFileW
|
[0x003] = 'Path not found', --ERROR_PATH_NOT_FOUND, CreateDirectoryW
|
[0x005] = 'Access denied', --ERROR_ACCESS_DENIED, CreateFileW
|
[0x050] = 'File exists', --ERROR_FILE_EXISTS, CreateFileW
|
[0x091] = 'Directory not empty', --ERROR_DIR_NOT_EMPTY, RemoveDirectoryW
|
[0x0b7] = 'Already exists', --ERROR_ALREADY_EXISTS, CreateDirectoryW
|
[0x10B] = 'Directory not found', --ERROR_DIRECTORY, FindFirstFileW
|
|
--TODO: mmap
|
[0x0008] = 'File too short', --readonly file too short
|
[0x0057] = 'Out of mem', --size or address too large
|
[0x0070] = 'Sisk full',
|
[0x01E7] = 'Address in use', --address in use
|
[0x03ee] = 'File too short', --file has zero size
|
[0x05af] = 'Out of mem', --swapfile too short
|
|
}
|
|
local function check(ret, errcode)
|
if ret then return ret end
|
errcode = errcode or C.GetLastError()
|
local buf, bufsz = errbuf(256)
|
local sz = C.FormatMessageA(
|
FORMAT_MESSAGE_FROM_SYSTEM, nil, errcode, 0, buf, bufsz, nil)
|
local err =
|
error_classes[errcode]
|
or (sz > 0
|
and ffi.string(buf, sz):gsub('[\r\n]+$', '')
|
or 'Error '..errcode)
|
return ret, err, errcode
|
end
|
|
--utf16/utf8 conversion ------------------------------------------------------
|
|
cdef[[
|
int MultiByteToWideChar(
|
UINT CodePage,
|
DWORD dwFlags,
|
LPCSTR lpMultiByteStr,
|
int cbMultiByte,
|
LPWSTR lpWideCharStr,
|
int cchWideChar
|
);
|
int WideCharToMultiByte(
|
UINT CodePage,
|
DWORD dwFlags,
|
LPCWSTR lpWideCharStr,
|
int cchWideChar,
|
LPSTR lpMultiByteStr,
|
int cbMultiByte,
|
LPCSTR lpDefaultChar,
|
LPBOOL lpUsedDefaultChar
|
);
|
]]
|
|
local CP_UTF8 = 65001
|
|
local wcsbuf = mkbuf'WCHAR'
|
|
local function wcs(s, msz, wbuf) --string -> WCHAR[?]
|
msz = msz and msz + 1 or #s + 1
|
wbuf = wbuf or wcsbuf
|
local wsz = C.MultiByteToWideChar(CP_UTF8, 0, s, msz, nil, 0)
|
assert(wsz > 0) --should never happen otherwise
|
local buf = wbuf(wsz)
|
local sz = C.MultiByteToWideChar(CP_UTF8, 0, s, msz, buf, wsz)
|
assert(sz == wsz) --should never happen otherwise
|
return buf
|
end
|
|
--open/close -----------------------------------------------------------------
|
|
cdef[[
|
HANDLE CreateFileW(
|
LPCWSTR lpFileName,
|
DWORD dwDesiredAccess,
|
DWORD dwShareMode,
|
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
|
DWORD dwCreationDisposition,
|
DWORD dwFlagsAndAttributes,
|
HANDLE hTemplateFile
|
);
|
BOOL CloseHandle(HANDLE hObject);
|
]]
|
|
--CreateFile access rights flags
|
local t = {
|
--FILE_* (specific access rights)
|
list_directory = 1, --dirs: allow listing
|
read_data = 1, --files: allow reading data
|
add_file = 2, --dirs: allow creating files
|
write_data = 2, --files: allow writting data
|
add_subdirectory = 4, --dirs: allow creating subdirs
|
append_data = 4, --files: allow appending data
|
create_pipe_instance = 4, --pipes: allow creating a pipe
|
delete_child = 0x40, --dirs: allow deleting dir contents
|
traverse = 0x20, --dirs: allow traversing (not effective)
|
execute = 0x20, --exes: allow exec'ing
|
read_attributes = 0x80, --allow reading attrs
|
write_attributes = 0x100, --allow setting attrs
|
read_ea = 8, --allow reading extended attrs
|
write_ea = 0x10, --allow writting extended attrs
|
--object's standard access rights
|
delete = 0x00010000,
|
read_control = 0x00020000, --allow r/w the security descriptor
|
write_dac = 0x00040000,
|
write_owner = 0x00080000,
|
synchronize = 0x00100000,
|
--STANDARD_RIGHTS_*
|
standard_rights_required = 0x000F0000,
|
standard_rights_read = 0x00020000, --read_control
|
standard_rights_write = 0x00020000, --read_control
|
standard_rights_execute = 0x00020000, --read_control
|
standard_rights_all = 0x001F0000,
|
--GENERIC_*
|
generic_read = 0x80000000,
|
generic_write = 0x40000000,
|
generic_execute = 0x20000000,
|
generic_all = 0x10000000,
|
}
|
--FILE_ALL_ACCESS
|
t.all_access = bit.bor(
|
t.standard_rights_required,
|
t.synchronize,
|
0x1ff)
|
--FILE_GENERIC_*
|
t.read = bit.bor(
|
t.standard_rights_read,
|
t.read_data,
|
t.read_attributes,
|
t.read_ea,
|
t.synchronize)
|
t.write = bit.bor(
|
t.standard_rights_write,
|
t.write_data,
|
t.write_attributes,
|
t.write_ea,
|
t.append_data,
|
t.synchronize)
|
t.execute = bit.bor(
|
t.standard_rights_execute,
|
t.read_attributes,
|
t.execute,
|
t.synchronize)
|
local access_bits = t
|
|
--CreateFile sharing flags
|
local sharing_bits = {
|
--FILE_SHARE_*
|
read = 0x00000001, --allow us/others to read
|
write = 0x00000002, --allow us/others to write
|
delete = 0x00000004, --allow us/others to delete or rename
|
}
|
|
--CreateFile creation disposition flags
|
local creation_bits = {
|
create_new = 1, --create or fail
|
create_always = 2, --open or create + truncate
|
open_existing = 3, --open or fail
|
open_always = 4, --open or create
|
truncate_existing = 5, --open + truncate or fail
|
}
|
|
local FILE_ATTRIBUTE_NORMAL = 0x00000080 --for when no bits are set
|
|
--CreateFile flags & attributes
|
local attr_bits = {
|
--FILE_ATTRIBUTE_*
|
readonly = 0x00000001,
|
hidden = 0x00000002,
|
system = 0x00000004,
|
archive = 0x00000020,
|
temporary = 0x00000100,
|
sparse_file = 0x00000200,
|
reparse_point = 0x00000400,
|
compressed = 0x00000800,
|
directory = 0x00000010,
|
device = 0x00000040,
|
--offline = 0x00001000, --reserved (used by Remote Storage)
|
not_indexed = 0x00002000, --FILE_ATTRIBUTE_NOT_CONTENT_INDEXED
|
encrypted = 0x00004000,
|
--virtual = 0x00010000, --reserved
|
}
|
|
local flag_bits = {
|
--FILE_FLAG_*
|
write_through = 0x80000000,
|
overlapped = 0x40000000,
|
no_buffering = 0x20000000,
|
random_access = 0x10000000,
|
sequential_scan = 0x08000000,
|
delete_on_close = 0x04000000,
|
backup_semantics = 0x02000000,
|
posix_semantics = 0x01000000,
|
open_reparse_point = 0x00200000,
|
open_no_recall = 0x00100000,
|
first_pipe_instance = 0x00080000,
|
}
|
|
local str_opt = {
|
r = {
|
access = 'read',
|
creation = 'open_existing',
|
flags = 'backup_semantics'},
|
w = {
|
access = 'write file_read_attributes',
|
creation = 'create_always',
|
flags = 'backup_semantics'},
|
['r+'] = {
|
access = 'read write',
|
creation = 'open_existing',
|
flags = 'backup_semantics'},
|
['w+'] = {
|
access = 'read write',
|
creation = 'create_always',
|
flags = 'backup_semantics'},
|
}
|
|
--expose this because the frontend will set its metatype at the end.
|
local file_ct = ffi.typeof[[
|
struct {
|
HANDLE handle;
|
}
|
]]
|
|
function fs.open(path, opt)
|
opt = opt or 'r'
|
if type(opt) == 'string' then
|
opt = assert(str_opt[opt], 'invalid option %s', opt)
|
end
|
local access = flags(opt.access or 'read', access_bits)
|
local sharing = flags(opt.sharing or 'read', sharing_bits)
|
local creation = flags(opt.creation or 'open_existing', creation_bits)
|
local attrbits = flags(opt.attrs, attr_bits)
|
attrbits = attrbits == 0 and FILE_ATTRIBUTE_NORMAL or attrbits
|
local flagbits = flags(opt.flags, flag_bits)
|
local attflags = bit.bor(attrbits, flagbits)
|
local h = C.CreateFileW(
|
wcs(path), access, sharing, nil, creation, attflags, nil)
|
if h == INVALID_HANDLE_VALUE then return check() end
|
return ffi.gc(file_ct(h), file.close)
|
end
|
|
function file.closed(f)
|
return f.handle == INVALID_HANDLE_VALUE
|
end
|
|
function file.close(f)
|
if f:closed() then return end
|
local ret = C.CloseHandle(f.handle)
|
if ret == 0 then return check(false) end
|
f.handle = INVALID_HANDLE_VALUE
|
ffi.gc(f, nil)
|
return true
|
end
|
|
function fs.wrap_handle(h)
|
return file_ct(h)
|
end
|
|
cdef[[
|
int _fileno(struct FILE *stream);
|
HANDLE _get_osfhandle(int fd);
|
]]
|
|
function fs.wrap_fd(fd)
|
local h = C._get_osfhandle(fd)
|
if h == nil then return check_errno() end
|
return fs.wrap_handle(h)
|
end
|
|
function fs.fileno(file)
|
local fd = C._fileno(file)
|
return check_errno(fd ~= -1 and fd or nil)
|
end
|
|
function fs.wrap_file(file)
|
local fd, err, errno = fs.fileno(file)
|
if not fd then return nil, err, errno end
|
return fs.wrap_fd(fd)
|
end
|
|
-- the following definitions are borrowed with few tweaks from
|
-- https://github.com/malkia/luajit-winapi/blob/master/ffi/winapi/windows/shell32.lua
|
|
cdef[[
|
typedef unsigned char BYTE;
|
# pragma pack( push, 1 )
|
typedef struct SHITEMID {
|
USHORT cb;
|
BYTE abID[1];
|
} SHITEMID;
|
# pragma pack( pop )
|
# pragma pack( push, 1 )
|
typedef struct ITEMIDLIST {
|
SHITEMID mkid;
|
} ITEMIDLIST;
|
# pragma pack( pop )
|
typedef ITEMIDLIST *PIDLIST_ABSOLUTE; //Pointer
|
typedef ITEMIDLIST *PCIDLIST_ABSOLUTE; //Pointer
|
typedef ITEMIDLIST *PIDLIST_RELATIVE; //Pointer
|
typedef ITEMIDLIST *PCUITEMID_CHILD; //Pointer
|
typedef PCUITEMID_CHILD *PCUITEMID_CHILD_ARRAY; //Pointer
|
typedef int32_t HRESULT; //Integer
|
|
HRESULT SHOpenFolderAndSelectItems(
|
PCIDLIST_ABSOLUTE pidlFolder, UINT cidl, PCUITEMID_CHILD_ARRAY* apidl, DWORD dwFlags);
|
PIDLIST_ABSOLUTE ILCreateFromPath(LPCWSTR pszPath);
|
void ILFree(PIDLIST_RELATIVE pidl);
|
]]
|
|
function fs.shell_open_and_select(path)
|
local shell = ffi.load('Shell32.dll')
|
local pidl = shell.ILCreateFromPath(wcs(path))
|
if pidl then
|
shell.SHOpenFolderAndSelectItems(pidl, 0, nil, 0)
|
shell.ILFree(pidl)
|
end
|
end
|
|
cdef[[
|
void OutputDebugStringW(LPCWSTR lpOutputString);
|
]]
|
|
function fs.output_debug_string(str)
|
local kernel = ffi.load('Kernel32.dll')
|
kernel.OutputDebugStringW(wcs(str))
|
end
|
|
--stdio streams --------------------------------------------------------------
|
|
cdef[[
|
FILE *_fdopen(int fd, const char *mode);
|
int _open_osfhandle (HANDLE osfhandle, int flags);
|
]]
|
|
function file.stream(f, mode)
|
local flags = 0
|
local fd = C._open_osfhandle(f.handle, flags)
|
if fd == -1 then return check_errno() end
|
local fs = C._fdopen(fd, mode)
|
if fs == nil then return check_errno() end
|
ffi.gc(f, nil) --fclose() will close the handle
|
ffi.gc(fs, stream.close)
|
return fs
|
end
|
|
--i/o ------------------------------------------------------------------------
|
|
cdef[[
|
typedef struct _OVERLAPPED {
|
ULONG_PTR Internal;
|
ULONG_PTR InternalHigh;
|
union {
|
struct {
|
DWORD Offset;
|
DWORD OffsetHigh;
|
};
|
PVOID Pointer;
|
};
|
HANDLE hEvent;
|
} OVERLAPPED, *LPOVERLAPPED;
|
|
typedef struct _OVERLAPPED_ENTRY {
|
ULONG_PTR lpCompletionKey;
|
LPOVERLAPPED lpOverlapped;
|
ULONG_PTR Internal;
|
DWORD dwNumberOfBytesTransferred;
|
} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;
|
|
BOOL ReadFile(
|
HANDLE hFile,
|
LPVOID lpBuffer,
|
DWORD nNumberOfBytesToRead,
|
LPDWORD lpNumberOfBytesRead,
|
LPOVERLAPPED lpOverlapped
|
);
|
|
BOOL WriteFile(
|
HANDLE hFile,
|
LPCVOID lpBuffer,
|
DWORD nNumberOfBytesToWrite,
|
LPDWORD lpNumberOfBytesWritten,
|
LPOVERLAPPED lpOverlapped
|
);
|
|
BOOL FlushFileBuffers(HANDLE hFile);
|
|
BOOL SetFilePointerEx(
|
HANDLE hFile,
|
LARGE_INTEGER liDistanceToMove,
|
PLARGE_INTEGER lpNewFilePointer,
|
DWORD dwMoveMethod
|
);
|
]]
|
|
local dwbuf = ffi.new'DWORD[1]'
|
|
function file.read(f, buf, sz)
|
local ok = C.ReadFile(f.handle, buf, sz, dwbuf, nil) ~= 0
|
if not ok then return check() end
|
return dwbuf[0]
|
end
|
|
function file.write(f, buf, sz)
|
local ok = C.WriteFile(f.handle, buf, sz or #buf, dwbuf, nil) ~= 0
|
if not ok then return check() end
|
return dwbuf[0]
|
end
|
|
function file.flush(f)
|
return check(C.FlushFileBuffers(f.handle) ~= 0)
|
end
|
|
local ofsbuf = ffi.new'LARGE_INTEGER[1]'
|
function file._seek(f, whence, offset)
|
ofsbuf[0].QuadPart = offset
|
local ok = C.SetFilePointerEx(f.handle, ofsbuf[0], libuf, whence) ~= 0
|
if not ok then return check() end
|
return tonumber(libuf[0].QuadPart)
|
end
|
|
--truncate/getsize/setsize ---------------------------------------------------
|
|
cdef[[
|
BOOL SetEndOfFile(HANDLE hFile);
|
BOOL GetFileSizeEx(HANDLE hFile, PLARGE_INTEGER lpFileSize);
|
]]
|
|
--NOTE: seeking beyond file size and then truncating the file incurs no delay
|
--on NTFS, but that's not because the file becomes sparse (it doesn't, and
|
--disk space _is_ reserved), but because the extra zero bytes are not written
|
--until the first write call _that requires it_. This is a good optimization
|
--since usually the file will be written sequentially after the truncation
|
--in which case those extra zero bytes will never get a chance to be written.
|
function file.truncate(f, opt)
|
return check(C.SetEndOfFile(f.handle) ~= 0)
|
end
|
|
function file_getsize(f)
|
local ok = C.GetFileSizeEx(f.handle, libuf) ~= 0
|
if not ok then return check() end
|
return tonumber(libuf[0].QuadPart)
|
end
|
|
--filesystem operations ------------------------------------------------------
|
|
cdef[[
|
BOOL CreateDirectoryW(LPCWSTR, LPSECURITY_ATTRIBUTES);
|
BOOL RemoveDirectoryW(LPCWSTR);
|
int SetCurrentDirectoryW(LPCWSTR lpPathName);
|
DWORD GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer);
|
BOOL DeleteFileW(LPCWSTR lpFileName);
|
BOOL MoveFileExW(
|
LPCWSTR lpExistingFileName,
|
LPCWSTR lpNewFileName,
|
DWORD dwFlags
|
);
|
]]
|
|
local move_bits = {
|
--MOVEFILE_*
|
replace_existing = 0x1,
|
copy_allowed = 0x2,
|
delay_until_reboot = 0x4,
|
fail_if_not_trackable = 0x20,
|
write_through = 0x8, --for when copy_allowed
|
}
|
|
--TODO: MoveFileExW is actually NOT atomic.
|
--Use SetFileInformationByHandle with FILE_RENAME_INFO and ReplaceIfExists
|
--which is atomic and also works on open handles which is even more atomic :)
|
local default_move_opt = 'replace_existing write_through' --posix
|
function fs.move(oldpath, newpath, opt)
|
return check(C.MoveFileExW(
|
wcs(oldpath),
|
wcs(newpath, nil, wbuf),
|
flags(opt or default_move_opt, move_bits)
|
) ~= 0)
|
end
|
|
--symlinks & hardlinks -------------------------------------------------------
|
|
cdef[[
|
BOOL CreateSymbolicLinkW (
|
LPCWSTR lpSymlinkFileName,
|
LPCWSTR lpTargetFileName,
|
DWORD dwFlags
|
);
|
BOOL CreateHardLinkW(
|
LPCWSTR lpFileName,
|
LPCWSTR lpExistingFileName,
|
LPSECURITY_ATTRIBUTES lpSecurityAttributes
|
);
|
|
BOOL DeviceIoControl(
|
HANDLE hDevice,
|
DWORD dwIoControlCode,
|
LPVOID lpInBuffer,
|
DWORD nInBufferSize,
|
LPVOID lpOutBuffer,
|
DWORD nOutBufferSize,
|
LPDWORD lpBytesReturned,
|
LPOVERLAPPED lpOverlapped
|
);
|
]]
|
|
local SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1
|
|
function fs.mksymlink(link_path, target_path, is_dir)
|
local flags = is_dir and SYMBOLIC_LINK_FLAG_DIRECTORY or 0
|
return check(C.CreateSymbolicLinkW(
|
wcs(link_path),
|
wcs(target_path, nil, wbuf),
|
flags) ~= 0)
|
end
|
|
function fs.mkhardlink(link_path, target_path)
|
return check(C.CreateHardLinkW(
|
wcs(link_path),
|
wcs(target_path, nil, wbuf),
|
nil) ~= 0)
|
end
|
|
cdef[[
|
DWORD GetFileAttributesW (
|
LPCWSTR lpFileName
|
);
|
]]
|
|
local INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
|
local FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
|
function fs.is_symlink(path)
|
if not path then return nil end
|
local flags = C.GetFileAttributesW(wcs(path))
|
if flags == INVALID_FILE_ATTRIBUTES then return false end
|
return bit.band(flags, FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT
|
end
|
|
ffi.metatype(file_ct, {__index = file})
|
ffi.metatype(stream_ct, {__index = stream})
|
|
else
|
error('platform not Windows')
|
end
|
|
return fs
|