blob: 39a22f640feda974d9766c6ffc673024435401e6 [file] [log] [blame]
/*
* 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.
*
* @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;
}
}
}
}