From e82eddd9e007597e8283922653de0e5abb94c0dc Mon Sep 17 00:00:00 2001 From: mitchell <70453897+orbitalquark@users.noreply.github.com> Date: Sun, 25 Oct 2020 00:43:25 -0400 Subject: Handle recursive symlinks in `lfs.walk()`. --- core/lfs_ext.lua | 17 +++++++++++------ test/test.lua | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/core/lfs_ext.lua b/core/lfs_ext.lua index c24ee2e9..5bbe609a 100644 --- a/core/lfs_ext.lua +++ b/core/lfs_ext.lua @@ -19,9 +19,14 @@ module('lfs')]] lfs.default_filter = {--[[Extensions]]'!.a','!.bmp','!.bz2','!.class','!.dll','!.exe','!.gif','!.gz','!.jar','!.jpeg','!.jpg','!.o','!.pdf','!.png','!.so','!.tar','!.tgz','!.tif','!.tiff','!.xz','!.zip',--[[Directories]]'!/%.bzr$','!/%.git$','!/%.hg$','!/%.svn$','!/_FOSSIL_$','!/node_modules$'} -- Documentation is in `lfs.walk()`. +-- @param seen Utility table that holds directories seen. If there is a +-- duplicate, stop walking down that path (it's probably a recursive symlink). -- @param level Utility value indicating the directory level this function is -- at. -local function walk(dir, filter, n, include_dirs, level) +local function walk(dir, filter, n, include_dirs, seen, level) + local sep = not WIN32 and '/' or '\\' + local os_dir = not WIN32 and dir or dir:gsub('/', sep) + seen[os_dir], seen[os_dir .. sep] = true, true for basename in lfs.dir(dir) do if basename:find('^%.%.?$') then goto continue end -- ignore . and .. local filename = dir .. (dir ~= '/' and '/' or '') .. basename @@ -42,14 +47,14 @@ local function walk(dir, filter, n, include_dirs, level) include = include or (not patt:find('^!') and filename:find(patt)) end if not include then goto continue end - local sep = not WIN32 and '/' or '\\' local os_filename = not WIN32 and filename or filename:gsub('/', sep) if mode == 'file' then coroutine.yield(os_filename) - elseif mode == 'directory' then + elseif mode == 'directory' and + not seen[lfs.symlinkattributes(filename, 'target')] then if include_dirs then coroutine.yield(os_filename .. sep) end if n and (level or 0) >= n then goto continue end - walk(filename, filter, n, include_dirs, (level or 0) + 1) + walk(filename, filter, n, include_dirs, seen, (level or 0) + 1) end ::continue:: end @@ -77,7 +82,7 @@ end -- @see filter -- @name walk function lfs.walk(dir, filter, n, include_dirs) - assert_type(dir, 'string', 1) + dir = assert_type(dir, 'string', 1):match('^(.-)[/\\]?$') if not assert_type(filter, 'string/table/nil', 2) then filter = lfs.default_filter end @@ -104,7 +109,7 @@ function lfs.walk(dir, filter, n, include_dirs) end end local co = coroutine.create( - function() walk(dir, processed_filter, n, include_dirs) end) + function() walk(dir, processed_filter, n, include_dirs, {}) end) return function() return select(2, coroutine.resume(co)) end end diff --git a/test/test.lua b/test/test.lua index ee4cbd0f..b1b4ee74 100644 --- a/test/test.lua +++ b/test/test.lua @@ -720,6 +720,30 @@ function test_lfs_ext_walk_win32() _G.WIN32 = win32 -- reset just in case end +function test_lfs_ext_walk_symlinks() + local dir = os.tmpname() + os.remove(dir) + lfs.mkdir(dir) + lfs.mkdir(dir .. '/1') + io.open(dir .. '/1/foo', 'w'):close() + lfs.mkdir(dir .. '/1/bar') + io.open(dir .. '/1/bar/baz', 'w'):close() + lfs.link(dir .. '/1/', dir .. '/1/bar/quux', true) -- trailing '/' on purpose + lfs.mkdir(dir .. '/2') + io.open(dir .. '/2/foobar', 'w'):close() + lfs.link(dir .. '/2/foobar', dir .. '/2/foobaz', true) + lfs.link(dir .. '/2', dir .. '/1/2', true) + local files = {} + for filename in lfs.walk(dir .. '/1/') do -- trailing '/' on purpose + files[#files + 1] = filename + end + table.sort(files) + local expected_files = {dir .. '/1/foo', dir .. '/1/bar/baz', dir .. '/1/2/foobar', dir .. '/1/2/foobaz'} + table.sort(expected_files) + assert_equal(files, expected_files) + os.execute('rm -r ' .. dir) +end + function test_lfs_ext_abs_path() assert_equal(lfs.abspath('bar', '/foo'), '/foo/bar') assert_equal(lfs.abspath('./bar', '/foo'), '/foo/bar') -- cgit v1.2.3