@@ -7,20 +7,22 @@ import (
77 "encoding/hex"
88 "encoding/json"
99 "fmt"
10+ "io/fs"
1011 "net"
1112 "net/url"
1213 "os"
1314 "reflect"
1415 "strconv"
1516 "strings"
1617 "sync"
18+ "syscall"
1719 "time"
1820 "unicode/utf8"
1921
2022 "golang.org/x/crypto/sha3"
2123 "golang.org/x/text/language"
2224
23- urn "github.com/leodido/go-urn"
25+ "github.com/leodido/go-urn"
2426)
2527
2628// Func accepts a FieldLevel interface for all validation needs. The return
@@ -127,6 +129,7 @@ var (
127129 "uri" : isURI ,
128130 "urn_rfc2141" : isUrnRFC2141 , // RFC 2141
129131 "file" : isFile ,
132+ "filepath" : isFilePath ,
130133 "base64" : isBase64 ,
131134 "base64url" : isBase64URL ,
132135 "base64rawurl" : isBase64RawURL ,
@@ -199,6 +202,7 @@ var (
199202 "html_encoded" : isHTMLEncoded ,
200203 "url_encoded" : isURLEncoded ,
201204 "dir" : isDir ,
205+ "dirpath" : isDirPath ,
202206 "json" : isJSON ,
203207 "jwt" : isJWT ,
204208 "hostname_port" : isHostnamePort ,
@@ -1464,7 +1468,7 @@ func isUrnRFC2141(fl FieldLevel) bool {
14641468 panic (fmt .Sprintf ("Bad field type %T" , field .Interface ()))
14651469}
14661470
1467- // isFile is the validation function for validating if the current field's value is a valid file path.
1471+ // isFile is the validation function for validating if the current field's value is a valid existing file path.
14681472func isFile (fl FieldLevel ) bool {
14691473 field := fl .Field ()
14701474
@@ -1481,6 +1485,57 @@ func isFile(fl FieldLevel) bool {
14811485 panic (fmt .Sprintf ("Bad field type %T" , field .Interface ()))
14821486}
14831487
1488+ // isFilePath is the validation function for validating if the current field's value is a valid file path.
1489+ func isFilePath (fl FieldLevel ) bool {
1490+
1491+ var exists bool
1492+ var err error
1493+
1494+ field := fl .Field ()
1495+
1496+ // If it exists, it obviously is valid.
1497+ // This is done first to avoid code duplication and unnecessary additional logic.
1498+ if exists = isFile (fl ); exists {
1499+ return true
1500+ }
1501+
1502+ // It does not exist but may still be a valid filepath.
1503+ switch field .Kind () {
1504+ case reflect .String :
1505+ // Every OS allows for whitespace, but none
1506+ // let you use a file with no filename (to my knowledge).
1507+ // Unless you're dealing with raw inodes, but I digress.
1508+ if strings .TrimSpace (field .String ()) == "" {
1509+ return false
1510+ }
1511+ // We make sure it isn't a directory.
1512+ if strings .HasSuffix (field .String (), string (os .PathSeparator )) {
1513+ return false
1514+ }
1515+ if _ , err = os .Stat (field .String ()); err != nil {
1516+ switch t := err .(type ) {
1517+ case * fs.PathError :
1518+ if t .Err == syscall .EINVAL {
1519+ // It's definitely an invalid character in the filepath.
1520+ return false
1521+ }
1522+ // It could be a permission error, a does-not-exist error, etc.
1523+ // Out-of-scope for this validation, though.
1524+ return true
1525+ default :
1526+ // Something went *seriously* wrong.
1527+ /*
1528+ Per https://pkg.go.dev/os#Stat:
1529+ "If there is an error, it will be of type *PathError."
1530+ */
1531+ panic (err )
1532+ }
1533+ }
1534+ }
1535+
1536+ panic (fmt .Sprintf ("Bad field type %T" , field .Interface ()))
1537+ }
1538+
14841539// isE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number.
14851540func isE164 (fl FieldLevel ) bool {
14861541 return e164Regex .MatchString (fl .Field ().String ())
@@ -2354,7 +2409,7 @@ func isFQDN(fl FieldLevel) bool {
23542409 return fqdnRegexRFC1123 .MatchString (val )
23552410}
23562411
2357- // isDir is the validation function for validating if the current field's value is a valid directory.
2412+ // isDir is the validation function for validating if the current field's value is a valid existing directory.
23582413func isDir (fl FieldLevel ) bool {
23592414 field := fl .Field ()
23602415
@@ -2370,6 +2425,64 @@ func isDir(fl FieldLevel) bool {
23702425 panic (fmt .Sprintf ("Bad field type %T" , field .Interface ()))
23712426}
23722427
2428+ // isDirPath is the validation function for validating if the current field's value is a valid directory.
2429+ func isDirPath (fl FieldLevel ) bool {
2430+
2431+ var exists bool
2432+ var err error
2433+
2434+ field := fl .Field ()
2435+
2436+ // If it exists, it obviously is valid.
2437+ // This is done first to avoid code duplication and unnecessary additional logic.
2438+ if exists = isDir (fl ); exists {
2439+ return true
2440+ }
2441+
2442+ // It does not exist but may still be a valid path.
2443+ switch field .Kind () {
2444+ case reflect .String :
2445+ // Every OS allows for whitespace, but none
2446+ // let you use a dir with no name (to my knowledge).
2447+ // Unless you're dealing with raw inodes, but I digress.
2448+ if strings .TrimSpace (field .String ()) == "" {
2449+ return false
2450+ }
2451+ if _ , err = os .Stat (field .String ()); err != nil {
2452+ switch t := err .(type ) {
2453+ case * fs.PathError :
2454+ if t .Err == syscall .EINVAL {
2455+ // It's definitely an invalid character in the path.
2456+ return false
2457+ }
2458+ // It could be a permission error, a does-not-exist error, etc.
2459+ // Out-of-scope for this validation, though.
2460+ // Lastly, we make sure it is a directory.
2461+ if strings .HasSuffix (field .String (), string (os .PathSeparator )) {
2462+ return true
2463+ } else {
2464+ return false
2465+ }
2466+ default :
2467+ // Something went *seriously* wrong.
2468+ /*
2469+ Per https://pkg.go.dev/os#Stat:
2470+ "If there is an error, it will be of type *PathError."
2471+ */
2472+ panic (err )
2473+ }
2474+ }
2475+ // We repeat the check here to make sure it is an explicit directory in case the above os.Stat didn't trigger an error.
2476+ if strings .HasSuffix (field .String (), string (os .PathSeparator )) {
2477+ return true
2478+ } else {
2479+ return false
2480+ }
2481+ }
2482+
2483+ panic (fmt .Sprintf ("Bad field type %T" , field .Interface ()))
2484+ }
2485+
23732486// isJSON is the validation function for validating if the current field's value is a valid json string.
23742487func isJSON (fl FieldLevel ) bool {
23752488 field := fl .Field ()
0 commit comments