Add isValidFilename (#13561)

* Add os.isValidFilename

* Add os.isValidFilename

* Peer Review Feedbacks https://github.com/nim-lang/Nim/pull/13561#discussion_r388013139

* Peer Review Feedbacks https://github.com/nim-lang/Nim/pull/13561#issuecomment-595259568

* Add since to const

* Update the documentation comment

* Update the changelog

* Update lib/pure/os.nim

Co-Authored-By: Dominik Picheta <dominikpicheta@googlemail.com>

* Update lib/pure/os.nim

Co-Authored-By: Dominik Picheta <dominikpicheta@googlemail.com>

* Peer Review Feedbacks, Add more Tests

Co-authored-by: Dominik Picheta <dominikpicheta@googlemail.com>
This commit is contained in:
Juan Carlos 2020-03-07 11:10:22 -03:00 • committed by GitHub
commit 6a5b3811e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 69 additions and 1 deletions

View file

@ -69,7 +69,7 @@
- Added `minIndex`, `maxIndex` and `unzip` to the `sequtils` module. - Added `minIndex`, `maxIndex` and `unzip` to the `sequtils` module.
- Added `os.isRelativeTo` to tell whether a path is relative to another - Added `os.isRelativeTo` to tell whether a path is relative to another
- Added `resetOutputFormatters` to `unittest` - Added `resetOutputFormatters` to `unittest`
- Added `os.isValidFilename` that returns `true` if `filename` argument is valid for crossplatform use.
- Added a `with` macro for easy function chaining that's available - Added a `with` macro for easy function chaining that's available
everywhere, there is no need to concern your APIs with returning the first argument everywhere, there is no need to concern your APIs with returning the first argument

View file

@ -48,6 +48,20 @@ import
const weirdTarget = defined(nimscript) or defined(js) const weirdTarget = defined(nimscript) or defined(js)
since (1, 1):
const
invalidFilenameChars* = {'/', '\\', ':', '*', '?', '"', '<', '>', '|', '^', '\0'} ## \
## Characters that may produce invalid filenames across Linux, Windows, Mac, etc.
## You can check if your filename contains these char and strip them for safety.
## Mac bans ``':'``, Linux bans ``'/'``, Windows bans all others.
invalidFilenames* = [
"CON", "PRN", "AUX", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"] ## \
## Filenames that may be invalid across Linux, Windows, Mac, etc.
## You can check if your filename match these and rename it for safety
## (Currently all invalid filenames are from Windows only).
when weirdTarget: when weirdTarget:
discard discard
elif defined(windows): elif defined(windows):
@ -3194,6 +3208,32 @@ proc setLastModificationTime*(file: string, t: times.Time) {.noNimScript.} =
discard h.closeHandle discard h.closeHandle
if res == 0'i32: raiseOSError(osLastError(), file) if res == 0'i32: raiseOSError(osLastError(), file)
func isValidFilename*(filename: string, maxLen = 259.Positive): bool {.since: (1, 1).} =
## Returns true if ``filename`` is valid for crossplatform use.
##
## This is useful if you want to copy or save files across Windows, Linux, Mac, etc.
## You can pass full paths as argument too, but func only checks filenames.
## It uses ``invalidFilenameChars``, ``invalidFilenames`` and ``maxLen`` to verify the specified ``filename``.
##
## .. code-block:: nim
## assert not isValidFilename(" foo") ## Leading white space
## assert not isValidFilename("foo ") ## Trailing white space
## assert not isValidFilename("foo.") ## Ends with Dot
## assert not isValidFilename("con.txt") ## "CON" is invalid (Windows)
## assert not isValidFilename("OwO:UwU") ## ":" is invalid (Mac)
## assert not isValidFilename("aux.bat") ## "AUX" is invalid (Windows)
##
# https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception
# https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
result = true
let f = filename.splitFile()
if unlikely(f.name.len + f.ext.len > maxLen or
f.name[0] == ' ' or f.name[^1] == ' ' or f.name[^1] == '.' or
find(f.name, invalidFilenameChars) != -1): return false
for invalid in invalidFilenames:
if cmpIgnoreCase(f.name, invalid) == 0: return false
when isMainModule: when isMainModule:
assert quoteShellWindows("aaa") == "aaa" assert quoteShellWindows("aaa") == "aaa"
@ -3228,3 +3268,31 @@ when isMainModule:
doAssert r"D:\".normalizePathEnd == r"D:" doAssert r"D:\".normalizePathEnd == r"D:"
doAssert r"E:/".normalizePathEnd(trailingSep = true) == r"E:\" doAssert r"E:/".normalizePathEnd(trailingSep = true) == r"E:\"
doAssert "/".normalizePathEnd == r"\" doAssert "/".normalizePathEnd == r"\"
block isValidFilenameTest:
# Negative Tests.
doAssert not isValidFilename("abcd", maxLen = 2)
doAssert not isValidFilename("0123456789", maxLen = 8)
doAssert not isValidFilename("con")
doAssert not isValidFilename("aux")
doAssert not isValidFilename("prn")
doAssert not isValidFilename("OwO|UwU")
doAssert not isValidFilename(" foo")
doAssert not isValidFilename("foo ")
doAssert not isValidFilename("foo.")
doAssert not isValidFilename("con.txt")
doAssert not isValidFilename("aux.bat")
doAssert not isValidFilename("prn.exe")
doAssert not isValidFilename("nim>.nim")
doAssert not isValidFilename(" foo.log")
# Positive Tests.
doAssert isValidFilename("abcd", maxLen = 42.Positive)
doAssert isValidFilename("c0n")
doAssert isValidFilename("foo.aux")
doAssert isValidFilename("bar.prn")
doAssert isValidFilename("OwO_UwU")
doAssert isValidFilename("cron")
doAssert isValidFilename("ux.bat")
doAssert isValidFilename("nim.nim")
doAssert isValidFilename("foo.log")