org.armedbear.lisp.Pathname Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of abcl Show documentation
Show all versions of abcl Show documentation
Common Lisp implementation running on the JVM
/*
* Pathname.java
*
* Copyright (C) 2003-2007 Peter Graves
* $Id: Pathname.java 13461 2011-08-11 17:01:41Z ehuelsmann $
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent
* modules, and to copy and distribute the resulting executable under
* terms of your choice, provided that you also meet, for each linked
* independent module, the terms and conditions of the license of that
* module. An independent module is a module which is not derived from
* or based on this library. If you modify this library, you may extend
* this exception to your version of the library, but you are not
* obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
package org.armedbear.lisp;
import static org.armedbear.lisp.Lisp.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
public class Pathname extends LispObject {
/** The path component separator used by internally generated
* path namestrings.
*/
public final static char separator = '/';
protected LispObject host = NIL;
protected LispObject device = NIL;
protected LispObject directory = NIL;
protected LispObject name = NIL;
// A string, NIL, :WILD or :UNSPECIFIC.
protected LispObject type = NIL;
// A positive integer, or NIL, :WILD, :UNSPECIFIC, or :NEWEST.
protected LispObject version = NIL;
private volatile String namestring;
/** The protocol for changing any instance field (i.e. 'host',
* 'type', etc.) is to call this method after changing the field
* to recompute the namestring. We could do this with
* setter/getters, but that choose not to in order to avoid the
* performance indirection penalty.
*
* TODO There is no "perfomance penalty" in contemporary
* compilers which inline such access, so it would be better to
* implement this as setter/getter ME 20110622
*
* Although, given the number of bugs that crop up when this
* protocol is not adhered to, maybe we should consider it.
*/
public void invalidateNamestring() {
namestring = null;
}
private static final Primitive _INVALIDATE_NAMESTRING
= new pf_invalidate_namestring();
@DocString(name="%invalidate-namestring",
args="pathname",
returns="pathname")
private static class pf_invalidate_namestring extends Primitive {
pf_invalidate_namestring() {
super("%invalidate-namestring", PACKAGE_EXT, false);
}
@Override
public LispObject execute(LispObject first) {
((Pathname)coerceToPathname(first)).invalidateNamestring();
return first;
}
}
protected Pathname() {}
/** Copy constructor which shares no structure with the original. */
protected Pathname(Pathname p) {
if (p.host != NIL) {
if (p.host instanceof SimpleString) {
host = new SimpleString(((SimpleString)p.host).getStringValue());
} else if (p.host instanceof Symbol) {
host = p.host;
} else if (p.host instanceof Cons) {
host = new Cons((Cons)p.host);
} else {
Debug.assertTrue(false);
}
}
if (p.device != NIL) {
if (p.device instanceof SimpleString) {
device = new SimpleString(((SimpleString)p.device).getStringValue());
} else if (p.device instanceof Cons) {
Cons jars = (Cons)p.device;
device = new Cons(NIL, NIL);
LispObject first = jars.car();
if (first instanceof Pathname) {
((Cons)device).car = new Pathname((Pathname)first);
} else {
Debug.assertTrue(false);
}
if (!jars.cdr().equals(NIL)) {
if (jars.cdr() instanceof Cons) {
((Cons)device).cdr = new Cons(new Pathname((Pathname)jars.cdr().car()), NIL);
} else {
Debug.assertTrue(false);
}
}
} else if (p.device instanceof Symbol) {
device = p.device;
} else {
Debug.assertTrue(false);
}
}
if (p.directory != NIL) {
if (p.directory instanceof Cons) {
directory = NIL;
for (LispObject list = p.directory; list != NIL; list = list.cdr()) {
LispObject o = list.car();
if (o instanceof Symbol) {
directory = directory.push(o);
} else if (o instanceof SimpleString) {
directory = directory.push(new SimpleString(((SimpleString)o).getStringValue()));
} else {
Debug.assertTrue(false);
}
}
directory.nreverse();
} else {
Debug.assertTrue(false);
}
}
if (p.name != NIL) {
if (p.name instanceof SimpleString) {
name = new SimpleString(((SimpleString)p.name).getStringValue());
} else if (p.name instanceof Symbol) {
name = p.name;
} else {
Debug.assertTrue(false);
}
}
if (p.type != NIL) {
if (p.type instanceof SimpleString) {
type = new SimpleString(((SimpleString)p.type).getStringValue());
} else if (p.type instanceof Symbol) {
type = p.type;
} else {
Debug.assertTrue(false);
}
}
if (p.version != NIL) {
if (p.version instanceof Symbol) {
version = p.version;
} else if (p.version instanceof LispInteger) {
version = p.version;
} else {
Debug.assertTrue(false);
}
}
}
public Pathname(String s) {
init(s);
}
public static boolean isSupportedProtocol(String protocol) {
// There is no programmatic way to know what protocols will
// sucessfully construct a URL, so we check for well known ones...
if ("jar".equals(protocol)
|| "file".equals(protocol))
// || "http".equals(protocol)) XXX remove this as an optimization
{
return true;
}
// ... and try the entire constructor with some hopefully
// reasonable parameters for everything else.
try {
new URL(protocol, "example.org", "foo");
return true;
} catch (MalformedURLException e) {
return false;
}
}
public Pathname(URL url) {
// URL handling is now buried in init(String), as the URI
// escaping mechanism didn't interact well with '+' and other
// characters.
init(url.toString());
}
static final Symbol SCHEME = internKeyword("SCHEME");
static final Symbol AUTHORITY = internKeyword("AUTHORITY");
static final Symbol QUERY = internKeyword("QUERY");
static final Symbol FRAGMENT = internKeyword("FRAGMENT");
static final private String jarSeparator = "!/";
private final void init(String s) {
if (s == null) {
return;
}
if (s.equals(".") || s.equals("./")
|| (Utilities.isPlatformWindows && s.equals(".\\"))) {
directory = new Cons(Keyword.RELATIVE);
return;
}
if (s.equals("..") || s.equals("../")) {
directory = list(Keyword.RELATIVE, Keyword.UP);
return;
}
if (Utilities.isPlatformWindows) {
if (s.startsWith("\\\\")) { // XXX What if string starts with '//'?
//UNC path support
// match \\\\[directories-and-files]
int shareIndex = s.indexOf('\\', 2);
int dirIndex = s.indexOf('\\', shareIndex + 1);
if (shareIndex == -1 || dirIndex == -1) {
error(new LispError("Unsupported UNC path format: \"" + s + '"'));
}
host = new SimpleString(s.substring(2, shareIndex));
device = new SimpleString(s.substring(shareIndex + 1, dirIndex));
Pathname p = new Pathname(s.substring(dirIndex));
directory = p.directory;
name = p.name;
type = p.type;
version = p.version;
invalidateNamestring();
return;
}
}
// A JAR file
if (s.startsWith("jar:") && s.endsWith(jarSeparator)) {
LispObject jars = NIL;
int i = s.lastIndexOf(jarSeparator, s.length() - jarSeparator.length() - 1);
String jar = null;
if (i == -1) {
jar = s;
} else {
// There can be no more than two jar references and the
// inner one must be a file reference within the outer.
jar = "jar:file:" + s.substring(i + jarSeparator.length());
s = s.substring("jar:".length(), i + jarSeparator.length());
Pathname p = new Pathname(s);
jars = jars.push(p.device.car());
}
if (jar.startsWith("jar:file:")) {
String file
= jar.substring("jar:file:".length(),
jar.length() - jarSeparator.length());
Pathname jarPathname;
if (file.length() > 0) {
URL url = null;
URI uri = null;
try {
url = new URL("file:" + file);
uri = url.toURI();
} catch (MalformedURLException e1) {
error(new SimpleError("Failed to create URI from "
+ "'" + file + "'"
+ ": " + e1.getMessage()));
} catch (URISyntaxException e2) {
error(new SimpleError("Failed to create URI from "
+ "'" + file + "'"
+ ": " + e2.getMessage()));
}
String path = uri.getPath();
if (path == null) {
// We allow "jar:file:baz.jar!/" to construct a relative
// path for jar files, so MERGE-PATHNAMES means something.
jarPathname = new Pathname(uri.getSchemeSpecificPart());
} else {
jarPathname = new Pathname((new File(path)).getPath());
}
} else {
jarPathname = new Pathname("");
}
jars = jars.push(jarPathname);
} else {
URL url = null;
try {
url = new URL(jar.substring("jar:".length(), jar.length() - 2));
Pathname p = new Pathname(url);
jars = jars.push(p);
} catch (MalformedURLException e) {
error(new LispError("Failed to parse URL "
+ "'" + url + "'"
+ e.getMessage()));
}
}
jars = jars.nreverse();
device = jars;
invalidateNamestring();
return;
}
// An entry in a JAR file
final int separatorIndex = s.lastIndexOf(jarSeparator);
if (separatorIndex > 0 && s.startsWith("jar:")) {
final String jarURL = s.substring(0, separatorIndex + jarSeparator.length());
URL url = null;
try {
url = new URL(jarURL);
} catch (MalformedURLException ex) {
error(new LispError("Failed to parse URL "
+ "'" + jarURL + "'"
+ ex.getMessage()));
}
Pathname d = new Pathname(url);
if (device instanceof Cons) {
LispObject[] jars = d.copyToArray();
// XXX Is this ever reached? If so, need to append lists
Debug.assertTrue(false);
} else {
device = d.device;
}
s = "/" + s.substring(separatorIndex + jarSeparator.length());
Pathname p = new Pathname("file:" + s); // Use URI escaping rules
directory = p.directory;
name = p.name;
type = p.type;
version = p.version;
return;
}
// A URL
if (isValidURL(s)) {
URL url = null;
try {
url = new URL(s);
} catch (MalformedURLException e) {
Debug.assertTrue(false);
}
String scheme = url.getProtocol();
if (scheme.equals("file")) {
URI uri = null;
try {
uri = url.toURI();
} catch (URISyntaxException ex) {
error(new SimpleError("Improper URI syntax for "
+ "'" + url.toString() + "'"
+ ": " + ex.toString()));
}
String uriPath = uri.getPath();
if (null == uriPath) {
// We make an exception for forms like "file:z:/foo/path"
uriPath = uri.getSchemeSpecificPart();
if (uriPath == null || uriPath.equals("")) {
error(new LispError("The URI has no path: " + uri));
}
}
final File file = new File(uriPath);
final Pathname p = new Pathname(file.getPath());
this.host = p.host;
this.device = p.device;
this.directory = p.directory;
this.name = p.name;
this.type = p.type;
this.version = p.version;
return;
}
Debug.assertTrue(scheme != null);
URI uri = null;
try {
uri = url.toURI().normalize();
} catch (URISyntaxException e) {
error(new LispError("Couldn't form URI from "
+ "'" + url + "'"
+ " because: " + e));
}
String authority = uri.getAuthority();
Debug.assertTrue(authority != null);
host = NIL;
host = host.push(SCHEME);
host = host.push(new SimpleString(scheme));
host = host.push(AUTHORITY);
host = host.push(new SimpleString(authority));
device = NIL;
// URI encode necessary characters
String path = uri.getRawPath();
if (path == null) {
path = "";
}
String query = uri.getRawQuery();
if (query != null) {
host = host.push(QUERY);
host = host.push(new SimpleString(query));
}
String fragment = uri.getRawFragment();
if (fragment != null) {
host = host.push(FRAGMENT);
host = host.push(new SimpleString(fragment));
}
Pathname p = new Pathname(path != null ? path : "");
directory = p.directory;
name = p.name;
type = p.type;
host = host.nreverse();
invalidateNamestring();
return;
}
if (Utilities.isPlatformWindows) {
if (s.contains("\\")) {
s = s.replace("\\", "/");
}
}
// Expand user home directories
if (Utilities.isPlatformUnix) {
if (s.equals("~")) {
s = System.getProperty("user.home").concat("/");
} else if (s.startsWith("~/")) {
s = System.getProperty("user.home").concat(s.substring(1));
}
}
namestring = s;
if (Utilities.isPlatformWindows) {
if (s.length() >= 2 && s.charAt(1) == ':') {
device = new SimpleString(s.charAt(0));
s = s.substring(2);
}
}
String d = null;
// Find last file separator char.
for (int i = s.length(); i-- > 0;) {
if (s.charAt(i) == '/') {
d = s.substring(0, i + 1);
s = s.substring(i + 1);
break;
}
}
if (d != null) {
if (s.equals("..")) {
d = d.concat(s);
s = "";
}
directory = parseDirectory(d);
}
if (s.startsWith(".")
// No TYPE can be parsed
&& (s.indexOf(".", 1) == -1
|| s.substring(s.length() -1).equals("."))) {
name = new SimpleString(s);
return;
}
int index = s.lastIndexOf('.');
String n = null;
String t = null;
if (index > 0) {
n = s.substring(0, index);
t = s.substring(index + 1);
} else if (s.length() > 0) {
n = s;
}
if (n != null) {
if (n.equals("*")) {
name = Keyword.WILD;
} else {
name = new SimpleString(n);
}
}
if (t != null) {
if (t.equals("*")) {
type = Keyword.WILD;
} else {
type = new SimpleString(t);
}
}
}
private static final LispObject parseDirectory(String d) {
if (d.equals("/") || (Utilities.isPlatformWindows && d.equals("\\"))) {
return new Cons(Keyword.ABSOLUTE);
}
LispObject result;
if (d.startsWith("/") || (Utilities.isPlatformWindows && d.startsWith("\\"))) {
result = new Cons(Keyword.ABSOLUTE);
} else {
result = new Cons(Keyword.RELATIVE);
}
StringTokenizer st = new StringTokenizer(d, "/\\");
while (st.hasMoreTokens()) {
String token = st.nextToken();
LispObject obj;
if (token.equals("*")) {
obj = Keyword.WILD;
} else if (token.equals("**")) {
obj = Keyword.WILD_INFERIORS;
} else if (token.equals("..")) {
if (result.car() instanceof AbstractString) {
result = result.cdr();
continue;
}
obj = Keyword.UP;
} else {
obj = new SimpleString(token);
}
result = new Cons(obj, result);
}
return result.nreverse();
}
@Override
public LispObject getParts() {
LispObject parts = NIL;
parts = parts.push(new Cons("HOST", host));
parts = parts.push(new Cons("DEVICE", device));
parts = parts.push(new Cons("DIRECTORY", directory));
parts = parts.push(new Cons("NAME", name));
parts = parts.push(new Cons("TYPE", type));
parts = parts.push(new Cons("VERSION", version));
return parts.nreverse();
}
@Override
public LispObject typeOf() {
if (isURL()) {
return Symbol.URL_PATHNAME;
}
if (isJar()) {
return Symbol.JAR_PATHNAME;
}
return Symbol.PATHNAME;
}
@Override
public LispObject classOf() {
if (isURL()) {
return BuiltInClass.URL_PATHNAME;
}
if (isJar()) {
return BuiltInClass.JAR_PATHNAME;
}
return BuiltInClass.PATHNAME;
}
@Override
public LispObject typep(LispObject type) {
if (type == Symbol.PATHNAME) {
return T;
}
if (type == Symbol.JAR_PATHNAME && isJar()) {
return T;
}
if (type == Symbol.URL_PATHNAME && isURL()) {
return T;
}
if (type == BuiltInClass.PATHNAME) {
return T;
}
if (type == BuiltInClass.JAR_PATHNAME && isJar()) {
return T;
}
if (type == BuiltInClass.URL_PATHNAME && isURL()) {
return T;
}
return super.typep(type);
}
public final LispObject getDevice() {
return device;
}
public String getNamestring() {
if (namestring != null) {
return namestring;
}
if (name == NIL && type != NIL) {
Debug.assertTrue(namestring == null);
return null;
}
if (directory instanceof AbstractString) {
Debug.assertTrue(false);
}
StringBuilder sb = new StringBuilder();
// "If a pathname is converted to a namestring, the symbols NIL and
// :UNSPECIFIC cause the field to be treated as if it were empty. That
// is, both NIL and :UNSPECIFIC cause the component not to appear in
// the namestring." 19.2.2.2.3.1
if (host != NIL) {
Debug.assertTrue(host instanceof AbstractString
|| isURL());
if (isURL()) {
LispObject scheme = Symbol.GETF.execute(host, SCHEME, NIL);
LispObject authority = Symbol.GETF.execute(host, AUTHORITY, NIL);
Debug.assertTrue(scheme != NIL);
sb.append(scheme.getStringValue());
sb.append(":");
if (authority != NIL) {
sb.append("//");
sb.append(authority.getStringValue());
}
} else if (this instanceof LogicalPathname) {
sb.append(host.getStringValue());
sb.append(':');
} else {
// UNC paths now use unprintable representation
return null;
}
}
boolean uriEncoded = false;
if (device == NIL) {
} else if (device == Keyword.UNSPECIFIC) {
} else if (isJar()) {
LispObject[] jars = ((Cons) device).copyToArray();
StringBuilder prefix = new StringBuilder();
for (int i = 0; i < jars.length; i++) {
prefix.append("jar:");
if (!((Pathname)jars[i]).isURL() && i == 0) {
sb.append("file:");
uriEncoded = true;
}
Pathname jar = (Pathname) jars[i];
String encodedNamestring;
if (uriEncoded) {
encodedNamestring = uriEncode(jar.getNamestring());
} else {
encodedNamestring = jar.getNamestring();
}
sb.append(encodedNamestring);
sb.append("!/");
}
sb = prefix.append(sb);
} else if (device instanceof AbstractString) {
sb.append(device.getStringValue());
if (this instanceof LogicalPathname
|| host == NIL) {
sb.append(':'); // non-UNC paths
}
} else {
Debug.assertTrue(false);
}
String directoryNamestring = getDirectoryNamestring();
if (uriEncoded) {
directoryNamestring = uriEncode(directoryNamestring);
}
if (isJar()) {
if (directoryNamestring.startsWith("/")) {
sb.append(directoryNamestring.substring(1));
} else {
sb.append(directoryNamestring);
}
} else {
sb.append(directoryNamestring);
}
if (name instanceof AbstractString) {
String n = name.getStringValue();
if (n.indexOf('/') >= 0) {
Debug.assertTrue(namestring == null);
return null;
}
if (uriEncoded) {
sb.append(uriEncode(n));
} else {
sb.append(n);
}
} else if (name == Keyword.WILD) {
sb.append('*');
}
if (type != NIL && type != Keyword.UNSPECIFIC) {
sb.append('.');
if (type instanceof AbstractString) {
String t = type.getStringValue();
// Allow Windows shortcuts to include TYPE
if (!(t.endsWith(".lnk") && Utilities.isPlatformWindows)) {
if (t.indexOf('.') >= 0) {
Debug.assertTrue(namestring == null);
return null;
}
}
if (uriEncoded) {
sb.append(uriEncode(t));
} else {
sb.append(t);
}
} else if (type == Keyword.WILD) {
sb.append('*');
} else {
Debug.assertTrue(false);
}
}
if (isURL()) {
LispObject o = Symbol.GETF.execute(host, QUERY, NIL);
if (o != NIL) {
sb.append("?");
sb.append(o.getStringValue());
}
o = Symbol.GETF.execute(host, FRAGMENT, NIL);
if (o != NIL) {
sb.append("#");
sb.append(o.getStringValue());
}
}
if (this instanceof LogicalPathname) {
if (version.integerp()) {
sb.append('.');
int base = Fixnum.getValue(Symbol.PRINT_BASE.symbolValue());
if (version instanceof Fixnum) {
sb.append(Integer.toString(((Fixnum) version).value, base).toUpperCase());
} else if (version instanceof Bignum) {
sb.append(((Bignum) version).value.toString(base).toUpperCase());
}
} else if (version == Keyword.WILD) {
sb.append(".*");
} else if (version == Keyword.NEWEST) {
sb.append(".NEWEST");
}
}
namestring = sb.toString();
// XXX Decide if this is necessary
// if (isURL()) {
// namestring = Utilities.uriEncode(namestring);
// }
return namestring;
}
protected String getDirectoryNamestring() {
validateDirectory(true);
StringBuilder sb = new StringBuilder();
// "If a pathname is converted to a namestring, the symbols NIL and
// :UNSPECIFIC cause the field to be treated as if it were empty. That
// is, both NIL and :UNSPECIFIC cause the component not to appear in
// the namestring." 19.2.2.2.3.1
if (directory != NIL) {
final char separatorChar = '/';
LispObject temp = directory;
LispObject part = temp.car();
temp = temp.cdr();
if (part == Keyword.ABSOLUTE) {
sb.append(separatorChar);
} else if (part == Keyword.RELATIVE) {
if (temp == NIL) {
// #p"./"
sb.append('.');
sb.append(separatorChar);
}
// else: Nothing to do.
} else {
error(new FileError("Unsupported directory component "
+ part.printObject() + ".",
this));
}
while (temp != NIL) {
part = temp.car();
if (part instanceof AbstractString) {
sb.append(part.getStringValue());
} else if (part == Keyword.WILD) {
sb.append('*');
} else if (part == Keyword.WILD_INFERIORS) {
sb.append("**");
} else if (part == Keyword.UP) {
sb.append("..");
} else {
error(new FileError("Unsupported directory component " + part.princToString() + ".",
this));
}
sb.append(separatorChar);
temp = temp.cdr();
}
}
return sb.toString();
}
/** @return The representation of this pathname suitable for
* referencing an entry in a Zip/JAR file
*/
protected String asEntryPath() {
Pathname p = new Pathname();
p.directory = directory;
p.name = name;
p.type = type;
p.invalidateNamestring();
String path = p.getNamestring();
StringBuilder result = new StringBuilder();
result.append(path);
// Entries in jar files are always relative, but Pathname
// directories are :ABSOLUTE.
if (result.length() > 1
&& result.substring(0, 1).equals("/")) {
return result.substring(1);
}
return result.toString();
}
@Override
public boolean equal(LispObject obj) {
if (this == obj) {
return true;
}
if (obj instanceof Pathname) {
Pathname p = (Pathname) obj;
if (Utilities.isPlatformWindows) {
if (!host.equalp(p.host)) {
return false;
}
if (!device.equalp(p.device)) {
return false;
}
if (!directory.equalp(p.directory)) {
return false;
}
if (!name.equalp(p.name)) {
return false;
}
if (!type.equalp(p.type)) {
return false;
}
// Ignore version component.
//if (!version.equalp(p.version))
// return false;
} else {
// Unix.
if (!host.equal(p.host)) {
return false;
}
if (!device.equal(p.device)) {
return false;
}
if (!directory.equal(p.directory)) {
return false;
}
if (!name.equal(p.name)) {
return false;
}
if (!type.equal(p.type)) {
return false;
}
// Ignore version component.
//if (!version.equal(p.version))
// return false;
}
return true;
}
return false;
}
@Override
public boolean equalp(LispObject obj) {
return equal(obj);
}
@Override
public int sxhash() {
return ((host.sxhash()
^ device.sxhash()
^ directory.sxhash()
^ name.sxhash()
^ type.sxhash()) & 0x7fffffff);
}
@Override
public String printObject() {
final LispThread thread = LispThread.currentThread();
final boolean printReadably = (Symbol.PRINT_READABLY.symbolValue(thread) != NIL);
final boolean printEscape = (Symbol.PRINT_ESCAPE.symbolValue(thread) != NIL);
boolean useNamestring;
String s = null;
s = getNamestring();
if (s != null) {
useNamestring = true;
if (printReadably) {
// We have a namestring. Check for pathname components that
// can't be read from the namestring.
if ((host != NIL && !isURL())
|| version != NIL)
{
useNamestring = false;
} else if (name instanceof AbstractString) {
String n = name.getStringValue();
if (n.equals(".") || n.equals("..")) {
useNamestring = false;
} else if (n.indexOf(File.separatorChar) >= 0) {
useNamestring = false;
}
}
}
} else {
useNamestring = false;
}
StringBuilder sb = new StringBuilder();
if (useNamestring) {
if (printReadably || printEscape) {
sb.append("#P\"");
}
final int limit = s.length();
for (int i = 0; i < limit; i++) {
char c = s.charAt(i);
if (printReadably || printEscape) {
if (c == '\"' || c == '\\') {
sb.append('\\');
}
}
sb.append(c);
}
if (printReadably || printEscape) {
sb.append('"');
}
return sb.toString();
}
sb.append("PATHNAME (with no namestring) ");
if (host != NIL) {
sb.append(":HOST ");
sb.append(host.printObject());
sb.append(" ");
}
if (device != NIL) {
sb.append(":DEVICE ");
sb.append(device.printObject());
sb.append(" ");
}
if (directory != NIL) {
sb.append(":DIRECTORY ");
sb.append(directory.printObject());
sb.append(" ");
}
if (name != NIL) {
sb.append(":NAME ");
sb.append(name.printObject());
sb.append(" ");
}
if (type != NIL) {
sb.append(":TYPE ");
sb.append(type.printObject());
sb.append(" ");
}
if (version != NIL) {
sb.append(":VERSION ");
sb.append(version.printObject());
sb.append(" ");
}
if (sb.charAt(sb.length() - 1) == ' ') {
sb.setLength(sb.length() - 1);
}
return unreadableString(sb.toString());
}
// A logical host is represented as the string that names it.
// (defvar *logical-pathname-translations* (make-hash-table :test 'equal))
public static HashTable LOGICAL_PATHNAME_TRANSLATIONS =
HashTable.newEqualHashTable(64, NIL, NIL);
private static final Symbol _LOGICAL_PATHNAME_TRANSLATIONS_ =
exportSpecial("*LOGICAL-PATHNAME-TRANSLATIONS*", PACKAGE_SYS,
LOGICAL_PATHNAME_TRANSLATIONS);
public static Pathname parseNamestring(String s) {
return new Pathname(s);
}
public static boolean isValidURL(String s) {
// On Windows, the scheme "[A-Z]:.*" is ambiguous; reject as urls
// This special case reduced exceptions while compiling Maxima by 90%+
if (Utilities.isPlatformWindows && s.length() >= 2 && s.charAt(1) == ':') {
char c = s.charAt(0);
if (('A' <= s.charAt(0) && s.charAt(0) <= 'Z')
|| ('a' <= s.charAt(0) && s.charAt(0) <= 'z'))
return false;
}
if (s.indexOf(':') == -1) // no schema separator; can't be valid
return false;
try {
URL url = new URL(s);
} catch (MalformedURLException e) {
// Generating an exception is a heavy operation,
// we want to try hard not to get into this branch, without
// implementing the URL class ourselves
return false;
}
return true;
}
// public static URL toURL(Pathname p) {
// try {
// return p.toURL();
// } catch (MalformedURLException e) {
// Debug.assertTrue(false);
// return null; // not reached
// }
// }
URLConnection getURLConnection() {
Debug.assertTrue(isURL());
URL url = this.toURL();
URLConnection result = null;
try {
result = url.openConnection();
} catch (IOException e) {
error(new FileError("Failed to open URL connection.",
this));
}
return result;
}
public static Pathname parseNamestring(AbstractString namestring) {
// Check for a logical pathname host.
String s = namestring.getStringValue();
if (!isValidURL(s)) {
String h = getHostString(s);
if (h != null && LOGICAL_PATHNAME_TRANSLATIONS.get(new SimpleString(h)) != null) {
// A defined logical pathname host.
return new LogicalPathname(h, s.substring(s.indexOf(':') + 1));
}
}
return new Pathname(s);
}
// XXX was @return Pathname
public static LogicalPathname parseNamestring(AbstractString namestring,
AbstractString host)
{
String s = namestring.getStringValue();
// Look for a logical pathname host in the namestring.
String h = getHostString(s);
if (h != null) {
if (!h.equals(host.getStringValue())) {
error(new LispError("Host in " + s
+ " does not match requested host "
+ host.getStringValue()));
// Not reached.
return null;
}
// Remove host prefix from namestring.
s = s.substring(s.indexOf(':') + 1);
}
if (LOGICAL_PATHNAME_TRANSLATIONS.get(host) != null) {
// A defined logical pathname host.
return new LogicalPathname(host.getStringValue(), s);
}
error(new LispError(host.princToString() + " is not defined as a logical pathname host."));
// Not reached.
return null;
}
// "one or more uppercase letters, digits, and hyphens"
protected static String getHostString(String s) {
int colon = s.indexOf(':');
if (colon >= 0) {
return s.substring(0, colon).toUpperCase();
} else {
return null;
}
}
static final void checkCaseArgument(LispObject arg) {
if (arg != Keyword.COMMON && arg != Keyword.LOCAL) {
type_error(arg, list(Symbol.MEMBER, Keyword.COMMON,
Keyword.LOCAL));
}
}
private static final Primitive _PATHNAME_HOST = new pf_pathname_host();
@DocString(name="%pathname-host")
private static class pf_pathname_host extends Primitive {
pf_pathname_host() {
super("%pathname-host", PACKAGE_SYS, false);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
checkCaseArgument(second); // FIXME Why is this ignored?
return coerceToPathname(first).host;
}
}
private static final Primitive _PATHNAME_DEVICE = new pf_pathname_device();
@DocString(name="%pathname-device")
private static class pf_pathname_device extends Primitive {
pf_pathname_device() {
super("%pathname-device", PACKAGE_SYS, false);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
checkCaseArgument(second); // FIXME Why is this ignored?
return coerceToPathname(first).device;
}
}
private static final Primitive _PATHNAME_DIRECTORY = new pf_pathname_directory();
@DocString(name="%pathname-directory")
private static class pf_pathname_directory extends Primitive {
pf_pathname_directory() {
super("%pathname-directory", PACKAGE_SYS, false);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
checkCaseArgument(second); // FIXME Why is this ignored?
return coerceToPathname(first).directory;
}
}
private static final Primitive _PATHNAME_NAME = new pf_pathname_name();
@DocString(name="%pathname-name")
private static class pf_pathname_name extends Primitive {
pf_pathname_name() {
super ("%pathname-name", PACKAGE_SYS, false);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
checkCaseArgument(second); // FIXME Why is this ignored?
return coerceToPathname(first).name;
}
}
private static final Primitive _PATHNAME_TYPE = new pf_pathname_type();
@DocString(name="%pathname-type")
private static class pf_pathname_type extends Primitive {
pf_pathname_type() {
super("%pathname-type", PACKAGE_SYS, false);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
checkCaseArgument(second); // FIXME Why is this ignored?
return coerceToPathname(first).type;
}
}
private static final Primitive PATHNAME_VERSION = new pf_pathname_version();
@DocString(name="pathname-version",
args="pathname",
returns="version",
doc="Return the version component of PATHNAME.")
private static class pf_pathname_version extends Primitive {
pf_pathname_version() {
super("pathname-version", "pathname");
}
@Override
public LispObject execute(LispObject arg) {
return coerceToPathname(arg).version;
}
}
private static final Primitive NAMESTRING = new pf_namestring();
@DocString(name="namestring",
args="pathname",
returns="namestring",
doc="Returns the NAMESTRING of PATHNAME if it has one.\n"
+ "\n"
+ "If PATHNAME is of type url-pathname or jar-pathname the NAMESTRING is encoded\n"
+ "according to the uri percent escape rules.\n"
+ "\n"
+ "Signals an error if PATHNAME lacks a printable NAMESTRING representation.\n")
private static class pf_namestring extends Primitive {
pf_namestring() {
super("namestring", "pathname");
}
@Override
public LispObject execute(LispObject arg) {
Pathname pathname = coerceToPathname(arg);
String namestring = pathname.getNamestring();
if (namestring == null) {
error(new SimpleError("Pathname has no namestring: "
+ pathname.princToString()));
}
return new SimpleString(namestring);
}
}
private static final Primitive DIRECTORY_NAMESTRING = new pf_directory_namestring();
// TODO clarify uri encoding rules in implementation, then document
@DocString(name="directory-namestring",
args="pathname",
returns="namestring",
doc="Returns the NAMESTRING of directory porition of PATHNAME if it has one.")
private static class pf_directory_namestring extends Primitive {
pf_directory_namestring() {
super("directory-namestring", "pathname");
}
@Override
public LispObject execute(LispObject arg) {
return new SimpleString(coerceToPathname(arg).getDirectoryNamestring());
}
}
private static final Primitive PATHNAME = new pf_pathname();
@DocString(name="pathname",
args="pathspec",
returns="pathname",
doc="Returns the PATHNAME denoted by PATHSPEC.")
private static class pf_pathname extends Primitive {
pf_pathname() {
super("pathname", "pathspec");
}
@Override
public LispObject execute(LispObject arg) {
return coerceToPathname(arg);
}
}
private static final Primitive _PARSE_NAMESTRING = new pf_parse_namestring();
@DocString(name="%parse-namestring",
args="namestring host default-pathname",
returns="pathname, position")
private static class pf_parse_namestring extends Primitive {
pf_parse_namestring() {
super("%parse-namestring", PACKAGE_SYS, false,
"namestring host default-pathname");
}
@Override
public LispObject execute(LispObject first, LispObject second, LispObject third) {
final LispThread thread = LispThread.currentThread();
final AbstractString namestring = checkString(first);
// The HOST parameter must be a string or NIL.
if (second == NIL) {
// "If HOST is NIL, DEFAULT-PATHNAME is a logical pathname, and
// THING is a syntactically valid logical pathname namestring
// without an explicit host, then it is parsed as a logical
// pathname namestring on the host that is the host component
// of DEFAULT-PATHNAME."
third = coerceToPathname(third);
if (third instanceof LogicalPathname) {
second = ((LogicalPathname) third).host;
} else {
return thread.setValues(parseNamestring(namestring),
namestring.LENGTH());
}
}
Debug.assertTrue(second != NIL);
final AbstractString host = checkString(second);
return thread.setValues(parseNamestring(namestring, host),
namestring.LENGTH());
}
}
private static final Primitive MAKE_PATHNAME = new pf_make_pathname();
@DocString(name="make-pathname",
args="&key host device directory name type version defaults case",
returns="pathname",
doc="Constructs and returns a pathname from the supplied keyword arguments.")
private static class pf_make_pathname extends Primitive {
pf_make_pathname() {
super("make-pathname",
"&key host device directory name type version defaults case");
}
@Override
public LispObject execute(LispObject[] args) {
return _makePathname(args);
}
}
// Used by the #p reader.
public static final Pathname makePathname(LispObject args) {
return _makePathname(args.copyToArray());
}
public static final Pathname makePathname(File file) {
String namestring = null;
try {
namestring = file.getCanonicalPath();
} catch (IOException e) {
Debug.trace("Failed to make a Pathname from "
+ "." + file + "'");
return null;
}
return new Pathname(namestring);
}
static final Pathname _makePathname(LispObject[] args) {
if (args.length % 2 != 0) {
error(new ProgramError("Odd number of keyword arguments."));
}
LispObject host = NIL;
LispObject device = NIL;
LispObject directory = NIL;
LispObject name = NIL;
LispObject type = NIL;
LispObject version = NIL;
Pathname defaults = null;
boolean deviceSupplied = false;
boolean nameSupplied = false;
boolean typeSupplied = false;
boolean directorySupplied = false;
boolean versionSupplied = false;
for (int i = 0; i < args.length; i += 2) {
LispObject key = args[i];
LispObject value = args[i + 1];
if (key == Keyword.HOST) {
host = value;
} else if (key == Keyword.DEVICE) {
device = value;
deviceSupplied = true;
} else if (key == Keyword.DIRECTORY) {
directorySupplied = true;
if (value instanceof AbstractString) {
directory = list(Keyword.ABSOLUTE, value);
} else if (value == Keyword.WILD) {
directory = list(Keyword.ABSOLUTE, Keyword.WILD);
} else {
// a valid pathname directory is a string, a list of strings, nil, :wild, :unspecific
// ??? would be nice to (deftype pathname-arg ()
// '(or (member :wild :unspecific) string (and cons ,(mapcar ...
// Is this possible?
if ((value instanceof Cons
// XXX check that the elements of a list are themselves valid
|| value == Keyword.UNSPECIFIC
|| value.equals(NIL))) {
directory = value;
} else {
error(new TypeError("DIRECTORY argument not a string, list of strings, nil, :WILD, or :UNSPECIFIC.", value, NIL));
}
}
} else if (key == Keyword.NAME) {
name = value;
nameSupplied = true;
} else if (key == Keyword.TYPE) {
type = value;
typeSupplied = true;
} else if (key == Keyword.VERSION) {
version = value;
versionSupplied = true;
} else if (key == Keyword.DEFAULTS) {
defaults = coerceToPathname(value);
} else if (key == Keyword.CASE) {
// Ignored.
}
}
if (defaults != null) {
if (host == NIL) {
host = defaults.host;
}
if (!directorySupplied) {
directory = defaults.directory;
}
if (!deviceSupplied) {
device = defaults.device;
}
if (!nameSupplied) {
name = defaults.name;
}
if (!typeSupplied) {
type = defaults.type;
}
if (!versionSupplied) {
version = defaults.version;
}
}
final Pathname p;
final boolean logical;
LispObject logicalHost = NIL;
if (host != NIL) {
if (host instanceof AbstractString) {
logicalHost = LogicalPathname.canonicalizeStringComponent((AbstractString) host);
}
if (LOGICAL_PATHNAME_TRANSLATIONS.get(logicalHost) == null) {
// Not a defined logical pathname host -- A UNC path
//warning(new LispError(host.printObject() + " is not defined as a logical pathname host."));
p = new Pathname();
logical = false;
p.host = host;
} else {
p = new LogicalPathname();
logical = true;
p.host = logicalHost;
}
p.device = Keyword.UNSPECIFIC;
} else {
p = new Pathname();
logical = false;
}
if (device != NIL) {
if (logical) {
// "The device component of a logical pathname is always :UNSPECIFIC."
if (device != Keyword.UNSPECIFIC) {
error(new LispError("The device component of a logical pathname must be :UNSPECIFIC."));
}
} else {
p.device = device;
}
}
if (directory != NIL) {
if (logical) {
if (directory.listp()) {
LispObject d = NIL;
while (directory != NIL) {
LispObject component = directory.car();
if (component instanceof AbstractString) {
d = d.push(LogicalPathname.canonicalizeStringComponent((AbstractString) component));
} else {
d = d.push(component);
}
directory = directory.cdr();
}
p.directory = d.nreverse();
} else if (directory == Keyword.WILD || directory == Keyword.WILD_INFERIORS) {
p.directory = directory;
} else {
error(new LispError("Invalid directory component for logical pathname: " + directory.princToString()));
}
} else {
p.directory = directory;
}
}
if (name != NIL) {
if (logical && name instanceof AbstractString) {
p.name = LogicalPathname.canonicalizeStringComponent((AbstractString) name);
} else if (name instanceof AbstractString) {
p.name = validateStringComponent((AbstractString) name);
} else {
p.name = name;
}
}
if (type != NIL) {
if (logical && type instanceof AbstractString) {
p.type = LogicalPathname.canonicalizeStringComponent((AbstractString) type);
} else {
p.type = type;
}
}
p.version = version;
return p;
}
private static final AbstractString validateStringComponent(AbstractString s) {
final int limit = s.length();
for (int i = 0; i < limit; i++) {
char c = s.charAt(i);
// XXX '\\' should be illegal in all Pathnames at this point?
if (c == '/' || c == '\\' && Utilities.isPlatformWindows) {
error(new LispError("Invalid character #\\" + c
+ " in pathname component \"" + s
+ '"'));
// Not reached.
return null;
}
}
return s;
}
private final boolean validateDirectory(boolean signalError) {
LispObject temp = directory;
while (temp != NIL) {
LispObject first = temp.car();
temp = temp.cdr();
if (first == Keyword.ABSOLUTE || first == Keyword.WILD_INFERIORS) {
LispObject second = temp.car();
if (second == Keyword.UP || second == Keyword.BACK) {
if (signalError) {
StringBuilder sb = new StringBuilder();
sb.append(first.printObject());
sb.append(" may not be followed immediately by ");
sb.append(second.printObject());
sb.append('.');
error(new FileError(sb.toString(), this));
}
return false;
}
}
}
return true;
}
private static final Primitive PATHNAMEP = new pf_pathnamep();
@DocString(name="pathnamep",
args="object",
returns="generalized-boolean",
doc="Returns true if OBJECT is of type pathname; otherwise, returns false.")
private static class pf_pathnamep extends Primitive {
pf_pathnamep() {
super("pathnamep", "object");
}
@Override
public LispObject execute(LispObject arg) {
return arg instanceof Pathname ? T : NIL;
}
}
private static final Primitive LOGICAL_PATHNAME_P = new pf_logical_pathname_p();
@DocString(name="logical-pathname-p",
args="object",
returns="generalized-boolean",
doc="Returns true if OBJECT is of type logical-pathname; otherwise, returns false.")
private static class pf_logical_pathname_p extends Primitive {
pf_logical_pathname_p() {
super("logical-pathname-p", PACKAGE_SYS, true, "object");
}
@Override
public LispObject execute(LispObject arg) {
return arg instanceof LogicalPathname ? T : NIL;
}
}
private static final Primitive USER_HOMEDIR_PATHNAME = new pf_user_homedir_pathname();
@DocString(name="user-homedir-pathname",
args="&optional host",
returns="pathname",
doc="Determines the pathname that corresponds to the user's home directory.\n"
+ "The value returned is obtained from the JVM system propoerty 'user.home'.\n"
+ "If HOST is specified, returns NIL.")
private static class pf_user_homedir_pathname extends Primitive {
pf_user_homedir_pathname() {
super("user-homedir-pathname", "&optional host");
}
@Override
public LispObject execute(LispObject[] args) {
switch (args.length) {
case 0: {
String s = System.getProperty("user.home");
if (!s.endsWith(File.separator)) {
s = s.concat(File.separator);
}
return new Pathname(s);
}
case 1:
return NIL;
default:
return error(new WrongNumberOfArgumentsException(this, 0, 1));
}
}
}
private static final Primitive LIST_DIRECTORY = new pf_list_directory();
@DocString(name="list-directory",
args="directory &optional (resolve-symlinks t)",
returns="pathnames",
doc="Lists the contents of DIRECTORY, optionally resolving symbolic links.")
private static class pf_list_directory extends Primitive {
pf_list_directory() {
super("list-directory", PACKAGE_SYS, true, "directory &optional (resolve-symlinks t)");
}
@Override
public LispObject execute(LispObject arg) {
return execute(arg, T);
}
@Override
public LispObject execute(LispObject arg, LispObject arg2) {
Pathname pathname = coerceToPathname(arg);
if (pathname instanceof LogicalPathname) {
pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
}
LispObject result = NIL;
if (pathname.isJar()) {
String directory = pathname.asEntryPath();
Debug.assertTrue(directory != null); // We should only be listing directories
if (pathname.device.cdr() instanceof Cons) {
return error(new FileError("Unimplemented directory listing of JAR within JAR.", pathname));
}
if (directory.length() == 0) {
directory = "/*";
} else {
if (directory.endsWith("/")) {
directory = "/" + directory + "*";
} else {
directory = "/" + directory + "/*";
}
}
SimpleString wildcard = new SimpleString(directory);
SimpleString wildcardDirectory = new SimpleString(directory + "/");
ZipFile jar = ZipCache.get((Pathname)pathname.device.car());
LispObject matches;
for (Enumeration extends ZipEntry> entries = jar.entries();
entries.hasMoreElements();) {
ZipEntry entry = entries.nextElement();
String entryName = "/" + entry.getName();
if (entryName.endsWith("/")) {
matches = Symbol.PATHNAME_MATCH_P
.execute(new SimpleString(entryName), wildcardDirectory);
} else {
matches = Symbol.PATHNAME_MATCH_P.
execute(new SimpleString(entryName), wildcard);
}
if (!matches.equals(NIL)) {
String namestring = new String(pathname.getNamestring());
namestring = namestring.substring(0, namestring.lastIndexOf("!/") + 2)
+ entry.getName();
Pathname p = new Pathname(namestring);
result = new Cons(p, result);
}
}
return result;
}
if (pathname.isURL()) {
return error(new LispError("Unimplemented.")); // XXX
}
String s = pathname.getNamestring();
if (s != null) {
File f = new File(s);
if (f.isDirectory()) {
try {
File[] files = f.listFiles();
for (int i = files.length; i-- > 0;) {
File file = files[i];
Pathname p;
if (file.isDirectory()) {
p = Utilities.getDirectoryPathname(file);
} else {
if (arg2 != NIL) {
p = new Pathname(file.getCanonicalPath());
} else {
p = new Pathname(file.getAbsolutePath());
}
}
result = new Cons(p, result);
}
} catch (IOException e) {
return error(new FileError("Unable to list directory " + pathname.princToString() + ".",
pathname));
} catch (SecurityException e) {
Debug.trace(e);
} catch (NullPointerException e) {
Debug.trace(e);
}
}
}
return result;
}
}
@DocString(name="match-wild-jar-pathname",
args="wild-jar-pathname",
returns="pathnames",
doc="Returns the pathnames matching WILD-JAR-PATHNAME which is both wild and a jar-pathname.")
static final Primitive MATCH_WILD_JAR_PATHNAME = new pf_match_wild_jar_pathname();
private static class pf_match_wild_jar_pathname extends Primitive {
pf_match_wild_jar_pathname() {
super("match-wild-jar-pathname", PACKAGE_SYS, false, "wild-jar-pathname");
}
@Override
public LispObject execute(LispObject arg) {
Pathname pathname = coerceToPathname(arg);
if (pathname instanceof LogicalPathname) {
pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
}
if (!pathname.isJar()) {
return new FileError("Not a jar pathname.", pathname);
}
if (!pathname.isWild()) {
return new FileError("Not a wild pathname.", pathname);
}
Pathname jarPathname = new Pathname(pathname);
jarPathname.directory = NIL;
jarPathname.name = NIL;
jarPathname.type = NIL;
jarPathname.invalidateNamestring();
LispObject jarTruename = truename(jarPathname, false);
// We can't match anything in a non-existent jar
if (jarTruename == NIL) {
return NIL;
}
LispObject result = NIL;
String wild = "/" + pathname.asEntryPath();
final SimpleString wildcard = new SimpleString(wild);
if (pathname.device.cdr() instanceof Cons) {
ZipFile outerJar = ZipCache.get((Pathname)pathname.device.car());
String entryPath = ((Pathname)pathname.device.cdr().car()).getNamestring(); //???
if (entryPath.startsWith("/")) {
entryPath = entryPath.substring(1);
}
ZipEntry entry = outerJar.getEntry(entryPath);
InputStream inputStream = null;
try {
inputStream = outerJar.getInputStream(entry);
} catch (IOException e) {
return new FileError("Failed to read zip input stream inside zip.",
pathname);
}
ZipInputStream zipInputStream
= new ZipInputStream(inputStream);
try {
while ((entry = zipInputStream.getNextEntry()) != null) {
String entryName = "/" + entry.getName();
LispObject matches = Symbol.PATHNAME_MATCH_P
.execute(new SimpleString(entryName), wildcard);
if (!matches.equals(NIL)) {
String namestring = new String(pathname.getNamestring());
namestring = namestring.substring(0, namestring.lastIndexOf("!/") + 2)
+ entry.getName();
Pathname p = new Pathname(namestring);
result = new Cons(p, result);
}
}
} catch (IOException e) {
return new FileError("Failed to seek through zip inputstream inside zip.",
pathname);
}
} else {
ZipFile jar = ZipCache.get((Pathname)pathname.device.car());
for (Enumeration extends ZipEntry> entries = jar.entries();
entries.hasMoreElements();)
{
ZipEntry entry = entries.nextElement();
String entryName = "/" + entry.getName();
LispObject matches = Symbol.PATHNAME_MATCH_P
.execute(new SimpleString(entryName), wildcard);
if (!matches.equals(NIL)) {
String namestring = new String(pathname.getNamestring());
namestring = namestring.substring(0, namestring.lastIndexOf("!/") + 2)
+ entry.getName();
Pathname p = new Pathname(namestring);
result = new Cons(p, result);
}
}
}
return result;
}
}
public boolean isAbsolute() {
if (!directory.equals(NIL) || !(directory == null)) {
if (directory instanceof Cons) {
if (((Cons)directory).car().equals(Keyword.ABSOLUTE)) {
return true;
}
}
}
return false;
}
@DocString(name="pathname-jar-p",
args="pathname",
returns="generalized-boolean",
doc="Predicate functionfor whether PATHNAME references a jar.")
private static final Primitive PATHNAME_JAR_P = new pf_pathname_jar_p();
private static class pf_pathname_jar_p extends Primitive {
pf_pathname_jar_p() {
super("pathname-jar-p", PACKAGE_EXT, true);
}
@Override
public LispObject execute(LispObject arg) {
Pathname p = coerceToPathname(arg);
return p.isJar() ? T : NIL;
}
}
public boolean isJar() {
return (device instanceof Cons);
}
@DocString(name="pathname-url-p",
args="pathname",
returns="generalized-boolean",
doc="Predicate function for whether PATHNAME references a jaurl.")
private static final Primitive PATHNAME_URL_P = new pf_pathname_url_p();
private static class pf_pathname_url_p extends Primitive {
pf_pathname_url_p() {
super("pathname-url-p", PACKAGE_EXT, true, "pathname",
"Predicate for whether PATHNAME references a URL.");
}
@Override
public LispObject execute(LispObject arg) {
Pathname p = coerceToPathname(arg);
return p.isURL() ? T : NIL;
}
}
public boolean isURL() {
return (host instanceof Cons);
}
public boolean isWild() {
if (host == Keyword.WILD || host == Keyword.WILD_INFERIORS) {
return true;
}
if (device == Keyword.WILD || device == Keyword.WILD_INFERIORS) {
return true;
}
if (directory instanceof Cons) {
if (memq(Keyword.WILD, directory)) {
return true;
}
if (memq(Keyword.WILD_INFERIORS, directory)) {
return true;
}
Cons d = (Cons) directory;
while (true) {
if (d.car() instanceof AbstractString) {
String s = d.car().printObject();
if (s.contains("*")) {
return true;
}
}
if (d.cdr() == NIL || ! (d.cdr() instanceof Cons)) {
break;
}
d = (Cons)d.cdr();
}
}
if (name == Keyword.WILD || name == Keyword.WILD_INFERIORS) {
return true;
}
if (name instanceof AbstractString) {
if (name.printObject().contains("*")) {
return true;
}
}
if (type == Keyword.WILD || type == Keyword.WILD_INFERIORS) {
return true;
}
if (type instanceof AbstractString) {
if (type.printObject().contains("*")) {
return true;
}
}
if (version == Keyword.WILD || version == Keyword.WILD_INFERIORS) {
return true;
}
return false;
}
private static final Primitive _WILD_PATHNAME_P = new pf_wild_pathname_p();
@DocString(name="%wild-pathname-p",
args="pathname keyword",
returns="generalized-boolean",
doc="Predicate for determing whether PATHNAME contains wild components.\n"
+ "KEYWORD, if non-nil, should be one of :directory, :host, :device,\n"
+ ":name, :type, or :version indicating that only the specified component\n"
+ "should be checked for wildness.")
static final class pf_wild_pathname_p extends Primitive {
pf_wild_pathname_p() {
super("%wild-pathname-p", PACKAGE_SYS, true);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
Pathname pathname = coerceToPathname(first);
if (second == NIL) {
return pathname.isWild() ? T : NIL;
}
if (second == Keyword.DIRECTORY) {
if (pathname.directory instanceof Cons) {
if (memq(Keyword.WILD, pathname.directory)) {
return T;
}
if (memq(Keyword.WILD_INFERIORS, pathname.directory)) {
return T;
}
}
return NIL;
}
LispObject value;
if (second == Keyword.HOST) {
value = pathname.host;
} else if (second == Keyword.DEVICE) {
value = pathname.device;
} else if (second == Keyword.NAME) {
value = pathname.name;
} else if (second == Keyword.TYPE) {
value = pathname.type;
} else if (second == Keyword.VERSION) {
value = pathname.version;
} else {
return error(new ProgramError("Unrecognized keyword "
+ second.princToString() + "."));
}
if (value == Keyword.WILD || value == Keyword.WILD_INFERIORS) {
return T;
} else {
return NIL;
}
}
}
private static final Primitive MERGE_PATHNAMES = new pf_merge_pathnames();
@DocString(name="merge-pathnames",
args="pathname &optional default-pathname default-version",
returns="pathname",
doc="Constructs a pathname from PATHNAME by filling in any unsupplied components\n"
+ "with the corresponding values from DEFAULT-PATHNAME and DEFAULT-VERSION.")
static final class pf_merge_pathnames extends Primitive {
pf_merge_pathnames() {
super("merge-pathnames", "pathname &optional default-pathname default-version");
}
@Override
public LispObject execute(LispObject arg) {
Pathname pathname = coerceToPathname(arg);
Pathname defaultPathname =
coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue());
LispObject defaultVersion = Keyword.NEWEST;
return mergePathnames(pathname, defaultPathname, defaultVersion);
}
@Override
public LispObject execute(LispObject first, LispObject second) {
Pathname pathname = coerceToPathname(first);
Pathname defaultPathname =
coerceToPathname(second);
LispObject defaultVersion = Keyword.NEWEST;
return mergePathnames(pathname, defaultPathname, defaultVersion);
}
@Override
public LispObject execute(LispObject first, LispObject second,
LispObject third) {
Pathname pathname = coerceToPathname(first);
Pathname defaultPathname =
coerceToPathname(second);
LispObject defaultVersion = third;
return mergePathnames(pathname, defaultPathname, defaultVersion);
}
}
public static final Pathname mergePathnames(Pathname pathname, Pathname defaultPathname) {
return mergePathnames(pathname, defaultPathname, Keyword.NEWEST);
}
public static final Pathname mergePathnames(final Pathname pathname,
final Pathname defaultPathname,
final LispObject defaultVersion)
{
Pathname result;
Pathname p = new Pathname(pathname);
Pathname d;
if (pathname instanceof LogicalPathname) {
result = new LogicalPathname();
d = new Pathname(defaultPathname);
} else {
result = new Pathname();
if (defaultPathname instanceof LogicalPathname) {
d = LogicalPathname.translateLogicalPathname((LogicalPathname) defaultPathname);
} else {
d = new Pathname(defaultPathname);
}
}
if (pathname.host != NIL) {
result.host = p.host;
} else {
result.host = d.host;
}
if (pathname.device != NIL) { // XXX if device represent JARs we want to merge
result.device = p.device;
} else {
if (!p.isURL()) {
result.device = d.device;
}
}
if (pathname.isJar()) {
Cons jars = (Cons)result.device;
LispObject jar = jars.car;
if (jar instanceof Pathname) {
Pathname defaults = new Pathname(d);
if (defaults.isJar()) {
defaults.device = NIL;
}
Pathname o = mergePathnames((Pathname)jar, defaults);
if (o.directory instanceof Cons
&& ((Cons)o.directory).length() == 1) { // i.e. (:ABSOLUTE) or (:RELATIVE)
o.directory = NIL;
}
((Cons)result.device).car = o;
}
result.directory = p.directory;
} else {
result.directory = mergeDirectories(p.directory, d.directory);
}
if (pathname.name != NIL) {
result.name = p.name;
} else {
result.name = d.name;
}
if (pathname.type != NIL) {
result.type = p.type;
} else {
result.type = d.type;
}
// CLtLv2 MERGE-PATHNAMES
// "[T]he missing components in the given pathname are filled
// in from the defaults pathname, except that if no version is
// specified the default version is used."
// "The merging rules for the version are more complicated and
// depend on whether the pathname specifies a name. If the
// pathname doesn't specify a name, then the version, if not
// provided, will come from the defaults, just like the other
// components. However, if the pathname does specify a name,
// then the version is not affected by the defaults. The
// reason is that the version ``belongs to'' some other file
// name and is unlikely to have anything to do with the new
// one. Finally, if this process leaves the
// version missing, the default version is used."
if (p.version != NIL) {
result.version = p.version;
} else if (p.name == NIL) {
if (defaultPathname.version == NIL) {
result.version = defaultVersion;
} else {
result.version = defaultPathname.version;
}
} else if (defaultVersion == NIL) {
result.version = p.version;
}
if (result.version == NIL) {
result.version = defaultVersion;
}
if (pathname instanceof LogicalPathname) {
// When we're returning a logical
result.device = Keyword.UNSPECIFIC;
if (result.directory.listp()) {
LispObject original = result.directory;
LispObject canonical = NIL;
while (original != NIL) {
LispObject component = original.car();
if (component instanceof AbstractString) {
component = LogicalPathname.canonicalizeStringComponent((AbstractString) component);
}
canonical = canonical.push(component);
original = original.cdr();
}
result.directory = canonical.nreverse();
}
if (result.name instanceof AbstractString) {
result.name = LogicalPathname.canonicalizeStringComponent((AbstractString) result.name);
}
if (result.type instanceof AbstractString) {
result.type = LogicalPathname.canonicalizeStringComponent((AbstractString) result.type);
}
}
result.invalidateNamestring();
return result;
}
private static final LispObject mergeDirectories(LispObject dir,
LispObject defaultDir) {
if (dir == NIL) {
return defaultDir;
}
if (dir.car() == Keyword.RELATIVE && defaultDir != NIL) {
LispObject result = NIL;
while (defaultDir != NIL) {
result = new Cons(defaultDir.car(), result);
defaultDir = defaultDir.cdr();
}
dir = dir.cdr(); // Skip :RELATIVE.
while (dir != NIL) {
result = new Cons(dir.car(), result);
dir = dir.cdr();
}
LispObject[] array = result.copyToArray();
for (int i = 0; i < array.length - 1; i++) {
if (array[i] == Keyword.BACK) {
if (array[i + 1] instanceof AbstractString || array[i + 1] == Keyword.WILD) {
array[i] = null;
array[i + 1] = null;
}
}
}
result = NIL;
for (int i = 0; i < array.length; i++) {
if (array[i] != null) {
result = new Cons(array[i], result);
}
}
return result;
}
return dir;
}
public static final LispObject truename(Pathname pathname) {
return truename(pathname, false);
}
public static final LispObject truename(LispObject arg) {
return truename(arg, false);
}
public static final LispObject truename(LispObject arg, boolean errorIfDoesNotExist) {
final Pathname pathname = coerceToPathname(arg);
return truename(pathname, errorIfDoesNotExist);
}
/** @return The canonical TRUENAME as a Pathname if the pathname
* exists, otherwise returns NIL or possibly a subtype of
* LispError if there are logical problems with the input.
*/
public static final LispObject truename(Pathname pathname,
boolean errorIfDoesNotExist)
{
if (pathname instanceof LogicalPathname) {
pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
}
if (pathname.isWild()) {
return error(new FileError("Bad place for a wild pathname.",
pathname));
}
if (!(pathname.isJar() || pathname.isURL())) {
pathname
= mergePathnames(pathname,
coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue()),
NIL);
final String namestring = pathname.getNamestring();
if (namestring == null) {
return error(new FileError("Pathname has no namestring: "
+ pathname.princToString(),
pathname));
}
final File file = new File(namestring);
if (file.isDirectory()) {
return Utilities.getDirectoryPathname(file);
}
if (file.exists()) {
try {
return new Pathname(file.getCanonicalPath());
} catch (IOException e) {
return error(new FileError(e.getMessage(), pathname));
}
}
} else if (pathname.isURL()) {
if (pathname.getInputStream() != null) {
// If there is no type, query or fragment, we check to
// see if there is URL available "underneath".
if (pathname.name != NIL
&& pathname.type == NIL
&& Symbol.GETF.execute(pathname.host, QUERY, NIL) == NIL
&& Symbol.GETF.execute(pathname.host, FRAGMENT, NIL) == NIL) {
Pathname p = new Pathname(pathname.getNamestring() + "/");
if (p.getInputStream() != null) {
return p;
}
}
return pathname;
}
} else
jarfile: {
// Possibly canonicalize jar file directory
Cons jars = (Cons) pathname.device;
LispObject o = jars.car();
if (o instanceof Pathname && ! (((Pathname)o).isURL())) {
LispObject truename = Pathname.truename((Pathname)o, errorIfDoesNotExist);
if (truename != null
&& truename instanceof Pathname) {
Pathname truePathname = (Pathname)truename;
// A jar that is a directory makes no sense, so exit
if (truePathname.getNamestring().endsWith("/")) {
break jarfile;
}
jars.car = truePathname;
} else {
break jarfile;
}
}
// Check for existence of a JAR file and/or JarEntry
//
// Cases:
// 1. JAR
// 2. JAR in JAR
// 3. JAR with Entry
// 4. JAR in JAR with Entry
ZipFile jarFile = ZipCache.get((Pathname)jars.car());
String entryPath = pathname.asEntryPath();
if (jarFile != null) {
if (jars.cdr() instanceof Cons) {
Pathname inner = (Pathname) jars.cdr().car();
InputStream inputStream = Utilities.getInputStream(jarFile, inner);
if (inputStream != null) {
if (entryPath.length() == 0) {
return pathname; // Case 2
} else {
ZipInputStream zipInputStream
= new ZipInputStream(inputStream);
ZipEntry entry = Utilities.getEntry(zipInputStream,
entryPath,
false);
if (entry != null) {
// XXX this could possibly be a directory?
return pathname; // Case 4
}
}
}
} else {
if (entryPath.length() == 0) {
return pathname; // Case 1
} else {
ZipEntry entry = jarFile.getEntry(entryPath);
if (entry != null) {
// ensure this isn't a directory
if (entry.isDirectory()) {
break jarfile;
}
try {
InputStream input = jarFile.getInputStream(entry);
if (input != null) {
return pathname; // Case 3
}
} catch (IOException e) {
break jarfile;
}
}
}
}
}
}
error:
if (errorIfDoesNotExist) {
StringBuilder sb = new StringBuilder("The file ");
sb.append(pathname.princToString());
sb.append(" does not exist.");
return error(new FileError(sb.toString(), pathname));
}
return NIL;
}
protected static URL makeURL(Pathname pathname) {
URL result = null;
try {
if (pathname.isURL()) {
result = new URL(pathname.getNamestring());
} else {
// XXX Properly encode Windows drive letters and UNC paths
// XXX ensure that we have cannonical path?
result = new URL("file://" + pathname.getNamestring());
}
} catch (MalformedURLException e) {
Debug.trace("Could not form URL from " + pathname);
}
return result;
}
public InputStream getInputStream() {
InputStream result = null;
if (isJar()) {
String entryPath = asEntryPath();
// XXX We only return the bytes of an entry in a JAR
Debug.assertTrue(entryPath != null);
ZipFile jarFile = ZipCache.get((Pathname)device.car());
Debug.assertTrue(jarFile != null);
// Is this a JAR within a JAR?
if (device.cdr() instanceof Cons) {
Pathname inner = (Pathname) device.cdr().car();
InputStream input = Utilities.getInputStream(jarFile, inner);
ZipInputStream zipInputStream = new ZipInputStream(input);
result = Utilities.getEntryAsInputStream(zipInputStream, entryPath);
} else {
ZipEntry entry = jarFile.getEntry(entryPath);
if (entry == null) {
Debug.trace("Failed to get InputStream for "
+ "'" + getNamestring() + "'");
// XXX should this be fatal?
Debug.assertTrue(false);
}
try {
result = jarFile.getInputStream(entry);
} catch (IOException e) {
Debug.warn("Failed to get InputStream from "
+ "'" + getNamestring() + "'"
+ ": " + e);
}
}
} else if (isURL()) {
URL url = this.toURL();
try {
result = url.openStream();
} catch (IOException e) {
Debug.warn("Failed to get InputStream from "
+ "'" + getNamestring() + "'"
+ ": " + e);
}
} else {
File file = Utilities.getFile(this);
try {
result = new FileInputStream(file);
} catch (IOException e) {
Debug.warn("Failed to get InputStream from "
+ "'" + getNamestring() + "'"
+ ": " + e);
}
}
return result;
}
/** @return Time in milliseconds since the UNIX epoch at which the
* resource was last modified, or 0 if the time is unknown.
*/
public long getLastModified() {
if (!(isJar() || isURL())) {
File f = Utilities.getFile(this);
return f.lastModified();
}
if (isJar()) {
// JAR cases
// 0. JAR from URL
// 1. JAR
// 2. JAR in JAR
// 3. Entry in JAR
// 4. Entry in JAR in JAR
String entryPath = asEntryPath();
Cons d = (Cons)device;
if (d.cdr().equals(NIL)) {
if (entryPath.length() == 0) {
LispObject o = d.car();
// 0. JAR from URL
// 1. JAR
return ((Pathname)o).getLastModified();
} else {
// 3. Entry in JAR
final ZipEntry entry
= ZipCache.get((Pathname)device.car()).getEntry(entryPath);
if (entry == null) {
return 0;
}
final long time = entry.getTime();
if (time == -1) {
return 0;
}
return time;
}
} else {
ZipFile outerJar = ZipCache.get((Pathname)d.car());
if (entryPath.length() == 0) {
// 4. JAR in JAR
String jarPath = ((Pathname)d.cdr()).asEntryPath();
final ZipEntry entry = outerJar.getEntry(jarPath);
final long time = entry.getTime();
if (time == -1) {
return 0;
}
return time;
} else {
// 5. Entry in JAR in JAR
String innerJarPath = ((Pathname)d.cdr()).asEntryPath();
ZipEntry entry = outerJar.getEntry(entryPath);
ZipInputStream innerJarInputStream
= Utilities.getZipInputStream(outerJar, innerJarPath);
ZipEntry innerEntry = Utilities.getEntry(innerJarInputStream,
entryPath);
long time = innerEntry.getTime();
if (time == -1) {
return 0;
}
return time;
}
}
}
if (isURL()) {
return getURLConnection().getLastModified();
}
return 0;
}
private static final Primitive MKDIR = new pf_mkdir();
@DocString(name="mkdir",
args="pathname",
returns="generalized-boolean",
doc="Attempts to create directory at PATHNAME returning the success or failure.")
private static class pf_mkdir extends Primitive {
pf_mkdir() {
super("mkdir", PACKAGE_SYS, false, "pathname");
}
@Override
public LispObject execute(LispObject arg) {
final Pathname pathname = coerceToPathname(arg);
if (pathname.isWild()) {
error(new FileError("Bad place for a wild pathname.", pathname));
}
Pathname defaultedPathname =
mergePathnames(pathname,
coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue()),
NIL);
if (defaultedPathname.isURL() || defaultedPathname.isJar()) {
return new FileError("Cannot mkdir with a "
+ (defaultedPathname.isURL() ? "URL" : "jar")
+ " Pathname.",
defaultedPathname);
}
File file = Utilities.getFile(defaultedPathname);
return file.mkdir() ? T : NIL;
}
}
private static final Primitive RENAME_FILE = new pf_rename_file();
@DocString(name="rename-file",
args="filespec new-name",
returns="defaulted-new-name, old-truename, new-truename",
doc="rename-file modifies the file system in such a way that the file indicated by FILESPEC is renamed to DEFAULTED-NEW-NAME.")
private static class pf_rename_file extends Primitive {
pf_rename_file() {
super("rename-file", "filespec new-name");
}
@Override
public LispObject execute(LispObject first, LispObject second) {
final Pathname original = (Pathname) truename(first, true);
final String originalNamestring = original.getNamestring();
Pathname newName = coerceToPathname(second);
if (newName.isWild()) {
error(new FileError("Bad place for a wild pathname.", newName));
}
if (original.isJar()) {
error(new FileError("Bad place for a jar pathname.", original));
}
if (newName.isJar()) {
error(new FileError("Bad place for a jar pathname.", newName));
}
if (original.isURL()) {
error(new FileError("Bad place for a URL pathname.", original));
}
if (newName.isURL()) {
error(new FileError("Bad place for a jar pathname.", newName));
}
newName = mergePathnames(newName, original, NIL);
final String newNamestring;
if (newName instanceof LogicalPathname) {
newNamestring = LogicalPathname.translateLogicalPathname((LogicalPathname) newName).getNamestring();
} else {
newNamestring = newName.getNamestring();
}
if (originalNamestring != null && newNamestring != null) {
final File source = new File(originalNamestring);
final File destination = new File(newNamestring);
if (Utilities.isPlatformWindows) {
if (destination.isFile()) {
ZipCache.remove(destination);
destination.delete();
}
}
if (source.renameTo(destination)) { // Success!
return LispThread.currentThread().setValues(newName, original,
truename(newName, true));
}
}
return error(new FileError("Unable to rename "
+ original.princToString()
+ " to " + newName.princToString()
+ "."));
}
}
// TODO clarify uri encoding cases in implementation and document
private static final Primitive FILE_NAMESTRING = new pf_file_namestring();
@DocString(name="file-namestring",
args="pathname",
returns="namestring",
doc="Returns just the name, type, and version components of PATHNAME.")
private static class pf_file_namestring extends Primitive {
pf_file_namestring() {
super("file-namestring", "pathname");
}
@Override
public LispObject execute(LispObject arg) {
Pathname p = coerceToPathname(arg);
StringBuilder sb = new StringBuilder();
if (p.name instanceof AbstractString) {
sb.append(p.name.getStringValue());
} else if (p.name == Keyword.WILD) {
sb.append('*');
} else {
return NIL;
}
if (p.type instanceof AbstractString) {
sb.append('.');
sb.append(p.type.getStringValue());
} else if (p.type == Keyword.WILD) {
sb.append(".*");
}
return new SimpleString(sb);
}
}
private static final Primitive HOST_NAMESTRING = new pf_host_namestring();
@DocString(name="host-namestring",
args="pathname",
returns="namestring",
doc="Returns the host name of PATHNAME.")
private static class pf_host_namestring extends Primitive {
pf_host_namestring() {
super("host-namestring", "pathname");
}
@Override
public LispObject execute(LispObject arg) {
return coerceToPathname(arg).host; // XXX URL-PATHNAME
}
}
public URL toURL() {
try {
if (isURL()) {
return new URL(getNamestring());
} else {
return toFile().toURI().toURL();
}
} catch (MalformedURLException e) {
error(new LispError(getNamestring() + " is not a valid URL"));
return null; // not reached
}
}
public File toFile() {
if(!isURL()) {
return new File(getNamestring());
} else {
throw new RuntimeException(this + " does not represent a file");
}
}
static {
LispObject obj = Symbol.DEFAULT_PATHNAME_DEFAULTS.getSymbolValue();
Symbol.DEFAULT_PATHNAME_DEFAULTS.setSymbolValue(coerceToPathname(obj));
}
@DocString(name="uri-decode",
args="string",
returns="string",
doc="Decode STRING percent escape sequences in the manner of URI encodings.")
private static final Primitive URI_DECODE = new pf_uri_decode();
private static final class pf_uri_decode extends Primitive {
pf_uri_decode() {
super("uri-decode", PACKAGE_EXT, true);
}
@Override
public LispObject execute(LispObject arg) {
if (!(arg instanceof AbstractString)) {
return error(new TypeError(arg, Symbol.STRING));
}
String result = uriDecode(((AbstractString)arg).toString());
return new SimpleString(result);
}
};
static String uriDecode(String s) {
try {
URI uri = new URI("file://foo?" + s);
return uri.getQuery();
} catch (URISyntaxException e) {}
return null; // Error
}
@DocString(name="uri-encode",
args="string",
returns="string",
doc="Encode percent escape sequences in the manner of URI encodings.")
private static final Primitive URI_ENCODE = new pf_uri_encode();
private static final class pf_uri_encode extends Primitive {
pf_uri_encode() {
super("uri-encode", PACKAGE_EXT, true);
}
@Override
public LispObject execute(LispObject arg) {
if (!(arg instanceof AbstractString)) {
return error(new TypeError(arg, Symbol.STRING));
}
String result = uriEncode(((AbstractString)arg).toString());
return new SimpleString(result);
}
};
static String uriEncode(String s) {
// The constructor we use here only allows absolute paths, so
// we manipulate the input and output correspondingly.
String u;
if (!s.startsWith("/")) {
u = "/" + s;
} else {
u = new String(s);
}
try {
URI uri = new URI("file", "", u, "");
String result = uri.getRawPath();
if (!s.startsWith("/")) {
return result.substring(1);
}
return result;
} catch (URISyntaxException e) {
Debug.assertTrue(false);
}
return null; // Error
}
}