1 /* 2 * Copyright (c) 2015, 2017, 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 package jdk.internal.jrtfs; 26 27 import java.io.File; 28 import java.io.IOError; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.net.URI; 33 import java.net.URISyntaxException; 34 import java.nio.channels.FileChannel; 35 import java.nio.channels.SeekableByteChannel; 36 import java.nio.file.*; 37 import java.nio.file.DirectoryStream.Filter; 38 import java.nio.file.attribute.BasicFileAttributes; 39 import java.nio.file.attribute.BasicFileAttributeView; 40 import java.nio.file.attribute.FileAttribute; 41 import java.nio.file.attribute.FileTime; 42 import java.util.Iterator; 43 import java.util.Map; 44 import java.util.NoSuchElementException; 45 import java.util.Objects; 46 import java.util.Set; 47 import static java.nio.file.StandardOpenOption.*; 48 import static java.nio.file.StandardCopyOption.*; 49 50 /** 51 * Base class for Path implementation of jrt file systems. 52 * 53 * @implNote This class needs to maintain JDK 8 source compatibility. 54 * 55 * It is used internally in the JDK to implement jimage/jrtfs access, 56 * but also compiled and delivered as part of the jrtfs.jar to support access 57 * to the jimage file provided by the shipped JDK by tools running on JDK 8. 58 */ 59 final class JrtPath implements Path { 60 61 final JrtFileSystem jrtfs; 62 private final String path; 63 private volatile int[] offsets; 64 65 JrtPath(JrtFileSystem jrtfs, String path) { 66 this.jrtfs = jrtfs; 67 this.path = normalize(path); 68 this.resolved = null; 69 } 70 71 JrtPath(JrtFileSystem jrtfs, String path, boolean normalized) { 72 this.jrtfs = jrtfs; 73 this.path = normalized ? path : normalize(path); 74 this.resolved = null; 75 } 76 77 final String getName() { 78 return path; 79 } 80 81 @Override 82 public final JrtPath getRoot() { 83 if (this.isAbsolute()) { 84 return jrtfs.getRootPath(); 85 } else { 86 return null; 87 } 88 } 89 90 @Override 91 public final JrtPath getFileName() { 92 if (path.isEmpty()) 93 return this; 94 if (path.length() == 1 && path.charAt(0) == '/') 95 return null; 96 int off = path.lastIndexOf('/'); 97 if (off == -1) 98 return this; 99 return new JrtPath(jrtfs, path.substring(off + 1), true); 100 } 101 102 @Override 103 public final JrtPath getParent() { 104 initOffsets(); 105 int count = offsets.length; 106 if (count == 0) { // no elements so no parent 107 return null; 108 } 109 int off = offsets[count - 1] - 1; 110 if (off <= 0) { // parent is root only (may be null) 111 return getRoot(); 112 } 113 return new JrtPath(jrtfs, path.substring(0, off)); 114 } 115 116 @Override 117 public final int getNameCount() { 118 initOffsets(); 119 return offsets.length; 120 } 121 122 @Override 123 public final JrtPath getName(int index) { 124 initOffsets(); 125 if (index < 0 || index >= offsets.length) { 126 throw new IllegalArgumentException("index: " + 127 index + ", offsets length: " + offsets.length); 128 } 129 int begin = offsets[index]; 130 int end; 131 if (index == (offsets.length - 1)) { 132 end = path.length(); 133 } else { 134 end = offsets[index + 1]; 135 } 136 return new JrtPath(jrtfs, path.substring(begin, end)); 137 } 138 139 @Override 140 public final JrtPath subpath(int beginIndex, int endIndex) { 141 initOffsets(); 142 if (beginIndex < 0 || endIndex > offsets.length || 143 beginIndex >= endIndex) { 144 throw new IllegalArgumentException( 145 "beginIndex: " + beginIndex + ", endIndex: " + endIndex + 146 ", offsets length: " + offsets.length); 147 } 148 // starting/ending offsets 149 int begin = offsets[beginIndex]; 150 int end; 151 if (endIndex == offsets.length) { 152 end = path.length(); 153 } else { 154 end = offsets[endIndex]; 155 } 156 return new JrtPath(jrtfs, path.substring(begin, end)); 157 } 158 159 @Override 160 public final JrtPath toRealPath(LinkOption... options) throws IOException { 161 return jrtfs.toRealPath(this, options); 162 } 163 164 @Override 165 public final JrtPath toAbsolutePath() { 166 if (isAbsolute()) 167 return this; 168 return new JrtPath(jrtfs, "/" + path, true); 169 } 170 171 @Override 172 public final URI toUri() { 173 try { 174 String p = toAbsolutePath().path; 175 if (!p.startsWith("/modules") || p.contains("..")) { 176 throw new IOError(new RuntimeException(p + " cannot be represented as URI")); 177 } 178 179 p = p.substring("/modules".length()); 180 if (p.isEmpty()) { 181 p = "/"; 182 } 183 return new URI("jrt", p, null); 184 } catch (URISyntaxException ex) { 185 throw new AssertionError(ex); 186 } 187 } 188 189 private boolean equalsNameAt(JrtPath other, int index) { 190 int mbegin = offsets[index]; 191 int mlen; 192 if (index == (offsets.length - 1)) { 193 mlen = path.length() - mbegin; 194 } else { 195 mlen = offsets[index + 1] - mbegin - 1; 196 } 197 int obegin = other.offsets[index]; 198 int olen; 199 if (index == (other.offsets.length - 1)) { 200 olen = other.path.length() - obegin; 201 } else { 202 olen = other.offsets[index + 1] - obegin - 1; 203 } 204 if (mlen != olen) { 205 return false; 206 } 207 int n = 0; 208 while (n < mlen) { 209 if (path.charAt(mbegin + n) != other.path.charAt(obegin + n)) { 210 return false; 211 } 212 n++; 213 } 214 return true; 215 } 216 217 @Override 218 public final JrtPath relativize(Path other) { 219 final JrtPath o = checkPath(other); 220 if (o.equals(this)) { 221 return new JrtPath(jrtfs, "", true); 222 } 223 if (path.isEmpty()) { 224 return o; 225 } 226 if (jrtfs != o.jrtfs || isAbsolute() != o.isAbsolute()) { 227 throw new IllegalArgumentException( 228 "Incorrect filesystem or path: " + other); 229 } 230 final String tp = this.path; 231 final String op = o.path; 232 if (op.startsWith(tp)) { // fast path 233 int off = tp.length(); 234 if (op.charAt(off - 1) == '/') 235 return new JrtPath(jrtfs, op.substring(off), true); 236 if (op.charAt(off) == '/') 237 return new JrtPath(jrtfs, op.substring(off + 1), true); 238 } 239 int mc = this.getNameCount(); 240 int oc = o.getNameCount(); 241 int n = Math.min(mc, oc); 242 int i = 0; 243 while (i < n) { 244 if (!equalsNameAt(o, i)) { 245 break; 246 } 247 i++; 248 } 249 int dotdots = mc - i; 250 int len = dotdots * 3 - 1; 251 if (i < oc) { 252 len += (o.path.length() - o.offsets[i] + 1); 253 } 254 StringBuilder sb = new StringBuilder(len); 255 while (dotdots > 0) { 256 sb.append(".."); 257 if (sb.length() < len) { // no tailing slash at the end 258 sb.append('/'); 259 } 260 dotdots--; 261 } 262 if (i < oc) { 263 sb.append(o.path, o.offsets[i], o.path.length()); 264 } 265 return new JrtPath(jrtfs, sb.toString(), true); 266 } 267 268 @Override 269 public JrtFileSystem getFileSystem() { 270 return jrtfs; 271 } 272 273 @Override 274 public final boolean isAbsolute() { 275 return !path.isEmpty() && path.charAt(0) == '/'; 276 } 277 278 @Override 279 public final JrtPath resolve(Path other) { 280 final JrtPath o = checkPath(other); 281 if (this.path.isEmpty() || o.isAbsolute()) { 282 return o; 283 } 284 if (o.path.isEmpty()) { 285 return this; 286 } 287 StringBuilder sb = new StringBuilder(path.length() + o.path.length() + 1); 288 sb.append(path); 289 if (path.charAt(path.length() - 1) != '/') 290 sb.append('/'); 291 sb.append(o.path); 292 return new JrtPath(jrtfs, sb.toString(), true); 293 } 294 295 @Override 296 public final Path resolveSibling(Path other) { 297 Objects.requireNonNull(other, "other"); 298 Path parent = getParent(); 299 return (parent == null) ? other : parent.resolve(other); 300 } 301 302 @Override 303 public final boolean startsWith(Path other) { 304 if (!(Objects.requireNonNull(other) instanceof JrtPath)) 305 return false; 306 final JrtPath o = (JrtPath)other; 307 final String tp = this.path; 308 final String op = o.path; 309 if (isAbsolute() != o.isAbsolute() || !tp.startsWith(op)) { 310 return false; 311 } 312 int off = op.length(); 313 if (off == 0) { 314 return tp.isEmpty(); 315 } 316 // check match is on name boundary 317 return tp.length() == off || tp.charAt(off) == '/' || 318 off == 0 || op.charAt(off - 1) == '/'; 319 } 320 321 @Override 322 public final boolean endsWith(Path other) { 323 if (!(Objects.requireNonNull(other) instanceof JrtPath)) 324 return false; 325 final JrtPath o = (JrtPath)other; 326 final JrtPath t = this; 327 int olast = o.path.length() - 1; 328 if (olast > 0 && o.path.charAt(olast) == '/') { 329 olast--; 330 } 331 int last = t.path.length() - 1; 332 if (last > 0 && t.path.charAt(last) == '/') { 333 last--; 334 } 335 if (olast == -1) { // o.path.length == 0 336 return last == -1; 337 } 338 if ((o.isAbsolute() && (!t.isAbsolute() || olast != last)) 339 || last < olast) { 340 return false; 341 } 342 for (; olast >= 0; olast--, last--) { 343 if (o.path.charAt(olast) != t.path.charAt(last)) { 344 return false; 345 } 346 } 347 return o.path.charAt(olast + 1) == '/' || 348 last == -1 || t.path.charAt(last) == '/'; 349 } 350 351 @Override 352 public final JrtPath resolve(String other) { 353 return resolve(getFileSystem().getPath(other)); 354 } 355 356 @Override 357 public final Path resolveSibling(String other) { 358 return resolveSibling(getFileSystem().getPath(other)); 359 } 360 361 @Override 362 public final boolean startsWith(String other) { 363 return startsWith(getFileSystem().getPath(other)); 364 } 365 366 @Override 367 public final boolean endsWith(String other) { 368 return endsWith(getFileSystem().getPath(other)); 369 } 370 371 @Override 372 public final JrtPath normalize() { 373 String res = getResolved(); 374 if (res == path) { // no change 375 return this; 376 } 377 return new JrtPath(jrtfs, res, true); 378 } 379 380 private JrtPath checkPath(Path path) { 381 Objects.requireNonNull(path); 382 if (!(path instanceof JrtPath)) 383 throw new ProviderMismatchException("path class: " + 384 path.getClass()); 385 return (JrtPath) path; 386 } 387 388 // create offset list if not already created 389 private void initOffsets() { 390 if (this.offsets == null) { 391 int len = path.length(); 392 // count names 393 int count = 0; 394 int off = 0; 395 while (off < len) { 396 char c = path.charAt(off++); 397 if (c != '/') { 398 count++; 399 off = path.indexOf('/', off); 400 if (off == -1) 401 break; 402 } 403 } 404 // populate offsets 405 int[] offsets = new int[count]; 406 count = 0; 407 off = 0; 408 while (off < len) { 409 char c = path.charAt(off); 410 if (c == '/') { 411 off++; 412 } else { 413 offsets[count++] = off++; 414 off = path.indexOf('/', off); 415 if (off == -1) 416 break; 417 } 418 } 419 this.offsets = offsets; 420 } 421 } 422 423 private volatile String resolved; 424 425 final String getResolvedPath() { 426 String r = resolved; 427 if (r == null) { 428 if (isAbsolute()) { 429 r = getResolved(); 430 } else { 431 r = toAbsolutePath().getResolvedPath(); 432 } 433 resolved = r; 434 } 435 return r; 436 } 437 438 // removes redundant slashs, replace "\" to separator "/" 439 // and check for invalid characters 440 private static String normalize(String path) { 441 int len = path.length(); 442 if (len == 0) { 443 return path; 444 } 445 char prevC = 0; 446 for (int i = 0; i < len; i++) { 447 char c = path.charAt(i); 448 if (c == '\\' || c == '\u0000') { 449 return normalize(path, i); 450 } 451 if (c == '/' && prevC == '/') { 452 return normalize(path, i - 1); 453 } 454 prevC = c; 455 } 456 if (prevC == '/' && len > 1) { 457 return path.substring(0, len - 1); 458 } 459 return path; 460 } 461 462 private static String normalize(String path, int off) { 463 int len = path.length(); 464 StringBuilder to = new StringBuilder(len); 465 to.append(path, 0, off); 466 char prevC = 0; 467 while (off < len) { 468 char c = path.charAt(off++); 469 if (c == '\\') { 470 c = '/'; 471 } 472 if (c == '/' && prevC == '/') { 473 continue; 474 } 475 if (c == '\u0000') { 476 throw new InvalidPathException(path, 477 "Path: NUL character not allowed"); 478 } 479 to.append(c); 480 prevC = c; 481 } 482 len = to.length(); 483 if (len > 1 && to.charAt(len - 1) == '/') { 484 to.deleteCharAt(len - 1); 485 } 486 return to.toString(); 487 } 488 489 // Remove DotSlash(./) and resolve DotDot (..) components 490 private String getResolved() { 491 int length = path.length(); 492 if (length == 0 || (path.indexOf("./") == -1 && path.charAt(length - 1) != '.')) { 493 return path; 494 } else { 495 return resolvePath(); 496 } 497 } 498 499 private String resolvePath() { 500 int length = path.length(); 501 char[] to = new char[length]; 502 int nc = getNameCount(); 503 int[] lastM = new int[nc]; 504 int lastMOff = -1; 505 int m = 0; 506 for (int i = 0; i < nc; i++) { 507 int n = offsets[i]; 508 int len = (i == offsets.length - 1) ? length - n 509 : offsets[i + 1] - n - 1; 510 if (len == 1 && path.charAt(n) == '.') { 511 if (m == 0 && path.charAt(0) == '/') // absolute path 512 to[m++] = '/'; 513 continue; 514 } 515 if (len == 2 && path.charAt(n) == '.' && path.charAt(n + 1) == '.') { 516 if (lastMOff >= 0) { 517 m = lastM[lastMOff--]; // retreat 518 continue; 519 } 520 if (path.charAt(0) == '/') { // "/../xyz" skip 521 if (m == 0) 522 to[m++] = '/'; 523 } else { // "../xyz" -> "../xyz" 524 if (m != 0 && to[m-1] != '/') 525 to[m++] = '/'; 526 while (len-- > 0) 527 to[m++] = path.charAt(n++); 528 } 529 continue; 530 } 531 if (m == 0 && path.charAt(0) == '/' || // absolute path 532 m != 0 && to[m-1] != '/') { // not the first name 533 to[m++] = '/'; 534 } 535 lastM[++lastMOff] = m; 536 while (len-- > 0) 537 to[m++] = path.charAt(n++); 538 } 539 if (m > 1 && to[m - 1] == '/') 540 m--; 541 return (m == to.length) ? new String(to) : new String(to, 0, m); 542 } 543 544 @Override 545 public final String toString() { 546 return path; 547 } 548 549 @Override 550 public final int hashCode() { 551 return path.hashCode(); 552 } 553 554 @Override 555 public final boolean equals(Object obj) { 556 return obj instanceof JrtPath && 557 this.path.equals(((JrtPath) obj).path); 558 } 559 560 @Override 561 public final int compareTo(Path other) { 562 final JrtPath o = checkPath(other); 563 return path.compareTo(o.path); 564 } 565 566 @Override 567 public final WatchKey register( 568 WatchService watcher, 569 WatchEvent.Kind<?>[] events, 570 WatchEvent.Modifier... modifiers) { 571 Objects.requireNonNull(watcher, "watcher"); 572 Objects.requireNonNull(events, "events"); 573 Objects.requireNonNull(modifiers, "modifiers"); 574 throw new UnsupportedOperationException(); 575 } 576 577 @Override 578 public final WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) { 579 return register(watcher, events, new WatchEvent.Modifier[0]); 580 } 581 582 @Override 583 public final File toFile() { 584 throw new UnsupportedOperationException(); 585 } 586 587 @Override 588 public final Iterator<Path> iterator() { 589 return new Iterator<Path>() { 590 private int i = 0; 591 592 @Override 593 public boolean hasNext() { 594 return (i < getNameCount()); 595 } 596 597 @Override 598 public Path next() { 599 if (i < getNameCount()) { 600 Path result = getName(i); 601 i++; 602 return result; 603 } else { 604 throw new NoSuchElementException(); 605 } 606 } 607 608 @Override 609 public void remove() { 610 throw new ReadOnlyFileSystemException(); 611 } 612 }; 613 } 614 615 // Helpers for JrtFileSystemProvider and JrtFileSystem 616 617 final JrtPath readSymbolicLink() throws IOException { 618 if (!jrtfs.isLink(this)) { 619 throw new IOException("not a symbolic link"); 620 } 621 return jrtfs.resolveLink(this); 622 } 623 624 final boolean isHidden() { 625 return false; 626 } 627 628 final void createDirectory(FileAttribute<?>... attrs) 629 throws IOException { 630 jrtfs.createDirectory(this, attrs); 631 } 632 633 final InputStream newInputStream(OpenOption... options) throws IOException { 634 if (options.length > 0) { 635 for (OpenOption opt : options) { 636 if (opt != READ) { 637 throw new UnsupportedOperationException("'" + opt + "' not allowed"); 638 } 639 } 640 } 641 return jrtfs.newInputStream(this); 642 } 643 644 final DirectoryStream<Path> newDirectoryStream(Filter<? super Path> filter) 645 throws IOException { 646 return new JrtDirectoryStream(this, filter); 647 } 648 649 final void delete() throws IOException { 650 jrtfs.deleteFile(this, true); 651 } 652 653 final void deleteIfExists() throws IOException { 654 jrtfs.deleteFile(this, false); 655 } 656 657 final JrtFileAttributes getAttributes(LinkOption... options) throws IOException { 658 JrtFileAttributes zfas = jrtfs.getFileAttributes(this, options); 659 if (zfas == null) { 660 throw new NoSuchFileException(toString()); 661 } 662 return zfas; 663 } 664 665 final void setAttribute(String attribute, Object value, LinkOption... options) 666 throws IOException { 667 JrtFileAttributeView.setAttribute(this, attribute, value); 668 } 669 670 final Map<String, Object> readAttributes(String attributes, LinkOption... options) 671 throws IOException { 672 return JrtFileAttributeView.readAttributes(this, attributes, options); 673 } 674 675 final void setTimes(FileTime mtime, FileTime atime, FileTime ctime) 676 throws IOException { 677 jrtfs.setTimes(this, mtime, atime, ctime); 678 } 679 680 final FileStore getFileStore() throws IOException { 681 // each JrtFileSystem only has one root (as requested for now) 682 if (exists()) { 683 return jrtfs.getFileStore(this); 684 } 685 throw new NoSuchFileException(path); 686 } 687 688 final boolean isSameFile(Path other) throws IOException { 689 if (this == other || this.equals(other)) { 690 return true; 691 } 692 if (other == null || this.getFileSystem() != other.getFileSystem()) { 693 return false; 694 } 695 this.checkAccess(); 696 JrtPath o = (JrtPath) other; 697 o.checkAccess(); 698 return this.getResolvedPath().equals(o.getResolvedPath()) || 699 jrtfs.isSameFile(this, o); 700 } 701 702 final SeekableByteChannel newByteChannel(Set<? extends OpenOption> options, 703 FileAttribute<?>... attrs) 704 throws IOException 705 { 706 return jrtfs.newByteChannel(this, options, attrs); 707 } 708 709 final FileChannel newFileChannel(Set<? extends OpenOption> options, 710 FileAttribute<?>... attrs) 711 throws IOException { 712 return jrtfs.newFileChannel(this, options, attrs); 713 } 714 715 final void checkAccess(AccessMode... modes) throws IOException { 716 if (modes.length == 0) { // check if the path exists 717 jrtfs.checkNode(this); // no need to follow link. the "link" node 718 // is built from real node under "/module" 719 } else { 720 boolean w = false; 721 for (AccessMode mode : modes) { 722 switch (mode) { 723 case READ: 724 break; 725 case WRITE: 726 w = true; 727 break; 728 case EXECUTE: 729 throw new AccessDeniedException(toString()); 730 default: 731 throw new UnsupportedOperationException(); 732 } 733 } 734 jrtfs.checkNode(this); 735 if (w && jrtfs.isReadOnly()) { 736 throw new AccessDeniedException(toString()); 737 } 738 } 739 } 740 741 final boolean exists() { 742 try { 743 return jrtfs.exists(this); 744 } catch (IOException x) {} 745 return false; 746 } 747 748 final OutputStream newOutputStream(OpenOption... options) throws IOException { 749 if (options.length == 0) { 750 return jrtfs.newOutputStream(this, CREATE_NEW, WRITE); 751 } 752 return jrtfs.newOutputStream(this, options); 753 } 754 755 final void move(JrtPath target, CopyOption... options) throws IOException { 756 if (this.jrtfs == target.jrtfs) { 757 jrtfs.copyFile(true, this, target, options); 758 } else { 759 copyToTarget(target, options); 760 delete(); 761 } 762 } 763 764 final void copy(JrtPath target, CopyOption... options) throws IOException { 765 if (this.jrtfs == target.jrtfs) { 766 jrtfs.copyFile(false, this, target, options); 767 } else { 768 copyToTarget(target, options); 769 } 770 } 771 772 private void copyToTarget(JrtPath target, CopyOption... options) 773 throws IOException { 774 boolean replaceExisting = false; 775 boolean copyAttrs = false; 776 for (CopyOption opt : options) { 777 if (opt == REPLACE_EXISTING) { 778 replaceExisting = true; 779 } else if (opt == COPY_ATTRIBUTES) { 780 copyAttrs = true; 781 } 782 } 783 // attributes of source file 784 BasicFileAttributes jrtfas = getAttributes(); 785 // check if target exists 786 boolean exists; 787 if (replaceExisting) { 788 try { 789 target.deleteIfExists(); 790 exists = false; 791 } catch (DirectoryNotEmptyException x) { 792 exists = true; 793 } 794 } else { 795 exists = target.exists(); 796 } 797 if (exists) { 798 throw new FileAlreadyExistsException(target.toString()); 799 } 800 if (jrtfas.isDirectory()) { 801 // create directory or file 802 target.createDirectory(); 803 } else { 804 try (InputStream is = jrtfs.newInputStream(this); 805 OutputStream os = target.newOutputStream()) { 806 byte[] buf = new byte[8192]; 807 int n; 808 while ((n = is.read(buf)) != -1) { 809 os.write(buf, 0, n); 810 } 811 } 812 } 813 if (copyAttrs) { 814 BasicFileAttributeView view = 815 Files.getFileAttributeView(target, BasicFileAttributeView.class); 816 try { 817 view.setTimes(jrtfas.lastModifiedTime(), 818 jrtfas.lastAccessTime(), 819 jrtfas.creationTime()); 820 } catch (IOException x) { 821 try { 822 target.delete(); // rollback? 823 } catch (IOException ignore) {} 824 throw x; 825 } 826 } 827 } 828 }