/***** BEGIN LICENSE BLOCK ***** * Version: EPL 2.0/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Eclipse Public * License Version 2.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-v20.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 java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Watchable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import jnr.posix.FileStat; import jnr.posix.POSIX; import jnr.posix.Passwd; import jnr.posix.util.Platform; import org.jcodings.Encoding; import org.jcodings.specific.USASCIIEncoding; import org.jruby.anno.JRubyMethod; import org.jruby.anno.JRubyClass; import org.jruby.api.Create; import org.jruby.exceptions.RaiseException; import org.jruby.runtime.Arity; import org.jruby.runtime.Block; import org.jruby.runtime.ClassIndex; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.backtrace.RubyStackTraceElement; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.*; import org.jruby.ast.util.ArgsUtil; import static org.jruby.RubyEnumerator.enumeratorize; import static org.jruby.RubyFile.filePathConvert; import static org.jruby.RubyString.UTF8; import static org.jruby.api.Access.dirClass; import static org.jruby.api.Access.encodingService; import static org.jruby.api.Access.objectClass; import static org.jruby.api.Check.checkEmbeddedNulls; import static org.jruby.api.Convert.asBoolean; import static org.jruby.api.Convert.asFixnum; import static org.jruby.api.Convert.toInt; import static org.jruby.api.Create.newSharedString; import static org.jruby.api.Create.newString; import static org.jruby.api.Define.defineClass; import static org.jruby.api.Error.argumentError; import static org.jruby.api.Error.notImplementedError; import static org.jruby.api.Error.runtimeError; import static org.jruby.api.Warn.warn; import static org.jruby.util.RubyStringBuilder.str; import static org.jruby.util.io.EncodingUtils.newExternalStringWithEncoding; /** * The Ruby built-in class Dir. * * @author jvoegele */ @JRubyClass(name = "Dir", include = "Enumerable") public class RubyDir extends RubyObject implements Closeable { private RubyString path; // What we passed to the constructor for method 'path' protected FileResource 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 Encoding encoding; public RubyDir(Ruby runtime, RubyClass type) { super(runtime, type); } public static RubyClass createDirClass(ThreadContext context, RubyClass Object, RubyModule Enumerable) { return defineClass(context, "Dir", Object, RubyDir::new). reifiedClass(RubyDir.class). classIndex(ClassIndex.DIR). include(context, Enumerable). defineMethods(context, RubyDir.class); } private void checkDir(ThreadContext context) { checkDirIgnoreClosed(context); if (!isOpen) throw context.runtime.newIOError("closed directory"); } private void checkDirIgnoreClosed(ThreadContext context) { testFrozen("Dir"); // update snapshot (if changed) : if (snapshot == null || dir.exists() && dir.lastModified() > lastModified) { lastModified = dir.lastModified(); snapshot = list(dir); } } /** * 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. * * @param context current context * @param path target path * @return a new Dir object */ @JRubyMethod(name = "initialize") public IRubyObject initialize(ThreadContext context, IRubyObject path) { return initializeCommon(context, path, context.nil); } /** * Like {@link #initialize(ThreadContext, IRubyObject)} but accepts an :encoding option. * * @param context current context * @param path target path * @param encOpts encoding options * @return a new Dir object */ @JRubyMethod(name = "initialize") public IRubyObject initialize(ThreadContext context, IRubyObject path, IRubyObject encOpts) { return initializeCommon(context, path, encOpts); } private RubyDir initializeCommon(ThreadContext context, IRubyObject pathArg, IRubyObject encOpts) { Encoding encoding = null; if (!encOpts.isNil()) { RubyHash opts = encOpts.convertToHash(); IRubyObject encodingArg = ArgsUtil.extractKeywordArg(context, opts, "encoding"); if (encodingArg != null && !encodingArg.isNil()) { encoding = encodingService(context).getEncodingFromObject(encodingArg); } } if (encoding == null) encoding = encodingService(context).getFileSystemEncoding(); RubyString newPath = checkEmbeddedNulls(context, RubyFile.get_path(context, pathArg)); this.path = newPath; this.pos = 0; this.encoding = encoding; String adjustedPath = RubyFile.getAdjustedPath(context, newPath); checkDirIsTwoSlashesOnWindows(context.runtime, adjustedPath); this.dir = JRubyFile.createResource(context, adjustedPath); this.snapshot = getEntries(context, dir, adjustedPath); return this; } // ----- Ruby Class Methods ---------------------------------------------------- private static ArrayList dirGlobs(ThreadContext context, String cwd, IRubyObject[] args, int flags, boolean sort) { ArrayList dirs = new ArrayList<>(args.length); for ( int i = 0; i < args.length; i++ ) { dirs.addAll(Dir.push_glob(context.runtime, cwd, globArgumentAsByteList(context, RubyFile.get_path(context, args[i])), flags, sort)); } return dirs; } private static RubyArray asRubyStringList(Ruby runtime, List dirs) { final int size = dirs.size(); if ( size == 0 ) return RubyArray.newEmptyArray(runtime); IRubyObject[] dirStrings = new IRubyObject[ size ]; for ( int i = 0; i < size; i++ ) { dirStrings[i] = RubyString.newStringNoCopy(runtime, dirs.get(i)); } return RubyArray.newArrayMayCopy(runtime, dirStrings); } private static String getCWD(Ruby runtime) { final String cwd = runtime.getCurrentDirectory(); // ^(uri|jar|file|classpath):([^:]*:)?//?.* if (cwd.startsWith("uri:") || cwd.startsWith("jar:") || cwd.startsWith("file:")) { // "classpath:" mapped into "uri:classloader:" return cwd; } try { // NOTE: likely not necessary as we already canonicalized while setting? return new JRubyFile(cwd).getCanonicalPath(); } catch (IOException e) { return cwd; } } private static final String[] BASE_KEYWORDS = new String[] { "base", "sort" }; private static final String[] BASE_FLAGS_KEYWORDS = new String[] { "base", "sort", "flags" }; private static class GlobOptions { String base = null; int flags = 0; boolean sort = true; } // returns null (no kwargs present), "" kwargs but no base key, "something" kwargs with base key (which might be ""). private static void globOptions(ThreadContext context, IRubyObject[] args, String[] keys, GlobOptions options) { Ruby runtime = context.runtime; if (args.length > 1) { IRubyObject tmp = TypeConverter.checkHashType(runtime, args[args.length - 1]); boolean processFlags = keys == BASE_FLAGS_KEYWORDS; if (tmp == context.nil) { if (processFlags) options.flags = toInt(context, args[1]); } else { IRubyObject[] rets = ArgsUtil.extractKeywordArgs(context, (RubyHash) tmp, keys); if (args.length == 3 && processFlags) options.flags = toInt(context, args[1]); if (processFlags && rets[2] != null) options.flags |= toInt(context, rets[2]); if (rets[1] != null) { if (!(rets[1] instanceof RubyBoolean)) { throw argumentError(context, str(runtime, "expected true or false as sort:", rets[1])); } options.sort = !runtime.getFalse().equals(rets[1]); // weirdly only explicit false is honored for sort. } if (rets[0] == null || rets[0].isNil()) { options.base = ""; return; } RubyString path = RubyFile.get_path(context, rets[0]); Encoding[] enc = {path.getEncoding()}; String base = path.getUnicodeValue(); // Deep down in glob things are unhappy if base is not absolute. if (!base.isEmpty()) { base = RubyFile.expandPath(context, base, enc, runtime.getCurrentDirectory(), true, false); } options.base = base; } if (processFlags) options.flags &= ~Dir.FNM_CASEFOLD; } } @JRubyMethod(name = "[]", rest = true, meta = true, keywords = true) public static IRubyObject aref(ThreadContext context, IRubyObject recv, IRubyObject[] args) { ThreadContext.resetCallInfo(context); Ruby runtime = context.runtime; GlobOptions options = new GlobOptions(); globOptions(context, args, BASE_KEYWORDS, options); List dirs; String base = options.base; if (args.length == 1) { String dir = base == null || base.isEmpty() ? runtime.getCurrentDirectory() : base; dirs = Dir.push_glob(runtime, dir, globArgumentAsByteList(context, args[0]), 0, options.sort); } else { IRubyObject[] arefArgs; if (base != null) { // kwargs are present arefArgs = ArraySupport.newCopy(args, args.length - 1); } else { arefArgs = args; base = ""; } String dir = base.isEmpty() ? runtime.getCurrentDirectory() : base; dirs = dirGlobs(context, dir, arefArgs, 0, options.sort); } return asRubyStringList(runtime, dirs); } private static ByteList globArgumentAsByteList(ThreadContext context, IRubyObject arg) { RubyString str; if (!(arg instanceof RubyString)) { str = RubyFile.get_path(context, arg); } else if (StringSupport.strNullCheck(arg)[0] == null) { throw argumentError(context, "nul-separated glob pattern is deprecated"); } else { str = (RubyString) arg; // FIXME: It is possible this can just be EncodingUtils.strCompatAndValid() but the spec says specifically it must be ascii compat which is more constrained than that method. if (!str.getEncoding().isAsciiCompatible()) { throw context.runtime.newEncodingCompatibilityError("incompatible character encodings: " + str.getEncoding() + " and " + USASCIIEncoding.INSTANCE); } } return filePathConvert(context, str).getByteList(); } /** * 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 = 2, checkArity = false, meta = true, keywords = true) public static IRubyObject glob(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { ThreadContext.resetCallInfo(context); Arity.checkArgumentCount(context, args, 1, 3); Ruby runtime = context.runtime; GlobOptions options = new GlobOptions(); globOptions(context, args, BASE_FLAGS_KEYWORDS, options); List dirs; String base = options.base; if (base != null && !base.isEmpty() && !(JRubyFile.createResource(context, base).exists())){ dirs = Collections.EMPTY_LIST; } else { IRubyObject tmp = args[0].checkArrayType(); String dir = base == null || base.isEmpty() ? runtime.getCurrentDirectory() : base; if (tmp.isNil()) { dirs = Dir.push_glob(runtime, dir, globArgumentAsByteList(context, args[0]), options.flags, options.sort); } else { dirs = dirGlobs(context, dir, ((RubyArray) tmp).toJavaArray(context), options.flags, options.sort); } } if (block.isGiven()) { for (int i = 0; i < dirs.size(); i++) { block.yield(context, newString(context, dirs.get(i))); } return context.nil; } return asRubyStringList(runtime, dirs); } @Deprecated(since = "10.0.0.0") public RubyArray entries() { return entries(getCurrentContext()); } /** * @return all entries for this Dir */ @JRubyMethod(name = "entries") public RubyArray entries(ThreadContext context) { return newEntryArray(context, snapshot, encoding, false); } /** * Returns an array containing all of the filenames in the given directory. */ @JRubyMethod(name = "entries", meta = true) public static RubyArray entries(ThreadContext context, IRubyObject recv, IRubyObject arg) { RubyString path = checkEmbeddedNulls(context, RubyFile.get_path(context, arg)); return entriesCommon(context, path, context.runtime.getDefaultFilesystemEncoding(), false); } @JRubyMethod(name = "entries", meta = true) public static RubyArray entries(ThreadContext context, IRubyObject recv, IRubyObject arg, IRubyObject opts) { RubyString path = checkEmbeddedNulls(context, RubyFile.get_path(context, arg)); Encoding encoding = getEncodingFromOpts(context, opts); return entriesCommon(context, path, encoding, false); } private static RubyArray entriesCommon(ThreadContext context, IRubyObject path, Encoding encoding, final boolean childrenOnly) { Ruby runtime = context.runtime; String adjustedPath = RubyFile.getAdjustedPath(context, path); checkDirIsTwoSlashesOnWindows(runtime, adjustedPath); FileResource directory = JRubyFile.createResource(context, adjustedPath); String[] files = getEntries(context, directory, adjustedPath); return newEntryArray(context, files, encoding, childrenOnly); } private static RubyArray newEntryArray(ThreadContext context, String[] files, Encoding encoding, boolean childrenOnly) { var result = Create.allocArray(context, files.length); for (String file : files) { if (childrenOnly) { // removeIf(f -> f.equals(".") || f.equals("..")); final int len = file.length(); if (len == 1 && file.charAt(0) == '.') continue; if (len == 2 && file.charAt(0) == '.' && file.charAt(1) == '.') continue; } result.append(context, newExternalStringWithEncoding(context.runtime, file, encoding)); } return result; } private static final String[] NO_FILES = StringSupport.EMPTY_STRING_ARRAY; private static String[] getEntries(ThreadContext context, FileResource dir, String path) { if (!dir.isDirectory()) { if (dir.exists()) { throw context.runtime.newErrnoENOTDIRError(path); } throw context.runtime.newErrnoENOENTError(path); } if (!dir.canRead()) throw context.runtime.newErrnoEACCESError(path); String[] list = dir.list(); return list == null ? NO_FILES : list; } // MRI behavior: just plain '//' or '\\\\' are considered illegal on Windows. private static void checkDirIsTwoSlashesOnWindows(Ruby runtime, String path) { if (Platform.IS_WINDOWS && ("//".equals(path) || "\\\\".equals(path))) { throw runtime.newErrnoEINVALError("Invalid argument - " + path); } } /** Changes the current directory to path */ @JRubyMethod(meta = true) public static IRubyObject chdir(ThreadContext context, IRubyObject recv, IRubyObject path, Block block) { return chdirCommon(context, block, checkEmbeddedNulls(context, RubyFile.get_path(context, path)), true); } /** Changes the current directory to path */ @JRubyMethod(meta = true) public static IRubyObject chdir(ThreadContext context, IRubyObject recv, Block block) { RubyHash env = context.runtime.getENV(); if (env.op_aref(context, newString(context, "LOG_DIR")).isNil() && env.op_aref(context, newString(context, "HOME")).isNil()){ throw argumentError(context, "HOME/LOGDIR not set"); } RubyString path = getHomeDirectoryPath(context); return chdirCommon(context, block, path, true); } private static IRubyObject chdirCommon(ThreadContext context, Block block, RubyString path, boolean yieldPath) { Ruby runtime = context.runtime; String adjustedPath = RubyFile.adjustRootPathOnWindows(runtime, path.asJavaString(), null); checkDirIsTwoSlashesOnWindows(runtime, adjustedPath); adjustedPath = getExistingDir(runtime, adjustedPath).canonicalPath(); if (runtime.getChdirThread() != null && context.getThread() != runtime.getChdirThread()) { throw runtimeError(context, "conflicting chdir during another chdir block"); } if(!block.isGiven() && runtime.getChdirThread() != null) { RubyStackTraceElement location = runtime.getChdirLocation(); warn(context, "conflicting chdir during another chdir block\n" + location.getFileName() + ":" + location.getLineNumber() + ": note: previous chdir was here"); } if (block.isGiven()) { runtime.setChdirThread(context.getThread()); final String oldCwd = runtime.getCurrentDirectory(); runtime.setCurrentDirectory(adjustedPath); try { return yieldPath ? block.yield(context, path) : block.yieldSpecific(context); } finally { runtime.setChdirThread(null); getExistingDir(runtime, oldCwd); // needed in case the block deleted the oldCwd runtime.setCurrentDirectory(oldCwd); } } runtime.setCurrentDirectory(adjustedPath); return asFixnum(context, 0); } @JRubyMethod(name = "fchdir", meta = true, notImplemented = true) public static IRubyObject fchdir(ThreadContext context, IRubyObject dirClass, IRubyObject ignored) { throw notImplementedError(context, "Dir.fchdir"); } @JRubyMethod(name = "chdir") public IRubyObject chdir(ThreadContext context, Block block) { return chdirCommon(context, block, checkEmbeddedNulls(context, RubyFile.get_path(context, path)), false); } @Deprecated(since = "10.0.0.0") public static IRubyObject chroot(IRubyObject recv, IRubyObject path) { return chroot(((RubyBasicObject) recv).getCurrentContext(), recv, path); } /** * Changes the root directory (only allowed by super user). Not available on all platforms. */ @JRubyMethod(name = "chroot", meta = true, notImplemented = true) public static IRubyObject chroot(ThreadContext context, IRubyObject recv, IRubyObject path) { throw context.runtime.newNotImplementedError("chroot not implemented: chroot is non-portable and is not supported."); } /** * Returns an array containing all of the filenames except for "." and ".." in the given directory. */ @JRubyMethod(name = "children") public RubyArray children(ThreadContext context) { return entriesCommon(context, path, encoding, true); } @JRubyMethod(name = "children", meta = true) public static RubyArray children(ThreadContext context, IRubyObject recv, IRubyObject arg) { return children(context, recv, arg, context.nil); } @JRubyMethod(name = "children", meta = true) public static RubyArray children(ThreadContext context, IRubyObject recv, IRubyObject arg, IRubyObject opts) { RubyDir dir = (RubyDir) ((RubyClass) recv).newInstance(context, arg, opts, Block.NULL_BLOCK); return dir.children(context); } @Deprecated(since = "10.0.0.0") public static IRubyObject rmdir(IRubyObject recv, IRubyObject path) { return rmdir(recv.getRuntime().getCurrentContext(), recv, path); } @Deprecated(since = "10.0.0.0") public static IRubyObject rmdir19(IRubyObject recv, IRubyObject path) { return rmdir(((RubyBasicObject) recv).getCurrentContext(), recv, path); } /** * Deletes the directory specified by path. The directory must be empty. */ @JRubyMethod(name = {"rmdir", "unlink", "delete"}, meta = true) public static IRubyObject rmdir(ThreadContext context, IRubyObject recv, IRubyObject path) { return rmdirCommon(context, checkEmbeddedNulls(context, RubyFile.get_path(context, path)).asJavaString()); } private static RubyFixnum rmdirCommon(ThreadContext context, String path) { JRubyFile directory = getDirForRmdir(context.runtime, path); // at this point, only thing preventing delete should be non-emptiness if (context.runtime.getPosix().rmdir(directory.toString()) < 0) { throw context.runtime.newErrnoENOTEMPTYError(path); } return asFixnum(context, 0); } /** * Executes the block once for each entry in the directory except * for "." and "..", passing the filename of each entry as parameter * to the block. */ @JRubyMethod(name = "each_child", meta = true) public static IRubyObject each_child(ThreadContext context, IRubyObject recv, IRubyObject arg, Block block) { return eachChildCommon(context, recv, RubyFile.get_path(context, arg), null, block); } /** * Executes the block once for each entry in the directory except * for "." and "..", passing the filename of each entry as parameter * to the block. */ @JRubyMethod(name = "each_child", meta = true) public static IRubyObject each_child(ThreadContext context, IRubyObject recv, IRubyObject arg, IRubyObject encOpts, Block block) { Encoding encoding = getEncodingFromOpts(context, encOpts); return eachChildCommon(context, recv, RubyFile.get_path(context, arg), encOpts, block); } /** * Executes the block once for each file in the directory specified by * path. */ @JRubyMethod(name = "foreach", meta = true) public static IRubyObject foreach(ThreadContext context, IRubyObject recv, IRubyObject path, Block block) { return foreachCommon(context, recv, RubyFile.get_path(context, path), null, block); } @JRubyMethod(name = "foreach", meta = true) public static IRubyObject foreach(ThreadContext context, IRubyObject recv, IRubyObject path, IRubyObject encOpts, Block block) { return foreachCommon(context, recv, RubyFile.get_path(context, path), encOpts, block); } private static Encoding getEncodingFromOpts(ThreadContext context, IRubyObject encOpts) { Encoding encoding = null; if (!encOpts.isNil()) { IRubyObject opts = ArgsUtil.getOptionsArg(context, encOpts); if (opts.isNil()) throw argumentError(context, 2, 1, 1); IRubyObject encodingArg = ArgsUtil.extractKeywordArg(context, (RubyHash) opts, "encoding"); if (encodingArg != null && !encodingArg.isNil()) { encoding = encodingService(context).getEncodingFromObject(encodingArg); } } if (encoding == null) encoding = context.runtime.getDefaultEncoding(); return encoding; } private static IRubyObject eachChildCommon(ThreadContext context, IRubyObject recv, RubyString path, IRubyObject encOpts, Block block) { if (block.isGiven()) { Encoding encoding = encOpts == null ? context.runtime.getDefaultEncoding() : getEncodingFromOpts(context, encOpts); RubyDir dir = (RubyDir) dirClass(context).newInstance(context, path, Block.NULL_BLOCK); dir.each_child(context, encoding, block); return context.nil; } return encOpts == null ? enumeratorize(context.runtime, recv, "each_child", path) : enumeratorize(context.runtime, recv, "each_child", path, encOpts); } private static IRubyObject foreachCommon(ThreadContext context, IRubyObject recv, RubyString path, IRubyObject encOpts, Block block) { if (block.isGiven()) { Encoding encoding = encOpts == null ? context.runtime.getDefaultEncoding() : getEncodingFromOpts(context, encOpts); RubyDir dir = (RubyDir) dirClass(context).newInstance(context, path, Block.NULL_BLOCK); dir.each(context, encoding, block); return context.nil; } return encOpts == null ? enumeratorize(context.runtime, recv, "foreach", path) : enumeratorize(context.runtime, recv, "foreach", path, encOpts); } @Deprecated(since = "10.0.0.0") public static RubyString getwd(IRubyObject recv) { return getwd(((RubyBasicObject) recv).getCurrentContext(), recv); } /** Returns the current directory. */ @JRubyMethod(name = {"getwd", "pwd"}, meta = true) public static RubyString getwd(ThreadContext context, IRubyObject recv) { return newFilesystemString(context, getCWD(context.runtime)); } /** * Returns the home directory of the current user or the named user if given. */ @JRubyMethod(name = "home", meta = true) public static IRubyObject home(ThreadContext context, IRubyObject recv) { return getHomeDirectoryPath(context); } @JRubyMethod(name = "home", meta = true) public static IRubyObject home(ThreadContext context, IRubyObject recv, IRubyObject user) { if (user == null || user.isNil() || user.toString().isEmpty()) { return getHomeDirectoryPath(context); } RubyString userString = user.convertToString(); userString.verifyAsciiCompatible(); return getHomeDirectoryPath(context, userString.toString()); } /** * Creates the directory specified by path. Note that the * mode parameter is provided only to support existing Ruby * code, and is ignored. */ @JRubyMethod(name = "mkdir", required = 1, optional = 1, checkArity = false, meta = true) public static IRubyObject mkdir(ThreadContext context, IRubyObject recv, IRubyObject... args) { Arity.checkArgumentCount(context, args, 1, 2); return mkdirCommon(context, RubyFile.get_path(context, args[0]).asJavaString(), args); } private static IRubyObject mkdirCommon(ThreadContext context, String path, IRubyObject[] args) { if (path.startsWith("uri:")) throw context.runtime.newErrnoEACCESError(path); path = dirFromPath(path, context.runtime); FileResource res = JRubyFile.createResource(context.runtime, path); if (res.exists()) throw context.runtime.newErrnoEEXISTError(path); String name = path.replace('\\', '/'); boolean startsWithDriveLetterOnWindows = RubyFile.startsWithDriveLetterOnWindows(name); // don't attempt to create a dir for drive letters if (startsWithDriveLetterOnWindows) { // path is just drive letter plus : if (path.length() == 2) return asFixnum(context, 0); // path is drive letter plus : plus leading or trailing / if (path.length() == 3 && (path.charAt(0) == '/' || path.charAt(2) == '/')) return asFixnum(context, 0); // path is drive letter plus : plus leading and trailing / if (path.length() == 4 && (path.charAt(0) == '/' && path.charAt(3) == '/')) return asFixnum(context, 0); } File newDir = res.unwrap(File.class); if (File.separatorChar == '\\') newDir = new File(newDir.getPath()); int mode = args.length == 2 ? toInt(context, args[1]) : 0777; if (context.runtime.getPosix().mkdir(newDir.getAbsolutePath(), mode) < 0) { // FIXME: This is a system error based on errno throw context.runtime.newSystemCallError("mkdir failed"); } return asFixnum(context, 0); } /** * Returns a new directory object for path. If a block is * provided, a new directory object is passed to the block, which closes the * directory object before terminating. */ @JRubyMethod(name = "open", meta = true) public static IRubyObject open(ThreadContext context, IRubyObject recv, IRubyObject path, Block block) { RubyDir directory = (RubyDir) dirClass(context).newInstance(context, path, Block.NULL_BLOCK); if (!block.isGiven()) return directory; try { return block.yield(context, directory); } finally { directory.close(); } } @JRubyMethod(name = "open", meta = true) public static IRubyObject open(ThreadContext context, IRubyObject recv, IRubyObject path, IRubyObject encOpts, Block block) { RubyDir directory = (RubyDir) dirClass(context).newInstance(context, path, encOpts, Block.NULL_BLOCK); if (!block.isGiven()) return directory; try { return block.yield(context, directory); } finally { directory.close(); } } // ----- Ruby Instance Methods ------------------------------------------------- /** * Closes the directory stream. */ @JRubyMethod(name = "close") public IRubyObject close(ThreadContext context) { // Make sure any read()s after close fail. checkDirIgnoreClosed(context); isOpen = false; return context.nil; } @Deprecated(since = "10.0.0.0") public final void close() { close(getCurrentContext()); } /** * Executes the block once for each entry in the directory. */ public IRubyObject each(ThreadContext context, Encoding enc, Block block) { checkDir(context); String[] contents = snapshot; for (pos = 0; pos < contents.length; pos++) { block.yield(context, RubyString.newString(context.runtime, contents[pos], enc)); } return this; } /** * Executes the block once for each entry in the directory. */ @JRubyMethod(name = "each") public IRubyObject each(ThreadContext context, Block block) { return block.isGiven() ? each(context, encoding, block) : enumeratorize(context.runtime, this, "each"); } @JRubyMethod(name = "each") public IRubyObject each(ThreadContext context, IRubyObject encOpts, Block block) { Encoding encoding = getEncodingFromOpts(context, encOpts); return block.isGiven() ? each(context, encoding, block) : enumeratorize(context.runtime, this, "each", encOpts); } /** * Executes the block once for each child in the directory * (i.e. all the directory entries except for "." and ".."). */ public IRubyObject each_child(ThreadContext context, Encoding enc, Block block) { checkDir(context); String[] contents = snapshot; for (pos = 0; pos < contents.length; pos++) { if (StringSupport.contentEquals(contents[pos], '.')) continue; /* current dir */ if (StringSupport.contentEquals(contents[pos], '.', '.')) continue; /* parent dir */ block.yield(context, RubyString.newString(context.runtime, contents[pos], enc)); } return this; } /** * Executes the block once for each child in the directory * (i.e. all the directory entries except for "." and ".."). */ public IRubyObject each_child(ThreadContext context, Block block) { return each_child(context, encoding, block); } @JRubyMethod(name = "each_child") public IRubyObject rb_each_child(ThreadContext context, Block block) { return block.isGiven() ? each_child(context, block) : enumeratorize(context.runtime, children(context), "each"); } @JRubyMethod public IRubyObject inspect(ThreadContext context) { StringBuilder part = new StringBuilder(); String cname = getMetaClass().getRealClass().getName(context); part.append("#<").append(cname).append(':'); if (path != null) { part.append(path.asJavaString()); } part.append('>'); return newString(context, part.toString()); } /** * Returns the current position in the directory. */ @JRubyMethod(name = {"tell", "pos"}) public RubyInteger tell(ThreadContext context) { checkDir(context); return asFixnum(context, pos); } @Deprecated(since = "10.0.0.0") public RubyInteger tell() { return tell(getCurrentContext()); } @Deprecated(since = "10.0.0.0") public IRubyObject seek(IRubyObject newPos) { return seek(getCurrentContext(), newPos); } /** * Moves to a position d. pos must be a value * returned by tell or 0. */ @JRubyMethod(name = "seek") public IRubyObject seek(ThreadContext context, IRubyObject newPos) { checkDir(context); set_pos(context, newPos); return this; } @Deprecated(since = "10.0.0.0") public IRubyObject set_pos(IRubyObject newPos) { return set_pos(getCurrentContext(), newPos); } @JRubyMethod(name = "pos=") public IRubyObject set_pos(ThreadContext context, IRubyObject newPos) { int pos2 = toInt(context, newPos); if (pos2 >= 0) this.pos = pos2; return newPos; } @JRubyMethod(name = {"path", "to_path"}) public IRubyObject path(ThreadContext context) { return path == null ? context.nil : path.strDup(context.runtime); } @JRubyMethod public IRubyObject to_path(ThreadContext context) { return path(context); } public String getPath() { return path == null ? null : path.asJavaString(); } @Deprecated(since = "10.0.0.0") public IRubyObject read() { return read(getCurrentContext()); } /** Returns the next entry from this directory. */ @JRubyMethod(name = "read") public IRubyObject read(ThreadContext context) { checkDir(context); final String[] snapshot = this.snapshot; if (pos >= snapshot.length) return context.nil; RubyString result = newExternalStringWithEncoding(context.runtime, snapshot[pos], encoding); pos++; return result; } @Deprecated(since = "10.0.0.0") public IRubyObject rewind() { return rewind(getCurrentContext()); } /** Moves position in this directory to the first entry. */ @JRubyMethod(name = "rewind") public IRubyObject rewind(ThreadContext context) { checkDir(context); pos = 0; return this; } @JRubyMethod(name = "empty?", meta = true) public static IRubyObject empty_p(ThreadContext context, IRubyObject recv, IRubyObject path) { RubyFileStat fileStat = context.runtime.newFileStat(RubyFile.get_path(context, path).asJavaString(), false); boolean isDirectory = fileStat.directory_p(context).isTrue(); return asBoolean(context, isDirectory && entries(context, recv, path).getLength() <= 2); } @JRubyMethod(name = "exist?", meta = true) public static IRubyObject exist(ThreadContext context, IRubyObject recv, IRubyObject arg) { // Capture previous exception if any. IRubyObject exception = context.getErrorInfo(); RubyString path = RubyFile.get_path(context, arg); try { return context.runtime.newFileStat(path.asJavaString(), false).directory_p(context); } catch (Exception e) { context.setErrorInfo(exception); // Restore $! return context.fals; } } @Deprecated(since = "10.0.0.0") public static IRubyObject exists_p(ThreadContext context, IRubyObject recv, IRubyObject arg) { if (context.runtime.warningsEnabled()) { context.runtime.getWarnings().warnDeprecatedAlternate("Dir.exists?", "Dir.exist?"); } return exist(context, recv, arg); } @JRubyMethod(name = "fileno", notImplemented = true) public IRubyObject fileno(ThreadContext context) { throw context.runtime.newNotImplementedError("Dir#fileno"); } @JRubyMethod(name = "for_fd", meta = true, notImplemented = true) public static IRubyObject for_fd(ThreadContext context, IRubyObject recv, IRubyObject ignored) { throw context.runtime.newNotImplementedError("Dir#for_fd"); } // ----- Helper Methods -------------------------------------------------------- /** Returns a Java File object for the specified path. If * path is not a directory, throws IOError. * * @param path path for which to return the File object. * @param mustExist is true the directory must exist. If false it must not. */ // split out - no longer used protected static FileResource getDir(final Ruby runtime, final String path, final boolean mustExist) { String dir = dirFromPath(path, runtime); FileResource result = JRubyFile.createResource(runtime, dir); if (mustExist && (result == null || !result.exists())) throw runtime.newErrnoENOENTError(dir); boolean isDirectory = result.isDirectory(); if (mustExist && !isDirectory) throw runtime.newErrnoENOTDIRError(path); if (!mustExist && isDirectory) throw runtime.newErrnoEEXISTError(dir); return result; } private static FileResource getExistingDir(final Ruby runtime, final String path) { FileResource result = JRubyFile.createResource(runtime, path); if (result == null || !result.exists()) throw runtime.newErrnoENOENTError(path); if (!result.isDirectory()) throw runtime.newErrnoENOTDIRError(path); return result; } /** * Similar to getDir, but performs different checks to match rmdir behavior. * @param runtime * @param path * @return */ protected static JRubyFile getDirForRmdir(final Ruby runtime, final String path) { String dir = dirFromPath(path, runtime); JRubyFile directory = JRubyFile.create(runtime.getCurrentDirectory(), dir); // Order is important here...File.exists() will return false if the parent // dir can't be read, so we check permissions first // no permission File parentFile = directory.getParentFile(); if (parentFile.exists() && ! parentFile.canWrite()) throw runtime.newErrnoEACCESError(path); // Since we transcode we depend on posix to lookup stat stuff since // java.io.File does not seem to cut it. A failed stat will throw ENOENT. FileStat stat = runtime.getPosix().stat(directory.toString()); // is not directory if (!stat.isDirectory()) throw runtime.newErrnoENOTDIRError(path); return directory; } private static String dirFromPath(final String path, final Ruby runtime) throws RaiseException { String dir = path; String[] pathParts = RubyFile.splitURI(path); if (pathParts != null) { if (pathParts[0].startsWith("file:") && pathParts[1].length() > 0 && pathParts[1].indexOf(".jar!/") == -1) { dir = pathParts[1]; } else { throw runtime.newErrnoENOTDIRError(dir); } } return dir; } private static String[] list(FileResource directory) { final String[] contents = directory.list(); // If an IO exception occurs (something odd, but possible) // A directory may return null. return contents == null ? NO_FILES : contents; } /** * @deprecated no longer used */ @Deprecated(since = "9.4-") protected static List getContents(FileResource directory) { final String[] contents = directory.list(); // If an IO exception occurs (something odd, but possible. A directory may return null. return contents != null ? Arrays.asList(contents) : Collections.emptyList(); } /** * @deprecated no longer used */ @Deprecated(since = "9.4-") protected static List getContents(FileResource directory, Ruby runtime) { final String[] contents = directory.list(); if (contents == null) return Collections.emptyList(); final List result = new ArrayList<>(contents.length); for (int i = 0; i < contents.length; i++) { result.add(runtime.newString(contents[i])); } return result; } /** * Returns the home directory of the specified user on the * system. If the home directory of the specified user cannot be found, * an ArgumentError it thrown. */ public static IRubyObject getHomeDirectoryPath(ThreadContext context, String user) { /* * TODO: This version is better than the hackish previous one. Windows * behavior needs to be defined though. I suppose this version * could be improved more too. * TODO: /etc/passwd is also inadequate for MacOSX since it does not * use /etc/passwd for regular user accounts */ POSIX posix = context.runtime.getNativePosix(); if (posix != null) { try { // try to use POSIX for this first Passwd passwd = context.runtime.getPosix().getpwnam(user); if (passwd != null) return newFilesystemString(context, passwd.getHome()); } catch (Exception e) { } } // fall back on other ways if (Platform.IS_WINDOWS) { if (user.equals(posix.getlogin())) return getHomeDirectoryPath(context); } else { String passwd; try { FileInputStream stream = new FileInputStream("/etc/passwd"); int readBytes = stream.available(); byte[] bytes = new byte[readBytes]; readBytes = stream.read(bytes); stream.close(); passwd = new String(bytes, 0, readBytes); } catch (IOException ioe) { return context.nil; } List rows = StringSupport.split(passwd, '\n'); for (int i = 0; i < rows.size(); i++) { List fields = StringSupport.split(rows.get(i), ':'); if (fields.get(0).equals(user)) { String home = fields.get(5); return newFilesystemString(context, home); } } } throw argumentError(context, "user " + user + " doesn't exist"); } private static RubyString newFilesystemString(ThreadContext context, String home) { return newExternalStringWithEncoding(context.runtime, home, context.runtime.getDefaultFilesystemEncoding()); } static final ByteList HOME = new ByteList(new byte[] {'H','O','M','E'}, false); public static RubyString getHomeDirectoryPath(ThreadContext context) { final RubyString homeKey = newSharedString(context, HOME); IRubyObject home = context.runtime.getENV().op_aref(context, homeKey); return getHomeDirectoryPath(context, home); } public static Optional getHomeFromEnv(Ruby runtime) { final RubyString homeKey = newSharedString(runtime.getCurrentContext(), HOME); final RubyHash env = runtime.getENV(); return !env.hasKey(homeKey) ? Optional.empty() : Optional.of(env.op_aref(runtime.getCurrentContext(), homeKey).toString()); } private static final ByteList user_home = new ByteList(new byte[] {'u','s','e','r','.','h','o','m','e'}, false); static RubyString getHomeDirectoryPath(ThreadContext context, IRubyObject home) { RubyHash env = context.runtime.getENV(); if (home == null || home == context.nil) { IRubyObject ENV_JAVA = objectClass(context).getConstant(context, "ENV_JAVA"); home = ENV_JAVA.callMethod(context, "[]", newString(context, user_home, UTF8)); } if (home == null || home == context.nil) home = env.op_aref(context, newString(context, "LOGDIR")); if (home == null || home == context.nil) throw argumentError(context, "user.home/LOGDIR not set"); return (RubyString) home.dup(); } @Override public T toJava(Class target) { if (target == File.class) { final String path = getPath(); return path == null ? null : (T) new File(path); } if (target == Path.class || target == Watchable.class) { final String path = getPath(); return path == null ? null : (T) FileSystems.getDefault().getPath(path); } return super.toJava(target); } @Deprecated(since = "10.0.0.0") public static IRubyObject chdir(ThreadContext context, IRubyObject recv, IRubyObject[] args, Block block) { return switch (args.length) { case 0 -> chdir(context, recv, block); case 1 -> chdir(context, recv, args[0], block); default -> throw argumentError(context, args.length, 0, 1); }; } }