IoUtils.java revision 433e3fac172d0c4449051b0c61c0c63b298a0903
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package libcore.io; 18 19import java.io.Closeable; 20import java.io.EOFException; 21import java.io.File; 22import java.io.FileDescriptor; 23import java.io.FileNotFoundException; 24import java.io.IOException; 25import java.io.InputStream; 26import java.io.RandomAccessFile; 27import java.net.BindException; 28import java.net.ConnectException; 29import java.net.InetAddress; 30import java.net.Inet4Address; 31import java.net.Inet6Address; 32import java.io.Reader; 33import java.io.StringWriter; 34import java.net.BindException; 35import java.net.Inet4Address; 36import java.net.Inet6Address; 37import java.net.InetAddress; 38import java.net.InetSocketAddress; 39import java.net.NetworkInterface; 40import java.net.Socket; 41import java.net.SocketAddress; 42import java.net.SocketException; 43import java.net.SocketOptions; 44import java.net.SocketTimeoutException; 45import java.net.UnknownHostException; 46import java.nio.charset.Charsets; 47import java.util.Arrays; 48import libcore.util.MutableInt; 49import static libcore.io.OsConstants.*; 50 51// TODO: kill this! 52import org.apache.harmony.luni.platform.Platform; 53 54public final class IoUtils { 55 private IoUtils() { 56 } 57 58 /** 59 * Implements java.io/java.net "available" semantics. 60 */ 61 public static int available(FileDescriptor fd) throws IOException { 62 try { 63 MutableInt available = new MutableInt(0); 64 int rc = Libcore.os.ioctlInt(fd, FIONREAD, available); 65 if (available.value < 0) { 66 // If the fd refers to a regular file, the result is the difference between 67 // the file size and the file position. This may be negative if the position 68 // is past the end of the file. If the fd refers to a special file masquerading 69 // as a regular file, the result may be negative because the special file 70 // may appear to have zero size and yet a previous read call may have 71 // read some amount of data and caused the file position to be advanced. 72 available.value = 0; 73 } 74 return available.value; 75 } catch (ErrnoException errnoException) { 76 if (errnoException.errno == ENOTTY) { 77 // The fd is unwilling to opine about its read buffer. 78 return 0; 79 } 80 throw errnoException.rethrowAsIOException(); 81 } 82 } 83 84 /** 85 * java.io only throws FileNotFoundException when opening files, regardless of what actually 86 * went wrong. Additionally, java.io is more restrictive than POSIX when it comes to opening 87 * directories: POSIX says read-only is okay, but java.io doesn't even allow that. We also 88 * have an Android-specific hack to alter the default permissions. 89 */ 90 public static FileDescriptor open(String path, int flags) throws FileNotFoundException { 91 FileDescriptor fd = null; 92 try { 93 // On Android, we don't want default permissions to allow global access. 94 int mode = ((flags & O_ACCMODE) == O_RDONLY) ? 0 : 0600; 95 fd = Libcore.os.open(path, flags, mode); 96 if (fd.valid()) { 97 // Posix open(2) fails with EISDIR only if you ask for write permission. 98 // Java disallows reading directories too. 99 boolean isDirectory = false; 100 if (S_ISDIR(Libcore.os.fstat(fd).st_mode)) { 101 throw new ErrnoException("open", EISDIR); 102 } 103 } 104 return fd; 105 } catch (ErrnoException errnoException) { 106 try { 107 if (fd != null) { 108 close(fd); 109 } 110 } catch (IOException ignored) { 111 } 112 FileNotFoundException ex = new FileNotFoundException(path + ": " + errnoException.getMessage()); 113 ex.initCause(errnoException); 114 throw ex; 115 } 116 } 117 118 /** 119 * java.io thinks that a read at EOF is an error and should return -1, contrary to traditional 120 * Unix practice where you'd read until you got 0 bytes (and any future read would return -1). 121 */ 122 public static int read(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws IOException { 123 Arrays.checkOffsetAndCount(bytes.length, byteOffset, byteCount); 124 if (byteCount == 0) { 125 return 0; 126 } 127 try { 128 int readCount = Libcore.os.read(fd, bytes, byteOffset, byteCount); 129 if (readCount == 0) { 130 return -1; 131 } 132 return readCount; 133 } catch (ErrnoException errnoException) { 134 if (errnoException.errno == EAGAIN) { 135 // We return 0 rather than throw if we try to read from an empty non-blocking pipe. 136 return 0; 137 } 138 throw errnoException.rethrowAsIOException(); 139 } 140 } 141 142 /** 143 * java.io always writes every byte it's asked to, or fails with an error. (That is, unlike 144 * Unix it never just writes as many bytes as happens to be convenient.) 145 */ 146 public static void write(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount) throws IOException { 147 Arrays.checkOffsetAndCount(bytes.length, byteOffset, byteCount); 148 if (byteCount == 0) { 149 return; 150 } 151 try { 152 while (byteCount > 0) { 153 int bytesWritten = Libcore.os.write(fd, bytes, byteOffset, byteCount); 154 byteCount -= bytesWritten; 155 byteOffset += bytesWritten; 156 } 157 } catch (ErrnoException errnoException) { 158 throw errnoException.rethrowAsIOException(); 159 } 160 } 161 162 public static void bind(FileDescriptor fd, InetAddress address, int port) throws SocketException { 163 if (address instanceof Inet6Address && ((Inet6Address) address).getScopeId() == 0) { 164 // Linux won't let you bind a link-local address without a scope id. Find one. 165 NetworkInterface nif = NetworkInterface.getByInetAddress(address); 166 if (nif == null) { 167 throw new SocketException("Can't bind to a link-local address without a scope id: " + address); 168 } 169 try { 170 address = Inet6Address.getByAddress(address.getHostName(), address.getAddress(), nif.getIndex()); 171 } catch (UnknownHostException ex) { 172 throw new AssertionError(ex); // Can't happen. 173 } 174 } 175 try { 176 Libcore.os.bind(fd, address, port); 177 } catch (ErrnoException errnoException) { 178 throw new BindException(errnoException.getMessage(), errnoException); 179 } 180 } 181 182 /** 183 * Calls close(2) on 'fd'. Also resets the internal int to -1. Does nothing if 'fd' is null 184 * or invalid. 185 */ 186 public static void close(FileDescriptor fd) throws IOException { 187 try { 188 if (fd != null && fd.valid()) { 189 Libcore.os.close(fd); 190 } 191 } catch (ErrnoException errnoException) { 192 throw errnoException.rethrowAsIOException(); 193 } 194 } 195 196 /** 197 * Closes 'closeable', ignoring any exceptions. Does nothing if 'closeable' is null. 198 */ 199 public static void closeQuietly(Closeable closeable) { 200 if (closeable != null) { 201 try { 202 closeable.close(); 203 } catch (IOException ignored) { 204 } 205 } 206 } 207 208 /** 209 * Closes 'fd', ignoring any exceptions. Does nothing if 'fd' is null or invalid. 210 */ 211 public static void closeQuietly(FileDescriptor fd) { 212 try { 213 IoUtils.close(fd); 214 } catch (IOException ignored) { 215 } 216 } 217 218 /** 219 * Closes 'socket', ignoring any exceptions. Does nothing if 'socket' is null. 220 */ 221 public static void closeQuietly(Socket socket) { 222 if (socket != null) { 223 try { 224 socket.close(); 225 } catch (Exception ignored) { 226 } 227 } 228 } 229 230 /** 231 * Connects socket 'fd' to 'inetAddress' on 'port', with no timeout. The lack of a timeout 232 * means this method won't throw SocketTimeoutException. 233 */ 234 public static boolean connect(FileDescriptor fd, InetAddress inetAddress, int port) throws SocketException { 235 try { 236 return IoUtils.connect(fd, inetAddress, port, 0); 237 } catch (SocketTimeoutException ex) { 238 throw new AssertionError(ex); // Can't happen for a connect without a timeout. 239 } 240 } 241 242 /** 243 * Connects socket 'fd' to 'inetAddress' on 'port', with a the given 'timeoutMs'. 244 * Use timeoutMs == 0 for a blocking connect with no timeout. 245 */ 246 public static boolean connect(FileDescriptor fd, InetAddress inetAddress, int port, int timeoutMs) throws SocketException, SocketTimeoutException { 247 try { 248 return connectErrno(fd, inetAddress, port, timeoutMs); 249 } catch (ErrnoException errnoException) { 250 throw new ConnectException(connectDetail(inetAddress, port, timeoutMs) + ": " + errnoException.getMessage(), errnoException); 251 } catch (SocketException ex) { 252 throw ex; // We don't want to doubly wrap these. 253 } catch (SocketTimeoutException ex) { 254 throw ex; // We don't want to doubly wrap these. 255 } catch (IOException ex) { 256 throw new SocketException(ex); 257 } 258 } 259 260 private static boolean connectErrno(FileDescriptor fd, InetAddress inetAddress, int port, int timeoutMs) throws IOException { 261 // With no timeout, just call connect(2) directly. 262 if (timeoutMs == 0) { 263 Libcore.os.connect(fd, inetAddress, port); 264 return true; 265 } 266 267 // With a timeout, we set the socket to non-blocking, connect(2), and then loop 268 // using select(2) to decide whether we're connected, whether we should keep waiting, 269 // or whether we've seen a permanent failure and should give up. 270 long finishTimeMs = System.currentTimeMillis() + timeoutMs; 271 IoUtils.setBlocking(fd, false); 272 try { 273 try { 274 Libcore.os.connect(fd, inetAddress, port); 275 return true; // We connected immediately. 276 } catch (ErrnoException errnoException) { 277 if (errnoException.errno != EINPROGRESS) { 278 throw errnoException; 279 } 280 // EINPROGRESS means we should keep trying... 281 } 282 int remainingTimeoutMs; 283 do { 284 remainingTimeoutMs = (int) (finishTimeMs - System.currentTimeMillis()); 285 if (remainingTimeoutMs <= 0) { 286 throw new SocketTimeoutException(connectDetail(inetAddress, port, timeoutMs)); 287 } 288 } while (!Platform.NETWORK.isConnected(fd, remainingTimeoutMs)); 289 return true; // Or we'd have thrown. 290 } finally { 291 IoUtils.setBlocking(fd, true); 292 } 293 } 294 295 private static String connectDetail(InetAddress inetAddress, int port, int timeoutMs) { 296 String detail = "failed to connect to " + inetAddress + " (port " + port + ")"; 297 if (timeoutMs > 0) { 298 detail += " after " + timeoutMs + "ms"; 299 } 300 return detail; 301 } 302 303 /** 304 * Sets 'fd' to be blocking or non-blocking, according to the state of 'blocking'. 305 */ 306 public static void setBlocking(FileDescriptor fd, boolean blocking) throws IOException { 307 try { 308 int flags = Libcore.os.fcntlVoid(fd, F_GETFL); 309 if (!blocking) { 310 flags |= O_NONBLOCK; 311 } else { 312 flags &= ~O_NONBLOCK; 313 } 314 Libcore.os.fcntlLong(fd, F_SETFL, flags); 315 } catch (ErrnoException errnoException) { 316 throw errnoException.rethrowAsIOException(); 317 } 318 } 319 320 public static FileDescriptor socket(boolean stream) throws SocketException { 321 FileDescriptor fd; 322 try { 323 fd = Libcore.os.socket(AF_INET6, stream ? SOCK_STREAM : SOCK_DGRAM, 0); 324 325 // The RFC (http://www.ietf.org/rfc/rfc3493.txt) says that IPV6_MULTICAST_HOPS defaults 326 // to 1. The Linux kernel (at least up to 2.6.38) accidentally defaults to 64 (which 327 // would be correct for the *unicast* hop limit). 328 // See http://www.spinics.net/lists/netdev/msg129022.html, though no patch appears to 329 // have been applied as a result of that discussion. If that bug is ever fixed, we can 330 // remove this code. Until then, we manually set the hop limit on IPv6 datagram sockets. 331 // (IPv4 is already correct.) 332 if (!stream) { 333 Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 1); 334 } 335 336 return fd; 337 } catch (ErrnoException errnoException) { 338 throw errnoException.rethrowAsSocketException(); 339 } 340 } 341 342 // Socket options used by java.net but not exposed in SocketOptions. 343 public static final int JAVA_MCAST_JOIN_GROUP = 19; 344 public static final int JAVA_MCAST_LEAVE_GROUP = 20; 345 public static final int JAVA_IP_MULTICAST_TTL = 17; 346 347 /** 348 * java.net has its own socket options similar to the underlying Unix ones. We paper over the 349 * differences here. 350 */ 351 public static Object getSocketOption(FileDescriptor fd, int option) throws SocketException { 352 try { 353 return getSocketOptionErrno(fd, option); 354 } catch (ErrnoException errnoException) { 355 throw errnoException.rethrowAsSocketException(); 356 } 357 } 358 359 private static Object getSocketOptionErrno(FileDescriptor fd, int option) throws SocketException { 360 switch (option) { 361 case SocketOptions.IP_MULTICAST_IF: 362 // This is IPv4-only. 363 return Libcore.os.getsockoptInAddr(fd, IPPROTO_IP, IP_MULTICAST_IF); 364 case SocketOptions.IP_MULTICAST_IF2: 365 // This is IPv6-only. 366 return Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF); 367 case SocketOptions.IP_MULTICAST_LOOP: 368 // Since setting this from java.net always sets IPv4 and IPv6 to the same value, 369 // it doesn't matter which we return. 370 return booleanFromInt(Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP)); 371 case IoUtils.JAVA_IP_MULTICAST_TTL: 372 // Since setting this from java.net always sets IPv4 and IPv6 to the same value, 373 // it doesn't matter which we return. 374 return Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS); 375 case SocketOptions.IP_TOS: 376 // Since setting this from java.net always sets IPv4 and IPv6 to the same value, 377 // it doesn't matter which we return. 378 return Libcore.os.getsockoptInt(fd, IPPROTO_IPV6, IPV6_TCLASS); 379 case SocketOptions.SO_BROADCAST: 380 return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_BROADCAST)); 381 case SocketOptions.SO_KEEPALIVE: 382 return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_KEEPALIVE)); 383 case SocketOptions.SO_LINGER: 384 StructLinger linger = Libcore.os.getsockoptLinger(fd, SOL_SOCKET, SO_LINGER); 385 if (!linger.isOn()) { 386 return false; 387 } 388 return linger.l_linger; 389 case SocketOptions.SO_OOBINLINE: 390 return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_OOBINLINE)); 391 case SocketOptions.SO_RCVBUF: 392 return Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_SNDBUF); 393 case SocketOptions.SO_REUSEADDR: 394 return booleanFromInt(Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_REUSEADDR)); 395 case SocketOptions.SO_SNDBUF: 396 return Libcore.os.getsockoptInt(fd, SOL_SOCKET, SO_SNDBUF); 397 case SocketOptions.SO_TIMEOUT: 398 return (int) Libcore.os.getsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO).toMillis(); 399 case SocketOptions.TCP_NODELAY: 400 return booleanFromInt(Libcore.os.getsockoptInt(fd, IPPROTO_TCP, TCP_NODELAY)); 401 default: 402 throw new SocketException("Unknown socket option: " + option); 403 } 404 } 405 406 private static boolean booleanFromInt(int i) { 407 return (i != 0); 408 } 409 410 private static int booleanToInt(boolean b) { 411 return b ? 1 : 0; 412 } 413 414 /** 415 * java.net has its own socket options similar to the underlying Unix ones. We paper over the 416 * differences here. 417 */ 418 public static void setSocketOption(FileDescriptor fd, int option, Object value) throws SocketException { 419 try { 420 setSocketOptionErrno(fd, option, value); 421 } catch (ErrnoException errnoException) { 422 throw errnoException.rethrowAsSocketException(); 423 } 424 } 425 426 private static void setSocketOptionErrno(FileDescriptor fd, int option, Object value) throws SocketException { 427 switch (option) { 428 case SocketOptions.IP_MULTICAST_IF: 429 throw new UnsupportedOperationException("Use IP_MULTICAST_IF2 on Android"); 430 case SocketOptions.IP_MULTICAST_IF2: 431 // Although IPv6 was cleaned up to use int, IPv4 uses an ip_mreqn containing an int. 432 Libcore.os.setsockoptIpMreqn(fd, IPPROTO_IP, IP_MULTICAST_IF, (Integer) value); 433 Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, (Integer) value); 434 return; 435 case SocketOptions.IP_MULTICAST_LOOP: 436 // Although IPv6 was cleaned up to use int, IPv4 multicast loopback uses a byte. 437 Libcore.os.setsockoptByte(fd, IPPROTO_IP, IP_MULTICAST_LOOP, booleanToInt((Boolean) value)); 438 Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, booleanToInt((Boolean) value)); 439 return; 440 case IoUtils.JAVA_IP_MULTICAST_TTL: 441 // Although IPv6 was cleaned up to use int, and IPv4 non-multicast TTL uses int, 442 // IPv4 multicast TTL uses a byte. 443 Libcore.os.setsockoptByte(fd, IPPROTO_IP, IP_MULTICAST_TTL, (Integer) value); 444 Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (Integer) value); 445 return; 446 case SocketOptions.IP_TOS: 447 Libcore.os.setsockoptInt(fd, IPPROTO_IP, IP_TOS, (Integer) value); 448 Libcore.os.setsockoptInt(fd, IPPROTO_IPV6, IPV6_TCLASS, (Integer) value); 449 return; 450 case SocketOptions.SO_BROADCAST: 451 Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_BROADCAST, booleanToInt((Boolean) value)); 452 return; 453 case SocketOptions.SO_KEEPALIVE: 454 Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_KEEPALIVE, booleanToInt((Boolean) value)); 455 return; 456 case SocketOptions.SO_LINGER: 457 boolean on = false; 458 int seconds = 0; 459 if (value instanceof Integer) { 460 on = true; 461 seconds = Math.min((Integer) value, 65535); 462 } 463 StructLinger linger = new StructLinger(booleanToInt(on), seconds); 464 Libcore.os.setsockoptLinger(fd, SOL_SOCKET, SO_LINGER, linger); 465 return; 466 case SocketOptions.SO_OOBINLINE: 467 Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_OOBINLINE, booleanToInt((Boolean) value)); 468 return; 469 case SocketOptions.SO_RCVBUF: 470 Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, (Integer) value); 471 return; 472 case SocketOptions.SO_REUSEADDR: 473 Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_REUSEADDR, booleanToInt((Boolean) value)); 474 return; 475 case SocketOptions.SO_SNDBUF: 476 Libcore.os.setsockoptInt(fd, SOL_SOCKET, SO_SNDBUF, (Integer) value); 477 return; 478 case SocketOptions.SO_TIMEOUT: 479 int millis = (Integer) value; 480 StructTimeval tv = StructTimeval.fromMillis(millis); 481 Libcore.os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, tv); 482 return; 483 case SocketOptions.TCP_NODELAY: 484 Libcore.os.setsockoptInt(fd, IPPROTO_TCP, TCP_NODELAY, booleanToInt((Boolean) value)); 485 return; 486 case IoUtils.JAVA_MCAST_JOIN_GROUP: 487 case IoUtils.JAVA_MCAST_LEAVE_GROUP: 488 StructGroupReq groupReq = (StructGroupReq) value; 489 int level = (groupReq.gr_group instanceof Inet4Address) ? IPPROTO_IP : IPPROTO_IPV6; 490 int op = (option == JAVA_MCAST_JOIN_GROUP) ? MCAST_JOIN_GROUP : MCAST_LEAVE_GROUP; 491 Libcore.os.setsockoptGroupReq(fd, level, op, groupReq); 492 return; 493 default: 494 throw new SocketException("Unknown socket option: " + option); 495 } 496 } 497 498 public static InetAddress getSocketLocalAddress(FileDescriptor fd) { 499 SocketAddress sa = Libcore.os.getsockname(fd); 500 InetSocketAddress isa = (InetSocketAddress) sa; 501 return isa.getAddress(); 502 } 503 504 public static int getSocketLocalPort(FileDescriptor fd) { 505 SocketAddress sa = Libcore.os.getsockname(fd); 506 InetSocketAddress isa = (InetSocketAddress) sa; 507 return isa.getPort(); 508 } 509 510 /** 511 * Returns the contents of 'path' as a byte array. 512 */ 513 public static byte[] readFileAsByteArray(String path) throws IOException { 514 return readFileAsBytes(path).toByteArray(); 515 } 516 517 /** 518 * Returns the contents of 'path' as a string. The contents are assumed to be UTF-8. 519 */ 520 public static String readFileAsString(String path) throws IOException { 521 return readFileAsBytes(path).toString(Charsets.UTF_8); 522 } 523 524 /** 525 * Returns the remainder of 'reader' as a string, closing it when done. 526 */ 527 public static String readReaderAsString(Reader reader) throws IOException { 528 StringWriter writer = new StringWriter(); 529 char[] buffer = new char[8192]; 530 int count; 531 while ((count = reader.read(buffer)) != -1) { 532 writer.write(buffer, 0, count); 533 } 534 reader.close(); 535 return writer.toString(); 536 } 537 538 private static UnsafeByteSequence readFileAsBytes(String path) throws IOException { 539 RandomAccessFile f = null; 540 try { 541 f = new RandomAccessFile(path, "r"); 542 UnsafeByteSequence bytes = new UnsafeByteSequence((int) f.length()); 543 byte[] buffer = new byte[8192]; 544 while (true) { 545 int byteCount = f.read(buffer); 546 if (byteCount == -1) { 547 return bytes; 548 } 549 bytes.write(buffer, 0, byteCount); 550 } 551 } finally { 552 IoUtils.closeQuietly(f); 553 } 554 } 555 556 /** 557 * Recursively delete everything in {@code dir}. 558 */ 559 public static void deleteContents(File dir) throws IOException { 560 for (File file : dir.listFiles()) { 561 if (file.isDirectory()) { 562 deleteContents(file); 563 } 564 if (!file.delete()) { 565 throw new IOException("failed to delete file: " + file); 566 } 567 } 568 } 569 570 /** 571 * Returns the ASCII characters up to but not including the next "\r\n", or 572 * "\n". 573 * 574 * @throws EOFException if the stream is exhausted before the next newline 575 * character. 576 */ 577 public static String readLine(InputStream in) throws IOException { 578 // TODO: support UTF-8 here instead 579 580 StringBuilder result = new StringBuilder(80); 581 while (true) { 582 int c = in.read(); 583 if (c == -1) { 584 throw new EOFException(); 585 } else if (c == '\n') { 586 break; 587 } 588 589 result.append((char) c); 590 } 591 int length = result.length(); 592 if (length > 0 && result.charAt(length - 1) == '\r') { 593 result.setLength(length - 1); 594 } 595 return result.toString(); 596 } 597} 598