Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20260521144643.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:zap: add helpers for supporting `json` marshalling with better performance than `encoding/json`
1 change: 1 addition & 0 deletions changes/20260521162032.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: add helpers for serialising yaml
1 change: 1 addition & 0 deletions changes/20260521183044.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: add helpers to validate files against a JSON schema in `jsonschema`
1 change: 1 addition & 0 deletions changes/20260521184825.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[filesystem]` Added validation rules for checking that a file path has the required extension
104 changes: 0 additions & 104 deletions utils/filesystem/filepath.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package filesystem

import (
"io/fs"
"path"
"path/filepath"
"strings"
"syscall"

validation "github.com/go-ozzo/ozzo-validation/v4"

"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/platform"
Expand Down Expand Up @@ -282,103 +278,3 @@ func EvalSymlinks(fs FS, pathWithSymlinks string) (populatedPath string, err err
func EndsWithPathSeparator(fs FS, filePath string) bool {
return strings.HasSuffix(filePath, "/") || strings.HasSuffix(filePath, string(fs.PathSeparator()))
}

// NewPathValidationRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid not empty path.
// `when` describes whether the rule is enforced or not
func NewPathValidationRule(filesystem FS, when bool) validation.Rule {
return &pathValidationRule{condition: when, filesystem: filesystem}
}

// NewOSPathValidationRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid path for the Operating System's filesystem.
// `when` describes whether the rule is enforced or not
func NewOSPathValidationRule(when bool) validation.Rule {
return NewPathValidationRule(GetGlobalFileSystem(), when)
}

type pathValidationRule struct {
condition bool
filesystem FS
}

func (r *pathValidationRule) Validate(value interface{}) error {
err := validation.Required.When(r.condition).Validate(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrUndefined, err, "path [%v] is required", value)
}
if !r.condition {
return nil
}
pathString, err := validation.EnsureString(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value)
}
pathString = strings.TrimSpace(pathString)
// This check is here because it validates the path on any platform (it is a cross-platform check)
// Indeed if the path exists, then it can only be valid.
if r.filesystem.Exists(pathString) {
return nil
}

// Inspired from https://github.com/go-playground/validator/blob/84254aeb5a59e615ec0b66ab53b988bc0677f55e/baked_in.go#L1604 and https://stackoverflow.com/questions/35231846/golang-check-if-string-is-valid-path
if pathString == "" {
return commonerrors.Newf(commonerrors.ErrUndefined, "the path [%v] is empty", value)
}
// This check is to catch errors on Linux. It does not work as well on Windows.
if _, err := r.filesystem.Stat(pathString); err != nil {
switch t := err.(type) {
case *fs.PathError:
if t.Err == syscall.EINVAL {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "the path [%v] has invalid characters", value)
}
default:
// make the linter happy
}
}
// The following case is not caught on Windows by the check above.
if strings.Contains(pathString, "\n") {
return commonerrors.Newf(commonerrors.ErrInvalid, "the path [%v] has carriage returns characters", value)
}

// TODO add platform validation checks: e.g. https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN on windows

return nil
}

// NewPathExistRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid not empty path and actually exists.
// `when` describes whether the rule is enforced or not.
func NewPathExistRule(filesystem FS, when bool) validation.Rule {
return &pathExistValidationRule{filesystem: filesystem, condition: when}
}

// NewOSPathExistRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid path for the Operating system's filesystem and actually exists.
// `when` describes whether the rule is enforced or not.
func NewOSPathExistRule(when bool) validation.Rule {
return NewPathExistRule(GetGlobalFileSystem(), when)
}

type pathExistValidationRule struct {
condition bool
filesystem FS
}

func (r *pathExistValidationRule) Validate(value interface{}) error {
err := NewPathValidationRule(r.filesystem, r.condition).Validate(value)
if err != nil {
return err
}
if !r.condition {
return nil
}
path, err := validation.EnsureString(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value)
}
if !r.filesystem.Exists(path) {
err = commonerrors.Newf(commonerrors.ErrNotFound, "path [%v] does not exist", path)
}
return err
}
68 changes: 0 additions & 68 deletions utils/filesystem/filepath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
"github.com/ARM-software/golang-utils/utils/platform"
)

Expand Down Expand Up @@ -219,72 +217,6 @@ func TestEndsWithPathSeparator(t *testing.T) {
}
}

func TestNewPathExistRule(t *testing.T) {
t.Run("disable", func(t *testing.T) {
err := NewOSPathExistRule(false).Validate(faker.URL())
require.NoError(t, err)
})
t.Run("happy existing path", func(t *testing.T) {
require.NoError(t, NewOSPathExistRule(true).Validate(TempDirectory()))
testDir, err := TempDirInTempDir("test-path-rule-")
require.NoError(t, err)
defer func() { _ = Rm(testDir) }()
require.NoError(t, NewOSPathExistRule(true).Validate(testDir))
testFile, err := TouchTempFile(testDir, "test-file*.test")
require.NoError(t, err)
require.NoError(t, NewOSPathExistRule(true).Validate(testFile))
})
t.Run("non-existent path but valid", func(t *testing.T) {
err := NewOSPathExistRule(true).Validate(strings.ReplaceAll(faker.Sentence(), " ", "/"))
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrNotFound)
err = NewOSPathValidationRule(true).Validate(strings.ReplaceAll(faker.Sentence(), " ", "/"))
require.NoError(t, err)
err = NewOSPathExistRule(true).Validate(faker.URL())
require.Error(t, err)
errortest.AssertError(t, err, commonerrors.ErrNotFound)
err = NewOSPathValidationRule(true).Validate(faker.URL())
require.NoError(t, err)
})

t.Run("invalid paths", func(t *testing.T) {
tests := []struct {
entry any
expectedError []error
}{
{
entry: nil,
expectedError: []error{commonerrors.ErrUndefined, commonerrors.ErrInvalid},
},
{
entry: " ",
expectedError: []error{commonerrors.ErrUndefined, commonerrors.ErrInvalid},
},
{
entry: 123,
expectedError: []error{commonerrors.ErrInvalid},
},
{
entry: fmt.Sprintf("%v\n%v\n%v", faker.Paragraph(), faker.Paragraph(), faker.Sentence()),
expectedError: []error{commonerrors.ErrInvalid},
},
}
for i := range tests {
test := tests[i]
t.Run(fmt.Sprintf("%v", test.entry), func(t *testing.T) {
err := NewOSPathValidationRule(true).Validate(test.entry)
require.Error(t, err)
errortest.AssertError(t, err, test.expectedError...)
err = NewOSPathExistRule(true).Validate(test.entry)
require.Error(t, err)
errortest.AssertError(t, err, test.expectedError...)
})
}

})

}

func TestFilePathJoin(t *testing.T) {
embedFS, err := NewEmbedFileSystem(&testContent)
require.NoError(t, err)
Expand Down
174 changes: 174 additions & 0 deletions utils/filesystem/rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package filesystem

import (
"io/fs"
"strings"
"syscall"

validation "github.com/go-ozzo/ozzo-validation/v4"

"github.com/ARM-software/golang-utils/utils/collection"
"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/reflection"
)

// NewPathValidationRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid not empty path.
// `when` describes whether the rule is enforced or not.
func NewPathValidationRule(filesystem FS, when bool) validation.Rule {
return &pathValidationRule{condition: when, filesystem: filesystem}
}

// NewOSPathValidationRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid path for the operating system's
// filesystem.
// `when` describes whether the rule is enforced or not.
func NewOSPathValidationRule(when bool) validation.Rule {
return NewPathValidationRule(GetGlobalFileSystem(), when)
}

type pathValidationRule struct {
condition bool
filesystem FS
}

func (r *pathValidationRule) Validate(value any) error {
err := validation.Required.When(r.condition).Validate(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrUndefined, err, "path [%v] is required", value)
}
if !r.condition {
return nil
}
pathString, err := validation.EnsureString(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value)
}
pathString = strings.TrimSpace(pathString)
// This check is here because it validates the path on any platform (it is a cross-platform check)
// Indeed if the path exists, then it can only be valid.
if r.filesystem.Exists(pathString) {
return nil
}

// Inspired from https://github.com/go-playground/validator/blob/84254aeb5a59e615ec0b66ab53b988bc0677f55e/baked_in.go#L1604 and https://stackoverflow.com/questions/35231846/golang-check-if-string-is-valid-path
if pathString == "" {
return commonerrors.Newf(commonerrors.ErrUndefined, "the path [%v] is empty", value)
}
// This check is to catch errors on Linux. It does not work as well on Windows.
if _, err := r.filesystem.Stat(pathString); err != nil {
switch t := err.(type) {
case *fs.PathError:
if t.Err == syscall.EINVAL {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "the path [%v] has invalid characters", value)
}
default:
// make the linter happy
}
}
// The following case is not caught on Windows by the check above.
if strings.Contains(pathString, "\n") {
return commonerrors.Newf(commonerrors.ErrInvalid, "the path [%v] has carriage returns characters", value)
}

// TODO add platform validation checks: e.g. https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN on windows

return nil
}

// NewPathExistRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid not empty path and actually
// exists.
// `when` describes whether the rule is enforced or not.
func NewPathExistRule(filesystem FS, when bool) validation.Rule {
return &pathExistValidationRule{filesystem: filesystem, condition: when}
}

// NewOSPathExistRule returns a validation rule to use in configuration.
// The rule checks whether a string is a valid path for the operating system's
// filesystem and actually exists.
// `when` describes whether the rule is enforced or not.
func NewOSPathExistRule(when bool) validation.Rule {
return NewPathExistRule(GetGlobalFileSystem(), when)
}

type pathExistValidationRule struct {
condition bool
filesystem FS
}

func (r *pathExistValidationRule) Validate(value any) error {
err := NewPathValidationRule(r.filesystem, r.condition).Validate(value)
if err != nil {
return err
}
if !r.condition {
return nil
}
path, err := validation.EnsureString(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value)
}
if !r.filesystem.Exists(path) {
err = commonerrors.Newf(commonerrors.ErrNotFound, "path [%v] does not exist", path)
}
return err
}

// NewPathExtensionRule returns a validation rule that checks whether a path has
// an extension present in the supplied list.
// `when` describes whether the rule is enforced or not.
func NewPathExtensionRule(filesystem FS, when bool, extensions ...string) validation.Rule {
return &pathExtensionValidationRule{filesystem: filesystem, condition: when, extensions: normaliseExtensions(filesystem, extensions...)}
}

// NewOSPathExtensionRule returns a validation rule that checks whether a path
// on the global filesystem has an extension present in the supplied list.
// `when` describes whether the rule is enforced or not.
func NewOSPathExtensionRule(when bool, extensions ...string) validation.Rule {
return NewPathExtensionRule(GetGlobalFileSystem(), when, extensions...)
}

type pathExtensionValidationRule struct {
condition bool
filesystem FS
extensions []string
}

func (r *pathExtensionValidationRule) Validate(value any) error {
err := NewPathValidationRule(r.filesystem, r.condition).Validate(value)
if err != nil {
return err
}
if !r.condition {
return nil
}
if len(r.extensions) == 0 {
return commonerrors.UndefinedVariable("allowed file extensions")
}

pathString, err := validation.EnsureString(value)
if err != nil {
return commonerrors.WrapErrorf(commonerrors.ErrInvalid, err, "path [%v] must be a string", value)
}

extension := strings.ToLower(FilePathExt(r.filesystem, strings.TrimSpace(pathString)))
if reflection.IsEmpty(extension) {
return commonerrors.Newf(commonerrors.ErrNoExtension, "path [%v] has no extension", value)
}
if collection.In(r.extensions, extension, collection.StringMatch) {
return nil
}
return commonerrors.Newf(commonerrors.ErrInvalid, "path [%v] must have one of the extensions %v", value, r.extensions)

}

func normaliseExtensions(fs FS, extensions ...string) []string {
return collection.Map[string, string](extensions, func(extension string) string {
extension = strings.TrimSpace(extension)
if !strings.HasPrefix(extension, ".") {
extension = "." + extension
}
return strings.ToLower(FilePathClean(fs, extension))
})
}
Loading
Loading