44 "fmt"
55 "strconv"
66 "strings"
7+ "sync/atomic"
78 "time"
89
910 "github.com/lestrrat-go/jwx/v3/internal/json"
@@ -52,21 +53,61 @@ func intToTime(v any, t *time.Time) bool {
5253 return true
5354}
5455
55- func parseNumericString (x string ) (time.Time , error ) {
56- var t time.Time // empty time for empty return value
56+ // shouldParseIntOnly determines whether strict integer-only parsing should be enforced.
57+ // This function implements option-override semantics:
58+ //
59+ // If parseIntOnly is non-nil (explicitly set):
60+ // - Return the dereferenced value, ignoring the global Pedantic flag
61+ // - This allows callers to override the global setting for specific operations
62+ //
63+ // If parseIntOnly is nil (unspecified):
64+ // - Fall back to the global Pedantic flag value
65+ // - This maintains the default behavior when no override is specified
66+ //
67+ // This design allows three distinct behaviors:
68+ // - Pass nil: use global setting (backward compatible default)
69+ // - Pass ptr to false: explicitly lenient, even if global is strict
70+ // - Pass ptr to true: explicitly strict, even if global is lenient
71+ func shouldParseIntOnly (parseIntOnly * bool ) bool {
72+ if parseIntOnly != nil {
73+ // Explicit override: use the specified value, ignore global
74+ return * parseIntOnly
75+ }
76+ // No override: fall back to global setting
77+ return atomic .LoadUint32 (& Pedantic ) == 1
78+ }
5779
58- // Only check for the escape hatch if it's the pedantic
59- // flag is off
60- if Pedantic != 1 {
61- // This is an escape hatch for non-conformant providers
62- // that gives us RFC3339 instead of epoch time
63- for _ , r := range x {
64- // 0x30 = '0', 0x39 = '9', 0x2E = tokens.Period
65- if (r >= 0x30 && r <= 0x39 ) || r == 0x2E {
66- continue
67- }
80+ func detectRFC3339 (r rune ) bool {
81+ return ! (r >= tokens .Zero && r <= tokens .Nine ) && r != tokens .Period
82+ }
83+
84+ // parseNumericString parses a string representation of a Unix timestamp and returns
85+ // the corresponding time.Time value. The function handles both integer and fractional
86+ // (floating-point) timestamp values.
87+ //
88+ // The parseIntOnly parameter controls whether RFC3339 timestamp strings are accepted.
89+ // When parseIntOnly is true, only numeric string representations (epoch timestamps) are
90+ // accepted, and the function will return an error for RFC3339 formatted strings.
91+ // When parseIntOnly is false, the function will attempt to parse RFC3339 timestamps
92+ // as a fallback for non-conformant providers.
93+ //
94+ // IMPORTANT: This function does not consult global state. The caller is responsible
95+ // for determining the effective parseIntOnly value by combining any per-parse flags
96+ // with the global Pedantic flag (see shouldParseIntOnly helper).
97+ //
98+ // For fractional timestamps, the ParsePrecision variable controls how many fractional
99+ // digits are considered. The function pads or truncates fractional values to 9 digits
100+ // (nanosecond precision) as needed.
101+ func parseNumericString (x string , parseIntOnly bool ) (time.Time , error ) {
102+ var t time.Time // empty time for empty return value
68103
69- // if it got here, then it probably isn't epoch time
104+ // RFC3339 escape hatch for non-conformant providers.
105+ // Only active when parseIntOnly is false (lenient mode).
106+ if ! parseIntOnly {
107+ // Quick check: if string contains any non-numeric character (excluding period),
108+ // try parsing as RFC3339. RFC3339 timestamps always contain non-numeric characters
109+ // (T, Z, :, -) so this provides an early detection mechanism.
110+ if strings .ContainsFunc (x , detectRFC3339 ) {
70111 tv , err := time .Parse (time .RFC3339 , x )
71112 if err != nil {
72113 return t , fmt .Errorf (`value is not number of seconds since the epoch, and attempt to parse it as RFC3339 timestamp failed: %w` , err )
@@ -107,7 +148,27 @@ func parseNumericString(x string) (time.Time, error) {
107148 return time .Unix (n , nsecs ).UTC (), nil
108149}
109150
110- func (n * NumericDate ) Accept (v any , errOnNull bool ) error {
151+ // Accept validates and assigns a value to the NumericDate. The method supports
152+ // multiple input types including numeric values (int, float), json.Number, string
153+ // representations of numbers, and time.Time values.
154+ //
155+ // The errOnNull parameter controls whether nil values should be rejected (true)
156+ // or accepted as zero time (false). When errOnNull is true and v is nil, an error
157+ // is returned. When errOnNull is false and v is nil, the NumericDate is set to
158+ // the zero time value.
159+ //
160+ // The parseIntOnly parameter controls whether RFC3339 timestamp strings should
161+ // be rejected. This parameter uses option-override semantics:
162+ // - If parseIntOnly is nil: use the global Pedantic flag setting
163+ // - If parseIntOnly is non-nil: use the specified value, ignoring the global flag
164+ //
165+ // This allows callers to explicitly override the global setting when needed, while
166+ // defaulting to the global setting when no override is specified.
167+ //
168+ // Supported input types include int8, int16, int32, int64, int, float32, float64,
169+ // json.Number, string (numeric or RFC3339 when allowed), and time.Time. Any other
170+ // type will result in an error.
171+ func (n * NumericDate ) Accept (v any , errOnNull bool , parseIntOnly * bool ) error {
111172 // Handle nil case first
112173 if v == nil {
113174 if errOnNull {
@@ -118,28 +179,31 @@ func (n *NumericDate) Accept(v any, errOnNull bool) error {
118179 return nil
119180 }
120181
182+ // Resolve effective pedantic mode by combining parameter with global flag
183+ effectivePedantic := shouldParseIntOnly (parseIntOnly )
184+
121185 var t time.Time
122186 switch x := v .(type ) {
123187 case float32 :
124- tv , err := parseNumericString (fmt .Sprintf (`%.9f` , x ))
188+ tv , err := parseNumericString (fmt .Sprintf (`%.9f` , x ), effectivePedantic )
125189 if err != nil {
126190 return fmt .Errorf (`failed to accept float32 %.9f: %w` , x , err )
127191 }
128192 t = tv
129193 case float64 :
130- tv , err := parseNumericString (fmt .Sprintf (`%.9f` , x ))
194+ tv , err := parseNumericString (fmt .Sprintf (`%.9f` , x ), effectivePedantic )
131195 if err != nil {
132196 return fmt .Errorf (`failed to accept float32 %.9f: %w` , x , err )
133197 }
134198 t = tv
135199 case json.Number :
136- tv , err := parseNumericString (x .String ())
200+ tv , err := parseNumericString (x .String (), effectivePedantic )
137201 if err != nil {
138202 return fmt .Errorf (`failed to accept json.Number %q: %w` , x .String (), err )
139203 }
140204 t = tv
141205 case string :
142- tv , err := parseNumericString (x )
206+ tv , err := parseNumericString (x , effectivePedantic )
143207 if err != nil {
144208 return fmt .Errorf (`failed to accept string %q: %w` , x , err )
145209 }
@@ -194,7 +258,8 @@ func (n *NumericDate) UnmarshalJSON(data []byte) error {
194258 }
195259
196260 var n2 NumericDate
197- if err := n2 .Accept (v , false ); err != nil {
261+ // Pass nil to use global flag (no explicit override for direct unmarshaling)
262+ if err := n2 .Accept (v , false , nil ); err != nil {
198263 return fmt .Errorf (`invalid value for NumericDate: %w` , err )
199264 }
200265 * n = n2
0 commit comments