/* * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at * http://www.eclipse.org/legal/epl-2.0. * * This Source Code may also be made available under the following Secondary * Licenses when the conditions for such availability set forth in the * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, * version 2 with the GNU Classpath Exception, which is available at * https://www.gnu.org/software/classpath/license.html. * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ package com.sun.mail.mbox; import javax.mail.*; import javax.mail.event.*; import javax.mail.internet.*; import javax.mail.util.*; import java.io.*; import java.util.*; import com.sun.mail.util.LineInputStream; /** * This class represents a mailbox file containing RFC822 style email messages. * * @author John Mani * @author Bill Shannon */ public class MboxFolder extends Folder { private String name; // null => the default folder private boolean is_inbox = false; private int total; // total number of messages in mailbox private volatile boolean opened = false; private List messages; private TempFile temp; private MboxStore mstore; private MailFile folder; private long file_size; // the size the last time we read or wrote it private long saved_file_size; // size at the last open, close, or expunge private boolean special_imap_message; private static final boolean homeRelative = Boolean.getBoolean("mail.mbox.homerelative"); /** * Metadata for each message, to avoid instantiating MboxMessage * objects for messages we're not going to look at.

* * MboxFolder keeps an array of these objects corresponding to * each message in the folder. Access to the array elements is * synchronized, but access to contents of this object is not. * The metadata stored here is only accessed if the message field * is null; otherwise the MboxMessage object contains the metadata. */ static final class MessageMetadata { public long start; // offset in temp file of start of this message // public long end; // offset in temp file of end of this message public long dataend; // offset of end of message data, <= "end" public MboxMessage message; // the message itself public boolean recent; // message is recent? public boolean deleted; // message is marked deleted? public boolean imap; // special imap message? } public MboxFolder(MboxStore store, String name) { super(store); this.mstore = store; this.name = name; if (name != null && name.equalsIgnoreCase("INBOX")) is_inbox = true; folder = mstore.getMailFile(name == null ? "~" : name); if (folder.exists()) saved_file_size = folder.length(); else saved_file_size = -1; } public char getSeparator() { return File.separatorChar; } public Folder[] list(String pattern) throws MessagingException { if (!folder.isDirectory()) throw new MessagingException("not a directory"); if (name == null) return list(null, pattern, true); else return list(name + File.separator, pattern, false); } /* * Version of list shared by MboxStore and MboxFolder. */ protected Folder[] list(String ref, String pattern, boolean fromStore) throws MessagingException { if (ref != null && ref.length() == 0) ref = null; int i; String refdir = null; String realdir = null; pattern = canonicalize(ref, pattern); if ((i = indexOfAny(pattern, "%*")) >= 0) { refdir = pattern.substring(0, i); } else { refdir = pattern; } if ((i = refdir.lastIndexOf(File.separatorChar)) >= 0) { // get rid of anything after directory name refdir = refdir.substring(0, i + 1); realdir = mstore.mb.filename(mstore.user, refdir); } else if (refdir.length() == 0 || refdir.charAt(0) != '~') { // no separator and doesn't start with "~" => home or cwd refdir = null; if (homeRelative) realdir = mstore.home; else realdir = "."; } else { realdir = mstore.mb.filename(mstore.user, refdir); } List flist = new ArrayList(); listWork(realdir, refdir, pattern, fromStore ? 0 : 1, flist); if (Match.path("INBOX", pattern, '\0')) flist.add("INBOX"); Folder fl[] = new Folder[flist.size()]; for (i = 0; i < fl.length; i++) { fl[i] = createFolder(mstore, flist.get(i)); } return fl; } public String getName() { if (name == null) return ""; else if (is_inbox) return "INBOX"; else return folder.getName(); } public String getFullName() { if (name == null) return ""; else return name; } public Folder getParent() { if (name == null) return null; else if (is_inbox) return createFolder(mstore, null); else // XXX - have to recognize other folders under default folder return createFolder(mstore, folder.getParent()); } public boolean exists() { return folder.exists(); } public int getType() { if (folder.isDirectory()) return HOLDS_FOLDERS; else return HOLDS_MESSAGES; } public Flags getPermanentFlags() { return MboxStore.permFlags; } public synchronized boolean hasNewMessages() { if (folder instanceof UNIXFile) { UNIXFile f = (UNIXFile)folder; if (f.length() > 0) { long atime = f.lastAccessed(); long mtime = f.lastModified(); //System.out.println(name + " atime " + atime + " mtime " + mtime); return atime < mtime; } return false; } long current_size; if (folder.exists()) current_size = folder.length(); else current_size = -1; // if we've never opened the folder, remember the size now // (will cause us to return false the first time) if (saved_file_size < 0) saved_file_size = current_size; return current_size > saved_file_size; } public synchronized Folder getFolder(String name) throws MessagingException { if (folder.exists() && !folder.isDirectory()) throw new MessagingException("not a directory"); return createFolder(mstore, (this.name == null ? "~" : this.name) + File.separator + name); } public synchronized boolean create(int type) throws MessagingException { switch (type) { case HOLDS_FOLDERS: if (!folder.mkdirs()) { return false; } break; case HOLDS_MESSAGES: if (folder.exists()) { return false; } try { (new FileOutputStream((File)folder)).close(); } catch (FileNotFoundException fe) { File parent = new File(folder.getParent()); if (!parent.mkdirs()) throw new MessagingException("can't create folder: " + name); try { (new FileOutputStream((File)folder)).close(); } catch (IOException ex3) { throw new MessagingException("can't create folder: " + name, ex3); } } catch (IOException e) { throw new MessagingException("can't create folder: " + name, e); } break; default: throw new MessagingException("type not supported"); } notifyFolderListeners(FolderEvent.CREATED); return true; } public synchronized boolean delete(boolean recurse) throws MessagingException { checkClosed(); if (name == null) throw new MessagingException("can't delete default folder"); boolean ret = true; if (recurse && folder.isDirectory()) ret = delete(new File(folder.getPath())); if (ret && folder.delete()) { notifyFolderListeners(FolderEvent.DELETED); return true; } return false; } /** * Recursively delete the specified file/directory. */ private boolean delete(File f) { File[] files = f.listFiles(); boolean ret = true; for (int i = 0; ret && i < files.length; i++) { if (files[i].isDirectory()) ret = delete(files[i]); else ret = files[i].delete(); } return ret; } public synchronized boolean renameTo(Folder f) throws MessagingException { checkClosed(); if (name == null) throw new MessagingException("can't rename default folder"); if (!(f instanceof MboxFolder)) throw new MessagingException("can't rename to: " + f.getName()); String newname = ((MboxFolder)f).folder.getPath(); if (folder.renameTo(new File(newname))) { notifyFolderRenamedListeners(f); return true; } return false; } /* Ensure the folder is open */ void checkOpen() throws IllegalStateException { if (!opened) throw new IllegalStateException("Folder is not Open"); } /* Ensure the folder is not open */ private void checkClosed() throws IllegalStateException { if (opened) throw new IllegalStateException("Folder is Open"); } /* * Check that the given message number is within the range * of messages present in this folder. If the message * number is out of range, we check to see if new messages * have arrived. */ private void checkRange(int msgno) throws MessagingException { if (msgno < 1) // message-numbers start at 1 throw new IndexOutOfBoundsException("message number < 1"); if (msgno <= total) return; // Out of range, let's check if there are any new messages. getMessageCount(); if (msgno > total) // Still out of range ? Throw up ... throw new IndexOutOfBoundsException(msgno + " > " + total); } /* Ensure the folder is open & readable */ private void checkReadable() throws IllegalStateException { if (!opened || (mode != READ_ONLY && mode != READ_WRITE)) throw new IllegalStateException("Folder is not Readable"); } /* Ensure the folder is open & writable */ private void checkWritable() throws IllegalStateException { if (!opened || mode != READ_WRITE) throw new IllegalStateException("Folder is not Writable"); } public boolean isOpen() { return opened; } /* * Open the folder in the specified mode. */ public synchronized void open(int mode) throws MessagingException { if (opened) throw new IllegalStateException("Folder is already Open"); if (!folder.exists()) throw new FolderNotFoundException(this, "Folder doesn't exist: " + folder.getPath()); this.mode = mode; switch (mode) { case READ_WRITE: default: if (!folder.canWrite()) throw new MessagingException("Open Failure, can't write: " + folder.getPath()); // fall through... case READ_ONLY: if (!folder.canRead()) throw new MessagingException("Open Failure, can't read: " + folder.getPath()); break; } if (is_inbox && folder instanceof InboxFile) { InboxFile inf = (InboxFile)folder; if (!inf.openLock(mode == READ_WRITE ? "rw" : "r")) throw new MessagingException("Failed to lock INBOX"); } if (!folder.lock("r")) throw new MessagingException("Failed to lock folder: " + name); messages = new ArrayList(); total = 0; Message[] msglist = null; try { temp = new TempFile(null); saved_file_size = folder.length(); msglist = load(0L, false); } catch (IOException e) { throw new MessagingException("IOException", e); } finally { folder.unlock(); } notifyConnectionListeners(ConnectionEvent.OPENED); if (msglist != null) notifyMessageAddedListeners(msglist); opened = true; // now really opened } public synchronized void close(boolean expunge) throws MessagingException { checkOpen(); try { if (mode == READ_WRITE) { try { writeFolder(true, expunge); } catch (IOException e) { throw new MessagingException("I/O Exception", e); } } messages = null; } finally { opened = false; if (is_inbox && folder instanceof InboxFile) { InboxFile inf = (InboxFile)folder; inf.closeLock(); } temp.close(); temp = null; notifyConnectionListeners(ConnectionEvent.CLOSED); } } /** * Re-write the folder with the current contents of the messages. * If closing is true, turn off the RECENT flag. If expunge is * true, don't write out deleted messages (only used from close() * when the message cache won't be accessed again). * * Return the number of messages written. */ protected int writeFolder(boolean closing, boolean expunge) throws IOException, MessagingException { /* * First, see if there have been any changes. */ int modified = 0, deleted = 0, recent = 0; for (int msgno = 1; msgno <= total; msgno++) { MessageMetadata md = messages.get(messageIndexOf(msgno)); MboxMessage msg = md.message; if (msg != null) { Flags flags = msg.getFlags(); if (msg.isModified() || !msg.origFlags.equals(flags)) modified++; if (flags.contains(Flags.Flag.DELETED)) deleted++; if (flags.contains(Flags.Flag.RECENT)) recent++; } else { if (md.deleted) deleted++; if (md.recent) recent++; } } if ((!closing || recent == 0) && (!expunge || deleted == 0) && modified == 0) return 0; /* * Have to save any new mail that's been appended to the * folder since we last loaded it. */ if (!folder.lock("rw")) throw new MessagingException("Failed to lock folder: " + name); int oldtotal = total; // XXX Message[] msglist = null; if (folder.length() != file_size) msglist = load(file_size, !closing); // don't use the folder's FD, need to re-open in order to trunc the file OutputStream os = new BufferedOutputStream(new FileOutputStream((File)folder)); int wr = 0; boolean keep = true; try { if (special_imap_message) appendStream(getMessageStream(0), os); for (int msgno = 1; msgno <= total; msgno++) { MessageMetadata md = messages.get(messageIndexOf(msgno)); MboxMessage msg = md.message; if (msg != null) { if (expunge && msg.isSet(Flags.Flag.DELETED)) continue; // skip it; if (closing && msgno <= oldtotal && msg.isSet(Flags.Flag.RECENT)) msg.setFlag(Flags.Flag.RECENT, false); writeMboxMessage(msg, os); } else { if (expunge && md.deleted) continue; // skip it; if (closing && msgno <= oldtotal && md.recent) { // have to instantiate message so that we can // clear the recent flag msg = (MboxMessage)getMessage(msgno); msg.setFlag(Flags.Flag.RECENT, false); writeMboxMessage(msg, os); } else { appendStream(getMessageStream(msgno), os); } } folder.touchlock(); wr++; } // If no messages in the mailbox, and we're closing, // maybe we should remove the mailbox. if (wr == 0 && closing) { String skeep = ((MboxStore)store).getSession(). getProperty("mail.mbox.deleteEmpty"); if (skeep != null && skeep.equalsIgnoreCase("true")) keep = false; } } catch (IOException e) { throw e; } catch (MessagingException e) { throw e; } catch (Exception e) { e.printStackTrace(); throw new MessagingException("unexpected exception " + e); } finally { // close the folder, flushing out the data try { os.close(); file_size = saved_file_size = folder.length(); if (!keep) { folder.delete(); file_size = 0; } } catch (IOException ex) {} if (keep) { // make sure the access time is greater than the mod time // XXX - would be nice to have utime() try { Thread.sleep(1000); // sleep for a second } catch (InterruptedException ex) {} InputStream is = null; try { is = new FileInputStream((File)folder); is.read(); // read a byte } catch (IOException ex) {} // ignore errors try { if (is != null) is.close(); is = null; } catch (IOException ex) {} // ignore errors } folder.unlock(); if (msglist != null) notifyMessageAddedListeners(msglist); } return wr; } /** * Append the input stream to the output stream, closing the * input stream when done. */ private static final void appendStream(InputStream is, OutputStream os) throws IOException { try { byte[] buf = new byte[64 * 1024]; int len; while ((len = is.read(buf)) > 0) os.write(buf, 0, len); } finally { is.close(); } } /** * Write a MimeMessage to the specified OutputStream in a * format suitable for a UNIX mailbox, i.e., including a correct * Content-Length header and with the local platform's line * terminating convention.

* * If the message is really a MboxMessage, use its writeToFile * method, which has access to the UNIX From line. Otherwise, do * all the work here, creating an appropriate UNIX From line. */ public static void writeMboxMessage(MimeMessage msg, OutputStream os) throws IOException, MessagingException { try { if (msg instanceof MboxMessage) { ((MboxMessage)msg).writeToFile(os); } else { // XXX - modify the message to preserve the flags in headers MboxMessage.setHeadersFromFlags(msg); ContentLengthCounter cos = new ContentLengthCounter(); NewlineOutputStream nos = new NewlineOutputStream(cos); msg.writeTo(nos); nos.flush(); os = new NewlineOutputStream(os, true); os = new ContentLengthUpdater(os, cos.getSize()); PrintStream pos = new PrintStream(os, false, "iso-8859-1"); pos.println(getUnixFrom(msg)); msg.writeTo(pos); pos.flush(); } } catch (MessagingException me) { throw me; } catch (IOException ioe) { throw ioe; } } /** * Construct an appropriately formatted UNIX From line using * the sender address and the date in the message. */ protected static String getUnixFrom(MimeMessage msg) { Address[] afrom; String from; Date ddate; String date; try { if ((afrom = msg.getFrom()) == null || !(afrom[0] instanceof InternetAddress) || (from = ((InternetAddress)afrom[0]).getAddress()) == null) from = "UNKNOWN"; if ((ddate = msg.getReceivedDate()) == null || (ddate = msg.getSentDate()) == null) ddate = new Date(); } catch (MessagingException e) { from = "UNKNOWN"; ddate = new Date(); } date = ddate.toString(); // date is of the form "Sat Aug 12 02:30:00 PDT 1995" // need to strip out the timezone return "From " + from + " " + date.substring(0, 20) + date.substring(24); } public synchronized int getMessageCount() throws MessagingException { if (!opened) return -1; boolean locked = false; Message[] msglist = null; try { if (folder.length() != file_size) { if (!folder.lock("r")) throw new MessagingException("Failed to lock folder: " + name); locked = true; msglist = load(file_size, true); } } catch (IOException e) { throw new MessagingException("I/O Exception", e); } finally { if (locked) { folder.unlock(); if (msglist != null) notifyMessageAddedListeners(msglist); } } return total; } /** * Get the specified message. Note that messages are numbered * from 1. */ public synchronized Message getMessage(int msgno) throws MessagingException { checkReadable(); checkRange(msgno); MessageMetadata md = messages.get(messageIndexOf(msgno)); MboxMessage m = md.message; if (m == null) { InputStream is = getMessageStream(msgno); try { m = loadMessage(is, msgno, mode == READ_WRITE); } catch (IOException ex) { MessagingException mex = new MessageRemovedException("mbox message gone", ex); throw mex; } md.message = m; } return m; } private final int messageIndexOf(int msgno) { return special_imap_message ? msgno : msgno - 1; } private InputStream getMessageStream(int msgno) { int index = messageIndexOf(msgno); MessageMetadata md = messages.get(index); return temp.newStream(md.start, md.dataend); } public synchronized void appendMessages(Message[] msgs) throws MessagingException { if (!folder.lock("rw")) throw new MessagingException("Failed to lock folder: " + name); OutputStream os = null; boolean err = false; try { os = new BufferedOutputStream( new FileOutputStream(((File)folder).getPath(), true)); // XXX - should use getAbsolutePath()? for (int i = 0; i < msgs.length; i++) { if (msgs[i] instanceof MimeMessage) { writeMboxMessage((MimeMessage)msgs[i], os); } else { err = true; continue; } folder.touchlock(); } } catch (IOException e) { throw new MessagingException("I/O Exception", e); } catch (MessagingException e) { throw e; } catch (Exception e) { e.printStackTrace(); throw new MessagingException("unexpected exception " + e); } finally { if (os != null) try { os.close(); } catch (IOException e) { // ignored } folder.unlock(); } if (opened) getMessageCount(); // loads new messages as a side effect if (err) throw new MessagingException("Can't append non-Mime message"); } public synchronized Message[] expunge() throws MessagingException { checkWritable(); /* * First, write out the folder to make sure we have permission, * disk space, etc. */ int wr = total; // number of messages written out try { wr = writeFolder(false, true); } catch (IOException e) { throw new MessagingException("expunge failed", e); } if (wr == total) // wrote them all => nothing expunged return new Message[0]; /* * Now, actually get rid of the expunged messages. */ int del = 0; Message[] msglist = new Message[total - wr]; int msgno = 1; while (msgno <= total) { MessageMetadata md = messages.get(messageIndexOf(msgno)); MboxMessage msg = md.message; if (msg != null) { if (msg.isSet(Flags.Flag.DELETED)) { msg.setExpunged(true); msglist[del] = msg; del++; messages.remove(messageIndexOf(msgno)); total--; } else { msg.setMessageNumber(msgno); // update message number msgno++; } } else { if (md.deleted) { // have to instantiate it for the notification msg = (MboxMessage)getMessage(msgno); msg.setExpunged(true); msglist[del] = msg; del++; messages.remove(messageIndexOf(msgno)); total--; } else { msgno++; } } } if (del != msglist.length) // this is really an assert throw new MessagingException("expunge delete count wrong"); notifyMessageRemovedListeners(true, msglist); return msglist; } /* * Load more messages from the folder starting at the specified offset. */ private Message[] load(long offset, boolean notify) throws MessagingException, IOException { int oldtotal = total; MessageLoader loader = new MessageLoader(temp); int loaded = loader.load(folder.getFD(), offset, messages); total += loaded; file_size = folder.length(); if (offset == 0 && loaded > 0) { /* * If the first message is the special message that the * IMAP server adds to the mailbox, remember that we've * seen it so it won't be shown to the user. */ MessageMetadata md = messages.get(0); if (md.imap) { special_imap_message = true; total--; } } if (notify) { Message[] msglist = new Message[total - oldtotal]; for (int i = oldtotal, j = 0; i < total; i++, j++) msglist[j] = getMessage(i + 1); return msglist; } else return null; } /** * Parse the input stream and return an appropriate message object. * The InputStream must be a SharedInputStream. */ private MboxMessage loadMessage(InputStream is, int msgno, boolean writable) throws MessagingException, IOException { LineInputStream in = new LineInputStream(is); /* * Read lines until a UNIX From line, * skipping blank lines. */ String line; String unix_from = null; while ((line = in.readLine()) != null) { if (line.trim().length() == 0) continue; if (line.startsWith("From ")) { /* * A UNIX From line looks like: * From address Day Mon DD HH:MM:SS YYYY */ unix_from = line; int i; // find the space after the address, before the date i = unix_from.indexOf(' ', 5); if (i < 0) continue; // not a valid UNIX From line break; } throw new MessagingException("Garbage in mailbox: " + line); } if (unix_from == null) throw new EOFException("end of mailbox"); /* * Now load the RFC822 headers into an InternetHeaders object. */ InternetHeaders hdrs = new InternetHeaders(is); // the rest is the message content SharedInputStream sis = (SharedInputStream)is; InputStream stream = sis.newStream(sis.getPosition(), -1); return new MboxMessage(this, hdrs, stream, msgno, unix_from, writable); } /* * Only here to make accessible to MboxMessage. */ protected void notifyMessageChangedListeners(int type, Message m) { super.notifyMessageChangedListeners(type, m); } /** * this is an exact duplicate of the Folder.getURL except it doesn't * add a beginning '/' to the URLName. */ public URLName getURLName() { // XXX - note: this should not be done this way with the // new javax.mail apis. URLName storeURL = getStore().getURLName(); if (name == null) return storeURL; char separator = getSeparator(); String fullname = getFullName(); StringBuilder encodedName = new StringBuilder(); // We need to encode each of the folder's names, and replace // the store's separator char with the URL char '/'. StringTokenizer tok = new StringTokenizer( fullname, Character.toString(separator), true); while (tok.hasMoreTokens()) { String s = tok.nextToken(); if (s.charAt(0) == separator) encodedName.append("/"); else // XXX - should encode, but since there's no decoder... //encodedName.append(java.net.URLEncoder.encode(s)); encodedName.append(s); } return new URLName(storeURL.getProtocol(), storeURL.getHost(), storeURL.getPort(), encodedName.toString(), storeURL.getUsername(), null /* no password */); } /** * Create an MboxFolder object, or a subclass thereof. * Can be overridden by subclasses of MboxFolder so that * the appropriate subclass is created by the list method. */ protected Folder createFolder(MboxStore store, String name) { return new MboxFolder(store, name); } /* * Support routines for list(). */ /** * Return a canonicalized pattern given a reference name and a pattern. */ private static String canonicalize(String ref, String pat) { if (ref == null) return pat; try { if (pat.length() == 0) { return ref; } else if (pat.charAt(0) == File.separatorChar) { return ref.substring(0, ref.indexOf(File.separatorChar)) + pat; } else { return ref + pat; } } catch (StringIndexOutOfBoundsException e) { return pat; } } /** * Return the first index of any of the characters in "any" in "s", * or -1 if none are found. * * This should be a method on String. */ private static int indexOfAny(String s, String any) { try { int len = s.length(); for (int i = 0; i < len; i++) { if (any.indexOf(s.charAt(i)) >= 0) return i; } return -1; } catch (StringIndexOutOfBoundsException e) { return -1; } } /** * The recursive part of generating the list of mailboxes. * realdir is the full pathname to the directory to search. * dir is the name the user uses, often a relative name that's * relative to the user's home directory. dir (if not null) always * has a trailing file separator character. * * @param realdir real pathname of directory to start looking in * @param dir user's name for realdir * @param pat pattern to match against * @param level level of the directory hierarchy we're in * @param flist list to which to add folder names that match */ // Derived from the c-client listWork() function. private void listWork(String realdir, String dir, String pat, int level, List flist) { String sl[]; File fdir = new File(realdir); try { sl = fdir.list(); } catch (SecurityException e) { return; // can't read it, ignore it } if (level == 0 && dir != null && Match.path(dir, pat, File.separatorChar)) flist.add(dir); if (sl == null) return; // nothing return, we're done if (realdir.charAt(realdir.length() - 1) != File.separatorChar) realdir += File.separator; for (int i = 0; i < sl.length; i++) { if (sl[i].charAt(0) == '.') continue; // ignore all "dot" files for now String md = realdir + sl[i]; File mf = new File(md); if (!mf.exists()) continue; String name; if (dir != null) name = dir + sl[i]; else name = sl[i]; if (mf.isDirectory()) { if (Match.path(name, pat, File.separatorChar)) { flist.add(name); name += File.separator; } else { name += File.separator; if (Match.path(name, pat, File.separatorChar)) flist.add(name); } if (Match.dir(name, pat, File.separatorChar)) listWork(md, name, pat, level + 1, flist); } else { if (Match.path(name, pat, File.separatorChar)) flist.add(name); } } } } /** * Pattern matching support class for list(). * Should probably be more public. */ // Translated from the c-client functions pmatch_full() and dmatch(). class Match { /** * Pathname pattern match * * @param s base string * @param pat pattern string * @param delim delimiter character * @return true if base matches pattern */ static public boolean path(String s, String pat, char delim) { try { return path(s, 0, s.length(), pat, 0, pat.length(), delim); } catch (StringIndexOutOfBoundsException e) { return false; } } static private boolean path(String s, int s_index, int s_len, String pat, int p_index, int p_len, char delim) throws StringIndexOutOfBoundsException { while (p_index < p_len) { char c = pat.charAt(p_index); switch (c) { case '%': if (++p_index >= p_len) // % at end of pattern // ok if no delimiters return delim == 0 || s.indexOf(delim, s_index) < 0; // scan remainder until delimiter do { if (path(s, s_index, s_len, pat, p_index, p_len, delim)) return true; } while (s.charAt(s_index) != delim && ++s_index < s_len); // ran into a delimiter or ran out of string without a match return false; case '*': if (++p_index >= p_len) // end of pattern? return true; // unconditional match do { if (path(s, s_index, s_len, pat, p_index, p_len, delim)) return true; } while (++s_index < s_len); // ran out of string without a match return false; default: // if ran out of string or no match, fail if (s_index >= s_len || c != s.charAt(s_index)) return false; // try the next string and pattern characters s_index++; p_index++; } } return s_index >= s_len; } /** * Directory pattern match * * @param s base string * @param pat pattern string * @return true if base is a matching directory of pattern */ static public boolean dir(String s, String pat, char delim) { try { return dir(s, 0, s.length(), pat, 0, pat.length(), delim); } catch (StringIndexOutOfBoundsException e) { return false; } } static private boolean dir(String s, int s_index, int s_len, String pat, int p_index, int p_len, char delim) throws StringIndexOutOfBoundsException { while (p_index < p_len) { char c = pat.charAt(p_index); switch (c) { case '%': if (s_index >= s_len) // end of base? return true; // subset match if (++p_index >= p_len) // % at end of pattern? return false; // no inferiors permitted do { if (dir(s, s_index, s_len, pat, p_index, p_len, delim)) return true; } while (s.charAt(s_index) != delim && ++s_index < s_len); if (s_index + 1 == s_len) // s ends with a delimiter return true; // must be a subset of pattern return dir(s, s_index, s_len, pat, p_index, p_len, delim); case '*': return true; // unconditional match default: if (s_index >= s_len) // end of base? return c == delim; // matched if at delimiter if (c != s.charAt(s_index)) return false; // try the next string and pattern characters s_index++; p_index++; } } return s_index >= s_len; } } /** * A ByteArrayOutputStream that allows us to share the byte array * rather than copy it. Eventually could replace this with something * that doesn't require a single contiguous byte array. */ class SharedByteArrayOutputStream extends ByteArrayOutputStream { public SharedByteArrayOutputStream(int size) { super(size); } public InputStream toStream() { return new SharedByteArrayInputStream(buf, 0, count); } }