├── .gitignore ├── .luacheckrc ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── dist.ini ├── lfs_ffi.lua ├── lfs_spec.lua ├── lock_unlock.lua ├── test_lock.lua ├── test_valgrind.lua ├── travis.sh └── valgrind.suppress /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | luafilesystem-* 43 | .luacheckcache 44 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | cache = true 2 | std = 'ngx_lua' 3 | files["*_spec.lua"].std = "+busted" 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | 4 | matrix: 5 | include: 6 | - os: linux 7 | env: LUA="luajit 2.0" 8 | - os: linux 9 | env: LUA="luajit 2.1" 10 | addons: 11 | apt: 12 | packages: 13 | - valgrind 14 | - os: osx 15 | language: generic 16 | env: LUA="luajit 2.1" 17 | 18 | before_install: 19 | - pip install hererocks 20 | - hererocks lua_install -r^ --$LUA # install all stuff into ./lua_install 21 | - export PATH=$PATH:$PWD/lua_install/bin 22 | 23 | install: 24 | - luarocks install luacheck 25 | - luarocks install busted 26 | 27 | script: 28 | - luacheck --std max+busted *.lua 29 | - busted --verbose . || exit 1 30 | - if [ "$LUA" = "luajit 2.1" ]; then bash travis.sh; fi 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 罗泽轩 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 3 | INSTALL ?= install 4 | 5 | .PHONY: all install 6 | 7 | all: ; 8 | 9 | install: all 10 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR) 11 | $(INSTALL) lfs_ffi.lua $(DESTDIR)/$(LUA_LIB_DIR) 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luafilesystem 2 | 3 | [![Build Status](https://travis-ci.org/spacewander/luafilesystem.svg?branch=master)](https://travis-ci.org/spacewander/luafilesystem) 4 | 5 | Reimplement luafilesystem via LuaJIT FFI. 6 | 7 | This project doesn't support Windows. If you need the Windows support, use 8 | https://github.com/sonoro1234/luafilesystem instead. 9 | 10 | ## Docs 11 | 12 | It should be compatible with vanilla luafilesystem: 13 | http://keplerproject.github.io/luafilesystem/manual.html#reference 14 | 15 | What you only need is replacing `require('lfs')` to `require('lfs_ffi')`.` 16 | 17 | ## Installation 18 | 19 | `[sudo] opm get spacewander/luafilesystem` 20 | 21 | Run `resty -e "lfs = require('lfs_ffi') print(lfs.attributes('.', 'mode'))"` to validate the installation. 22 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=luafilesystem 2 | abstract=Reimplement luafilesystem via LuaJIT FFI 3 | author=spacewander 4 | is_original=yes 5 | license=mit 6 | lib_dir=. 7 | doc_dir=. 8 | exclude_files=*_spec.lua lock_unlock.lua test_lock.lua 9 | repo_link=https://github.com/spacewander/luafilesystem 10 | -------------------------------------------------------------------------------- /lfs_ffi.lua: -------------------------------------------------------------------------------- 1 | local bit = require "bit" 2 | local ffi = require "ffi" 3 | 4 | 5 | local pairs = pairs 6 | local tonumber = tonumber 7 | local type = type 8 | local band = bit.band 9 | local rshift = bit.rshift 10 | local io_type = io.type 11 | local lib = ffi.C 12 | local ffi_errno = ffi.errno 13 | local ffi_new = ffi.new 14 | local ffi_str = ffi.string 15 | local concat = table.concat 16 | local has_table_new, new_tab = pcall(require, "table.new") 17 | if not has_table_new or type(new_tab) ~= "function" then 18 | new_tab = function () return {} end 19 | end 20 | 21 | 22 | local _M = { 23 | _VERSION = "0.3", 24 | } 25 | 26 | -- common utils/constants 27 | local IS_64_BIT = ffi.abi('64bit') 28 | local ERANGE = 'Result too large' 29 | local int = ffi.typeof("int") 30 | 31 | if not pcall(ffi.typeof, "ssize_t") then 32 | -- LuaJIT 2.0 doesn't have ssize_t as a builtin type, let's define it 33 | ffi.cdef("typedef intptr_t ssize_t") 34 | end 35 | 36 | ffi.cdef([[ 37 | char* strerror(int errnum); 38 | ]]) 39 | 40 | local function errno() 41 | return ffi_str(lib.strerror(ffi_errno())) 42 | end 43 | 44 | local OS = ffi.os 45 | if OS == "Windows" then 46 | error("Windows is not supported anymore. Use https://github.com/sonoro1234/luafilesystem instead.") 47 | end 48 | 49 | -- sys/syslimits.h 50 | local MAXPATH 51 | if OS == 'Linux' then 52 | MAXPATH = 4096 53 | else 54 | MAXPATH = 1024 55 | end 56 | 57 | -- misc 58 | ffi.cdef([[ 59 | char *getcwd(char *buf, size_t size); 60 | int chdir(const char *path); 61 | int rmdir(const char *pathname); 62 | typedef unsigned int mode_t; 63 | int mkdir(const char *pathname, mode_t mode); 64 | typedef size_t time_t; 65 | struct utimebuf { 66 | time_t actime; 67 | time_t modtime; 68 | }; 69 | int utime(const char *file, const struct utimebuf *times); 70 | int link(const char *oldpath, const char *newpath); 71 | int symlink(const char *oldpath, const char *newpath); 72 | ]]) 73 | 74 | function _M.chdir(path) 75 | if lib.chdir(path) == 0 then 76 | return true 77 | end 78 | return nil, errno() 79 | end 80 | 81 | function _M.currentdir() 82 | local size = MAXPATH 83 | while true do 84 | local buf = ffi_new("char[?]", size) 85 | if lib.getcwd(buf, size) ~= nil then 86 | return ffi_str(buf) 87 | end 88 | local err = errno() 89 | if err ~= ERANGE then 90 | return nil, err 91 | end 92 | size = size * 2 93 | end 94 | end 95 | 96 | function _M.mkdir(path, mode) 97 | if lib.mkdir(path, mode or 509) == 0 then 98 | return true 99 | end 100 | return nil, errno() 101 | end 102 | 103 | function _M.rmdir(path) 104 | if lib.rmdir(path) == 0 then 105 | return true 106 | end 107 | return nil, errno() 108 | end 109 | 110 | function _M.touch(path, actime, modtime) 111 | local buf 112 | 113 | if type(actime) == "number" then 114 | modtime = modtime or actime 115 | buf = ffi_new("struct utimebuf") 116 | buf.actime = actime 117 | buf.modtime = modtime 118 | end 119 | 120 | local p = ffi_new("unsigned char[?]", #path + 1, path) 121 | if lib.utime(p, buf) == 0 then 122 | return true 123 | end 124 | return nil, errno() 125 | end 126 | 127 | function _M.setmode() 128 | return true, "binary" 129 | end 130 | 131 | function _M.link(old, new, symlink) 132 | local f = symlink and lib.symlink or lib.link 133 | if f(old, new) == 0 then 134 | return true 135 | end 136 | return nil, errno() 137 | end 138 | 139 | local dirent_def 140 | if OS == 'OSX' or OS == 'BSD' then 141 | dirent_def = [[ 142 | /* _DARWIN_FEATURE_64_BIT_INODE is NOT defined here? */ 143 | struct dirent { 144 | uint32_t d_ino; 145 | uint16_t d_reclen; 146 | uint8_t d_type; 147 | uint8_t d_namlen; 148 | char d_name[256]; 149 | }; 150 | ]] 151 | else 152 | dirent_def = [[ 153 | struct dirent { 154 | int64_t d_ino; 155 | size_t d_off; 156 | unsigned short d_reclen; 157 | unsigned char d_type; 158 | char d_name[256]; 159 | }; 160 | ]] 161 | end 162 | 163 | ffi.cdef(dirent_def .. [[ 164 | typedef struct __dirstream DIR; 165 | DIR *opendir(const char *name); 166 | struct dirent *readdir(DIR *dirp); 167 | int closedir(DIR *dirp); 168 | ]]) 169 | 170 | local function close(dir) 171 | if dir._dentry ~= nil then 172 | lib.closedir(dir._dentry) 173 | dir._dentry = nil 174 | dir.closed = true 175 | end 176 | end 177 | 178 | local function iterator(dir) 179 | if dir.closed ~= false then error("closed directory") end 180 | 181 | local entry = lib.readdir(dir._dentry) 182 | if entry ~= nil then 183 | return ffi_str(entry.d_name) 184 | else 185 | close(dir) 186 | return nil 187 | end 188 | end 189 | 190 | local dir_obj_type = ffi.metatype([[ 191 | struct { 192 | DIR *_dentry; 193 | bool closed; 194 | } 195 | ]], 196 | {__index = { 197 | next = iterator, 198 | close = close, 199 | }, __gc = close 200 | }) 201 | 202 | function _M.dir(path) 203 | local dentry = lib.opendir(path) 204 | if dentry == nil then 205 | error("cannot open "..path.." : "..errno()) 206 | end 207 | local dir_obj = ffi_new(dir_obj_type) 208 | dir_obj._dentry = dentry 209 | dir_obj.closed = false; 210 | return iterator, dir_obj 211 | end 212 | 213 | local SEEK_SET = 0 214 | local F_SETLK = (OS == 'Linux') and 6 or 8 215 | local mode_ltype_map 216 | local flock_def 217 | if OS == 'Linux' then 218 | flock_def = [[ 219 | struct flock { 220 | short int l_type; 221 | short int l_whence; 222 | int64_t l_start; 223 | int64_t l_len; 224 | int l_pid; 225 | }; 226 | ]] 227 | mode_ltype_map = { 228 | r = 0, -- F_RDLCK 229 | w = 1, -- F_WRLCK 230 | u = 2, -- F_UNLCK 231 | } 232 | else 233 | flock_def = [[ 234 | struct flock { 235 | int64_t l_start; 236 | int64_t l_len; 237 | int32_t l_pid; 238 | short l_type; 239 | short l_whence; 240 | }; 241 | ]] 242 | mode_ltype_map = { 243 | r = 1, -- F_RDLCK 244 | u = 2, -- F_UNLCK 245 | w = 3, -- F_WRLCK 246 | } 247 | end 248 | 249 | ffi.cdef(flock_def..[[ 250 | int fileno(struct FILE *stream); 251 | int fcntl(int fd, int cmd, ... /* arg */ ); 252 | int unlink(const char *path); 253 | ]]) 254 | 255 | local function lock(fd, mode, start, len) 256 | local flock = ffi_new('struct flock') 257 | flock.l_type = mode_ltype_map[mode] 258 | flock.l_whence = SEEK_SET 259 | flock.l_start = start or 0 260 | flock.l_len = len or 0 261 | if lib.fcntl(fd, F_SETLK, flock) == -1 then 262 | return nil, errno() 263 | end 264 | return true 265 | end 266 | 267 | function _M.lock(filehandle, mode, start, length) 268 | if mode ~= 'r' and mode ~= 'w' then 269 | error("lock: invalid mode") 270 | end 271 | if io_type(filehandle) ~= 'file' then 272 | error("lock: invalid file") 273 | end 274 | local fd = lib.fileno(filehandle) 275 | local ok, err = lock(fd, mode, start, length) 276 | if not ok then 277 | return nil, err 278 | end 279 | return true 280 | end 281 | 282 | function _M.unlock(filehandle, start, length) 283 | if io_type(filehandle) ~= 'file' then 284 | error("unlock: invalid file") 285 | end 286 | local fd = lib.fileno(filehandle) 287 | local ok, err = lock(fd, 'u', start, length) 288 | if not ok then 289 | return nil, err 290 | end 291 | return true 292 | end 293 | 294 | -- lock related 295 | local dir_lock_struct = 'struct {char *lockname;}' 296 | local function create_lockfile(dir_lock, path, lockname) 297 | dir_lock.lockname = ffi_new('char[?]', #lockname + 1, lockname) 298 | return lib.symlink(path, lockname) == 0 299 | end 300 | 301 | local function delete_lockfile(dir_lock) 302 | return lib.unlink(dir_lock.lockname) 303 | end 304 | 305 | local function unlock_dir(dir_lock) 306 | if dir_lock.lockname ~= nil then 307 | dir_lock:delete_lockfile() 308 | dir_lock.lockname = nil 309 | end 310 | return true 311 | end 312 | 313 | local dir_lock_type = ffi.metatype(dir_lock_struct, 314 | {__gc = unlock_dir, 315 | __index = { 316 | free = unlock_dir, 317 | create_lockfile = create_lockfile, 318 | delete_lockfile = delete_lockfile, 319 | }} 320 | ) 321 | 322 | function _M.lock_dir(path, _) 323 | -- It's interesting that the lock_dir from vanilla lfs just ignores second parameter. 324 | -- So, I follow this behavior too :) 325 | local dir_lock = ffi_new(dir_lock_type) 326 | local lockname = path .. '/lockfile.lfs' 327 | if not dir_lock:create_lockfile(path, lockname) then 328 | return nil, errno() 329 | end 330 | return dir_lock 331 | end 332 | 333 | -- stat related 334 | local stat_func 335 | local lstat_func 336 | if OS == 'Linux' then 337 | ffi.cdef([[ 338 | long syscall(int number, ...); 339 | ]]) 340 | local ARCH = ffi.arch 341 | -- Taken from justincormack/ljsyscall 342 | local stat_syscall_num 343 | local lstat_syscall_num 344 | if ARCH == 'x64' then 345 | ffi.cdef([[ 346 | typedef struct { 347 | unsigned long st_dev; 348 | unsigned long st_ino; 349 | unsigned long st_nlink; 350 | unsigned int st_mode; 351 | unsigned int st_uid; 352 | unsigned int st_gid; 353 | unsigned int __pad0; 354 | unsigned long st_rdev; 355 | long st_size; 356 | long st_blksize; 357 | long st_blocks; 358 | unsigned long st_atime; 359 | unsigned long st_atime_nsec; 360 | unsigned long st_mtime; 361 | unsigned long st_mtime_nsec; 362 | unsigned long st_ctime; 363 | unsigned long st_ctime_nsec; 364 | long __unused[3]; 365 | } stat; 366 | ]]) 367 | stat_syscall_num = 4 368 | lstat_syscall_num = 6 369 | elseif ARCH == 'x86' then 370 | ffi.cdef([[ 371 | typedef struct { 372 | unsigned long long st_dev; 373 | unsigned char __pad0[4]; 374 | unsigned long __st_ino; 375 | unsigned int st_mode; 376 | unsigned int st_nlink; 377 | unsigned long st_uid; 378 | unsigned long st_gid; 379 | unsigned long long st_rdev; 380 | unsigned char __pad3[4]; 381 | long long st_size; 382 | unsigned long st_blksize; 383 | unsigned long long st_blocks; 384 | unsigned long st_atime; 385 | unsigned long st_atime_nsec; 386 | unsigned long st_mtime; 387 | unsigned int st_mtime_nsec; 388 | unsigned long st_ctime; 389 | unsigned long st_ctime_nsec; 390 | unsigned long long st_ino; 391 | } stat; 392 | ]]) 393 | stat_syscall_num = IS_64_BIT and 106 or 195 394 | lstat_syscall_num = IS_64_BIT and 107 or 196 395 | elseif ARCH == 's390x' then 396 | ffi.cdef([[ 397 | typedef struct { 398 | unsigned long st_dev; 399 | unsigned long st_ino; 400 | unsigned long st_nlink; 401 | unsigned int st_mode; 402 | unsigned int st_uid; 403 | unsigned int st_gid; 404 | int __pad1; 405 | unsigned long st_rdev; 406 | long st_size; 407 | long st_atime; 408 | long st_atime_nsec; 409 | long st_mtime; 410 | long st_mtime_nsec; 411 | long st_ctime; 412 | long st_ctime_nsec; 413 | long st_blksize; 414 | long st_blocks; 415 | long __unused[3]; 416 | } stat; 417 | ]]) 418 | stat_syscall_num = 106 419 | lstat_syscall_num = 107 420 | elseif ARCH == 'arm' then 421 | if IS_64_BIT then 422 | ffi.cdef([[ 423 | typedef struct { 424 | unsigned long st_dev; 425 | unsigned long st_ino; 426 | unsigned int st_mode; 427 | unsigned int st_nlink; 428 | unsigned int st_uid; 429 | unsigned int st_gid; 430 | unsigned long st_rdev; 431 | unsigned long __pad1; 432 | long st_size; 433 | int st_blksize; 434 | int __pad2; 435 | long st_blocks; 436 | long st_atime; 437 | unsigned long st_atime_nsec; 438 | long st_mtime; 439 | unsigned long st_mtime_nsec; 440 | long st_ctime; 441 | unsigned long st_ctime_nsec; 442 | unsigned int __unused4; 443 | unsigned int __unused5; 444 | } stat; 445 | ]]) 446 | stat_syscall_num = 106 447 | lstat_syscall_num = 107 448 | else 449 | ffi.cdef([[ 450 | typedef struct { 451 | unsigned long long st_dev; 452 | unsigned char __pad0[4]; 453 | unsigned long __st_ino; 454 | unsigned int st_mode; 455 | unsigned int st_nlink; 456 | unsigned long st_uid; 457 | unsigned long st_gid; 458 | unsigned long long st_rdev; 459 | unsigned char __pad3[4]; 460 | long long st_size; 461 | unsigned long st_blksize; 462 | unsigned long long st_blocks; 463 | unsigned long st_atime; 464 | unsigned long st_atime_nsec; 465 | unsigned long st_mtime; 466 | unsigned int st_mtime_nsec; 467 | unsigned long st_ctime; 468 | unsigned long st_ctime_nsec; 469 | unsigned long long st_ino; 470 | } stat; 471 | ]]) 472 | stat_syscall_num = 195 473 | lstat_syscall_num = 196 474 | end 475 | elseif ARCH == 'ppc64le' then 476 | ffi.cdef([[ 477 | typedef struct { 478 | unsigned long st_dev; 479 | unsigned long st_ino; 480 | unsigned long st_nlink; 481 | unsigned int st_mode; 482 | unsigned int st_uid; 483 | unsigned int st_gid; 484 | int __pad2; 485 | unsigned long st_rdev; 486 | long st_size; 487 | long st_blksize; 488 | long st_blocks; 489 | long st_atime; 490 | long st_atime_nsec; 491 | long st_mtime; 492 | long st_mtime_nsec; 493 | long st_ctime; 494 | long st_ctime_nsec; 495 | unsigned long __unused4; 496 | unsigned long __unused5; 497 | unsigned long __unused6; 498 | } stat; 499 | ]]) 500 | stat_syscall_num = 106 501 | lstat_syscall_num = 107 502 | elseif ARCH == 'ppc' or ARCH == 'ppcspe' then 503 | ffi.cdef([[ 504 | typedef struct { 505 | unsigned long long st_dev; 506 | unsigned long long st_ino; 507 | unsigned int st_mode; 508 | unsigned int st_nlink; 509 | unsigned int st_uid; 510 | unsigned int st_gid; 511 | unsigned long long st_rdev; 512 | unsigned long long __pad1; 513 | long long st_size; 514 | int st_blksize; 515 | int __pad2; 516 | long long st_blocks; 517 | int st_atime; 518 | unsigned int st_atime_nsec; 519 | int st_mtime; 520 | unsigned int st_mtime_nsec; 521 | int st_ctime; 522 | unsigned int st_ctime_nsec; 523 | unsigned int __unused4; 524 | unsigned int __unused5; 525 | } stat; 526 | ]]) 527 | stat_syscall_num = IS_64_BIT and 106 or 195 528 | lstat_syscall_num = IS_64_BIT and 107 or 196 529 | elseif ARCH == 'mips' or ARCH == 'mipsel' then 530 | ffi.cdef([[ 531 | typedef struct { 532 | unsigned long st_dev; 533 | unsigned long __st_pad0[3]; 534 | unsigned long long st_ino; 535 | mode_t st_mode; 536 | nlink_t st_nlink; 537 | uid_t st_uid; 538 | gid_t st_gid; 539 | unsigned long st_rdev; 540 | unsigned long __st_pad1[3]; 541 | long long st_size; 542 | time_t st_atime; 543 | unsigned long st_atime_nsec; 544 | time_t st_mtime; 545 | unsigned long st_mtime_nsec; 546 | time_t st_ctime; 547 | unsigned long st_ctime_nsec; 548 | unsigned long st_blksize; 549 | unsigned long __st_pad2; 550 | long long st_blocks; 551 | long __st_padding4[14]; 552 | } stat; 553 | ]]) 554 | stat_syscall_num = IS_64_BIT and 4106 or 4213 555 | lstat_syscall_num = IS_64_BIT and 4107 or 4214 556 | end 557 | 558 | if ARCH == 'arm64' then 559 | ffi.cdef([[ 560 | typedef struct { 561 | unsigned long st_dev; 562 | unsigned long st_ino; 563 | unsigned int st_mode; 564 | unsigned int st_nlink; 565 | unsigned int st_uid; 566 | unsigned int st_gid; 567 | unsigned long st_rdev; 568 | unsigned long __pad1; 569 | long st_size; 570 | int st_blksize; 571 | int __pad2; 572 | long st_blocks; 573 | long st_atime; 574 | unsigned long st_atime_nsec; 575 | long st_mtime; 576 | unsigned long st_mtime_nsec; 577 | long st_ctime; 578 | unsigned long st_ctime_nsec; 579 | unsigned int __unused4; 580 | unsigned int __unused5; 581 | } stat; 582 | ]]) 583 | 584 | local STAT = { 585 | _AT_FDCWD = -0x64, 586 | _AT_REMOVEDIR = 0x200, 587 | _AT_SYMLINK_NOFOLLOW = 0x100, 588 | _AT_EACCESS = 0x200, 589 | } 590 | 591 | -- On arm64 stat and lstat do not exist as syscall, so use newfstatat instead 592 | -- int newfstatat(int dirfd, const char *filename, struct stat *statbuf, int flags) 593 | stat_func = function(filepath, buf) 594 | return lib.syscall(79, int(STAT._AT_FDCWD), filepath, buf, 0) 595 | end 596 | lstat_func = function(filepath, buf) 597 | return lib.syscall(79, int(STAT._AT_FDCWD), filepath, buf, int(STAT._AT_SYMLINK_NOFOLLOW)) 598 | end 599 | elseif stat_syscall_num then 600 | stat_func = function(filepath, buf) 601 | return lib.syscall(stat_syscall_num, filepath, buf) 602 | end 603 | lstat_func = function(filepath, buf) 604 | return lib.syscall(lstat_syscall_num, filepath, buf) 605 | end 606 | else 607 | ffi.cdef('typedef struct {} stat;') 608 | stat_func = function() error("TODO support other Linux architectures") end 609 | lstat_func = stat_func 610 | end 611 | 612 | elseif OS == 'OSX' then 613 | ffi.cdef([[ 614 | struct timespec { 615 | time_t tv_sec; 616 | long tv_nsec; 617 | }; 618 | typedef struct { 619 | uint32_t st_dev; 620 | uint16_t st_mode; 621 | uint16_t st_nlink; 622 | uint64_t st_ino; 623 | uint32_t st_uid; 624 | uint32_t st_gid; 625 | uint32_t st_rdev; 626 | struct timespec st_atimespec; 627 | struct timespec st_mtimespec; 628 | struct timespec st_ctimespec; 629 | struct timespec st_birthtimespec; 630 | int64_t st_size; 631 | int64_t st_blocks; 632 | int32_t st_blksize; 633 | uint32_t st_flags; 634 | uint32_t st_gen; 635 | int32_t st_lspare; 636 | int64_t st_qspare[2]; 637 | } stat; 638 | int stat64(const char *path, stat *buf); 639 | int lstat64(const char *path, stat *buf); 640 | ]]) 641 | stat_func = lib.stat64 642 | lstat_func = lib.lstat64 643 | else 644 | ffi.cdef('typedef struct {} stat;') 645 | stat_func = function() error('TODO: support other posix system') end 646 | lstat_func = stat_func 647 | end 648 | 649 | local STAT = { 650 | FMT = 0xF000, 651 | FSOCK = 0xC000, 652 | FLNK = 0xA000, 653 | FREG = 0x8000, 654 | FBLK = 0x6000, 655 | FDIR = 0x4000, 656 | FCHR = 0x2000, 657 | FIFO = 0x1000, 658 | } 659 | 660 | local ftype_name_map = { 661 | [STAT.FREG] = 'file', 662 | [STAT.FDIR] = 'directory', 663 | [STAT.FLNK] = 'link', 664 | [STAT.FSOCK] = 'socket', 665 | [STAT.FCHR] = 'char device', 666 | [STAT.FBLK] = "block device", 667 | [STAT.FIFO] = "named pipe", 668 | } 669 | 670 | local function mode_to_ftype(mode) 671 | local ftype = band(mode, STAT.FMT) 672 | return ftype_name_map[ftype] or 'other' 673 | end 674 | 675 | local function mode_to_perm(mode) 676 | local perm_bits = band(mode, tonumber(777, 8)) 677 | local perm = new_tab(9, 0) 678 | local i = 9 679 | while i > 0 do 680 | local perm_bit = band(perm_bits, 7) 681 | perm[i] = (band(perm_bit, 1) > 0 and 'x' or '-') 682 | perm[i-1] = (band(perm_bit, 2) > 0 and 'w' or '-') 683 | perm[i-2] = (band(perm_bit, 4) > 0 and 'r' or '-') 684 | i = i - 3 685 | perm_bits = rshift(perm_bits, 3) 686 | end 687 | return concat(perm) 688 | end 689 | 690 | local function time_or_timespec(time, timespec) 691 | local t = tonumber(time) 692 | if not t and timespec then 693 | t = tonumber(timespec.tv_sec) 694 | end 695 | return t 696 | end 697 | 698 | local attr_handlers = { 699 | access = function(st) return time_or_timespec(st.st_atime, st.st_atimespec) end, 700 | blksize = function(st) return tonumber(st.st_blksize) end, 701 | blocks = function(st) return tonumber(st.st_blocks) end, 702 | change = function(st) return time_or_timespec(st.st_ctime, st.st_ctimespec) end, 703 | dev = function(st) return tonumber(st.st_dev) end, 704 | gid = function(st) return tonumber(st.st_gid) end, 705 | ino = function(st) return tonumber(st.st_ino) end, 706 | mode = function(st) return mode_to_ftype(st.st_mode) end, 707 | modification = function(st) return time_or_timespec(st.st_mtime, st.st_mtimespec) end, 708 | nlink = function(st) return tonumber(st.st_nlink) end, 709 | permissions = function(st) return mode_to_perm(st.st_mode) end, 710 | rdev = function(st) return tonumber(st.st_rdev) end, 711 | size = function(st) return tonumber(st.st_size) end, 712 | uid = function(st) return tonumber(st.st_uid) end, 713 | } 714 | local mt = { 715 | __index = function(self, attr_name) 716 | local func = attr_handlers[attr_name] 717 | return func and func(self) 718 | end 719 | } 720 | local stat_type = ffi.metatype('stat', mt) 721 | 722 | -- Add target field for symlinkattributes, which is the absolute path of linked target 723 | local get_link_target_path 724 | if OS == 'Windows' then 725 | local ENOSYS = 40 726 | function get_link_target_path() 727 | return nil, "could not obtain link target: Function not implemented ",ENOSYS 728 | end 729 | else 730 | ffi.cdef('ssize_t readlink(const char *path, char *buf, size_t bufsize);') 731 | function get_link_target_path(link_path, statbuf) 732 | local size = statbuf.st_size 733 | size = size == 0 and MAXPATH or size 734 | local buf = ffi.new('char[?]', size + 1) 735 | local read = lib.readlink(link_path, buf, size) 736 | if read == -1 then 737 | return nil, "could not obtain link target: "..errno(), ffi.errno() 738 | end 739 | if read > size then 740 | return nil, "not enought size for readlink: "..errno(), ffi.errno() 741 | end 742 | buf[size] = 0 743 | return ffi_str(buf) 744 | end 745 | end 746 | 747 | local stat_buf = ffi.new(stat_type) 748 | local function attributes(filepath, attr, follow_symlink) 749 | local func = follow_symlink and stat_func or lstat_func 750 | if func(filepath, stat_buf) == -1 then 751 | return nil, string.format("cannot obtain information from file '%s' : %s", 752 | tostring(filepath),errno()), ffi.errno() 753 | end 754 | 755 | local atype = type(attr) 756 | if atype == 'string' then 757 | local value, err, errn 758 | if attr == 'target' and not follow_symlink then 759 | value, err, errn = get_link_target_path(filepath, stat_buf) 760 | return value, err, errn 761 | else 762 | value = stat_buf[attr] 763 | end 764 | if value == nil then 765 | error("invalid attribute name '" .. attr .. "'") 766 | end 767 | return value 768 | else 769 | local tab = (atype == 'table') and attr or {} 770 | for k, _ in pairs(attr_handlers) do 771 | tab[k] = stat_buf[k] 772 | end 773 | if not follow_symlink then 774 | tab.target = get_link_target_path(filepath, stat_buf) 775 | end 776 | return tab 777 | end 778 | end 779 | 780 | function _M.attributes(filepath, attr) 781 | return attributes(filepath, attr, true) 782 | end 783 | 784 | function _M.symlinkattributes(filepath, attr) 785 | return attributes(filepath, attr, false) 786 | end 787 | 788 | return _M 789 | -------------------------------------------------------------------------------- /lfs_spec.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | local vanilla_lfs = require('lfs') 3 | local lfs = require('./lfs_ffi') 4 | 5 | 6 | local eq = assert.are.same 7 | local is_nil = assert.is_nil 8 | local is_not_nil = assert.is_not_nil 9 | local is_true = assert.is_true 10 | local has_error = assert.has_error 11 | local posix = ffi.os ~= 'Windows' 12 | local linux = ffi.os == 'Linux' 13 | 14 | local attr_names = { 15 | 'access', 16 | 'change', 17 | 'dev', 18 | 'gid', 19 | 'ino', 20 | 'mode', 21 | 'modification', 22 | 'nlink', 23 | 'permissions', 24 | 'rdev', 25 | 'size', 26 | 'uid' 27 | } 28 | if posix then 29 | local extra_attrs = {'blksize', 'blocks'} 30 | for i = 1, #extra_attrs do 31 | table.insert(attr_names, extra_attrs[i]) 32 | end 33 | end 34 | 35 | describe('lfs', function() 36 | describe('#attributes', function() 37 | it('without argument', function() 38 | local info = lfs.attributes('.') 39 | eq(vanilla_lfs.attributes('.'), info) 40 | end) 41 | 42 | it('with attribute name', function() 43 | for i = 1, #attr_names do 44 | local attr = attr_names[i] 45 | local info = lfs.attributes('.', attr) 46 | eq(vanilla_lfs.attributes('.', attr), info, 47 | attr..' is not equal') 48 | end 49 | end) 50 | 51 | it('with attributes table', function() 52 | local tab = {"table", "for", "attributes"} 53 | local info = lfs.attributes('.', tab) 54 | eq(vanilla_lfs.attributes('.', tab), info) 55 | end) 56 | 57 | it('with nonexisted file', function() 58 | local info, err = lfs.attributes('nonexisted') 59 | is_nil(info) 60 | eq("cannot obtain information from file 'nonexisted' : No such file or directory", err) 61 | end) 62 | 63 | it('with nonexisted attribute', function() 64 | has_error(function() lfs.attributes('.', 'nonexisted') end, 65 | "invalid attribute name 'nonexisted'") 66 | if not posix then 67 | has_error(function() lfs.attributes('.', 'blocks') end, 68 | "invalid attribute name 'blocks'") 69 | end 70 | end) 71 | end) 72 | 73 | describe('#symlinkattributes', function() 74 | local symlink = 'lfs_ffi.lua.link' 75 | 76 | it('link failed', function() 77 | if posix then 78 | local res, err = lfs.link('xxx', symlink) 79 | is_nil(res) 80 | eq(err, 'No such file or directory') 81 | end 82 | end) 83 | 84 | it('hard link', function() 85 | local _, err = lfs.link('lfs_ffi.lua', symlink) 86 | is_nil(err) 87 | eq(vanilla_lfs.attributes(symlink, 'mode'), 'file') 88 | eq(vanilla_lfs.symlinkattributes(symlink, 'mode'), 'file') 89 | end) 90 | 91 | it('soft link', function() 92 | if posix then 93 | local _, err = lfs.link('lfs_ffi.lua', symlink, true) 94 | is_nil(err) 95 | eq(vanilla_lfs.attributes(symlink, 'mode'), 'file') 96 | eq(vanilla_lfs.symlinkattributes(symlink, 'mode'), 'link') 97 | end 98 | end) 99 | 100 | it('without argument', function() 101 | lfs.link('lfs_ffi.lua', symlink, true) 102 | local info = lfs.symlinkattributes(symlink) 103 | local expected_info = vanilla_lfs.symlinkattributes(symlink) 104 | for k, v in pairs(expected_info) do 105 | eq(v, info[k], k..'is not equal') 106 | end 107 | end) 108 | 109 | it('with attribute name', function() 110 | lfs.link('lfs_ffi.lua', symlink, true) 111 | for i = 1, #attr_names do 112 | local attr = attr_names[i] 113 | local info = lfs.symlinkattributes(symlink, attr) 114 | eq(vanilla_lfs.symlinkattributes(symlink, attr), info, 115 | attr..' is not equal') 116 | end 117 | end) 118 | 119 | it('add target field', function() 120 | if posix then 121 | lfs.link('lfs_ffi.lua', symlink, true) 122 | eq('lfs_ffi.lua', lfs.symlinkattributes(symlink, 'target')) 123 | eq('lfs_ffi.lua', lfs.symlinkattributes(symlink).target) 124 | end 125 | end) 126 | 127 | it('link with pseudo file', function() 128 | if linux then 129 | local pseudo_filename = '/proc/self' 130 | lfs.link(pseudo_filename, symlink, true) 131 | eq(pseudo_filename, lfs.symlinkattributes(symlink, 'target')) 132 | eq(pseudo_filename, lfs.symlinkattributes(symlink).target) 133 | end 134 | end) 135 | 136 | after_each(function() 137 | os.remove(symlink) 138 | end) 139 | end) 140 | 141 | describe('#setmode', function() 142 | local fh 143 | before_each(function() 144 | fh = io.open('lfs_ffi.lua') 145 | end) 146 | 147 | it('setmode', function() 148 | local ok, mode = lfs.setmode(fh, 'binary') 149 | is_true(ok) 150 | if posix then 151 | -- On posix platform, always return 'binary' 152 | eq('binary', mode) 153 | else 154 | eq( 'text', mode) 155 | local _ 156 | _, mode = lfs.setmode(fh, 'text') 157 | eq('binary', mode) 158 | end 159 | end) 160 | 161 | if not posix then 162 | it('setmode incorrect mode', function() 163 | has_error(function() lfs.setmode(fh, 'bin') end, 'setmode: invalid mode') 164 | end) 165 | 166 | it('setmode incorrect file', function() 167 | has_error(function() lfs.setmode('file', 'binary') end, 'setmode: invalid file') 168 | end) 169 | end 170 | end) 171 | 172 | describe('#dir', function() 173 | it('mkdir', function() 174 | lfs.mkdir('test') 175 | end) 176 | 177 | it('return err if mkdir failed', function() 178 | local res, err = lfs.mkdir('test') 179 | is_nil(res) 180 | eq('File exists', err) 181 | end) 182 | 183 | it('raise error if open dir failed', function() 184 | if posix then 185 | has_error(function() lfs.dir('nonexisted') end, 186 | "cannot open nonexisted : No such file or directory") 187 | else 188 | -- Like vanilla lfs, we only check path's length in Windows 189 | local ok, msg = pcall(function() lfs.dir(('12345'):rep(64)) end) 190 | is_true(not ok) 191 | is_not_nil(msg:find('path too long')) 192 | end 193 | end) 194 | 195 | if posix or os.getenv('CI') ~= 'True' then 196 | it('iterate dir', function() 197 | local _, dir_obj = lfs.dir('test') 198 | local names = {} 199 | while true do 200 | local name = dir_obj:next() 201 | if not name then break end 202 | names[#names + 1] = name 203 | end 204 | table.sort(names) 205 | eq({'.', '..'}, names) 206 | is_true(dir_obj.closed) 207 | end) 208 | 209 | it('iterate dir via iterator', function() 210 | local iter, dir_obj = lfs.dir('test') 211 | local names = {} 212 | while true do 213 | local name = iter(dir_obj) 214 | if not name then break end 215 | names[#names + 1] = name 216 | end 217 | table.sort(names) 218 | eq({'.', '..'}, names) 219 | is_true(dir_obj.closed) 220 | end) 221 | end 222 | 223 | it('close', function() 224 | local _, dir_obj = lfs.dir('.') 225 | dir_obj:close() 226 | has_error(function() dir_obj:next() end, "closed directory") 227 | end) 228 | 229 | it('chdir and currentdir', function() 230 | lfs.chdir('test') 231 | local cur_dir = lfs.currentdir() 232 | lfs.chdir('..') 233 | assert.is_not_nil(cur_dir:find('test$')) 234 | end) 235 | 236 | it('return err if chdir failed', function() 237 | local res, err = lfs.chdir('nonexisted') 238 | is_nil(res) 239 | eq('No such file or directory', err) 240 | end) 241 | 242 | it('rmdir', function() 243 | lfs.rmdir('test') 244 | end) 245 | 246 | it('return err if rmdir failed', function() 247 | local res, err = lfs.rmdir('test') 248 | is_nil(res) 249 | eq('No such file or directory', err) 250 | end) 251 | end) 252 | 253 | describe('#touch', function() 254 | local touched = 'temp' 255 | 256 | before_each(function() 257 | local f = io.open(touched, 'w') 258 | f:write('a') 259 | f:close() 260 | end) 261 | 262 | after_each(function() 263 | os.remove(touched) 264 | end) 265 | 266 | it('touch failed', function() 267 | local _, err = lfs.touch('nonexisted', 1) 268 | eq('No such file or directory', err) 269 | end) 270 | 271 | it('set atime', function() 272 | local _, err = lfs.touch(touched, 1) 273 | is_nil(err) 274 | eq(vanilla_lfs.attributes(touched, 'access'), 1) 275 | end) 276 | 277 | it('set both atime and mtime', function() 278 | local _, err = lfs.touch(touched, 1, 2) 279 | is_nil(err) 280 | eq(vanilla_lfs.attributes(touched, 'access'), 1) 281 | eq(vanilla_lfs.attributes(touched, 'modification'), 2) 282 | end) 283 | end) 284 | 285 | -- Just smoke testing 286 | describe('#lock', function() 287 | local fh 288 | setup(function() 289 | fh = io.open('temp.txt', 'w') 290 | fh:write('1234567890') 291 | fh:close() 292 | end) 293 | 294 | before_each(function() 295 | fh = io.open('temp.txt', 'r+') 296 | end) 297 | 298 | it('lock', function() 299 | local _, err = lfs.lock(fh, 'r', 2, 8) 300 | is_nil(err) 301 | end) 302 | 303 | it('lock exclusively', function() 304 | if posix then 305 | local _, err = lfs.lock(fh, 'w') 306 | is_nil(err) 307 | end 308 | end) 309 | 310 | it('lock: invalid mode', function() 311 | has_error(function() lfs.lock('temp.txt', 'u') end, 'lock: invalid mode') 312 | end) 313 | 314 | it('lock: invalid file', function() 315 | has_error(function() lfs.lock('temp.txt', 'w') end, 'lock: invalid file') 316 | end) 317 | 318 | it('unlock', function() 319 | local _, err = lfs.lock(fh, 'w', 4, 9) 320 | is_nil(err) 321 | if posix then 322 | _, err = lfs.unlock(fh, 3, 11) 323 | is_nil(err) 324 | else 325 | _, err = lfs.unlock(fh, 3, 11) 326 | eq('Permission denied', err) 327 | _, err = lfs.unlock(fh, 4, 9) 328 | is_nil(err) 329 | end 330 | end) 331 | 332 | it('unlock: invalid file', function() 333 | has_error(function() lfs.unlock('temp.txt') end, 'unlock: invalid file') 334 | end) 335 | 336 | after_each(function() 337 | fh:close() 338 | end) 339 | 340 | teardown(function() 341 | os.remove('temp.txt') 342 | end) 343 | end) 344 | 345 | describe('#lock_dir', function() 346 | it('lock_dir', function() 347 | local lock, err, _ 348 | lock, err = lfs.lock_dir('.') 349 | is_nil(err) 350 | lock:free() 351 | -- lock again after unlock 352 | _, err = lfs.lock_dir('.') 353 | is_nil(err) 354 | -- lock again without unlock 355 | _, err = lfs.lock_dir('.') 356 | is_not_nil(err) 357 | end) 358 | end) 359 | end) 360 | -------------------------------------------------------------------------------- /lock_unlock.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | local lfs = require('./lfs_ffi') 3 | 4 | local fh = io.open('temp.txt', 'r+') 5 | 6 | local start = os.clock() 7 | while true do 8 | local ok = lfs.lock(fh, 'w', 2, 7) 9 | if ok then 10 | print('get lock') 11 | break 12 | end 13 | if os.clock() - start > 5 then 14 | print('ERROR: timeout') 15 | return 16 | end 17 | end 18 | 19 | start = os.clock() 20 | while os.clock() - start < 3 do 21 | end 22 | 23 | print('unlock') 24 | local _, err = lfs.unlock(fh) 25 | print(err) 26 | -------------------------------------------------------------------------------- /test_lock.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local file = io.open('temp.txt', 'w') 4 | file:write('0123456789') 5 | file:close() 6 | 7 | io.popen('lua lock_unlock.lua') 8 | os.execute('lua lock_unlock.lua') 9 | 10 | os.remove('temp.txt') 11 | -------------------------------------------------------------------------------- /test_valgrind.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | -- This file drives valgrind test 4 | -- See .travis.yml for more detail. 5 | require "busted.runner"({ standalone = false }) 6 | -------------------------------------------------------------------------------- /travis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ./test_lock.lua > /tmp/lfs 5 | grep 'ERROR' /tmp/lfs && exit 1 6 | if [ "$(uname)" = "Linux" ]; then 7 | LUA=luajit 8 | test -z "$(which $LUA)" && LUA=$PWD/lua_install/bin/lua 9 | valgrind --error-exitcode=42 --tool=memcheck \ 10 | --gen-suppressions=all --suppressions=valgrind.suppress \ 11 | "$LUA" test_valgrind.lua . 12 | fi 13 | -------------------------------------------------------------------------------- /valgrind.suppress: -------------------------------------------------------------------------------- 1 | { 2 | 3 | Memcheck:Cond 4 | obj:* 5 | } 6 | --------------------------------------------------------------------------------