-
Notifications
You must be signed in to change notification settings - Fork 240
Fix domain/hostname generation to comply with RFC 1123 #1901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
kingthorin
merged 1 commit into
datafaker-net:main
from
kingthorin:company-name-domains
Aug 20, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
105 changes: 91 additions & 14 deletions
105
src/main/java/net/datafaker/internal/helper/FakerIDN.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,109 @@ | ||
| package net.datafaker.internal.helper; | ||
|
|
||
| import java.net.IDN; | ||
| import java.util.Locale; | ||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| /** | ||
| * Created by tshick on 10/30/16. | ||
| * Helper for converting domain names to ASCII using Punycode (IDN). | ||
| * <p> | ||
| * This class wraps {@link IDN#toASCII(String)} with fallback handling for edge cases | ||
| * (e.g., bidirectional text in Farsi and Hebrew). The output is guaranteed to be a | ||
| * valid RFC 1123 hostname (LDH-compliant per RFC 1035 / RFC 1123). | ||
| * <p> | ||
| * Inline hostname sanitization ensures all output conforms to RFC 1123 LDH rules: | ||
| * - Each label 1-63 characters, total ≤253 characters | ||
| * - Labels contain only letters, digits, hyphens | ||
| * - Labels cannot start/end with hyphens | ||
| * - Falls back to "example"/"example.com" (RFC 6761) if unsanitizable | ||
| * <p> | ||
| * See RFC 3490 (IDNA), RFC 3491 (Punycode), RFC 1123, and RFC 6761. | ||
| * | ||
| * @since 1.0.0 | ||
| */ | ||
| public class FakerIDN { | ||
| private static final int MAX_LABEL_LENGTH = 63; | ||
| private static final int MAX_HOSTNAME_LENGTH = 253; | ||
| /** | ||
| * {@link IDN#toASCII(String)} is too picky for our needs. It was throwing exceptions for fa.yml and | ||
| * he.yml as they are Bidi languages and something was causing them to die. This is kind of a brute force | ||
| * fix, but it appears to fix the issue. | ||
| * Converts a string to an ASCII hostname, applying Punycode (IDN) encoding where needed. | ||
| * Never throws; returns valid RFC 1123 hostname or "example" fallback. | ||
| * | ||
| * @param input the domain name to convert | ||
| * @return valid ASCII hostname (LDH-compliant per RFC 1123, never null or empty) | ||
| */ | ||
| public static String toASCII(String in) { | ||
| public static String toASCII(@Nullable String input) { | ||
| if (input == null || input.isEmpty()) { | ||
| return "example"; | ||
| } | ||
|
|
||
| @Nullable String asciiResult = tryFullConversion(input); | ||
| if (asciiResult == null) { | ||
| asciiResult = tryCharacterByCharacter(input); | ||
| } | ||
|
|
||
| if (asciiResult == null || asciiResult.isEmpty()) { | ||
| asciiResult = "example"; | ||
| } | ||
|
|
||
| // Sanitize to RFC 1123: lowercase, [a-z0-9-] only, no leading/trailing hyphens | ||
| return sanitizeHostname(asciiResult); | ||
| } | ||
|
|
||
| private static String sanitizeHostname(String input) { | ||
| String[] labels = input.split("\\.", -1); | ||
| StringBuilder result = new StringBuilder(); | ||
|
|
||
| for (int i = 0; i < labels.length; i++) { | ||
| if (i > 0) result.append("."); | ||
| result.append(sanitizeLabel(labels[i])); | ||
| } | ||
|
|
||
| String hostname = result.toString(); | ||
| if (hostname.length() > MAX_HOSTNAME_LENGTH) { | ||
| hostname = hostname.substring(0, MAX_HOSTNAME_LENGTH).replaceAll("\\.$", ""); | ||
| } | ||
| return hostname.isEmpty() ? "example.com" : hostname; | ||
| } | ||
|
|
||
| private static String sanitizeLabel(String input) { | ||
| String result = input.toLowerCase(Locale.ROOT); | ||
| StringBuilder sanitized = new StringBuilder(); | ||
| for (char c : result.toCharArray()) { | ||
| if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') { | ||
| sanitized.append(c); | ||
| } | ||
| } | ||
| result = sanitized.toString(); | ||
| // Preserve xn-- (Punycode prefix) but collapse other consecutive hyphens | ||
| if (!result.startsWith("xn--")) { | ||
| while (result.contains("--")) result = result.replace("--", "-"); | ||
| } | ||
| result = result.replaceAll("^-+|-+$", ""); | ||
| if (result.length() > MAX_LABEL_LENGTH) { | ||
| result = result.substring(0, MAX_LABEL_LENGTH).replaceAll("-+$", ""); | ||
| } | ||
| return result.isEmpty() ? "example" : result; | ||
| } | ||
|
|
||
| @Nullable | ||
| private static String tryFullConversion(String input) { | ||
| try { | ||
| return IDN.toASCII(in); | ||
| return IDN.toASCII(input); | ||
| } catch (IllegalArgumentException ignore) { | ||
| // let's continue with the character by character encoding hack. | ||
| return null; | ||
| } | ||
| final StringBuilder asciiResult = new StringBuilder(); | ||
| for (int i = 0; i < in.length(); i++) { | ||
| } | ||
|
|
||
| @Nullable | ||
| private static String tryCharacterByCharacter(String input) { | ||
| final StringBuilder result = new StringBuilder(input.length()); | ||
| for (int i = 0; i < input.length(); i++) { | ||
| try { | ||
| asciiResult.append(IDN.toASCII(in.substring(i, i + 1))); | ||
| result.append(IDN.toASCII(input.substring(i, i + 1))); | ||
| } catch (IllegalArgumentException ignored) { | ||
| // Skip characters that cannot be converted | ||
| } | ||
| } | ||
| if (asciiResult.isEmpty()) { | ||
| throw new RuntimeException("Unable to convert \"%s\" to ASCII".formatted(in)); | ||
| } | ||
| return asciiResult.toString(); | ||
| return result.length() > 0 ? result.toString() : null; | ||
| } | ||
| } |
140 changes: 140 additions & 0 deletions
140
src/main/java/net/datafaker/internal/helper/HostnameHelper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| package net.datafaker.internal.helper; | ||
|
|
||
| import java.net.IDN; | ||
| import java.util.Locale; | ||
|
|
||
| /** | ||
| * Helper for creating valid hostnames according to RFC 1123 (Letter-Digit-Hyphen rules). | ||
| * <p> | ||
| * This class provides methods to sanitize strings into valid DNS hostnames that conform to: | ||
| * - RFC 1035: Original DNS specification | ||
| * - RFC 1123: Relaxed hostname rules (allows digits at start of label) | ||
| * <p> | ||
| * Hostname rules per RFC 1123: | ||
| * - Each label (segment between dots) is 1-63 characters | ||
| * - Total hostname including dots must be ≤253 characters | ||
| * - Labels contain only letters, digits, and hyphens (LDH) | ||
| * - Labels cannot start or end with hyphens | ||
| * - Labels cannot be purely numeric | ||
| * | ||
| * @since 3.0.0 | ||
| */ | ||
| public class HostnameHelper { | ||
| private static final int MAX_LABEL_LENGTH = 63; | ||
| private static final int MAX_HOSTNAME_LENGTH = 253; | ||
|
|
||
| /** | ||
| * Converts a string to a valid ASCII hostname label, sanitizing per RFC 1123 LDH rules. | ||
| * <p> | ||
| * Process: | ||
| * 1. Apply IDN.toASCII() for non-ASCII input (converts to Punycode if needed) | ||
| * 2. Convert to lowercase | ||
| * 3. Keep only [a-z0-9-] | ||
| * 4. Collapse consecutive hyphens (except preserve "xn--" Punycode prefix) | ||
| * 5. Strip leading/trailing hyphens | ||
| * 6. Enforce length ≤ 63 characters | ||
| * 7. Fall back to "example" if result is empty | ||
| * <p> | ||
| * The fallback value "example" is an IETF-reserved special-use domain name (RFC 6761) | ||
| * intended for documentation and examples. | ||
| * | ||
| * @param input the string to sanitize | ||
| * @param locale the locale for lowercase conversion (e.g., Locale.ROOT) | ||
| * @return a valid ASCII hostname label (never null or empty) | ||
| */ | ||
| public static String toAsciiHostnameLabel(String input, Locale locale) { | ||
| if (input == null || input.isEmpty()) { | ||
| return "example"; | ||
| } | ||
|
|
||
| // 1. Try IDN conversion for Unicode input (converts to Punycode if needed) | ||
| String result = input; | ||
| try { | ||
| result = IDN.toASCII(input); | ||
| } catch (IllegalArgumentException ignore) { | ||
| // Not a valid IDN label; proceed with raw input | ||
| } | ||
|
|
||
| // 2. Lowercase the input | ||
| result = result.toLowerCase(locale); | ||
|
|
||
| // 3. Keep only [a-z0-9-] | ||
| StringBuilder sanitized = new StringBuilder(); | ||
| for (char c : result.toCharArray()) { | ||
| if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') { | ||
| sanitized.append(c); | ||
| } | ||
| } | ||
|
|
||
| result = sanitized.toString(); | ||
|
|
||
| // 4. Collapse consecutive hyphens (preserve "xn--" Punycode prefix) | ||
| if (!result.startsWith("xn--")) { | ||
| while (result.contains("--")) { | ||
| result = result.replace("--", "-"); | ||
| } | ||
| } | ||
|
|
||
| // 5. Strip leading/trailing hyphens | ||
| result = result.replaceAll("^-+|-+$", ""); | ||
|
|
||
| // 6. Enforce max label length | ||
| if (result.length() > MAX_LABEL_LENGTH) { | ||
| result = result.substring(0, MAX_LABEL_LENGTH); | ||
| result = result.replaceAll("-+$", ""); | ||
| } | ||
|
|
||
| // 7. Return valid result or fallback | ||
| if (result.isEmpty()) { | ||
| return "example"; | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Sanitizes each label of a hostname independently and recombines. | ||
| * <p> | ||
| * If input contains dots, each segment is sanitized separately, then recombined. | ||
| * This is useful for domain names where each label has independent rules. | ||
| * <p> | ||
| * Fallback behavior: | ||
| * - Null or empty input returns "example.com" | ||
| * - Result that becomes empty after sanitization returns "example.com" | ||
| * <p> | ||
| * The fallback values are IETF-reserved special-use domain names (RFC 6761) | ||
| * intended for documentation and examples. | ||
| * | ||
| * @param input the hostname/domain to sanitize (may contain dots) | ||
| * @param locale the locale for lowercase conversion | ||
| * @return a valid ASCII hostname (never null or empty) | ||
| */ | ||
| public static String toAsciiHostname(String input, Locale locale) { | ||
| if (input == null || input.isEmpty()) { | ||
| return "example.com"; | ||
| } | ||
|
|
||
| String[] labels = input.split("\\.", -1); | ||
| StringBuilder result = new StringBuilder(); | ||
|
|
||
| for (int i = 0; i < labels.length; i++) { | ||
| if (i > 0) { | ||
| result.append("."); | ||
| } | ||
| result.append(toAsciiHostnameLabel(labels[i], locale)); | ||
| } | ||
|
|
||
| String hostname = result.toString(); | ||
|
|
||
| // Enforce total length | ||
| if (hostname.length() > MAX_HOSTNAME_LENGTH) { | ||
| // Truncate from the end, preserving the TLD | ||
| hostname = hostname.substring(0, MAX_HOSTNAME_LENGTH); | ||
| hostname = hostname.replaceAll("\\.$", ""); | ||
| } | ||
|
|
||
| if (hostname.isEmpty()) { | ||
| return "example.com"; | ||
| } | ||
| return hostname; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.