ata-validator 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/formats.js ADDED
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ // Format predicates that read the string once, with no regular expression
4
+ // and no allocation. Each has a twin that emits the same check as source for
5
+ // the code generator; tests/test_formats_single_pass.js holds the two
6
+ // together and fuzzes both against the regular-expression forms they replaced.
7
+ // Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns.
8
+ // uuid was tried the same way and measured 57.3 against 59.4 ns: V8's regular
9
+ // expression for a fixed-length hex pattern is already that fast, so it stays.
10
+
11
+ function isDigit (c) { return c >= 48 && c <= 57; }
12
+
13
+ function date (s) {
14
+ if (s.length !== 10) return false;
15
+ for (let i = 0; i < 10; i++) {
16
+ const c = s.charCodeAt(i);
17
+ if (i === 4 || i === 7) { if (c !== 45) return false; } else if (!isDigit(c)) return false;
18
+ }
19
+ const m = (s.charCodeAt(5) - 48) * 10 + (s.charCodeAt(6) - 48);
20
+ const d = (s.charCodeAt(8) - 48) * 10 + (s.charCodeAt(9) - 48);
21
+ return m >= 1 && m <= 12 && d >= 1 && d <= 31;
22
+ }
23
+
24
+ // RFC 3339 date-time, read once. The form this replaces ran a regular
25
+ // expression for the shape and then `Date.parse` for the calendar, which
26
+ // builds a date object to answer a question about the string. Everything the
27
+ // parse rejected is checked here directly: month, the day count for that month
28
+ // in that year, and the ranges of the clock and the offset.
29
+ function daysInMonth (year, month) {
30
+ if (month === 2) {
31
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
32
+ return leap ? 29 : 28;
33
+ }
34
+ return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
35
+ }
36
+
37
+ function twoDigits (s, i) {
38
+ return (s.charCodeAt(i) - 48) * 10 + (s.charCodeAt(i + 1) - 48);
39
+ }
40
+
41
+ function dateTime (s) {
42
+ const n = s.length;
43
+ // The shortest accepted form is 2026-08-31T12:00:00Z
44
+ if (n < 20) return false;
45
+ for (let i = 0; i < 19; i++) {
46
+ const c = s.charCodeAt(i);
47
+ if (i === 4 || i === 7) { if (c !== 45) return false; continue; }
48
+ if (i === 10) { if (c !== 84 && c !== 116) return false; continue; }
49
+ if (i === 13 || i === 16) { if (c !== 58) return false; continue; }
50
+ if (c < 48 || c > 57) return false;
51
+ }
52
+
53
+ const year = (s.charCodeAt(0) - 48) * 1000 + (s.charCodeAt(1) - 48) * 100 +
54
+ (s.charCodeAt(2) - 48) * 10 + (s.charCodeAt(3) - 48);
55
+ const month = twoDigits(s, 5);
56
+ if (month < 1 || month > 12) return false;
57
+ const day = twoDigits(s, 8);
58
+ if (day < 1 || day > daysInMonth(year, month)) return false;
59
+ if (twoDigits(s, 11) > 23) return false;
60
+ if (twoDigits(s, 14) > 59) return false;
61
+ if (twoDigits(s, 17) > 59) return false;
62
+
63
+ let i = 19;
64
+ if (s.charCodeAt(i) === 46) {
65
+ i++;
66
+ const start = i;
67
+ while (i < n) {
68
+ const c = s.charCodeAt(i);
69
+ if (c < 48 || c > 57) break;
70
+ i++;
71
+ }
72
+ if (i === start) return false;
73
+ }
74
+
75
+ const c = s.charCodeAt(i);
76
+ if (c === 90 || c === 122) return i === n - 1;
77
+ if (c !== 43 && c !== 45) return false;
78
+ if (n - i !== 6) return false;
79
+ if (!isDigit(s.charCodeAt(i + 1)) || !isDigit(s.charCodeAt(i + 2))) return false;
80
+ if (s.charCodeAt(i + 3) !== 58) return false;
81
+ if (!isDigit(s.charCodeAt(i + 4)) || !isDigit(s.charCodeAt(i + 5))) return false;
82
+ if (twoDigits(s, i + 1) > 23) return false;
83
+ if (twoDigits(s, i + 4) > 59) return false;
84
+ return true;
85
+ }
86
+
87
+ // Four decimal octets, no leading zeros, no empty octets, nothing else.
88
+ // Also used for the dotted tail of an IPv4-mapped IPv6 address, which is why
89
+ // the work is done over a range rather than a whole string: taking a slice for
90
+ // it would allocate on a path that exists to avoid allocating.
91
+ function ipv4 (s) {
92
+ return ipv4Range(s, 0, s.length);
93
+ }
94
+
95
+ function ipv4Range (s, from, to) {
96
+ const n = to - from;
97
+ if (n < 7 || n > 15) return false;
98
+ let octets = 0, value = 0, digits = 0;
99
+ for (let i = from; i <= to; i++) {
100
+ const c = i < to ? s.charCodeAt(i) : 46;
101
+ if (c === 46) {
102
+ if (digits === 0 || value > 255) return false;
103
+ octets++; value = 0; digits = 0;
104
+ if (octets > 4) return false;
105
+ } else if (isDigit(c)) {
106
+ if (digits === 1 && value === 0) return false;
107
+ value = value * 10 + (c - 48); digits++;
108
+ if (digits > 3) return false;
109
+ } else return false;
110
+ }
111
+ return octets === 4;
112
+ }
113
+
114
+ // RFC 4291 address: up to eight groups of one to four hex digits, at most one
115
+ // run of "::" standing in for the zero groups, and an optional dotted IPv4
116
+ // tail in the last position, which counts as two groups. The forms this
117
+ // replaces were a character-class test plus two `split(':')` calls, which
118
+ // allocated twice and accepted things like "12345::1"; they also disagreed
119
+ // with each other about an IPv4 tail. Answers match Node's own `net.isIPv6`,
120
+ // which the test uses as its oracle.
121
+ function ipv6 (s) {
122
+ const n = s.length;
123
+ if (n < 2 || n > 45) return false;
124
+
125
+ let end = n;
126
+ let groups = 0;
127
+ // A dot can only belong to a trailing IPv4 address, which fills two groups.
128
+ const dot = s.indexOf('.');
129
+ if (dot !== -1) {
130
+ const lastColon = s.lastIndexOf(':', dot);
131
+ if (lastColon === -1) return false;
132
+ if (!ipv4Range(s, lastColon + 1, n)) return false;
133
+ end = lastColon + 1; // keep the colon: the group scan ends on it
134
+ groups = 2;
135
+ }
136
+
137
+ let compressed = false;
138
+ let digits = 0;
139
+ let i = 0;
140
+
141
+ if (s.charCodeAt(0) === 58 && s.charCodeAt(1) !== 58) return false;
142
+
143
+ while (i < end) {
144
+ const c = s.charCodeAt(i);
145
+ if (c === 58) {
146
+ if (digits > 0) { groups++; digits = 0; }
147
+ if (i + 1 < n && s.charCodeAt(i + 1) === 58) {
148
+ if (compressed) return false;
149
+ compressed = true;
150
+ i += 2;
151
+ if (i < n && s.charCodeAt(i) === 58) return false;
152
+ continue;
153
+ }
154
+ i++;
155
+ // A single colon must have something on both sides, and the group scan
156
+ // ending on a colon only happens when an IPv4 tail follows it.
157
+ if (i === end && end === n) return false;
158
+ continue;
159
+ }
160
+ if (isDigit(c) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70)) {
161
+ if (++digits > 4) return false;
162
+ i++;
163
+ continue;
164
+ }
165
+ return false;
166
+ }
167
+ if (digits > 0) groups++;
168
+
169
+ if (groups > 8) return false;
170
+ return compressed ? groups < 8 : groups === 8;
171
+ }
172
+
173
+ // Labels of letters, digits and hyphens, each 1 to 63 characters, none
174
+ // starting or ending with a hyphen, joined by single dots, 253 characters at
175
+ // most. Same answers as the expression it replaces, read once.
176
+ function hostname (s) {
177
+ const n = s.length;
178
+ if (n === 0 || n > 253) return false;
179
+ let labelLength = 0;
180
+ let previous = 46; // a dot, so a leading hyphen is refused like a leading dot
181
+ for (let i = 0; i < n; i++) {
182
+ const c = s.charCodeAt(i);
183
+ if (c === 46) {
184
+ if (labelLength === 0 || previous === 45) return false;
185
+ labelLength = 0;
186
+ previous = c;
187
+ continue;
188
+ }
189
+ const alnum = isDigit(c) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90);
190
+ if (!alnum && c !== 45) return false;
191
+ if (c === 45 && previous === 46) return false; // label starts with a hyphen
192
+ if (++labelLength > 63) return false;
193
+ previous = c;
194
+ }
195
+ return labelLength !== 0 && previous !== 45;
196
+ }
197
+
198
+ // Source twins. `v` is the expression holding the string; when `isStr` is
199
+ // false the check is wrapped in a typeof guard, matching FORMAT_CODEGEN's
200
+ // convention. Each returns a statement that `return false`s on mismatch.
201
+ function guard (v, isStr, body) { return isStr ? `{${body}}` : `if(typeof ${v}==='string'){${body}}`; }
202
+
203
+ function dateSource (v, isStr) {
204
+ return guard(v, isStr, `if(${v}.length!==10)return false;for(let _i=0;_i<10;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false}else if(_c<48||_c>57)return false}const _m=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48),_d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_m<1||_m>12||_d<1||_d>31)return false`);
205
+ }
206
+
207
+ function dateTimeSource (v, isStr) {
208
+ return guard(v, isStr, `const _n=${v}.length;if(_n<20)return false;for(let _i=0;_i<19;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false;continue}if(_i===10){if(_c!==84&&_c!==116)return false;continue}if(_i===13||_i===16){if(_c!==58)return false;continue}if(_c<48||_c>57)return false}` +
209
+ `const _y=(${v}.charCodeAt(0)-48)*1000+(${v}.charCodeAt(1)-48)*100+(${v}.charCodeAt(2)-48)*10+(${v}.charCodeAt(3)-48);` +
210
+ `const _mo=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48);if(_mo<1||_mo>12)return false;` +
211
+ `const _dm=_mo===2?(((_y%4===0&&_y%100!==0)||_y%400===0)?29:28):(_mo===4||_mo===6||_mo===9||_mo===11?30:31);` +
212
+ `const _d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_d<1||_d>_dm)return false;` +
213
+ `if((${v}.charCodeAt(11)-48)*10+(${v}.charCodeAt(12)-48)>23)return false;` +
214
+ `if((${v}.charCodeAt(14)-48)*10+(${v}.charCodeAt(15)-48)>59)return false;` +
215
+ `if((${v}.charCodeAt(17)-48)*10+(${v}.charCodeAt(18)-48)>59)return false;` +
216
+ `let _i2=19;if(${v}.charCodeAt(19)===46){_i2=20;const _st=_i2;while(_i2<_n){const _c2=${v}.charCodeAt(_i2);if(_c2<48||_c2>57)break;_i2++}if(_i2===_st)return false}` +
217
+ `const _tz=${v}.charCodeAt(_i2);` +
218
+ `if(_tz===90||_tz===122){if(_i2!==_n-1)return false}` +
219
+ `else{if(_tz!==43&&_tz!==45)return false;if(_n-_i2!==6)return false;` +
220
+ `const _oh=${v}.charCodeAt(_i2+1),_oh2=${v}.charCodeAt(_i2+2),_om=${v}.charCodeAt(_i2+4),_om2=${v}.charCodeAt(_i2+5);` +
221
+ `if(_oh<48||_oh>57||_oh2<48||_oh2>57||_om<48||_om>57||_om2<48||_om2>57)return false;` +
222
+ `if(${v}.charCodeAt(_i2+3)!==58)return false;` +
223
+ `if((_oh-48)*10+(_oh2-48)>23)return false;if((_om-48)*10+(_om2-48)>59)return false}`);
224
+ }
225
+
226
+ function hostnameSource (v, isStr) {
227
+ return guard(v, isStr, `const _n=${v}.length;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
228
+ `for(let _i=0;_i<_n;_i++){const _c=${v}.charCodeAt(_i);` +
229
+ `if(_c===46){if(_ll===0||_pv===45)return false;_ll=0;_pv=_c;continue}` +
230
+ `const _an=(_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90);` +
231
+ `if(!_an&&_c!==45)return false;` +
232
+ `if(_c===45&&_pv===46)return false;` +
233
+ `if(++_ll>63)return false;_pv=_c}` +
234
+ `if(_ll===0||_pv===45)return false`);
235
+ }
236
+
237
+ function ipv6Source (v, isStr) {
238
+ return guard(v, isStr, `const _n=${v}.length;if(_n<2||_n>45)return false;let _end=_n,_g=0;` +
239
+ `const _dot=${v}.indexOf('.');` +
240
+ `if(_dot!==-1){const _lc=${v}.lastIndexOf(':',_dot);if(_lc===-1)return false;` +
241
+ `{const _f=_lc+1,_t=_n,_ln=_t-_f;if(_ln<7||_ln>15)return false;let _o=0,_val=0,_dg=0;` +
242
+ `for(let _i=_f;_i<=_t;_i++){const _c=_i<_t?${v}.charCodeAt(_i):46;` +
243
+ `if(_c===46){if(_dg===0||_val>255)return false;_o++;_val=0;_dg=0;if(_o>4)return false}` +
244
+ `else if(_c>=48&&_c<=57){if(_dg===1&&_val===0)return false;_val=_val*10+(_c-48);_dg++;if(_dg>3)return false}` +
245
+ `else return false}if(_o!==4)return false}` +
246
+ `_end=_lc+1;_g=2}` +
247
+ `let _cp=false,_dg2=0,_i2=0;` +
248
+ `if(${v}.charCodeAt(0)===58&&${v}.charCodeAt(1)!==58)return false;` +
249
+ `while(_i2<_end){const _c2=${v}.charCodeAt(_i2);` +
250
+ `if(_c2===58){if(_dg2>0){_g++;_dg2=0}` +
251
+ `if(_i2+1<_n&&${v}.charCodeAt(_i2+1)===58){if(_cp)return false;_cp=true;_i2+=2;if(_i2<_n&&${v}.charCodeAt(_i2)===58)return false;continue}` +
252
+ `_i2++;if(_i2===_end&&_end===_n)return false;continue}` +
253
+ `if((_c2>=48&&_c2<=57)||(_c2>=97&&_c2<=102)||(_c2>=65&&_c2<=70)){if(++_dg2>4)return false;_i2++;continue}` +
254
+ `return false}` +
255
+ `if(_dg2>0)_g++;if(_g>8)return false;if(_cp){if(_g>=8)return false}else if(_g!==8)return false`);
256
+ }
257
+
258
+ function ipv4Source (v, isStr) {
259
+ return guard(v, isStr, `const _n=${v}.length;if(_n<7||_n>15)return false;let _o=0,_val=0,_dg=0;for(let _i=0;_i<=_n;_i++){const _c=_i<_n?${v}.charCodeAt(_i):46;if(_c===46){if(_dg===0||_val>255)return false;_o++;_val=0;_dg=0;if(_o>4)return false}else if(_c>=48&&_c<=57){if(_dg===1&&_val===0)return false;_val=_val*10+(_c-48);_dg++;if(_dg>3)return false}else return false}if(_o!==4)return false`);
260
+ }
261
+
262
+ module.exports = { date, ipv4, dateTime, ipv6, hostname, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource };
@@ -247,18 +247,19 @@ function multipleOfOk(d, m) {
247
247
  return Math.abs(q - Math.round(q)) < 1e-9;
248
248
  }
249
249
 
250
+ const _formats = require('./formats');
250
251
  const FORMAT_CHECKS = {
251
252
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1; },
252
- date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31; },
253
- 'date-time': (s) => /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/.test(s) && !isNaN(Date.parse(s)),
253
+ date: _formats.date,
254
+ 'date-time': _formats.dateTime,
254
255
  time: (s) => /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.test(s),
255
256
  duration: (s) => /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/.test(s) && s !== 'P' && !s.endsWith('T'),
256
257
  uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
257
258
  uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s) && !/[\s\u0000-\u001f\u007f]/.test(s),
258
259
  'uri-reference': (s) => !/[\s\u0000-\u001f\u007f]/.test(s),
259
- ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every((n) => { const v = +n; return n !== '' && v >= 0 && v <= 255 && String(v) === n; }); },
260
- ipv6: (s) => s !== '' && /^[0-9a-fA-F:.]+$/.test(s) && s.split(':').length >= 3 && s.split(':').length <= 8,
261
- hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s),
260
+ ipv4: _formats.ipv4,
261
+ ipv6: _formats.ipv6,
262
+ hostname: _formats.hostname,
262
263
  regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { try { new RegExp(s); return true; } catch { return false; } } },
263
264
  'json-pointer': (s) => s === '' || (/^\//.test(s) && !/~(?![01])/.test(s)),
264
265
  };