/***** BEGIN LICENSE BLOCK *****
* Version: EPL 1.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Eclipse Public
* License Version 1.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.eclipse.org/legal/epl-v10.html
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* Copyright (C) 2002-2004 Anders Bengtsson
* Copyright (C) 2002-2004 Jan Arne Petersen
* Copyright (C) 2004 Thomas E Enebo
* Copyright (C) 2004-2005 Charles O Nutter
* Copyright (C) 2004 Stefan Matthias Aust
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****/
package org.jruby;
import static org.jruby.RubyEnumerator.enumeratorize;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import jnr.posix.FileStat;
import org.jruby.anno.JRubyMethod;
import org.jruby.anno.JRubyClass;
import jnr.posix.util.Platform;
import org.jcodings.Encoding;
import org.jcodings.specific.UTF8Encoding;
import org.jruby.exceptions.RaiseException;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.Block;
import org.jruby.runtime.ClassIndex;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.Dir;
import org.jruby.util.JRubyFile;
import org.jruby.util.ByteList;
import static org.jruby.CompatVersion.*;
/**
* .The Ruby built-in class Dir.
*
* @author jvoegele
*/
@JRubyClass(name = "Dir", include = "Enumerable")
public class RubyDir extends RubyObject {
private RubyString path; // What we passed to the constructor for method 'path'
protected JRubyFile dir;
private long lastModified = Long.MIN_VALUE;
private String[] snapshot; // snapshot of contents of directory
private int pos; // current position in directory
private boolean isOpen = true;
private final static Encoding UTF8 = UTF8Encoding.INSTANCE;
public RubyDir(Ruby runtime, RubyClass type) {
super(runtime, type);
}
private static final ObjectAllocator DIR_ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new RubyDir(runtime, klass);
}
};
public static RubyClass createDirClass(Ruby runtime) {
RubyClass dirClass = runtime.defineClass("Dir", runtime.getObject(), DIR_ALLOCATOR);
runtime.setDir(dirClass);
dirClass.index = ClassIndex.DIR;
dirClass.setReifiedClass(RubyDir.class);
dirClass.includeModule(runtime.getEnumerable());
dirClass.defineAnnotatedMethods(RubyDir.class);
return dirClass;
}
private final void checkDir() {
testFrozen("Dir");
update();
if (!isOpen) throw getRuntime().newIOError("closed directory");
}
private void update() {
if (snapshot == null || dir.exists() && dir.lastModified() > lastModified) {
lastModified = dir.lastModified();
List snapshotList = new ArrayList();
snapshotList.add(".");
snapshotList.add("..");
snapshotList.addAll(getContents(dir));
snapshot = (String[]) snapshotList.toArray(new String[snapshotList.size()]);
}
}
/**
* Creates a new Dir. This method takes a snapshot of the
* contents of the directory at creation time, so changes to the contents
* of the directory will not be reflected during the lifetime of the
* Dir object returned, so a new Dir instance
* must be created to reflect changes to the underlying file system.
*/
@JRubyMethod(compat = RUBY1_8)
public IRubyObject initialize(IRubyObject arg) {
RubyString newPath = arg.convertToString();
path = newPath;
pos = 0;
String adjustedPath = RubyFile.adjustRootPathOnWindows(getRuntime(), newPath.toString(), null);
checkDirIsTwoSlashesOnWindows(getRuntime(), adjustedPath);
dir = JRubyFile.create(getRuntime().getCurrentDirectory(), adjustedPath);
List snapshotList = RubyDir.getEntries(getRuntime(), adjustedPath);
snapshot = (String[]) snapshotList.toArray(new String[snapshotList.size()]);
return this;
}
@JRubyMethod(name = "initialize", compat = RUBY1_9)
public IRubyObject initialize19(IRubyObject arg) {
return initialize(RubyFile.get_path(getRuntime().getCurrentContext(), arg));
}
// ----- Ruby Class Methods ----------------------------------------------------
private static List dirGlobs(ThreadContext context, String cwd, IRubyObject[] args, int flags) {
List dirs = new ArrayList();
for (int i = 0; i < args.length; i++) {
dirs.addAll(Dir.push_glob(cwd, globArgumentAsByteList(context, args[i]), flags));
}
return dirs;
}
private static IRubyObject asRubyStringList(Ruby runtime, List dirs) {
List allFiles = new ArrayList();
Encoding enc = runtime.getDefaultExternalEncoding();
if (enc == null) {
enc = UTF8;
}
for (ByteList dir : dirs) {
allFiles.add(RubyString.newString(runtime, dir, enc));
}
IRubyObject[] tempFileList = new IRubyObject[allFiles.size()];
allFiles.toArray(tempFileList);
return runtime.newArrayNoCopy(tempFileList);
}
private static String getCWD(Ruby runtime) {
try {
return new org.jruby.util.NormalizedFile(runtime.getCurrentDirectory()).getCanonicalPath();
} catch (Exception e) {
return runtime.getCurrentDirectory();
}
}
@JRubyMethod(name = "[]", required = 1, rest = true, meta = true)
public static IRubyObject aref(ThreadContext context, IRubyObject recv, IRubyObject[] args) {
Ruby runtime = context.runtime;
List dirs;
if (args.length == 1) {
Pattern pattern = Pattern.compile("file:(.*)!/(.*)");
String glob = args[0].toString();
Matcher matcher = pattern.matcher(glob);
if (matcher.find()) {
String jarFileName = matcher.group(1);
String jarUri = "file:" + jarFileName + "!/";
String fileGlobString = matcher.group(2);
String filePatternString = convertGlobToRegEx(fileGlobString);
Pattern filePattern = Pattern.compile(filePatternString);
try {
JarFile jarFile = new JarFile(jarFileName);
List allFiles = new ArrayList();
Enumeration entries = jarFile.entries();
while (entries.hasMoreElements()) {
String entry = entries.nextElement().getName();
String chomped_entry = entry.endsWith("/") ? entry.substring(0, entry.length() - 1) : entry;
if (filePattern.matcher(chomped_entry).find()) {
allFiles.add(RubyString.newString(runtime, jarUri + chomped_entry.toString()));
}
}
IRubyObject[] tempFileList = new IRubyObject[allFiles.size()];
allFiles.toArray(tempFileList);
return runtime.newArrayNoCopy(tempFileList);
} catch (IOException e) {
return runtime.newArrayNoCopy(new IRubyObject[0]);
}
}
dirs = Dir.push_glob(getCWD(runtime), globArgumentAsByteList(context, args[0]), 0);
} else {
dirs = dirGlobs(context, getCWD(runtime), args, 0);
}
return asRubyStringList(runtime, dirs);
}
private static ByteList globArgumentAsByteList(ThreadContext context, IRubyObject arg) {
if (context.runtime.is1_9()) return RubyFile.get_path(context, arg).getByteList();
return arg.convertToString().getByteList();
}
private static String convertGlobToRegEx(String line) {
line = line.trim();
StringBuilder sb = new StringBuilder(line.length());
sb.append("^");
boolean escaping = false;
int inCurlies = 0;
for (char currentChar : line.toCharArray()) {
switch (currentChar) {
case '*':
if (escaping)
sb.append("\\*");
else
sb.append("[^/]*");
escaping = false;
break;
case '?':
if (escaping)
sb.append("\\?");
else
sb.append('.');
escaping = false;
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
sb.append('\\');
sb.append(currentChar);
escaping = false;
break;
case '\\':
if (escaping) {
sb.append("\\\\");
escaping = false;
} else
escaping = true;
break;
case '{':
if (escaping) {
sb.append("\\{");
} else {
sb.append('(');
inCurlies++;
}
escaping = false;
break;
case '}':
if (inCurlies > 0 && !escaping) {
sb.append(')');
inCurlies--;
} else if (escaping)
sb.append("\\}");
else
sb.append("}");
escaping = false;
break;
case ',':
if (inCurlies > 0 && !escaping) {
sb.append('|');
} else if (escaping)
sb.append("\\,");
else
sb.append(",");
break;
default:
escaping = false;
sb.append(currentChar);
}
}
sb.append("$");
return sb.toString().replace("[^/]*[^/]*/", ".*").replace("[^/]*[^/]*", ".*");
}
/**
* Returns an array of filenames matching the specified wildcard pattern
* pat. If a block is given, the array is iterated internally
* with each filename is passed to the block in turn. In this case, Nil is
* returned.
*/
@JRubyMethod(required = 1, optional = 1, meta = true)
public static IRubyObject glob(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) {
Ruby runtime = context.runtime;
int flags = args.length == 2 ? RubyNumeric.num2int(args[1]) : 0;
List dirs;
IRubyObject tmp = args[0].checkArrayType();
if (tmp.isNil()) {
dirs = Dir.push_glob(runtime.getCurrentDirectory(), globArgumentAsByteList(context, args[0]), flags);
} else {
dirs = dirGlobs(context, getCWD(runtime), ((RubyArray) tmp).toJavaArray(), flags);
}
if (block.isGiven()) {
for (int i = 0; i < dirs.size(); i++) {
Encoding enc = runtime.getDefaultExternalEncoding();
if (enc == null) {
enc = UTF8;
}
block.yield(context, RubyString.newString(runtime, dirs.get(i), enc));
}
return runtime.getNil();
}
return asRubyStringList(runtime, dirs);
}
/**
* @return all entries for this Dir
*/
@JRubyMethod(name = "entries")
public RubyArray entries() {
return getRuntime().newArrayNoCopy(JavaUtil.convertJavaArrayToRuby(getRuntime(), snapshot));
}
/**
* Returns an array containing all of the filenames in the given directory.
*/
@JRubyMethod(name = "entries", meta = true, compat = RUBY1_8)
public static RubyArray entries(IRubyObject recv, IRubyObject path) {
return entriesCommon(recv.getRuntime(), path.convertToString().getUnicodeValue());
}
@JRubyMethod(name = "entries", meta = true, compat = RUBY1_9)
public static RubyArray entries19(ThreadContext context, IRubyObject recv, IRubyObject arg) {
return entriesCommon(context.runtime, RubyFile.get_path(context, arg).asJavaString());
}
@JRubyMethod(name = "entries", meta = true, compat = RUBY1_9)
public static RubyArray entries19(ThreadContext context, IRubyObject recv, IRubyObject arg, IRubyObject opts) {
// FIXME: do something with opts
return entriesCommon(context.runtime, RubyFile.get_path(context, arg).asJavaString());
}
private static RubyArray entriesCommon(Ruby runtime, String path) {
String adjustedPath = RubyFile.adjustRootPathOnWindows(runtime, path, null);
checkDirIsTwoSlashesOnWindows(runtime, adjustedPath);
Object[] files = getEntries(runtime, adjustedPath).toArray();
return runtime.newArrayNoCopy(JavaUtil.convertJavaArrayToRuby(runtime, files));
}
private static List getEntries(Ruby runtime, String path) {
if (!RubyFileTest.directory_p(runtime, RubyString.newString(runtime, path)).isTrue()) {
throw runtime.newErrnoENOENTError("No such directory: " + path);
}
if (path.startsWith("jar:")) path = path.substring(4);
if (path.startsWith("file:")) return entriesIntoAJarFile(runtime, path);
return entriesIntoADirectory(runtime, path);
}
private static List entriesIntoADirectory(Ruby runtime, String path) {
final JRubyFile directory = JRubyFile.create(runtime.getCurrentDirectory(), path);
List fileList = getContents(directory);
fileList.add(0, ".");
fileList.add(1, "..");
return fileList;
}
private static List entriesIntoAJarFile(Ruby runtime, String path) {
String file = path.substring(5);
int bang = file.indexOf('!');
if (bang == -1) {
return entriesIntoADirectory(runtime, path.substring(5));
}
if (bang == file.length() - 1) {
file = file + "/";
}
String jar = file.substring(0, bang);
String after = file.substring(bang + 2);
if (after.length() > 0 && after.charAt(after.length() - 1) != '/') {
after = after + "/";
}
JarFile jf;
try {
jf = new JarFile(jar);
} catch (IOException e) {
throw new RuntimeException("Valid JAR file expected", e);
}
List fileList = new ArrayList();
Enumeration