Skip to content

Commit

Permalink
(New validator) Validate non-existing but valid file/directory paths (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnybubonic committed Mar 19, 2023
1 parent f560fd4 commit 2e43671
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 14 deletions.
6 changes: 4 additions & 2 deletions README.md
Expand Up @@ -222,8 +222,10 @@ Baked-in Validations
### Other:
| Tag | Description |
| - | - |
| dir | Directory |
| file | File path |
| dir | Existing Directory |
| dirpath | Directory Path |
| file | Existing File |
| filepath | File Path |
| isdefault | Is Default |
| len | Length |
| max | Maximum |
Expand Down
119 changes: 116 additions & 3 deletions baked_in.go
Expand Up @@ -7,20 +7,22 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unicode/utf8"

"golang.org/x/crypto/sha3"
"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
Expand Down Expand Up @@ -127,6 +129,7 @@ var (
"uri": isURI,
"urn_rfc2141": isUrnRFC2141, // RFC 2141
"file": isFile,
"filepath": isFilePath,
"base64": isBase64,
"base64url": isBase64URL,
"base64rawurl": isBase64RawURL,
Expand Down Expand Up @@ -199,6 +202,7 @@ var (
"html_encoded": isHTMLEncoded,
"url_encoded": isURLEncoded,
"dir": isDir,
"dirpath": isDirPath,
"json": isJSON,
"jwt": isJWT,
"hostname_port": isHostnamePort,
Expand Down Expand Up @@ -1464,7 +1468,7 @@ func isUrnRFC2141(fl FieldLevel) bool {
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 {
field := fl.Field()

Expand All @@ -1481,6 +1485,57 @@ func isFile(fl FieldLevel) bool {
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.
func isE164(fl FieldLevel) bool {
return e164Regex.MatchString(fl.Field().String())
Expand Down Expand Up @@ -2354,7 +2409,7 @@ func isFQDN(fl FieldLevel) bool {
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 {
field := fl.Field()

Expand All @@ -2370,6 +2425,64 @@ func isDir(fl FieldLevel) bool {
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.
func isJSON(fl FieldLevel) bool {
field := fl.Field()
Expand Down
27 changes: 26 additions & 1 deletion doc.go
Expand Up @@ -863,14 +863,25 @@ This validates that a string value is a valid JWT
Usage: jwt
# File path
# File
This validates that a string value contains a valid file path and that
the file exists on the machine.
This is done using os.Stat, which is a platform independent function.
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
This validates that a string value contains a valid url
Expand Down Expand Up @@ -912,6 +923,7 @@ you can use this with the omitempty tag.
Usage: base64url
# Base64RawURL String
This validates that a string value contains a valid base64 URL safe value,
Expand All @@ -922,6 +934,7 @@ you can use this with the omitempty tag.
Usage: base64url
# Bitcoin Address
This validates that a string value contains a valid bitcoin address.
Expand Down Expand Up @@ -1254,6 +1267,18 @@ This is done using os.Stat, which is a platform independent function.
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
This validates that a string value contains a valid DNS hostname and port that
Expand Down
88 changes: 80 additions & 8 deletions validator_test.go
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
Expand Down Expand Up @@ -3819,12 +3820,14 @@ func TestDataURIValidation(t *testing.T) {
{"data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", true},
{"data:text/plain;base64,Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==", true},
{"image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false},
{"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" +
"UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" +
"rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" +
"FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" +
"QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" +
"Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true},
{
"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" +
"UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" +
"rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" +
"FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" +
"QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" +
"Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true,
},
{"data:image/png;base64,12345", false},
{"", false},
{"data:text,:;base85,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false},
Expand Down Expand Up @@ -5732,6 +5735,39 @@ func TestFileValidation(t *testing.T) {
}, "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) {
validate := New()

Expand Down Expand Up @@ -10569,6 +10605,40 @@ func TestDirValidation(t *testing.T) {
}, "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) {
tests := []struct {
Value string `validate:"startswith=(/^ヮ^)/*:・゚✧"`
Expand Down Expand Up @@ -12361,10 +12431,12 @@ func TestPostCodeByIso3166Alpha2(t *testing.T) {
{"00803", true},
{"1234567", false},
},
"LC": { // not support regexp for post code
"LC": {
// not support regexp for post code
{"123456", false},
},
"XX": { // not support country
"XX": {
// not support country
{"123456", false},
},
}
Expand Down

0 comments on commit 2e43671

Please sign in to comment.