/*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.jrtfs;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.*;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import static java.nio.file.StandardOpenOption.*;
import static java.nio.file.StandardCopyOption.*;

Base class for Path implementation of jrt file systems.
Implementation Note:This class needs to maintain JDK 8 source compatibility. It is used internally in the JDK to implement jimage/jrtfs access, but also compiled and delivered as part of the jrtfs.jar to support access to the jimage file provided by the shipped JDK by tools running on JDK 8.
/** * Base class for Path implementation of jrt file systems. * * @implNote This class needs to maintain JDK 8 source compatibility. * * It is used internally in the JDK to implement jimage/jrtfs access, * but also compiled and delivered as part of the jrtfs.jar to support access * to the jimage file provided by the shipped JDK by tools running on JDK 8. */
final class JrtPath implements Path { final JrtFileSystem jrtfs; private final String path; private volatile int[] offsets; JrtPath(JrtFileSystem jrtfs, String path) { this.jrtfs = jrtfs; this.path = normalize(path); this.resolved = null; } JrtPath(JrtFileSystem jrtfs, String path, boolean normalized) { this.jrtfs = jrtfs; this.path = normalized ? path : normalize(path); this.resolved = null; } final String getName() { return path; } @Override public final JrtPath getRoot() { if (this.isAbsolute()) { return jrtfs.getRootPath(); } else { return null; } } @Override public final JrtPath getFileName() { if (path.isEmpty()) return this; if (path.length() == 1 && path.charAt(0) == '/') return null; int off = path.lastIndexOf('/'); if (off == -1) return this; return new JrtPath(jrtfs, path.substring(off + 1), true); } @Override public final JrtPath getParent() { initOffsets(); int count = offsets.length; if (count == 0) { // no elements so no parent return null; } int off = offsets[count - 1] - 1; if (off <= 0) { // parent is root only (may be null) return getRoot(); } return new JrtPath(jrtfs, path.substring(0, off)); } @Override public final int getNameCount() { initOffsets(); return offsets.length; } @Override public final JrtPath getName(int index) { initOffsets(); if (index < 0 || index >= offsets.length) { throw new IllegalArgumentException("index: " + index + ", offsets length: " + offsets.length); } int begin = offsets[index]; int end; if (index == (offsets.length - 1)) { end = path.length(); } else { end = offsets[index + 1]; } return new JrtPath(jrtfs, path.substring(begin, end)); } @Override public final JrtPath subpath(int beginIndex, int endIndex) { initOffsets(); if (beginIndex < 0 || endIndex > offsets.length || beginIndex >= endIndex) { throw new IllegalArgumentException( "beginIndex: " + beginIndex + ", endIndex: " + endIndex + ", offsets length: " + offsets.length); } // starting/ending offsets int begin = offsets[beginIndex]; int end; if (endIndex == offsets.length) { end = path.length(); } else { end = offsets[endIndex]; } return new JrtPath(jrtfs, path.substring(begin, end)); } @Override public final JrtPath toRealPath(LinkOption... options) throws IOException { return jrtfs.toRealPath(this, options); } @Override public final JrtPath toAbsolutePath() { if (isAbsolute()) return this; return new JrtPath(jrtfs, "/" + path, true); } @Override public final URI toUri() { try { return new URI("jrt", toAbsolutePath().path, null); } catch (URISyntaxException ex) { throw new AssertionError(ex); } } private boolean equalsNameAt(JrtPath other, int index) { int mbegin = offsets[index]; int mlen; if (index == (offsets.length - 1)) { mlen = path.length() - mbegin; } else { mlen = offsets[index + 1] - mbegin - 1; } int obegin = other.offsets[index]; int olen; if (index == (other.offsets.length - 1)) { olen = other.path.length() - obegin; } else { olen = other.offsets[index + 1] - obegin - 1; } if (mlen != olen) { return false; } int n = 0; while (n < mlen) { if (path.charAt(mbegin + n) != other.path.charAt(obegin + n)) { return false; } n++; } return true; } @Override public final JrtPath relativize(Path other) { final JrtPath o = checkPath(other); if (o.equals(this)) { return new JrtPath(jrtfs, "", true); } if (path.isEmpty()) { return o; } if (jrtfs != o.jrtfs || isAbsolute() != o.isAbsolute()) { throw new IllegalArgumentException( "Incorrect filesystem or path: " + other); } final String tp = this.path; final String op = o.path; if (op.startsWith(tp)) { // fast path int off = tp.length(); if (op.charAt(off - 1) == '/') return new JrtPath(jrtfs, op.substring(off), true); if (op.charAt(off) == '/') return new JrtPath(jrtfs, op.substring(off + 1), true); } int mc = this.getNameCount(); int oc = o.getNameCount(); int n = Math.min(mc, oc); int i = 0; while (i < n) { if (!equalsNameAt(o, i)) { break; } i++; } int dotdots = mc - i; int len = dotdots * 3 - 1; if (i < oc) { len += (o.path.length() - o.offsets[i] + 1); } StringBuilder sb = new StringBuilder(len); while (dotdots > 0) { sb.append(".."); if (sb.length() < len) { // no tailing slash at the end sb.append('/'); } dotdots--; } if (i < oc) { sb.append(o.path, o.offsets[i], o.path.length()); } return new JrtPath(jrtfs, sb.toString(), true); } @Override public JrtFileSystem getFileSystem() { return jrtfs; } @Override public final boolean isAbsolute() { return !path.isEmpty() && path.charAt(0) == '/'; } @Override public final JrtPath resolve(Path other) { final JrtPath o = checkPath(other); if (this.path.isEmpty() || o.isAbsolute()) { return o; } if (o.path.isEmpty()) { return this; } StringBuilder sb = new StringBuilder(path.length() + o.path.length() + 1); sb.append(path); if (path.charAt(path.length() - 1) != '/') sb.append('/'); sb.append(o.path); return new JrtPath(jrtfs, sb.toString(), true); } @Override public final Path resolveSibling(Path other) { Objects.requireNonNull(other, "other"); Path parent = getParent(); return (parent == null) ? other : parent.resolve(other); } @Override public final boolean startsWith(Path other) { if (!(Objects.requireNonNull(other) instanceof JrtPath)) return false; final JrtPath o = (JrtPath)other; final String tp = this.path; final String op = o.path; if (isAbsolute() != o.isAbsolute() || !tp.startsWith(op)) { return false; } int off = op.length(); if (off == 0) { return tp.isEmpty(); } // check match is on name boundary return tp.length() == off || tp.charAt(off) == '/' || off == 0 || op.charAt(off - 1) == '/'; } @Override public final boolean endsWith(Path other) { if (!(Objects.requireNonNull(other) instanceof JrtPath)) return false; final JrtPath o = (JrtPath)other; final JrtPath t = this; int olast = o.path.length() - 1; if (olast > 0 && o.path.charAt(olast) == '/') { olast--; } int last = t.path.length() - 1; if (last > 0 && t.path.charAt(last) == '/') { last--; } if (olast == -1) { // o.path.length == 0 return last == -1; } if ((o.isAbsolute() && (!t.isAbsolute() || olast != last)) || last < olast) { return false; } for (; olast >= 0; olast--, last--) { if (o.path.charAt(olast) != t.path.charAt(last)) { return false; } } return o.path.charAt(olast + 1) == '/' || last == -1 || t.path.charAt(last) == '/'; } @Override public final JrtPath resolve(String other) { return resolve(getFileSystem().getPath(other)); } @Override public final Path resolveSibling(String other) { return resolveSibling(getFileSystem().getPath(other)); } @Override public final boolean startsWith(String other) { return startsWith(getFileSystem().getPath(other)); } @Override public final boolean endsWith(String other) { return endsWith(getFileSystem().getPath(other)); } @Override public final JrtPath normalize() { String res = getResolved(); if (res == path) { // no change return this; } return new JrtPath(jrtfs, res, true); } private JrtPath checkPath(Path path) { Objects.requireNonNull(path); if (!(path instanceof JrtPath)) throw new ProviderMismatchException("path class: " + path.getClass()); return (JrtPath) path; } // create offset list if not already created private void initOffsets() { if (this.offsets == null) { int len = path.length(); // count names int count = 0; int off = 0; while (off < len) { char c = path.charAt(off++); if (c != '/') { count++; off = path.indexOf('/', off); if (off == -1) break; } } // populate offsets int[] offsets = new int[count]; count = 0; off = 0; while (off < len) { char c = path.charAt(off); if (c == '/') { off++; } else { offsets[count++] = off++; off = path.indexOf('/', off); if (off == -1) break; } } this.offsets = offsets; } } private volatile String resolved; final String getResolvedPath() { String r = resolved; if (r == null) { if (isAbsolute()) { r = getResolved(); } else { r = toAbsolutePath().getResolvedPath(); } resolved = r; } return r; } // removes redundant slashs, replace "\" to separator "/" // and check for invalid characters private static String normalize(String path) { int len = path.length(); if (len == 0) { return path; } char prevC = 0; for (int i = 0; i < len; i++) { char c = path.charAt(i); if (c == '\\' || c == '\u0000') { return normalize(path, i); } if (c == '/' && prevC == '/') { return normalize(path, i - 1); } prevC = c; } if (prevC == '/' && len > 1) { return path.substring(0, len - 1); } return path; } private static String normalize(String path, int off) { int len = path.length(); StringBuilder to = new StringBuilder(len); to.append(path, 0, off); char prevC = 0; while (off < len) { char c = path.charAt(off++); if (c == '\\') { c = '/'; } if (c == '/' && prevC == '/') { continue; } if (c == '\u0000') { throw new InvalidPathException(path, "Path: NUL character not allowed"); } to.append(c); prevC = c; } len = to.length(); if (len > 1 && to.charAt(len - 1) == '/') { to.deleteCharAt(len - 1); } return to.toString(); } // Remove DotSlash(./) and resolve DotDot (..) components private String getResolved() { int length = path.length(); if (length == 0 || (path.indexOf("./") == -1 && path.charAt(length - 1) != '.')) { return path; } else { return resolvePath(); } } private String resolvePath() { int length = path.length(); char[] to = new char[length]; int nc = getNameCount(); int[] lastM = new int[nc]; int lastMOff = -1; int m = 0; for (int i = 0; i < nc; i++) { int n = offsets[i]; int len = (i == offsets.length - 1) ? length - n : offsets[i + 1] - n - 1; if (len == 1 && path.charAt(n) == '.') { if (m == 0 && path.charAt(0) == '/') // absolute path to[m++] = '/'; continue; } if (len == 2 && path.charAt(n) == '.' && path.charAt(n + 1) == '.') { if (lastMOff >= 0) { m = lastM[lastMOff--]; // retreat continue; } if (path.charAt(0) == '/') { // "/../xyz" skip if (m == 0) to[m++] = '/'; } else { // "../xyz" -> "../xyz" if (m != 0 && to[m-1] != '/') to[m++] = '/'; while (len-- > 0) to[m++] = path.charAt(n++); } continue; } if (m == 0 && path.charAt(0) == '/' || // absolute path m != 0 && to[m-1] != '/') { // not the first name to[m++] = '/'; } lastM[++lastMOff] = m; while (len-- > 0) to[m++] = path.charAt(n++); } if (m > 1 && to[m - 1] == '/') m--; return (m == to.length) ? new String(to) : new String(to, 0, m); } @Override public final String toString() { return path; } @Override public final int hashCode() { return path.hashCode(); } @Override public final boolean equals(Object obj) { return obj instanceof JrtPath && this.path.equals(((JrtPath) obj).path); } @Override public final int compareTo(Path other) { final JrtPath o = checkPath(other); return path.compareTo(o.path); } @Override public final WatchKey register( WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) { Objects.requireNonNull(watcher, "watcher"); Objects.requireNonNull(events, "events"); Objects.requireNonNull(modifiers, "modifiers"); throw new UnsupportedOperationException(); } @Override public final WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) { return register(watcher, events, new WatchEvent.Modifier[0]); } @Override public final File toFile() { throw new UnsupportedOperationException(); } @Override public final Iterator<Path> iterator() { return new Iterator<Path>() { private int i = 0; @Override public boolean hasNext() { return (i < getNameCount()); } @Override public Path next() { if (i < getNameCount()) { Path result = getName(i); i++; return result; } else { throw new NoSuchElementException(); } } @Override public void remove() { throw new ReadOnlyFileSystemException(); } }; } // Helpers for JrtFileSystemProvider and JrtFileSystem final JrtPath readSymbolicLink() throws IOException { if (!jrtfs.isLink(this)) { throw new IOException("not a symbolic link"); } return jrtfs.resolveLink(this); } final boolean isHidden() { return false; } final void createDirectory(FileAttribute<?>... attrs) throws IOException { jrtfs.createDirectory(this, attrs); } final InputStream newInputStream(OpenOption... options) throws IOException { if (options.length > 0) { for (OpenOption opt : options) { if (opt != READ) { throw new UnsupportedOperationException("'" + opt + "' not allowed"); } } } return jrtfs.newInputStream(this); } final DirectoryStream<Path> newDirectoryStream(Filter<? super Path> filter) throws IOException { return new JrtDirectoryStream(this, filter); } final void delete() throws IOException { jrtfs.deleteFile(this, true); } final void deleteIfExists() throws IOException { jrtfs.deleteFile(this, false); } final JrtFileAttributes getAttributes(LinkOption... options) throws IOException { JrtFileAttributes zfas = jrtfs.getFileAttributes(this, options); if (zfas == null) { throw new NoSuchFileException(toString()); } return zfas; } final void setAttribute(String attribute, Object value, LinkOption... options) throws IOException { JrtFileAttributeView.setAttribute(this, attribute, value); } final Map<String, Object> readAttributes(String attributes, LinkOption... options) throws IOException { return JrtFileAttributeView.readAttributes(this, attributes, options); } final void setTimes(FileTime mtime, FileTime atime, FileTime ctime) throws IOException { jrtfs.setTimes(this, mtime, atime, ctime); } final FileStore getFileStore() throws IOException { // each JrtFileSystem only has one root (as requested for now) if (exists()) { return jrtfs.getFileStore(this); } throw new NoSuchFileException(path); } final boolean isSameFile(Path other) throws IOException { if (this == other || this.equals(other)) { return true; } if (other == null || this.getFileSystem() != other.getFileSystem()) { return false; } this.checkAccess(); JrtPath o = (JrtPath) other; o.checkAccess(); return this.getResolvedPath().equals(o.getResolvedPath()) || jrtfs.isSameFile(this, o); } final SeekableByteChannel newByteChannel(Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { return jrtfs.newByteChannel(this, options, attrs); } final FileChannel newFileChannel(Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { return jrtfs.newFileChannel(this, options, attrs); } final void checkAccess(AccessMode... modes) throws IOException { if (modes.length == 0) { // check if the path exists jrtfs.checkNode(this); // no need to follow link. the "link" node // is built from real node under "/module" } else { boolean w = false; for (AccessMode mode : modes) { switch (mode) { case READ: break; case WRITE: w = true; break; case EXECUTE: throw new AccessDeniedException(toString()); default: throw new UnsupportedOperationException(); } } jrtfs.checkNode(this); if (w && jrtfs.isReadOnly()) { throw new AccessDeniedException(toString()); } } } final boolean exists() { try { return jrtfs.exists(this); } catch (IOException x) {} return false; } final OutputStream newOutputStream(OpenOption... options) throws IOException { if (options.length == 0) { return jrtfs.newOutputStream(this, CREATE_NEW, WRITE); } return jrtfs.newOutputStream(this, options); } final void move(JrtPath target, CopyOption... options) throws IOException { if (this.jrtfs == target.jrtfs) { jrtfs.copyFile(true, this, target, options); } else { copyToTarget(target, options); delete(); } } final void copy(JrtPath target, CopyOption... options) throws IOException { if (this.jrtfs == target.jrtfs) { jrtfs.copyFile(false, this, target, options); } else { copyToTarget(target, options); } } private void copyToTarget(JrtPath target, CopyOption... options) throws IOException { boolean replaceExisting = false; boolean copyAttrs = false; for (CopyOption opt : options) { if (opt == REPLACE_EXISTING) { replaceExisting = true; } else if (opt == COPY_ATTRIBUTES) { copyAttrs = true; } } // attributes of source file BasicFileAttributes jrtfas = getAttributes(); // check if target exists boolean exists; if (replaceExisting) { try { target.deleteIfExists(); exists = false; } catch (DirectoryNotEmptyException x) { exists = true; } } else { exists = target.exists(); } if (exists) { throw new FileAlreadyExistsException(target.toString()); } if (jrtfas.isDirectory()) { // create directory or file target.createDirectory(); } else { try (InputStream is = jrtfs.newInputStream(this); OutputStream os = target.newOutputStream()) { byte[] buf = new byte[8192]; int n; while ((n = is.read(buf)) != -1) { os.write(buf, 0, n); } } } if (copyAttrs) { BasicFileAttributeView view = Files.getFileAttributeView(target, BasicFileAttributeView.class); try { view.setTimes(jrtfas.lastModifiedTime(), jrtfas.lastAccessTime(), jrtfas.creationTime()); } catch (IOException x) { try { target.delete(); // rollback? } catch (IOException ignore) {} throw x; } } } }