blob: 01ff002846cedd63963660e1a6f1a5aba759a5de [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed 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.
*/
package com.android.sched.vfs;
import com.google.common.base.Splitter;
import com.android.sched.util.config.HasKeyId;
import com.android.sched.util.config.MessageDigestFactory;
import com.android.sched.util.config.ThreadConfig;
import com.android.sched.util.config.expression.LongExpression;
import com.android.sched.util.config.id.BooleanPropertyId;
import com.android.sched.util.config.id.IntegerPropertyId;
import com.android.sched.util.config.id.MessageDigestPropertyId;
import com.android.sched.util.file.CannotCloseException;
import com.android.sched.util.file.CannotCreateFileException;
import com.android.sched.util.file.CannotDeleteFileException;
import com.android.sched.util.file.CannotGetModificationTimeException;
import com.android.sched.util.file.NoSuchFileException;
import com.android.sched.util.file.NotDirectoryException;
import com.android.sched.util.file.NotFileException;
import com.android.sched.util.file.Statusful;
import com.android.sched.util.file.StreamFileStatus;
import com.android.sched.util.file.WrongPermissionException;
import com.android.sched.util.location.ColumnAndLineLocation;
import com.android.sched.util.location.Location;
import com.android.sched.vfs.CaseInsensitiveFS.CaseInsensitiveVDir;
import com.android.sched.vfs.CaseInsensitiveFS.CaseInsensitiveVFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.attribute.FileTime;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
/**
* A filter implementation of a {@link VFS} which take a {@link VFS}, case insensitive or not, and
* store in it files and directories with their name encoded through a digest algorithm. With that
* filter, even if the real {@link VFS} is case insensitive, the filtered {@link VFS} acts as a case
* sensitive {@link VFS}.
*/
@HasKeyId
public class CaseInsensitiveFS extends BaseVFS<CaseInsensitiveVDir, CaseInsensitiveVFile> implements
VFS, Statusful {
static final String INDEX_NAME = "index";
static final String DEBUG_NAME = "index.dbg";
public static final IntegerPropertyId NB_GROUP = IntegerPropertyId
.create("sched.vfs.case-insensitive.group.count",
"Number of directory used to encode a path name").withMin(0).addDefaultValue(1);
public static final IntegerPropertyId SZ_GROUP = IntegerPropertyId
.create("sched.vfs.case-insensitive.group.size",
"Number of letters in directory name used to encode a path name")
.requiredIf(NB_GROUP.getValue().isGreater(LongExpression.getConstant(0))).withMin(0)
.addDefaultValue(2);
@Nonnull
public static final MessageDigestPropertyId ALGO = MessageDigestPropertyId.create(
"sched.vfs.case-insensitive.algo", "Algorithm used to encode a path name").addDefaultValue(
"SHA");
@Nonnull
public static final BooleanPropertyId DEBUG = BooleanPropertyId.create(
"sched.vfs.case-insensitive.debug",
"generate an index file '" + DEBUG_NAME + "' for debugging purpose").addDefaultValue(false);
@Nonnull
private static final char INDEX_SEPARATOR = '/';
@Nonnull
private static final Splitter splitter = Splitter.on(INDEX_SEPARATOR);
@Nonnegative
private final int numGroups;
@Nonnegative
private final int groupSize;
@Nonnull
private final MessageDigestFactory mdf;
private final boolean debug;
@Nonnull
private final CaseInsensitiveVDir root = new CaseInsensitiveVDir(this, null, "");
@Nonnull
private final Set<Capabilities> capabilities;
@Override
@Nonnull
public String getDescription() {
return "case insensitive wrapper";
}
static class CaseInsensitiveVDir extends InMemoryVDir {
@CheckForNull
protected final VDir parent;
CaseInsensitiveVDir(
@Nonnull BaseVFS<? extends InMemoryVDir, ? extends CaseInsensitiveVFile> vfs,
@CheckForNull VDir parent, @Nonnull String name) {
super(vfs, name);
this.parent = parent;
}
@Override
@Nonnull
public VPath getPath() {
if (parent != null) {
return parent.getPath().clone().appendPath(new VPath(name, '/'));
} else {
return VPath.ROOT;
}
}
@Override
@Nonnull
public BaseVFile getVFile(@Nonnull String name) throws NoSuchFileException,
NotFileException {
return vfs.getVFile(this, name);
}
@Override
@Nonnull
public BaseVDir getVDir(@Nonnull String name) throws NotDirectoryException,
NoSuchFileException {
return vfs.getVDir(this, name);
}
@Override
@Nonnull
public BaseVFile createVFile(@Nonnull String name) throws CannotCreateFileException {
return vfs.createVFile(this, name);
}
@Override
@Nonnull
public BaseVDir createVDir(@Nonnull String name) throws CannotCreateFileException {
return vfs.createVDir(this, name);
}
@Override
@Nonnull
public Collection<? extends BaseVElement> list() {
return vfs.list(this);
}
@CheckForNull
public VDir getParent() {
return parent;
}
}
static class CaseInsensitiveVFile extends ParentVFile {
@CheckForNull
private BaseVFile encodedFile;
CaseInsensitiveVFile(
@Nonnull BaseVFS<? extends InMemoryVDir, ? extends CaseInsensitiveVFile> vfs,
@Nonnull VDir parent, @Nonnull String name) {
super(vfs, parent, name);
}
private void setEncodedFile(@Nonnull BaseVFile encodedFile) {
this.encodedFile = encodedFile;
}
@CheckForNull
private BaseVFile getEncodedFile() {
return encodedFile;
}
@Override
public void delete() throws CannotDeleteFileException {
vfs.delete(this);
}
private void deleteFromCache() {
((InMemoryVDir) parent).internalDelete(name);
}
}
@Nonnull
private final BaseVFS<BaseVDir, BaseVFile> vfs;
private boolean used = false;
public CaseInsensitiveFS(@Nonnull VFS vfs) throws BadVFSFormatException {
this(vfs, ThreadConfig.get(NB_GROUP).intValue(), ThreadConfig.get(SZ_GROUP).intValue(),
ThreadConfig.get(ALGO), ThreadConfig.get(DEBUG).booleanValue());
}
@SuppressWarnings("unchecked")
public CaseInsensitiveFS(@Nonnull VFS vfs, int numGroups, int groupSize,
@Nonnull MessageDigestFactory mdf, boolean debug)
throws BadVFSFormatException {
this.vfs = (BaseVFS<BaseVDir, BaseVFile>) vfs;
Set<Capabilities> capabilities = EnumSet.copyOf(vfs.getCapabilities());
capabilities.add(Capabilities.CASE_SENSITIVE);
capabilities.add(Capabilities.UNIQUE_ELEMENT);
this.capabilities = Collections.unmodifiableSet(capabilities);
this.numGroups = numGroups;
this.groupSize = groupSize;
this.mdf = mdf;
this.debug = debug;
initVFS();
}
private void initVFS() throws BadVFSFormatException {
LineNumberReader reader = null;
VFile file = null;
try {
try {
file = vfs.getRootDir().getVFile(INDEX_NAME);
} catch (NoSuchFileException e) {
if (!vfs.getRootDir().isEmpty()) {
// If VFS is not empty, index file is missing
throw new BadVFSFormatException(this, vfs.getLocation(), e);
}
return;
} catch (NotFileException e) {
throw new BadVFSFormatException(this, vfs.getLocation(), e);
}
try {
reader = new LineNumberReader(new InputStreamReader(file.getInputStream()));
} catch (WrongPermissionException e) {
throw new BadVFSFormatException(this, vfs.getLocation(), e);
}
String line;
try {
while ((line = reader.readLine()) != null) {
if (line.charAt(1) != ':') {
throw new BadVFSFormatException(this, vfs.getLocation(),
new WrongFileFormatException(new ColumnAndLineLocation(file.getLocation(),
reader.getLineNumber())));
}
char type = line.charAt(0);
switch (type) {
case 'd':
loadVDir(line.substring(2));
break;
case 'f':
loadVFile(line.substring(2));
break;
default:
throw new BadVFSFormatException(this, vfs.getLocation(),
new WrongFileFormatException(new ColumnAndLineLocation(file.getLocation(),
reader.getLineNumber())));
}
}
} catch (NotDirectoryException | NotFileException e) {
throw new BadVFSFormatException(this, vfs.getLocation(), e);
} catch (IOException e) {
throw new BadVFSFormatException(this, vfs.getLocation(), e);
}
} finally {
if (reader != null) {
assert file != null;
try {
reader.close();
} catch (IOException e) {
// Ignore
}
}
}
}
private void loadVDir(@Nonnull String path) throws NotDirectoryException {
CaseInsensitiveVDir currentDir = getRootDir();
Iterator<String> pathElementIterator = splitter.split(path).iterator();
String pathElement = null;
while (pathElementIterator.hasNext()) {
pathElement = pathElementIterator.next();
assert !pathElement.isEmpty();
currentDir = loadVDir(currentDir, pathElement);
}
}
private void loadVFile(@Nonnull String path)
throws NotDirectoryException, NotFileException {
CaseInsensitiveVDir currentDir = getRootDir();
Iterator<String> pathElementIterator = splitter.split(path).iterator();
String pathElement = null;
while (pathElementIterator.hasNext()) {
pathElement = pathElementIterator.next();
assert !pathElement.isEmpty();
if (pathElementIterator.hasNext()) {
// simpleName is a dir name
currentDir = loadVDir(currentDir, pathElement);
}
}
loadVFile(currentDir, pathElement);
}
@Override
@Nonnull
public Set<Capabilities> getCapabilities() {
return capabilities;
}
@Override
@Nonnull
public Location getLocation() {
return vfs.getLocation();
}
@Override
@Nonnull
public String getPath() {
return vfs.getPath();
}
@Override
@Nonnull
public CaseInsensitiveVDir getRootDir() {
used = true;
return root;
}
@Override
public synchronized void close() throws CannotCloseException {
if (!closed) {
try {
PrintStream printer =
new PrintStream(vfs.getRootDir().createVFile(INDEX_NAME).getOutputStream());
printIndex(printer, getRootDir());
printer.flush();
printer.close();
if (debug) {
printer = new PrintStream(vfs.getRootDir().createVFile(DEBUG_NAME).getOutputStream());
printDebug(printer, getRootDir());
printer.flush();
printer.close();
}
} catch (WrongPermissionException | CannotCreateFileException e) {
throw new CannotCloseException(this, e);
}
vfs.close();
closed = true;
}
}
private void printIndex(@Nonnull PrintStream printer, @Nonnull InMemoryVDir dir) {
Collection<? extends BaseVElement> elements = dir.list();
if (elements.size() > 0) {
for (BaseVElement element : elements) {
if (element.isVDir()) {
printIndex(printer, (InMemoryVDir) element);
} else {
CaseInsensitiveVFile file = (CaseInsensitiveVFile) element;
printer.print("f:");
printer.print(file.getPath().getPathAsString(INDEX_SEPARATOR));
printer.println();
}
}
} else {
printer.print("d:");
printer.print(dir.getPath().getPathAsString(INDEX_SEPARATOR));
printer.println();
}
}
private void printDebug(@Nonnull PrintStream printer, @Nonnull InMemoryVDir dir) {
Collection<? extends BaseVElement> elements = dir.list();
printer.print("d:");
printer.print(dir.getPath().getPathAsString(File.separatorChar));
printer.println();
for (BaseVElement element : elements) {
if (element.isVDir()) {
printDebug(printer, (InMemoryVDir) element);
} else {
CaseInsensitiveVFile file = (CaseInsensitiveVFile) element;
printer.print("f:");
printer.print(loadAndGetEncodedFile(file).getPath().getPathAsString(File.separatorChar));
printer.print(":");
printer.print(file.getPath().getPathAsString(File.separatorChar));
printer.println();
}
}
}
//
// Stream
//
@Nonnull
private BaseVFile loadAndGetEncodedFile(@Nonnull CaseInsensitiveVFile file) {
BaseVFile encodedFile = file.getEncodedFile();
if (encodedFile == null) {
try {
encodedFile = vfs.getRootDir().getVFile(encode(file.getPath()));
file.setEncodedFile(encodedFile);
} catch (NotDirectoryException | NotFileException | NoSuchFileException e) {
throw new RuntimeBadVFSFormatException(vfs, vfs.getLocation(), e);
}
}
return encodedFile;
}
@Override
@Nonnull
InputStream openRead(@Nonnull CaseInsensitiveVFile file) throws WrongPermissionException {
assert !isClosed();
return loadAndGetEncodedFile(file).getInputStream();
}
@Override
@Nonnull
OutputStream openWrite(@Nonnull CaseInsensitiveVFile file) throws WrongPermissionException {
return openWrite(file, false);
}
@Override
@Nonnull
OutputStream openWrite(@Nonnull CaseInsensitiveVFile file, boolean append)
throws WrongPermissionException {
assert !isClosed();
return loadAndGetEncodedFile(file).getOutputStream(append);
}
//
// VElement
//
@Override
@Nonnull
CaseInsensitiveVDir getVDir(@Nonnull CaseInsensitiveVDir parent, @Nonnull String name)
throws NotDirectoryException, NoSuchFileException {
CaseInsensitiveVDir vDir = getVDirFromCache(parent, name);
if (vDir != null) {
return vDir;
} else {
throw new NoSuchFileException(getVDirLocation(parent, name));
}
}
@Override
@Nonnull
CaseInsensitiveVFile getVFile(@Nonnull CaseInsensitiveVDir parent, @Nonnull String name)
throws NotFileException, NoSuchFileException {
CaseInsensitiveVFile vFile = getVFileFromCache(parent, name);
if (vFile != null) {
return vFile;
} else {
throw new NoSuchFileException(getVFileLocation(parent, name));
}
}
@CheckForNull
CaseInsensitiveVFile getVFileFromCache(@Nonnull CaseInsensitiveVDir parent, @Nonnull String name)
throws NotFileException {
BaseVElement element = parent.getFromCache(name);
if (element == null) {
return null;
} else if (!element.isVDir()) {
return (CaseInsensitiveVFile) element;
} else {
throw new NotFileException(getVFileLocation(parent, name));
}
}
@CheckForNull
CaseInsensitiveVDir getVDirFromCache(@Nonnull CaseInsensitiveVDir parent, @Nonnull String name)
throws NotDirectoryException {
BaseVElement element = parent.getFromCache(name);
if (element == null) {
return null;
} else if (element.isVDir()) {
return (CaseInsensitiveVDir) element;
} else {
throw new NotDirectoryException(getVDirLocation(parent, name));
}
}
@Override
@Nonnull
CaseInsensitiveVDir createVDir(@Nonnull CaseInsensitiveVDir parent,
@Nonnull String name) throws CannotCreateFileException {
assert !isClosed();
try {
return loadVDir(parent, name);
} catch (NotDirectoryException e) {
throw new CannotCreateFileException(getVDirLocation(parent, name));
}
}
@Override
@Nonnull
synchronized CaseInsensitiveVFile createVFile(
@Nonnull CaseInsensitiveVDir parent, @Nonnull String name) throws CannotCreateFileException {
assert !isClosed();
try {
CaseInsensitiveVFile vFile = getVFileFromCache(parent, name);
if (vFile != null) {
return vFile;
} else {
CaseInsensitiveVFile original = new CaseInsensitiveVFile(this, parent, name);
BaseVFile encoded = vfs.getRootDir().createVFile(encode(original.getPath()));
original.setEncodedFile(encoded);
parent.putInCache(name, original);
return original;
}
} catch (NotFileException e) {
throw new CannotCreateFileException(getVFileLocation(parent, name));
}
}
@Nonnull
synchronized CaseInsensitiveVDir loadVDir(
@Nonnull CaseInsensitiveVDir parent, @Nonnull String name) throws NotDirectoryException {
assert !isClosed();
CaseInsensitiveVDir vDir = getVDirFromCache(parent, name);
if (vDir != null) {
return vDir;
} else {
CaseInsensitiveVDir dir = new CaseInsensitiveVDir(this, parent, name);
parent.putInCache(name, dir);
return dir;
}
}
@Nonnull
synchronized CaseInsensitiveVFile loadVFile(
@Nonnull CaseInsensitiveVDir parent, @Nonnull String name) throws NotFileException {
assert !isClosed();
CaseInsensitiveVFile vFile = getVFileFromCache(parent, name);
if (vFile != null) {
return vFile;
} else {
CaseInsensitiveVFile original = new CaseInsensitiveVFile(this, parent, name);
parent.putInCache(name, original);
return original;
}
}
@Override
@Nonnull
void delete(@Nonnull CaseInsensitiveVFile file) throws CannotDeleteFileException {
assert !isClosed();
try {
BaseVFile encoded = vfs.getRootDir().getVFile(encode(file.getPath()));
vfs.delete(encoded);
file.deleteFromCache();
} catch (NotDirectoryException e) {
throw new CannotDeleteFileException(file);
} catch (NotFileException e) {
throw new CannotDeleteFileException(file);
} catch (NoSuchFileException e) {
throw new CannotDeleteFileException(file);
}
}
@Override
@Nonnull
Collection<? extends BaseVElement> list(@Nonnull CaseInsensitiveVDir dir) {
return dir.getAllFromCache();
}
@Override
boolean isEmpty(@Nonnull CaseInsensitiveVDir dir) {
return dir.isEmpty();
}
@Override
@Nonnull
public FileTime getLastModified(@Nonnull CaseInsensitiveVFile file)
throws CannotGetModificationTimeException {
return vfs.getLastModified(loadAndGetEncodedFile(file));
}
//
// Location
//
@Override
@Nonnull
Location getVFileLocation(@Nonnull CaseInsensitiveVFile file) {
return vfs.getVFileLocation(loadAndGetEncodedFile(file));
}
@Override
@Nonnull
Location getVFileLocation(@Nonnull CaseInsensitiveVDir parent, @Nonnull String name) {
return vfs.getRootDir().getVFileLocation(
encode(parent.getPath().clone().appendPath(new VPath(name, '/'))));
}
@Override
@Nonnull
Location getVDirLocation(@Nonnull CaseInsensitiveVDir dir) {
return vfs.getRootDir().getVDirLocation(encode(dir.getPath()));
}
@Override
@Nonnull
Location getVDirLocation(@Nonnull CaseInsensitiveVDir parent, @Nonnull String name) {
return vfs.getRootDir().getVDirLocation(
encode(parent.getPath().clone().appendPath(new VPath(name, '/'))));
}
@Override
@Nonnull
Location getVFileLocation(@Nonnull CaseInsensitiveVDir parent, @Nonnull VPath path) {
return vfs.getRootDir().getVFileLocation(encode(parent.getPath().clone().appendPath(path)));
}
@Override
@Nonnull
Location getVDirLocation(@Nonnull CaseInsensitiveVDir parent, @Nonnull VPath path) {
return vfs.getRootDir().getVDirLocation(encode(parent.getPath().clone().appendPath(path)));
}
//
// Misc
//
@Override
public boolean needsSequentialWriting() {
return vfs.needsSequentialWriting();
}
//
// Encode / Decode
//
@Nonnull
private VPath encode(@Nonnull VPath path) {
char[] digest = encode(mdf.create().digest(path.getPathAsString('/').getBytes()));
StringBuilder sb = new StringBuilder();
int idx = 0;
try {
for (int groupIdx = 0; groupIdx < numGroups; groupIdx++) {
for (int letterIdx = 0; letterIdx < groupSize; letterIdx++) {
sb.append(digest[idx++]);
}
sb.append('/');
}
if (idx < digest.length) {
sb.append(digest, idx, digest.length - idx);
} else {
// Remove the last /, it is not a directory here
sb.setLength(sb.length() - 1);
}
} catch (IndexOutOfBoundsException e) {
}
return new VPath(sb.toString(), '/');
}
@Nonnull
private static final byte[] code = "0123456789ABCDEF".getBytes();
@Nonnull
static char[] encode(@Nonnull byte[] bytes) {
char[] array = new char[bytes.length * 2];
for (int idx = 0; idx < bytes.length; idx++) {
array[(idx << 1)] = (char) code[(bytes[idx] & 0xF0) >> 4];
array[(idx << 1) + 1] = (char) code[(bytes[idx] & 0x0F)];
}
return array;
}
@Override
@Nonnull
VPath getPathFromDir(@Nonnull CaseInsensitiveVDir parent, @Nonnull CaseInsensitiveVFile file) {
StringBuilder path = getPathFromDirInternal(parent, (CaseInsensitiveVDir) file.getParent())
.append(file.getName());
return new VPath(path.toString(), '/');
}
@Nonnull
private StringBuilder getPathFromDirInternal(@Nonnull CaseInsensitiveVDir baseDir,
@Nonnull CaseInsensitiveVDir currentDir) {
if (baseDir == currentDir) {
return new StringBuilder();
}
CaseInsensitiveVDir currentParent = (CaseInsensitiveVDir) currentDir.getParent();
assert currentParent != null;
return getPathFromDirInternal(baseDir, currentParent).append(currentDir.getName()).append('/');
}
@Override
@Nonnull
VPath getPathFromRoot(@Nonnull CaseInsensitiveVFile file) {
return getPathFromDir(root, file);
}
@Override
@Nonnull
public StreamFileStatus getStatus() {
if (!used) {
return StreamFileStatus.NOT_USED;
} else if (closed) {
return StreamFileStatus.CLOSED;
} else {
return StreamFileStatus.OPEN;
}
}
@Override
@CheckForNull
public String getInfoString() {
return vfs.getInfoString();
}
@Override
public String toString() {
return "ciFS >> " + vfs.toString();
}
}