1 /* 2 * Copyright (c) 2009, 2011, 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 org.openjdk.jigsaw; 27 28 import java.lang.module.*; 29 import java.io.*; 30 import java.net.URI; 31 import java.security.*; 32 import java.security.cert.*; 33 import java.util.*; 34 import java.util.jar.*; 35 import java.util.zip.*; 36 37 import static java.nio.file.StandardCopyOption.*; 38 39 /** 40 * A simple module library which stores data directly in the filesystem 41 * 42 * @see Library 43 */ 44 45 // ## TODO: Move remaining parent-searching logic upward into Library class 46 47 // On-disk library layout 48 // 49 // $LIB/%jigsaw-library 50 // com.foo.bar/1.2.3/info (= module-info.class) 51 // index (list of defined classes) 52 // config (resolved configuration, if a root) 53 // classes/com/foo/bar/... 54 // resources/com/foo/bar/... 55 // lib/libbar.so 56 // bin/bar 57 // signer (signer's certchain & timestamp) 58 // 59 // ## Issue: Concurrent access to the module library 60 // ## e.g. a module is being removed while a running application 61 // ## is depending on it 62 63 public final class SimpleLibrary 64 extends Library 65 { 66 67 private static abstract class MetaData { 68 69 protected final int maxMajorVersion; 70 protected final int maxMinorVersion; 71 protected int majorVersion; 72 protected int minorVersion; 73 private FileConstants.Type type; 74 private File file; 75 76 protected MetaData(int maxMajor, int maxMinor, 77 FileConstants.Type t, File f) 78 { 79 maxMajorVersion = majorVersion = maxMajor; 80 maxMinorVersion = minorVersion = maxMinor; 81 type = t; 82 file = f; 83 } 84 85 protected abstract void storeRest(DataOutputStream out) 86 throws IOException; 87 88 void store() throws IOException { 89 OutputStream fo = new FileOutputStream(file); 90 DataOutputStream out 91 = new DataOutputStream(new BufferedOutputStream(fo)); 92 try { 93 out.writeInt(FileConstants.MAGIC); 94 out.writeShort(type.value()); 95 out.writeShort(majorVersion); 96 out.writeShort(minorVersion); 97 storeRest(out); 98 } finally { 99 out.close(); 100 } 101 } 102 103 protected abstract void loadRest(DataInputStream in) 104 throws IOException; 105 106 protected void load() throws IOException { 107 InputStream fi = new FileInputStream(file); 108 try { 109 DataInputStream in 110 = new DataInputStream(new BufferedInputStream(fi)); 111 int m = in.readInt(); 112 if (m != FileConstants.MAGIC) 113 throw new IOException(file + ": Invalid magic number"); 114 int typ = in.readShort(); 115 if (typ != type.value()) 116 throw new IOException(file + ": Invalid file type"); 117 int maj = in.readShort(); 118 int min = in.readShort(); 119 if ( maj > maxMajorVersion 120 || (maj == maxMajorVersion && min > maxMinorVersion)) { 121 throw new IOException(file 122 + ": Futuristic version number"); 123 } 124 majorVersion = maj; 125 minorVersion = min; 126 loadRest(in); 127 } catch (EOFException x) { 128 throw new IOException(file + ": Invalid library metadata", 129 x); 130 } finally { 131 fi.close(); 132 } 133 } 134 135 } 136 137 /** 138 * Defines the storage options that SimpleLibrary supports. 139 */ 140 public static enum StorageOption { 141 DEFLATED, 142 } 143 144 private static final class Header 145 extends MetaData 146 { 147 private static final String FILE 148 = FileConstants.META_PREFIX + "jigsaw-library"; 149 150 private static final int MAJOR_VERSION = 0; 151 private static final int MINOR_VERSION = 1; 152 153 private static final int DEFLATED = 1 << 0; 154 155 private File parent; 156 private Set<StorageOption> opts; 157 158 public File parent() { return parent; } 159 public boolean isDeflated() { 160 return opts.contains(StorageOption.DEFLATED); 161 } 162 163 private Header(File root, File p, Set<StorageOption> opts) { 164 super(MAJOR_VERSION, MINOR_VERSION, 165 FileConstants.Type.LIBRARY_HEADER, 166 new File(root, FILE)); 167 this.parent = p; 168 this.opts = new HashSet<>(opts); 169 } 170 171 private Header(File root) { 172 this(root, null, Collections.<StorageOption>emptySet()); 173 } 174 175 protected void storeRest(DataOutputStream out) 176 throws IOException 177 { 178 int flags = 0; 179 if (isDeflated()) 180 flags |= DEFLATED; 181 out.writeShort(flags); 182 out.writeByte((parent != null) ? 1 : 0); 183 if (parent != null) 184 out.writeUTF(parent.toString()); 185 } 186 187 protected void loadRest(DataInputStream in) 188 throws IOException 189 { 190 opts = new HashSet<StorageOption>(); 191 int flags = in.readShort(); 192 if ((flags & DEFLATED) == DEFLATED) 193 opts.add(StorageOption.DEFLATED); 194 int b = in.readByte(); 195 if (b != 0) 196 parent = new File(in.readUTF()); 197 } 198 199 private static Header load(File f) 200 throws IOException 201 { 202 Header h = new Header(f); 203 h.load(); 204 return h; 205 } 206 207 } 208 209 private final File root; 210 private final File canonicalRoot; 211 private File parentPath = null; 212 private SimpleLibrary parent = null; 213 private final Header hd; 214 215 public String name() { return root.toString(); } 216 public File root() { return canonicalRoot; } 217 public int majorVersion() { return hd.majorVersion; } 218 public int minorVersion() { return hd.minorVersion; } 219 public SimpleLibrary parent() { return parent; } 220 public boolean isDeflated() { return hd.isDeflated(); } 221 222 private URI location = null; 223 public URI location() { 224 if (location == null) 225 location = root().toURI(); 226 return location; 227 } 228 229 @Override 230 public String toString() { 231 return (this.getClass().getName() 232 + "[" + canonicalRoot 233 + ", v" + hd.majorVersion + "." + hd.minorVersion + "]"); 234 } 235 236 private SimpleLibrary(File path, boolean create, File parentPath, Set<StorageOption> opts) 237 throws IOException 238 { 239 root = path; 240 canonicalRoot = root.getCanonicalFile(); 241 if (root.exists()) { 242 if (!root.isDirectory()) 243 throw new IOException(root + ": Exists but is not a directory"); 244 hd = Header.load(root); 245 if (hd.parent() != null) { 246 parent = open(hd.parent()); 247 parentPath = hd.parent(); 248 } 249 return; 250 } 251 if (!create) 252 throw new FileNotFoundException(root.toString()); 253 if (parentPath != null) { 254 this.parent = open(parentPath); 255 this.parentPath = this.parent.root(); 256 } 257 if (!root.mkdirs()) 258 throw new IOException(root + ": Cannot create library directory"); 259 hd = new Header(canonicalRoot, this.parentPath, opts); 260 hd.store(); 261 } 262 263 public static SimpleLibrary create(File path, File parent, Set<StorageOption> opts) 264 throws IOException 265 { 266 return new SimpleLibrary(path, true, parent, opts); 267 } 268 269 public static SimpleLibrary create(File path, File parent) 270 throws IOException 271 { 272 return new SimpleLibrary(path, true, parent, Collections.<StorageOption>emptySet()); 273 } 274 275 public static SimpleLibrary create(File path, Set<StorageOption> opts) 276 throws IOException 277 { 278 // ## Should default parent to $JAVA_HOME/lib/modules 279 return new SimpleLibrary(path, true, null, opts); 280 } 281 282 public static SimpleLibrary open(File path) 283 throws IOException 284 { 285 return new SimpleLibrary(path, false, null, Collections.<StorageOption>emptySet()); 286 } 287 288 private static final JigsawModuleSystem jms 289 = JigsawModuleSystem.instance(); 290 291 private static final class Index 292 extends MetaData 293 { 294 295 private static String FILE = "index"; 296 297 private static int MAJOR_VERSION = 0; 298 private static int MINOR_VERSION = 1; 299 300 private Set<String> publicClasses; 301 public Set<String> publicClasses() { return publicClasses; } 302 303 private Set<String> otherClasses; 304 public Set<String> otherClasses() { return otherClasses; } 305 306 private Index(File root) { 307 super(MAJOR_VERSION, MINOR_VERSION, 308 FileConstants.Type.LIBRARY_MODULE_INDEX, 309 new File(root, FILE)); 310 // Unsorted on input, because we don't need it sorted 311 publicClasses = new HashSet<String>(); 312 otherClasses = new HashSet<String>(); 313 } 314 315 private void storeSet(Set<String> cnset, DataOutputStream out) 316 throws IOException 317 { 318 // Sorted on output, because we can afford it 319 List<String> cns = new ArrayList<String>(cnset); 320 Collections.sort(cns); 321 out.writeInt(cns.size()); 322 for (String cn : cns) 323 out.writeUTF(cn); 324 } 325 326 protected void storeRest(DataOutputStream out) 327 throws IOException 328 { 329 storeSet(publicClasses, out); 330 storeSet(otherClasses, out); 331 } 332 333 private void loadSet(DataInputStream in, Set<String> cnset) 334 throws IOException 335 { 336 int n = in.readInt(); 337 for (int i = 0; i < n; i++) 338 cnset.add(in.readUTF()); 339 } 340 341 protected void loadRest(DataInputStream in) 342 throws IOException 343 { 344 loadSet(in, publicClasses); 345 loadSet(in, otherClasses); 346 } 347 348 private static Index load(File f) 349 throws IOException 350 { 351 Index ix = new Index(f); 352 ix.load(); 353 return ix; 354 } 355 356 } 357 358 private static final class StoredConfiguration 359 extends MetaData 360 { 361 362 private static String FILE = "config"; 363 364 private static int MAJOR_VERSION = 0; 365 private static int MINOR_VERSION = 1; 366 367 private Configuration<Context> cf; 368 369 private static void delete(File root) { 370 new File(root, FILE).delete(); 371 } 372 373 private StoredConfiguration(File root, Configuration<Context> conf) 374 { 375 super(MAJOR_VERSION, MINOR_VERSION, 376 FileConstants.Type.LIBRARY_MODULE_CONFIG, 377 new File(root, FILE)); 378 cf = conf; 379 } 380 381 protected void storeRest(DataOutputStream out) 382 throws IOException 383 { 384 assert cf.roots().size() == 1; 385 out.writeUTF(cf.roots().iterator().next().toString()); 386 // Contexts 387 out.writeInt(cf.contexts().size()); 388 for (Context cx : cf.contexts()) { 389 out.writeUTF(cx.name()); 390 // Module ids, and their libraries 391 out.writeInt(cx.modules().size()); 392 for (ModuleId mid : cx.modules()) { 393 out.writeUTF(mid.toString()); 394 File lp = cx.findLibraryPathForModule(mid); 395 if (lp == null) 396 out.writeUTF(""); 397 else 398 out.writeUTF(lp.toString()); 399 } 400 // Local class map 401 out.writeInt(cx.localClasses().size()); 402 for (Map.Entry<String,ModuleId> me 403 : cx.moduleForLocalClassMap().entrySet()) { 404 out.writeUTF(me.getKey()); 405 out.writeUTF(me.getValue().toString()); 406 } 407 // Remote package map 408 out.writeInt(cx.contextForRemotePackageMap().size()); 409 for (Map.Entry<String,String> me 410 : cx.contextForRemotePackageMap().entrySet()) { 411 out.writeUTF(me.getKey()); 412 out.writeUTF(me.getValue()); 413 } 414 // Suppliers 415 out.writeInt(cx.remoteContexts().size()); 416 for (String cxn : cx.remoteContexts()) { 417 out.writeUTF(cxn); 418 } 419 } 420 } 421 422 protected void loadRest(DataInputStream in) 423 throws IOException 424 { 425 String root = in.readUTF(); 426 ModuleId rmid = jms.parseModuleId(root); 427 cf = new Configuration<Context>(rmid); 428 // Contexts 429 int nContexts = in.readInt(); 430 for (int i = 0; i < nContexts; i++) { 431 Context cx = new Context(); 432 String cxn = in.readUTF(); 433 // Module ids 434 int nModules = in.readInt(); 435 for (int j = 0; j < nModules; j++) { 436 ModuleId mid = jms.parseModuleId(in.readUTF()); 437 cx.add(mid); 438 cf.put(mid.name(), cx); 439 String lps = in.readUTF(); 440 if (lps.length() > 0) 441 cx.putLibraryPathForModule(mid, new File(lps)); 442 } 443 cx.freeze(); 444 assert cx.name().equals(cxn); 445 cf.add(cx); 446 // Local class map 447 int nClasses = in.readInt(); 448 for (int j = 0; j < nClasses; j++) 449 cx.putModuleForLocalClass(in.readUTF(), 450 jms.parseModuleId(in.readUTF())); 451 // Remote package map 452 int nPackages = in.readInt(); 453 for (int j = 0; j < nPackages; j++) 454 cx.putContextForRemotePackage(in.readUTF(), in.readUTF()); 455 456 // Suppliers 457 int nSuppliers = in.readInt(); 458 for (int j = 0; j < nSuppliers; j++) 459 cx.addSupplier(in.readUTF()); 460 } 461 462 } 463 464 private static StoredConfiguration load(File f) 465 throws IOException 466 { 467 StoredConfiguration sp = new StoredConfiguration(f, null); 468 sp.load(); 469 return sp; 470 } 471 472 } 473 474 private static final class Signers 475 extends MetaData { 476 477 private static String FILE = "signer"; 478 private static int MAJOR_VERSION = 0; 479 private static int MINOR_VERSION = 1; 480 481 private CertificateFactory cf = null; 482 private Set<CodeSigner> signers; 483 private Set<CodeSigner> signers() { return signers; } 484 485 private Signers(File root, Set<CodeSigner> signers) { 486 super(MAJOR_VERSION, MINOR_VERSION, 487 FileConstants.Type.LIBRARY_MODULE_SIGNER, 488 new File(root, FILE)); 489 this.signers = signers; 490 } 491 492 protected void storeRest(DataOutputStream out) 493 throws IOException 494 { 495 out.writeInt(signers.size()); 496 for (CodeSigner signer : signers) { 497 try { 498 CertPath signerCertPath = signer.getSignerCertPath(); 499 out.write(signerCertPath.getEncoded("PkiPath")); 500 Timestamp ts = signer.getTimestamp(); 501 out.writeByte((ts != null) ? 1 : 0); 502 if (ts != null) { 503 out.writeLong(ts.getTimestamp().getTime()); 504 out.write(ts.getSignerCertPath().getEncoded("PkiPath")); 505 } 506 } catch (CertificateEncodingException cee) { 507 throw new IOException(cee); 508 } 509 } 510 } 511 512 protected void loadRest(DataInputStream in) 513 throws IOException 514 { 515 int size = in.readInt(); 516 for (int i = 0; i < size; i++) { 517 try { 518 if (cf == null) 519 cf = CertificateFactory.getInstance("X.509"); 520 CertPath signerCertPath = cf.generateCertPath(in, "PkiPath"); 521 int b = in.readByte(); 522 if (b != 0) { 523 Date timestamp = new Date(in.readLong()); 524 CertPath tsaCertPath = cf.generateCertPath(in, "PkiPath"); 525 Timestamp ts = new Timestamp(timestamp, tsaCertPath); 526 signers.add(new CodeSigner(signerCertPath, ts)); 527 } else { 528 signers.add(new CodeSigner(signerCertPath, null)); 529 } 530 } catch (CertificateException ce) { 531 throw new IOException(ce); 532 } 533 } 534 } 535 536 private static Signers load(File f) 537 throws IOException 538 { 539 Signers signers = new Signers(f, new HashSet<CodeSigner>()); 540 signers.load(); 541 return signers; 542 } 543 } 544 545 private void gatherLocalModuleIds(File mnd, Set<ModuleId> mids) 546 throws IOException 547 { 548 if (!mnd.isDirectory()) 549 throw new IOException(mnd + ": Not a directory"); 550 if (!mnd.canRead()) 551 throw new IOException(mnd + ": Not readable"); 552 for (String v : mnd.list()) { 553 mids.add(jms.parseModuleId(mnd.getName(), v)); 554 } 555 } 556 557 private void gatherLocalModuleIds(Set<ModuleId> mids) 558 throws IOException 559 { 560 File[] mnds = root.listFiles(); 561 for (File mnd : mnds) { 562 if (mnd.getName().startsWith(FileConstants.META_PREFIX)) 563 continue; 564 gatherLocalModuleIds(mnd, mids); 565 } 566 } 567 568 protected void gatherLocalModuleIds(String moduleName, 569 Set<ModuleId> mids) 570 throws IOException 571 { 572 if (moduleName == null) { 573 gatherLocalModuleIds(mids); 574 return; 575 } 576 File mnd = new File(root, moduleName); 577 if (mnd.exists()) 578 gatherLocalModuleIds(mnd, mids); 579 } 580 581 private void checkModuleId(ModuleId mid) { 582 Version v = mid.version(); 583 if (v == null) 584 return; 585 if (!(v instanceof JigsawVersion)) 586 throw new IllegalArgumentException(mid + ": Not a Jigsaw module id"); 587 } 588 589 private File moduleDir(ModuleId mid) { 590 Version v = mid.version(); 591 String vs = (v != null) ? v.toString() : "default"; 592 return new File(new File(root, mid.name()), vs); 593 } 594 595 private void checkModuleDir(File md) 596 throws IOException 597 { 598 if (!md.isDirectory()) 599 throw new IOException(md + ": Not a directory"); 600 if (!md.canRead()) 601 throw new IOException(md + ": Not readable"); 602 } 603 604 private File findModuleDir(ModuleId mid) 605 throws IOException 606 { 607 checkModuleId(mid); 608 File md = moduleDir(mid); 609 if (!md.exists()) 610 return null; 611 checkModuleDir(md); 612 return md; 613 } 614 615 private void deleteModuleDir(ModuleId mid) 616 throws IOException 617 { 618 File md = findModuleDir(mid); 619 if (md == null) 620 return; 621 Files.deleteTree(md); 622 File mnd = md.getParentFile(); 623 if (mnd.list().length == 0) { 624 if (!mnd.delete()) 625 throw new IOException(mnd + ": Cannot delete"); 626 } 627 } 628 629 public byte[] readLocalModuleInfoBytes(ModuleId mid) 630 throws IOException 631 { 632 File md = findModuleDir(mid); 633 if (md == null) 634 return null; 635 return Files.load(new File(md, "info")); 636 } 637 638 public CodeSigner[] readLocalCodeSigners(ModuleId mid) 639 throws IOException 640 { 641 File md = findModuleDir(mid); 642 if (md == null) 643 return null; 644 // Only one signer is currently supported 645 File f = new File(md, "signer"); 646 // ## concurrency issues : what is the expected behavior if file is 647 // ## removed by another thread/process here? 648 if (!f.exists()) 649 return null; 650 return Signers.load(md).signers().toArray(new CodeSigner[0]); 651 } 652 653 // ## Close all zip files when we close this library 654 private Map<ModuleId, Object> contentForModule = new HashMap<>(); 655 private Object NONE = new Object(); 656 657 private Object findContent(ModuleId mid) 658 throws IOException 659 { 660 Object o = contentForModule.get(mid); 661 if (o != null) 662 return o; 663 if (o == NONE) 664 return null; 665 File md = findModuleDir(mid); 666 if (md == null) { 667 contentForModule.put(mid, NONE); 668 return null; 669 } 670 File cf = new File(md, "classes"); 671 if (cf.isFile()) { 672 ZipFile zf = new ZipFile(cf); 673 contentForModule.put(mid, zf); 674 return zf; 675 } 676 if (cf.isDirectory()) { 677 contentForModule.put(mid, cf); 678 return cf; 679 } 680 contentForModule.put(mid, NONE); 681 return null; 682 } 683 684 private byte[] loadContent(ZipFile zf, String path) 685 throws IOException 686 { 687 ZipEntry ze = zf.getEntry(path); 688 if (ze == null) 689 return null; 690 return Files.load(zf.getInputStream(ze), (int)ze.getSize()); 691 } 692 693 private byte[] loadContent(ModuleId mid, String path) 694 throws IOException 695 { 696 Object o = findContent(mid); 697 if (o == null) 698 return null; 699 if (o instanceof ZipFile) { 700 ZipFile zf = (ZipFile)o; 701 ZipEntry ze = zf.getEntry(path); 702 if (ze == null) 703 return null; 704 return Files.load(zf.getInputStream(ze), (int)ze.getSize()); 705 } 706 if (o instanceof File) { 707 File f = new File((File)o, path); 708 if (!f.exists()) 709 return null; 710 return Files.load(f); 711 } 712 assert false; 713 return null; 714 } 715 716 private URI locateContent(ModuleId mid, String path) 717 throws IOException 718 { 719 Object o = findContent(mid); 720 if (o == null) 721 return null; 722 if (o instanceof ZipFile) { 723 ZipFile zf = (ZipFile)o; 724 ZipEntry ze = zf.getEntry(path); 725 if (ze == null) 726 return null; 727 return URI.create("jar:" 728 + new File(zf.getName()).toURI().toString() 729 + "!/" + path); 730 } 731 if (o instanceof File) { 732 File f = new File((File)o, path); 733 if (!f.exists()) 734 return null; 735 return f.toURI(); 736 } 737 assert false; 738 return null; 739 } 740 741 public byte[] readLocalClass(ModuleId mid, String className) 742 throws IOException 743 { 744 return loadContent(mid, className.replace('.', '/') + ".class"); 745 } 746 747 public List<String> listLocalClasses(ModuleId mid, boolean all) 748 throws IOException 749 { 750 File md = findModuleDir(mid); 751 if (md == null) 752 return null; 753 Index ix = Index.load(md); 754 int os = all ? ix.otherClasses().size() : 0; 755 ArrayList<String> cns 756 = new ArrayList<String>(ix.publicClasses().size() + os); 757 cns.addAll(ix.publicClasses()); 758 if (all) 759 cns.addAll(ix.otherClasses()); 760 return cns; 761 } 762 763 public Configuration<Context> readConfiguration(ModuleId mid) 764 throws IOException 765 { 766 File md = findModuleDir(mid); 767 if (md == null) { 768 if (parent != null) 769 return parent.readConfiguration(mid); 770 return null; 771 } 772 StoredConfiguration scf = StoredConfiguration.load(md); 773 return scf.cf; 774 } 775 776 private boolean addToIndex(ClassInfo ci, Index ix) 777 throws IOException 778 { 779 if (ci.isModuleInfo()) 780 return false; 781 if (ci.moduleName() != null) { 782 // ## From early Jigsaw development; can probably delete now 783 throw new IOException("Old-style class file with" 784 + " module attribute"); 785 } 786 if (ci.isPublic()) 787 ix.publicClasses().add(ci.name()); 788 else 789 ix.otherClasses().add(ci.name()); 790 return true; 791 } 792 793 private void reIndex(ModuleId mid) 794 throws IOException 795 { 796 797 File md = findModuleDir(mid); 798 if (md == null) 799 throw new IllegalArgumentException(mid + ": No such module"); 800 File cd = new File(md, "classes"); 801 final Index ix = new Index(md); 802 803 if (cd.isDirectory()) { 804 Files.walkTree(cd, new Files.Visitor<File>() { 805 public void accept(File f) throws IOException { 806 if (f.getPath().endsWith(".class")) 807 addToIndex(ClassInfo.read(f), ix); 808 } 809 }); 810 } else if (cd.isFile()) { 811 FileInputStream fis = new FileInputStream(cd); 812 ZipInputStream zis = new ZipInputStream(fis); 813 ZipEntry ze; 814 while ((ze = zis.getNextEntry()) != null) { 815 if (!ze.getName().endsWith(".class")) 816 continue; 817 addToIndex(ClassInfo.read(Files.nonClosingStream(zis), 818 ze.getSize(), 819 mid + ":" + ze.getName()), 820 ix); 821 } 822 } 823 824 ix.store(); 825 } 826 827 /** 828 * Strip the debug attributes from the classes in a given module 829 * directory. 830 */ 831 private void strip(File md) throws IOException { 832 File classes = new File(md, "classes"); 833 if (classes.isFile()) { 834 File pf = new File(md, "classes.pack"); 835 try (JarFile jf = new JarFile(classes); 836 FileOutputStream out = new FileOutputStream(pf)) 837 { 838 Pack200.Packer packer = Pack200.newPacker(); 839 Map<String,String> p = packer.properties(); 840 p.put("com.sun.java.util.jar.pack.strip.debug", Pack200.Packer.TRUE); 841 packer.pack(jf, out); 842 } 843 844 try (OutputStream out = new FileOutputStream(classes); 845 JarOutputStream jos = new JarOutputStream(out)) 846 { 847 Pack200.Unpacker unpacker = Pack200.newUnpacker(); 848 unpacker.unpack(pf, jos); 849 } finally { 850 pf.delete(); 851 } 852 } 853 } 854 855 private void install(Manifest mf, File dst, boolean strip) 856 throws IOException 857 { 858 if (mf.classes().size() > 1) 859 throw new IllegalArgumentException("Multiple module-class" 860 + " directories" 861 + " not yet supported"); 862 if (mf.classes().size() < 1) 863 throw new IllegalArgumentException("At least one module-class" 864 + " directory required"); 865 File classes = mf.classes().get(0); 866 final String mn = mf.module(); 867 868 File mif = new File(classes, "module-info.class"); 869 File src = null; 870 if (mif.exists()) { 871 src = classes; 872 } else { 873 src = new File(classes, mn); 874 mif = new File(src, "module-info.class"); 875 } 876 byte[] bs = Files.load(mif); 877 ModuleInfo mi = jms.parseModuleInfo(bs); 878 if (!mi.id().name().equals(mn)) { 879 // ## Need a more appropriate throwable here 880 throw new Error(mif + " is for module " + mi.id().name() 881 + ", not " + mn); 882 } 883 String m = mi.id().name(); 884 JigsawVersion v = (JigsawVersion)mi.id().version(); 885 String vs = (v == null) ? "default" : v.toString(); 886 File mdst = new File(new File(dst, m), vs); 887 if (mdst.exists()) 888 Files.deleteTree(mdst); 889 Files.mkdirs(mdst, "module"); 890 Files.store(bs, new File(mdst, "info")); 891 File cldst = new File(mdst, "classes"); 892 893 // Delete the config file, if one exists 894 StoredConfiguration.delete(mdst); 895 896 if (false) { 897 898 // ## Retained for now in case we later want to add an option 899 // ## to install into a tree rather than a zip file 900 901 // Copy class files and build index 902 final Index ix = new Index(mdst); 903 Files.copyTree(src, cldst, new Files.Filter<File>() { 904 public boolean accept(File f) throws IOException { 905 if (f.isDirectory()) 906 return true; 907 if (f.getName().endsWith(".class")) { 908 return addToIndex(ClassInfo.read(f), ix); 909 } else { 910 return true; 911 } 912 }}); 913 ix.store(); 914 } else { 915 FileOutputStream fos 916 = new FileOutputStream(new File(mdst, "classes")); 917 JarOutputStream jos 918 = new JarOutputStream(new BufferedOutputStream(fos)); 919 try { 920 921 // Copy class files and build index 922 final Index ix = new Index(mdst); 923 Files.storeTree(src, jos, isDeflated(), new Files.Filter<File>() { 924 public boolean accept(File f) throws IOException { 925 if (f.isDirectory()) 926 return true; 927 if (f.getName().endsWith(".class")) { 928 return addToIndex(ClassInfo.read(f), ix); 929 } else { 930 return true; 931 } 932 }}); 933 ix.store(); 934 } finally { 935 jos.close(); 936 } 937 if (strip) 938 strip(mdst); 939 } 940 941 } 942 943 private void install(Collection<Manifest> mfs, File dst, boolean strip) 944 throws IOException 945 { 946 for (Manifest mf : mfs) 947 install(mf, dst, strip); 948 } 949 950 public void installFromManifests(Collection<Manifest> mfs, boolean strip) 951 throws ConfigurationException, IOException 952 { 953 install(mfs, root, strip); 954 configure(null); 955 } 956 957 @Override 958 public void installFromManifests(Collection<Manifest> mfs) 959 throws ConfigurationException, IOException 960 { 961 installFromManifests(mfs, false); 962 } 963 964 private ModuleFileVerifier.Parameters mfvParams; 965 966 private ModuleId install(InputStream is, boolean verifySignature, boolean strip) 967 throws ConfigurationException, IOException, SignatureException 968 { 969 BufferedInputStream bin = new BufferedInputStream(is); 970 DataInputStream in = new DataInputStream(bin); 971 File md = null; 972 try (ModuleFile.Reader mr = new ModuleFile.Reader(in)) { 973 byte[] mib = mr.readStart(); 974 ModuleInfo mi = jms.parseModuleInfo(mib); 975 md = moduleDir(mi.id()); 976 ModuleId mid = mi.id(); 977 if (md.exists()) 978 throw new ConfigurationException(mid + ": Already installed"); 979 if (!md.mkdirs()) 980 throw new IOException(md + ": Cannot create"); 981 982 if (verifySignature && mr.hasSignature()) { 983 ModuleFileVerifier mfv = new SignedModule.PKCS7Verifier(mr); 984 if (mfvParams == null) { 985 mfvParams = new SignedModule.VerifierParameters(); 986 } 987 // Verify the module signature and validate the signer's 988 // certificate chain 989 Set<CodeSigner> signers = mfv.verifySignature(mfvParams); 990 991 // Verify the module header hash and the module info hash 992 mfv.verifyHashesStart(mfvParams); 993 994 // ## Check policy - is signer trusted and what permissions 995 // ## should be granted? 996 997 // Store signer info 998 new Signers(md, signers).store(); 999 1000 // Read and verify the rest of the hashes 1001 mr.readRest(md, isDeflated()); 1002 mfv.verifyHashesRest(mfvParams); 1003 } else { 1004 mr.readRest(md, isDeflated()); 1005 } 1006 1007 if (strip) 1008 strip(md); 1009 reIndex(mid); // ## Could do this while reading module file 1010 return mid; 1011 1012 } catch (IOException | SignatureException x) { 1013 if (md != null && md.exists()) { 1014 try { 1015 Files.deleteTree(md); 1016 } catch (IOException y) { 1017 y.initCause(x); 1018 throw y; 1019 } 1020 } 1021 throw x; 1022 } 1023 } 1024 1025 private ModuleId installFromJarFile(File mf, boolean verifySignature, boolean strip) 1026 throws ConfigurationException, IOException, SignatureException 1027 { 1028 File md = null; 1029 try (JarFile jf = new JarFile(mf, verifySignature)) { 1030 ModuleInfo mi = jf.getModuleInfo(); 1031 if (mi == null) 1032 throw new ConfigurationException(mf + ": not a modular JAR file"); 1033 1034 md = moduleDir(mi.id()); 1035 ModuleId mid = mi.id(); 1036 if (md.exists()) 1037 throw new ConfigurationException(mid + ": Already installed"); 1038 if (!md.mkdirs()) 1039 throw new IOException(md + ": Cannot create"); 1040 1041 boolean signed = false; 1042 1043 // copy the jar file to the module library 1044 File classesDir = new File(md, "classes"); 1045 try (FileOutputStream fos = new FileOutputStream(classesDir); 1046 BufferedOutputStream bos = new BufferedOutputStream(fos); 1047 JarOutputStream jos = new JarOutputStream(bos)) { 1048 jos.setLevel(0); 1049 1050 Enumeration<JarEntry> entries = jf.entries(); 1051 while (entries.hasMoreElements()) { 1052 JarEntry je = entries.nextElement(); 1053 try (InputStream is = jf.getInputStream(je)) { 1054 if (je.getName().equals(JarFile.MODULEINFO_NAME)) { 1055 java.nio.file.Files.copy(is, md.toPath().resolve("info")); 1056 } else { 1057 writeJarEntry(is, je, jos); 1058 } 1059 } 1060 if (!signed) { 1061 String name = je.getName().toUpperCase(Locale.ENGLISH); 1062 signed = name.startsWith("META-INF/") 1063 && name.endsWith(".SF"); 1064 } 1065 } 1066 } 1067 1068 try { 1069 if (verifySignature && signed) { 1070 // validate the code signers 1071 Set<CodeSigner> signers = getSigners(jf); 1072 SignedModule.validateSigners(signers); 1073 // store the signers 1074 new Signers(md, signers).store(); 1075 } 1076 } catch (CertificateException ce) { 1077 throw new SignatureException(ce); 1078 } 1079 1080 if (strip) 1081 strip(md); 1082 reIndex(mid); 1083 return mid; 1084 } catch (IOException | SignatureException x) { 1085 if (md != null && md.exists()) { 1086 try { 1087 Files.deleteTree(md); 1088 } catch (IOException y) { 1089 y.initCause(x); 1090 throw y; 1091 } 1092 } 1093 throw x; 1094 } 1095 } 1096 1097 /** 1098 * Returns the set of signers of the specified jar file. Each signer 1099 * must have signed all relevant entries. 1100 */ 1101 private static Set<CodeSigner> getSigners(JarFile jf) 1102 throws SignatureException 1103 { 1104 Set<CodeSigner> signers = new HashSet<>(); 1105 Enumeration<JarEntry> entries = jf.entries(); 1106 while (entries.hasMoreElements()) { 1107 JarEntry je = entries.nextElement(); 1108 String name = je.getName().toUpperCase(Locale.ENGLISH); 1109 if (name.endsWith("/") || isSigningRelated(name)) 1110 continue; 1111 1112 // A signed modular jar can be signed by multiple signers. 1113 // However, all entries must be signed by each of these signers. 1114 // Signers that only sign a subset of entries are ignored. 1115 CodeSigner[] jeSigners = je.getCodeSigners(); 1116 if (jeSigners == null || jeSigners.length == 0) 1117 throw new SignatureException("Found unsigned entry in " 1118 + "signed modular JAR"); 1119 1120 Set<CodeSigner> jeSignerSet = 1121 new HashSet<>(Arrays.asList(jeSigners)); 1122 if (signers.isEmpty()) 1123 signers.addAll(jeSignerSet); 1124 else { 1125 if (signers.retainAll(jeSignerSet) && signers.isEmpty()) 1126 throw new SignatureException("No signers in common in " 1127 + "signed modular JAR"); 1128 } 1129 } 1130 return signers; 1131 } 1132 1133 // true if file is part of the signature mechanism itself 1134 private static boolean isSigningRelated(String name) { 1135 if (!name.startsWith("META-INF/")) { 1136 return false; 1137 } 1138 name = name.substring(9); 1139 if (name.indexOf('/') != -1) { 1140 return false; 1141 } 1142 if (name.endsWith(".DSA") || 1143 name.endsWith(".RSA") || 1144 name.endsWith(".SF") || 1145 name.endsWith(".EC") || 1146 name.startsWith("SIG-") || 1147 name.equals("MANIFEST.MF")) { 1148 return true; 1149 } 1150 return false; 1151 } 1152 1153 private void writeJarEntry(InputStream is, JarEntry je, JarOutputStream jos) 1154 throws IOException, SignatureException 1155 { 1156 JarEntry entry = new JarEntry(je.getName()); 1157 entry.setMethod(isDeflated() ? ZipEntry.DEFLATED : ZipEntry.STORED); 1158 entry.setTime(je.getTime()); 1159 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 1160 int size = 0; 1161 byte[] bs = new byte[1024]; 1162 int cc = 0; 1163 // This will throw a SecurityException if a signature is invalid. 1164 while ((cc = is.read(bs)) > 0) { 1165 baos.write(bs, 0, cc); 1166 size += cc; 1167 } 1168 if (!isDeflated()) { 1169 entry.setSize(size); 1170 entry.setCrc(je.getCrc()); 1171 entry.setCompressedSize(size); 1172 } 1173 jos.putNextEntry(entry); 1174 if (baos.size() > 0) 1175 baos.writeTo(jos); 1176 jos.closeEntry(); 1177 } catch (SecurityException se) { 1178 throw new SignatureException(se); 1179 } 1180 } 1181 1182 private ModuleId install(File mf, boolean verifySignature, boolean strip) 1183 throws ConfigurationException, IOException, SignatureException 1184 { 1185 ModuleId mid; 1186 if (mf.getName().endsWith(".jar")) 1187 mid = installFromJarFile(mf, verifySignature, strip); 1188 else { 1189 try (FileInputStream in = new FileInputStream(mf)) { 1190 mid = install(in, verifySignature, strip); 1191 } 1192 } 1193 return mid; 1194 } 1195 1196 public void install(Collection<File> mfs, boolean verifySignature, boolean strip) 1197 throws ConfigurationException, IOException, SignatureException 1198 { 1199 List<ModuleId> mids = new ArrayList<>(); 1200 boolean complete = false; 1201 Throwable ox = null; 1202 try { 1203 for (File mf : mfs) 1204 mids.add(install(mf, verifySignature, strip)); 1205 configure(mids); 1206 complete = true; 1207 } catch (IOException|ConfigurationException x) { 1208 ox = x; 1209 throw x; 1210 } finally { 1211 if (!complete) { 1212 try { 1213 for (ModuleId mid : mids) 1214 deleteModuleDir(mid); 1215 } catch (IOException x) { 1216 if (ox != null) 1217 x.initCause(ox); 1218 throw x; 1219 } 1220 } 1221 } 1222 } 1223 1224 @Override 1225 public void install(Collection<File> mfs, boolean verifySignature) 1226 throws ConfigurationException, IOException, SignatureException 1227 { 1228 install(mfs, verifySignature, false); 1229 } 1230 1231 // Public entry point, since the Resolver itself is package-private 1232 // 1233 public Resolution resolve(Collection<ModuleIdQuery> midqs) 1234 throws ConfigurationException, IOException 1235 { 1236 return Resolver.run(this, midqs); 1237 } 1238 1239 public void install(Resolution res, boolean verifySignature, boolean strip) 1240 throws ConfigurationException, IOException, SignatureException 1241 { 1242 // ## Handle case of installing multiple root modules 1243 assert res.rootQueries.size() == 1; 1244 ModuleIdQuery midq = res.rootQueries.iterator().next(); 1245 ModuleInfo root = null; 1246 for (ModuleInfo mi : res.modules) { 1247 if (midq.matches(mi.id())) { 1248 root = mi; 1249 break; 1250 } 1251 } 1252 assert root != null; 1253 1254 // Download 1255 // 1256 for (ModuleId mid : res.modulesNeeded()) { 1257 URI u = res.locationForName.get(mid.name()); 1258 assert u != null; 1259 RemoteRepository rr = repositoryList().firstRepository(); 1260 assert rr != null; 1261 install(rr.fetch(mid), verifySignature, strip); 1262 res.locationForName.put(mid.name(), location()); 1263 // ## If something goes wrong, delete all our modules 1264 } 1265 1266 // Configure 1267 // 1268 Configuration<Context> cf 1269 = Configurator.configure(this, res); 1270 new StoredConfiguration(moduleDir(root.id()), cf).store(); 1271 } 1272 1273 @Override 1274 public void install(Resolution res, boolean verifySignature) 1275 throws ConfigurationException, IOException, SignatureException 1276 { 1277 install(res, verifySignature, false); 1278 } 1279 1280 /** 1281 * <p> Pre-install one or more modules to an arbitrary destination 1282 * directory. </p> 1283 * 1284 * <p> A pre-installed module has the same format as within the library 1285 * itself, except that there is never a configuration file. </p> 1286 * 1287 * <p> This method is provided for use by the module-packaging tool. </p> 1288 * 1289 * @param mfs 1290 * The manifest describing the contents of the modules to be 1291 * pre-installed 1292 * 1293 * @param dst 1294 * The destination directory, with one subdirectory per module 1295 * name, each of which contains one subdirectory per version 1296 */ 1297 public void preInstall(Collection<Manifest> mfs, File dst) 1298 throws IOException 1299 { 1300 Files.mkdirs(dst, "module destination"); 1301 install(mfs, dst, false); 1302 } 1303 1304 public void preInstall(Manifest mf, File dst) 1305 throws IOException 1306 { 1307 preInstall(Collections.singleton(mf), dst); 1308 } 1309 1310 /** 1311 * <p> Update the configurations of any root modules affected by the 1312 * copying of the named modules, in pre-installed format, into this 1313 * library. </p> 1314 * 1315 * @param mids 1316 * The module ids of the new or updated modules, or 1317 * {@code null} if the configuration of every root module 1318 * should be (re)computed 1319 */ 1320 public void configure(List<ModuleId> mids) 1321 throws ConfigurationException, IOException 1322 { 1323 // ## mids not used yet 1324 for (ModuleInfo mi : listLocalRootModuleInfos()) { 1325 // ## We could be a lot more clever about this! 1326 Configuration<Context> cf 1327 = Configurator.configure(this, mi.id().toQuery()); 1328 new StoredConfiguration(moduleDir(mi.id()), cf).store(); 1329 } 1330 } 1331 1332 public URI findLocalResource(ModuleId mid, String name) 1333 throws IOException 1334 { 1335 return locateContent(mid, name); 1336 } 1337 1338 public File findLocalNativeLibrary(ModuleId mid, String name) 1339 throws IOException 1340 { 1341 File md = findModuleDir(mid); 1342 if (md == null) 1343 return null; 1344 File f = new File(new File(md, "lib"), name); 1345 if (!f.exists()) 1346 return null; 1347 return f; 1348 } 1349 1350 public File classPath(ModuleId mid) 1351 throws IOException 1352 { 1353 File md = findModuleDir(mid); 1354 if (md == null) { 1355 if (parent != null) 1356 return parent.classPath(mid); 1357 return null; 1358 } 1359 // ## Check for other formats here 1360 return new File(md, "classes"); 1361 } 1362 1363 /** 1364 * <p> Re-index the classes of the named previously-installed modules, and 1365 * then update the configurations of any affected root modules. </p> 1366 * 1367 * <p> This method is intended for use during development, when a build 1368 * process may update a previously-installed module in place, adding or 1369 * removing classes. </p> 1370 * 1371 * @param mids 1372 * The module ids of the new or updated modules, or 1373 * {@code null} if the configuration of every root module 1374 * should be (re)computed 1375 */ 1376 public void reIndex(List<ModuleId> mids) 1377 throws ConfigurationException, IOException 1378 { 1379 for (ModuleId mid : mids) 1380 reIndex(mid); 1381 configure(mids); 1382 } 1383 1384 1385 // -- Repositories -- 1386 1387 private static class RepoList 1388 implements RemoteRepositoryList 1389 { 1390 1391 private static final int MINOR_VERSION = 0; 1392 private static final int MAJOR_VERSION = 0; 1393 1394 private final File root; 1395 private final File listFile; 1396 1397 private RepoList(File r) { 1398 root = new File(r, FileConstants.META_PREFIX + "repos"); 1399 listFile = new File(root, FileConstants.META_PREFIX + "list"); 1400 } 1401 1402 private static FileHeader fileHeader() { 1403 return (new FileHeader() 1404 .type(FileConstants.Type.REMOTE_REPO_LIST) 1405 .majorVersion(MAJOR_VERSION) 1406 .minorVersion(MINOR_VERSION)); 1407 } 1408 1409 private List<RemoteRepository> repos = null; 1410 private long nextRepoId = 0; 1411 1412 private File repoDir(long id) { 1413 return new File(root, Long.toHexString(id)); 1414 } 1415 1416 private void load() throws IOException { 1417 1418 repos = new ArrayList<>(); 1419 if (!root.exists() || !listFile.exists()) 1420 return; 1421 FileInputStream fin = new FileInputStream(listFile); 1422 DataInputStream in 1423 = new DataInputStream(new BufferedInputStream(fin)); 1424 try { 1425 1426 FileHeader fh = fileHeader(); 1427 fh.read(in); 1428 nextRepoId = in.readLong(); 1429 int n = in.readInt(); 1430 long[] ids = new long[n]; 1431 for (int i = 0; i < n; i++) 1432 ids[i] = in.readLong(); 1433 RemoteRepository parent = null; 1434 1435 // Load in reverse order so that parents are correct 1436 for (int i = n - 1; i >= 0; i--) { 1437 long id = ids[i]; 1438 RemoteRepository rr 1439 = RemoteRepository.open(repoDir(id), id, parent); 1440 repos.add(rr); 1441 parent = rr; 1442 } 1443 Collections.reverse(repos); 1444 1445 } finally { 1446 in.close(); 1447 } 1448 1449 } 1450 1451 private List<RemoteRepository> roRepos = null; 1452 1453 // Unmodifiable 1454 public List<RemoteRepository> repositories() throws IOException { 1455 if (repos == null) { 1456 load(); 1457 roRepos = Collections.unmodifiableList(repos); 1458 } 1459 return roRepos; 1460 } 1461 1462 public RemoteRepository firstRepository() throws IOException { 1463 repositories(); 1464 return repos.isEmpty() ? null : repos.get(0); 1465 } 1466 1467 private void store() throws IOException { 1468 File newfn = new File(root, "list.new"); 1469 FileOutputStream fout = new FileOutputStream(newfn); 1470 DataOutputStream out 1471 = new DataOutputStream(new BufferedOutputStream(fout)); 1472 try { 1473 try { 1474 fileHeader().write(out); 1475 out.writeLong(nextRepoId); 1476 out.writeInt(repos.size()); 1477 for (RemoteRepository rr : repos) 1478 out.writeLong(rr.id()); 1479 } finally { 1480 out.close(); 1481 } 1482 } catch (IOException x) { 1483 newfn.delete(); 1484 throw x; 1485 } 1486 java.nio.file.Files.move(newfn.toPath(), listFile.toPath(), ATOMIC_MOVE); 1487 } 1488 1489 public RemoteRepository add(URI u, int position) 1490 throws IOException 1491 { 1492 1493 if (repos == null) 1494 load(); 1495 for (RemoteRepository rr : repos) { 1496 if (rr.location().equals(u)) // ## u not canonical 1497 throw new IllegalStateException(u + ": Already in" 1498 + " repository list"); 1499 } 1500 if (!root.exists()) { 1501 if (!root.mkdir()) 1502 throw new IOException(root + ": Cannot create directory"); 1503 } 1504 1505 if (repos.size() == Integer.MAX_VALUE) 1506 throw new IllegalStateException("Too many repositories"); 1507 if (position < 0) 1508 throw new IllegalArgumentException("Invalid index"); 1509 1510 long id = nextRepoId++; 1511 RemoteRepository rr = RemoteRepository.create(repoDir(id), u, id); 1512 try { 1513 rr.updateCatalog(true); 1514 } catch (IOException x) { 1515 repoDir(id).delete(); 1516 throw x; 1517 } 1518 1519 if (position >= repos.size()) { 1520 repos.add(rr); 1521 } else if (position >= 0) { 1522 List<RemoteRepository> prefix 1523 = new ArrayList<>(repos.subList(0, position)); 1524 List<RemoteRepository> suffix 1525 = new ArrayList<>(repos.subList(position, repos.size())); 1526 repos.clear(); 1527 repos.addAll(prefix); 1528 repos.add(rr); 1529 repos.addAll(suffix); 1530 } 1531 store(); 1532 1533 return rr; 1534 1535 } 1536 1537 public boolean remove(RemoteRepository rr) 1538 throws IOException 1539 { 1540 if (!repos.remove(rr)) 1541 return false; 1542 store(); 1543 File rd = repoDir(rr.id()); 1544 for (File f : rd.listFiles()) { 1545 if (!f.delete()) 1546 throw new IOException(f + ": Cannot delete"); 1547 } 1548 if (!rd.delete()) 1549 throw new IOException(rd + ": Cannot delete"); 1550 return true; 1551 } 1552 1553 public boolean areCatalogsStale() throws IOException { 1554 for (RemoteRepository rr : repos) { 1555 if (rr.isCatalogStale()) 1556 return true; 1557 } 1558 return false; 1559 } 1560 1561 public boolean updateCatalogs(boolean force) throws IOException { 1562 boolean updated = false; 1563 for (RemoteRepository rr : repos) { 1564 if (rr.updateCatalog(force)) 1565 updated = true; 1566 } 1567 return updated; 1568 } 1569 1570 } 1571 1572 private RemoteRepositoryList repoList = null; 1573 1574 public RemoteRepositoryList repositoryList() 1575 throws IOException 1576 { 1577 if (repoList == null) 1578 repoList = new RepoList(root); 1579 return repoList; 1580 } 1581 1582 } --- EOF ---