diff --git a/CHANGELOG.md b/CHANGELOG.md index cfbd8759a..9b171b0b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ This format is based on [Keep a Changelog](http://keepachangelog.com/). This project does not adhere to [Semantic Versioning](https://semver.org/) and minor version changes can have incompatible API changes. These incompatible API changes will largely affect those who have custom validator or walker implementations. Those who just use the library to validate using the standard JSON Schema Draft specifications may not need changes. ## [Unreleased] +### Changed + +- `hostname` rejects names longer than 253 characters. +- `hostname` checks a label that starts with `xn--`, in any case, as an A-label. It must be at most 63 characters of Punycode that encodes back to itself, and its U-label must pass the IDNA2008 rules listed below. `xn--X`, `xn--hello-txk` and `xn--1-0hc` (U-label `1א`, which breaks the RFC 5893 Bidi Rule) are now rejected; before, any LDH label passed. The Bidi Rule applies within each A-label only: the other labels stay RFC 1123 LDH labels, so `3com.xn--mgbaam7a8h` remains valid. +- `idn-hostname` decodes A-labels with an RFC 3492 Punycode decoder instead of `java.net.IDN` (IDNA2003). A-labels with characters IDNA2008 allows in context, such as U+00DF, U+03C2, U+200C and U+200D, are accepted when the RFC 5892 rules allow them. A-labels are case-insensitive. +- `idn-hostname` measures the 63-character label limit in A-label form for U-labels too, and limits names to 253 characters in A-label form. +- `idn-hostname` rejects more than one trailing label separator, such as `a..` and `xn--4dbc..`; before, any number was accepted. A single trailing separator is still accepted and does not count toward the 253 characters. +- IDNA2008 rules, for both formats: + - Hyphen positions are counted in code points, and each supplementary code point is tested once rather than as two surrogates. + - ZWJ and ZWNJ are accepted after any virama (Canonical_Combining_Class 9, as of Unicode 16.0), not only U+094D. The ZWNJ joining-type check steps over supplementary transparent marks. + - The KATAKANA MIDDLE DOT, GREEK LOWER NUMERAL SIGN and Hebrew GERESH/GERSHAYIM rules use the Unicode script rather than code blocks. KATAKANA MIDDLE DOT accepts a Hiragana, Katakana or Han character anywhere in the label (RFC 5892 A.7). + - RFC 5893 Bidi Rule: a label with an R, AL or AN character must satisfy the rule, so `0ا` is an invalid `idn-hostname` and `xn--1-0hc` (`1א`) an invalid `hostname` label. In `idn-hostname`, every other label of such a name must satisfy it too, so `1a.אב` and `1a.xn--4dbc` are invalid there. + - Following RFC 6452, U+0CF1 and U+0CF2 are valid and U+19DA is not, overriding the bundled table. + - The Unicode tables load safely across threads. +- Code points added after Unicode 5.2: the bundled RFC 5892 Appendix B table predates them, so they are classified with the running JDK's Unicode data, approximating the RFC 5892 derivation. Letters, marks and digits that are stable under NFKC and case folding are accepted, such as Adlam small letters and new Cherokee capitals. Emoji, symbols, upper case, format controls, letters that fold to another letter (U+1C80) and the default-ignorable U+180F are rejected, as are Cherokee small letters. +- Known limits: + - Verdicts for code points newer than the JDK's Unicode version depend on the JDK. For example, a Unicode 14 letter is rejected on Java 17 (Unicode 13) and accepted on Java 21. + - The virama list stops at Unicode 16.0. + - `idn-hostname` does not apply UTS 46 mapping, and still accepts a single trailing label separator (`example.`). +- Removed the unused `UnicodeDatabase.isGreek`, `isHebrew` and `isKatakana`, which tested code blocks rather than scripts. +- Updated the vendored `hostname.json` and `idn-hostname.json` suite files to the current JSON-Schema-Test-Suite, including `draft-next`. Cases that need UTS 46 mapping or trailing-separator rejection are disabled with a reason. + ## 3.0.9- 2026-11-03 ### Breaking changes diff --git a/src/main/java/com/networknt/schema/format/Formats.java b/src/main/java/com/networknt/schema/format/Formats.java index 5178238e8..44f202034 100644 --- a/src/main/java/com/networknt/schema/format/Formats.java +++ b/src/main/java/com/networknt/schema/format/Formats.java @@ -39,7 +39,7 @@ static PatternFormat pattern(String name, String regex) { static { List formats = new ArrayList<>(); - formats.add(pattern("hostname", "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$", "format.hostname")); + formats.add(new HostnameFormat()); formats.add(pattern("ipv4", "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$", "format.ipv4")); formats.add(new IPv6Format()); formats.add(pattern("json-pointer", "^(/([^/#~]|[~](?=[01]))*)*$", "format.json-pointer")); diff --git a/src/main/java/com/networknt/schema/format/HostnameFormat.java b/src/main/java/com/networknt/schema/format/HostnameFormat.java new file mode 100644 index 000000000..072e49748 --- /dev/null +++ b/src/main/java/com/networknt/schema/format/HostnameFormat.java @@ -0,0 +1,55 @@ +package com.networknt.schema.format; + +import java.util.regex.Pattern; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.utils.RFC5892; + +/** + * Format for hostname. + *

+ * A hostname is a sequence of LDH labels (RFC 1123, section 2.1) of at most 63 characters + * each, separated by dots, with an overall length of at most 253 characters. A label that + * starts with the ACE prefix {@code xn--}, in any case, is an A-label and must also be valid + * Punycode that decodes to a valid U-label (RFC 5890, section 2.3.2.1); see + * {@link RFC5892#isValidALabel(String)}. The RFC 5893 Bidi Rule applies within each A-label + * only: the other labels are RFC 1123 LDH labels, so a digit-first label such as {@code 3com} stays + * valid beside a right-to-left A-label. The cross-label Bidi Rule applies to {@code idn-hostname}. + */ +public class HostnameFormat implements Format { + private static final int MAX_LENGTH = 253; + + private static final String LABEL = "[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?"; + + private static final Pattern PATTERN = Pattern.compile(LABEL + "(\\." + LABEL + ")*"); + + private static final String ACE_PREFIX = "xn--"; + + @Override + public boolean matches(ExecutionContext executionContext, String value) { + if (null == value) return true; + if (value.length() > MAX_LENGTH || !PATTERN.matcher(value).matches()) { + return false; + } + // Walk the labels without splitting, so only an A-label is copied out of the value. + for (int start = 0; start < value.length(); ) { + int end = value.indexOf('.', start); + if (end < 0) end = value.length(); + if (value.regionMatches(true, start, ACE_PREFIX, 0, ACE_PREFIX.length())) { + if (!RFC5892.isValidALabel(value.substring(start, end))) return false; + } + start = end + 1; + } + return true; + } + + @Override + public String getName() { + return "hostname"; + } + + @Override + public String getMessageKey() { + return "format.hostname"; + } +} diff --git a/src/main/java/com/networknt/schema/utils/Punycode.java b/src/main/java/com/networknt/schema/utils/Punycode.java new file mode 100644 index 000000000..d6c056675 --- /dev/null +++ b/src/main/java/com/networknt/schema/utils/Punycode.java @@ -0,0 +1,139 @@ +package com.networknt.schema.utils; + +/** + * Punycode (RFC 3492) for IDNA2008 A-labels. + *

+ * {@link java.net.IDN} implements IDNA2003, which maps or rejects code points that IDNA2008 + * allows, such as U+00DF (sharp s), U+03C2 (final sigma), U+200C (ZWNJ) and U+200D (ZWJ). Decoding + * the Punycode directly leaves those code points for the RFC 5892 rules to judge. + */ +final class Punycode { + private static final int BASE = 36; + private static final int TMIN = 1; + private static final int TMAX = 26; + private static final int SKEW = 38; + private static final int DAMP = 700; + private static final int INITIAL_BIAS = 72; + private static final int INITIAL_N = 0x80; + private static final char DELIMITER = '-'; + + private Punycode() { + } + + /** + * Decodes a Punycode string (an A-label without its {@code xn--} prefix). + * + * @param input the Punycode string + * @return the decoded string, or {@code null} if the input is not valid Punycode + */ + static String decode(String input) { + int b = input.lastIndexOf(DELIMITER); + if (b < 0) b = 0; + // Every decoded code point consumes at least one input character, so this never overflows. + int[] output = new int[input.length()]; + int length = 0; + for (int j = 0; j < b; j++) { + char c = input.charAt(j); + if (c >= 0x80) return null; + output[length++] = c; + } + int n = INITIAL_N; + int bias = INITIAL_BIAS; + int i = 0; + for (int in = b > 0 ? b + 1 : 0; in < input.length(); ) { + int oldi = i; + int w = 1; + for (int k = BASE; ; k += BASE) { + if (in >= input.length()) return null; + int digit = digit(input.charAt(in++)); + if (digit < 0) return null; + if (digit > (Integer.MAX_VALUE - i) / w) return null; + i += digit * w; + int t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias; + if (digit < t) break; + if (w > Integer.MAX_VALUE / (BASE - t)) return null; + w *= BASE - t; + } + bias = adapt(i - oldi, length + 1, oldi == 0); + if (i / (length + 1) > Integer.MAX_VALUE - n) return null; + n += i / (length + 1); + i %= length + 1; + if (n > Character.MAX_CODE_POINT || (n >= Character.MIN_SURROGATE && n <= Character.MAX_SURROGATE)) { + return null; + } + System.arraycopy(output, i, output, i + 1, length - i); + output[i++] = n; + length++; + } + return new String(output, 0, length); + } + + /** + * Encodes a string as Punycode (without the {@code xn--} prefix). + * + * @param input the string to encode + * @return the Punycode string + */ + static String encode(String input) { + int[] codePoints = input.codePoints().toArray(); + StringBuilder output = new StringBuilder(); + for (int c : codePoints) { + if (c < 0x80) output.append((char) c); + } + int b = output.length(); + int h = b; + if (b > 0) output.append(DELIMITER); + int n = INITIAL_N; + int delta = 0; + int bias = INITIAL_BIAS; + while (h < codePoints.length) { + int m = Integer.MAX_VALUE; + for (int c : codePoints) { + if (c >= n && c < m) m = c; + } + delta += (m - n) * (h + 1); + n = m; + for (int c : codePoints) { + if (c < n) delta++; + if (c == n) { + int q = delta; + for (int k = BASE; ; k += BASE) { + int t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias; + if (q < t) break; + output.append(encodeDigit(t + (q - t) % (BASE - t))); + q = (q - t) / (BASE - t); + } + output.append(encodeDigit(q)); + bias = adapt(delta, h + 1, h == b); + delta = 0; + h++; + } + } + delta++; + n++; + } + return output.toString(); + } + + private static int adapt(int delta, int numPoints, boolean firstTime) { + delta = firstTime ? delta / DAMP : delta / 2; + delta += delta / numPoints; + int k = 0; + while (delta > ((BASE - TMIN) * TMAX) / 2) { + delta /= BASE - TMIN; + k += BASE; + } + return k + (BASE - TMIN + 1) * delta / (delta + SKEW); + } + + private static int digit(char c) { + if (c >= '0' && c <= '9') return c - '0' + 26; + if (c >= 'a' && c <= 'z') return c - 'a'; + if (c >= 'A' && c <= 'Z') return c - 'A'; + return -1; + } + + private static char encodeDigit(int d) { + return (char) (d < 26 ? 'a' + d : '0' + d - 26); + } +} diff --git a/src/main/java/com/networknt/schema/utils/RFC5892.java b/src/main/java/com/networknt/schema/utils/RFC5892.java index 5c44434ed..349a15e66 100644 --- a/src/main/java/com/networknt/schema/utils/RFC5892.java +++ b/src/main/java/com/networknt/schema/utils/RFC5892.java @@ -1,9 +1,9 @@ package com.networknt.schema.utils; -import java.net.IDN; import java.text.Normalizer; -import java.text.ParseException; +import java.util.Arrays; import java.util.BitSet; +import java.util.Locale; import java.util.function.BiPredicate; import static com.networknt.schema.utils.UnicodeDatabase.*; @@ -21,13 +21,34 @@ public class RFC5892 { private static final String ACE_PREFIX = "xn--"; private static final int ACE_PREFIX_LENGTH = ACE_PREFIX.length(); + private static final int MAX_LABEL_LENGTH = 63; + private static final int MAX_NAME_LENGTH = 253; + + /** The result of {@link #checkULabel(String)}. */ + private enum LabelStatus { + INVALID, + /** Valid, without R, AL or AN characters. */ + NO_RTL, + /** Valid, with an R, AL or AN character, and it satisfies the Bidi Rule. */ + HAS_RTL + } private static final int GREEK_LOWER_NUMERAL_SIGN = 0x0375; private static final int HEBREW_GERESH = 0x05F3; private static final int HEBREW_GERSHAYIM = 0x05F4; private static final int KATAKANA_MIDDLE_DOT = 0x30FB; private static final int MIDDLE_DOT = 0x00B7; - private static final int VIRAMA = 0x94D; + /** + * Code points with Canonical_Combining_Class Virama (9), from Unicode 16.0.0. Sorted, for binary search. + */ + private static final int[] VIRAMAS = { + 0x094D, 0x09CD, 0x0A4D, 0x0ACD, 0x0B4D, 0x0BCD, 0x0C4D, 0x0CCD, 0x0D3B, 0x0D3C, 0x0D4D, 0x0DCA, + 0x0E3A, 0x0EBA, 0x0F84, 0x1039, 0x103A, 0x1714, 0x1715, 0x1734, 0x17D2, 0x1A60, 0x1B44, 0x1BAA, + 0x1BAB, 0x1BF2, 0x1BF3, 0x2D7F, 0xA806, 0xA82C, 0xA8C4, 0xA953, 0xA9C0, 0xAAF6, 0xABED, 0x10A3F, + 0x11046, 0x11070, 0x1107F, 0x110B9, 0x11133, 0x11134, 0x111C0, 0x11235, 0x112EA, 0x1134D, 0x113CE, + 0x113CF, 0x113D0, 0x11442, 0x114C2, 0x115BF, 0x1163F, 0x116B6, 0x1172B, 0x11839, 0x1193D, 0x1193E, + 0x119E0, 0x11A34, 0x11A47, 0x11A99, 0x11C3F, 0x11D44, 0x11D45, 0x11D97, 0x11F41, 0x11F42, 0x1612F + }; private static final int ZERO_WIDTH_JOINER = 0x200D; private static final int ZERO_WIDTH_NON_JOINER = 0x200C; @@ -35,6 +56,7 @@ public class RFC5892 { private static final BitSet CONTEXTO = new BitSet(0x110000); private static final BitSet DISALLOWED = new BitSet(0x110000); private static final BitSet UNASSIGNED = new BitSet(0x110000); + private static volatile boolean loaded; private static final BiPredicate RULE_ARABIC_INDIC_DIGITS_RULE = RFC5892::testArabicIndicDigit; private static final BiPredicate RULE_EXTENDED_ARABIC_INDIC_DIGITS_RULE = RFC5892::testExtendedArabicIndicDigit; @@ -52,42 +74,90 @@ public class RFC5892 { private static final BiPredicate IDNA_RULES = ALLOWED_CHARACTER - .and(RULE_ARABIC_INDIC_DIGITS_RULE) - .and(RULE_EXTENDED_ARABIC_INDIC_DIGITS_RULE) - .and(RULE_GREEK_LOWER_NUMERAL_SIGN) - .and(RULE_HEBREW_GERESH_GERSHAYIM) - .and(RULE_KATAKANA_MIDDLE_DOT) - .and(RULE_MIDDLE_DOT) - .and(RULE_ZERO_WIDTH_JOINER) - .and(RULE_ZERO_WIDTH_NON_JOINER) - ; + .and(RULE_ARABIC_INDIC_DIGITS_RULE) + .and(RULE_EXTENDED_ARABIC_INDIC_DIGITS_RULE) + .and(RULE_GREEK_LOWER_NUMERAL_SIGN) + .and(RULE_HEBREW_GERESH_GERSHAYIM) + .and(RULE_KATAKANA_MIDDLE_DOT) + .and(RULE_MIDDLE_DOT) + .and(RULE_ZERO_WIDTH_JOINER) + .and(RULE_ZERO_WIDTH_NON_JOINER); + // Composed once, so checking a label allocates no predicate. + private static final BiPredicate IDNA_RULES_LTR = IDNA_RULES.and(LTR); + private static final BiPredicate IDNA_RULES_RTL = IDNA_RULES.and(RTL); private static boolean isContextJ(int codepoint) { - if (CONTEXTJ.isEmpty()) loadDerivedProperties(); + if (!loaded) loadDerivedProperties(); return CONTEXTJ.get(codepoint); } private static boolean isContextO(int codepoint) { - if (CONTEXTO.isEmpty()) loadDerivedProperties(); + if (!loaded) loadDerivedProperties(); return CONTEXTO.get(codepoint); } private static boolean isDisallowed(int codepoint) { - if (DISALLOWED.isEmpty()) loadDerivedProperties(); + if (!loaded) loadDerivedProperties(); return DISALLOWED.get(codepoint); } private static boolean isUnassigned(int codepoint) { - if (UNASSIGNED.isEmpty()) loadDerivedProperties(); + if (!loaded) loadDerivedProperties(); return UNASSIGNED.get(codepoint); } private static boolean testAllowedCharacter(String s, int i) { int c = s.codePointAt(i); - return !isDisallowed(c) && !isUnassigned(c) // RFC 5891 4.2.2. Rejection of Characters That Are Not Permitted + return isPermitted(c) // RFC 5891 4.2.2. Rejection of Characters That Are Not Permitted && !isContextJ(c) && !isContextO(c); // RFC 5891 4.2.3.3. Contextual Rules } + private static boolean isPermitted(int c) { + switch (c) { + // RFC 6452: Unicode 6.0 changed the general category of these three, and IDNA2008 kept the newly + // derived values instead of adding exceptions, so they differ from the Unicode 5.2 table. + case 0x0CF1: // KANNADA SIGN JIHVAMULIYA, now Lo + case 0x0CF2: // KANNADA SIGN UPADHMANIYA, now Lo + return true; + case 0x19DA: // NEW TAI LUE THAM DIGIT ONE, now No + return false; + default: + return !isDisallowed(c) && (!isUnassigned(c) || isDerivedPValid(c)); + } + } + + /** + * Classifies a code point that the bundled RFC 5892 table (Unicode 5.2) lists as unassigned, using + * the running JDK's Unicode data and an approximation of the RFC 5892 section 3 derivation: it is + * PVALID only if it is a lowercase, modifier or other letter, a nonspacing or spacing mark, or a + * decimal digit (LetterDigits without Lu), and NFKC and lower-casing leave it unchanged (Unstable). + * Symbols such as emoji, punctuation, upper case and format controls added since stay rejected. + */ + private static boolean isDerivedPValid(int c) { + int type = getType(c); + // Cherokee is the one script whose case folding maps small letters to capitals, so its small + // letters (Unicode 8) are unstable under NFKC_Casefold while its capitals stay PVALID. + if (UnicodeScript.CHEROKEE == UnicodeScript.of(c)) return UPPERCASE_LETTER == type; + // MONGOLIAN FREE VARIATION SELECTOR FOUR (Unicode 14) is a Default_Ignorable_Code_Point, so it is DISALLOWED. + if (0x180F == c) return false; + switch (type) { + case LOWERCASE_LETTER: + case MODIFIER_LETTER: + case OTHER_LETTER: + case NON_SPACING_MARK: + case COMBINING_SPACING_MARK: + case DECIMAL_DIGIT_NUMBER: + break; + default: + return false; + } + String s = new String(Character.toChars(c)); + // Case folding approximated as lower(upper(c)): it catches letters that fold to another letter, + // such as CYRILLIC SMALL LETTER ROUNDED VE (U+1C80), which folds to U+0432. + return s.equals(Normalizer.normalize(s, Normalizer.Form.NFKC)) && s.equals(s.toLowerCase(Locale.ROOT)) + && c == Character.toLowerCase(Character.toUpperCase(c)); + } + /** * Whenever dots are used as label separators, the following characters MUST be * recognized as dots: U+002E (full stop), U+3002 (ideographic full stop), @@ -95,81 +165,202 @@ private static boolean testAllowedCharacter(String s, int i) { */ private static String LABEL_SEPARATOR_REGEX = "[\\.\\u3002\\uff0e\\uff61]"; + /** + * Validates an internationalized host name (the {@code idn-hostname} format). + * + * @param value the host name + * @return whether every label is a valid U-label or A-label, the name is at most 253 characters in + * A-label form, and, if any label has an R, AL or AN character, every label satisfies the RFC 5893 Bidi + * Rule (so {@code 1a.xn--4dbc}, whose second label is Hebrew, is invalid); a single trailing label + * separator is accepted and not counted + */ public static boolean isValid(String value) { if ("".equals(value)) { return false; // empty string should fail } // RFC 5892 calls each segment in a host name a label. They are separated by all the recognized label separators. - String[] labels = value.split(LABEL_SEPARATOR_REGEX); - if (labels.length == 0) { - return false; // a value made up only of label separators (e.g. "." or "...") has no labels - } - for (String label : labels) { - // String.split() drops trailing empty strings, so a trailing label - // separator never produces an empty label here. A leading or interior - // empty label (e.g. ".example" or "a..b") is invalid. + // A limit of -1 keeps trailing empty strings. A single trailing separator (the root, "example.") + // is allowed and its empty label is dropped; any other empty label (".example", "a..b", "a..", + // "." or "...") is invalid. + String[] labels = value.split(LABEL_SEPARATOR_REGEX, -1); + int labelCount = labels.length; + if (1 < labelCount && labels[labelCount - 1].isEmpty()) labelCount--; + int asciiLength = labelCount - 1; // the separators between labels + boolean bidiDomainName = false; + for (int n = 0; n < labelCount; n++) { + String label = labels[n]; if (label.isEmpty()) return false; - String unicode = label; + String unicode; + int aceLength; if (isACE(label)) { - // IDN returns the original value when it encounters an issue converting to Unicode - unicode = IDN.toUnicode(label, IDN.USE_STD3_ASCII_RULES); - if (unicode.equalsIgnoreCase(label)) return false; + aceLength = label.length(); + if (MAX_LABEL_LENGTH < aceLength) return false; // checked before the quadratic decode + unicode = toUnicode(label); + if (null == unicode) return false; + } else { + // An A-label is at least as long as its U-label has code points. + if (MAX_LABEL_LENGTH < label.codePointCount(0, label.length())) return false; + unicode = label; + aceLength = isAscii(label) ? label.length() : ACE_PREFIX_LENGTH + Punycode.encode(label).length(); } + // RFC 5891 4.2.4. Registration Validation Requirements + if (MAX_LABEL_LENGTH < aceLength) return false; + asciiLength += aceLength; + if (MAX_NAME_LENGTH < asciiLength) return false; + + labels[n] = unicode; // from here on, labels[0..n] hold U-labels; the Bidi check below reads them back + + LabelStatus status = checkULabel(unicode); + if (LabelStatus.INVALID == status) return false; + // RFC 5893 2. Once a label has an R, AL or AN character, the name is a Bidi domain name and every + // label must satisfy the Bidi Rule. checkULabel has already applied it to right-to-left labels. + if (LabelStatus.HAS_RTL == status) { + if (!bidiDomainName) { + bidiDomainName = true; + // The labels before this one have no right-to-left characters and were not checked yet. + for (int m = 0; m < n; m++) { + if (!satisfiesBidiRule(labels[m])) return false; + } + } + } else if (bidiDomainName && !satisfiesBidiRule(unicode)) { + return false; + } + } + return true; + } - int len = unicode.length(); - BiPredicate rules; + /** + * Validates an A-label in a {@code hostname}: it must be valid Punycode that encodes back to + * itself, at most 63 characters, and its U-label must pass the RFC 5891 hyphen, combining mark, + * contextual, bidi and disallowed-character rules. + * + * @param label the label, including the ACE prefix + * @return whether the label is a valid A-label + */ + public static boolean isValidALabel(String label) { + if (MAX_LABEL_LENGTH < label.length() || !isACE(label)) return false; + String unicode = toUnicode(label); + return null != unicode && LabelStatus.INVALID != checkULabel(unicode); + } - // RFC 5891 5.4. Validation and Character List Testing - if (!Normalizer.isNormalized(unicode, Normalizer.Form.NFC)) return false; + /** + * Applies the per-label IDNA2008 rules, including the RFC 5893 Bidi Rule when the label has an R, AL or AN character. + * + * @param unicode the U-label + * @return {@link LabelStatus#INVALID} if a rule fails, otherwise whether the label has an R, AL or AN character + */ + private static LabelStatus checkULabel(String unicode) { + int len = unicode.length(); + BiPredicate rules; - // RFC 5891 4.2.3.1. Hyphen Restrictions - if ('-' == unicode.charAt(0) || '-' == unicode.codePointBefore(len)) return false; - if (4 <= len && '-' == unicode.codePointAt(2) && '-' == unicode.codePointAt(3)) return false; + // RFC 5891 5.4. Validation and Character List Testing + if (!Normalizer.isNormalized(unicode, Normalizer.Form.NFC)) return LabelStatus.INVALID; - // RFC 5891 4.2.3.2. Leading Combining Marks - if (isCombiningMark(unicode.codePointAt(0))) return false; + // RFC 5891 4.2.3.1. Hyphen Restrictions, by code point position + if ('-' == unicode.charAt(0) || '-' == unicode.codePointBefore(len)) return LabelStatus.INVALID; + // Four chars hold at least two code points, so the offset exists. + if (4 <= len && unicode.startsWith("--", unicode.offsetByCodePoints(0, 2))) return LabelStatus.INVALID; - // RFC 5893 2. The Bidi Rule - switch (getDirectionality(unicode.codePointAt(0))) { - case DIRECTIONALITY_LEFT_TO_RIGHT: - rules = IDNA_RULES.and(LTR); - break; + // RFC 5891 4.2.3.2. Leading Combining Marks + if (isCombiningMark(unicode.codePointAt(0))) return LabelStatus.INVALID; + + // RFC 5893 2. The Bidi Rule + switch (getDirectionality(unicode.codePointAt(0))) { + case DIRECTIONALITY_LEFT_TO_RIGHT: + rules = IDNA_RULES_LTR; + break; + case DIRECTIONALITY_RIGHT_TO_LEFT: + case DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + rules = IDNA_RULES_RTL; + break; + case DIRECTIONALITY_EUROPEAN_NUMBER: + case DIRECTIONALITY_OTHER_NEUTRALS: + rules = IDNA_RULES; + break; + default: return LabelStatus.INVALID; + } + + // Step by code point, so a supplementary character is tested once and never as a lone low surrogate. + for (int i = 0; i < len; i += Character.charCount(unicode.codePointAt(i))) { + if (!rules.test(unicode, i)) return LabelStatus.INVALID; + } + + // RFC 5893 2. A label with an R, AL or AN character makes the name a Bidi domain name, so the full Bidi Rule applies to it. + if (!hasRightToLeft(unicode)) return LabelStatus.NO_RTL; + return satisfiesBidiRule(unicode) ? LabelStatus.HAS_RTL : LabelStatus.INVALID; + } + + private static boolean hasRightToLeft(String label) { + for (int i = 0; i < label.length(); i += Character.charCount(label.codePointAt(i))) { + switch (getDirectionality(label.codePointAt(i))) { case DIRECTIONALITY_RIGHT_TO_LEFT: case DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: - rules = IDNA_RULES.and(RTL); - break; - case DIRECTIONALITY_EUROPEAN_NUMBER: - case DIRECTIONALITY_OTHER_NEUTRALS: - rules = IDNA_RULES; + case DIRECTIONALITY_ARABIC_NUMBER: + return true; + default: break; - default: return false; - } - - for (int i = 0; i < len; ++i) { - if (!rules.test(unicode, i)) return false; } + } + return false; + } - try { - String ace = IDN.toASCII(unicode, IDN.USE_STD3_ASCII_RULES); - if (63 < ace.length()) return false; // RFC 5891 4.2.4. Registration Validation Requirements - } catch (IllegalArgumentException e) { - Throwable t = e.getCause(); - if (t instanceof ParseException) { - String m = t.getMessage(); - // Ignore this. Java does not have the latest spec. - return m.startsWith("The input does not conform to the rules for BiDi code points"); - } - return false; - } + /** + * RFC 5893 2, rules 1, 3, 4 and 6. Rules 2 and 5, the allowed character classes, are checked by the + * LTR and RTL predicates. + */ + private static boolean satisfiesBidiRule(String label) { + byte first = getDirectionality(label.codePointAt(0)); + boolean rtl = first == DIRECTIONALITY_RIGHT_TO_LEFT || first == DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; + // Rule 1: the first character must have the Bidi property L, R or AL. + if (!rtl && first != DIRECTIONALITY_LEFT_TO_RIGHT) return false; + + boolean europeanNumber = false; + boolean arabicNumber = false; + byte last = first; + for (int i = 0; i < label.length(); i += Character.charCount(label.codePointAt(i))) { + byte d = getDirectionality(label.codePointAt(i)); + if (d == DIRECTIONALITY_EUROPEAN_NUMBER) europeanNumber = true; + if (d == DIRECTIONALITY_ARABIC_NUMBER) arabicNumber = true; + if (d != DIRECTIONALITY_NONSPACING_MARK) last = d; + } + if (rtl) { + // Rule 4: an RTL label must not contain both EN and AN. + if (europeanNumber && arabicNumber) return false; + // Rule 3: an RTL label must end with R, AL, EN or AN, followed by zero or more NSM. + return last == DIRECTIONALITY_RIGHT_TO_LEFT || last == DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC + || last == DIRECTIONALITY_EUROPEAN_NUMBER || last == DIRECTIONALITY_ARABIC_NUMBER; } + // Rule 6: an LTR label must end with L or EN, followed by zero or more NSM. + return last == DIRECTIONALITY_LEFT_TO_RIGHT || last == DIRECTIONALITY_EUROPEAN_NUMBER; + } + + /** + * Converts an A-label to its U-label (RFC 5890 2.3.2.1). The label is lower-cased first, since + * the ACE prefix and the basic code points of an A-label are case-insensitive. + * + * @param label the A-label, including the ACE prefix + * @return the U-label, or {@code null} if the label is not a valid A-label: the Punycode + * does not decode, does not encode back to the same label, or decodes to ASCII only + */ + private static String toUnicode(String label) { + if (!isAscii(label)) return null; + String punycode = label.substring(ACE_PREFIX_LENGTH).toLowerCase(Locale.ROOT); + String unicode = Punycode.decode(punycode); + if (null == unicode || unicode.isEmpty() || isAscii(unicode)) return null; + if (!Punycode.encode(unicode).equals(punycode)) return null; + return unicode; + } + private static boolean isAscii(String value) { + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) >= 0x80) return false; + } return true; } private static boolean isACE(String value) { - return ACE_PREFIX_LENGTH <= value.length() && - ACE_PREFIX.equalsIgnoreCase(value.substring(0, ACE_PREFIX_LENGTH)); + return value.regionMatches(true, 0, ACE_PREFIX, 0, ACE_PREFIX_LENGTH); } private static boolean isCombiningMark(int codepoint) { @@ -250,8 +441,7 @@ private static boolean testGreekLowerNumeralSign(String s, int i) { if (GREEK_LOWER_NUMERAL_SIGN == c) { // There must be a Greek character after this symbol if (s.length() == 1 + i) return false; - int following = s.codePointAt(i + 1); - return isGreek(following); + return UnicodeScript.GREEK == UnicodeScript.of(s.codePointAt(i + 1)); } return true; } @@ -269,8 +459,7 @@ private static boolean testHebrewPuncuation(String s, int i) { if (HEBREW_GERESH == c || HEBREW_GERSHAYIM == c) { // There must be a Hebrew character before this symbol if (0 == i) return false; - int preceding = s.codePointAt(i - 1); - return isHebrew(preceding); + return UnicodeScript.HEBREW == UnicodeScript.of(s.codePointBefore(i)); } return true; } @@ -286,10 +475,12 @@ private static boolean testHebrewPuncuation(String s, int i) { private static boolean testKatakanaMiddleDot(String s, int i) { int c = s.codePointAt(i); if (KATAKANA_MIDDLE_DOT == c) { - // There must be a Katakana, Hiragana or Han character after this symbol - if (s.length() == 1 + i) return false; - int following = s.codePointAt(i + 1); - return isKatakana(following); + // The label must contain a Hiragana, Katakana or Han character anywhere, not just after this symbol + return s.codePoints().anyMatch(cp -> { + if (KATAKANA_MIDDLE_DOT == cp) return false; + UnicodeScript script = UnicodeScript.of(cp); + return UnicodeScript.HIRAGANA == script || UnicodeScript.KATAKANA == script || UnicodeScript.HAN == script; + }); } return true; } @@ -308,13 +499,15 @@ private static boolean testeMiddleDotRule(String s, int i) { // There must be a 'l' character before and after this symbol if (0 == i) return false; if (s.length() == 1 + i) return false; - int preceding = s.codePointAt(i - 1); - int following = s.codePointAt(i + 1); - return 'l' == preceding && 'l' == following; + return 'l' == s.codePointBefore(i) && 'l' == s.codePointAt(i + 1); } return true; } + private static boolean isVirama(int codepoint) { + return Arrays.binarySearch(VIRAMAS, codepoint) >= 0; + } + /** * Determines whether the ZERO WIDTH JOINER conforms to the RFC 5892 specification. * @@ -328,8 +521,7 @@ private static boolean testZeroWidthJoiner(String s, int i) { if (ZERO_WIDTH_JOINER == c) { // There must be a virama character before this symbol. if (0 == i) return false; - int preceding = s.codePointAt(i - 1); - return VIRAMA == preceding; + return isVirama(s.codePointBefore(i)); } return true; } @@ -350,10 +542,11 @@ private static boolean testZeroWidthNonJoiner(String s, int i) { if (0 == i) return false; int preceding = s.codePointBefore(i); - if (VIRAMA == preceding) return true; + if (isVirama(preceding)) return true; + // Step by code point, as several scripts have supplementary transparent marks. int j = i; - while (0 < j && isJoinTypeTransparent(s.codePointBefore(j))) --j; + while (0 < j && isJoinTypeTransparent(s.codePointBefore(j))) j -= Character.charCount(s.codePointBefore(j)); if (0 == j) return false; preceding = s.codePointBefore(j); @@ -363,7 +556,7 @@ private static boolean testZeroWidthNonJoiner(String s, int i) { int len = s.length(); if (len == j) return false; - while (j < len && isJoinTypeTransparent(s.codePointAt(j))) ++j; + while (j < len && isJoinTypeTransparent(s.codePointAt(j))) j += Character.charCount(s.codePointAt(j)); if (len == j) return false; int following = s.codePointAt(j); @@ -389,7 +582,7 @@ private static boolean testExtendedArabicIndicDigit(String s, int i) { } private static synchronized void loadDerivedProperties() { - if (DISALLOWED.isEmpty()) { + if (!loaded) { UCDLoader.loadMapping("/ucd/RFC5892-appendix-B.txt", v -> { switch (v) { case "CONTEXTJ": return CONTEXTJ; @@ -410,6 +603,8 @@ private static synchronized void loadDerivedProperties() { CONTEXTO.clear(HEBREW_GERSHAYIM); CONTEXTO.clear(KATAKANA_MIDDLE_DOT); CONTEXTO.clear(MIDDLE_DOT); + // The volatile write publishes the filled tables to threads that read loaded without the lock. + loaded = true; } } diff --git a/src/main/java/com/networknt/schema/utils/UnicodeDatabase.java b/src/main/java/com/networknt/schema/utils/UnicodeDatabase.java index 70812ccfb..07f1c54b2 100644 --- a/src/main/java/com/networknt/schema/utils/UnicodeDatabase.java +++ b/src/main/java/com/networknt/schema/utils/UnicodeDatabase.java @@ -5,40 +5,18 @@ public class UnicodeDatabase { private static final BitSet ARABIC_INDIC_DIGITS = new BitSet(0x11000); private static final BitSet EXTENDED_ARABIC_INDIC_DIGITS = new BitSet(0x11000); - private static final BitSet GREEK_CHARACTERS = new BitSet(0x2000); - private static final BitSet HEBREW_CHARACTERS = new BitSet(0x0600); - private static final BitSet KATAKANA_CHARACTERS = new BitSet(0x33000); private static final BitSet JOIN_TYPE_CAUSING = new BitSet(0x110000); private static final BitSet JOIN_TYPE_DUAL = new BitSet(0x110000); private static final BitSet JOIN_TYPE_LEFT = new BitSet(0x110000); private static final BitSet JOIN_TYPE_RIGHT = new BitSet(0x110000); private static final BitSet JOIN_TYPE_TRANSPARENT = new BitSet(0x110000); + private static volatile boolean joiningTypesLoaded; static { // TODO: Should we initialize this lazily? ARABIC_INDIC_DIGITS.set(0x0660, 0x066A); EXTENDED_ARABIC_INDIC_DIGITS.set(0x06F0, 0x6FA); - GREEK_CHARACTERS.set(0x0370, 0x0400); - GREEK_CHARACTERS.set(0x1F00, 0x2000); - HEBREW_CHARACTERS.set(0x0590, 0x0600); - KATAKANA_CHARACTERS.set(0x2E80, 0x2F00); // The CJK Radicals Supplement code block - KATAKANA_CHARACTERS.set(0x2F00, 0x2FE0); // The Kangxi Radicals code block - KATAKANA_CHARACTERS.set(0x3000, 0x3040); // The CJK Symbols and Punctuation code block - KATAKANA_CHARACTERS.set(0x3040, 0x30A0); // The Hiragana code block. - KATAKANA_CHARACTERS.set(0x30A0, 0x3100); // The Katakana code block. - KATAKANA_CHARACTERS.set(0x3400, 0x4DC0); // The CJK Unified Ideographs Extension A code block - KATAKANA_CHARACTERS.set(0x4E00, 0xA000); // The CJK Unified Ideographs code block - KATAKANA_CHARACTERS.set(0xF900, 0xFB00); // The CJK Compatibility Ideographs code block - KATAKANA_CHARACTERS.set(0x16FE0, 0x17000); // The Ideographic Symbols and Punctuation code block - KATAKANA_CHARACTERS.set(0x20000, 0x2A6E0); // The CJK Unified Ideographs Extension B code block - KATAKANA_CHARACTERS.set(0x2A700, 0x2B740); // The CJK Unified Ideographs Extension C code block - KATAKANA_CHARACTERS.set(0x2B740, 0x2B820); // The CJK Unified Ideographs Extension D code block - KATAKANA_CHARACTERS.set(0x2B820, 0x2CEB0); // The CJK Unified Ideographs Extension E code block - KATAKANA_CHARACTERS.set(0x2CEB0, 0x2EBF0); // The CJK Unified Ideographs Extension F code block - KATAKANA_CHARACTERS.set(0x2F800, 0x2FA20); // The CJK Compatibility Ideographs Supplement code block - KATAKANA_CHARACTERS.set(0x30000, 0x31350); // The CJK Unified Ideographs Extension G code block - KATAKANA_CHARACTERS.set(0x31350, 0x323B0); // The CJK Unified Ideographs Extension H code block } public static boolean isArabicIndicDigit(int codepoint) { @@ -49,45 +27,33 @@ public static boolean isExtendedArabicIndicDigit(int codepoint) { return EXTENDED_ARABIC_INDIC_DIGITS.get(codepoint); } - public static boolean isGreek(int codepoint) { - return GREEK_CHARACTERS.get(codepoint); - } - - public static boolean isHebrew(int codepoint) { - return HEBREW_CHARACTERS.get(codepoint); - } - - public static boolean isKatakana(int codepoint) { - return KATAKANA_CHARACTERS.get(codepoint); - } - public static boolean isJoinTypeCausing(int codepoint) { - if (JOIN_TYPE_CAUSING.isEmpty()) loadJoiningTypes(); + if (!joiningTypesLoaded) loadJoiningTypes(); return JOIN_TYPE_CAUSING.get(codepoint); } public static boolean isJoinTypeDual(int codepoint) { - if (JOIN_TYPE_DUAL.isEmpty()) loadJoiningTypes(); + if (!joiningTypesLoaded) loadJoiningTypes(); return JOIN_TYPE_DUAL.get(codepoint); } public static boolean isJoinTypeLeft(int codepoint) { - if (JOIN_TYPE_LEFT.isEmpty()) loadJoiningTypes(); + if (!joiningTypesLoaded) loadJoiningTypes(); return JOIN_TYPE_LEFT.get(codepoint); } public static boolean isJoinTypeRight(int codepoint) { - if (JOIN_TYPE_RIGHT.isEmpty()) loadJoiningTypes(); + if (!joiningTypesLoaded) loadJoiningTypes(); return JOIN_TYPE_RIGHT.get(codepoint); } public static boolean isJoinTypeTransparent(int codepoint) { - if (JOIN_TYPE_TRANSPARENT.isEmpty()) loadJoiningTypes(); + if (!joiningTypesLoaded) loadJoiningTypes(); return JOIN_TYPE_TRANSPARENT.get(codepoint); } private static synchronized void loadJoiningTypes() { - if (JOIN_TYPE_DUAL.isEmpty()) { + if (!joiningTypesLoaded) { UCDLoader.loadMapping("/ucd/extracted/DerivedJoiningType.txt", v -> { switch (v) { case "C": return JOIN_TYPE_CAUSING; @@ -98,6 +64,8 @@ private static synchronized void loadJoiningTypes() { default: return null; } }); + // The volatile write publishes the filled tables to threads that read the flag without the lock. + joiningTypesLoaded = true; } } diff --git a/src/test/java/com/networknt/schema/format/HostnameFormatTest.java b/src/test/java/com/networknt/schema/format/HostnameFormatTest.java new file mode 100644 index 000000000..47af98544 --- /dev/null +++ b/src/test/java/com/networknt/schema/format/HostnameFormatTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.format; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class HostnameFormatTest { + + private static final HostnameFormat FORMAT = new HostnameFormat(); + + private static String repeat(String s, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) sb.append(s); + return sb.toString(); + } + + private static boolean matches(String value) { + return FORMAT.matches(null, value); + } + + @ParameterizedTest + @ValueSource(strings = { + "example", + "example.com", + "a.b-c.d1", + "xn--bcher-kva.example", + "XN--BCHER-KVA.EXAMPLE", + "xn--Bcher-kva.example", + "xn--9d6hc.example", + "xn--3zc0eybz95j.example", + // RFC 1123 labels beside a right-to-left A-label: the Bidi Rule applies within the A-label only. + "example.xn--mgbaam7a8h", + "3com.xn--mgbaam7a8h", + "123.xn--mgbaam7a8h", + "1a.xn--4dbc", + "a1.xn--4dbc", + "xn--4dbc.xn--mgbh0fb" + }) + void accepts(String value) { + assertTrue(matches(value), value); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "example.com.", + ".example.com", + "a..b", + "-example.com", + "example-.com", + "exa mple.com", + "xn--X", + "xn--abc-", + "xn--hello-txk.example", + "xn--ls8h.la", + "xn--e28h.example", + "xn--bd6h.example", + "xn--11-88t.example", + // RFC 5893 Bidi Rule within an A-label + "xn--1-0hc.example", // "1\u05D0": rule 1, a right-to-left label starts with a digit + "xn--0-zhc74b.example", // "\u05D00\u0660": rule 4, European and Arabic-Indic digits mixed + "xn--a-0hc.example" // "a\u05D0": rule 5, a left-to-right label with a right-to-left character + }) + void rejects(String value) { + assertFalse(matches(value), value); + } + + @Test + void limitsTheNameTo253Characters() { + String label = repeat("a", 63); + String name253 = label + "." + label + "." + label + "." + repeat("a", 61); + assertTrue(matches(name253)); + assertFalse(matches(name253 + "a")); + } + + @Test + void limitsEachLabelTo63Characters() { + assertTrue(matches(repeat("a", 63) + ".com")); + assertFalse(matches(repeat("a", 64) + ".com")); + assertFalse(matches("xn--" + repeat("a", 60) + ".com")); + } +} diff --git a/src/test/java/com/networknt/schema/utils/PunycodeTest.java b/src/test/java/com/networknt/schema/utils/PunycodeTest.java new file mode 100644 index 000000000..1c784a717 --- /dev/null +++ b/src/test/java/com/networknt/schema/utils/PunycodeTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class PunycodeTest { + + private static String codePoints(int... codePoints) { + return new String(codePoints, 0, codePoints.length); + } + + /** Sample strings from RFC 3492 section 7.1, plus the common "bücher" example. */ + static Stream rfc3492Samples() { + return Stream.of( + // (A) Arabic (Egyptian) + Arguments.of(codePoints(0x0644, 0x064A, 0x0647, 0x0645, 0x0627, 0x0628, 0x062A, 0x0643, 0x0644, + 0x0645, 0x0648, 0x0634, 0x0639, 0x0631, 0x0628, 0x064A, 0x061F), "egbpdaj6bu4bxfgehfvwxn"), + // (B) Chinese (simplified) + Arguments.of(codePoints(0x4ED6, 0x4EEC, 0x4E3A, 0x4EC0, 0x4E48, 0x4E0D, 0x8BF4, 0x4E2D, 0x6587), + "ihqwcrb4cv8a8dqg056pqjye"), + // (L) 3B, mixed case basic code points + Arguments.of(codePoints(0x0033, 0x5E74, 0x0042, 0x7D44, 0x91D1, 0x516B, 0x5148, 0x751F), + "3B-ww4c5e180e575a65lsy2b"), + // (R) + Arguments.of(codePoints(0x305D, 0x306E, 0x30B9, 0x30D4, 0x30FC, 0x30C9, 0x3067), "d9juau41awczczp"), + // (S) -> $1.00 <-, basic code points only + Arguments.of("-> $1.00 <-", "-> $1.00 <--"), + Arguments.of("bücher", "bcher-kva"), + // A supplementary-plane code point (U+1F4A9) + Arguments.of(codePoints(0x1F4A9), "ls8h")); + } + + @ParameterizedTest + @MethodSource("rfc3492Samples") + void decodes(String unicode, String punycode) { + assertEquals(unicode, Punycode.decode(punycode)); + } + + @ParameterizedTest + @MethodSource("rfc3492Samples") + void encodes(String unicode, String punycode) { + assertEquals(punycode, Punycode.encode(unicode)); + } + + @Test + void rejectsOverflow() { + assertNull(Punycode.decode("99999999999")); + } + + @Test + void rejectsSurrogatesAndCodePointsAboveTheUnicodeRange() { + assertNull(Punycode.decode("ib9b")); // U+D800, a lone surrogate + assertNull(Punycode.decode("bb00h")); // above U+10FFFF without overflowing an int + } + + @Test + void rejectsTruncatedInput() { + // "k" is a digit that needs a following digit to complete the variable-length integer. + assertNull(Punycode.decode("bcher-k")); + } + + @Test + void readsDelimiterAtIndexZeroAsADigit() { + // With no basic code points before it, the delimiter is not consumed, and "-" is not a digit. + assertNull(Punycode.decode("-kva")); + } + + @Test + void rejectsNonBasicCodePointsBeforeTheDelimiter() { + assertNull(Punycode.decode("bücher-kva")); + } + + @Test + void rejectsNonDigitsAfterTheDelimiter() { + assertNull(Punycode.decode("bcher-kvä")); + } +} diff --git a/src/test/java/com/networknt/schema/utils/RFC5892Test.java b/src/test/java/com/networknt/schema/utils/RFC5892Test.java new file mode 100644 index 000000000..a820439bb --- /dev/null +++ b/src/test/java/com/networknt/schema/utils/RFC5892Test.java @@ -0,0 +1,425 @@ +/* + * Copyright (c) 2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; +import org.opentest4j.TestAbortedException; + +/** + * Tests the {@code idn-hostname} rules in {@link RFC5892#isValid(String)} and the {@code hostname} + * A-label rules in {@link RFC5892#isValidALabel(String)}. + */ +class RFC5892Test { + + // Scripts added in Unicode 17.0, the version after VIRAMAS (16.0). The JDK has no public API for its Unicode + // version, but UnicodeScript.valueOf throws for an unknown name, so any of these resolving means newer than 16.0. + private static final String[] UNICODE_17_SCRIPTS = { "BERIA_ERFE", "SIDETIC", "TAI_YO", "TOLONG_SIKI" }; + + private static String repeat(String s, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) sb.append(s); + return sb.toString(); + } + + @Test + void uLabelAndItsALabelAgree() { + // 40 x U+00DF is a 46-character A-label: both forms are within the 63-character limit. + String uLabel = repeat("ß", 40); + String aLabel = "xn--" + Punycode.encode(uLabel); + assertTrue(aLabel.length() <= 63); + assertTrue(RFC5892.isValid(uLabel)); + assertTrue(RFC5892.isValid(aLabel)); + } + + @Test + void uLabelLengthIsMeasuredInALabelForm() { + // 60 code points, but the A-label is longer than 63 characters. + String uLabel = repeat("äb", 30); + assertTrue(("xn--" + Punycode.encode(uLabel)).length() > 63); + assertFalse(RFC5892.isValid(uLabel)); + } + + @Test + void aLabelsAreCaseInsensitive() { + assertTrue(RFC5892.isValid("xn--bcher-kva.example")); + assertTrue(RFC5892.isValid("XN--BCHER-KVA.example")); + assertTrue(RFC5892.isValid("xn--Bcher-kva.example")); + assertTrue(RFC5892.isValidALabel("XN--BCHER-KVA")); + assertTrue(RFC5892.isValidALabel("xn--Bcher-kva")); + } + + @Test + void overallLengthIsLimitedTo253Characters() { + String label = repeat("a", 63); + String name253 = label + "." + label + "." + label + "." + repeat("a", 61); + assertTrue(RFC5892.isValid(name253)); + assertFalse(RFC5892.isValid(name253 + "a")); + assertTrue(RFC5892.isValid(name253 + ".")); // the root separator does not count + assertFalse(RFC5892.isValid(name253 + "..")); + } + + @Test + void labelLongerThan63CharactersIsRejectedBeforeDecoding() { + assertFalse(RFC5892.isValid("xn--" + repeat("a", 60))); + assertFalse(RFC5892.isValidALabel("xn--" + repeat("a", 60))); + // A very long label is rejected by its length alone. + assertFalse(RFC5892.isValid("xn--" + repeat("99", 100_000))); + } + + @Test + void aLabelMustRoundTrip() { + assertFalse(RFC5892.isValidALabel("xn--X")); + // Decodes to ASCII only. + assertFalse(RFC5892.isValidALabel("xn--abc-")); + } + + private static String codePoints(int... codePoints) { + return new String(codePoints, 0, codePoints.length); + } + + @Test + void allowsLettersNewerThanTheBundledTable() { + // Adlam small letters (Unicode 9, Ll) are not in the Unicode 5.2 table. + assertTrue(RFC5892.isValidALabel("xn--9d6hc")); + assertTrue(RFC5892.isValid("xn--9d6hc.example")); + assertTrue(RFC5892.isValid(codePoints(0x1E922, 0x1E923) + ".example")); + } + + @Test + void rejectsSymbolsUpperCaseAndControlsNewerThanTheBundledTable() { + // U+1F600 and U+1F4A9 (emoji, So) + assertFalse(RFC5892.isValidALabel("xn--e28h")); + assertFalse(RFC5892.isValidALabel("xn--ls8h")); + assertFalse(RFC5892.isValid("xn--ls8h.la")); + // U+20BF BITCOIN SIGN (Sc) + assertFalse(RFC5892.isValidALabel("xn--a-0qn")); + // U+1C90 GEORGIAN MTAVRULI CAPITAL LETTER AN (Lu) + assertFalse(RFC5892.isValidALabel("xn--j4f")); + // U+1E900 ADLAM CAPITAL LETTER ALIF (Lu) + assertFalse(RFC5892.isValidALabel("xn--bd6h")); + // U+2066 LEFT-TO-RIGHT ISOLATE (Cf) + assertFalse(RFC5892.isValidALabel("xn--11-88t")); + } + + @Test + void zeroWidthJoinersMayFollowAnyVirama() { + // Devanagari, Sinhala and Malayalam ZWJ, Bengali ZWNJ + assertTrue(RFC5892.isValidALabel("xn--11b2ezcw70k")); + assertTrue(RFC5892.isValidALabel("xn--3zc0eybz95j")); + assertTrue(RFC5892.isValidALabel("xn--bwca1om25e")); + assertTrue(RFC5892.isValidALabel("xn--p5ba1o498f")); + // A ZWJ after a consonant, not a virama + assertFalse(RFC5892.isValid(codePoints(0x0D15, 0x200D, 0x0D15))); + } + + @Test + void hyphenPositionsAreCountedInCodePoints() { + // "--" in the 3rd and 4th code points is invalid, whether or not the first two are supplementary. + assertFalse(RFC5892.isValid(codePoints(0x4E00, 0x4E01, '-', '-', 'a'))); + assertFalse(RFC5892.isValid(codePoints(0x20000, 0x20001, '-', '-', 'a'))); + assertTrue(RFC5892.isValid(codePoints(0x20000, '-', '-', 'a'))); + } + + @Test + void hostnameALabelsStillApplyTheIdnaRules() { + // Leading combining mark + assertFalse(RFC5892.isValidALabel("xn--hello-txk")); + // Disallowed U+302E + assertFalse(RFC5892.isValidALabel("xn--07jt112bpxg")); + // ZERO WIDTH JOINER not preceded by anything + assertFalse(RFC5892.isValidALabel("xn--02b508i")); + // ZERO WIDTH JOINER preceded by a virama + assertTrue(RFC5892.isValidALabel("xn--11b2ezcw70k")); + } + + @Test + void appliesTheBidiRuleToALabelWithRightToLeftCharacters() { + // Rule 1: a label with a Hebrew letter must not start with a digit. + assertFalse(RFC5892.isValid("1\u05d0")); + assertFalse(RFC5892.isValidALabel("xn--" + Punycode.encode("1\u05d0"))); + // Rule 4: European and Arabic-Indic digits must not both appear in an RTL label. + assertFalse(RFC5892.isValid("\u06271\u0661")); + assertTrue(RFC5892.isValid("\u05d01")); + assertTrue(RFC5892.isValid("\u0627\u0661")); + } + + @Test + void cherokeeCapitalsStayValidAndItsSmallLettersAreNot() { + // Cherokee case folding maps small letters (Unicode 8) to the capitals, which stay PVALID. + assertTrue(RFC5892.isValid(codePoints(0x13A0, 0x13A1))); + assertFalse(RFC5892.isValid(codePoints(0xAB70, 0xAB71))); + } + + @Test + void katakanaMiddleDotNeedsAKanaOrHanCharacterAnywhereInTheLabel() { + // RFC 5892 A.7: any Hiragana, Katakana or Han character in the label, judged by script, not code block. + assertTrue(RFC5892.isValid(codePoints(0x4E00, 0x30FB))); + assertTrue(RFC5892.isValidALabel("xn--vek768f")); + assertTrue(RFC5892.isValid(codePoints(0x30FB, 'a', 0x4E00))); + assertFalse(RFC5892.isValid(codePoints(0x30FB, 0x30FC))); // PROLONGED SOUND MARK is Common, not Katakana + assertFalse(RFC5892.isValidALabel("xn--vekc")); + } + + @Test + void greekKeraiaAndHebrewPunctuationAreJudgedByScript() { + assertTrue(RFC5892.isValid(codePoints(0x0375, 0x1D26))); // GREEK LETTER SMALL CAPITAL GAMMA, outside the Greek blocks + assertFalse(RFC5892.isValid(codePoints(0x03B1, 0x0375, 0x03E3))); // COPTIC SMALL LETTER SHEI, inside the Greek block + assertFalse(RFC5892.isValidALabel("xn--wva3j0f")); + } + + @Test + void zeroWidthNonJoinerSkipsSupplementaryTransparentMarks() { + // Adlam letters are dual joining and U+1E944 ADLAM ALIF LENGTHENER is transparent. + assertTrue(RFC5892.isValid(codePoints(0x1E922, 0x1E944, 0x200C, 0x1E944, 0x1E923))); + assertTrue(RFC5892.isValidALabel("xn--0ug1411pea4kb")); + assertFalse(RFC5892.isValid(codePoints(0x1E922, 0x200C, 0x1E944, 0x1E944))); + } + + @Test + void followsTheUnicode60CategoryChangesListedInRfc6452() { + assertTrue(RFC5892.isValid(codePoints(0x0CF1))); + assertTrue(RFC5892.isValidALabel("xn--9uc")); + assertFalse(RFC5892.isValid(codePoints('a', 0x19DA))); + } + + @Test + void rejectsNewCodePointsThatAreUnstableUnderCaseFoldingOrIgnorable() { + assertFalse(RFC5892.isValid(codePoints('a', 0x1C80))); // CYRILLIC SMALL LETTER ROUNDED VE folds to U+0432 + assertFalse(RFC5892.isValidALabel("xn--a-wyl")); + assertTrue(RFC5892.isValid(codePoints(0x13F5))); // CHEROKEE LETTER MV, a capital added in Unicode 8 + } + + @Test + void rejectsTheDefaultIgnorableMongolianFreeVariationSelectorFour() { + // On a JDK older than Unicode 14 the code point is unassigned, so the assertion would pass for the wrong reason. + assumeTrue(Character.isDefined(0x180F)); + assertFalse(RFC5892.isValid(codePoints('a', 0x180F))); + } + + private static int[] viramas() throws Exception { + Field field = RFC5892.class.getDeclaredField("VIRAMAS"); + field.setAccessible(true); + return (int[]) field.get(null); + } + + @Test + void viramaTableIsSortedForBinarySearch() throws Exception { + int[] viramas = viramas(); + for (int n = 1; n < viramas.length; n++) assertTrue(viramas[n - 1] < viramas[n], "unsorted at index " + n); + } + + @Test + void viramaTableMatchesTheCombiningClassesOfTheRunningJdk() throws Exception { + Set table = new TreeSet<>(); + for (int virama : viramas()) { + if (Character.isDefined(virama)) table.add(virama); + } + Set derived = new TreeSet<>(); + for (int c = 0; c <= Character.MAX_CODE_POINT; c++) { + if (Character.isDefined(c) && hasCombiningClassNine(c)) derived.add(c); + } + Set extra = new TreeSet<>(table); + extra.removeAll(derived); + assertTrue(extra.isEmpty(), "VIRAMAS lists code points that are not Canonical_Combining_Class 9: " + hex(extra)); + Set missing = new TreeSet<>(derived); + missing.removeAll(table); + checkMissingViramas(missing, unicodeIsNewerThan16(RFC5892Test::jdkHasScript)); + } + + private static void checkMissingViramas(Set missing, boolean jdkNewerThan16) { + if (jdkNewerThan16) { + // The table is Unicode 16.0; a newer JDK may know newer viramas (17.0 adds none, so in practice from + // 18.0). Abort with the list, so it shows in the test report as a skipped test, without failing + // unrelated builds. + assumeTrue(missing.isEmpty(), () -> "Viramas newer than Unicode 16.0 missing from VIRAMAS: " + hex(missing)); + } else { + assertTrue(missing.isEmpty(), "VIRAMAS is missing Canonical_Combining_Class 9 code points: " + hex(missing)); + } + } + + @Test + void missingViramasAreSkippedOnNewerJdksAndFailOtherwise() { + Set none = new TreeSet<>(); + Set one = new TreeSet<>(List.of(0x1E6F5)); + checkMissingViramas(none, true); + checkMissingViramas(none, false); + TestAbortedException skipped = assertThrows(TestAbortedException.class, () -> checkMissingViramas(one, true)); + assertTrue(skipped.getMessage().contains("U+1E6F5")); + assertThrows(AssertionFailedError.class, () -> checkMissingViramas(one, false)); + } + + @Test + void detectsUnicodeNewerThan16WhenAnyUnicode17ScriptIsKnown() { + assertTrue(unicodeIsNewerThan16(name -> true)); + assertTrue(unicodeIsNewerThan16("TOLONG_SIKI"::equals)); + assertFalse(unicodeIsNewerThan16(name -> false)); + assertFalse(unicodeIsNewerThan16("TULU_TIGALARI"::equals)); // a Unicode 16.0 script + } + + @Test + void jdkScriptLookupMatchesTheCodePointsTheJdkDefines() { + // The blocks of the Unicode 17.0 scripts are unassigned in 16.0, so the JDK defines code points there exactly + // when it knows those scripts. On JDKs before 26 both sides are false; the test above covers the true result. + int[] blockStarts = { 0x16EA0, 0x10940, 0x1E6C0, 0x11DB0 }; // Beria Erfe, Sidetic, Tai Yo, Tolong Siki + boolean defined = false; + for (int start : blockStarts) { + for (int c = start; c < start + 16; c++) defined |= Character.isDefined(c); + } + assertEquals(defined, unicodeIsNewerThan16(RFC5892Test::jdkHasScript)); + assertTrue(jdkHasScript("LATIN")); + assertFalse(jdkHasScript("NOT_A_SCRIPT")); + } + + private static boolean unicodeIsNewerThan16(Predicate hasScript) { + for (String name : UNICODE_17_SCRIPTS) { + if (hasScript.test(name)) return true; + } + return false; + } + + private static boolean jdkHasScript(String name) { + try { + Character.UnicodeScript.valueOf(name); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static String hex(Set codePoints) { + StringBuilder sb = new StringBuilder(); + for (int c : codePoints) sb.append(sb.length() == 0 ? "" : ", ").append(String.format("U+%04X", c)); + return sb.toString(); + } + + // Canonical ordering (UAX 15) swaps adjacent marks when the first has the higher non-zero class. + // U+3099 has class 8 and U+05B0 class 10, so X has class 9 exactly when "X U+3099" and "U+05B0 X" both reorder. + private static boolean hasCombiningClassNine(int c) { + String x = codePoints(c); + if (!x.equals(Normalizer.normalize(x, Normalizer.Form.NFD))) return false; + String above8 = "a" + x + "\u3099"; + String below10 = "a\u05B0" + x; + return !above8.equals(Normalizer.normalize(above8, Normalizer.Form.NFD)) + && !below10.equals(Normalizer.normalize(below10, Normalizer.Form.NFD)); + } + + @Test + void zeroWidthJoinersMayFollowUnicode16Viramas() { + assumeTrue(Character.isDefined(0x113CE)); // TULU-TIGALARI SIGN VIRAMA, Unicode 16 + assertTrue(RFC5892.isValid(codePoints(0x0915, 0x113CE, 0x200D, 0x0915))); + } + + @Test + void acceptsAtMostOneTrailingLabelSeparator() { + assertTrue(RFC5892.isValid("a.")); + assertTrue(RFC5892.isValid("a.b\u3002")); + assertTrue(RFC5892.isValid("xn--4dbc.")); + assertFalse(RFC5892.isValid("a..")); + assertFalse(RFC5892.isValid("a.\uff0e")); + assertFalse(RFC5892.isValid("xn--4dbc..")); + assertFalse(RFC5892.isValid(".")); + assertFalse(RFC5892.isValid("...")); + assertFalse(RFC5892.isValid(".a")); + assertFalse(RFC5892.isValid("a..b")); + } + + @Test + void appliesTheBidiRuleToEveryLabelOfABidiDomainName() { + // RFC 5893 rule 1: in a name with a right-to-left label, no label may start with a digit. + assertFalse(RFC5892.isValid("1a.\u05D0\u05D1")); + assertFalse(RFC5892.isValid("\u05D0\u05D1.1")); + assertFalse(RFC5892.isValid("0a.\u05D0")); + // Rule 6: a left-to-right label may end with a digit. + assertTrue(RFC5892.isValid("\u05D0\u05D1.a1")); + assertTrue(RFC5892.isValid("\u05D0\u05D1.example1")); + // A name without right-to-left labels is not a Bidi domain name. + assertTrue(RFC5892.isValid("1a.b\u00FCcher")); + // A-labels count as right-to-left labels once decoded. + assertFalse(RFC5892.isValid("1a.xn--4dbc")); + assertTrue(RFC5892.isValid("a1.xn--4dbc")); + // An A-label before the first right-to-left label is checked against the Bidi Rule once that label is reached. + assertTrue(RFC5892.isValid("xn--bcher-kva.xn--4dbc")); + assertFalse(RFC5892.isValid("a.xn--1-eha.xn--4dbc")); // "1\u00FC" starts with a digit + assertTrue(RFC5892.isValid("xn--1-eha.example")); // not a Bidi domain name + } + + @Test + void loadsTheUnicodeTablesSafelyOnConcurrentFirstUse() throws Exception { + URL classes = RFC5892.class.getProtectionDomain().getCodeSource().getLocation(); + for (int round = 0; round < 20; round++) { + // A fresh copy of the utils classes, so the lazily loaded tables start empty in every round. + try (URLClassLoader loader = new ChildFirstLoader(classes)) { + Method isValid = loader.loadClass(RFC5892.class.getName()).getMethod("isValid", String.class); + int threads = 8; + CountDownLatch start = new CountDownLatch(1); + ExecutorService pool = Executors.newFixedThreadPool(threads); + try { + List> results = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + // A ZWNJ between two dual-joining Arabic letters needs both the IDNA and the joining-type tables. + String name = t % 2 == 0 ? "\u0628\u200C\u0628" : "b\u00FCcher.\u0628\u200C\u0628"; + results.add(pool.submit(() -> { + start.await(); + return isValid.invoke(null, name); + })); + } + start.countDown(); + for (Future result : results) assertEquals(Boolean.TRUE, result.get()); + } finally { + pool.shutdownNow(); + } + } + } + } + + /** Loads the utils package itself and delegates everything else, such as the bundled tables' anchor class. */ + private static final class ChildFirstLoader extends URLClassLoader { + ChildFirstLoader(URL classes) { + super(new URL[] {classes}, RFC5892Test.class.getClassLoader()); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!name.startsWith("com.networknt.schema.utils.")) return super.loadClass(name, resolve); + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + return null != c ? c : findClass(name); + } + } + } +} diff --git a/src/test/suite/tests/draft-next/optional/format/hostname.json b/src/test/suite/tests/draft-next/optional/format/hostname.json index bc3a60dcc..a0011bdd1 100644 --- a/src/test/suite/tests/draft-next/optional/format/hostname.json +++ b/src/test/suite/tests/draft-next/optional/format/hostname.json @@ -42,43 +42,63 @@ "valid": true }, { - "description": "a valid punycoded IDN hostname", - "data": "xn--4gbwdl.xn--wgbh1c", + "description": "single label", + "data": "hostname", "valid": true }, { - "description": "a host name starting with an illegal character", - "data": "-a-host-name-that-starts-with--", + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true + }, + { + "description": "empty string", + "data": "", "valid": false }, { - "description": "a host name containing illegal characters", - "data": "not_a_valid_host_name", + "description": "single dot", + "data": ".", "valid": false }, { - "description": "a host name with a component too long", - "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "description": "leading dot", + "data": ".example", "valid": false }, { - "description": "starts with hyphen", - "data": "-hostname", + "description": "trailing dot", + "data": "example.", "valid": false }, { - "description": "ends with hyphen", - "data": "hostname-", + "description": "IDN label separator", + "data": "example\uff0ecom", "valid": false }, { - "description": "starts with underscore", - "data": "_hostname", + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "starts with hyphen", + "data": "-hostname", "valid": false }, { - "description": "ends with underscore", - "data": "hostname_", + "description": "ends with hyphen", + "data": "hostname-", "valid": false }, { @@ -87,48 +107,278 @@ "valid": false }, { - "description": "maximum label length", + "description": "exceeds maximum overall length (256)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": false + }, + { + "description": "maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", "valid": true }, { - "description": "exceeds maximum label length", + "description": "exceeds maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false }, { - "description": "single label", - "data": "hostname", + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "invalid non-ASCII KELVIN SIGN (U+212A)", + "data": "\u212Aelvin.example.com", + "valid": false + }, + { + "description": "consecutive hyphens inside a label", + "data": "a--b.com", "valid": true + } + ] + }, + { + "description": "validation of A-label (punycode) host names", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "format": "hostname" + }, + "tests": [ + { + "description": "invalid Punycode", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.4 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "xn--X", + "valid": false }, { - "description": "single label with hyphen", - "data": "host-name", + "description": "a valid host name (example.test in Hangul)", + "data": "xn--9n2bp8q.xn--9t4b11yi5a", "valid": true }, { - "description": "single label with digits", - "data": "h0stn4me", + "description": "contains illegal char U+302E Hangul single dot tone mark", + "data": "xn--07jt112bpxg.xn--9t4b11yi5a", + "valid": false + }, + { + "description": "Begins with a Spacing Combining Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-txk", + "valid": false + }, + { + "description": "Begins with a Nonspacing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-zed", + "valid": false + }, + { + "description": "Begins with an Enclosing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-6bf", + "valid": false + }, + { + "description": "Exceptions that are PVALID, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--zca29lwxobi7a", "valid": true }, { - "description": "single label starting with digit", - "data": "1host", + "description": "Exceptions that are PVALID, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--qmbc", "valid": true }, { - "description": "single label ending with digit", - "data": "hostnam3", + "description": "Exceptions that are DISALLOWED, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--chb89f", + "valid": false + }, + { + "description": "Exceptions that are DISALLOWED, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6 Note: The two combining marks (U+302E and U+302F) are in the middle and not at the start", + "data": "xn--07jceefgh4c", + "valid": false + }, + { + "description": "MIDDLE DOT with no preceding 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--al-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing preceding", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-fda", + "valid": false + }, + { + "description": "MIDDLE DOT with no following 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--la-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing following", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-gda", + "valid": false + }, + { + "description": "MIDDLE DOT with surrounding 'l's", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--ll-0ea", "valid": true }, { - "description": "empty string", - "data": "", + "description": "Greek KERAIA not followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--S-jib3p", "valid": false }, { - "description": "single dot", - "data": ".", + "description": "Greek KERAIA not followed by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3j", + "valid": false + }, + { + "description": "Greek KERAIA followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3je", + "valid": true + }, + { + "description": "Hebrew GERESH not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--A-2hc5h", + "valid": false + }, + { + "description": "Hebrew GERESH not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--5db1e", + "valid": false + }, + { + "description": "Hebrew GERESH preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--4dbc5h", + "valid": true + }, + { + "description": "Hebrew GERSHAYIM not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--A-2hc8h", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--5db3e", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--4dbc8h", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--defabc-k64e", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with no other characters", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with Hiragana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--k8j5u", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Katakana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--bck0j", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek778f", + "valid": true + }, + { + "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngb6iyr", + "valid": false + }, + { + "description": "Arabic-Indic digits not mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngba1o", + "valid": true + }, + { + "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", + "data": "xn--0-gyc", + "valid": true + }, + { + "description": "ZERO WIDTH JOINER not preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2er09f", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--02b508i", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2ezcw70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1", + "data": "xn--11b2ezcs70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", + "data": "xn--ngba5hb2804a", + "valid": true + }, + { + "description": "contains \"--\" in the 3rd and 4th position", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "XN--aa---o47jg78q", "valid": false } ] diff --git a/src/test/suite/tests/draft-next/optional/format/idn-hostname.json b/src/test/suite/tests/draft-next/optional/format/idn-hostname.json index 1061f4243..97ed1d314 100644 --- a/src/test/suite/tests/draft-next/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft-next/optional/format/idn-hostname.json @@ -52,8 +52,13 @@ "valid": false }, { - "description": "a host name with a component too long", - "data": "실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실례례테스트례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례테스트례례실례.테스트", + "description": "a single label of 63 characters is valid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": true + }, + { + "description": "a single label of 64 characters is too long", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "valid": false }, { @@ -331,6 +336,150 @@ "description": "empty string", "data": "", "valid": false + }, + { + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "fullwidth digits are mapped to ASCII digits", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt maps U+FF11 to 0031, so this label maps to 123", + "data": "\uff11\uff12\uff13", + "valid": true + }, + { + "description": "a zero width space is ignored by the mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt lists U+200B as ignored, so this label maps to ab", + "data": "a\u200bb", + "valid": true + }, + { + "description": "an A-label with an uppercase Punycode body is valid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.3 lowercases the A-label before the round-trip test, and RFC 3492 section 5 gives A-Z and a-z the same digit-values", + "data": "xn--NXASMQ6B", + "valid": true + }, + { + "description": "the two Arabic-Indic digit blocks may appear in different labels", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.8 scopes the rule to \"this label\", so one block per label is valid", + "data": "\u0628\u0660.\u0628\u06f0", + "valid": true + }, + { + "description": "a label of only Arabic-Indic digits is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 condition 1 allows only L, R or AL as the first character, and an Arabic-Indic digit is AN", + "data": "\u0660\u0661", + "valid": false + }, + { + "description": "a non-NFC U-label is valid after UTS 46 normalisation", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 section 4 normalises to NFC before validating, so the decomposed form is accepted", + "data": "cafe\u0301.com", + "valid": true + }, + { + "description": "a label of 63 fullwidth characters is valid after mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps each U+FF41 to an ASCII letter, so this label is 63 octets in its mapped form", + "data": "\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41", + "valid": true + }, + { + "description": "an ACE prefix produced by the mapping step is decoded as an A-label", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps U+FF58 U+FF4E to \"xn\", so the mapped label is the A-label xn--nxasmq6b", + "data": "\uff58\uff4e--nxasmq6b", + "valid": true + }, + { + "description": "zero width non-joiner must pass at every occurrence", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.1", + "data": "\u0915\u094d\u200c\u0937x\u200cy", + "valid": false + }, + { + "description": "Bidi domain name with a digit-first label is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 a label in a Bidi domain name must start with an L, R or AL character", + "data": "0a.\u05d0", + "valid": false + }, + { + "description": "label starting with a digit before a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "0\u0627", + "valid": false + }, + { + "description": "left-to-right label containing a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "a\u05d0", + "valid": false + }, + { + "description": "right-to-left label mixing both digit types is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "\u05d00\u0660", + "valid": false + }, + { + "description": "A-label that decodes to a disallowed code point is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5892#section-2.6", + "data": "xn--7a", + "valid": false + }, + { + "description": "A-label that decodes to a Bidi rule violation is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5893", + "data": "xn--0ca24w", + "valid": false + }, + { + "description": "a U-label whose A-label form is longer than 63 octets is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-4.2.4 the 63-octet limit is on the A-label form, not the code point count", + "data": "\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc", + "valid": false + }, + { + "description": "empty label between two dots is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc1034#section-3.1", + "data": "a..b", + "valid": false + }, + { + "description": "a name longer than 253 characters is invalid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": false + }, + { + "description": "A-label that decodes to only ASCII is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 a U-label must have at least one non-ASCII character", + "data": "xn--example-", + "valid": false + }, + { + "description": "non-canonical Punycode that does not re-encode to itself is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.4 a label decoded from Punycode must be identical to the original A-label", + "data": "xn---9uc", + "valid": false } ] }, @@ -383,6 +532,74 @@ "description": "halfwidth ideographic full stop as label separator", "data": "a\uff61b", "valid": true + }, + { + "description": "leading dot", + "data": ".example", + "valid": false + }, + { + "description": "leading ideographic full stop", + "data": "\u3002example", + "valid": false + }, + { + "description": "leading fullwidth full stop", + "data": "\uff0eexample", + "valid": false + }, + { + "description": "leading halfwidth ideographic full stop", + "data": "\uff61example", + "valid": false + }, + { + "description": "trailing dot", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example.", + "valid": false + }, + { + "description": "trailing ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\u3002", + "valid": false + }, + { + "description": "trailing fullwidth full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff0e", + "valid": false + }, + { + "description": "trailing halfwidth ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff61", + "valid": false + }, + { + "description": "label too long if separator ignored (full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα.com", + "valid": true + }, + { + "description": "label too long if separator ignored (ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\u3002com", + "valid": true + }, + { + "description": "label too long if separator ignored (fullwidth full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff0ecom", + "valid": true + }, + { + "description": "label too long if separator ignored (halfwidth ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff61com", + "valid": true } ] } diff --git a/src/test/suite/tests/draft2019-09/optional/format/hostname.json b/src/test/suite/tests/draft2019-09/optional/format/hostname.json index 24bfdfc5a..36bb9521f 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/hostname.json +++ b/src/test/suite/tests/draft2019-09/optional/format/hostname.json @@ -42,43 +42,63 @@ "valid": true }, { - "description": "a valid punycoded IDN hostname", - "data": "xn--4gbwdl.xn--wgbh1c", + "description": "single label", + "data": "hostname", "valid": true }, { - "description": "a host name starting with an illegal character", - "data": "-a-host-name-that-starts-with--", + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true + }, + { + "description": "empty string", + "data": "", "valid": false }, { - "description": "a host name containing illegal characters", - "data": "not_a_valid_host_name", + "description": "single dot", + "data": ".", "valid": false }, { - "description": "a host name with a component too long", - "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "description": "leading dot", + "data": ".example", "valid": false }, { - "description": "starts with hyphen", - "data": "-hostname", + "description": "trailing dot", + "data": "example.", "valid": false }, { - "description": "ends with hyphen", - "data": "hostname-", + "description": "IDN label separator", + "data": "example\uff0ecom", "valid": false }, { - "description": "starts with underscore", - "data": "_hostname", + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "starts with hyphen", + "data": "-hostname", "valid": false }, { - "description": "ends with underscore", - "data": "hostname_", + "description": "ends with hyphen", + "data": "hostname-", "valid": false }, { @@ -87,48 +107,278 @@ "valid": false }, { - "description": "maximum label length", + "description": "exceeds maximum overall length (256)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": false + }, + { + "description": "maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", "valid": true }, { - "description": "exceeds maximum label length", + "description": "exceeds maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false }, { - "description": "single label", - "data": "hostname", + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "invalid non-ASCII KELVIN SIGN (U+212A)", + "data": "\u212Aelvin.example.com", + "valid": false + }, + { + "description": "consecutive hyphens inside a label", + "data": "a--b.com", "valid": true + } + ] + }, + { + "description": "validation of A-label (punycode) host names", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "format": "hostname" + }, + "tests": [ + { + "description": "invalid Punycode", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.4 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "xn--X", + "valid": false }, { - "description": "single label with hyphen", - "data": "host-name", + "description": "a valid host name (example.test in Hangul)", + "data": "xn--9n2bp8q.xn--9t4b11yi5a", "valid": true }, { - "description": "single label with digits", - "data": "h0stn4me", + "description": "contains illegal char U+302E Hangul single dot tone mark", + "data": "xn--07jt112bpxg.xn--9t4b11yi5a", + "valid": false + }, + { + "description": "Begins with a Spacing Combining Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-txk", + "valid": false + }, + { + "description": "Begins with a Nonspacing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-zed", + "valid": false + }, + { + "description": "Begins with an Enclosing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-6bf", + "valid": false + }, + { + "description": "Exceptions that are PVALID, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--zca29lwxobi7a", "valid": true }, { - "description": "single label starting with digit", - "data": "1host", + "description": "Exceptions that are PVALID, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--qmbc", "valid": true }, { - "description": "single label ending with digit", - "data": "hostnam3", + "description": "Exceptions that are DISALLOWED, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--chb89f", + "valid": false + }, + { + "description": "Exceptions that are DISALLOWED, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6 Note: The two combining marks (U+302E and U+302F) are in the middle and not at the start", + "data": "xn--07jceefgh4c", + "valid": false + }, + { + "description": "MIDDLE DOT with no preceding 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--al-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing preceding", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-fda", + "valid": false + }, + { + "description": "MIDDLE DOT with no following 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--la-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing following", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-gda", + "valid": false + }, + { + "description": "MIDDLE DOT with surrounding 'l's", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--ll-0ea", "valid": true }, { - "description": "empty string", - "data": "", + "description": "Greek KERAIA not followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--S-jib3p", "valid": false }, { - "description": "single dot", - "data": ".", + "description": "Greek KERAIA not followed by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3j", + "valid": false + }, + { + "description": "Greek KERAIA followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3je", + "valid": true + }, + { + "description": "Hebrew GERESH not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--A-2hc5h", + "valid": false + }, + { + "description": "Hebrew GERESH not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--5db1e", + "valid": false + }, + { + "description": "Hebrew GERESH preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--4dbc5h", + "valid": true + }, + { + "description": "Hebrew GERSHAYIM not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--A-2hc8h", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--5db3e", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--4dbc8h", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--defabc-k64e", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with no other characters", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with Hiragana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--k8j5u", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Katakana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--bck0j", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek778f", + "valid": true + }, + { + "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngb6iyr", + "valid": false + }, + { + "description": "Arabic-Indic digits not mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngba1o", + "valid": true + }, + { + "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", + "data": "xn--0-gyc", + "valid": true + }, + { + "description": "ZERO WIDTH JOINER not preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2er09f", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--02b508i", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2ezcw70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1", + "data": "xn--11b2ezcs70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", + "data": "xn--ngba5hb2804a", + "valid": true + }, + { + "description": "contains \"--\" in the 3rd and 4th position", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "XN--aa---o47jg78q", "valid": false } ] diff --git a/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json b/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json index 348c504c8..a416fdda1 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft2019-09/optional/format/idn-hostname.json @@ -52,8 +52,13 @@ "valid": false }, { - "description": "a host name with a component too long", - "data": "실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실례례테스트례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례테스트례례실례.테스트", + "description": "a single label of 63 characters is valid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": true + }, + { + "description": "a single label of 64 characters is too long", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "valid": false }, { @@ -331,6 +336,150 @@ "description": "empty string", "data": "", "valid": false + }, + { + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "fullwidth digits are mapped to ASCII digits", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt maps U+FF11 to 0031, so this label maps to 123", + "data": "\uff11\uff12\uff13", + "valid": true + }, + { + "description": "a zero width space is ignored by the mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt lists U+200B as ignored, so this label maps to ab", + "data": "a\u200bb", + "valid": true + }, + { + "description": "an A-label with an uppercase Punycode body is valid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.3 lowercases the A-label before the round-trip test, and RFC 3492 section 5 gives A-Z and a-z the same digit-values", + "data": "xn--NXASMQ6B", + "valid": true + }, + { + "description": "the two Arabic-Indic digit blocks may appear in different labels", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.8 scopes the rule to \"this label\", so one block per label is valid", + "data": "\u0628\u0660.\u0628\u06f0", + "valid": true + }, + { + "description": "a label of only Arabic-Indic digits is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 condition 1 allows only L, R or AL as the first character, and an Arabic-Indic digit is AN", + "data": "\u0660\u0661", + "valid": false + }, + { + "description": "a non-NFC U-label is valid after UTS 46 normalisation", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 section 4 normalises to NFC before validating, so the decomposed form is accepted", + "data": "cafe\u0301.com", + "valid": true + }, + { + "description": "a label of 63 fullwidth characters is valid after mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps each U+FF41 to an ASCII letter, so this label is 63 octets in its mapped form", + "data": "\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41", + "valid": true + }, + { + "description": "an ACE prefix produced by the mapping step is decoded as an A-label", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps U+FF58 U+FF4E to \"xn\", so the mapped label is the A-label xn--nxasmq6b", + "data": "\uff58\uff4e--nxasmq6b", + "valid": true + }, + { + "description": "zero width non-joiner must pass at every occurrence", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.1", + "data": "\u0915\u094d\u200c\u0937x\u200cy", + "valid": false + }, + { + "description": "Bidi domain name with a digit-first label is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 a label in a Bidi domain name must start with an L, R or AL character", + "data": "0a.\u05d0", + "valid": false + }, + { + "description": "label starting with a digit before a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "0\u0627", + "valid": false + }, + { + "description": "left-to-right label containing a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "a\u05d0", + "valid": false + }, + { + "description": "right-to-left label mixing both digit types is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "\u05d00\u0660", + "valid": false + }, + { + "description": "A-label that decodes to a disallowed code point is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5892#section-2.6", + "data": "xn--7a", + "valid": false + }, + { + "description": "A-label that decodes to a Bidi rule violation is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5893", + "data": "xn--0ca24w", + "valid": false + }, + { + "description": "a U-label whose A-label form is longer than 63 octets is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-4.2.4 the 63-octet limit is on the A-label form, not the code point count", + "data": "\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc", + "valid": false + }, + { + "description": "empty label between two dots is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc1034#section-3.1", + "data": "a..b", + "valid": false + }, + { + "description": "a name longer than 253 characters is invalid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": false + }, + { + "description": "A-label that decodes to only ASCII is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 a U-label must have at least one non-ASCII character", + "data": "xn--example-", + "valid": false + }, + { + "description": "non-canonical Punycode that does not re-encode to itself is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.4 a label decoded from Punycode must be identical to the original A-label", + "data": "xn---9uc", + "valid": false } ] }, @@ -383,6 +532,74 @@ "description": "halfwidth ideographic full stop as label separator", "data": "a\uff61b", "valid": true + }, + { + "description": "leading dot", + "data": ".example", + "valid": false + }, + { + "description": "leading ideographic full stop", + "data": "\u3002example", + "valid": false + }, + { + "description": "leading fullwidth full stop", + "data": "\uff0eexample", + "valid": false + }, + { + "description": "leading halfwidth ideographic full stop", + "data": "\uff61example", + "valid": false + }, + { + "description": "trailing dot", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example.", + "valid": false + }, + { + "description": "trailing ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\u3002", + "valid": false + }, + { + "description": "trailing fullwidth full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff0e", + "valid": false + }, + { + "description": "trailing halfwidth ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff61", + "valid": false + }, + { + "description": "label too long if separator ignored (full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα.com", + "valid": true + }, + { + "description": "label too long if separator ignored (ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\u3002com", + "valid": true + }, + { + "description": "label too long if separator ignored (fullwidth full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff0ecom", + "valid": true + }, + { + "description": "label too long if separator ignored (halfwidth ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff61com", + "valid": true } ] } diff --git a/src/test/suite/tests/draft2020-12/optional/format/hostname.json b/src/test/suite/tests/draft2020-12/optional/format/hostname.json index 57827c4d4..7cacfcfd2 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/hostname.json +++ b/src/test/suite/tests/draft2020-12/optional/format/hostname.json @@ -42,43 +42,63 @@ "valid": true }, { - "description": "a valid punycoded IDN hostname", - "data": "xn--4gbwdl.xn--wgbh1c", + "description": "single label", + "data": "hostname", "valid": true }, { - "description": "a host name starting with an illegal character", - "data": "-a-host-name-that-starts-with--", + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true + }, + { + "description": "empty string", + "data": "", "valid": false }, { - "description": "a host name containing illegal characters", - "data": "not_a_valid_host_name", + "description": "single dot", + "data": ".", "valid": false }, { - "description": "a host name with a component too long", - "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "description": "leading dot", + "data": ".example", "valid": false }, { - "description": "starts with hyphen", - "data": "-hostname", + "description": "trailing dot", + "data": "example.", "valid": false }, { - "description": "ends with hyphen", - "data": "hostname-", + "description": "IDN label separator", + "data": "example\uff0ecom", "valid": false }, { - "description": "starts with underscore", - "data": "_hostname", + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "starts with hyphen", + "data": "-hostname", "valid": false }, { - "description": "ends with underscore", - "data": "hostname_", + "description": "ends with hyphen", + "data": "hostname-", "valid": false }, { @@ -87,48 +107,278 @@ "valid": false }, { - "description": "maximum label length", + "description": "exceeds maximum overall length (256)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": false + }, + { + "description": "maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", "valid": true }, { - "description": "exceeds maximum label length", + "description": "exceeds maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false }, { - "description": "single label", - "data": "hostname", + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "invalid non-ASCII KELVIN SIGN (U+212A)", + "data": "\u212Aelvin.example.com", + "valid": false + }, + { + "description": "consecutive hyphens inside a label", + "data": "a--b.com", "valid": true + } + ] + }, + { + "description": "validation of A-label (punycode) host names", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "hostname" + }, + "tests": [ + { + "description": "invalid Punycode", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.4 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "xn--X", + "valid": false }, { - "description": "single label with hyphen", - "data": "host-name", + "description": "a valid host name (example.test in Hangul)", + "data": "xn--9n2bp8q.xn--9t4b11yi5a", "valid": true }, { - "description": "single label with digits", - "data": "h0stn4me", + "description": "contains illegal char U+302E Hangul single dot tone mark", + "data": "xn--07jt112bpxg.xn--9t4b11yi5a", + "valid": false + }, + { + "description": "Begins with a Spacing Combining Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-txk", + "valid": false + }, + { + "description": "Begins with a Nonspacing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-zed", + "valid": false + }, + { + "description": "Begins with an Enclosing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-6bf", + "valid": false + }, + { + "description": "Exceptions that are PVALID, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--zca29lwxobi7a", "valid": true }, { - "description": "single label starting with digit", - "data": "1host", + "description": "Exceptions that are PVALID, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--qmbc", "valid": true }, { - "description": "single label ending with digit", - "data": "hostnam3", + "description": "Exceptions that are DISALLOWED, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--chb89f", + "valid": false + }, + { + "description": "Exceptions that are DISALLOWED, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6 Note: The two combining marks (U+302E and U+302F) are in the middle and not at the start", + "data": "xn--07jceefgh4c", + "valid": false + }, + { + "description": "MIDDLE DOT with no preceding 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--al-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing preceding", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-fda", + "valid": false + }, + { + "description": "MIDDLE DOT with no following 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--la-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing following", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-gda", + "valid": false + }, + { + "description": "MIDDLE DOT with surrounding 'l's", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--ll-0ea", "valid": true }, { - "description": "empty string", - "data": "", + "description": "Greek KERAIA not followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--S-jib3p", "valid": false }, { - "description": "single dot", - "data": ".", + "description": "Greek KERAIA not followed by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3j", + "valid": false + }, + { + "description": "Greek KERAIA followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3je", + "valid": true + }, + { + "description": "Hebrew GERESH not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--A-2hc5h", + "valid": false + }, + { + "description": "Hebrew GERESH not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--5db1e", + "valid": false + }, + { + "description": "Hebrew GERESH preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--4dbc5h", + "valid": true + }, + { + "description": "Hebrew GERSHAYIM not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--A-2hc8h", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--5db3e", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--4dbc8h", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--defabc-k64e", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with no other characters", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with Hiragana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--k8j5u", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Katakana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--bck0j", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek778f", + "valid": true + }, + { + "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngb6iyr", + "valid": false + }, + { + "description": "Arabic-Indic digits not mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngba1o", + "valid": true + }, + { + "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", + "data": "xn--0-gyc", + "valid": true + }, + { + "description": "ZERO WIDTH JOINER not preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2er09f", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--02b508i", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2ezcw70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1", + "data": "xn--11b2ezcs70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", + "data": "xn--ngba5hb2804a", + "valid": true + }, + { + "description": "contains \"--\" in the 3rd and 4th position", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "XN--aa---o47jg78q", "valid": false } ] diff --git a/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json b/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json index f42ae969b..feaf55ab2 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft2020-12/optional/format/idn-hostname.json @@ -52,8 +52,13 @@ "valid": false }, { - "description": "a host name with a component too long", - "data": "실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실례례테스트례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례테스트례례실례.테스트", + "description": "a single label of 63 characters is valid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": true + }, + { + "description": "a single label of 64 characters is too long", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "valid": false }, { @@ -331,6 +336,150 @@ "description": "empty string", "data": "", "valid": false + }, + { + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "fullwidth digits are mapped to ASCII digits", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt maps U+FF11 to 0031, so this label maps to 123", + "data": "\uff11\uff12\uff13", + "valid": true + }, + { + "description": "a zero width space is ignored by the mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt lists U+200B as ignored, so this label maps to ab", + "data": "a\u200bb", + "valid": true + }, + { + "description": "an A-label with an uppercase Punycode body is valid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.3 lowercases the A-label before the round-trip test, and RFC 3492 section 5 gives A-Z and a-z the same digit-values", + "data": "xn--NXASMQ6B", + "valid": true + }, + { + "description": "the two Arabic-Indic digit blocks may appear in different labels", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.8 scopes the rule to \"this label\", so one block per label is valid", + "data": "\u0628\u0660.\u0628\u06f0", + "valid": true + }, + { + "description": "a label of only Arabic-Indic digits is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 condition 1 allows only L, R or AL as the first character, and an Arabic-Indic digit is AN", + "data": "\u0660\u0661", + "valid": false + }, + { + "description": "a non-NFC U-label is valid after UTS 46 normalisation", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 section 4 normalises to NFC before validating, so the decomposed form is accepted", + "data": "cafe\u0301.com", + "valid": true + }, + { + "description": "a label of 63 fullwidth characters is valid after mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps each U+FF41 to an ASCII letter, so this label is 63 octets in its mapped form", + "data": "\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41", + "valid": true + }, + { + "description": "an ACE prefix produced by the mapping step is decoded as an A-label", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps U+FF58 U+FF4E to \"xn\", so the mapped label is the A-label xn--nxasmq6b", + "data": "\uff58\uff4e--nxasmq6b", + "valid": true + }, + { + "description": "zero width non-joiner must pass at every occurrence", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.1", + "data": "\u0915\u094d\u200c\u0937x\u200cy", + "valid": false + }, + { + "description": "Bidi domain name with a digit-first label is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 a label in a Bidi domain name must start with an L, R or AL character", + "data": "0a.\u05d0", + "valid": false + }, + { + "description": "label starting with a digit before a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "0\u0627", + "valid": false + }, + { + "description": "left-to-right label containing a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "a\u05d0", + "valid": false + }, + { + "description": "right-to-left label mixing both digit types is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "\u05d00\u0660", + "valid": false + }, + { + "description": "A-label that decodes to a disallowed code point is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5892#section-2.6", + "data": "xn--7a", + "valid": false + }, + { + "description": "A-label that decodes to a Bidi rule violation is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5893", + "data": "xn--0ca24w", + "valid": false + }, + { + "description": "a U-label whose A-label form is longer than 63 octets is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-4.2.4 the 63-octet limit is on the A-label form, not the code point count", + "data": "\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc", + "valid": false + }, + { + "description": "empty label between two dots is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc1034#section-3.1", + "data": "a..b", + "valid": false + }, + { + "description": "a name longer than 253 characters is invalid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": false + }, + { + "description": "A-label that decodes to only ASCII is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 a U-label must have at least one non-ASCII character", + "data": "xn--example-", + "valid": false + }, + { + "description": "non-canonical Punycode that does not re-encode to itself is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.4 a label decoded from Punycode must be identical to the original A-label", + "data": "xn---9uc", + "valid": false } ] }, @@ -383,6 +532,74 @@ "description": "halfwidth ideographic full stop as label separator", "data": "a\uff61b", "valid": true + }, + { + "description": "leading dot", + "data": ".example", + "valid": false + }, + { + "description": "leading ideographic full stop", + "data": "\u3002example", + "valid": false + }, + { + "description": "leading fullwidth full stop", + "data": "\uff0eexample", + "valid": false + }, + { + "description": "leading halfwidth ideographic full stop", + "data": "\uff61example", + "valid": false + }, + { + "description": "trailing dot", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example.", + "valid": false + }, + { + "description": "trailing ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\u3002", + "valid": false + }, + { + "description": "trailing fullwidth full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff0e", + "valid": false + }, + { + "description": "trailing halfwidth ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff61", + "valid": false + }, + { + "description": "label too long if separator ignored (full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα.com", + "valid": true + }, + { + "description": "label too long if separator ignored (ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\u3002com", + "valid": true + }, + { + "description": "label too long if separator ignored (fullwidth full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff0ecom", + "valid": true + }, + { + "description": "label too long if separator ignored (halfwidth ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff61com", + "valid": true } ] } diff --git a/src/test/suite/tests/draft4/optional/format/hostname.json b/src/test/suite/tests/draft4/optional/format/hostname.json index 866a61788..6d937e865 100644 --- a/src/test/suite/tests/draft4/optional/format/hostname.json +++ b/src/test/suite/tests/draft4/optional/format/hostname.json @@ -43,6 +43,11 @@ "data": "xn--4gbwdl.xn--wgbh1c", "valid": true }, + { + "description": "hostname with consecutive hyphens (RFC1123)", + "data": "ab--cd.example", + "valid": true + }, { "description": "a host name starting with an illegal character", "data": "-a-host-name-that-starts-with--", @@ -83,6 +88,11 @@ "data": "host_name", "valid": false }, + { + "description": "exceeds maximum overall length (256)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": false + }, { "description": "maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", @@ -122,6 +132,41 @@ "description": "single dot", "data": ".", "valid": false + }, + { + "description": "leading dot", + "data": ".example", + "valid": false + }, + { + "description": "trailing dot", + "data": "example.", + "valid": false + }, + { + "description": "IDN label separator", + "data": "example\uff0ecom", + "valid": false + }, + { + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "invalid non-ASCII KELVIN SIGN (U+212A)", + "data": "\u212Aelvin.example.com", + "valid": false } ] } diff --git a/src/test/suite/tests/draft6/optional/format/hostname.json b/src/test/suite/tests/draft6/optional/format/hostname.json index 866a61788..6d937e865 100644 --- a/src/test/suite/tests/draft6/optional/format/hostname.json +++ b/src/test/suite/tests/draft6/optional/format/hostname.json @@ -43,6 +43,11 @@ "data": "xn--4gbwdl.xn--wgbh1c", "valid": true }, + { + "description": "hostname with consecutive hyphens (RFC1123)", + "data": "ab--cd.example", + "valid": true + }, { "description": "a host name starting with an illegal character", "data": "-a-host-name-that-starts-with--", @@ -83,6 +88,11 @@ "data": "host_name", "valid": false }, + { + "description": "exceeds maximum overall length (256)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": false + }, { "description": "maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", @@ -122,6 +132,41 @@ "description": "single dot", "data": ".", "valid": false + }, + { + "description": "leading dot", + "data": ".example", + "valid": false + }, + { + "description": "trailing dot", + "data": "example.", + "valid": false + }, + { + "description": "IDN label separator", + "data": "example\uff0ecom", + "valid": false + }, + { + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "invalid non-ASCII KELVIN SIGN (U+212A)", + "data": "\u212Aelvin.example.com", + "valid": false } ] } diff --git a/src/test/suite/tests/draft7/optional/format/hostname.json b/src/test/suite/tests/draft7/optional/format/hostname.json index 866a61788..bd5dcdea2 100644 --- a/src/test/suite/tests/draft7/optional/format/hostname.json +++ b/src/test/suite/tests/draft7/optional/format/hostname.json @@ -39,43 +39,63 @@ "valid": true }, { - "description": "a valid punycoded IDN hostname", - "data": "xn--4gbwdl.xn--wgbh1c", + "description": "single label", + "data": "hostname", "valid": true }, { - "description": "a host name starting with an illegal character", - "data": "-a-host-name-that-starts-with--", + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true + }, + { + "description": "empty string", + "data": "", "valid": false }, { - "description": "a host name containing illegal characters", - "data": "not_a_valid_host_name", + "description": "single dot", + "data": ".", "valid": false }, { - "description": "a host name with a component too long", - "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "description": "leading dot", + "data": ".example", "valid": false }, { - "description": "starts with hyphen", - "data": "-hostname", + "description": "trailing dot", + "data": "example.", "valid": false }, { - "description": "ends with hyphen", - "data": "hostname-", + "description": "IDN label separator", + "data": "example\uff0ecom", "valid": false }, { - "description": "starts with underscore", - "data": "_hostname", + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "starts with hyphen", + "data": "-hostname", "valid": false }, { - "description": "ends with underscore", - "data": "hostname_", + "description": "ends with hyphen", + "data": "hostname-", "valid": false }, { @@ -84,43 +104,275 @@ "valid": false }, { - "description": "maximum label length", + "description": "exceeds maximum overall length (256)", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": false + }, + { + "description": "maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", "valid": true }, { - "description": "exceeds maximum label length", + "description": "exceeds maximum label length (63)", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false }, { - "description": "single label", - "data": "hostname", + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "invalid non-ASCII KELVIN SIGN (U+212A)", + "data": "\u212Aelvin.example.com", + "valid": false + }, + { + "description": "consecutive hyphens inside a label", + "data": "a--b.com", "valid": true + } + ] + }, + { + "description": "validation of A-label (punycode) host names", + "schema": { "format": "hostname" }, + "tests": [ + { + "description": "invalid Punycode", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.4 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "xn--X", + "valid": false }, { - "description": "single label with hyphen", - "data": "host-name", + "description": "a valid host name (example.test in Hangul)", + "data": "xn--9n2bp8q.xn--9t4b11yi5a", "valid": true }, { - "description": "single label with digits", - "data": "h0stn4me", + "description": "contains illegal char U+302E Hangul single dot tone mark", + "data": "xn--07jt112bpxg.xn--9t4b11yi5a", + "valid": false + }, + { + "description": "Begins with a Spacing Combining Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-txk", + "valid": false + }, + { + "description": "Begins with a Nonspacing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-zed", + "valid": false + }, + { + "description": "Begins with an Enclosing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "xn--hello-6bf", + "valid": false + }, + { + "description": "Exceptions that are PVALID, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--zca29lwxobi7a", "valid": true }, { - "description": "single label ending with digit", - "data": "hostnam3", + "description": "Exceptions that are PVALID, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--qmbc", "valid": true }, { - "description": "empty string", - "data": "", + "description": "Exceptions that are DISALLOWED, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "xn--chb89f", "valid": false }, { - "description": "single dot", - "data": ".", + "description": "Exceptions that are DISALLOWED, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6 Note: The two combining marks (U+302E and U+302F) are in the middle and not at the start", + "data": "xn--07jceefgh4c", + "valid": false + }, + { + "description": "MIDDLE DOT with no preceding 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--al-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing preceding", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-fda", + "valid": false + }, + { + "description": "MIDDLE DOT with no following 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--la-0ea", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing following", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--l-gda", + "valid": false + }, + { + "description": "MIDDLE DOT with surrounding 'l's", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "xn--ll-0ea", + "valid": true + }, + { + "description": "Greek KERAIA not followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--S-jib3p", + "valid": false + }, + { + "description": "Greek KERAIA not followed by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3j", + "valid": false + }, + { + "description": "Greek KERAIA followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "xn--wva3je", + "valid": true + }, + { + "description": "Hebrew GERESH not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--A-2hc5h", + "valid": false + }, + { + "description": "Hebrew GERESH not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--5db1e", + "valid": false + }, + { + "description": "Hebrew GERESH preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "xn--4dbc5h", + "valid": true + }, + { + "description": "Hebrew GERSHAYIM not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--A-2hc8h", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--5db3e", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "xn--4dbc8h", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--defabc-k64e", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with no other characters", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with Hiragana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--k8j5u", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Katakana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--bck0j", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "xn--vek778f", + "valid": true + }, + { + "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngb6iyr", + "valid": false + }, + { + "description": "Arabic-Indic digits not mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "xn--ngba1o", + "valid": true + }, + { + "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", + "data": "xn--0-gyc", + "valid": true + }, + { + "description": "ZERO WIDTH JOINER not preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2er09f", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--02b508i", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "xn--11b2ezcw70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1", + "data": "xn--11b2ezcs70k", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", + "data": "xn--ngba5hb2804a", + "valid": true + }, + { + "description": "contains \"--\" in the 3rd and 4th position", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "XN--aa---o47jg78q", "valid": false } ] diff --git a/src/test/suite/tests/draft7/optional/format/idn-hostname.json b/src/test/suite/tests/draft7/optional/format/idn-hostname.json index 5c8cdc77b..dadc5c539 100644 --- a/src/test/suite/tests/draft7/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft7/optional/format/idn-hostname.json @@ -49,8 +49,13 @@ "valid": false }, { - "description": "a host name with a component too long", - "data": "실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실례례테스트례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례테스트례례실례.테스트", + "description": "a single label of 63 characters is valid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": true + }, + { + "description": "a single label of 64 characters is too long", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "valid": false }, { @@ -314,6 +319,11 @@ "data": "h0stn4me", "valid": true }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, { "description": "single label ending with digit", "data": "hostnam3", @@ -323,6 +333,150 @@ "description": "empty string", "data": "", "valid": false + }, + { + "description": "trailing newline is invalid", + "data": "example.com\n", + "valid": false + }, + { + "description": "leading space is invalid", + "data": " example.com", + "valid": false + }, + { + "description": "space inside a label is invalid", + "data": "ex ample.com", + "valid": false + }, + { + "description": "fullwidth digits are mapped to ASCII digits", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt maps U+FF11 to 0031, so this label maps to 123", + "data": "\uff11\uff12\uff13", + "valid": true + }, + { + "description": "a zero width space is ignored by the mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "IdnaMappingTable.txt lists U+200B as ignored, so this label maps to ab", + "data": "a\u200bb", + "valid": true + }, + { + "description": "an A-label with an uppercase Punycode body is valid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.3 lowercases the A-label before the round-trip test, and RFC 3492 section 5 gives A-Z and a-z the same digit-values", + "data": "xn--NXASMQ6B", + "valid": true + }, + { + "description": "the two Arabic-Indic digit blocks may appear in different labels", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.8 scopes the rule to \"this label\", so one block per label is valid", + "data": "\u0628\u0660.\u0628\u06f0", + "valid": true + }, + { + "description": "a label of only Arabic-Indic digits is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 condition 1 allows only L, R or AL as the first character, and an Arabic-Indic digit is AN", + "data": "\u0660\u0661", + "valid": false + }, + { + "description": "a non-NFC U-label is valid after UTS 46 normalisation", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 section 4 normalises to NFC before validating, so the decomposed form is accepted", + "data": "cafe\u0301.com", + "valid": true + }, + { + "description": "a label of 63 fullwidth characters is valid after mapping", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps each U+FF41 to an ASCII letter, so this label is 63 octets in its mapped form", + "data": "\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41\uff41", + "valid": true + }, + { + "description": "an ACE prefix produced by the mapping step is decoded as an A-label", + "disabled": true, + "reason": "UTS 46 mapping is not implemented", + "comment": "UTS 46 maps U+FF58 U+FF4E to \"xn\", so the mapped label is the A-label xn--nxasmq6b", + "data": "\uff58\uff4e--nxasmq6b", + "valid": true + }, + { + "description": "zero width non-joiner must pass at every occurrence", + "comment": "https://www.rfc-editor.org/rfc/rfc5892#appendix-A.1", + "data": "\u0915\u094d\u200c\u0937x\u200cy", + "valid": false + }, + { + "description": "Bidi domain name with a digit-first label is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2 a label in a Bidi domain name must start with an L, R or AL character", + "data": "0a.\u05d0", + "valid": false + }, + { + "description": "label starting with a digit before a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "0\u0627", + "valid": false + }, + { + "description": "left-to-right label containing a right-to-left letter is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "a\u05d0", + "valid": false + }, + { + "description": "right-to-left label mixing both digit types is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5893#section-2", + "data": "\u05d00\u0660", + "valid": false + }, + { + "description": "A-label that decodes to a disallowed code point is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5892#section-2.6", + "data": "xn--7a", + "valid": false + }, + { + "description": "A-label that decodes to a Bidi rule violation is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 https://www.rfc-editor.org/rfc/rfc5893", + "data": "xn--0ca24w", + "valid": false + }, + { + "description": "a U-label whose A-label form is longer than 63 octets is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-4.2.4 the 63-octet limit is on the A-label form, not the code point count", + "data": "\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc\u00fc", + "valid": false + }, + { + "description": "empty label between two dots is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc1034#section-3.1", + "data": "a..b", + "valid": false + }, + { + "description": "a name longer than 253 characters is invalid", + "data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "valid": false + }, + { + "description": "A-label that decodes to only ASCII is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5890#section-2.3.2.1 a U-label must have at least one non-ASCII character", + "data": "xn--example-", + "valid": false + }, + { + "description": "non-canonical Punycode that does not re-encode to itself is invalid", + "comment": "https://www.rfc-editor.org/rfc/rfc5891#section-5.4 a label decoded from Punycode must be identical to the original A-label", + "data": "xn---9uc", + "valid": false } ] }, @@ -372,6 +526,74 @@ "description": "halfwidth ideographic full stop as label separator", "data": "a\uff61b", "valid": true + }, + { + "description": "leading dot", + "data": ".example", + "valid": false + }, + { + "description": "leading ideographic full stop", + "data": "\u3002example", + "valid": false + }, + { + "description": "leading fullwidth full stop", + "data": "\uff0eexample", + "valid": false + }, + { + "description": "leading halfwidth ideographic full stop", + "data": "\uff61example", + "valid": false + }, + { + "description": "trailing dot", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example.", + "valid": false + }, + { + "description": "trailing ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\u3002", + "valid": false + }, + { + "description": "trailing fullwidth full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff0e", + "valid": false + }, + { + "description": "trailing halfwidth ideographic full stop", + "disabled": true, + "reason": "A trailing label separator is still accepted", + "data": "example\uff61", + "valid": false + }, + { + "description": "label too long if separator ignored (full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα.com", + "valid": true + }, + { + "description": "label too long if separator ignored (ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\u3002com", + "valid": true + }, + { + "description": "label too long if separator ignored (fullwidth full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff0ecom", + "valid": true + }, + { + "description": "label too long if separator ignored (halfwidth ideographic full stop)", + "data": "παράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπαράδειγμαπα\uff61com", + "valid": true } ] }