(New validator) Validate non-existing but valid file/directory paths (#1022)

pull/1081/head
brent s 2 years ago committed by GitHub
parent f560fd4e07
commit 2e43671001
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      README.md
  2. 119
      baked_in.go
  3. 27
      doc.go
  4. 80
      validator_test.go

@ -222,8 +222,10 @@ Baked-in Validations
### Other: ### Other:
| Tag | Description | | Tag | Description |
| - | - | | - | - |
| dir | Directory | | dir | Existing Directory |
| file | File path | | dirpath | Directory Path |
| file | Existing File |
| filepath | File Path |
| isdefault | Is Default | | isdefault | Is Default |
| len | Length | | len | Length |
| max | Maximum | | max | Maximum |

@ -7,6 +7,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"net" "net"
"net/url" "net/url"
"os" "os"
@ -14,13 +15,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"golang.org/x/text/language" "golang.org/x/text/language"
urn "github.com/leodido/go-urn" "github.com/leodido/go-urn"
) )
// Func accepts a FieldLevel interface for all validation needs. The return // Func accepts a FieldLevel interface for all validation needs. The return
@ -127,6 +129,7 @@ var (
"uri": isURI, "uri": isURI,
"urn_rfc2141": isUrnRFC2141, // RFC 2141 "urn_rfc2141": isUrnRFC2141, // RFC 2141
"file": isFile, "file": isFile,
"filepath": isFilePath,
"base64": isBase64, "base64": isBase64,
"base64url": isBase64URL, "base64url": isBase64URL,
"base64rawurl": isBase64RawURL, "base64rawurl": isBase64RawURL,
@ -199,6 +202,7 @@ var (
"html_encoded": isHTMLEncoded, "html_encoded": isHTMLEncoded,
"url_encoded": isURLEncoded, "url_encoded": isURLEncoded,
"dir": isDir, "dir": isDir,
"dirpath": isDirPath,
"json": isJSON, "json": isJSON,
"jwt": isJWT, "jwt": isJWT,
"hostname_port": isHostnamePort, "hostname_port": isHostnamePort,
@ -1464,7 +1468,7 @@ func isUrnRFC2141(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %T", field.Interface())) panic(fmt.Sprintf("Bad field type %T", field.Interface()))
} }
// isFile is the validation function for validating if the current field's value is a valid file path. // isFile is the validation function for validating if the current field's value is a valid existing file path.
func isFile(fl FieldLevel) bool { func isFile(fl FieldLevel) bool {
field := fl.Field() field := fl.Field()
@ -1481,6 +1485,57 @@ func isFile(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %T", field.Interface())) panic(fmt.Sprintf("Bad field type %T", field.Interface()))
} }
// isFilePath is the validation function for validating if the current field's value is a valid file path.
func isFilePath(fl FieldLevel) bool {
var exists bool
var err error
field := fl.Field()
// If it exists, it obviously is valid.
// This is done first to avoid code duplication and unnecessary additional logic.
if exists = isFile(fl); exists {
return true
}
// It does not exist but may still be a valid filepath.
switch field.Kind() {
case reflect.String:
// Every OS allows for whitespace, but none
// let you use a file with no filename (to my knowledge).
// Unless you're dealing with raw inodes, but I digress.
if strings.TrimSpace(field.String()) == "" {
return false
}
// We make sure it isn't a directory.
if strings.HasSuffix(field.String(), string(os.PathSeparator)) {
return false
}
if _, err = os.Stat(field.String()); err != nil {
switch t := err.(type) {
case *fs.PathError:
if t.Err == syscall.EINVAL {
// It's definitely an invalid character in the filepath.
return false
}
// It could be a permission error, a does-not-exist error, etc.
// Out-of-scope for this validation, though.
return true
default:
// Something went *seriously* wrong.
/*
Per https://pkg.go.dev/os#Stat:
"If there is an error, it will be of type *PathError."
*/
panic(err)
}
}
}
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
}
// isE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number. // isE164 is the validation function for validating if the current field's value is a valid e.164 formatted phone number.
func isE164(fl FieldLevel) bool { func isE164(fl FieldLevel) bool {
return e164Regex.MatchString(fl.Field().String()) return e164Regex.MatchString(fl.Field().String())
@ -2354,7 +2409,7 @@ func isFQDN(fl FieldLevel) bool {
return fqdnRegexRFC1123.MatchString(val) return fqdnRegexRFC1123.MatchString(val)
} }
// isDir is the validation function for validating if the current field's value is a valid directory. // isDir is the validation function for validating if the current field's value is a valid existing directory.
func isDir(fl FieldLevel) bool { func isDir(fl FieldLevel) bool {
field := fl.Field() field := fl.Field()
@ -2370,6 +2425,64 @@ func isDir(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %T", field.Interface())) panic(fmt.Sprintf("Bad field type %T", field.Interface()))
} }
// isDirPath is the validation function for validating if the current field's value is a valid directory.
func isDirPath(fl FieldLevel) bool {
var exists bool
var err error
field := fl.Field()
// If it exists, it obviously is valid.
// This is done first to avoid code duplication and unnecessary additional logic.
if exists = isDir(fl); exists {
return true
}
// It does not exist but may still be a valid path.
switch field.Kind() {
case reflect.String:
// Every OS allows for whitespace, but none
// let you use a dir with no name (to my knowledge).
// Unless you're dealing with raw inodes, but I digress.
if strings.TrimSpace(field.String()) == "" {
return false
}
if _, err = os.Stat(field.String()); err != nil {
switch t := err.(type) {
case *fs.PathError:
if t.Err == syscall.EINVAL {
// It's definitely an invalid character in the path.
return false
}
// It could be a permission error, a does-not-exist error, etc.
// Out-of-scope for this validation, though.
// Lastly, we make sure it is a directory.
if strings.HasSuffix(field.String(), string(os.PathSeparator)) {
return true
} else {
return false
}
default:
// Something went *seriously* wrong.
/*
Per https://pkg.go.dev/os#Stat:
"If there is an error, it will be of type *PathError."
*/
panic(err)
}
}
// We repeat the check here to make sure it is an explicit directory in case the above os.Stat didn't trigger an error.
if strings.HasSuffix(field.String(), string(os.PathSeparator)) {
return true
} else {
return false
}
}
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
}
// isJSON is the validation function for validating if the current field's value is a valid json string. // isJSON is the validation function for validating if the current field's value is a valid json string.
func isJSON(fl FieldLevel) bool { func isJSON(fl FieldLevel) bool {
field := fl.Field() field := fl.Field()

@ -863,7 +863,8 @@ This validates that a string value is a valid JWT
Usage: jwt Usage: jwt
# File path
# File
This validates that a string value contains a valid file path and that This validates that a string value contains a valid file path and that
the file exists on the machine. the file exists on the machine.
@ -871,6 +872,16 @@ This is done using os.Stat, which is a platform independent function.
Usage: file Usage: file
# File Path
This validates that a string value contains a valid file path but does not
validate the existence of that file.
This is done using os.Stat, which is a platform independent function.
Usage: filepath
# URL String # URL String
This validates that a string value contains a valid url This validates that a string value contains a valid url
@ -912,6 +923,7 @@ you can use this with the omitempty tag.
Usage: base64url Usage: base64url
# Base64RawURL String # Base64RawURL String
This validates that a string value contains a valid base64 URL safe value, This validates that a string value contains a valid base64 URL safe value,
@ -922,6 +934,7 @@ you can use this with the omitempty tag.
Usage: base64url Usage: base64url
# Bitcoin Address # Bitcoin Address
This validates that a string value contains a valid bitcoin address. This validates that a string value contains a valid bitcoin address.
@ -1254,6 +1267,18 @@ This is done using os.Stat, which is a platform independent function.
Usage: dir Usage: dir
# Directory Path
This validates that a string value contains a valid directory but does
not validate the existence of that directory.
This is done using os.Stat, which is a platform independent function.
It is safest to suffix the string with os.PathSeparator if the directory
may not exist at the time of validation.
Usage: dirpath
# HostPort # HostPort
This validates that a string value contains a valid DNS hostname and port that This validates that a string value contains a valid DNS hostname and port that

@ -8,6 +8,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
@ -3819,12 +3820,14 @@ func TestDataURIValidation(t *testing.T) {
{"data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", true}, {"data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", true},
{"data:text/plain;base64,Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==", true}, {"data:text/plain;base64,Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==", true},
{"image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, {"image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false},
{"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" + {
"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" +
"UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" + "UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" +
"rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" + "rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" +
"FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" + "FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" +
"QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" + "QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" +
"Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true}, "Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true,
},
{"data:image/png;base64,12345", false}, {"data:image/png;base64,12345", false},
{"", false}, {"", false},
{"data:text,:;base85,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, {"data:text,:;base85,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false},
@ -5732,6 +5735,39 @@ func TestFileValidation(t *testing.T) {
}, "Bad field type int") }, "Bad field type int")
} }
func TestFilePathValidation(t *testing.T) {
validate := New()
tests := []struct {
title string
param string
expected bool
}{
{"empty filepath", "", false},
{"valid filepath", filepath.Join("testdata", "a.go"), true},
{"invalid filepath", filepath.Join("testdata", "no\000.go"), false},
{"directory, not a filepath", "testdata" + string(os.PathSeparator), false},
}
for _, test := range tests {
errs := validate.Var(test.param, "filepath")
if test.expected {
if !IsEqual(errs, nil) {
t.Fatalf("Test: '%s' failed Error: %s", test.title, errs)
}
} else {
if IsEqual(errs, nil) {
t.Fatalf("Test: '%s' failed Error: %s", test.title, errs)
}
}
}
PanicMatches(t, func() {
_ = validate.Var(6, "filepath")
}, "Bad field type int")
}
func TestEthereumAddressValidation(t *testing.T) { func TestEthereumAddressValidation(t *testing.T) {
validate := New() validate := New()
@ -10569,6 +10605,40 @@ func TestDirValidation(t *testing.T) {
}, "Bad field type int") }, "Bad field type int")
} }
func TestDirPathValidation(t *testing.T) {
validate := New()
tests := []struct {
title string
param string
expected bool
}{
{"empty dirpath", "", false},
{"valid dirpath - exists", "testdata", true},
{"valid dirpath - explicit", "testdatanoexist" + string(os.PathSeparator), true},
{"invalid dirpath", "testdata\000" + string(os.PathSeparator), false},
{"file, not a dirpath", filepath.Join("testdata", "a.go"), false},
}
for _, test := range tests {
errs := validate.Var(test.param, "dirpath")
if test.expected {
if !IsEqual(errs, nil) {
t.Fatalf("Test: '%s' failed Error: %s", test.title, errs)
}
} else {
if IsEqual(errs, nil) {
t.Fatalf("Test: '%s' failed Error: %s", test.title, errs)
}
}
}
PanicMatches(t, func() {
_ = validate.Var(6, "filepath")
}, "Bad field type int")
}
func TestStartsWithValidation(t *testing.T) { func TestStartsWithValidation(t *testing.T) {
tests := []struct { tests := []struct {
Value string `validate:"startswith=(/^ヮ^)/*:・゚✧"` Value string `validate:"startswith=(/^ヮ^)/*:・゚✧"`
@ -12361,10 +12431,12 @@ func TestPostCodeByIso3166Alpha2(t *testing.T) {
{"00803", true}, {"00803", true},
{"1234567", false}, {"1234567", false},
}, },
"LC": { // not support regexp for post code "LC": {
// not support regexp for post code
{"123456", false}, {"123456", false},
}, },
"XX": { // not support country "XX": {
// not support country
{"123456", false}, {"123456", false},
}, },
} }

Loading…
Cancel
Save