58870
|
1 |
/*
|
|
2 |
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
|
|
3 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
4 |
*
|
|
5 |
* This code is free software; you can redistribute it and/or modify it
|
|
6 |
* under the terms of the GNU General Public License version 2 only, as
|
|
7 |
* published by the Free Software Foundation. Oracle designates this
|
|
8 |
* particular file as subject to the "Classpath" exception as provided
|
|
9 |
* by Oracle in the LICENSE file that accompanied this code.
|
|
10 |
*
|
|
11 |
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
12 |
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
13 |
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
14 |
* version 2 for more details (a copy is included in the LICENSE file that
|
|
15 |
* accompanied this code).
|
|
16 |
*
|
|
17 |
* You should have received a copy of the GNU General Public License version
|
|
18 |
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
19 |
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
20 |
*
|
|
21 |
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
22 |
* or visit www.oracle.com if you need additional information or have any
|
|
23 |
* questions.
|
|
24 |
*/
|
|
25 |
|
|
26 |
package jdk.dns.client.internal;
|
|
27 |
|
|
28 |
import jdk.dns.client.ex.DnsInvalidNameException;
|
|
29 |
|
|
30 |
import java.util.ArrayList;
|
|
31 |
|
|
32 |
/**
|
|
33 |
* {@code DnsName} implements compound names for DNS as specified by
|
|
34 |
* RFCs 1034 and 1035, and as updated and clarified by RFCs 1123 and 2181.
|
|
35 |
*
|
|
36 |
* <p> The labels in a domain name correspond to JNDI atomic names.
|
|
37 |
* Each label must be less than 64 octets in length, and only the
|
|
38 |
* optional root label at the end of the name may be 0 octets long.
|
|
39 |
* The sum of the lengths of all labels in a name, plus the number of
|
|
40 |
* non-root labels plus 1, must be less than 256. The textual
|
|
41 |
* representation of a domain name consists of the labels, escaped as
|
|
42 |
* needed, dot-separated, and ordered right-to-left.
|
|
43 |
*
|
|
44 |
* <p> A label consists of a sequence of octets, each of which may
|
|
45 |
* have any value from 0 to 255.
|
|
46 |
*
|
|
47 |
* <p> <em>Host names</em> are a subset of domain names.
|
|
48 |
* Their labels contain only ASCII letters, digits, and hyphens, and
|
|
49 |
* none may begin or end with a hyphen. While names not conforming to
|
|
50 |
* these rules may be valid domain names, they will not be usable by a
|
|
51 |
* number of DNS applications, and should in most cases be avoided.
|
|
52 |
*
|
|
53 |
* <p> DNS does not specify an encoding (such as UTF-8) to use for
|
|
54 |
* octets with non-ASCII values. As of this writing there is some
|
|
55 |
* work going on in this area, but it is not yet finalized.
|
|
56 |
* {@code DnsName} currently converts any non-ASCII octets into
|
|
57 |
* characters using ISO-LATIN-1 encoding, in effect taking the
|
|
58 |
* value of each octet and storing it directly into the low-order byte
|
|
59 |
* of a Java character and <i>vice versa</i>. As a consequence, no
|
|
60 |
* character in a DNS name will ever have a non-zero high-order byte.
|
|
61 |
* When the work on internationalizing domain names has stabilized
|
|
62 |
* (see for example <i>draft-ietf-idn-idna-10.txt</i>), {@code DnsName}
|
|
63 |
* may be updated to conform to that work.
|
|
64 |
*
|
|
65 |
* <p> Backslash ({@code \}) is used as the escape character in the
|
|
66 |
* textual representation of a domain name. The character sequence
|
|
67 |
* `{@code \DDD}', where {@code DDD} is a 3-digit decimal number
|
|
68 |
* (with leading zeros if needed), represents the octet whose value
|
|
69 |
* is {@code DDD}. The character sequence `{@code \C}', where
|
|
70 |
* {@code C} is a character other than {@code '0'} through
|
|
71 |
* {@code '9'}, represents the octet whose value is that of
|
|
72 |
* {@code C} (again using ISO-LATIN-1 encoding); this is particularly
|
|
73 |
* useful for escaping {@code '.'} or backslash itself. Backslash is
|
|
74 |
* otherwise not allowed in a domain name. Note that escape characters
|
|
75 |
* are interpreted when a name is parsed. So, for example, the character
|
|
76 |
* sequences `{@code S}', `{@code \S}', and `{@code \083}' each
|
|
77 |
* represent the same one-octet name. The {@code toString()} method
|
|
78 |
* does not generally insert escape sequences except where necessary.
|
|
79 |
* If, however, the {@code DnsName} was constructed using unneeded
|
|
80 |
* escapes, those escapes may appear in the {@code toString} result.
|
|
81 |
*
|
|
82 |
* <p> Atomic names passed as parameters to methods of
|
|
83 |
* {@code DnsName}, and those returned by them, are unescaped. So,
|
|
84 |
* for example, <code>(new DnsName()).add("a.b")</code> creates an
|
|
85 |
* object representing the one-label domain name {@code a\.b}, and
|
|
86 |
* calling {@code get(0)} on this object returns {@code "a.b"}.
|
|
87 |
*
|
|
88 |
* <p> While DNS names are case-preserving, comparisons between them
|
|
89 |
* are case-insensitive. When comparing names containing non-ASCII
|
|
90 |
* octets, {@code DnsName} uses case-insensitive comparison
|
|
91 |
* between pairs of ASCII values, and exact binary comparison
|
|
92 |
* otherwise.
|
|
93 |
*
|
|
94 |
* <p> A {@code DnsName} instance is not synchronized against
|
|
95 |
* concurrent access by multiple threads.
|
|
96 |
*
|
|
97 |
* @author Scott Seligman
|
|
98 |
*/
|
|
99 |
|
|
100 |
// Stripped copy with removed serialization code and jndi.Name interface
|
|
101 |
|
|
102 |
public final class DnsName {
|
|
103 |
|
|
104 |
// If non-null, the domain name represented by this DnsName.
|
|
105 |
private String domain = "";
|
|
106 |
|
|
107 |
// The labels of this domain name, as a list of strings. Index 0
|
|
108 |
// corresponds to the leftmost (least significant) label: note that
|
|
109 |
// this is the reverse of the ordering used by the Name interface.
|
|
110 |
private ArrayList<String> labels = new ArrayList<>();
|
|
111 |
|
|
112 |
// The number of octets needed to carry this domain name in a DNS
|
|
113 |
// packet. Equal to the sum of the lengths of each label, plus the
|
|
114 |
// number of non-root labels, plus 1. Must remain less than 256.
|
|
115 |
private short octets = 1;
|
|
116 |
|
|
117 |
|
|
118 |
/**
|
|
119 |
* Constructs a {@code DnsName} representing the empty domain name.
|
|
120 |
*/
|
|
121 |
public DnsName() {
|
|
122 |
}
|
|
123 |
|
|
124 |
/**
|
|
125 |
* Constructs a {@code DnsName} representing a given domain name.
|
|
126 |
*
|
|
127 |
* @param name the domain name to parse
|
|
128 |
* @throws DnsInvalidNameException if {@code name} does not conform
|
|
129 |
* to DNS syntax.
|
|
130 |
*/
|
|
131 |
public DnsName(String name) throws DnsInvalidNameException {
|
|
132 |
parse(name);
|
|
133 |
}
|
|
134 |
|
|
135 |
/*
|
|
136 |
* Returns a new DnsName with its name components initialized to
|
|
137 |
* the components of "n" in the range [beg,end). Indexing is as
|
|
138 |
* for the Name interface, with 0 being the most significant.
|
|
139 |
*/
|
|
140 |
private DnsName(DnsName n, int beg, int end) {
|
|
141 |
// Compute indexes into "labels", which has least-significant label
|
|
142 |
// at index 0 (opposite to the convention used for "beg" and "end").
|
|
143 |
int b = n.size() - end;
|
|
144 |
int e = n.size() - beg;
|
|
145 |
labels.addAll(n.labels.subList(b, e));
|
|
146 |
|
|
147 |
if (size() == n.size()) {
|
|
148 |
domain = n.domain;
|
|
149 |
octets = n.octets;
|
|
150 |
} else {
|
|
151 |
for (String label : labels) {
|
|
152 |
if (label.length() > 0) {
|
|
153 |
octets += (short) (label.length() + 1);
|
|
154 |
}
|
|
155 |
}
|
|
156 |
}
|
|
157 |
}
|
|
158 |
|
|
159 |
|
|
160 |
public String toString() {
|
|
161 |
if (domain == null) {
|
|
162 |
StringBuilder buf = new StringBuilder();
|
|
163 |
for (String label : labels) {
|
|
164 |
if (buf.length() > 0 || label.length() == 0) {
|
|
165 |
buf.append('.');
|
|
166 |
}
|
|
167 |
escape(buf, label);
|
|
168 |
}
|
|
169 |
domain = buf.toString();
|
|
170 |
}
|
|
171 |
return domain;
|
|
172 |
}
|
|
173 |
|
|
174 |
/**
|
|
175 |
* Does this domain name follow <em>host name</em> syntax?
|
|
176 |
*/
|
|
177 |
public boolean isHostName() {
|
|
178 |
for (String label : labels) {
|
|
179 |
if (!isHostNameLabel(label)) {
|
|
180 |
return false;
|
|
181 |
}
|
|
182 |
}
|
|
183 |
return true;
|
|
184 |
}
|
|
185 |
|
|
186 |
public short getOctets() {
|
|
187 |
return octets;
|
|
188 |
}
|
|
189 |
|
|
190 |
public int size() {
|
|
191 |
return labels.size();
|
|
192 |
}
|
|
193 |
|
|
194 |
public boolean isEmpty() {
|
|
195 |
return (size() == 0);
|
|
196 |
}
|
|
197 |
|
|
198 |
public int hashCode() {
|
|
199 |
int h = 0;
|
|
200 |
for (int i = 0; i < size(); i++) {
|
|
201 |
h = 31 * h + getKey(i).hashCode();
|
|
202 |
}
|
|
203 |
return h;
|
|
204 |
}
|
|
205 |
|
|
206 |
public boolean equals(Object obj) {
|
|
207 |
if (!(obj instanceof DnsName)) {
|
|
208 |
return false;
|
|
209 |
}
|
|
210 |
DnsName n = (DnsName) obj;
|
|
211 |
return ((size() == n.size()) && // shortcut: do sizes differ?
|
|
212 |
(compareTo(obj) == 0));
|
|
213 |
}
|
|
214 |
|
|
215 |
public int compareTo(Object obj) {
|
|
216 |
DnsName n = (DnsName) obj;
|
|
217 |
return compareRange(0, size(), n); // never 0 if sizes differ
|
|
218 |
}
|
|
219 |
|
|
220 |
public String get(int pos) {
|
|
221 |
if (pos < 0 || pos >= size()) {
|
|
222 |
throw new ArrayIndexOutOfBoundsException();
|
|
223 |
}
|
|
224 |
int i = size() - pos - 1; // index of "pos" component in "labels"
|
|
225 |
return labels.get(i);
|
|
226 |
}
|
|
227 |
|
|
228 |
public DnsName getPrefix(int pos) {
|
|
229 |
return new DnsName(this, 0, pos);
|
|
230 |
}
|
|
231 |
|
|
232 |
public DnsName getSuffix(int pos) {
|
|
233 |
return new DnsName(this, pos, size());
|
|
234 |
}
|
|
235 |
|
|
236 |
public Object clone() {
|
|
237 |
return new DnsName(this, 0, size());
|
|
238 |
}
|
|
239 |
|
|
240 |
public DnsName add(int pos, String comp) throws DnsInvalidNameException {
|
|
241 |
if (pos < 0 || pos > size()) {
|
|
242 |
throw new ArrayIndexOutOfBoundsException();
|
|
243 |
}
|
|
244 |
// Check for empty labels: may have only one, and only at end.
|
|
245 |
int len = comp.length();
|
|
246 |
if ((pos > 0 && len == 0) ||
|
|
247 |
(pos == 0 && hasRootLabel())) {
|
|
248 |
throw new DnsInvalidNameException(
|
|
249 |
"Empty label must be the last label in a domain name");
|
|
250 |
}
|
|
251 |
// Check total name length.
|
|
252 |
if (len > 0) {
|
|
253 |
if (octets + len + 1 >= 256) {
|
|
254 |
throw new DnsInvalidNameException("Name too long");
|
|
255 |
}
|
|
256 |
octets += (short) (len + 1);
|
|
257 |
}
|
|
258 |
|
|
259 |
int i = size() - pos; // index for insertion into "labels"
|
|
260 |
verifyLabel(comp);
|
|
261 |
labels.add(i, comp);
|
|
262 |
|
|
263 |
domain = null; // invalidate "domain"
|
|
264 |
return this;
|
|
265 |
}
|
|
266 |
|
|
267 |
public DnsName addAll(int pos, DnsName n) throws DnsInvalidNameException {
|
|
268 |
// "n" is a DnsName so we can insert it as a whole, rather than
|
|
269 |
// verifying and inserting it component-by-component.
|
|
270 |
// More code, but less work.
|
|
271 |
|
|
272 |
if (n.isEmpty()) {
|
|
273 |
return this;
|
|
274 |
}
|
|
275 |
// Check for empty labels: may have only one, and only at end.
|
|
276 |
if ((pos > 0 && n.hasRootLabel()) ||
|
|
277 |
(pos == 0 && hasRootLabel())) {
|
|
278 |
throw new DnsInvalidNameException(
|
|
279 |
"Empty label must be the last label in a domain name");
|
|
280 |
}
|
|
281 |
|
|
282 |
short newOctets = (short) (octets + n.octets - 1);
|
|
283 |
if (newOctets > 255) {
|
|
284 |
throw new DnsInvalidNameException("Name too long");
|
|
285 |
}
|
|
286 |
octets = newOctets;
|
|
287 |
int i = size() - pos; // index for insertion into "labels"
|
|
288 |
labels.addAll(i, n.labels);
|
|
289 |
|
|
290 |
// Preserve "domain" if we're appending or prepending,
|
|
291 |
// otherwise invalidate it.
|
|
292 |
if (isEmpty()) {
|
|
293 |
domain = n.domain;
|
|
294 |
} else if (domain == null || n.domain == null) {
|
|
295 |
domain = null;
|
|
296 |
} else if (pos == 0) {
|
|
297 |
domain += (n.domain.equals(".") ? "" : ".") + n.domain;
|
|
298 |
} else if (pos == size()) {
|
|
299 |
domain = n.domain + (domain.equals(".") ? "" : ".") + domain;
|
|
300 |
} else {
|
|
301 |
domain = null;
|
|
302 |
}
|
|
303 |
return this;
|
|
304 |
}
|
|
305 |
|
|
306 |
boolean hasRootLabel() {
|
|
307 |
return (!isEmpty() &&
|
|
308 |
get(0).isEmpty());
|
|
309 |
}
|
|
310 |
|
|
311 |
/*
|
|
312 |
* Helper method for public comparison methods. Lexicographically
|
|
313 |
* compares components of this name in the range [beg,end) with
|
|
314 |
* all components of "n". Indexing is as for the Name interface,
|
|
315 |
* with 0 being the most significant. Returns negative, zero, or
|
|
316 |
* positive as these name components are less than, equal to, or
|
|
317 |
* greater than those of "n".
|
|
318 |
*/
|
|
319 |
private int compareRange(int beg, int end, DnsName n) {
|
|
320 |
// aee: Removed CompositeName ClassCastException generation here
|
|
321 |
|
|
322 |
// Loop through labels, starting with most significant.
|
|
323 |
int minSize = Math.min(end - beg, n.size());
|
|
324 |
for (int i = 0; i < minSize; i++) {
|
|
325 |
String label1 = get(i + beg);
|
|
326 |
String label2 = n.get(i);
|
|
327 |
|
|
328 |
// int j = size() - (i + beg) - 1; // index of label1 in "labels"
|
|
329 |
// assert (label1 == labels.get(j));
|
|
330 |
|
|
331 |
int c = compareLabels(label1, label2);
|
|
332 |
if (c != 0) {
|
|
333 |
return c;
|
|
334 |
}
|
|
335 |
}
|
|
336 |
return ((end - beg) - n.size()); // longer range wins
|
|
337 |
}
|
|
338 |
|
|
339 |
/*
|
|
340 |
* Returns a key suitable for hashing the label at index i.
|
|
341 |
* Indexing is as for the Name interface, with 0 being the most
|
|
342 |
* significant.
|
|
343 |
*/
|
|
344 |
String getKey(int i) {
|
|
345 |
return keyForLabel(get(i));
|
|
346 |
}
|
|
347 |
|
|
348 |
|
|
349 |
/*
|
|
350 |
* Parses a domain name, setting the values of instance vars accordingly.
|
|
351 |
*/
|
|
352 |
private void parse(String name) throws DnsInvalidNameException {
|
|
353 |
|
|
354 |
StringBuilder label = new StringBuilder(); // label being parsed
|
|
355 |
|
|
356 |
for (int i = 0; i < name.length(); i++) {
|
|
357 |
char c = name.charAt(i);
|
|
358 |
|
|
359 |
if (c == '\\') { // found an escape sequence
|
|
360 |
c = getEscapedOctet(name, i++);
|
|
361 |
if (isDigit(name.charAt(i))) { // sequence is \DDD
|
|
362 |
i += 2; // consume remaining digits
|
|
363 |
}
|
|
364 |
label.append(c);
|
|
365 |
|
|
366 |
} else if (c != '.') { // an unescaped octet
|
|
367 |
label.append(c);
|
|
368 |
|
|
369 |
} else { // found '.' separator
|
|
370 |
add(0, label.toString()); // check syntax, then add label
|
|
371 |
// to end of name
|
|
372 |
label.delete(0, i); // clear buffer for next label
|
|
373 |
}
|
|
374 |
}
|
|
375 |
|
|
376 |
// If name is neither "." nor "", the octets (zero or more)
|
|
377 |
// from the rightmost dot onward are now added as the final
|
|
378 |
// label of the name. Those two are special cases in that for
|
|
379 |
// all other domain names, the number of labels is one greater
|
|
380 |
// than the number of dot separators.
|
|
381 |
if (!name.isEmpty() && !name.equals(".")) {
|
|
382 |
add(0, label.toString());
|
|
383 |
}
|
|
384 |
|
|
385 |
domain = name; // do this last, since add() sets it to null
|
|
386 |
}
|
|
387 |
|
|
388 |
/*
|
|
389 |
* Returns (as a char) the octet indicated by the escape sequence
|
|
390 |
* at a given position within a domain name.
|
|
391 |
* @throws InvalidNameException if a valid escape sequence is not found.
|
|
392 |
*/
|
|
393 |
private static char getEscapedOctet(String name, int pos)
|
|
394 |
throws DnsInvalidNameException {
|
|
395 |
try {
|
|
396 |
// assert (name.charAt(pos) == '\\');
|
|
397 |
char c1 = name.charAt(++pos);
|
|
398 |
if (isDigit(c1)) { // sequence is `\DDD'
|
|
399 |
char c2 = name.charAt(++pos);
|
|
400 |
char c3 = name.charAt(++pos);
|
|
401 |
if (isDigit(c2) && isDigit(c3)) {
|
|
402 |
return (char)
|
|
403 |
((c1 - '0') * 100 + (c2 - '0') * 10 + (c3 - '0'));
|
|
404 |
} else {
|
|
405 |
throw new DnsInvalidNameException(
|
|
406 |
"Invalid escape sequence in " + name);
|
|
407 |
}
|
|
408 |
} else { // sequence is `\C'
|
|
409 |
return c1;
|
|
410 |
}
|
|
411 |
} catch (IndexOutOfBoundsException e) {
|
|
412 |
throw new DnsInvalidNameException(
|
|
413 |
"Invalid escape sequence in " + name);
|
|
414 |
}
|
|
415 |
}
|
|
416 |
|
|
417 |
/*
|
|
418 |
* Checks that this label is valid.
|
|
419 |
* @throws InvalidNameException if label is not valid.
|
|
420 |
*/
|
|
421 |
private static void verifyLabel(String label) throws DnsInvalidNameException {
|
|
422 |
if (label.length() > 63) {
|
|
423 |
throw new DnsInvalidNameException(
|
|
424 |
"Label exceeds 63 octets: " + label);
|
|
425 |
}
|
|
426 |
// Check for two-byte characters.
|
|
427 |
for (int i = 0; i < label.length(); i++) {
|
|
428 |
char c = label.charAt(i);
|
|
429 |
if ((c & 0xFF00) != 0) {
|
|
430 |
throw new DnsInvalidNameException(
|
|
431 |
"Label has two-byte char: " + label);
|
|
432 |
}
|
|
433 |
}
|
|
434 |
}
|
|
435 |
|
|
436 |
/*
|
|
437 |
* Does this label conform to host name syntax?
|
|
438 |
*/
|
|
439 |
private static boolean isHostNameLabel(String label) {
|
|
440 |
for (int i = 0; i < label.length(); i++) {
|
|
441 |
char c = label.charAt(i);
|
|
442 |
if (!isHostNameChar(c)) {
|
|
443 |
return false;
|
|
444 |
}
|
|
445 |
}
|
|
446 |
return !(label.startsWith("-") || label.endsWith("-"));
|
|
447 |
}
|
|
448 |
|
|
449 |
private static boolean isHostNameChar(char c) {
|
|
450 |
return (c == '-' ||
|
|
451 |
c >= 'a' && c <= 'z' ||
|
|
452 |
c >= 'A' && c <= 'Z' ||
|
|
453 |
c >= '0' && c <= '9');
|
|
454 |
}
|
|
455 |
|
|
456 |
private static boolean isDigit(char c) {
|
|
457 |
return (c >= '0' && c <= '9');
|
|
458 |
}
|
|
459 |
|
|
460 |
/*
|
|
461 |
* Append a label to buf, escaping as needed.
|
|
462 |
*/
|
|
463 |
private static void escape(StringBuilder buf, String label) {
|
|
464 |
for (int i = 0; i < label.length(); i++) {
|
|
465 |
char c = label.charAt(i);
|
|
466 |
if (c == '.' || c == '\\') {
|
|
467 |
buf.append('\\');
|
|
468 |
}
|
|
469 |
buf.append(c);
|
|
470 |
}
|
|
471 |
}
|
|
472 |
|
|
473 |
/*
|
|
474 |
* Compares two labels, ignoring case for ASCII values.
|
|
475 |
* Returns negative, zero, or positive as the first label
|
|
476 |
* is less than, equal to, or greater than the second.
|
|
477 |
* See keyForLabel().
|
|
478 |
*/
|
|
479 |
private static int compareLabels(String label1, String label2) {
|
|
480 |
int min = Math.min(label1.length(), label2.length());
|
|
481 |
for (int i = 0; i < min; i++) {
|
|
482 |
char c1 = label1.charAt(i);
|
|
483 |
char c2 = label2.charAt(i);
|
|
484 |
if (c1 >= 'A' && c1 <= 'Z') {
|
|
485 |
c1 += 'a' - 'A'; // to lower case
|
|
486 |
}
|
|
487 |
if (c2 >= 'A' && c2 <= 'Z') {
|
|
488 |
c2 += 'a' - 'A'; // to lower case
|
|
489 |
}
|
|
490 |
if (c1 != c2) {
|
|
491 |
return (c1 - c2);
|
|
492 |
}
|
|
493 |
}
|
|
494 |
return (label1.length() - label2.length()); // the longer one wins
|
|
495 |
}
|
|
496 |
|
|
497 |
/*
|
|
498 |
* Returns a key suitable for hashing a label. Two labels map to
|
|
499 |
* the same key iff they are equal, taking possible case-folding
|
|
500 |
* into account. See compareLabels().
|
|
501 |
*/
|
|
502 |
private static String keyForLabel(String label) {
|
|
503 |
StringBuilder sb = new StringBuilder(label.length());
|
|
504 |
for (int i = 0; i < label.length(); i++) {
|
|
505 |
char c = label.charAt(i);
|
|
506 |
if (c >= 'A' && c <= 'Z') {
|
|
507 |
c += 'a' - 'A'; // to lower case
|
|
508 |
}
|
|
509 |
sb.append(c);
|
|
510 |
}
|
|
511 |
return sb.toString();
|
|
512 |
}
|
|
513 |
}
|