-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbinary.go
More file actions
325 lines (286 loc) · 8.93 KB
/
binary.go
File metadata and controls
325 lines (286 loc) · 8.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
package schema
import (
"encoding/base64"
"errors"
"fmt"
"github.com/nyxstack/i18n"
)
// BinaryFormat represents the encoding format for binary data
type BinaryFormat int
const (
BinaryFormatBase64 BinaryFormat = 0 // Standard base64 encoding
BinaryFormatBase64URL BinaryFormat = 1 // URL-safe base64 encoding
BinaryFormatHex BinaryFormat = 2 // Hexadecimal encoding
)
// Default error messages for binary validation
var (
binaryRequiredError = i18n.S("value is required")
binaryTypeError = i18n.S("value must be a string")
binaryInvalidBase64Error = i18n.S("must be valid base64 encoded data")
binaryInvalidBase64URLError = i18n.S("must be valid base64url encoded data")
binaryInvalidHexError = i18n.S("must be valid hexadecimal encoded data")
binaryHexLengthError = i18n.S("hex string must have even length")
)
func binaryInvalidFormatError(format string) i18n.TranslatedFunc {
return i18n.F("must be valid %s encoded binary data", format)
}
func binarySizeTooSmallError(actual, min int) i18n.TranslatedFunc {
return i18n.F("binary data size %d bytes is less than minimum %d bytes", actual, min)
}
func binarySizeTooLargeError(actual, max int) i18n.TranslatedFunc {
return i18n.F("binary data size %d bytes exceeds maximum %d bytes", actual, max)
}
// BinarySchema represents binary data validation schema
type BinarySchema struct {
Schema
format BinaryFormat
minSize *int
maxSize *int
formatError ErrorMessage
sizeError ErrorMessage
}
// Binary creates a new binary schema with base64 encoding
func Binary() *BinarySchema {
return &BinarySchema{
format: BinaryFormatBase64,
}
}
// Base64 creates a new binary schema with standard base64 encoding
func Base64() *BinarySchema {
return &BinarySchema{
format: BinaryFormatBase64,
}
}
// Base64URL creates a new binary schema with URL-safe base64 encoding
func Base64URL() *BinarySchema {
return &BinarySchema{
format: BinaryFormatBase64URL,
}
}
// Hex creates a new binary schema with hexadecimal encoding
func Hex() *BinarySchema {
return &BinarySchema{
format: BinaryFormatHex,
}
}
// Format sets the binary encoding format
func (s *BinarySchema) Format(format BinaryFormat) *BinarySchema {
s.format = format
return s
}
// MinSize sets the minimum size constraint in bytes
func (s *BinarySchema) MinSize(min int) *BinarySchema {
s.minSize = &min
return s
}
// MaxSize sets the maximum size constraint in bytes
func (s *BinarySchema) MaxSize(max int) *BinarySchema {
s.maxSize = &max
return s
}
// Size sets both minimum and maximum size constraints in bytes
func (s *BinarySchema) Size(min, max int) *BinarySchema {
s.minSize = &min
s.maxSize = &max
return s
}
// FormatError sets custom error message for format validation
func (s *BinarySchema) FormatError(err ErrorMessage) *BinarySchema {
s.formatError = err
return s
}
// SizeError sets custom error message for size validation
func (s *BinarySchema) SizeError(err ErrorMessage) *BinarySchema {
s.sizeError = err
return s
}
// Required marks the binary data as required (non-empty)
func (s *BinarySchema) Required() *BinarySchema {
s.Schema.required = true
return s
}
// Parse validates binary data
func (s *BinarySchema) Parse(value interface{}, ctx *ValidationContext) ParseResult {
var errors []ValidationError
// Convert to string
binaryStr, ok := value.(string)
if !ok {
message := binaryTypeError(ctx.Locale)
errors = append(errors, NewPrimitiveError(value, message, "invalid_type"))
return ParseResult{Valid: false, Value: value, Errors: errors}
}
// Required validation
if s.Schema.required && binaryStr == "" {
message := binaryRequiredError(ctx.Locale)
errors = append(errors, NewPrimitiveError(binaryStr, message, "required"))
return ParseResult{Valid: false, Value: value, Errors: errors}
}
// If empty and not required, return early
if binaryStr == "" {
return ParseResult{Valid: true, Value: binaryStr, Errors: nil}
}
// Decode and validate format
decodedData, err := s.validateAndDecode(binaryStr, ctx)
if err != nil {
// err is already a localized error message
errors = append(errors, NewPrimitiveError(binaryStr, err.Error(), "format"))
return ParseResult{Valid: false, Value: value, Errors: errors}
}
// Validate size constraints
dataSize := len(decodedData)
if s.minSize != nil && dataSize < *s.minSize {
message := binarySizeTooSmallError(dataSize, *s.minSize)(ctx.Locale)
if !isEmptyErrorMessage(s.sizeError) {
message = resolveErrorMessage(s.sizeError, ctx)
}
errors = append(errors, NewPrimitiveError(binaryStr, message, "min_size"))
}
if s.maxSize != nil && dataSize > *s.maxSize {
message := binarySizeTooLargeError(dataSize, *s.maxSize)(ctx.Locale)
if !isEmptyErrorMessage(s.sizeError) {
message = resolveErrorMessage(s.sizeError, ctx)
}
errors = append(errors, NewPrimitiveError(binaryStr, message, "max_size"))
}
// Return result
if len(errors) > 0 {
return ParseResult{Valid: false, Value: value, Errors: errors}
}
return ParseResult{Valid: true, Value: binaryStr, Errors: nil}
}
// decodeBinary decodes binary data according to the specified format
func (s *BinarySchema) decodeBinary(data string) ([]byte, error) {
switch s.format {
case BinaryFormatBase64:
return base64.StdEncoding.DecodeString(data)
case BinaryFormatBase64URL:
return base64.RawURLEncoding.DecodeString(data)
case BinaryFormatHex:
// Simple hex validation and decoding
if len(data)%2 != 0 {
// Use the same error message as for format validation
return nil, errors.New(binaryInvalidHexError("en"))
}
decoded := make([]byte, len(data)/2)
for i := 0; i < len(data); i += 2 {
var b byte
_, err := fmt.Sscanf(data[i:i+2], "%02x", &b)
if err != nil {
return nil, err
}
decoded[i/2] = b
}
return decoded, nil
default:
return base64.StdEncoding.DecodeString(data)
}
}
// validateAndDecode validates the format and returns decoded data with localized error messages
func (s *BinarySchema) validateAndDecode(data string, ctx *ValidationContext) ([]byte, error) {
switch s.format {
case BinaryFormatBase64:
decoded, err := base64.StdEncoding.DecodeString(data)
if err != nil {
message := binaryInvalidBase64Error(ctx.Locale)
if !isEmptyErrorMessage(s.formatError) {
message = resolveErrorMessage(s.formatError, ctx)
}
return nil, errors.New(message)
}
return decoded, nil
case BinaryFormatBase64URL:
decoded, err := base64.RawURLEncoding.DecodeString(data)
if err != nil {
message := binaryInvalidBase64URLError(ctx.Locale)
if !isEmptyErrorMessage(s.formatError) {
message = resolveErrorMessage(s.formatError, ctx)
}
return nil, errors.New(message)
}
return decoded, nil
case BinaryFormatHex:
// Check hex string length first
if len(data)%2 != 0 {
message := binaryHexLengthError(ctx.Locale)
if !isEmptyErrorMessage(s.formatError) {
message = resolveErrorMessage(s.formatError, ctx)
}
return nil, errors.New(message)
}
// Decode hex
decoded := make([]byte, len(data)/2)
for i := 0; i < len(data); i += 2 {
var b byte
_, err := fmt.Sscanf(data[i:i+2], "%02x", &b)
if err != nil {
message := binaryInvalidHexError(ctx.Locale)
if !isEmptyErrorMessage(s.formatError) {
message = resolveErrorMessage(s.formatError, ctx)
}
return nil, errors.New(message)
}
decoded[i/2] = b
}
return decoded, nil
default:
decoded, err := base64.StdEncoding.DecodeString(data)
if err != nil {
message := binaryInvalidBase64Error(ctx.Locale)
if !isEmptyErrorMessage(s.formatError) {
message = resolveErrorMessage(s.formatError, ctx)
}
return nil, errors.New(message)
}
return decoded, nil
}
}
// getFormatErrorMessage returns the appropriate error message for format validation
func (s *BinarySchema) getFormatErrorMessage(ctx *ValidationContext) string {
if !isEmptyErrorMessage(s.formatError) {
return resolveErrorMessage(s.formatError, ctx)
}
switch s.format {
case BinaryFormatBase64:
return binaryInvalidBase64Error(ctx.Locale)
case BinaryFormatBase64URL:
return binaryInvalidBase64URLError(ctx.Locale)
case BinaryFormatHex:
return binaryInvalidHexError(ctx.Locale)
default:
return binaryInvalidBase64Error(ctx.Locale)
}
}
// getFormatName returns the format name for JSON Schema
func (s *BinarySchema) getFormatName() string {
switch s.format {
case BinaryFormatBase64:
return "base64"
case BinaryFormatBase64URL:
return "base64url"
case BinaryFormatHex:
return "hex"
default:
return "base64"
}
}
// JSON generates JSON Schema for binary validation
func (s *BinarySchema) JSON() map[string]interface{} {
schema := map[string]interface{}{
"type": "string",
}
// Add format
format := s.getFormatName()
if format == "base64" {
schema["contentEncoding"] = "base64"
} else {
schema["format"] = format
}
// Add size constraints (these apply to the decoded binary data)
if s.minSize != nil {
schema["minLength"] = *s.minSize
}
if s.maxSize != nil {
schema["maxLength"] = *s.maxSize
}
return schema
}