001/*
002 * Copyright 2015-2020 the original author or authors
003 *
004 * This software is licensed under the Apache License, Version 2.0,
005 * the GNU Lesser General Public License version 2 or later ("LGPL")
006 * and the WTFPL.
007 * You may choose either license to govern your use of this software only
008 * upon the condition that you accept all of the terms of either
009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
010 */
011package org.minidns.dnslabel;
012
013import java.io.ByteArrayOutputStream;
014import java.nio.charset.StandardCharsets;
015import java.util.Locale;
016
017/**
018 * A DNS label is an individual component of a DNS name. Labels are usually shown separated by dots.
019 * <p>
020 * This class implements {@link Comparable} which compares DNS labels according to the Canonical DNS Name Order as
021 * specified in <a href="https://tools.ietf.org/html/rfc4034#section-6.1">RFC 4034 § 6.1</a>.
022 * </p>
023 * <p>
024 * Note that as per <a href="https://tools.ietf.org/html/rfc2181#section-11">RFC 2181 § 11</a> DNS labels may contain
025 * any byte.
026 * </p>
027 * 
028 * @see <a href="https://tools.ietf.org/html/rfc5890#section-2.2">RFC 5890 § 2.2. DNS-Related Terminology</a>
029 * @author Florian Schmaus
030 *
031 */
032public abstract class DnsLabel implements CharSequence, Comparable<DnsLabel> {
033
034    /**
035     * The maximum length of a DNS label in octets.
036     *
037     * @see <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a>
038     */
039    public static final int MAX_LABEL_LENGTH_IN_OCTETS = 63;
040
041    public static final DnsLabel WILDCARD_LABEL = DnsLabel.from("*");
042
043    /**
044     * Whether or not the DNS label is validated on construction.
045     */
046    public static boolean VALIDATE = true;
047
048    public final String label;
049
050    protected DnsLabel(String label) {
051        this.label = label;
052
053        if (!VALIDATE) {
054            return;
055        }
056
057        setBytesIfRequired();
058        if (byteCache.length > MAX_LABEL_LENGTH_IN_OCTETS) {
059            throw new LabelToLongException(label);
060        }
061    }
062
063    private transient String internationalizedRepresentation;
064
065    public final String getInternationalizedRepresentation() {
066        if (internationalizedRepresentation == null) {
067            internationalizedRepresentation = getInternationalizedRepresentationInternal();
068        }
069        return internationalizedRepresentation;
070    }
071
072    protected String getInternationalizedRepresentationInternal() {
073        return label;
074    }
075
076    public final String getLabelType() {
077        return getClass().getSimpleName();
078    }
079
080    @Override
081    public final int length() {
082        return label.length();
083    }
084
085    @Override
086    public final char charAt(int index) {
087        return label.charAt(index);
088    }
089
090    @Override
091    public final CharSequence subSequence(int start, int end) {
092        return label.subSequence(start, end);
093    }
094
095    @Override
096    public final String toString() {
097        return label;
098    }
099
100    @Override
101    public final boolean equals(Object other) {
102        if (!(other instanceof DnsLabel)) {
103            return false;
104        }
105        DnsLabel otherDnsLabel = (DnsLabel) other;
106        return label.equals(otherDnsLabel.label);
107    }
108
109    @Override
110    public final int hashCode() {
111        return label.hashCode();
112    }
113
114    private transient DnsLabel lowercasedVariant;
115
116    public final DnsLabel asLowercaseVariant() {
117        if (lowercasedVariant == null) {
118            String lowercaseLabel = label.toLowerCase(Locale.US);
119            lowercasedVariant = DnsLabel.from(lowercaseLabel);
120        }
121        return lowercasedVariant;
122    }
123
124    private transient byte[] byteCache;
125
126    private void setBytesIfRequired() {
127        if (byteCache == null) {
128            byteCache = label.getBytes(StandardCharsets.US_ASCII);
129        }
130    }
131
132    public final void writeToBoas(ByteArrayOutputStream byteArrayOutputStream) {
133        setBytesIfRequired();
134
135        byteArrayOutputStream.write(byteCache.length);
136        byteArrayOutputStream.write(byteCache, 0, byteCache.length);
137    }
138
139    @Override
140    public final int compareTo(DnsLabel other) {
141        String myCanonical = asLowercaseVariant().label;
142        String otherCanonical = other.asLowercaseVariant().label;
143
144        return myCanonical.compareTo(otherCanonical);
145    }
146
147    public static DnsLabel from(String label) {
148        if (label == null || label.isEmpty()) {
149            throw new IllegalArgumentException("Label is null or empty");
150        }
151
152        if (LdhLabel.isLdhLabel(label)) {
153            return LdhLabel.fromInternal(label);
154        }
155
156        return NonLdhLabel.fromInternal(label);
157    }
158
159    public static DnsLabel[] from(String[] labels) {
160        DnsLabel[] res = new DnsLabel[labels.length];
161
162        for (int i = 0; i < labels.length; i++) {
163            res[i] = DnsLabel.from(labels[i]);
164        }
165
166        return res;
167    }
168
169    public static boolean isIdnAcePrefixed(String string) {
170        return string.toLowerCase(Locale.US).startsWith("xn--");
171    }
172
173    public static class LabelToLongException extends IllegalArgumentException {
174
175        /**
176         * 
177         */
178        private static final long serialVersionUID = 1L;
179
180        public final String label;
181
182        LabelToLongException(String label) {
183            this.label = label;
184        }
185    }
186}