Skip to content

Commit

Permalink
Implement Unix file regularity check (nim-lang#20448) (nim-lang#20628)
Browse files Browse the repository at this point in the history
* Implement Unix file regularity check

* update std/dirs also
  • Loading branch information
a-mr authored and bung87 committed Jul 29, 2023
1 parent d2ff23c commit f33ad7b
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 45 deletions.
11 changes: 7 additions & 4 deletions lib/pure/os.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,8 @@ type
creationTime*: times.Time ## Time file was created. Not supported on all systems!
blockSize*: int ## Preferred I/O block size for this object.
## In some filesystems, this may vary from file to file.
isRegular*: bool ## Is file regular? (on Unix some "files"
## can be non-regular like FIFOs, devices)

template rawToFormalFileInfo(rawInfo, path, formalInfo): untyped =
## Transforms the native file info structure into the one nim uses.
Expand Down Expand Up @@ -1155,14 +1157,14 @@ template rawToFormalFileInfo(rawInfo, path, formalInfo): untyped =
checkAndIncludeMode(S_IWOTH, fpOthersWrite)
checkAndIncludeMode(S_IXOTH, fpOthersExec)

formalInfo.kind =
(formalInfo.kind, formalInfo.isRegular) =
if S_ISDIR(rawInfo.st_mode):
pcDir
(pcDir, true)
elif S_ISLNK(rawInfo.st_mode):
assert(path != "") # symlinks can't occur for file handles
getSymlinkFileKind(path)
else:
pcFile
(pcFile, S_ISREG(rawInfo.st_mode))

when defined(js):
when not declared(FileHandle):
Expand Down Expand Up @@ -1215,7 +1217,8 @@ proc getFileInfo*(path: string, followSymlink = true): FileInfo {.noWeirdTarget.
##
## When `followSymlink` is true (default), symlinks are followed and the
## information retrieved is information related to the symlink's target.
## Otherwise, information on the symlink itself is retrieved.
## Otherwise, information on the symlink itself is retrieved (however,
## field `isRegular` is still determined from the target on Unix).
##
## If the information cannot be retrieved, such as when the path doesn't
## exist, or when permission restrictions prevent the program from retrieving
Expand Down
32 changes: 18 additions & 14 deletions lib/std/dirs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -121,30 +121,33 @@ iterator walkDirs*(pattern: Path): Path {.tags: [ReadDirEffect].} =
for p in walkDirs(pattern.string):
yield Path(p)

iterator walkDir*(dir: Path; relative = false, checkDir = false):
iterator walkDir*(dir: Path; relative = false, checkDir = false,
onlyRegular = false):
tuple[kind: PathComponent, path: Path] {.tags: [ReadDirEffect].} =
## Walks over the directory `dir` and yields for each directory or file in
## `dir`. The component type and full path for each item are returned.
##
## Walking is not recursive. If ``relative`` is true (default: false)
## the resulting path is shortened to be relative to ``dir``.
##
## If `checkDir` is true, `OSError` is raised when `dir`
## doesn't exist.
for (k, p) in walkDir(dir.string, relative, checkDir):
## Walking is not recursive.
## * If `relative` is true (default: false)
## the resulting path is shortened to be relative to ``dir``,
## otherwise the full path is returned.
## * If `checkDir` is true, `OSError` is raised when `dir`
## doesn't exist.
## * If `onlyRegular` is true, then (besides all directories) only *regular*
## files (**without** special "file" objects like FIFOs, device files,
## etc) will be yielded on Unix.
for (k, p) in walkDir(dir.string, relative, checkDir, onlyRegular):
yield (k, Path(p))

iterator walkDirRec*(dir: Path,
yieldFilter = {pcFile}, followFilter = {pcDir},
relative = false, checkDir = false): Path {.tags: [ReadDirEffect].} =
relative = false, checkDir = false, onlyRegular = false):
Path {.tags: [ReadDirEffect].} =
## Recursively walks over the directory `dir` and yields for each file
## or directory in `dir`.
##
## If ``relative`` is true (default: false) the resulting path is
## shortened to be relative to ``dir``, otherwise the full path is returned.
##
## If `checkDir` is true, `OSError` is raised when `dir`
## doesn't exist.
## Options `relative`, `checkdir`, `onlyRegular` are explained in
## [walkDir iterator] description.
##
## .. warning:: Modifying the directory structure while the iterator
## is traversing may result in undefined behavior!
Expand Down Expand Up @@ -173,5 +176,6 @@ iterator walkDirRec*(dir: Path,
## * `walkFiles iterator`_
## * `walkDirs iterator`_
## * `walkDir iterator`_
for p in walkDirRec(dir.string, yieldFilter, followFilter, relative, checkDir):
for p in walkDirRec(dir.string, yieldFilter, followFilter, relative,
checkDir, onlyRegular):
yield Path(p)
13 changes: 8 additions & 5 deletions lib/std/private/oscommon.nim
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@ type


when defined(posix) and not weirdTarget:
proc getSymlinkFileKind*(path: string): PathComponent =
proc getSymlinkFileKind*(path: string):
tuple[pc: PathComponent, isRegular: bool] =
# Helper function.
var s: Stat
assert(path != "")
if stat(path, s) == 0'i32 and S_ISDIR(s.st_mode):
result = pcLinkToDir
else:
result = pcLinkToFile
result = (pcLinkToFile, true)
if stat(path, s) == 0'i32:
if S_ISDIR(s.st_mode):
result = (pcLinkToDir, true)
elif not S_ISREG(s.st_mode):
result = (pcLinkToFile, false)

proc tryMoveFSObject*(source, dest: string, isDir: bool): bool {.noWeirdTarget.} =
## Moves a file (or directory if `isDir` is true) from `source` to `dest`.
Expand Down
46 changes: 28 additions & 18 deletions lib/std/private/osdirs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,21 @@ proc staticWalkDir(dir: string; relative: bool): seq[
tuple[kind: PathComponent, path: string]] =
discard

iterator walkDir*(dir: string; relative = false, checkDir = false):
iterator walkDir*(dir: string; relative = false, checkDir = false,
onlyRegular = false):
tuple[kind: PathComponent, path: string] {.tags: [ReadDirEffect].} =
## Walks over the directory `dir` and yields for each directory or file in
## `dir`. The component type and full path for each item are returned.
##
## Walking is not recursive. If ``relative`` is true (default: false)
## the resulting path is shortened to be relative to ``dir``.
##
## If `checkDir` is true, `OSError` is raised when `dir`
## doesn't exist.
## Walking is not recursive.
## * If `relative` is true (default: false)
## the resulting path is shortened to be relative to ``dir``,
## otherwise the full path is returned.
## * If `checkDir` is true, `OSError` is raised when `dir`
## doesn't exist.
## * If `onlyRegular` is true, then (besides all directories) only *regular*
## files (**without** special "file" objects like FIFOs, device files,
## etc) will be yielded on Unix.
##
## **Example:**
##
Expand Down Expand Up @@ -234,40 +239,44 @@ iterator walkDir*(dir: string; relative = false, checkDir = false):
y = path
var k = pcFile

template resolveSymlink() =
var isRegular: bool
(k, isRegular) = getSymlinkFileKind(path)
if onlyRegular and not isRegular: continue

template kSetGeneric() = # pure Posix component `k` resolution
if lstat(path.cstring, s) < 0'i32: continue # don't yield
elif S_ISDIR(s.st_mode):
k = pcDir
elif S_ISLNK(s.st_mode):
k = getSymlinkFileKind(path)
resolveSymlink()
elif onlyRegular and not S_ISREG(s.st_mode): continue

when defined(linux) or defined(macosx) or
defined(bsd) or defined(genode) or defined(nintendoswitch):
case x.d_type
of DT_DIR: k = pcDir
of DT_LNK:
if dirExists(path): k = pcLinkToDir
else: k = pcLinkToFile
resolveSymlink()
of DT_UNKNOWN:
kSetGeneric()
else: # e.g. DT_REG etc
discard # leave it as pcFile
else: # DT_REG or special "files" like FIFOs
if onlyRegular and x.d_type != DT_REG: continue
else: discard # leave it as pcFile
else: # assuming that field `d_type` is not present
kSetGeneric()

yield (k, y)

iterator walkDirRec*(dir: string,
yieldFilter = {pcFile}, followFilter = {pcDir},
relative = false, checkDir = false): string {.tags: [ReadDirEffect].} =
relative = false, checkDir = false, onlyRegular = false):
string {.tags: [ReadDirEffect].} =
## Recursively walks over the directory `dir` and yields for each file
## or directory in `dir`.
##
## If ``relative`` is true (default: false) the resulting path is
## shortened to be relative to ``dir``, otherwise the full path is returned.
##
## If `checkDir` is true, `OSError` is raised when `dir`
## doesn't exist.
## Options `relative`, `checkdir`, `onlyRegular` are explained in
## [walkDir iterator] description.
##
## .. warning:: Modifying the directory structure while the iterator
## is traversing may result in undefined behavior!
Expand Down Expand Up @@ -301,7 +310,8 @@ iterator walkDirRec*(dir: string,
var checkDir = checkDir
while stack.len > 0:
let d = stack.pop()
for k, p in walkDir(dir / d, relative = true, checkDir = checkDir):
for k, p in walkDir(dir / d, relative = true, checkDir = checkDir,
onlyRegular = onlyRegular):
let rel = d / p
if k in {pcDir, pcLinkToDir} and k in followFilter:
stack.add rel
Expand Down
29 changes: 28 additions & 1 deletion tests/stdlib/tgetfileinfo.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ discard """
"""

import os, strutils
import std/syncio
import std/[syncio, assertions]
# Cases
# 1 - String : Existing File : Symlink true
# 2 - String : Existing File : Symlink false
Expand Down Expand Up @@ -127,10 +127,37 @@ proc testGetFileInfo =
echo pcLinkToDir
echo pcLinkToFile

doAssert dirInfo.isRegular == true
doAssert fileInfo.isRegular == true
when defined(posix):
doAssert linkDirInfo.isRegular == true
doAssert linkFileInfo.isRegular == true

removeDir(dirPath)
removeFile(filePath)
when defined(posix):
removeFile(linkDirPath)
removeFile(linkFilePath)

# Test that `isRegular` is set correctly
block:
when defined(posix):
let
tmp = getTempDir()
fifoPath = tmp / "test-fifo"
linkFifoPath = tmp / "test-link-fifo"

doAssert execShellCmd("mkfifo " & fifoPath) == 0
createSymlink(fifoPath, linkFifoPath)

let
fifoInfo = getFileInfo(fifoPath)
linkFifoInfo = getFileInfo(linkFifoPath)

doAssert fifoInfo.isRegular == false
doAssert linkFifoInfo.isRegular == false

removeFile(fifoPath)
removeFile(linkFifoPath)

testGetFileInfo()
19 changes: 17 additions & 2 deletions tests/stdlib/tos.nim
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ block walkDirRec:

removeDir("walkdir_test")

import std/sequtils

block: # walkDir
doAssertRaises(OSError):
for a in walkDir("nonexistent", checkDir = true): discard
Expand All @@ -358,6 +360,21 @@ block: # walkDir
doAssert k == pcLinkToDir
removeDir("walkdir_test")

when defined(posix):
block walkDirRegular:
createDir("walkdir_test")
doAssert execShellCmd("mkfifo walkdir_test/fifo") == 0
createSymlink("fifo", "walkdir_test/fifo_link")
let withSpecialFiles = toSeq(walkDir("walkdir_test", relative = true))
doAssert (withSpecialFiles.len == 2 and
(pcFile, "fifo") in withSpecialFiles and
(pcLinkToFile, "fifo_link") in withSpecialFiles)
# now Unix special files are excluded from walkdir output:
let onlyRegularFiles = toSeq(walkDir("walkdir_test", relative = true,
onlyRegular = true))
doAssert onlyRegularFiles.len == 0
removeDir("walkdir_test")

block normalizedPath:
doAssert normalizedPath("") == ""
block relative:
Expand Down Expand Up @@ -708,8 +725,6 @@ block: # isAdmin
# In Azure on POSIX tests run as a normal user
if isAzure and defined(posix): doAssert not isAdmin()

import std/sequtils

when doslikeFileSystem:
import std/private/ntpath

Expand Down
2 changes: 1 addition & 1 deletion tools/nimgrep.nim
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ iterator walkDirBasic(dir: string, walkOptC: WalkOptComp[Pattern]): string
let rightDirForFiles = d.isRightDirectory(walkOptC)
var files = newSeq[string]()
var dirs = newSeq[string]()
for kind, path in walkDir(d):
for kind, path in walkDir(d, onlyRegular = true):
case kind
of pcFile:
if path.hasRightPath(walkOptC) and rightDirForFiles:
Expand Down

0 comments on commit f33ad7b

Please sign in to comment.