Source for gnu.java.security.PolicyFile

   1: /* PolicyFile.java -- policy file reader
   2:    Copyright (C) 2004, 2005, 2006  Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: package gnu.java.security;
  39: 
  40: import gnu.classpath.debug.Component;
  41: import gnu.classpath.debug.SystemLogger;
  42: import gnu.java.security.action.GetPropertyAction;
  43: 
  44: import java.io.File;
  45: import java.io.IOException;
  46: import java.io.InputStreamReader;
  47: import java.io.StreamTokenizer;
  48: import java.lang.reflect.Constructor;
  49: import java.net.MalformedURLException;
  50: import java.net.URL;
  51: import java.security.AccessController;
  52: import java.security.CodeSource;
  53: import java.security.KeyStore;
  54: import java.security.KeyStoreException;
  55: import java.security.Permission;
  56: import java.security.PermissionCollection;
  57: import java.security.Permissions;
  58: import java.security.Policy;
  59: import java.security.Principal;
  60: import java.security.PrivilegedActionException;
  61: import java.security.PrivilegedExceptionAction;
  62: import java.security.Security;
  63: import java.security.UnresolvedPermission;
  64: import java.security.cert.Certificate;
  65: import java.security.cert.X509Certificate;
  66: import java.util.Enumeration;
  67: import java.util.HashMap;
  68: import java.util.Iterator;
  69: import java.util.LinkedList;
  70: import java.util.List;
  71: import java.util.Map;
  72: import java.util.StringTokenizer;
  73: import java.util.logging.Logger;
  74: 
  75: /**
  76:  * An implementation of a {@link java.security.Policy} object whose
  77:  * permissions are specified by a <em>policy file</em>.
  78:  *
  79:  * <p>The approximate syntax of policy files is:</p>
  80:  *
  81:  * <pre>
  82:  * policyFile ::= keystoreOrGrantEntries ;
  83:  *
  84:  * keystoreOrGrantEntries ::= keystoreOrGrantEntry |
  85:  *                            keystoreOrGrantEntries keystoreOrGrantEntry |
  86:  *                            EMPTY ;
  87:  *
  88:  * keystoreOrGrantEntry ::= keystoreEntry | grantEntry ;
  89:  *
  90:  * keystoreEntry ::= "keystore" keystoreUrl ';' |
  91:  *                   "keystore" keystoreUrl ',' keystoreAlgorithm ';' ;
  92:  *
  93:  * keystoreUrl ::= URL ;
  94:  * keystoreAlgorithm ::= STRING ;
  95:  *
  96:  * grantEntry ::= "grant" domainParameters '{' permissions '}' ';'
  97:  *
  98:  * domainParameters ::= domainParameter |
  99:  *                      domainParameter ',' domainParameters ;
 100:  *
 101:  * domainParameter ::= "signedBy" signerNames |
 102:  *                     "codeBase" codeBaseUrl |
 103:  *                     "principal" principalClassName principalName |
 104:  *                     "principal" principalName ;
 105:  *
 106:  * signerNames ::= quotedString ;
 107:  * codeBaseUrl ::= URL ;
 108:  * principalClassName ::= STRING ;
 109:  * principalName ::= quotedString ;
 110:  *
 111:  * quotedString ::= quoteChar STRING quoteChar ;
 112:  * quoteChar ::= '"' | '\'';
 113:  *
 114:  * permissions ::= permission | permissions permission ;
 115:  *
 116:  * permission ::= "permission" permissionClassName permissionTarget permissionAction |
 117:  *                "permission" permissionClassName permissionTarget |
 118:  *                "permission" permissionClassName;
 119:  * </pre>
 120:  *
 121:  * <p>Comments are either form of Java comments. Keystore entries only
 122:  * affect subsequent grant entries, so if a grant entry preceeds a
 123:  * keystore entry, that grant entry is not affected by that keystore
 124:  * entry. Certian instances of <code>${property-name}</code> will be
 125:  * replaced with <code>System.getProperty("property-name")</code> in
 126:  * quoted strings.</p>
 127:  *
 128:  * <p>This class will load the following files when created or
 129:  * refreshed, in order:</p>
 130:  *
 131:  * <ol>
 132:  * <li>The file <code>${java.home}/lib/security/java.policy</code>.</li>
 133:  * <li>All URLs specified by security properties
 134:  * <code>"policy.file.<i>n</i>"</code>, for increasing <i>n</i>
 135:  * starting from 1. The sequence stops at the first undefined
 136:  * property, so you must set <code>"policy.file.1"</code> if you also
 137:  * set <code>"policy.file.2"</code>, and so on.</li>
 138:  * <li>The URL specified by the property
 139:  * <code>"java.security.policy"</code>.</li>
 140:  * </ol>
 141:  *
 142:  * @author Casey Marshall (csm@gnu.org)
 143:  * @see java.security.Policy
 144:  */
 145: public final class PolicyFile extends Policy
 146: {
 147: 
 148:   // Constants and fields.
 149:   // -------------------------------------------------------------------------
 150: 
 151:   protected static final Logger logger = SystemLogger.SYSTEM;
 152:   // Added to cut redundant AccessController.doPrivileged calls
 153:   private static GetPropertyAction prop = new GetPropertyAction("file.seperator");
 154:   private static final String fs = (String) AccessController.doPrivileged(prop);
 155:   
 156:   private static final String DEFAULT_POLICY =
 157:     (String) AccessController.doPrivileged(prop.setParameters("java.home"))
 158:     + fs + "lib" + fs + "security" + fs + "java.policy";
 159:   private static final String DEFAULT_USER_POLICY =
 160:     (String) AccessController.doPrivileged(prop.setParameters("user.home")) +
 161:     fs + ".java.policy";
 162: 
 163:   private final Map cs2pc;
 164: 
 165:   // Constructors.
 166:   // -------------------------------------------------------------------------
 167: 
 168:   public PolicyFile()
 169:   {
 170:     cs2pc = new HashMap();
 171:     refresh();
 172:   }
 173: 
 174:   // Instance methods.
 175:   // -------------------------------------------------------------------------
 176: 
 177:   public PermissionCollection getPermissions(CodeSource codeSource)
 178:   {
 179:     Permissions perms = new Permissions();
 180:     for (Iterator it = cs2pc.entrySet().iterator(); it.hasNext(); )
 181:       {
 182:         Map.Entry e = (Map.Entry) it.next();
 183:         CodeSource cs = (CodeSource) e.getKey();
 184:         if (cs.implies(codeSource))
 185:           {
 186:             logger.log (Component.POLICY, "{0} -> {1}", new Object[]
 187:               { cs, codeSource });
 188:             PermissionCollection pc = (PermissionCollection) e.getValue();
 189:             for (Enumeration ee = pc.elements(); ee.hasMoreElements(); )
 190:               {
 191:                 perms.add((Permission) ee.nextElement());
 192:               }
 193:           }
 194:         else
 195:           logger.log (Component.POLICY, "{0} !-> {1}", new Object[]
 196:             { cs, codeSource });
 197:       }
 198:     logger.log (Component.POLICY, "returning permissions {0} for {1}",
 199:                 new Object[] { perms, codeSource });
 200:     return perms;
 201:   }
 202: 
 203:   public void refresh()
 204:   {
 205:     cs2pc.clear();
 206:     final List policyFiles = new LinkedList();
 207:     try
 208:       {
 209:         policyFiles.add (new File (DEFAULT_POLICY).toURL());
 210:         policyFiles.add (new File (DEFAULT_USER_POLICY).toURL ());
 211: 
 212:         AccessController.doPrivileged(
 213:           new PrivilegedExceptionAction()
 214:           {
 215:             public Object run() throws Exception
 216:             {
 217:               String allow = Security.getProperty ("policy.allowSystemProperty");
 218:               if (allow == null || Boolean.getBoolean (allow))
 219:                 {
 220:                   String s = System.getProperty ("java.security.policy");
 221:                   logger.log (Component.POLICY, "java.security.policy={0}", s);
 222:                   if (s != null)
 223:                     {
 224:                       boolean only = s.startsWith ("=");
 225:                       if (only)
 226:                         s = s.substring (1);
 227:                       policyFiles.clear ();
 228:                       policyFiles.add (new URL (s));
 229:                       if (only)
 230:                         return null;
 231:                     }
 232:                 }
 233:               for (int i = 1; ; i++)
 234:                 {
 235:                   String pname = "policy.url." + i;
 236:                   String s = Security.getProperty (pname);
 237:                   logger.log (Component.POLICY, "{0}={1}", new Object []
 238:                     { pname, s });
 239:                   if (s == null)
 240:                     break;
 241:                   policyFiles.add (new URL (s));
 242:                 }
 243:               return null;
 244:             }
 245:           });
 246:       }
 247:     catch (PrivilegedActionException pae)
 248:       {
 249:         logger.log (Component.POLICY, "reading policy properties", pae);
 250:       }
 251:     catch (MalformedURLException mue)
 252:       {
 253:         logger.log (Component.POLICY, "setting default policies", mue);
 254:       }
 255: 
 256:     logger.log (Component.POLICY, "building policy from URLs {0}",
 257:                 policyFiles);
 258:     for (Iterator it = policyFiles.iterator(); it.hasNext(); )
 259:       {
 260:         try
 261:           {
 262:             URL url = (URL) it.next();
 263:             parse(url);
 264:           }
 265:         catch (IOException ioe)
 266:           {
 267:             logger.log (Component.POLICY, "reading policy", ioe);
 268:           }
 269:       }
 270:   }
 271: 
 272:   public String toString()
 273:   {
 274:     return super.toString() + " [ " + cs2pc.toString() + " ]";
 275:   }
 276: 
 277:   // Own methods.
 278:   // -------------------------------------------------------------------------
 279: 
 280:   private static final int STATE_BEGIN = 0;
 281:   private static final int STATE_GRANT = 1;
 282:   private static final int STATE_PERMS = 2;
 283: 
 284:   /**
 285:    * Parse a policy file, incorporating the permission definitions
 286:    * described therein.
 287:    *
 288:    * @param url The URL of the policy file to read.
 289:    * @throws IOException if an I/O error occurs, or if the policy file
 290:    * cannot be parsed.
 291:    */
 292:   private void parse(final URL url) throws IOException
 293:   {
 294:     logger.log (Component.POLICY, "reading policy file from {0}", url);
 295:     final StreamTokenizer in = new StreamTokenizer(new InputStreamReader(url.openStream()));
 296:     in.resetSyntax();
 297:     in.slashSlashComments(true);
 298:     in.slashStarComments(true);
 299:     in.wordChars('A', 'Z');
 300:     in.wordChars('a', 'z');
 301:     in.wordChars('0', '9');
 302:     in.wordChars('.', '.');
 303:     in.wordChars('_', '_');
 304:     in.wordChars('$', '$');
 305:     in.whitespaceChars(' ', ' ');
 306:     in.whitespaceChars('\t', '\t');
 307:     in.whitespaceChars('\f', '\f');
 308:     in.whitespaceChars('\n', '\n');
 309:     in.whitespaceChars('\r', '\r');
 310:     in.quoteChar('\'');
 311:     in.quoteChar('"');
 312: 
 313:     int tok;
 314:     int state = STATE_BEGIN;
 315:     List keystores = new LinkedList();
 316:     URL currentBase = null;
 317:     List currentCerts = new LinkedList();
 318:     Permissions currentPerms = new Permissions();
 319:     while ((tok = in.nextToken()) != StreamTokenizer.TT_EOF)
 320:       {
 321:         switch (tok)
 322:           {
 323:           case '{':
 324:             if (state != STATE_GRANT)
 325:               error(url, in, "spurious '{'");
 326:             state = STATE_PERMS;
 327:             tok = in.nextToken();
 328:             break;
 329:           case '}':
 330:             if (state != STATE_PERMS)
 331:               error(url, in, "spurious '}'");
 332:             state = STATE_BEGIN;
 333:             currentPerms.setReadOnly();
 334:             Certificate[] c = null;
 335:             if (!currentCerts.isEmpty())
 336:               c = (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()]);
 337:             cs2pc.put(new CodeSource(currentBase, c), currentPerms);
 338:             currentCerts.clear();
 339:             currentPerms = new Permissions();
 340:             currentBase = null;
 341:             tok = in.nextToken();
 342:             if (tok != ';')
 343:               in.pushBack();
 344:             continue;
 345:           }
 346:         if (tok != StreamTokenizer.TT_WORD)
 347:           {
 348:             error(url, in, "expecting word token");
 349:           }
 350: 
 351:         // keystore "<keystore-path>" [',' "<keystore-type>"] ';'
 352:         if (in.sval.equalsIgnoreCase("keystore"))
 353:           {
 354:             String alg = KeyStore.getDefaultType();
 355:             tok = in.nextToken();
 356:             if (tok != '"' && tok != '\'')
 357:               error(url, in, "expecting key store URL");
 358:             String store = in.sval;
 359:             tok = in.nextToken();
 360:             if (tok == ',')
 361:               {
 362:                 tok = in.nextToken();
 363:                 if (tok != '"' && tok != '\'')
 364:                   error(url, in, "expecting key store type");
 365:                 alg = in.sval;
 366:                 tok = in.nextToken();
 367:               }
 368:             if (tok != ';')
 369:               error(url, in, "expecting semicolon");
 370:             try
 371:               {
 372:                 KeyStore keystore = KeyStore.getInstance(alg);
 373:                 keystore.load(new URL(url, store).openStream(), null);
 374:                 keystores.add(keystore);
 375:               }
 376:             catch (Exception x)
 377:               {
 378:                 error(url, in, x.toString());
 379:               }
 380:           }
 381:         else if (in.sval.equalsIgnoreCase("grant"))
 382:           {
 383:             if (state != STATE_BEGIN)
 384:               error(url, in, "extraneous grant keyword");
 385:             state = STATE_GRANT;
 386:           }
 387:         else if (in.sval.equalsIgnoreCase("signedBy"))
 388:           {
 389:             if (state != STATE_GRANT && state != STATE_PERMS)
 390:               error(url, in, "spurious 'signedBy'");
 391:             if (keystores.isEmpty())
 392:               error(url, in, "'signedBy' with no keystores");
 393:             tok = in.nextToken();
 394:             if (tok != '"' && tok != '\'')
 395:               error(url, in, "expecting signedBy name");
 396:             StringTokenizer st = new StringTokenizer(in.sval, ",");
 397:             while (st.hasMoreTokens())
 398:               {
 399:                 String alias = st.nextToken();
 400:                 for (Iterator it = keystores.iterator(); it.hasNext(); )
 401:                   {
 402:                     KeyStore keystore = (KeyStore) it.next();
 403:                     try
 404:                       {
 405:                         if (keystore.isCertificateEntry(alias))
 406:                           currentCerts.add(keystore.getCertificate(alias));
 407:                       }
 408:                     catch (KeyStoreException kse)
 409:                       {
 410:                         error(url, in, kse.toString());
 411:                       }
 412:                   }
 413:               }
 414:             tok = in.nextToken();
 415:             if (tok != ',')
 416:               {
 417:                 if (state != STATE_GRANT)
 418:                   error(url, in, "spurious ','");
 419:                 in.pushBack();
 420:               }
 421:           }
 422:         else if (in.sval.equalsIgnoreCase("codeBase"))
 423:           {
 424:             if (state != STATE_GRANT)
 425:               error(url, in, "spurious 'codeBase'");
 426:             tok = in.nextToken();
 427:             if (tok != '"' && tok != '\'')
 428:               error(url, in, "expecting code base URL");
 429:             String base = expand(in.sval);
 430:             if (File.separatorChar != '/')
 431:               base = base.replace(File.separatorChar, '/');
 432:             try
 433:               {
 434:                 currentBase = new URL(base);
 435:               }
 436:             catch (MalformedURLException mue)
 437:               {
 438:                 error(url, in, mue.toString());
 439:               }
 440:             tok = in.nextToken();
 441:             if (tok != ',')
 442:               in.pushBack();
 443:           }
 444:         else if (in.sval.equalsIgnoreCase("principal"))
 445:           {
 446:             if (state != STATE_GRANT)
 447:               error(url, in, "spurious 'principal'");
 448:             tok = in.nextToken();
 449:             if (tok == StreamTokenizer.TT_WORD)
 450:               {
 451:                 tok = in.nextToken();
 452:                 if (tok != '"' && tok != '\'')
 453:                   error(url, in, "expecting principal name");
 454:                 String name = in.sval;
 455:                 Principal p = null;
 456:                 try
 457:                   {
 458:                     Class pclass = Class.forName(in.sval);
 459:                     Constructor c =
 460:                       pclass.getConstructor(new Class[] { String.class });
 461:                     p = (Principal) c.newInstance(new Object[] { name });
 462:                   }
 463:                 catch (Exception x)
 464:                   {
 465:                     error(url, in, x.toString());
 466:                   }
 467:                 for (Iterator it = keystores.iterator(); it.hasNext(); )
 468:                   {
 469:                     KeyStore ks = (KeyStore) it.next();
 470:                     try
 471:                       {
 472:                         for (Enumeration e = ks.aliases(); e.hasMoreElements(); )
 473:                           {
 474:                             String alias = (String) e.nextElement();
 475:                             if (ks.isCertificateEntry(alias))
 476:                               {
 477:                                 Certificate cert = ks.getCertificate(alias);
 478:                                 if (!(cert instanceof X509Certificate))
 479:                                   continue;
 480:                                 if (p.equals(((X509Certificate) cert).getSubjectDN()) ||
 481:                                     p.equals(((X509Certificate) cert).getSubjectX500Principal()))
 482:                                   currentCerts.add(cert);
 483:                               }
 484:                           }
 485:                       }
 486:                     catch (KeyStoreException kse)
 487:                       {
 488:                         error(url, in, kse.toString());
 489:                       }
 490:                   }
 491:               }
 492:             else if (tok == '"' || tok == '\'')
 493:               {
 494:                 String alias = in.sval;
 495:                 for (Iterator it = keystores.iterator(); it.hasNext(); )
 496:                   {
 497:                     KeyStore ks = (KeyStore) it.next();
 498:                     try
 499:                       {
 500:                         if (ks.isCertificateEntry(alias))
 501:                           currentCerts.add(ks.getCertificate(alias));
 502:                       }
 503:                     catch (KeyStoreException kse)
 504:                       {
 505:                         error(url, in, kse.toString());
 506:                       }
 507:                   }
 508:               }
 509:             else
 510:               error(url, in, "expecting principal");
 511:             tok = in.nextToken();
 512:             if (tok != ',')
 513:               in.pushBack();
 514:           }
 515:         else if (in.sval.equalsIgnoreCase("permission"))
 516:           {
 517:             if (state != STATE_PERMS)
 518:               error(url, in, "spurious 'permission'");
 519:             tok = in.nextToken();
 520:             if (tok != StreamTokenizer.TT_WORD)
 521:               error(url, in, "expecting permission class name");
 522:             String className = in.sval;
 523:             Class clazz = null;
 524:             try
 525:               {
 526:                 clazz = Class.forName(className);
 527:               }
 528:             catch (ClassNotFoundException cnfe)
 529:               {
 530:               }
 531:             tok = in.nextToken();
 532:             if (tok == ';')
 533:               {
 534:                 if (clazz == null)
 535:                   {
 536:                     currentPerms.add(new UnresolvedPermission(className,
 537:               null, null, (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()])));
 538:                     continue;
 539:                   }
 540:                 try
 541:                   {
 542:                     currentPerms.add((Permission) clazz.newInstance());
 543:                   }
 544:                 catch (Exception x)
 545:                   {
 546:                     error(url, in, x.toString());
 547:                   }
 548:                 continue;
 549:               }
 550:             if (tok != '"' && tok != '\'')
 551:               error(url, in, "expecting permission target");
 552:             String target = expand(in.sval);
 553:             tok = in.nextToken();
 554:             if (tok == ';')
 555:               {
 556:                 if (clazz == null)
 557:                   {
 558:                     currentPerms.add(new UnresolvedPermission(className,
 559:               target, null, (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()])));
 560:                     continue;
 561:                   }
 562:                 try
 563:                   {
 564:                     Constructor c =
 565:                       clazz.getConstructor(new Class[] { String.class });
 566:                     currentPerms.add((Permission) c.newInstance(
 567:                       new Object[] { target }));
 568:                   }
 569:                 catch (Exception x)
 570:                   {
 571:                     error(url, in, x.toString());
 572:                   }
 573:                 continue;
 574:               }
 575:             if (tok != ',')
 576:               error(url, in, "expecting ','");
 577:             tok = in.nextToken();
 578:             if (tok == StreamTokenizer.TT_WORD)
 579:               {
 580:                 if (!in.sval.equalsIgnoreCase("signedBy"))
 581:                   error(url, in, "expecting 'signedBy'");
 582:                 try
 583:                   {
 584:                     Constructor c =
 585:                       clazz.getConstructor(new Class[] { String.class });
 586:                     currentPerms.add((Permission) c.newInstance(
 587:                       new Object[] { target }));
 588:                   }
 589:                 catch (Exception x)
 590:                   {
 591:                     error(url, in, x.toString());
 592:                   }
 593:                 in.pushBack();
 594:                 continue;
 595:               }
 596:             if (tok != '"' && tok != '\'')
 597:               error(url, in, "expecting permission action");
 598:             String action = in.sval;
 599:             if (clazz == null)
 600:               {
 601:                 currentPerms.add(new UnresolvedPermission(className,
 602:           target, action, (Certificate[]) currentCerts.toArray(new Certificate[currentCerts.size()])));
 603:                 continue;
 604:               }
 605:             else
 606:               {
 607:                 try
 608:                   {
 609:                     Constructor c = clazz.getConstructor(
 610:                       new Class[] { String.class, String.class });
 611:                     currentPerms.add((Permission) c.newInstance(
 612:                       new Object[] { target, action }));
 613:                   }
 614:                 catch (Exception x)
 615:                   {
 616:                     error(url, in, x.toString());
 617:                   }
 618:               }
 619:             tok = in.nextToken();
 620:             if (tok != ';' && tok != ',')
 621:               error(url, in, "expecting ';' or ','");
 622:           }
 623:       }
 624:   }
 625: 
 626:   /**
 627:    * Expand all instances of <code>"${property-name}"</code> into
 628:    * <code>System.getProperty("property-name")</code>.
 629:    */
 630:   private static String expand(final String s)
 631:   {
 632:     final StringBuffer result = new StringBuffer();
 633:     final StringBuffer prop = new StringBuffer();
 634:     int state = 0;
 635:     for (int i = 0; i < s.length(); i++)
 636:       {
 637:         switch (state)
 638:           {
 639:           case 0:
 640:             if (s.charAt(i) == '$')
 641:               state = 1;
 642:             else
 643:               result.append(s.charAt(i));
 644:             break;
 645:           case 1:
 646:             if (s.charAt(i) == '{')
 647:               state = 2;
 648:             else
 649:               {
 650:                 state = 0;
 651:                 result.append('$').append(s.charAt(i));
 652:               }
 653:             break;
 654:           case 2:
 655:             if (s.charAt(i) == '}')
 656:               {
 657:                 String p = prop.toString();
 658:                 if (p.equals("/"))
 659:                   p = "file.separator";
 660:                 p = System.getProperty(p);
 661:                 if (p == null)
 662:                   p = "";
 663:                 result.append(p);
 664:                 prop.setLength(0);
 665:                 state = 0;
 666:               }
 667:             else
 668:               prop.append(s.charAt(i));
 669:             break;
 670:           }
 671:       }
 672:     if (state != 0)
 673:       result.append('$').append('{').append(prop);
 674:     return result.toString();
 675:   }
 676: 
 677:   /**
 678:    * I miss macros.
 679:    */
 680:   private static void error(URL base, StreamTokenizer in, String msg)
 681:     throws IOException
 682:   {
 683:     throw new IOException(base+":"+in.lineno()+": "+msg);
 684:   }
 685: }