Skip to content

Commit 59600e6

Browse files
authored
Version 1.1.11 (#1553)
* Linear Scan for Common Formats | 2020-12 Spec Updates * ChangeLog * Version
1 parent 0c1ad6c commit 59600e6

20 files changed

Lines changed: 888 additions & 497 deletions

File tree

changelog/1.1.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
---
44

55
### Version Updates
6+
- [Revision 1.1.11](https://github.com/sinclairzx81/typebox/pull/1553)
7+
- Linear Scan for Common Formats | 2020-12 Spec Alignment
68
- [Revision 1.1.10](https://github.com/sinclairzx81/typebox/pull/1552)
79
- Distributive Mapped Types
810
- [Revision 1.1.9](https://github.com/sinclairzx81/typebox/pull/1551)

src/format/email.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,34 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29-
const Email = /^(?!.*\.\.)[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i
29+
import { IsIPv4Internal } from './ipv4.ts'
3030

31-
/** Returns true if the value is an Email */
31+
/**
32+
* Returns true if the value is an Email
33+
* @specification Json Schema 2020-12
34+
*/
3235
export function IsEmail(value: string): boolean {
33-
return Email.test(value)
36+
const dot = value.indexOf('.')
37+
const at = value.indexOf('@')
38+
const quoted = value[0] === '"' && value[at - 1] === '"'
39+
const ipLiteral = value[at + 1] === '[' && value[value.length - 1] === ']'
40+
const ipv6 = ipLiteral && value.indexOf(':', at) !== -1
41+
const ipv4 = ipLiteral && !ipv6 && IsIPv4Internal(value, at + 2, value.length - 1)
42+
return (at > 0 && at < value.length - 1) &&
43+
!(
44+
// .test@example.com
45+
(!quoted && dot === 0) ||
46+
// te..st@example.com
47+
(!quoted && dot !== -1 && value.indexOf('.', dot + 1) === dot + 1) ||
48+
// test.@example.com
49+
(dot !== -1 && value.indexOf('@', dot) === dot + 1) ||
50+
// joe bloggs@example.com
51+
(!quoted && value.indexOf(' ') !== -1) ||
52+
// user1@oceania.org, user2@oceania.org
53+
(value.indexOf(',') !== -1) ||
54+
// joe.bloggs@[127.0.0.300]
55+
(ipLiteral && !ipv4 && !ipv6) ||
56+
// joe.bloggs@invalid=domain.com
57+
(value.indexOf('=', at) !== -1)
58+
)
3459
}

src/format/format.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,24 @@ THE SOFTWARE.
2828

2929
export * from './_registry.ts'
3030

31-
export * from './date-time.ts'
32-
export * from './date.ts'
33-
export * from './duration.ts'
34-
export * from './email.ts'
35-
export * from './hostname.ts'
36-
export * from './idn-email.ts'
37-
export * from './idn-hostname.ts'
38-
export * from './ipv4.ts'
39-
export * from './ipv6.ts'
40-
export * from './iri-reference.ts'
41-
export * from './iri.ts'
42-
export * from './json-pointer-uri-fragment.ts'
43-
export * from './json-pointer.ts'
44-
export * from './regex.ts'
45-
export * from './relative-json-pointer.ts'
46-
export * from './time.ts'
47-
export * from './uri-reference.ts'
48-
export * from './uri-template.ts'
49-
export * from './uri.ts'
50-
export * from './url.ts'
51-
export * from './uuid.ts'
31+
export { IsDateTime } from './date-time.ts'
32+
export { IsDate } from './date.ts'
33+
export { IsDuration } from './duration.ts'
34+
export { IsEmail } from './email.ts'
35+
export { IsHostname } from './hostname.ts'
36+
export { IsIdnEmail } from './idn-email.ts'
37+
export { IsIdnHostname } from './idn-hostname.ts'
38+
export { IsIPv4 } from './ipv4.ts'
39+
export { IsIPv6 } from './ipv6.ts'
40+
export { IsIriReference } from './iri-reference.ts'
41+
export { IsIri } from './iri.ts'
42+
export { IsJsonPointerUriFragment } from './json-pointer-uri-fragment.ts'
43+
export { IsJsonPointer } from './json-pointer.ts'
44+
export { IsRegex } from './regex.ts'
45+
export { IsRelativeJsonPointer } from './relative-json-pointer.ts'
46+
export { IsTime } from './time.ts'
47+
export { IsUriReference } from './uri-reference.ts'
48+
export { IsUriTemplate } from './uri-template.ts'
49+
export { IsUri } from './uri.ts'
50+
export { IsUrl } from './url.ts'
51+
export { IsUuid } from './uuid.ts'

src/format/hostname.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,36 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29-
const Hostname = /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i
30-
31-
/** Returns true if the value is a Hostname */
29+
/**
30+
* Returns true if the value matches RFC 1123 hostname syntax.
31+
* @specification https://tools.ietf.org/html/rfc1123
32+
*/
3233
export function IsHostname(value: string): boolean {
33-
return Hostname.test(value)
34+
if (value.length > 253 || value.length === 0) return false
35+
let start = 0
36+
let prev = 0
37+
for (let i = 0; i < value.length; i++) {
38+
const ch = value.charCodeAt(i)
39+
if (ch === 46) { // '.'
40+
// trailing dot is valid e.g. "example.com." but not "."
41+
if (i === value.length - 1 && start < i) break
42+
const len = i - start
43+
if (len === 0 || len > 63 || value.charCodeAt(start) === 45 || prev === 45) return false
44+
start = i + 1
45+
} else if (
46+
!(
47+
(ch >= 97 && ch <= 122) || // a-z
48+
(ch >= 65 && ch <= 90) || // A-Z
49+
(ch >= 48 && ch <= 57) || // 0-9
50+
ch === 45 // '-'
51+
)
52+
) {
53+
return false
54+
}
55+
prev = ch
56+
}
57+
const length = value.length - start
58+
const first = value.charCodeAt(start)
59+
const last = value.charCodeAt(value.length - 1)
60+
return length > 0 && length <= 63 && first !== 45 && last !== 45
3461
}

src/format/idn-email.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29-
const IdnEmail = /^(?!.*\.\.)[\p{L}\p{N}!#$%&'*+/=?^_`{|}~-]+(?:\.[\p{L}\p{N}!#$%&'*+/=?^_`{|}~-]+)*@[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?(?:\.[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?)*$/iu
29+
import { IsEmail } from './email.ts'
3030

31-
/** Returns true if the value is a Idn Email */
31+
/**
32+
* Returns true if the value is an IdnEmail
33+
* @specification Json Schema 2020-12
34+
*/
3235
export function IsIdnEmail(value: string): boolean {
33-
return IdnEmail.test(value)
36+
return IsEmail(value)
3437
}

src/format/idn-hostname.ts

Lines changed: 69 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -26,110 +26,102 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29-
function IsValidAdjacentForKatakanaMiddleDot(char: string): boolean {
30-
const codePoint = char.codePointAt(0)
31-
// deno-coverage-ignore - internal condition never reached
32-
if (codePoint === undefined) return false
29+
function IsValidAdjacentForKatakanaMiddleDot(cp: number): boolean {
3330
return (
34-
(codePoint >= 0x3040 && codePoint <= 0x309F) || // Hiragana
35-
(codePoint >= 0x30A0 && codePoint <= 0x30FF && codePoint !== 0x30FB) || // Katakana (excluding U+30FB)
36-
(codePoint >= 0x4E00 && codePoint <= 0x9FFF) // Han (CJK Unified Ideographs)
31+
(cp >= 0x3040 && cp <= 0x309F) || // Hiragana
32+
(cp >= 0x30A0 && cp <= 0x30FF && cp !== 0x30FB) || // Katakana (excluding U+30FB)
33+
(cp >= 0x4E00 && cp <= 0x9FFF) // Han (CJK Unified Ideographs)
3734
)
3835
}
3936
/**
40-
* Returns true if the value is a Hostname
41-
* @specification
37+
* Returns true if the value is an IDN Hostname
38+
* @specification Json Schema 2020-12
4239
*/
4340
export function IsIdnHostname(value: string): boolean {
44-
if (value.length === 0) return false
45-
if (value.includes(' ')) return false
46-
// Allowed label separators per RFC3490: U+002E, U+3002, U+FF0E, U+FF61.
47-
const separators = /[\u002E\u3002\uFF0E\uFF61]/g
48-
// Normalize (NFC) and replace allowed separators with a dot.
49-
const normalized = value.normalize('NFC').replace(separators, '.')
41+
if (value.length === 0 || value.includes(' ')) return false
42+
// Normalize (NFC) and replace allowed separators with a dot
43+
// Allowed label separators per RFC3490: U+002E, U+3002, U+FF0E, U+FF61
44+
const normalized = value.normalize('NFC').replace(/[\u002E\u3002\uFF0E\uFF61]/g, '.')
5045
if (normalized.length > 253) return false
51-
// Split into labels; disallow empty labels.
5246
const labels = normalized.split('.')
53-
if (labels.some((label) => label.length === 0)) return false
54-
5547
for (const label of labels) {
56-
// Each label must be ≤ 63 characters.
57-
if (label.length > 63) return false
58-
// Labels must not begin or end with a hyphen.
59-
if (label.startsWith('-') || label.endsWith('-')) return false
60-
61-
// A-label (punycode) checks.
62-
if (/^xn--/i.test(label)) {
48+
if (label.length === 0 || label.length > 63) return false
49+
// Labels must not begin or end with a hyphen
50+
if (label.charCodeAt(0) === 45 || label.charCodeAt(label.length - 1) === 45) return false
51+
// A-label (punycode) checks
52+
if (
53+
(label.charCodeAt(0) === 120 || label.charCodeAt(0) === 88) && // 'x' or 'X'
54+
(label.charCodeAt(1) === 110 || label.charCodeAt(1) === 78) && // 'n' or 'N'
55+
label.charCodeAt(2) === 45 && // '-'
56+
label.charCodeAt(3) === 45 // '-'
57+
) {
6358
const punycodePart = label.slice(4)
64-
if (punycodePart.length < 2) return false
65-
if (punycodePart.includes('---')) return false
59+
if (punycodePart.length < 2 || punycodePart.includes('---')) return false
6660
continue
6761
}
68-
// U-label: Reject if any disallowed code points occur.
69-
// Disallowed: U+302E, U+302F, U+3031, U+3032, U+3033, U+3034, U+3035, U+303B, U+0640, U+07FA.
70-
if (/[\u302E\u302F\u3031\u3032\u3033\u3034\u3035\u303B\u0640\u07FA]/.test(label)) {
71-
return false
72-
}
73-
// Disallow labels starting with certain combining marks.
74-
const firstChar = label.charAt(0)
75-
if (/[\u0903\u0300\u0488]/.test(firstChar)) return false
76-
77-
// Check each character within the label.
62+
// U-label checks
63+
let hasArabicIndic = false
64+
let hasExtendedArabicIndic = false
7865
for (let i = 0; i < label.length; i++) {
79-
const char = label.charAt(i)
80-
// --- MIDDLE DOT (U+00B7) ---
81-
// Must be flanked on both sides by "l" or "L".
82-
if (char === '\u00B7') {
66+
// deno-coverage-ignore
67+
const cp = label.codePointAt(i) ?? 0
68+
// Disallowed code points
69+
if (
70+
cp === 0x302E || cp === 0x302F ||
71+
cp === 0x3031 || cp === 0x3032 || cp === 0x3033 || cp === 0x3034 || cp === 0x3035 ||
72+
cp === 0x303B || cp === 0x0640 || cp === 0x07FA
73+
) return false
74+
// Disallow labels starting with certain combining marks
75+
if (i === 0 && (cp === 0x0903 || cp === 0x0300 || cp === 0x0488)) return false
76+
// MIDDLE DOT (U+00B7) must be flanked by 'l' or 'L'
77+
if (cp === 0x00B7) {
8378
if (i === 0 || i === label.length - 1) return false
84-
const prev = label.charAt(i - 1)
85-
const next = label.charAt(i + 1)
86-
if (!/^[lL]$/.test(prev) || !/^[lL]$/.test(next)) return false
79+
// deno-coverage-ignore
80+
const prev = label.codePointAt(i - 1) ?? 0
81+
// deno-coverage-ignore
82+
const next = label.codePointAt(i + 1) ?? 0
83+
if ((prev !== 108 && prev !== 76) || (next !== 108 && next !== 76)) return false
8784
}
88-
// --- KATAKANA MIDDLE DOT (U+30FB) ---
89-
if (char === '\u30FB') {
90-
// If label is a single character, it's invalid.
85+
// KATAKANA MIDDLE DOT (U+30FB) | U+30FB is below U+FFFF so stride is always 1
86+
if (cp === 0x30FB) {
9187
if (label.length === 1) return false
9288
if (i === 0) {
93-
// At beginning: check following character.
94-
const next = label.charAt(i + 1)
89+
// deno-coverage-ignore
90+
const next = label.codePointAt(i + 1) ?? 0
9591
if (!IsValidAdjacentForKatakanaMiddleDot(next)) return false
9692
} else {
97-
// In the middle: check both adjacent characters.
98-
const prev = label.charAt(i - 1)
99-
const next = label.charAt(i + 1)
100-
if (!IsValidAdjacentForKatakanaMiddleDot(prev) || !IsValidAdjacentForKatakanaMiddleDot(next)) {
101-
return false
102-
}
93+
// deno-coverage-ignore
94+
const prev = label.codePointAt(i - 1) ?? 0
95+
// deno-coverage-ignore
96+
const next = label.codePointAt(i + 1) ?? 0
97+
if (!IsValidAdjacentForKatakanaMiddleDot(prev) || !IsValidAdjacentForKatakanaMiddleDot(next)) return false
10398
}
10499
}
105-
// --- Greek Keraia (U+0375) ---
106-
if (char === '\u0375') {
100+
// Greek KERAIA (U+0375) | U+0375 is below U+FFFF so stride is always 1
101+
if (cp === 0x0375) {
107102
if (i === label.length - 1) return false
108-
const next = label.charAt(i + 1)
109-
if (!/[\u0370-\u03FF]/.test(next)) return false
103+
// deno-coverage-ignore
104+
const next = label.codePointAt(i + 1) ?? 0
105+
if (next < 0x0370 || next > 0x03FF) return false
110106
}
111-
112-
// --- Hebrew GERESH (U+05F3) and GERSHAYIM (U+05F4) ---
113-
if (char === '\u05F3' || char === '\u05F4') {
107+
// Hebrew GERESH (U+05F3) and GERSHAYIM (U+05F4)
108+
if (cp === 0x05F3 || cp === 0x05F4) {
114109
if (i === 0) return false
115-
const prev = label.charAt(i - 1)
116-
if (!/[\u05D0-\u05EA]/.test(prev)) return false
110+
// deno-coverage-ignore
111+
const prev = label.codePointAt(i - 1) ?? 0
112+
if (prev < 0x05D0 || prev > 0x05EA) return false
117113
}
118-
// --- ZERO WIDTH JOINER (U+200D) ---
119-
if (char === '\u200D') {
114+
// ZERO WIDTH JOINER (U+200D)
115+
if (cp === 0x200D) {
120116
if (i === 0) return false
121-
const prev = label.charAt(i - 1)
122-
if (prev !== '\u094D') return false
117+
// deno-coverage-ignore
118+
const prev = label.codePointAt(i - 1) ?? 0
119+
if (prev !== 0x094D) return false
123120
}
124-
// ZERO WIDTH NON-JOINER (U+200C) is allowed.
125-
}
126-
// --- Arabic-Indic digits vs. Extended Arabic-Indic digits ---
127-
let hasArabicIndic = false
128-
let hasExtendedArabicIndic = false
129-
for (let i = 0; i < label.length; i++) {
130-
const char = label.charAt(i)
131-
if (/[\u0660-\u0669]/.test(char)) hasArabicIndic = true
132-
if (/[\u06F0-\u06F9]/.test(char)) hasExtendedArabicIndic = true
121+
// Arabic-Indic digits
122+
if (cp >= 0x0660 && cp <= 0x0669) hasArabicIndic = true
123+
// Extended Arabic-Indic digits
124+
if (cp >= 0x06F0 && cp <= 0x06F9) hasExtendedArabicIndic = true
133125
}
134126
if (hasArabicIndic && hasExtendedArabicIndic) return false
135127
}

src/format/ipv4.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,37 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29-
const IPv4 = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/
30-
29+
// ------------------------------------------------------------------
30+
// Ranged Fast Path
31+
// ------------------------------------------------------------------
32+
/* Returns true if the value is a IPV4 address from index range offsets */
33+
export function IsIPv4Internal(value: string, start: number, end: number): boolean {
34+
let dots = 0
35+
let num = 0
36+
let digits = 0
37+
let leading = 0
38+
for (let i = start; i < end; i++) {
39+
const ch = value.charCodeAt(i)
40+
if (ch === 46) { // '.'
41+
if (digits === 0 || num > 255 || (leading === 48 && digits > 1)) return false
42+
dots++
43+
num = 0
44+
digits = 0
45+
leading = 0
46+
} else if (ch >= 48 && ch <= 57) { // '0'-'9'
47+
if (digits === 0) leading = ch
48+
num = num * 10 + (ch - 48)
49+
digits++
50+
} else {
51+
return false
52+
}
53+
}
54+
return dots === 3 && digits > 0 && num <= 255 && !(leading === 48 && digits > 1)
55+
}
3156
/**
3257
* Returns true if the value is a IPV4 address
33-
* @source ajv-formats
3458
* @specification http://tools.ietf.org/html/rfc2673#section-3.2
3559
*/
3660
export function IsIPv4(value: string): boolean {
37-
return IPv4.test(value)
61+
return IsIPv4Internal(value, 0, value.length)
3862
}

0 commit comments

Comments
 (0)