github.com/abemedia/appcast@v0.4.0/pkg/pipe/validate.go (about) 1 package pipe 2 3 import ( 4 "fmt" 5 "io/fs" 6 "reflect" 7 "strings" 8 "unicode" 9 10 "github.com/abemedia/appcast/pkg/version" 11 locales "github.com/go-playground/locales/en" 12 ut "github.com/go-playground/universal-translator" 13 "github.com/go-playground/validator/v10" 14 translations "github.com/go-playground/validator/v10/translations/en" 15 "golang.org/x/mod/semver" 16 ) 17 18 type Error struct{ Errors []string } 19 20 func (e *Error) Error() string { 21 return fmt.Sprintf("invalid config:\n %s", strings.Join(e.Errors, "\n ")) 22 } 23 24 func Validate(c *config) error { 25 v := validator.New(validator.WithRequiredStructEnabled(), validator.WithOmitAnonymousName()) 26 v.RegisterTagNameFunc(func(fld reflect.StructField) string { 27 name, _, _ := strings.Cut(fld.Tag.Get("yaml"), ",") 28 if name == "-" { 29 return "" 30 } 31 return name 32 }) 33 _ = v.RegisterValidation("dirname", isDirname) 34 _ = v.RegisterValidation("version", isVersion) 35 _ = v.RegisterValidation("version_constraint", isConstraint) 36 37 uni := ut.New(locales.New()) 38 trans, _ := uni.GetTranslator("en") 39 _ = registerTranslations(v, trans) 40 41 err := v.Struct(c) 42 validationErrors, ok := err.(validator.ValidationErrors) 43 if !ok { 44 return err 45 } 46 47 errs := &Error{Errors: make([]string, len(validationErrors))} 48 for i, ve := range validationErrors { 49 _, ns, _ := strings.Cut(ve.Namespace(), ".") 50 errs.Errors[i] = ns + strings.TrimPrefix(ve.Translate(trans), ve.Field()) 51 } 52 53 return errs 54 } 55 56 func registerTranslations(v *validator.Validate, trans ut.Translator) error { 57 err := translations.RegisterDefaultTranslations(v, trans) 58 if err != nil { 59 return err 60 } 61 62 translations := []struct { 63 name string 64 message string 65 }{ 66 { 67 name: "dir", 68 message: "{0} must be a valid path to a directory", 69 }, 70 { 71 name: "dirname", 72 message: "{0} must be a valid folder name", 73 }, 74 { 75 name: "version_constraint", 76 message: "{0} must be a valid version constraint", 77 }, 78 { 79 name: "version", 80 message: "{0} must be a valid semver version", 81 }, 82 { 83 name: "http_url", 84 message: "{0} must be a valid URL", 85 }, 86 { 87 name: "fqdn|http_url", 88 message: "{0} must be a valid URL or FQDN", 89 }, 90 } 91 92 for _, t := range translations { 93 err = v.RegisterTranslation(t.name, trans, func(ut ut.Translator) error { 94 return ut.Add(t.name, t.message, true) 95 }, func(ut ut.Translator, fe validator.FieldError) string { 96 t, _ := ut.T(fe.Tag(), fe.Field()) 97 return t 98 }) 99 if err != nil { 100 return err 101 } 102 } 103 104 return nil 105 } 106 107 // isDirname validates if a dirname is valid in an URL, as well as common targets. 108 // 109 // - RFC3986 allows the following in a URL path (i.e. pchar): A-Z a-z 0-9 - . _ ~ ! $ & ' ( ) * + , ; = : @ 110 // - GCS forbids # [ ] * ? : " < > | (See https://cloud.google.com/storage/docs/objects#naming) 111 // - S3 forbids \ { ^ } % ` ] " > [ ~ < # | (See https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html#object-key-guidelines-avoid-characters) 112 // - This leaves us with letters, numbers and - . _ ! $ ' ( ) + , ; = @ 113 func isDirname(fl validator.FieldLevel) bool { 114 s := fl.Field().String() 115 return fs.ValidPath(s) && !strings.ContainsFunc(s, func(r rune) bool { 116 return !unicode.IsLetter(r) && !unicode.IsNumber(r) && !strings.ContainsRune("/-._!$&'()+,;=@ ", r) 117 }) 118 } 119 120 func isVersion(fl validator.FieldLevel) bool { 121 v := fl.Field().String() 122 if !strings.HasPrefix(v, "v") { 123 v = "v" + v 124 } 125 return semver.IsValid(v) 126 } 127 128 func isConstraint(fl validator.FieldLevel) bool { 129 _, err := version.NewConstraint(fl.Field().String()) 130 return err == nil 131 }