1package eu.siacs.conversations.xmpp.jid;
2
3import android.util.LruCache;
4
5import net.java.otr4j.session.SessionID;
6
7import java.net.IDN;
8
9import eu.siacs.conversations.Config;
10import gnu.inet.encoding.Stringprep;
11import gnu.inet.encoding.StringprepException;
12
13/**
14 * The `Jid' class provides an immutable representation of a JID.
15 */
16public final class Jid {
17
18 private static LruCache<String,Jid> cache = new LruCache<>(1024);
19
20 private final String localpart;
21 private final String domainpart;
22 private final String resourcepart;
23
24 private static final char[] JID_ESCAPING_CHARS = {' ','"','&','\'','/',':','<','>','@','\\'};
25
26 // It's much more efficient to store the full JID as well as the parts instead of figuring them
27 // all out every time (since some characters are displayed but aren't used for comparisons).
28 private final String displayjid;
29
30 public String getLocalpart() {
31 return localpart;
32 }
33
34 public String getUnescapedLocalpart() {
35 if (localpart == null || !localpart.contains("\\")) {
36 return localpart;
37 } else {
38 String localpart = this.localpart;
39 for(char c : JID_ESCAPING_CHARS) {
40 localpart = localpart.replace(String.format ("\\%02x", (int)c),String.valueOf(c));
41 }
42 return localpart;
43 }
44 }
45
46 public String getDomainpart() {
47 return IDN.toUnicode(domainpart);
48 }
49
50 public String getResourcepart() {
51 return resourcepart;
52 }
53
54 public static Jid fromSessionID(final SessionID id) throws InvalidJidException{
55 if (id.getUserID().isEmpty()) {
56 return Jid.fromString(id.getAccountID());
57 } else {
58 return Jid.fromString(id.getAccountID()+"/"+id.getUserID());
59 }
60 }
61
62 public static Jid fromString(final String jid) throws InvalidJidException {
63 return Jid.fromString(jid, false);
64 }
65
66 public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException {
67 return new Jid(jid, safe);
68 }
69
70 public static Jid fromParts(final String localpart,
71 final String domainpart,
72 final String resourcepart) throws InvalidJidException {
73 String out;
74 if (localpart == null || localpart.isEmpty()) {
75 out = domainpart;
76 } else {
77 out = localpart + "@" + domainpart;
78 }
79 if (resourcepart != null && !resourcepart.isEmpty()) {
80 out = out + "/" + resourcepart;
81 }
82 return new Jid(out, false);
83 }
84
85 private Jid(final String jid, final boolean safe) throws InvalidJidException {
86 if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL);
87
88 Jid fromCache = Jid.cache.get(jid);
89 if (fromCache != null) {
90 displayjid = fromCache.displayjid;
91 localpart = fromCache.localpart;
92 domainpart = fromCache.domainpart;
93 resourcepart = fromCache.resourcepart;
94 return;
95 }
96
97 // Hackish Android way to count the number of chars in a string... should work everywhere.
98 final int atCount = jid.length() - jid.replace("@", "").length();
99 final int slashCount = jid.length() - jid.replace("/", "").length();
100
101 // Throw an error if there's anything obvious wrong with the JID...
102 if (jid.isEmpty() || jid.length() > 3071) {
103 throw new InvalidJidException(InvalidJidException.INVALID_LENGTH);
104 }
105
106 // Go ahead and check if the localpart or resourcepart is empty.
107 if (jid.startsWith("@") || (jid.endsWith("@") && slashCount == 0) || jid.startsWith("/") || (jid.endsWith("/") && slashCount < 2)) {
108 throw new InvalidJidException(InvalidJidException.INVALID_CHARACTER);
109 }
110
111 String finaljid;
112
113 final int domainpartStart;
114 final int atLoc = jid.indexOf("@");
115 final int slashLoc = jid.indexOf("/");
116 // If there is no "@" in the JID (eg. "example.net" or "example.net/resource")
117 // or there are one or more "@" signs but they're all in the resourcepart (eg. "example.net/@/rp@"):
118 if (atCount == 0 || (atCount > 0 && slashLoc != -1 && atLoc > slashLoc)) {
119 localpart = "";
120 finaljid = "";
121 domainpartStart = 0;
122 } else {
123 final String lp = jid.substring(0, atLoc);
124 try {
125 localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp);
126 } catch (final StringprepException e) {
127 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
128 }
129 if (localpart.isEmpty() || localpart.length() > 1023) {
130 throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
131 }
132 domainpartStart = atLoc + 1;
133 finaljid = lp + "@";
134 }
135
136 final String dp;
137 if (slashCount > 0) {
138 final String rp = jid.substring(slashLoc + 1, jid.length());
139 try {
140 resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp);
141 } catch (final StringprepException e) {
142 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
143 }
144 if (resourcepart.isEmpty() || resourcepart.length() > 1023) {
145 throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
146 }
147 try {
148 dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES);
149 } catch (final StringprepException e) {
150 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
151 }
152 finaljid = finaljid + dp + "/" + rp;
153 } else {
154 resourcepart = "";
155 try{
156 dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES);
157 } catch (final StringprepException e) {
158 throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e);
159 }
160 finaljid = finaljid + dp;
161 }
162
163 // Remove trailing "." before storing the domain part.
164 if (dp.endsWith(".")) {
165 try {
166 domainpart = IDN.toASCII(dp.substring(0, dp.length() - 1), IDN.USE_STD3_ASCII_RULES);
167 } catch (final IllegalArgumentException e) {
168 throw new InvalidJidException(e);
169 }
170 } else {
171 try {
172 domainpart = IDN.toASCII(dp, IDN.USE_STD3_ASCII_RULES);
173 } catch (final IllegalArgumentException e) {
174 throw new InvalidJidException(e);
175 }
176 }
177
178 // TODO: Find a proper domain validation library; validate individual parts, separators, etc.
179 if (domainpart.isEmpty() || domainpart.length() > 1023) {
180 throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH);
181 }
182
183 Jid.cache.put(jid, this);
184
185 this.displayjid = finaljid;
186 }
187
188 public Jid toBareJid() {
189 try {
190 return resourcepart.isEmpty() ? this : fromParts(localpart, domainpart, "");
191 } catch (final InvalidJidException e) {
192 // This should never happen.
193 throw new AssertionError("Jid " + this.toString() + " invalid");
194 }
195 }
196
197 public Jid toDomainJid() {
198 try {
199 return resourcepart.isEmpty() && localpart.isEmpty() ? this : fromString(getDomainpart());
200 } catch (final InvalidJidException e) {
201 // This should never happen.
202 throw new AssertionError("Jid " + this.toString() + " invalid");
203 }
204 }
205
206 @Override
207 public String toString() {
208 return displayjid;
209 }
210
211 public String toPreppedString() {
212 String out;
213 if (hasLocalpart()) {
214 out = localpart + '@' + domainpart;
215 } else {
216 out = domainpart;
217 }
218 if (!resourcepart.isEmpty()) {
219 out += '/'+resourcepart;
220 }
221 return out;
222 }
223
224 @Override
225 public boolean equals(final Object o) {
226 if (this == o) return true;
227 if (o == null || getClass() != o.getClass()) return false;
228
229 final Jid jid = (Jid) o;
230
231 return jid.hashCode() == this.hashCode();
232 }
233
234 @Override
235 public int hashCode() {
236 int result = localpart.hashCode();
237 result = 31 * result + domainpart.hashCode();
238 result = 31 * result + resourcepart.hashCode();
239 return result;
240 }
241
242 public boolean hasLocalpart() {
243 return !localpart.isEmpty();
244 }
245
246 public boolean isBareJid() {
247 return this.resourcepart.isEmpty();
248 }
249
250 public boolean isDomainJid() {
251 return !this.hasLocalpart();
252 }
253}