/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache 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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * * Origin Work at: https://github.com/gnodet/githubfs/blob/master/src/main/java/fr/gnodet/githubfs/GitHubPath.java */ package automately.core.file.nio; import automately.core.file.VirtualFile; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.Arrays; import java.util.Iterator; import java.util.NoSuchElementException; import static java.lang.System.arraycopy; public class UserFilePath implements Path { private final UserFileSystem fileSystem; private final byte[] path; private volatile int[] offsets; private volatile int hash = 0; private volatile byte[] resolved = null; UserFilePath(UserFileSystem fileSystem, VirtualFile file){ this.fileSystem = fileSystem; this.path = (file.pathAlias + file.name).getBytes(); } UserFilePath(UserFileSystem fileSystem, byte[] path) { this.fileSystem = fileSystem; this.path = path; } @Override public UserFileSystem getFileSystem() { return fileSystem; } @Override public boolean isAbsolute() { return (path.length > 0) && (path[0] == '/'); } @Override public UserFilePath getRoot() { return fileSystem.rootFilePath; } @Override public UserFilePath getFileName() { initOffsets(); int nbOffsets = offsets.length; if (nbOffsets == 0) { return this; } if (nbOffsets == 1 && path[0] != '/') { return this; } int offset = offsets[nbOffsets - 1]; int length = path.length - offset; byte[] path; if(this.path[offset + length - 1] == '/'){ path = new byte[length - 1]; arraycopy(this.path, offset, path, 0, length -1 ); } else { path = new byte[length]; arraycopy(this.path, offset, path, 0, length); } return new UserFilePath(fileSystem, path); } @Override public UserFilePath getParent() { initOffsets(); int nbOffsets = offsets.length; if (nbOffsets == 0) { return null; } int length = offsets[nbOffsets - 1]; if (length <= 0) { return getRoot(); } byte[] path = new byte[length]; arraycopy(this.path, 0, path, 0, length); if(path[length - 1] != '/'){ path[length - 1] = '/'; } return new UserFilePath(fileSystem, path); } @Override public int getNameCount() { initOffsets(); return offsets.length; } @Override public UserFilePath getName(int index) { initOffsets(); if (index < 0 || index >= offsets.length) { throw new IllegalArgumentException(); } int offset = this.offsets[index]; int length; if (index == offsets.length - 1) { length = path.length - offset; } else { length = offsets[index + 1] - offset - 1; } byte[] path = new byte[length]; arraycopy(this.path, offset, path, 0, length); return new UserFilePath(fileSystem, path); } @Override public UserFilePath subpath(int beginIndex, int endIndex) { initOffsets(); if ((beginIndex < 0) || (beginIndex >= this.offsets.length) || (endIndex > this.offsets.length) || (beginIndex >= endIndex)) { throw new IllegalArgumentException(); } int offset = this.offsets[beginIndex]; int length; if (endIndex == this.offsets.length) { length = this.path.length - offset; } else { length = this.offsets[endIndex] - offset - 1; } byte[] path = new byte[length]; arraycopy(this.path, offset, path, 0, length); return new UserFilePath(fileSystem, path); } @Override public boolean startsWith(Path other) { UserFilePath p1 = this; UserFilePath p2 = checkPath(other); if (p1.isAbsolute() != p2.isAbsolute() || p1.path.length < p2.path.length) { return false; } int length = p2.path.length; for (int idx = 0; idx < length; idx++) { if (p1.path[idx] != p2.path[idx]) { return false; } } return p1.path.length == p2.path.length || p2.path[length - 1] == '/' || p1.path[length] == '/'; } @Override public boolean startsWith(String other) { return startsWith(getFileSystem().getPath(other)); } @Override public boolean endsWith(Path other) { UserFilePath p1 = this; UserFilePath p2 = checkPath(other); int i1 = p1.path.length - 1; if (i1 > 0 && p1.path[i1] == '/') { i1--; } int i2 = p2.path.length - 1; if (i2 > 0 && p2.path[i2] == '/') { i2--; } if (i2 == -1) { return i1 == -1; } if ((p2.isAbsolute() && (!isAbsolute() || i2 != i1)) || (i1 < i2)) { return false; } for (; i2 >= 0; i1--) { if (p2.path[i2] != p1.path[i1]) { return false; } i2--; } return (p2.path[i2 + 1] == '/') || (i1 == -1) || (p1.path[i1] == '/'); } @Override public boolean endsWith(String other) { return endsWith(getFileSystem().getPath(other)); } @Override public UserFilePath normalize() { final int count = getNameCount(); if (count == 0) return this; boolean[] ignore = new boolean[count]; // true => ignore name int[] size = new int[count]; // length of name int remaining = count; // number of names remaining boolean hasDotDot = false; // has at least one .. boolean isAbsolute = isAbsolute(); // first pass: // 1. compute length of names // 2. mark all occurences of "." to ignore // 3. and look for any occurences of ".." for (int i=0; i<count; i++) { int begin = offsets[i]; int len; if (i == (offsets.length-1)) { len = path.length - begin; } else { len = offsets[i+1] - begin - 1; } size[i] = len; if (path[begin] == '.') { if (len == 1) { ignore[i] = true; // ignore "." remaining--; } else { if (path[begin+1] == '.') // ".." found hasDotDot = true; } } } // multiple passes to eliminate all occurences of name/.. if (hasDotDot) { int prevRemaining; do { prevRemaining = remaining; int prevName = -1; for (int i=0; i<count; i++) { if (ignore[i]) continue; // not a ".." if (size[i] != 2) { prevName = i; continue; } int begin = offsets[i]; if (path[begin] != '.' || path[begin+1] != '.') { prevName = i; continue; } // ".." found if (prevName >= 0) { // name/<ignored>/.. found so mark name and ".." to be // ignored ignore[prevName] = true; ignore[i] = true; remaining = remaining - 2; prevName = -1; } else { // Case: /<ignored>/.. so mark ".." as ignored if (isAbsolute) { boolean hasPrevious = false; for (int j=0; j<i; j++) { if (!ignore[j]) { hasPrevious = true; break; } } if (!hasPrevious) { // all proceeding names are ignored ignore[i] = true; remaining--; } } } } } while (prevRemaining > remaining); } // no redundant names if (remaining == count) return this; // corner case - all names removed if (remaining == 0) { return isAbsolute ? getRoot() : new UserFilePath(fileSystem, new byte[0]); } // compute length of result int len = remaining - 1; if (isAbsolute) len++; for (int i=0; i<count; i++) { if (!ignore[i]) len += size[i]; } byte[] result = new byte[len]; // copy names into result int pos = 0; if (isAbsolute) result[pos++] = '/'; for (int i=0; i<count; i++) { if (!ignore[i]) { System.arraycopy(path, offsets[i], result, pos, size[i]); pos += size[i]; if (--remaining > 0) { result[pos++] = '/'; } } } return new UserFilePath(fileSystem, result); } @Override public UserFilePath resolve(Path other) { UserFilePath p1 = this; UserFilePath p2 = checkPath(other); if (p2.isAbsolute()) { return p2; } byte[] result; if (p1.path[p1.path.length - 1] == '/') { result = new byte[p1.path.length + p2.path.length]; arraycopy(p1.path, 0, result, 0, p1.path.length); arraycopy(p2.path, 0, result, p1.path.length, p2.path.length); } else { result = new byte[p1.path.length + 1 + p2.path.length]; arraycopy(p1.path, 0, result, 0, p1.path.length); result[p1.path.length] = '/'; arraycopy(p2.path, 0, result, p1.path.length + 1, p2.path.length); } return new UserFilePath(fileSystem, result); } @Override public UserFilePath resolve(String other) { return resolve(getFileSystem().getPath(other)); } @Override public Path resolveSibling(Path other) { if (other == null) { throw new NullPointerException(); } UserFilePath parent = getParent(); return parent == null ? other : parent.resolve(other); } @Override public Path resolveSibling(String other) { return resolveSibling(getFileSystem().getPath(other)); } @Override public UserFilePath relativize(Path other) { UserFilePath p1 = this; UserFilePath p2 = checkPath(other); if (p2.equals(p1)) { return new UserFilePath(fileSystem, new byte[0]); } if (p1.isAbsolute() != p2.isAbsolute()) { throw new IllegalArgumentException(); } // Check how many segments are common int nbNames1 = p1.getNameCount(); int nbNames2 = p2.getNameCount(); int l = Math.min(nbNames1, nbNames2); int nbCommon = 0; while (nbCommon < l && equalsNameAt(p1, p2, nbCommon)) { nbCommon++; } int nbUp = nbNames1 - nbCommon; // Compute the resulting length int length = nbUp * 3 - 1; if (nbCommon < nbNames2) { length += p2.path.length - p2.offsets[nbCommon] + 1; } // Compute result byte[] result = new byte[length]; int idx = 0; while (nbUp-- > 0) { result[idx++] = '.'; result[idx++] = '.'; if (idx < length) { result[idx++] = '/'; } } // Copy remaining segments if (nbCommon < nbNames2) { arraycopy(p2.path, p2.offsets[nbCommon], result, idx, p2.path.length - p2.offsets[nbCommon]); } return new UserFilePath(fileSystem, result); } @Override public URI toUri() { return URI.create(new String(path)); } @Override public UserFilePath toAbsolutePath() { if (isAbsolute()) { return this; } byte[] result = new byte[path.length + 1]; result[0] = '/'; arraycopy(path, 0, result, 1, path.length); return new UserFilePath(fileSystem, result); } @Override public UserFilePath toRealPath(LinkOption... options) throws IOException { UserFilePath absolute = new UserFilePath(fileSystem, getResolvedPath()).toAbsolutePath(); fileSystem.provider().checkAccess(absolute); return absolute; } @Override public File toFile() { throw new UnsupportedOperationException(); } @Override public WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException { throw new UnsupportedOperationException(); } @Override public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException { throw new UnsupportedOperationException(); } @Override public Iterator<Path> iterator() { return new Iterator<Path>() { private int index = 0; public boolean hasNext() { return index < getNameCount(); } public Path next() { if (index < getNameCount()) { UserFilePath name = getName(index); index++; return name; } throw new NoSuchElementException(); } public void remove() { throw new UnsupportedOperationException(); } }; } @Override public int compareTo(Path paramPath) { UserFilePath p1 = this; UserFilePath p2 = checkPath(paramPath); byte[] a1 = p1.path; byte[] a2 = p2.path; int l1 = a1.length; int l2 = a2.length; for (int i = 0, l = Math.min(l1, l2); i < l; i++) { int b1 = a1[i] & 0xFF; int b2 = a2[i] & 0xFF; if (b1 != b2) { return b1 - b2; } } return l1 - l2; } private UserFilePath checkPath(Path paramPath) { if (paramPath == null) { throw new NullPointerException(); } if (!(paramPath instanceof UserFilePath)) { throw new ProviderMismatchException(); } return (UserFilePath) paramPath; } @Override public int hashCode() { int h = hash; if (h == 0) { h = hash = Arrays.hashCode(path); } return h; } @Override public boolean equals(Object obj) { return obj instanceof UserFilePath && ((UserFilePath) obj).fileSystem == fileSystem && compareTo((UserFilePath) obj) == 0; } @Override public String toString() { return new String(path, StandardCharsets.UTF_8); } public String toPathAlias() { String path = toString(); if(!path.endsWith("/")){ path += "/"; } return path; } private void initOffsets() { if (this.offsets == null) { int count = 0; int index = 0; while (index < path.length) { byte c = path[index++]; if (c != '/') { count++; while (index < path.length && path[index] != '/') { index++; } } } int[] result = new int[count]; count = 0; index = 0; while (index < path.length) { int m = path[index]; if (m == '/') { index++; } else { result[count++] = index++; while (index < path.length && path[index] != '/') { index++; } } } synchronized (this) { if (offsets == null) { offsets = result; } } } } private byte[] getResolvedPath() { byte[] r = resolved; if (r == null) { r = resolved = isAbsolute() ? getResolved() : toAbsolutePath().getResolvedPath(); } return r; } private byte[] getResolved() { if (path.length == 0) { return path; } for (byte c : path) { if (c == '.') { return doGetResolved(this); } } return path; } private static byte[] doGetResolved(UserFilePath p) { int nc = p.getNameCount(); byte[] path = p.path; int[] offsets = p.offsets; byte[] to = new byte[path.length]; 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) ? (path.length - n) : (offsets[i + 1] - n - 1); if (len == 1 && path[n] == (byte) '.') { if (m == 0 && path[0] == '/') // absolute path to[m++] = '/'; continue; } if (len == 2 && path[n] == '.' && path[n + 1] == '.') { if (lastMOff >= 0) { m = lastM[lastMOff--]; // retreat continue; } if (path[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[n++]; } continue; } if (m == 0 && path[0] == '/' || // absolute path m != 0 && to[m - 1] != '/') { // not the first name to[m++] = '/'; } lastM[++lastMOff] = m; while (len-- > 0) to[m++] = path[n++]; } if (m > 1 && to[m - 1] == '/') m--; return (m == to.length) ? to : Arrays.copyOf(to, m); } private static boolean equalsNameAt(UserFilePath p1, UserFilePath p2, int index) { int beg1 = p1.offsets[index]; int len1; if (index == p1.offsets.length - 1) { len1 = p1.path.length - beg1; } else { len1 = p1.offsets[index + 1] - beg1 - 1; } int beg2 = p2.offsets[index]; int len2; if (index == p2.offsets.length - 1) { len2 = p2.path.length - beg2; } else { len2 = p2.offsets[index + 1] - beg2 - 1; } if (len1 != len2) { return false; } for (int n = 0; n < len1; n++) { if (p1.path[beg1 + n] != p2.path[beg2 + n]) { return false; } } return true; } }