aboutsummaryrefslogtreecommitdiff
path: root/jimfs/src/main/java/com/google/common/jimfs
diff options
context:
space:
mode:
Diffstat (limited to 'jimfs/src/main/java/com/google/common/jimfs')
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AbstractAttributeView.java41
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AbstractWatchService.java305
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AclAttributeProvider.java155
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AttributeCopyOption.java31
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AttributeProvider.java179
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AttributeService.java423
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java239
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Configuration.java700
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Directory.java353
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DirectoryEntry.java167
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DosAttributeProvider.java200
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DowngradedDirectoryStream.java50
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DowngradedSeekableByteChannel.java81
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Feature.java105
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/File.java280
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileFactory.java121
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileLookup.java34
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileSystemState.java129
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java737
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileTree.java221
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/GlobToRegex.java400
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/GuardedBy.java36
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Handler.java92
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/HeapDisk.java145
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/InternalCharMatcher.java42
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Jimfs.java245
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsAsynchronousFileChannel.java231
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileChannel.java675
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileStore.java267
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java337
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystemProvider.java355
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java132
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsInputStream.java158
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsOutputStream.java116
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsPath.java445
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsSecureDirectoryStream.java217
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Name.java127
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Options.java149
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/OwnerAttributeProvider.java123
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathMatchers.java96
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathNormalization.java133
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathService.java269
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathType.java243
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathURLConnection.java147
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java250
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PosixAttributeProvider.java270
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/RegularFile.java661
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/StandardAttributeProviders.java58
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/SymbolicLink.java49
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/SystemJimfsFileSystemProvider.java277
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UnixAttributeProvider.java170
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UnixFileAttributeView.java26
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UnixPathType.java85
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UserDefinedAttributeProvider.java162
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UserLookupService.java114
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Util.java111
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java76
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/WindowsPathType.java208
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/package-info.java25
59 files changed, 12273 insertions, 0 deletions
diff --git a/jimfs/src/main/java/com/google/common/jimfs/AbstractAttributeView.java b/jimfs/src/main/java/com/google/common/jimfs/AbstractAttributeView.java
new file mode 100644
index 0000000..ed13566
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/AbstractAttributeView.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import java.nio.file.attribute.FileAttributeView;
+
+/**
+ * Abstract base class for {@link FileAttributeView} implementations.
+ *
+ * @author Colin Decker
+ */
+abstract class AbstractAttributeView implements FileAttributeView {
+
+ private final FileLookup lookup;
+
+ protected AbstractAttributeView(FileLookup lookup) {
+ this.lookup = checkNotNull(lookup);
+ }
+
+ /** Looks up the file to get or set attributes on. */
+ protected final File lookupFile() throws IOException {
+ return lookup.lookup();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/AbstractWatchService.java b/jimfs/src/main/java/com/google/common/jimfs/AbstractWatchService.java
new file mode 100644
index 0000000..6b4326d
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/AbstractWatchService.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.Watchable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Abstract implementation of {@link WatchService}. Provides the means for registering and managing
+ * keys but does not handle actually watching. Subclasses should implement the means of watching
+ * watchables, posting events to registered keys and queueing keys with the service by signalling
+ * them.
+ *
+ * @author Colin Decker
+ */
+abstract class AbstractWatchService implements WatchService {
+
+ private final BlockingQueue<WatchKey> queue = new LinkedBlockingQueue<>();
+ private final WatchKey poison = new Key(this, null, ImmutableSet.<WatchEvent.Kind<?>>of());
+
+ private final AtomicBoolean open = new AtomicBoolean(true);
+
+ /**
+ * Registers the given watchable with this service, returning a new watch key for it. This
+ * implementation just checks that the service is open and creates a key; subclasses may override
+ * it to do other things as well.
+ */
+ public Key register(Watchable watchable, Iterable<? extends WatchEvent.Kind<?>> eventTypes)
+ throws IOException {
+ checkOpen();
+ return new Key(this, watchable, eventTypes);
+ }
+
+ /** Returns whether or not this watch service is open. */
+ @VisibleForTesting
+ public boolean isOpen() {
+ return open.get();
+ }
+
+ /** Enqueues the given key if the watch service is open; does nothing otherwise. */
+ final void enqueue(Key key) {
+ if (isOpen()) {
+ queue.add(key);
+ }
+ }
+
+ /** Called when the given key is cancelled. Does nothing by default. */
+ public void cancelled(Key key) {}
+
+ @VisibleForTesting
+ ImmutableList<WatchKey> queuedKeys() {
+ return ImmutableList.copyOf(queue);
+ }
+
+ @NullableDecl
+ @Override
+ public WatchKey poll() {
+ checkOpen();
+ return check(queue.poll());
+ }
+
+ @NullableDecl
+ @Override
+ public WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException {
+ checkOpen();
+ return check(queue.poll(timeout, unit));
+ }
+
+ @Override
+ public WatchKey take() throws InterruptedException {
+ checkOpen();
+ return check(queue.take());
+ }
+
+ /** Returns the given key, throwing an exception if it's the poison. */
+ @NullableDecl
+ private WatchKey check(@NullableDecl WatchKey key) {
+ if (key == poison) {
+ // ensure other blocking threads get the poison
+ queue.offer(poison);
+ throw new ClosedWatchServiceException();
+ }
+ return key;
+ }
+
+ /** Checks that the watch service is open, throwing {@link ClosedWatchServiceException} if not. */
+ protected final void checkOpen() {
+ if (!open.get()) {
+ throw new ClosedWatchServiceException();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (open.compareAndSet(true, false)) {
+ queue.clear();
+ queue.offer(poison);
+ }
+ }
+
+ /** A basic implementation of {@link WatchEvent}. */
+ static final class Event<T> implements WatchEvent<T> {
+
+ private final Kind<T> kind;
+ private final int count;
+
+ @NullableDecl private final T context;
+
+ public Event(Kind<T> kind, int count, @NullableDecl T context) {
+ this.kind = checkNotNull(kind);
+ checkArgument(count >= 0, "count (%s) must be non-negative", count);
+ this.count = count;
+ this.context = context;
+ }
+
+ @Override
+ public Kind<T> kind() {
+ return kind;
+ }
+
+ @Override
+ public int count() {
+ return count;
+ }
+
+ @NullableDecl
+ @Override
+ public T context() {
+ return context;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Event) {
+ Event<?> other = (Event<?>) obj;
+ return kind().equals(other.kind())
+ && count() == other.count()
+ && Objects.equals(context(), other.context());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(kind(), count(), context());
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("kind", kind())
+ .add("count", count())
+ .add("context", context())
+ .toString();
+ }
+ }
+
+ /** Implementation of {@link WatchKey} for an {@link AbstractWatchService}. */
+ static final class Key implements WatchKey {
+
+ @VisibleForTesting static final int MAX_QUEUE_SIZE = 256;
+
+ private static WatchEvent<Object> overflowEvent(int count) {
+ return new Event<>(OVERFLOW, count, null);
+ }
+
+ private final AbstractWatchService watcher;
+ private final Watchable watchable;
+ private final ImmutableSet<WatchEvent.Kind<?>> subscribedTypes;
+
+ private final AtomicReference<State> state = new AtomicReference<>(State.READY);
+ private final AtomicBoolean valid = new AtomicBoolean(true);
+ private final AtomicInteger overflow = new AtomicInteger();
+
+ private final BlockingQueue<WatchEvent<?>> events = new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
+
+ public Key(
+ AbstractWatchService watcher,
+ @NullableDecl Watchable watchable,
+ Iterable<? extends WatchEvent.Kind<?>> subscribedTypes) {
+ this.watcher = checkNotNull(watcher);
+ this.watchable = watchable; // nullable for Watcher poison
+ this.subscribedTypes = ImmutableSet.copyOf(subscribedTypes);
+ }
+
+ /** Gets the current state of this key, State.READY or SIGNALLED. */
+ @VisibleForTesting
+ State state() {
+ return state.get();
+ }
+
+ /** Gets whether or not this key is subscribed to the given type of event. */
+ public boolean subscribesTo(WatchEvent.Kind<?> eventType) {
+ return subscribedTypes.contains(eventType);
+ }
+
+ /**
+ * Posts the given event to this key. After posting one or more events, {@link #signal()} must
+ * be called to cause the key to be enqueued with the watch service.
+ */
+ public void post(WatchEvent<?> event) {
+ if (!events.offer(event)) {
+ overflow.incrementAndGet();
+ }
+ }
+
+ /**
+ * Sets the state to SIGNALLED and enqueues this key with the watcher if it was previously in
+ * the READY state.
+ */
+ public void signal() {
+ if (state.getAndSet(State.SIGNALLED) == State.READY) {
+ watcher.enqueue(this);
+ }
+ }
+
+ @Override
+ public boolean isValid() {
+ return watcher.isOpen() && valid.get();
+ }
+
+ @Override
+ public List<WatchEvent<?>> pollEvents() {
+ // note: it's correct to be able to retrieve more events from a key without calling reset()
+ // reset() is ONLY for "returning" the key to the watch service to potentially be retrieved by
+ // another thread when you're finished with it
+ List<WatchEvent<?>> result = new ArrayList<>(events.size());
+ events.drainTo(result);
+ int overflowCount = overflow.getAndSet(0);
+ if (overflowCount != 0) {
+ result.add(overflowEvent(overflowCount));
+ }
+ return Collections.unmodifiableList(result);
+ }
+
+ @Override
+ public boolean reset() {
+ // calling reset() multiple times without polling events would cause key to be placed in
+ // watcher queue multiple times, but not much that can be done about that
+ if (isValid() && state.compareAndSet(State.SIGNALLED, State.READY)) {
+ // requeue if events are pending
+ if (!events.isEmpty()) {
+ signal();
+ }
+ }
+
+ return isValid();
+ }
+
+ @Override
+ public void cancel() {
+ valid.set(false);
+ watcher.cancelled(this);
+ }
+
+ @Override
+ public Watchable watchable() {
+ return watchable;
+ }
+
+ @VisibleForTesting
+ enum State {
+ READY,
+ SIGNALLED
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/AclAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/AclAttributeProvider.java
new file mode 100644
index 0000000..1fa0f15
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/AclAttributeProvider.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.List;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Attribute provider that provides the {@link AclFileAttributeView} ("acl").
+ *
+ * @author Colin Decker
+ */
+final class AclAttributeProvider extends AttributeProvider {
+
+ private static final ImmutableSet<String> ATTRIBUTES = ImmutableSet.of("acl");
+
+ private static final ImmutableSet<String> INHERITED_VIEWS = ImmutableSet.of("owner");
+
+ private static final ImmutableList<AclEntry> DEFAULT_ACL = ImmutableList.of();
+
+ @Override
+ public String name() {
+ return "acl";
+ }
+
+ @Override
+ public ImmutableSet<String> inherits() {
+ return INHERITED_VIEWS;
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ return ATTRIBUTES;
+ }
+
+ @Override
+ public ImmutableMap<String, ?> defaultValues(Map<String, ?> userProvidedDefaults) {
+ Object userProvidedAcl = userProvidedDefaults.get("acl:acl");
+
+ ImmutableList<AclEntry> acl = DEFAULT_ACL;
+ if (userProvidedAcl != null) {
+ acl = toAcl(checkType("acl", "acl", userProvidedAcl, List.class));
+ }
+
+ return ImmutableMap.of("acl:acl", acl);
+ }
+
+ @NullableDecl
+ @Override
+ public Object get(File file, String attribute) {
+ if (attribute.equals("acl")) {
+ return file.getAttribute("acl", "acl");
+ }
+
+ return null;
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ if (attribute.equals("acl")) {
+ checkNotCreate(view, attribute, create);
+ file.setAttribute("acl", "acl", toAcl(checkType(view, attribute, value, List.class)));
+ }
+ }
+
+ @SuppressWarnings("unchecked") // only cast after checking each element's type
+ private static ImmutableList<AclEntry> toAcl(List<?> list) {
+ ImmutableList<?> copy = ImmutableList.copyOf(list);
+ for (Object obj : copy) {
+ if (!(obj instanceof AclEntry)) {
+ throw new IllegalArgumentException(
+ "invalid element for attribute 'acl:acl': should be List<AclEntry>, "
+ + "found element of type "
+ + obj.getClass());
+ }
+ }
+
+ return (ImmutableList<AclEntry>) copy;
+ }
+
+ @Override
+ public Class<AclFileAttributeView> viewType() {
+ return AclFileAttributeView.class;
+ }
+
+ @Override
+ public AclFileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ return new View(lookup, (FileOwnerAttributeView) inheritedViews.get("owner"));
+ }
+
+ /** Implementation of {@link AclFileAttributeView}. */
+ private static final class View extends AbstractAttributeView implements AclFileAttributeView {
+
+ private final FileOwnerAttributeView ownerView;
+
+ public View(FileLookup lookup, FileOwnerAttributeView ownerView) {
+ super(lookup);
+ this.ownerView = checkNotNull(ownerView);
+ }
+
+ @Override
+ public String name() {
+ return "acl";
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public List<AclEntry> getAcl() throws IOException {
+ return (List<AclEntry>) lookupFile().getAttribute("acl", "acl");
+ }
+
+ @Override
+ public void setAcl(List<AclEntry> acl) throws IOException {
+ checkNotNull(acl);
+ lookupFile().setAttribute("acl", "acl", ImmutableList.copyOf(acl));
+ }
+
+ @Override
+ public UserPrincipal getOwner() throws IOException {
+ return ownerView.getOwner();
+ }
+
+ @Override
+ public void setOwner(UserPrincipal owner) throws IOException {
+ ownerView.setOwner(owner);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/AttributeCopyOption.java b/jimfs/src/main/java/com/google/common/jimfs/AttributeCopyOption.java
new file mode 100644
index 0000000..d9ce761
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/AttributeCopyOption.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+/**
+ * Options for how to handle copying of file attributes when copying a file.
+ *
+ * @author Colin Decker
+ */
+enum AttributeCopyOption {
+ /** Copy all attributes on the file. */
+ ALL,
+ /** Copy only the basic attributes (file times) of the file. */
+ BASIC,
+ /** Do not copy any of the file's attributes. */
+ NONE
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/AttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/AttributeProvider.java
new file mode 100644
index 0000000..f5cade2
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/AttributeProvider.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.Arrays;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Abstract provider for handling a specific file attribute view.
+ *
+ * @author Colin Decker
+ */
+public abstract class AttributeProvider {
+
+ /** Returns the view name that's used to get attributes from this provider. */
+ public abstract String name();
+
+ /** Returns the names of other providers that this provider inherits attributes from. */
+ public ImmutableSet<String> inherits() {
+ return ImmutableSet.of();
+ }
+
+ /** Returns the type of the view interface that this provider supports. */
+ public abstract Class<? extends FileAttributeView> viewType();
+
+ /**
+ * Returns a view of the file located by the given lookup callback. The given map contains the
+ * views inherited by this view.
+ */
+ public abstract FileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews);
+
+ /**
+ * Returns a map containing the default attribute values for this provider. The keys of the map
+ * are attribute identifier strings (in "view:attribute" form) and the value for each is the
+ * default value that should be set for that attribute when creating a new file.
+ *
+ * <p>The given map should be in the same format and contains user-provided default values. If the
+ * user provided any default values for attributes handled by this provider, those values should
+ * be checked to ensure they are of the correct type. Additionally, if any changes to a
+ * user-provided attribute are necessary (for example, creating an immutable defensive copy), that
+ * should be done. The resulting values should be included in the result map along with default
+ * values for any attributes the user did not provide a value for.
+ */
+ public ImmutableMap<String, ?> defaultValues(Map<String, ?> userDefaults) {
+ return ImmutableMap.of();
+ }
+
+ /** Returns the set of attributes that are always available from this provider. */
+ public abstract ImmutableSet<String> fixedAttributes();
+
+ /** Returns whether or not this provider supports the given attribute directly. */
+ public boolean supports(String attribute) {
+ return fixedAttributes().contains(attribute);
+ }
+
+ /**
+ * Returns the set of attributes supported by this view that are present in the given file. For
+ * most providers, this will be a fixed set of attributes.
+ */
+ public ImmutableSet<String> attributes(File file) {
+ return fixedAttributes();
+ }
+
+ /**
+ * Returns the value of the given attribute in the given file or null if the attribute is not
+ * supported by this provider.
+ */
+ @NullableDecl
+ public abstract Object get(File file, String attribute);
+
+ /**
+ * Sets the value of the given attribute in the given file object. The {@code create} parameter
+ * indicates whether or not the value is being set upon creation of a new file via a user-provided
+ * {@code FileAttribute}.
+ *
+ * @throws IllegalArgumentException if the given attribute is one supported by this provider but
+ * it is not allowed to be set by the user
+ * @throws UnsupportedOperationException if the given attribute is one supported by this provider
+ * and is allowed to be set by the user, but not on file creation and {@code create} is true
+ */
+ public abstract void set(File file, String view, String attribute, Object value, boolean create);
+
+ // optional
+
+ /**
+ * Returns the type of file attributes object this provider supports, or null if it doesn't
+ * support reading its attributes as an object.
+ */
+ @NullableDecl
+ public Class<? extends BasicFileAttributes> attributesType() {
+ return null;
+ }
+
+ /**
+ * Reads this provider's attributes from the given file as an attributes object.
+ *
+ * @throws UnsupportedOperationException if this provider does not support reading an attributes
+ * object
+ */
+ public BasicFileAttributes readAttributes(File file) {
+ throw new UnsupportedOperationException();
+ }
+
+ // exception helpers
+
+ /** Throws a runtime exception indicating that the given attribute cannot be set. */
+ protected static RuntimeException unsettable(String view, String attribute, boolean create) {
+ // This matches the behavior of the real file system implementations: if the attempt to set the
+ // attribute is being made during file creation, throw UOE even though the attribute is one
+ // that cannot be set under any circumstances
+ checkNotCreate(view, attribute, create);
+ throw new IllegalArgumentException("cannot set attribute '" + view + ":" + attribute + "'");
+ }
+
+ /**
+ * Checks that the attribute is not being set by the user on file creation, throwing an
+ * unsupported operation exception if it is.
+ */
+ protected static void checkNotCreate(String view, String attribute, boolean create) {
+ if (create) {
+ throw new UnsupportedOperationException(
+ "cannot set attribute '" + view + ":" + attribute + "' during file creation");
+ }
+ }
+
+ /**
+ * Checks that the given value is of the given type, returning the value if so and throwing an
+ * exception if not.
+ */
+ protected static <T> T checkType(String view, String attribute, Object value, Class<T> type) {
+ checkNotNull(value);
+ if (type.isInstance(value)) {
+ return type.cast(value);
+ }
+
+ throw invalidType(view, attribute, value, type);
+ }
+
+ /**
+ * Throws an illegal argument exception indicating that the given value is not one of the expected
+ * types for the given attribute.
+ */
+ protected static IllegalArgumentException invalidType(
+ String view, String attribute, Object value, Class<?>... expectedTypes) {
+ Object expected =
+ expectedTypes.length == 1 ? expectedTypes[0] : "one of " + Arrays.toString(expectedTypes);
+ throw new IllegalArgumentException(
+ "invalid type "
+ + value.getClass()
+ + " for attribute '"
+ + view
+ + ":"
+ + attribute
+ + "': expected "
+ + expected);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/AttributeService.java b/jimfs/src/main/java/com/google/common/jimfs/AttributeService.java
new file mode 100644
index 0000000..333a497
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/AttributeService.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Service providing all attribute related operations for a file store. One piece of the file store
+ * implementation.
+ *
+ * @author Colin Decker
+ */
+final class AttributeService {
+
+ private static final String ALL_ATTRIBUTES = "*";
+
+ private final ImmutableMap<String, AttributeProvider> providersByName;
+ private final ImmutableMap<Class<?>, AttributeProvider> providersByViewType;
+ private final ImmutableMap<Class<?>, AttributeProvider> providersByAttributesType;
+
+ private final ImmutableList<FileAttribute<?>> defaultValues;
+
+ /** Creates a new attribute service using the given configuration. */
+ public AttributeService(Configuration configuration) {
+ this(getProviders(configuration), configuration.defaultAttributeValues);
+ }
+
+ /**
+ * Creates a new attribute service using the given providers and user provided default attribute
+ * values.
+ */
+ public AttributeService(
+ Iterable<? extends AttributeProvider> providers, Map<String, ?> userProvidedDefaults) {
+ ImmutableMap.Builder<String, AttributeProvider> byViewNameBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder<Class<?>, AttributeProvider> byViewTypeBuilder = ImmutableMap.builder();
+ ImmutableMap.Builder<Class<?>, AttributeProvider> byAttributesTypeBuilder =
+ ImmutableMap.builder();
+
+ ImmutableList.Builder<FileAttribute<?>> defaultAttributesBuilder = ImmutableList.builder();
+
+ for (AttributeProvider provider : providers) {
+ byViewNameBuilder.put(provider.name(), provider);
+ byViewTypeBuilder.put(provider.viewType(), provider);
+ if (provider.attributesType() != null) {
+ byAttributesTypeBuilder.put(provider.attributesType(), provider);
+ }
+
+ for (Map.Entry<String, ?> entry : provider.defaultValues(userProvidedDefaults).entrySet()) {
+ defaultAttributesBuilder.add(new SimpleFileAttribute<>(entry.getKey(), entry.getValue()));
+ }
+ }
+
+ this.providersByName = byViewNameBuilder.build();
+ this.providersByViewType = byViewTypeBuilder.build();
+ this.providersByAttributesType = byAttributesTypeBuilder.build();
+ this.defaultValues = defaultAttributesBuilder.build();
+ }
+
+ private static Iterable<AttributeProvider> getProviders(Configuration configuration) {
+ Map<String, AttributeProvider> result = new HashMap<>();
+
+ for (AttributeProvider provider : configuration.attributeProviders) {
+ result.put(provider.name(), provider);
+ }
+
+ for (String view : configuration.attributeViews) {
+ addStandardProvider(result, view);
+ }
+
+ addMissingProviders(result);
+
+ return Collections.unmodifiableCollection(result.values());
+ }
+
+ private static void addMissingProviders(Map<String, AttributeProvider> providers) {
+ Set<String> missingViews = new HashSet<>();
+ for (AttributeProvider provider : providers.values()) {
+ for (String inheritedView : provider.inherits()) {
+ if (!providers.containsKey(inheritedView)) {
+ missingViews.add(inheritedView);
+ }
+ }
+ }
+
+ if (missingViews.isEmpty()) {
+ return;
+ }
+
+ // add any inherited views that were not listed directly
+ for (String view : missingViews) {
+ addStandardProvider(providers, view);
+ }
+
+ // in case any of the providers that were added themselves have missing views they inherit
+ addMissingProviders(providers);
+ }
+
+ private static void addStandardProvider(Map<String, AttributeProvider> result, String view) {
+ AttributeProvider provider = StandardAttributeProviders.get(view);
+
+ if (provider == null) {
+ if (!result.containsKey(view)) {
+ throw new IllegalStateException("no provider found for attribute view '" + view + "'");
+ }
+ } else {
+ result.put(provider.name(), provider);
+ }
+ }
+
+ /** Implements {@link FileSystem#supportedFileAttributeViews()}. */
+ public ImmutableSet<String> supportedFileAttributeViews() {
+ return providersByName.keySet();
+ }
+
+ /** Implements {@link FileStore#supportsFileAttributeView(Class)}. */
+ public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
+ return providersByViewType.containsKey(type);
+ }
+
+ /** Sets all initial attributes for the given file, including the given attributes if possible. */
+ public void setInitialAttributes(File file, FileAttribute<?>... attrs) {
+ // default values should already be sanitized by their providers
+ for (int i = 0; i < defaultValues.size(); i++) {
+ FileAttribute<?> attribute = defaultValues.get(i);
+
+ int separatorIndex = attribute.name().indexOf(':');
+ String view = attribute.name().substring(0, separatorIndex);
+ String attr = attribute.name().substring(separatorIndex + 1);
+ file.setAttribute(view, attr, attribute.value());
+ }
+
+ for (FileAttribute<?> attr : attrs) {
+ setAttribute(file, attr.name(), attr.value(), true);
+ }
+ }
+
+ /** Copies the attributes of the given file to the given copy file. */
+ public void copyAttributes(File file, File copy, AttributeCopyOption copyOption) {
+ switch (copyOption) {
+ case ALL:
+ file.copyAttributes(copy);
+ break;
+ case BASIC:
+ file.copyBasicAttributes(copy);
+ break;
+ default:
+ // don't copy
+ }
+ }
+
+ /**
+ * Gets the value of the given attribute for the given file. {@code attribute} must be of the form
+ * "view:attribute" or "attribute".
+ */
+ public Object getAttribute(File file, String attribute) {
+ String view = getViewName(attribute);
+ String attr = getSingleAttribute(attribute);
+ return getAttribute(file, view, attr);
+ }
+
+ /**
+ * Gets the value of the given attribute for the given view and file. Neither view nor attribute
+ * may have a ':' character.
+ */
+ public Object getAttribute(File file, String view, String attribute) {
+ Object value = getAttributeInternal(file, view, attribute);
+ if (value == null) {
+ throw new IllegalArgumentException("invalid attribute for view '" + view + "': " + attribute);
+ }
+ return value;
+ }
+
+ @NullableDecl
+ private Object getAttributeInternal(File file, String view, String attribute) {
+ AttributeProvider provider = providersByName.get(view);
+ if (provider == null) {
+ return null;
+ }
+
+ Object value = provider.get(file, attribute);
+ if (value == null) {
+ for (String inheritedView : provider.inherits()) {
+ value = getAttributeInternal(file, inheritedView, attribute);
+ if (value != null) {
+ break;
+ }
+ }
+ }
+
+ return value;
+ }
+
+ /** Sets the value of the given attribute to the given value for the given file. */
+ public void setAttribute(File file, String attribute, Object value, boolean create) {
+ String view = getViewName(attribute);
+ String attr = getSingleAttribute(attribute);
+ setAttributeInternal(file, view, attr, value, create);
+ }
+
+ private void setAttributeInternal(
+ File file, String view, String attribute, Object value, boolean create) {
+ AttributeProvider provider = providersByName.get(view);
+
+ if (provider != null) {
+ if (provider.supports(attribute)) {
+ provider.set(file, view, attribute, value, create);
+ return;
+ }
+
+ for (String inheritedView : provider.inherits()) {
+ AttributeProvider inheritedProvider = providersByName.get(inheritedView);
+ if (inheritedProvider.supports(attribute)) {
+ inheritedProvider.set(file, view, attribute, value, create);
+ return;
+ }
+ }
+ }
+
+ throw new UnsupportedOperationException(
+ "cannot set attribute '" + view + ":" + attribute + "'");
+ }
+
+ /**
+ * Returns an attribute view of the given type for the given file lookup callback, or {@code null}
+ * if the view type is not supported.
+ */
+ @SuppressWarnings("unchecked")
+ @NullableDecl
+ public <V extends FileAttributeView> V getFileAttributeView(FileLookup lookup, Class<V> type) {
+ AttributeProvider provider = providersByViewType.get(type);
+
+ if (provider != null) {
+ return (V) provider.view(lookup, createInheritedViews(lookup, provider));
+ }
+
+ return null;
+ }
+
+ private FileAttributeView getFileAttributeView(
+ FileLookup lookup,
+ Class<? extends FileAttributeView> viewType,
+ Map<String, FileAttributeView> inheritedViews) {
+ AttributeProvider provider = providersByViewType.get(viewType);
+ createInheritedViews(lookup, provider, inheritedViews);
+ return provider.view(lookup, ImmutableMap.copyOf(inheritedViews));
+ }
+
+ private ImmutableMap<String, FileAttributeView> createInheritedViews(
+ FileLookup lookup, AttributeProvider provider) {
+ if (provider.inherits().isEmpty()) {
+ return ImmutableMap.of();
+ }
+
+ Map<String, FileAttributeView> inheritedViews = new HashMap<>();
+ createInheritedViews(lookup, provider, inheritedViews);
+ return ImmutableMap.copyOf(inheritedViews);
+ }
+
+ private void createInheritedViews(
+ FileLookup lookup,
+ AttributeProvider provider,
+ Map<String, FileAttributeView> inheritedViews) {
+
+ for (String inherited : provider.inherits()) {
+ if (!inheritedViews.containsKey(inherited)) {
+ AttributeProvider inheritedProvider = providersByName.get(inherited);
+ FileAttributeView inheritedView =
+ getFileAttributeView(lookup, inheritedProvider.viewType(), inheritedViews);
+
+ inheritedViews.put(inherited, inheritedView);
+ }
+ }
+ }
+
+ /** Implements {@link Files#readAttributes(Path, String, LinkOption...)}. */
+ public ImmutableMap<String, Object> readAttributes(File file, String attributes) {
+ String view = getViewName(attributes);
+ List<String> attrs = getAttributeNames(attributes);
+
+ if (attrs.size() > 1 && attrs.contains(ALL_ATTRIBUTES)) {
+ // attrs contains * and other attributes
+ throw new IllegalArgumentException("invalid attributes: " + attributes);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ if (attrs.size() == 1 && attrs.contains(ALL_ATTRIBUTES)) {
+ // for 'view:*' format, get all keys for all providers for the view
+ AttributeProvider provider = providersByName.get(view);
+ readAll(file, provider, result);
+
+ for (String inheritedView : provider.inherits()) {
+ AttributeProvider inheritedProvider = providersByName.get(inheritedView);
+ readAll(file, inheritedProvider, result);
+ }
+ } else {
+ // for 'view:attr1,attr2,etc'
+ for (String attr : attrs) {
+ result.put(attr, getAttribute(file, view, attr));
+ }
+ }
+
+ return ImmutableMap.copyOf(result);
+ }
+
+ /**
+ * Returns attributes of the given file as an object of the given type.
+ *
+ * @throws UnsupportedOperationException if the given attributes type is not supported
+ */
+ @SuppressWarnings("unchecked")
+ public <A extends BasicFileAttributes> A readAttributes(File file, Class<A> type) {
+ AttributeProvider provider = providersByAttributesType.get(type);
+ if (provider != null) {
+ return (A) provider.readAttributes(file);
+ }
+
+ throw new UnsupportedOperationException("unsupported attributes type: " + type);
+ }
+
+ private static void readAll(File file, AttributeProvider provider, Map<String, Object> map) {
+ for (String attribute : provider.attributes(file)) {
+ Object value = provider.get(file, attribute);
+
+ // check for null to protect against race condition when an attribute present when
+ // attributes(file) was called is deleted before get() is called for that attribute
+ if (value != null) {
+ map.put(attribute, value);
+ }
+ }
+ }
+
+ private static String getViewName(String attribute) {
+ int separatorIndex = attribute.indexOf(':');
+
+ if (separatorIndex == -1) {
+ return "basic";
+ }
+
+ // separator must not be at the start or end of the string or appear more than once
+ if (separatorIndex == 0
+ || separatorIndex == attribute.length() - 1
+ || attribute.indexOf(':', separatorIndex + 1) != -1) {
+ throw new IllegalArgumentException("illegal attribute format: " + attribute);
+ }
+
+ return attribute.substring(0, separatorIndex);
+ }
+
+ private static final Splitter ATTRIBUTE_SPLITTER = Splitter.on(',');
+
+ private static ImmutableList<String> getAttributeNames(String attributes) {
+ int separatorIndex = attributes.indexOf(':');
+ String attributesPart = attributes.substring(separatorIndex + 1);
+
+ return ImmutableList.copyOf(ATTRIBUTE_SPLITTER.split(attributesPart));
+ }
+
+ private static String getSingleAttribute(String attribute) {
+ ImmutableList<String> attributeNames = getAttributeNames(attribute);
+
+ if (attributeNames.size() != 1 || ALL_ATTRIBUTES.equals(attributeNames.get(0))) {
+ throw new IllegalArgumentException("must specify a single attribute: " + attribute);
+ }
+
+ return attributeNames.get(0);
+ }
+
+ /** Simple implementation of {@link FileAttribute}. */
+ private static final class SimpleFileAttribute<T> implements FileAttribute<T> {
+
+ private final String name;
+ private final T value;
+
+ SimpleFileAttribute(String name, T value) {
+ this.name = checkNotNull(name);
+ this.value = checkNotNull(value);
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public T value() {
+ return value;
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java
new file mode 100644
index 0000000..6315ab7
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileTime;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Attribute provider that provides attributes common to all file systems, the {@link
+ * BasicFileAttributeView} ("basic" or no view prefix), and allows the reading of {@link
+ * BasicFileAttributes}.
+ *
+ * @author Colin Decker
+ */
+final class BasicAttributeProvider extends AttributeProvider {
+
+ private static final ImmutableSet<String> ATTRIBUTES =
+ ImmutableSet.of(
+ "size",
+ "fileKey",
+ "isDirectory",
+ "isRegularFile",
+ "isSymbolicLink",
+ "isOther",
+ "creationTime",
+ "lastAccessTime",
+ "lastModifiedTime");
+
+ @Override
+ public String name() {
+ return "basic";
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ return ATTRIBUTES;
+ }
+
+ @Override
+ public Object get(File file, String attribute) {
+ switch (attribute) {
+ case "size":
+ return file.size();
+ case "fileKey":
+ return file.id();
+ case "isDirectory":
+ return file.isDirectory();
+ case "isRegularFile":
+ return file.isRegularFile();
+ case "isSymbolicLink":
+ return file.isSymbolicLink();
+ case "isOther":
+ return !file.isDirectory() && !file.isRegularFile() && !file.isSymbolicLink();
+ case "creationTime":
+ return FileTime.fromMillis(file.getCreationTime());
+ case "lastAccessTime":
+ return FileTime.fromMillis(file.getLastAccessTime());
+ case "lastModifiedTime":
+ return FileTime.fromMillis(file.getLastModifiedTime());
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ switch (attribute) {
+ case "creationTime":
+ checkNotCreate(view, attribute, create);
+ file.setCreationTime(checkType(view, attribute, value, FileTime.class).toMillis());
+ break;
+ case "lastAccessTime":
+ checkNotCreate(view, attribute, create);
+ file.setLastAccessTime(checkType(view, attribute, value, FileTime.class).toMillis());
+ break;
+ case "lastModifiedTime":
+ checkNotCreate(view, attribute, create);
+ file.setLastModifiedTime(checkType(view, attribute, value, FileTime.class).toMillis());
+ break;
+ case "size":
+ case "fileKey":
+ case "isDirectory":
+ case "isRegularFile":
+ case "isSymbolicLink":
+ case "isOther":
+ throw unsettable(view, attribute, create);
+ default:
+ }
+ }
+
+ @Override
+ public Class<BasicFileAttributeView> viewType() {
+ return BasicFileAttributeView.class;
+ }
+
+ @Override
+ public BasicFileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ return new View(lookup);
+ }
+
+ @Override
+ public Class<BasicFileAttributes> attributesType() {
+ return BasicFileAttributes.class;
+ }
+
+ @Override
+ public BasicFileAttributes readAttributes(File file) {
+ return new Attributes(file);
+ }
+
+ /** Implementation of {@link BasicFileAttributeView}. */
+ private static final class View extends AbstractAttributeView implements BasicFileAttributeView {
+
+ protected View(FileLookup lookup) {
+ super(lookup);
+ }
+
+ @Override
+ public String name() {
+ return "basic";
+ }
+
+ @Override
+ public BasicFileAttributes readAttributes() throws IOException {
+ return new Attributes(lookupFile());
+ }
+
+ @Override
+ public void setTimes(
+ @NullableDecl FileTime lastModifiedTime,
+ @NullableDecl FileTime lastAccessTime,
+ @NullableDecl FileTime createTime)
+ throws IOException {
+ File file = lookupFile();
+
+ if (lastModifiedTime != null) {
+ file.setLastModifiedTime(lastModifiedTime.toMillis());
+ }
+
+ if (lastAccessTime != null) {
+ file.setLastAccessTime(lastAccessTime.toMillis());
+ }
+
+ if (createTime != null) {
+ file.setCreationTime(createTime.toMillis());
+ }
+ }
+ }
+
+ /** Implementation of {@link BasicFileAttributes}. */
+ static class Attributes implements BasicFileAttributes {
+
+ private final FileTime lastModifiedTime;
+ private final FileTime lastAccessTime;
+ private final FileTime creationTime;
+ private final boolean regularFile;
+ private final boolean directory;
+ private final boolean symbolicLink;
+ private final long size;
+ private final Object fileKey;
+
+ protected Attributes(File file) {
+ this.lastModifiedTime = FileTime.fromMillis(file.getLastModifiedTime());
+ this.lastAccessTime = FileTime.fromMillis(file.getLastAccessTime());
+ this.creationTime = FileTime.fromMillis(file.getCreationTime());
+ this.regularFile = file.isRegularFile();
+ this.directory = file.isDirectory();
+ this.symbolicLink = file.isSymbolicLink();
+ this.size = file.size();
+ this.fileKey = file.id();
+ }
+
+ @Override
+ public FileTime lastModifiedTime() {
+ return lastModifiedTime;
+ }
+
+ @Override
+ public FileTime lastAccessTime() {
+ return lastAccessTime;
+ }
+
+ @Override
+ public FileTime creationTime() {
+ return creationTime;
+ }
+
+ @Override
+ public boolean isRegularFile() {
+ return regularFile;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return directory;
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ return symbolicLink;
+ }
+
+ @Override
+ public boolean isOther() {
+ return false;
+ }
+
+ @Override
+ public long size() {
+ return size;
+ }
+
+ @Override
+ public Object fileKey() {
+ return fileKey;
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Configuration.java b/jimfs/src/main/java/com/google/common/jimfs/Configuration.java
new file mode 100644
index 0000000..06630eb
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Configuration.java
@@ -0,0 +1,700 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.jimfs.Feature.FILE_CHANNEL;
+import static com.google.common.jimfs.Feature.LINKS;
+import static com.google.common.jimfs.Feature.SECURE_DIRECTORY_STREAM;
+import static com.google.common.jimfs.Feature.SYMBOLIC_LINKS;
+import static com.google.common.jimfs.PathNormalization.CASE_FOLD_ASCII;
+import static com.google.common.jimfs.PathNormalization.NFC;
+import static com.google.common.jimfs.PathNormalization.NFD;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.nio.channels.FileChannel;
+import java.nio.file.FileSystem;
+import java.nio.file.InvalidPathException;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Immutable configuration for an in-memory file system. A {@code Configuration} is passed to a
+ * method in {@link Jimfs} such as {@link Jimfs#newFileSystem(Configuration)} to create a new {@link
+ * FileSystem} instance.
+ *
+ * @author Colin Decker
+ */
+public final class Configuration {
+
+ /**
+ * Returns the default configuration for a UNIX-like file system. A file system created with this
+ * configuration:
+ *
+ * <ul>
+ * <li>uses {@code /} as the path name separator (see {@link PathType#unix()} for more
+ * information on the path format)
+ * <li>has root {@code /} and working directory {@code /work}
+ * <li>performs case-sensitive file lookup
+ * <li>supports only the {@linkplain BasicFileAttributeView basic} file attribute view, to avoid
+ * overhead for unneeded attributes
+ * <li>supports hard links, symbolic links, {@link SecureDirectoryStream} and {@link
+ * FileChannel}
+ * </ul>
+ *
+ * <p>To create a modified version of this configuration, such as to include the full set of UNIX
+ * file attribute views, {@linkplain #toBuilder() create a builder}.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * Configuration config = Configuration.unix().toBuilder()
+ * .setAttributeViews("basic", "owner", "posix", "unix")
+ * .setWorkingDirectory("/home/user")
+ * .build(); </pre>
+ */
+ public static Configuration unix() {
+ return UnixHolder.UNIX;
+ }
+
+ private static final class UnixHolder {
+ private static final Configuration UNIX =
+ Configuration.builder(PathType.unix())
+ .setDisplayName("Unix")
+ .setRoots("/")
+ .setWorkingDirectory("/work")
+ .setAttributeViews("basic")
+ .setSupportedFeatures(LINKS, SYMBOLIC_LINKS, SECURE_DIRECTORY_STREAM, FILE_CHANNEL)
+ .build();
+ }
+
+ /**
+ * Returns the default configuration for a Mac OS X-like file system.
+ *
+ * <p>The primary differences between this configuration and the default {@link #unix()}
+ * configuration are that this configuration does Unicode normalization on the display and
+ * canonical forms of filenames and does case insensitive file lookup.
+ *
+ * <p>A file system created with this configuration:
+ *
+ * <ul>
+ * <li>uses {@code /} as the path name separator (see {@link PathType#unix()} for more
+ * information on the path format)
+ * <li>has root {@code /} and working directory {@code /work}
+ * <li>does Unicode normalization on paths, both for lookup and for {@code Path} objects
+ * <li>does case-insensitive (for ASCII characters only) lookup
+ * <li>supports only the {@linkplain BasicFileAttributeView basic} file attribute view, to avoid
+ * overhead for unneeded attributes
+ * <li>supports hard links, symbolic links and {@link FileChannel}
+ * </ul>
+ *
+ * <p>To create a modified version of this configuration, such as to include the full set of UNIX
+ * file attribute views or to use full Unicode case insensitivity, {@linkplain #toBuilder() create
+ * a builder}.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * Configuration config = Configuration.osX().toBuilder()
+ * .setAttributeViews("basic", "owner", "posix", "unix")
+ * .setNameCanonicalNormalization(NFD, CASE_FOLD_UNICODE)
+ * .setWorkingDirectory("/Users/user")
+ * .build(); </pre>
+ */
+ public static Configuration osX() {
+ return OsxHolder.OS_X;
+ }
+
+ private static final class OsxHolder {
+ private static final Configuration OS_X =
+ unix().toBuilder()
+ .setDisplayName("OSX")
+ .setNameDisplayNormalization(NFC) // matches JDK 1.7u40+ behavior
+ .setNameCanonicalNormalization(NFD, CASE_FOLD_ASCII) // NFD is default in HFS+
+ .setSupportedFeatures(LINKS, SYMBOLIC_LINKS, FILE_CHANNEL)
+ .build();
+ }
+
+ /**
+ * Returns the default configuration for a Windows-like file system. A file system created with
+ * this configuration:
+ *
+ * <ul>
+ * <li>uses {@code \} as the path name separator and recognizes {@code /} as a separator when
+ * parsing paths (see {@link PathType#windows()} for more information on path format)
+ * <li>has root {@code C:\} and working directory {@code C:\work}
+ * <li>performs case-insensitive (for ASCII characters only) file lookup
+ * <li>creates {@code Path} objects that use case-insensitive (for ASCII characters only)
+ * equality
+ * <li>supports only the {@linkplain BasicFileAttributeView basic} file attribute view, to avoid
+ * overhead for unneeded attributes
+ * <li>supports hard links, symbolic links and {@link FileChannel}
+ * </ul>
+ *
+ * <p>To create a modified version of this configuration, such as to include the full set of
+ * Windows file attribute views or to use full Unicode case insensitivity, {@linkplain
+ * #toBuilder() create a builder}.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * Configuration config = Configuration.windows().toBuilder()
+ * .setAttributeViews("basic", "owner", "dos", "acl", "user")
+ * .setNameCanonicalNormalization(CASE_FOLD_UNICODE)
+ * .setWorkingDirectory("C:\\Users\\user") // or "C:/Users/user"
+ * .build(); </pre>
+ */
+ public static Configuration windows() {
+ return WindowsHolder.WINDOWS;
+ }
+
+ private static final class WindowsHolder {
+ private static final Configuration WINDOWS =
+ Configuration.builder(PathType.windows())
+ .setDisplayName("Windows")
+ .setRoots("C:\\")
+ .setWorkingDirectory("C:\\work")
+ .setNameCanonicalNormalization(CASE_FOLD_ASCII)
+ .setPathEqualityUsesCanonicalForm(true) // matches real behavior of WindowsPath
+ .setAttributeViews("basic")
+ .setSupportedFeatures(LINKS, SYMBOLIC_LINKS, FILE_CHANNEL)
+ .build();
+ }
+
+ /**
+ * Returns a default configuration appropriate to the current operating system.
+ *
+ * <p>More specifically, if the operating system is Windows, {@link Configuration#windows()} is
+ * returned; if the operating system is Mac OS X, {@link Configuration#osX()} is returned;
+ * otherwise, {@link Configuration#unix()} is returned.
+ *
+ * <p>This is the configuration used by the {@code Jimfs.newFileSystem} methods that do not take a
+ * {@code Configuration} parameter.
+ *
+ * @since 1.1
+ */
+ public static Configuration forCurrentPlatform() {
+ String os = System.getProperty("os.name");
+
+ if (os.contains("Windows")) {
+ return windows();
+ } else if (os.contains("OS X")) {
+ return osX();
+ } else {
+ return unix();
+ }
+ }
+
+ /** Creates a new mutable {@link Configuration} builder using the given path type. */
+ public static Builder builder(PathType pathType) {
+ return new Builder(pathType);
+ }
+
+ // Path configuration
+ final PathType pathType;
+ final ImmutableSet<PathNormalization> nameDisplayNormalization;
+ final ImmutableSet<PathNormalization> nameCanonicalNormalization;
+ final boolean pathEqualityUsesCanonicalForm;
+
+ // Disk configuration
+ final int blockSize;
+ final long maxSize;
+ final long maxCacheSize;
+
+ // Attribute configuration
+ final ImmutableSet<String> attributeViews;
+ final ImmutableSet<AttributeProvider> attributeProviders;
+ final ImmutableMap<String, Object> defaultAttributeValues;
+
+ // Watch service
+ final WatchServiceConfiguration watchServiceConfig;
+
+ // Other
+ final ImmutableSet<String> roots;
+ final String workingDirectory;
+ final ImmutableSet<Feature> supportedFeatures;
+ private final String displayName;
+
+ /** Creates an immutable configuration object from the given builder. */
+ private Configuration(Builder builder) {
+ this.pathType = builder.pathType;
+ this.nameDisplayNormalization = builder.nameDisplayNormalization;
+ this.nameCanonicalNormalization = builder.nameCanonicalNormalization;
+ this.pathEqualityUsesCanonicalForm = builder.pathEqualityUsesCanonicalForm;
+ this.blockSize = builder.blockSize;
+ this.maxSize = builder.maxSize;
+ this.maxCacheSize = builder.maxCacheSize;
+ this.attributeViews = builder.attributeViews;
+ this.attributeProviders =
+ builder.attributeProviders == null
+ ? ImmutableSet.<AttributeProvider>of()
+ : ImmutableSet.copyOf(builder.attributeProviders);
+ this.defaultAttributeValues =
+ builder.defaultAttributeValues == null
+ ? ImmutableMap.<String, Object>of()
+ : ImmutableMap.copyOf(builder.defaultAttributeValues);
+ this.watchServiceConfig = builder.watchServiceConfig;
+ this.roots = builder.roots;
+ this.workingDirectory = builder.workingDirectory;
+ this.supportedFeatures = builder.supportedFeatures;
+ this.displayName = builder.displayName;
+ }
+
+ @Override
+ public String toString() {
+ if (displayName != null) {
+ return MoreObjects.toStringHelper(this).addValue(displayName).toString();
+ }
+ MoreObjects.ToStringHelper helper =
+ MoreObjects.toStringHelper(this)
+ .add("pathType", pathType)
+ .add("roots", roots)
+ .add("supportedFeatures", supportedFeatures)
+ .add("workingDirectory", workingDirectory);
+ if (!nameDisplayNormalization.isEmpty()) {
+ helper.add("nameDisplayNormalization", nameDisplayNormalization);
+ }
+ if (!nameCanonicalNormalization.isEmpty()) {
+ helper.add("nameCanonicalNormalization", nameCanonicalNormalization);
+ }
+ helper
+ .add("pathEqualityUsesCanonicalForm", pathEqualityUsesCanonicalForm)
+ .add("blockSize", blockSize)
+ .add("maxSize", maxSize);
+ if (maxCacheSize != Builder.DEFAULT_MAX_CACHE_SIZE) {
+ helper.add("maxCacheSize", maxCacheSize);
+ }
+ if (!attributeViews.isEmpty()) {
+ helper.add("attributeViews", attributeViews);
+ }
+ if (!attributeProviders.isEmpty()) {
+ helper.add("attributeProviders", attributeProviders);
+ }
+ if (!defaultAttributeValues.isEmpty()) {
+ helper.add("defaultAttributeValues", defaultAttributeValues);
+ }
+ if (watchServiceConfig != WatchServiceConfiguration.DEFAULT) {
+ helper.add("watchServiceConfig", watchServiceConfig);
+ }
+ return helper.toString();
+ }
+
+ /**
+ * Returns a new mutable builder that initially contains the same settings as this configuration.
+ */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /** Mutable builder for {@link Configuration} objects. */
+ public static final class Builder {
+
+ /** 8 KB. */
+ public static final int DEFAULT_BLOCK_SIZE = 8192;
+
+ /** 4 GB. */
+ public static final long DEFAULT_MAX_SIZE = 4L * 1024 * 1024 * 1024;
+
+ /** Equal to the configured max size. */
+ public static final long DEFAULT_MAX_CACHE_SIZE = -1;
+
+ // Path configuration
+ private final PathType pathType;
+ private ImmutableSet<PathNormalization> nameDisplayNormalization = ImmutableSet.of();
+ private ImmutableSet<PathNormalization> nameCanonicalNormalization = ImmutableSet.of();
+ private boolean pathEqualityUsesCanonicalForm = false;
+
+ // Disk configuration
+ private int blockSize = DEFAULT_BLOCK_SIZE;
+ private long maxSize = DEFAULT_MAX_SIZE;
+ private long maxCacheSize = DEFAULT_MAX_CACHE_SIZE;
+
+ // Attribute configuration
+ private ImmutableSet<String> attributeViews = ImmutableSet.of();
+ private Set<AttributeProvider> attributeProviders = null;
+ private Map<String, Object> defaultAttributeValues;
+
+ // Watch service
+ private WatchServiceConfiguration watchServiceConfig = WatchServiceConfiguration.DEFAULT;
+
+ // Other
+ private ImmutableSet<String> roots = ImmutableSet.of();
+ private String workingDirectory;
+ private ImmutableSet<Feature> supportedFeatures = ImmutableSet.of();
+ private String displayName;
+
+ private Builder(PathType pathType) {
+ this.pathType = checkNotNull(pathType);
+ }
+
+ private Builder(Configuration configuration) {
+ this.pathType = configuration.pathType;
+ this.nameDisplayNormalization = configuration.nameDisplayNormalization;
+ this.nameCanonicalNormalization = configuration.nameCanonicalNormalization;
+ this.pathEqualityUsesCanonicalForm = configuration.pathEqualityUsesCanonicalForm;
+ this.blockSize = configuration.blockSize;
+ this.maxSize = configuration.maxSize;
+ this.maxCacheSize = configuration.maxCacheSize;
+ this.attributeViews = configuration.attributeViews;
+ this.attributeProviders =
+ configuration.attributeProviders.isEmpty()
+ ? null
+ : new HashSet<>(configuration.attributeProviders);
+ this.defaultAttributeValues =
+ configuration.defaultAttributeValues.isEmpty()
+ ? null
+ : new HashMap<>(configuration.defaultAttributeValues);
+ this.watchServiceConfig = configuration.watchServiceConfig;
+ this.roots = configuration.roots;
+ this.workingDirectory = configuration.workingDirectory;
+ this.supportedFeatures = configuration.supportedFeatures;
+ // displayName intentionally not copied from the Configuration
+ }
+
+ /**
+ * Sets the normalizations that will be applied to the display form of filenames. The display
+ * form is used in the {@code toString()} of {@code Path} objects.
+ */
+ public Builder setNameDisplayNormalization(PathNormalization first, PathNormalization... more) {
+ this.nameDisplayNormalization = checkNormalizations(Lists.asList(first, more));
+ return this;
+ }
+
+ /**
+ * Returns the normalizations that will be applied to the canonical form of filenames in the
+ * file system. The canonical form is used to determine the equality of two filenames when
+ * performing a file lookup.
+ */
+ public Builder setNameCanonicalNormalization(
+ PathNormalization first, PathNormalization... more) {
+ this.nameCanonicalNormalization = checkNormalizations(Lists.asList(first, more));
+ return this;
+ }
+
+ private ImmutableSet<PathNormalization> checkNormalizations(
+ List<PathNormalization> normalizations) {
+ PathNormalization none = null;
+ PathNormalization normalization = null;
+ PathNormalization caseFold = null;
+ for (PathNormalization n : normalizations) {
+ checkNotNull(n);
+ checkNormalizationNotSet(n, none);
+
+ switch (n) {
+ case NONE:
+ none = n;
+ break;
+ case NFC:
+ case NFD:
+ checkNormalizationNotSet(n, normalization);
+ normalization = n;
+ break;
+ case CASE_FOLD_UNICODE:
+ case CASE_FOLD_ASCII:
+ checkNormalizationNotSet(n, caseFold);
+ caseFold = n;
+ break;
+ default:
+ throw new AssertionError(); // there are no other cases
+ }
+ }
+
+ if (none != null) {
+ return ImmutableSet.of();
+ }
+ return Sets.immutableEnumSet(normalizations);
+ }
+
+ private static void checkNormalizationNotSet(
+ PathNormalization n, @NullableDecl PathNormalization set) {
+ if (set != null) {
+ throw new IllegalArgumentException(
+ "can't set normalization " + n + ": normalization " + set + " already set");
+ }
+ }
+
+ /**
+ * Sets whether {@code Path} objects in the file system use the canonical form (true) or the
+ * display form (false) of filenames for determining equality of two paths.
+ *
+ * <p>The default is false.
+ */
+ public Builder setPathEqualityUsesCanonicalForm(boolean useCanonicalForm) {
+ this.pathEqualityUsesCanonicalForm = useCanonicalForm;
+ return this;
+ }
+
+ /**
+ * Sets the block size (in bytes) for the file system to use. All regular files will be
+ * allocated blocks of the given size, so this is the minimum granularity for file size.
+ *
+ * <p>The default is 8192 bytes (8 KB).
+ */
+ public Builder setBlockSize(int blockSize) {
+ checkArgument(blockSize > 0, "blockSize (%s) must be positive", blockSize);
+ this.blockSize = blockSize;
+ return this;
+ }
+
+ /**
+ * Sets the maximum size (in bytes) for the file system's in-memory file storage. This maximum
+ * size determines the maximum number of blocks that can be allocated to regular files, so it
+ * should generally be a multiple of the {@linkplain #setBlockSize(int) block size}. The actual
+ * maximum size will be the nearest multiple of the block size that is less than or equal to the
+ * given size.
+ *
+ * <p><b>Note:</b> The in-memory file storage will not be eagerly initialized to this size, so
+ * it won't use more memory than is needed for the files you create. Also note that in addition
+ * to this limit, you will of course be limited by the amount of heap space available to the JVM
+ * and the amount of heap used by other objects, both in the file system and elsewhere.
+ *
+ * <p>The default is 4 GB.
+ */
+ public Builder setMaxSize(long maxSize) {
+ checkArgument(maxSize > 0, "maxSize (%s) must be positive", maxSize);
+ this.maxSize = maxSize;
+ return this;
+ }
+
+ /**
+ * Sets the maximum amount of unused space (in bytes) in the file system's in-memory file
+ * storage that should be cached for reuse. By default, this will be equal to the {@linkplain
+ * #setMaxSize(long) maximum size} of the storage, meaning that all space that is freed when
+ * files are truncated or deleted is cached for reuse. This helps to avoid lots of garbage
+ * collection when creating and deleting many files quickly. This can be set to 0 to disable
+ * caching entirely (all freed blocks become available for garbage collection) or to some other
+ * number to put an upper bound on the maximum amount of unused space the file system will keep
+ * around.
+ *
+ * <p>Like the maximum size, the actual value will be the closest multiple of the block size
+ * that is less than or equal to the given size.
+ */
+ public Builder setMaxCacheSize(long maxCacheSize) {
+ checkArgument(maxCacheSize >= 0, "maxCacheSize (%s) may not be negative", maxCacheSize);
+ this.maxCacheSize = maxCacheSize;
+ return this;
+ }
+
+ /**
+ * Sets the attribute views the file system should support. By default, the following views may
+ * be specified:
+ *
+ * <table>
+ * <tr>
+ * <td><b>Name</b></td>
+ * <td><b>View Interface</b></td>
+ * <td><b>Attributes Interface</b></td>
+ * </tr>
+ * <tr>
+ * <td>{@code "basic"}</td>
+ * <td>{@link java.nio.file.attribute.BasicFileAttributeView BasicFileAttributeView}</td>
+ * <td>{@link java.nio.file.attribute.BasicFileAttributes BasicFileAttributes}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "owner"}</td>
+ * <td>{@link java.nio.file.attribute.FileOwnerAttributeView FileOwnerAttributeView}</td>
+ * <td>--</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "posix"}</td>
+ * <td>{@link java.nio.file.attribute.PosixFileAttributeView PosixFileAttributeView}</td>
+ * <td>{@link java.nio.file.attribute.PosixFileAttributes PosixFileAttributes}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "unix"}</td>
+ * <td>--</td>
+ * <td>--</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "dos"}</td>
+ * <td>{@link java.nio.file.attribute.DosFileAttributeView DosFileAttributeView}</td>
+ * <td>{@link java.nio.file.attribute.DosFileAttributes DosFileAttributes}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "acl"}</td>
+ * <td>{@link java.nio.file.attribute.AclFileAttributeView AclFileAttributeView}</td>
+ * <td>--</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "user"}</td>
+ * <td>{@link java.nio.file.attribute.UserDefinedFileAttributeView UserDefinedFileAttributeView}</td>
+ * <td>--</td>
+ * </tr>
+ * </table>
+ *
+ * <p>If any other views should be supported, attribute providers for those views must be
+ * {@linkplain #addAttributeProvider(AttributeProvider) added}.
+ */
+ public Builder setAttributeViews(String first, String... more) {
+ this.attributeViews = ImmutableSet.copyOf(Lists.asList(first, more));
+ return this;
+ }
+
+ /** Adds an attribute provider for a custom view for the file system to support. */
+ public Builder addAttributeProvider(AttributeProvider provider) {
+ checkNotNull(provider);
+ if (attributeProviders == null) {
+ attributeProviders = new HashSet<>();
+ }
+ attributeProviders.add(provider);
+ return this;
+ }
+
+ /**
+ * Sets the default value to use for the given file attribute when creating new files. The
+ * attribute must be in the form "view:attribute". The value must be of a type that the provider
+ * for the view accepts.
+ *
+ * <p>For the included attribute views, default values can be set for the following attributes:
+ *
+ * <table>
+ * <tr>
+ * <th>Attribute</th>
+ * <th>Legal Types</th>
+ * </tr>
+ * <tr>
+ * <td>{@code "owner:owner"}</td>
+ * <td>{@code String} (user name)</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "posix:group"}</td>
+ * <td>{@code String} (group name)</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "posix:permissions"}</td>
+ * <td>{@code String} (format "rwxrw-r--"), {@code Set<PosixFilePermission>}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "dos:readonly"}</td>
+ * <td>{@code Boolean}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "dos:hidden"}</td>
+ * <td>{@code Boolean}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "dos:archive"}</td>
+ * <td>{@code Boolean}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "dos:system"}</td>
+ * <td>{@code Boolean}</td>
+ * </tr>
+ * <tr>
+ * <td>{@code "acl:acl"}</td>
+ * <td>{@code List<AclEntry>}</td>
+ * </tr>
+ * </table>
+ */
+ public Builder setDefaultAttributeValue(String attribute, Object value) {
+ checkArgument(
+ ATTRIBUTE_PATTERN.matcher(attribute).matches(),
+ "attribute (%s) must be of the form \"view:attribute\"",
+ attribute);
+ checkNotNull(value);
+
+ if (defaultAttributeValues == null) {
+ defaultAttributeValues = new HashMap<>();
+ }
+
+ defaultAttributeValues.put(attribute, value);
+ return this;
+ }
+
+ private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("[^:]+:[^:]+");
+
+ /**
+ * Sets the roots for the file system.
+ *
+ * @throws InvalidPathException if any of the given roots is not a valid path for this builder's
+ * path type
+ * @throws IllegalArgumentException if any of the given roots is a valid path for this builder's
+ * path type but is not a root path with no name elements
+ */
+ public Builder setRoots(String first, String... more) {
+ List<String> roots = Lists.asList(first, more);
+ for (String root : roots) {
+ PathType.ParseResult parseResult = pathType.parsePath(root);
+ checkArgument(parseResult.isRoot(), "invalid root: %s", root);
+ }
+ this.roots = ImmutableSet.copyOf(roots);
+ return this;
+ }
+
+ /**
+ * Sets the path to the working directory for the file system. The working directory must be an
+ * absolute path starting with one of the configured roots.
+ *
+ * @throws InvalidPathException if the given path is not valid for this builder's path type
+ * @throws IllegalArgumentException if the given path is valid for this builder's path type but
+ * is not an absolute path
+ */
+ public Builder setWorkingDirectory(String workingDirectory) {
+ PathType.ParseResult parseResult = pathType.parsePath(workingDirectory);
+ checkArgument(
+ parseResult.isAbsolute(),
+ "working directory must be an absolute path: %s",
+ workingDirectory);
+ this.workingDirectory = checkNotNull(workingDirectory);
+ return this;
+ }
+
+ /**
+ * Sets the given features to be supported by the file system. Any features not provided here
+ * will not be supported.
+ */
+ public Builder setSupportedFeatures(Feature... features) {
+ supportedFeatures = Sets.immutableEnumSet(Arrays.asList(features));
+ return this;
+ }
+
+ /**
+ * Sets the configuration that {@link WatchService} instances created by the file system should
+ * use. The default configuration polls watched directories for changes every 5 seconds.
+ *
+ * @since 1.1
+ */
+ public Builder setWatchServiceConfiguration(WatchServiceConfiguration config) {
+ this.watchServiceConfig = checkNotNull(config);
+ return this;
+ }
+
+ private Builder setDisplayName(String displayName) {
+ this.displayName = checkNotNull(displayName);
+ return this;
+ }
+
+ /** Creates a new immutable configuration object from this builder. */
+ public Configuration build() {
+ return new Configuration(this);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Directory.java b/jimfs/src/main/java/com/google/common/jimfs/Directory.java
new file mode 100644
index 0000000..aaab83b
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Directory.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableSortedSet;
+import java.util.Iterator;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * A table of {@linkplain DirectoryEntry directory entries}.
+ *
+ * @author Colin Decker
+ */
+final class Directory extends File implements Iterable<DirectoryEntry> {
+
+ /** The entry linking to this directory in its parent directory. */
+ private DirectoryEntry entryInParent;
+
+ /** Creates a new normal directory with the given ID. */
+ public static Directory create(int id) {
+ return new Directory(id);
+ }
+
+ /** Creates a new root directory with the given ID and name. */
+ public static Directory createRoot(int id, Name name) {
+ return new Directory(id, name);
+ }
+
+ private Directory(int id) {
+ super(id);
+ put(new DirectoryEntry(this, Name.SELF, this));
+ }
+
+ private Directory(int id, Name rootName) {
+ this(id);
+ linked(new DirectoryEntry(this, rootName, this));
+ }
+
+ /**
+ * Creates a copy of this directory. The copy does <i>not</i> contain a copy of the entries in
+ * this directory.
+ */
+ @Override
+ Directory copyWithoutContent(int id) {
+ return Directory.create(id);
+ }
+
+ /**
+ * Returns the entry linking to this directory in its parent. If this directory has been deleted,
+ * this returns the entry for it in the directory it was in when it was deleted.
+ */
+ public DirectoryEntry entryInParent() {
+ return entryInParent;
+ }
+
+ /**
+ * Returns the parent of this directory. If this directory has been deleted, this returns the
+ * directory it was in when it was deleted.
+ */
+ public Directory parent() {
+ return entryInParent.directory();
+ }
+
+ @Override
+ void linked(DirectoryEntry entry) {
+ File parent = entry.directory(); // handles null check
+ this.entryInParent = entry;
+ forcePut(new DirectoryEntry(this, Name.PARENT, parent));
+ }
+
+ @Override
+ void unlinked() {
+ // we don't actually remove the parent link when this directory is unlinked, but the parent's
+ // link count should go down all the same
+ parent().decrementLinkCount();
+ }
+
+ /** Returns the number of entries in this directory. */
+ @VisibleForTesting
+ int entryCount() {
+ return entryCount;
+ }
+
+ /** Returns true if this directory has no entries other than those to itself and its parent. */
+ public boolean isEmpty() {
+ return entryCount() == 2;
+ }
+
+ /** Returns the entry for the given name in this table or null if no such entry exists. */
+ @NullableDecl
+ public DirectoryEntry get(Name name) {
+ int index = bucketIndex(name, table.length);
+
+ DirectoryEntry entry = table[index];
+ while (entry != null) {
+ if (name.equals(entry.name())) {
+ return entry;
+ }
+
+ entry = entry.next;
+ }
+ return null;
+ }
+
+ /**
+ * Links the given name to the given file in this directory.
+ *
+ * @throws IllegalArgumentException if {@code name} is a reserved name such as "." or if an entry
+ * already exists for the name
+ */
+ public void link(Name name, File file) {
+ DirectoryEntry entry = new DirectoryEntry(this, checkNotReserved(name, "link"), file);
+ put(entry);
+ file.linked(entry);
+ }
+
+ /**
+ * Unlinks the given name from the file it is linked to.
+ *
+ * @throws IllegalArgumentException if {@code name} is a reserved name such as "." or no entry
+ * exists for the name
+ */
+ public void unlink(Name name) {
+ DirectoryEntry entry = remove(checkNotReserved(name, "unlink"));
+ entry.file().unlinked();
+ }
+
+ /**
+ * Creates an immutable sorted snapshot of the names this directory contains, excluding "." and
+ * "..".
+ */
+ public ImmutableSortedSet<Name> snapshot() {
+ ImmutableSortedSet.Builder<Name> builder =
+ new ImmutableSortedSet.Builder<>(Name.displayOrdering());
+
+ for (DirectoryEntry entry : this) {
+ if (!isReserved(entry.name())) {
+ builder.add(entry.name());
+ }
+ }
+
+ return builder.build();
+ }
+
+ /** Checks that the given name is not "." or "..". Those names cannot be set/removed by users. */
+ private static Name checkNotReserved(Name name, String action) {
+ if (isReserved(name)) {
+ throw new IllegalArgumentException("cannot " + action + ": " + name);
+ }
+ return name;
+ }
+
+ /** Returns true if the given name is "." or "..". */
+ private static boolean isReserved(Name name) {
+ // all "." and ".." names are canonicalized to the same objects, so we can use identity
+ return name == Name.SELF || name == Name.PARENT;
+ }
+
+ // Simple hash table code to avoid allocation of Map.Entry objects when DirectoryEntry can
+ // serve the same purpose.
+
+ private static final int INITIAL_CAPACITY = 16;
+ private static final int INITIAL_RESIZE_THRESHOLD = (int) (INITIAL_CAPACITY * 0.75);
+
+ private DirectoryEntry[] table = new DirectoryEntry[INITIAL_CAPACITY];
+ private int resizeThreshold = INITIAL_RESIZE_THRESHOLD;
+
+ private int entryCount;
+
+ /** Returns the index of the bucket in the array where an entry for the given name should go. */
+ private static int bucketIndex(Name name, int tableLength) {
+ return name.hashCode() & (tableLength - 1);
+ }
+
+ /**
+ * Adds the given entry to the directory.
+ *
+ * @throws IllegalArgumentException if an entry with the given entry's name already exists in the
+ * directory
+ */
+ @VisibleForTesting
+ void put(DirectoryEntry entry) {
+ put(entry, false);
+ }
+
+ /**
+ * Adds the given entry to the directory. {@code overwriteExisting} determines whether an existing
+ * entry with the same name should be overwritten or an exception should be thrown.
+ */
+ private void put(DirectoryEntry entry, boolean overwriteExisting) {
+ int index = bucketIndex(entry.name(), table.length);
+
+ // find the place the new entry should go, ensuring an entry with the same name doesn't already
+ // exist along the way
+ DirectoryEntry prev = null;
+ DirectoryEntry curr = table[index];
+ while (curr != null) {
+ if (curr.name().equals(entry.name())) {
+ if (overwriteExisting) {
+ // just replace the existing entry; no need to expand, and entryCount doesn't change
+ if (prev != null) {
+ prev.next = entry;
+ } else {
+ table[index] = entry;
+ }
+ entry.next = curr.next;
+ curr.next = null;
+ entry.file().incrementLinkCount();
+ return;
+ } else {
+ throw new IllegalArgumentException("entry '" + entry.name() + "' already exists");
+ }
+ }
+
+ prev = curr;
+ curr = curr.next;
+ }
+
+ entryCount++;
+ if (expandIfNeeded()) {
+ // if the table was expanded, the index/entry we found is no longer applicable, so just add
+ // the entry normally
+ index = bucketIndex(entry.name(), table.length);
+ addToBucket(index, table, entry);
+ } else {
+ // otherwise, we just can use the index/entry we found
+ if (prev != null) {
+ prev.next = entry;
+ } else {
+ table[index] = entry;
+ }
+ }
+
+ entry.file().incrementLinkCount();
+ }
+
+ /**
+ * Adds the given entry to the directory, overwriting an existing entry with the same name if such
+ * an entry exists.
+ */
+ private void forcePut(DirectoryEntry entry) {
+ put(entry, true);
+ }
+
+ private boolean expandIfNeeded() {
+ if (entryCount <= resizeThreshold) {
+ return false;
+ }
+
+ DirectoryEntry[] newTable = new DirectoryEntry[table.length << 1];
+
+ // redistribute all current entries in the new table
+ for (DirectoryEntry entry : table) {
+ while (entry != null) {
+ int index = bucketIndex(entry.name(), newTable.length);
+ addToBucket(index, newTable, entry);
+ DirectoryEntry next = entry.next;
+ // set entry.next to null; it's always the last entry in its bucket after being added
+ entry.next = null;
+ entry = next;
+ }
+ }
+
+ this.table = newTable;
+ resizeThreshold <<= 1;
+ return true;
+ }
+
+ private static void addToBucket(
+ int bucketIndex, DirectoryEntry[] table, DirectoryEntry entryToAdd) {
+ DirectoryEntry prev = null;
+ DirectoryEntry existing = table[bucketIndex];
+ while (existing != null) {
+ prev = existing;
+ existing = existing.next;
+ }
+
+ if (prev != null) {
+ prev.next = entryToAdd;
+ } else {
+ table[bucketIndex] = entryToAdd;
+ }
+ }
+
+ /**
+ * Removes and returns the entry for the given name from the directory.
+ *
+ * @throws IllegalArgumentException if there is no entry with the given name in the directory
+ */
+ @VisibleForTesting
+ DirectoryEntry remove(Name name) {
+ int index = bucketIndex(name, table.length);
+
+ DirectoryEntry prev = null;
+ DirectoryEntry entry = table[index];
+ while (entry != null) {
+ if (name.equals(entry.name())) {
+ if (prev != null) {
+ prev.next = entry.next;
+ } else {
+ table[index] = entry.next;
+ }
+
+ entry.next = null;
+ entryCount--;
+ entry.file().decrementLinkCount();
+ return entry;
+ }
+
+ prev = entry;
+ entry = entry.next;
+ }
+
+ throw new IllegalArgumentException("no entry matching '" + name + "' in this directory");
+ }
+
+ @Override
+ public Iterator<DirectoryEntry> iterator() {
+ return new AbstractIterator<DirectoryEntry>() {
+ int index;
+ @NullableDecl DirectoryEntry entry;
+
+ @Override
+ protected DirectoryEntry computeNext() {
+ if (entry != null) {
+ entry = entry.next;
+ }
+
+ while (entry == null && index < table.length) {
+ entry = table[index++];
+ }
+
+ return entry != null ? entry : endOfData();
+ }
+ };
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/DirectoryEntry.java b/jimfs/src/main/java/com/google/common/jimfs/DirectoryEntry.java
new file mode 100644
index 0000000..5bff50f
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/DirectoryEntry.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.MoreObjects;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.NotLinkException;
+import java.nio.file.Path;
+import java.util.Objects;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Entry in a directory, containing references to the directory itself, the file the entry links to
+ * and the name of the entry.
+ *
+ * <p>May also represent a non-existent entry if the name does not link to any file in the
+ * directory.
+ */
+final class DirectoryEntry {
+
+ private final Directory directory;
+ private final Name name;
+
+ @NullableDecl private final File file;
+
+ @NullableDecl DirectoryEntry next; // for use in Directory
+
+ DirectoryEntry(Directory directory, Name name, @NullableDecl File file) {
+ this.directory = checkNotNull(directory);
+ this.name = checkNotNull(name);
+ this.file = file;
+ }
+
+ /** Returns {@code true} if and only if this entry represents an existing file. */
+ public boolean exists() {
+ return file != null;
+ }
+
+ /**
+ * Checks that this entry exists, throwing an exception if not.
+ *
+ * @return this
+ * @throws NoSuchFileException if this entry does not exist
+ */
+ public DirectoryEntry requireExists(Path pathForException) throws NoSuchFileException {
+ if (!exists()) {
+ throw new NoSuchFileException(pathForException.toString());
+ }
+ return this;
+ }
+
+ /**
+ * Checks that this entry does not exist, throwing an exception if it does.
+ *
+ * @return this
+ * @throws FileAlreadyExistsException if this entry does not exist
+ */
+ public DirectoryEntry requireDoesNotExist(Path pathForException)
+ throws FileAlreadyExistsException {
+ if (exists()) {
+ throw new FileAlreadyExistsException(pathForException.toString());
+ }
+ return this;
+ }
+
+ /**
+ * Checks that this entry exists and links to a directory, throwing an exception if not.
+ *
+ * @return this
+ * @throws NoSuchFileException if this entry does not exist
+ * @throws NotDirectoryException if this entry does not link to a directory
+ */
+ public DirectoryEntry requireDirectory(Path pathForException)
+ throws NoSuchFileException, NotDirectoryException {
+ requireExists(pathForException);
+ if (!file().isDirectory()) {
+ throw new NotDirectoryException(pathForException.toString());
+ }
+ return this;
+ }
+
+ /**
+ * Checks that this entry exists and links to a symbolic link, throwing an exception if not.
+ *
+ * @return this
+ * @throws NoSuchFileException if this entry does not exist
+ * @throws NotLinkException if this entry does not link to a symbolic link
+ */
+ public DirectoryEntry requireSymbolicLink(Path pathForException)
+ throws NoSuchFileException, NotLinkException {
+ requireExists(pathForException);
+ if (!file().isSymbolicLink()) {
+ throw new NotLinkException(pathForException.toString());
+ }
+ return this;
+ }
+
+ /** Returns the directory containing this entry. */
+ public Directory directory() {
+ return directory;
+ }
+
+ /** Returns the name of this entry. */
+ public Name name() {
+ return name;
+ }
+
+ /**
+ * Returns the file this entry links to.
+ *
+ * @throws IllegalStateException if the file does not exist
+ */
+ public File file() {
+ checkState(exists());
+ return file;
+ }
+
+ /** Returns the file this entry links to or {@code null} if the file does not exist */
+ @NullableDecl
+ public File fileOrNull() {
+ return file;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof DirectoryEntry) {
+ DirectoryEntry other = (DirectoryEntry) obj;
+ return directory.equals(other.directory)
+ && name.equals(other.name)
+ && Objects.equals(file, other.file);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(directory, name, file);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("directory", directory)
+ .add("name", name)
+ .add("file", file)
+ .toString();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/DosAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/DosAttributeProvider.java
new file mode 100644
index 0000000..51bf96b
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/DosAttributeProvider.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.DosFileAttributeView;
+import java.nio.file.attribute.DosFileAttributes;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Attribute provider that provides the {@link DosFileAttributeView} ("dos") and allows the reading
+ * of {@link DosFileAttributes}.
+ *
+ * @author Colin Decker
+ */
+final class DosAttributeProvider extends AttributeProvider {
+
+ private static final ImmutableSet<String> ATTRIBUTES =
+ ImmutableSet.of("readonly", "hidden", "archive", "system");
+
+ private static final ImmutableSet<String> INHERITED_VIEWS = ImmutableSet.of("basic", "owner");
+
+ @Override
+ public String name() {
+ return "dos";
+ }
+
+ @Override
+ public ImmutableSet<String> inherits() {
+ return INHERITED_VIEWS;
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ return ATTRIBUTES;
+ }
+
+ @Override
+ public ImmutableMap<String, ?> defaultValues(Map<String, ?> userProvidedDefaults) {
+ return ImmutableMap.of(
+ "dos:readonly", getDefaultValue("dos:readonly", userProvidedDefaults),
+ "dos:hidden", getDefaultValue("dos:hidden", userProvidedDefaults),
+ "dos:archive", getDefaultValue("dos:archive", userProvidedDefaults),
+ "dos:system", getDefaultValue("dos:system", userProvidedDefaults));
+ }
+
+ private static Boolean getDefaultValue(String attribute, Map<String, ?> userProvidedDefaults) {
+ Object userProvidedValue = userProvidedDefaults.get(attribute);
+ if (userProvidedValue != null) {
+ return checkType("dos", attribute, userProvidedValue, Boolean.class);
+ }
+
+ return false;
+ }
+
+ @NullableDecl
+ @Override
+ public Object get(File file, String attribute) {
+ if (ATTRIBUTES.contains(attribute)) {
+ return file.getAttribute("dos", attribute);
+ }
+
+ return null;
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ if (supports(attribute)) {
+ checkNotCreate(view, attribute, create);
+ file.setAttribute("dos", attribute, checkType(view, attribute, value, Boolean.class));
+ }
+ }
+
+ @Override
+ public Class<DosFileAttributeView> viewType() {
+ return DosFileAttributeView.class;
+ }
+
+ @Override
+ public DosFileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ return new View(lookup, (BasicFileAttributeView) inheritedViews.get("basic"));
+ }
+
+ @Override
+ public Class<DosFileAttributes> attributesType() {
+ return DosFileAttributes.class;
+ }
+
+ @Override
+ public DosFileAttributes readAttributes(File file) {
+ return new Attributes(file);
+ }
+
+ /** Implementation of {@link DosFileAttributeView}. */
+ private static final class View extends AbstractAttributeView implements DosFileAttributeView {
+
+ private final BasicFileAttributeView basicView;
+
+ public View(FileLookup lookup, BasicFileAttributeView basicView) {
+ super(lookup);
+ this.basicView = checkNotNull(basicView);
+ }
+
+ @Override
+ public String name() {
+ return "dos";
+ }
+
+ @Override
+ public DosFileAttributes readAttributes() throws IOException {
+ return new Attributes(lookupFile());
+ }
+
+ @Override
+ public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime)
+ throws IOException {
+ basicView.setTimes(lastModifiedTime, lastAccessTime, createTime);
+ }
+
+ @Override
+ public void setReadOnly(boolean value) throws IOException {
+ lookupFile().setAttribute("dos", "readonly", value);
+ }
+
+ @Override
+ public void setHidden(boolean value) throws IOException {
+ lookupFile().setAttribute("dos", "hidden", value);
+ }
+
+ @Override
+ public void setSystem(boolean value) throws IOException {
+ lookupFile().setAttribute("dos", "system", value);
+ }
+
+ @Override
+ public void setArchive(boolean value) throws IOException {
+ lookupFile().setAttribute("dos", "archive", value);
+ }
+ }
+
+ /** Implementation of {@link DosFileAttributes}. */
+ static class Attributes extends BasicAttributeProvider.Attributes implements DosFileAttributes {
+
+ private final boolean readOnly;
+ private final boolean hidden;
+ private final boolean archive;
+ private final boolean system;
+
+ protected Attributes(File file) {
+ super(file);
+ this.readOnly = (boolean) file.getAttribute("dos", "readonly");
+ this.hidden = (boolean) file.getAttribute("dos", "hidden");
+ this.archive = (boolean) file.getAttribute("dos", "archive");
+ this.system = (boolean) file.getAttribute("dos", "system");
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ @Override
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ @Override
+ public boolean isArchive() {
+ return archive;
+ }
+
+ @Override
+ public boolean isSystem() {
+ return system;
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/DowngradedDirectoryStream.java b/jimfs/src/main/java/com/google/common/jimfs/DowngradedDirectoryStream.java
new file mode 100644
index 0000000..3639fd0
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/DowngradedDirectoryStream.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.SecureDirectoryStream;
+import java.util.Iterator;
+
+/**
+ * A thin wrapper around a {@link SecureDirectoryStream} that exists only to implement {@link
+ * DirectoryStream} and NOT implement {@link SecureDirectoryStream}.
+ *
+ * @author Colin Decker
+ */
+final class DowngradedDirectoryStream implements DirectoryStream<Path> {
+
+ private final SecureDirectoryStream<Path> secureDirectoryStream;
+
+ DowngradedDirectoryStream(SecureDirectoryStream<Path> secureDirectoryStream) {
+ this.secureDirectoryStream = checkNotNull(secureDirectoryStream);
+ }
+
+ @Override
+ public Iterator<Path> iterator() {
+ return secureDirectoryStream.iterator();
+ }
+
+ @Override
+ public void close() throws IOException {
+ secureDirectoryStream.close();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/DowngradedSeekableByteChannel.java b/jimfs/src/main/java/com/google/common/jimfs/DowngradedSeekableByteChannel.java
new file mode 100644
index 0000000..5d4db8b
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/DowngradedSeekableByteChannel.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+
+/**
+ * A thin wrapper around a {@link FileChannel} that exists only to implement {@link
+ * SeekableByteChannel} but NOT extend {@link FileChannel}.
+ *
+ * @author Colin Decker
+ */
+final class DowngradedSeekableByteChannel implements SeekableByteChannel {
+
+ private final FileChannel channel;
+
+ DowngradedSeekableByteChannel(FileChannel channel) {
+ this.channel = checkNotNull(channel);
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException {
+ return channel.read(dst);
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ return channel.write(src);
+ }
+
+ @Override
+ public long position() throws IOException {
+ return channel.position();
+ }
+
+ @Override
+ public SeekableByteChannel position(long newPosition) throws IOException {
+ channel.position(newPosition);
+ return this;
+ }
+
+ @Override
+ public long size() throws IOException {
+ return channel.size();
+ }
+
+ @Override
+ public SeekableByteChannel truncate(long size) throws IOException {
+ channel.truncate(size);
+ return this;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return channel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ channel.close();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Feature.java b/jimfs/src/main/java/com/google/common/jimfs/Feature.java
new file mode 100644
index 0000000..d8e8b3d
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Feature.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.attribute.FileAttribute;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Optional file system features that may be supported or unsupported by a Jimfs file system
+ * instance.
+ *
+ * @author Colin Decker
+ */
+public enum Feature {
+
+ /**
+ * Feature controlling support for hard links to regular files.
+ *
+ * <p>Affected method:
+ *
+ * <ul>
+ * <li>{@link Files#createLink(Path, Path)}
+ * </ul>
+ *
+ * <p>If this feature is not enabled, this method will throw {@link
+ * UnsupportedOperationException}.
+ */
+ LINKS,
+
+ /**
+ * Feature controlling support for symbolic links.
+ *
+ * <p>Affected methods:
+ *
+ * <ul>
+ * <li>{@link Files#createSymbolicLink(Path, Path, FileAttribute...)}
+ * <li>{@link Files#readSymbolicLink(Path)}
+ * </ul>
+ *
+ * <p>If this feature is not enabled, these methods will throw {@link
+ * UnsupportedOperationException}.
+ */
+ SYMBOLIC_LINKS,
+
+ /**
+ * Feature controlling support for {@link SecureDirectoryStream}.
+ *
+ * <p>Affected methods:
+ *
+ * <ul>
+ * <li>{@link Files#newDirectoryStream(Path)}
+ * <li>{@link Files#newDirectoryStream(Path, DirectoryStream.Filter)}
+ * <li>{@link Files#newDirectoryStream(Path, String)}
+ * </ul>
+ *
+ * <p>If this feature is enabled, the {@link DirectoryStream} instances returned by these methods
+ * will also implement {@link SecureDirectoryStream}.
+ */
+ SECURE_DIRECTORY_STREAM,
+
+ /**
+ * Feature controlling support for {@link FileChannel}.
+ *
+ * <p>Affected methods:
+ *
+ * <ul>
+ * <li>{@link Files#newByteChannel(Path, OpenOption...)}
+ * <li>{@link Files#newByteChannel(Path, Set, FileAttribute...)}
+ * <li>{@link FileChannel#open(Path, OpenOption...)}
+ * <li>{@link FileChannel#open(Path, Set, FileAttribute...)}
+ * <li>{@link AsynchronousFileChannel#open(Path, OpenOption...)}
+ * <li>{@link AsynchronousFileChannel#open(Path, Set, ExecutorService, FileAttribute...)}
+ * </ul>
+ *
+ * <p>If this feature is not enabled, the {@link SeekableByteChannel} instances returned by the
+ * {@code Files} methods will not be {@code FileChannel} instances and the {@code
+ * FileChannel.open} and {@code AsynchronousFileChannel.open} methods will throw {@link
+ * UnsupportedOperationException}.
+ */
+ // TODO(cgdecker): Should support for AsynchronousFileChannel be a separate feature?
+ FILE_CHANNEL
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/File.java b/jimfs/src/main/java/com/google/common/jimfs/File.java
new file mode 100644
index 0000000..ce1cc00
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/File.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Table;
+import java.io.IOException;
+import java.util.concurrent.locks.ReadWriteLock;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * A file object, containing both the file's metadata and content.
+ *
+ * @author Colin Decker
+ */
+public abstract class File {
+
+ private final int id;
+
+ private int links;
+
+ private long creationTime;
+ private long lastAccessTime;
+ private long lastModifiedTime;
+
+ @NullableDecl // null when only the basic view is used (default)
+ private Table<String, String, Object> attributes;
+
+ File(int id) {
+ this.id = id;
+
+ long now = System.currentTimeMillis(); // TODO(cgdecker): Use a Clock
+ this.creationTime = now;
+ this.lastAccessTime = now;
+ this.lastModifiedTime = now;
+ }
+
+ /** Returns the ID of this file. */
+ public int id() {
+ return id;
+ }
+
+ /**
+ * Returns the size, in bytes, of this file's content. Directories and symbolic links have a size
+ * of 0.
+ */
+ public long size() {
+ return 0;
+ }
+
+ /** Returns whether or not this file is a directory. */
+ public final boolean isDirectory() {
+ return this instanceof Directory;
+ }
+
+ /** Returns whether or not this file is a regular file. */
+ public final boolean isRegularFile() {
+ return this instanceof RegularFile;
+ }
+
+ /** Returns whether or not this file is a symbolic link. */
+ public final boolean isSymbolicLink() {
+ return this instanceof SymbolicLink;
+ }
+
+ /**
+ * Creates a new file of the same type as this file with the given ID. Does not copy the content
+ * of this file unless the cost of copying the content is minimal. This is because this method is
+ * called with a hold on the file system's lock.
+ */
+ abstract File copyWithoutContent(int id);
+
+ /**
+ * Copies the content of this file to the given file. The given file must be the same type of file
+ * as this file and should have no content.
+ *
+ * <p>This method is used for copying the content of a file after copying the file itself. Does
+ * nothing by default.
+ */
+ void copyContentTo(File file) throws IOException {}
+
+ /**
+ * Returns the read-write lock for this file's content, or {@code null} if there is no content
+ * lock.
+ */
+ @NullableDecl
+ ReadWriteLock contentLock() {
+ return null;
+ }
+
+ /** Called when a stream or channel to this file is opened. */
+ void opened() {}
+
+ /**
+ * Called when a stream or channel to this file is closed. If there are no more streams or
+ * channels open to the file and it has been deleted, its contents may be deleted.
+ */
+ void closed() {}
+
+ /**
+ * Called when (a single link to) this file is deleted. There may be links remaining. Does nothing
+ * by default.
+ */
+ void deleted() {}
+
+ /** Returns whether or not this file is a root directory of the file system. */
+ final boolean isRootDirectory() {
+ // only root directories have their parent link pointing to themselves
+ return isDirectory() && equals(((Directory) this).parent());
+ }
+
+ /** Returns the current count of links to this file. */
+ public final synchronized int links() {
+ return links;
+ }
+
+ /**
+ * Called when this file has been linked in a directory. The given entry is the new directory
+ * entry that links to this file.
+ */
+ void linked(DirectoryEntry entry) {
+ checkNotNull(entry);
+ }
+
+ /** Called when this file has been unlinked from a directory, either for a move or delete. */
+ void unlinked() {}
+
+ /** Increments the link count for this file. */
+ final synchronized void incrementLinkCount() {
+ links++;
+ }
+
+ /** Decrements the link count for this file. */
+ final synchronized void decrementLinkCount() {
+ links--;
+ }
+
+ /** Gets the creation time of the file. */
+ @SuppressWarnings("GoodTime") // should return a java.time.Instant
+ public final synchronized long getCreationTime() {
+ return creationTime;
+ }
+
+ /** Gets the last access time of the file. */
+ @SuppressWarnings("GoodTime") // should return a java.time.Instant
+ public final synchronized long getLastAccessTime() {
+ return lastAccessTime;
+ }
+
+ /** Gets the last modified time of the file. */
+ @SuppressWarnings("GoodTime") // should return a java.time.Instant
+ public final synchronized long getLastModifiedTime() {
+ return lastModifiedTime;
+ }
+
+ /** Sets the creation time of the file. */
+ final synchronized void setCreationTime(long creationTime) {
+ this.creationTime = creationTime;
+ }
+
+ /** Sets the last access time of the file. */
+ final synchronized void setLastAccessTime(long lastAccessTime) {
+ this.lastAccessTime = lastAccessTime;
+ }
+
+ /** Sets the last modified time of the file. */
+ final synchronized void setLastModifiedTime(long lastModifiedTime) {
+ this.lastModifiedTime = lastModifiedTime;
+ }
+
+ /** Sets the last access time of the file to the current time. */
+ final void updateAccessTime() {
+ setLastAccessTime(System.currentTimeMillis());
+ }
+
+ /** Sets the last modified time of the file to the current time. */
+ final void updateModifiedTime() {
+ setLastModifiedTime(System.currentTimeMillis());
+ }
+
+ /**
+ * Returns the names of the attributes contained in the given attribute view in the file's
+ * attributes table.
+ */
+ public final synchronized ImmutableSet<String> getAttributeNames(String view) {
+ if (attributes == null) {
+ return ImmutableSet.of();
+ }
+ return ImmutableSet.copyOf(attributes.row(view).keySet());
+ }
+
+ /** Returns the attribute keys contained in the attributes map for the file. */
+ @VisibleForTesting
+ final synchronized ImmutableSet<String> getAttributeKeys() {
+ if (attributes == null) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ for (Table.Cell<String, String, Object> cell : attributes.cellSet()) {
+ builder.add(cell.getRowKey() + ':' + cell.getColumnKey());
+ }
+ return builder.build();
+ }
+
+ /** Gets the value of the given attribute in the given view. */
+ @NullableDecl
+ public final synchronized Object getAttribute(String view, String attribute) {
+ if (attributes == null) {
+ return null;
+ }
+ return attributes.get(view, attribute);
+ }
+
+ /** Sets the given attribute in the given view to the given value. */
+ public final synchronized void setAttribute(String view, String attribute, Object value) {
+ if (attributes == null) {
+ attributes = HashBasedTable.create();
+ }
+ attributes.put(view, attribute, value);
+ }
+
+ /** Deletes the given attribute from the given view. */
+ public final synchronized void deleteAttribute(String view, String attribute) {
+ if (attributes != null) {
+ attributes.remove(view, attribute);
+ }
+ }
+
+ /** Copies basic attributes (file times) from this file to the given file. */
+ final synchronized void copyBasicAttributes(File target) {
+ target.setFileTimes(creationTime, lastModifiedTime, lastAccessTime);
+ }
+
+ private synchronized void setFileTimes(
+ long creationTime, long lastModifiedTime, long lastAccessTime) {
+ this.creationTime = creationTime;
+ this.lastModifiedTime = lastModifiedTime;
+ this.lastAccessTime = lastAccessTime;
+ }
+
+ /** Copies the attributes from this file to the given file. */
+ final synchronized void copyAttributes(File target) {
+ copyBasicAttributes(target);
+ target.putAll(attributes);
+ }
+
+ private synchronized void putAll(@NullableDecl Table<String, String, Object> attributes) {
+ if (attributes != null && this.attributes != attributes) {
+ if (this.attributes == null) {
+ this.attributes = HashBasedTable.create();
+ }
+ this.attributes.putAll(attributes);
+ }
+ }
+
+ @Override
+ public final String toString() {
+ return MoreObjects.toStringHelper(this).add("id", id()).toString();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileFactory.java b/jimfs/src/main/java/com/google/common/jimfs/FileFactory.java
new file mode 100644
index 0000000..e26d41d
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/FileFactory.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Supplier;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Factory for creating new files and copying files. One piece of the file store implementation.
+ *
+ * @author Colin Decker
+ */
+final class FileFactory {
+
+ private final AtomicInteger idGenerator = new AtomicInteger();
+
+ private final HeapDisk disk;
+
+ /** Creates a new file factory using the given disk for regular files. */
+ public FileFactory(HeapDisk disk) {
+ this.disk = checkNotNull(disk);
+ }
+
+ private int nextFileId() {
+ return idGenerator.getAndIncrement();
+ }
+
+ /** Creates a new directory. */
+ public Directory createDirectory() {
+ return Directory.create(nextFileId());
+ }
+
+ /** Creates a new root directory with the given name. */
+ public Directory createRootDirectory(Name name) {
+ return Directory.createRoot(nextFileId(), name);
+ }
+
+ /** Creates a new regular file. */
+ @VisibleForTesting
+ RegularFile createRegularFile() {
+ return RegularFile.create(nextFileId(), disk);
+ }
+
+ /** Creates a new symbolic link referencing the given target path. */
+ @VisibleForTesting
+ SymbolicLink createSymbolicLink(JimfsPath target) {
+ return SymbolicLink.create(nextFileId(), target);
+ }
+
+ /** Creates and returns a copy of the given file. */
+ public File copyWithoutContent(File file) throws IOException {
+ return file.copyWithoutContent(nextFileId());
+ }
+
+ // suppliers to act as file creation callbacks
+
+ private final Supplier<Directory> directorySupplier = new DirectorySupplier();
+
+ private final Supplier<RegularFile> regularFileSupplier = new RegularFileSupplier();
+
+ /** Returns a supplier that creates directories. */
+ public Supplier<Directory> directoryCreator() {
+ return directorySupplier;
+ }
+
+ /** Returns a supplier that creates regular files. */
+ public Supplier<RegularFile> regularFileCreator() {
+ return regularFileSupplier;
+ }
+
+ /** Returns a supplier that creates a symbolic links to the given path. */
+ public Supplier<SymbolicLink> symbolicLinkCreator(JimfsPath target) {
+ return new SymbolicLinkSupplier(target);
+ }
+
+ private final class DirectorySupplier implements Supplier<Directory> {
+ @Override
+ public Directory get() {
+ return createDirectory();
+ }
+ }
+
+ private final class RegularFileSupplier implements Supplier<RegularFile> {
+ @Override
+ public RegularFile get() {
+ return createRegularFile();
+ }
+ }
+
+ private final class SymbolicLinkSupplier implements Supplier<SymbolicLink> {
+
+ private final JimfsPath target;
+
+ protected SymbolicLinkSupplier(JimfsPath target) {
+ this.target = checkNotNull(target);
+ }
+
+ @Override
+ public SymbolicLink get() {
+ return createSymbolicLink(target);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileLookup.java b/jimfs/src/main/java/com/google/common/jimfs/FileLookup.java
new file mode 100644
index 0000000..b427b0a
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/FileLookup.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import java.io.IOException;
+
+/**
+ * Callback for looking up a file.
+ *
+ * @author Colin Decker
+ */
+public interface FileLookup {
+
+ /**
+ * Looks up the file.
+ *
+ * @throws IOException if the lookup fails for any reason, such as the file not existing
+ */
+ File lookup() throws IOException;
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileSystemState.java b/jimfs/src/main/java/com/google/common/jimfs/FileSystemState.java
new file mode 100644
index 0000000..f15a5ff
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/FileSystemState.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Sets;
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.ClosedFileSystemException;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Object that manages the open/closed state of a file system, ensuring that all open resources are
+ * closed when the file system is closed and that file system methods throw an exception when the
+ * file system has been closed.
+ *
+ * @author Colin Decker
+ */
+final class FileSystemState implements Closeable {
+
+ private final Set<Closeable> resources = Sets.newConcurrentHashSet();
+ private final Runnable onClose;
+
+ private final AtomicBoolean open = new AtomicBoolean(true);
+
+ /** Count of resources currently in the process of being registered. */
+ private final AtomicInteger registering = new AtomicInteger();
+
+ FileSystemState(Runnable onClose) {
+ this.onClose = checkNotNull(onClose);
+ }
+
+ /** Returns whether or not the file system is open. */
+ public boolean isOpen() {
+ return open.get();
+ }
+
+ /**
+ * Checks that the file system is open, throwing {@link ClosedFileSystemException} if it is not.
+ */
+ public void checkOpen() {
+ if (!open.get()) {
+ throw new ClosedFileSystemException();
+ }
+ }
+
+ /**
+ * Registers the given resource to be closed when the file system is closed. Should be called when
+ * the resource is opened.
+ */
+ public <C extends Closeable> C register(C resource) {
+ // Initial open check to avoid incrementing registering if we already know it's closed.
+ // This is to prevent any possibility of a weird pathalogical situation where the do/while
+ // loop in close() keeps looping as register() is called repeatedly from multiple threads.
+ checkOpen();
+
+ registering.incrementAndGet();
+ try {
+ // Need to check again after marking registration in progress to avoid a potential race.
+ // (close() could have run completely between the first checkOpen() and
+ // registering.incrementAndGet().)
+ checkOpen();
+ resources.add(resource);
+ return resource;
+ } finally {
+ registering.decrementAndGet();
+ }
+ }
+
+ /** Unregisters the given resource. Should be called when the resource is closed. */
+ public void unregister(Closeable resource) {
+ resources.remove(resource);
+ }
+
+ /**
+ * Closes the file system, runs the {@code onClose} callback and closes all registered resources.
+ */
+ @Override
+ public void close() throws IOException {
+ if (open.compareAndSet(true, false)) {
+ onClose.run();
+
+ Throwable thrown = null;
+ do {
+ for (Closeable resource : resources) {
+ try {
+ resource.close();
+ } catch (Throwable e) {
+ if (thrown == null) {
+ thrown = e;
+ } else {
+ thrown.addSuppressed(e);
+ }
+ } finally {
+ // ensure the resource is removed even if it doesn't remove itself when closed
+ resources.remove(resource);
+ }
+ }
+
+ // It's possible for a thread registering a resource to register that resource after open
+ // has been set to false and even after we've looped through and closed all the resources.
+ // Since registering must be incremented *before* checking the state of open, however,
+ // when we reach this point in that situation either the register call is still in progress
+ // (registering > 0) or the new resource has been successfully added (resources not empty).
+ // In either case, we just need to repeat the loop until there are no more register calls
+ // in progress (no new calls can start and no resources left to close.
+ } while (registering.get() > 0 || !resources.isEmpty());
+ Throwables.propagateIfPossible(thrown, IOException.class);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java b/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java
new file mode 100644
index 0000000..62e8739
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java
@@ -0,0 +1,737 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.CREATE_NEW;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import java.io.IOException;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileSystemException;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * View of a file system with a specific working directory. As all file system operations need to
+ * work when given either relative or absolute paths, this class contains the implementation of most
+ * file system operations, with relative path operations resolving against the working directory.
+ *
+ * <p>A file system has one default view using the file system's working directory. Additional views
+ * may be created for use in {@link SecureDirectoryStream} instances, which each have a different
+ * working directory they use.
+ *
+ * @author Colin Decker
+ */
+final class FileSystemView {
+
+ private final JimfsFileStore store;
+
+ private final Directory workingDirectory;
+ private final JimfsPath workingDirectoryPath;
+
+ /** Creates a new file system view. */
+ public FileSystemView(
+ JimfsFileStore store, Directory workingDirectory, JimfsPath workingDirectoryPath) {
+ this.store = checkNotNull(store);
+ this.workingDirectory = checkNotNull(workingDirectory);
+ this.workingDirectoryPath = checkNotNull(workingDirectoryPath);
+ }
+
+ /** Returns whether or not this view and the given view belong to the same file system. */
+ private boolean isSameFileSystem(FileSystemView other) {
+ return store == other.store;
+ }
+
+ /** Returns the file system state. */
+ public FileSystemState state() {
+ return store.state();
+ }
+
+ /**
+ * Returns the path of the working directory at the time this view was created. Does not reflect
+ * changes to the path caused by the directory being moved.
+ */
+ public JimfsPath getWorkingDirectoryPath() {
+ return workingDirectoryPath;
+ }
+
+ /** Attempt to look up the file at the given path. */
+ DirectoryEntry lookUpWithLock(JimfsPath path, Set<? super LinkOption> options)
+ throws IOException {
+ store.readLock().lock();
+ try {
+ return lookUp(path, options);
+ } finally {
+ store.readLock().unlock();
+ }
+ }
+
+ /** Looks up the file at the given path without locking. */
+ private DirectoryEntry lookUp(JimfsPath path, Set<? super LinkOption> options)
+ throws IOException {
+ return store.lookUp(workingDirectory, path, options);
+ }
+
+ /**
+ * Creates a new directory stream for the directory located by the given path. The given {@code
+ * basePathForStream} is that base path that the returned stream will use. This will be the same
+ * as {@code dir} except for streams created relative to another secure stream.
+ */
+ public DirectoryStream<Path> newDirectoryStream(
+ JimfsPath dir,
+ DirectoryStream.Filter<? super Path> filter,
+ Set<? super LinkOption> options,
+ JimfsPath basePathForStream)
+ throws IOException {
+ Directory file = (Directory) lookUpWithLock(dir, options).requireDirectory(dir).file();
+ FileSystemView view = new FileSystemView(store, file, basePathForStream);
+ JimfsSecureDirectoryStream stream = new JimfsSecureDirectoryStream(view, filter, state());
+ return store.supportsFeature(Feature.SECURE_DIRECTORY_STREAM)
+ ? stream
+ : new DowngradedDirectoryStream(stream);
+ }
+
+ /** Snapshots the entries of the working directory of this view. */
+ public ImmutableSortedSet<Name> snapshotWorkingDirectoryEntries() {
+ store.readLock().lock();
+ try {
+ ImmutableSortedSet<Name> names = workingDirectory.snapshot();
+ workingDirectory.updateAccessTime();
+ return names;
+ } finally {
+ store.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns a snapshot mapping the names of each file in the directory at the given path to the
+ * last modified time of that file.
+ */
+ public ImmutableMap<Name, Long> snapshotModifiedTimes(JimfsPath path) throws IOException {
+ ImmutableMap.Builder<Name, Long> modifiedTimes = ImmutableMap.builder();
+
+ store.readLock().lock();
+ try {
+ Directory dir = (Directory) lookUp(path, Options.FOLLOW_LINKS).requireDirectory(path).file();
+ // TODO(cgdecker): Investigate whether WatchServices should keep a reference to the actual
+ // directory when SecureDirectoryStream is supported rather than looking up the directory
+ // each time the WatchService polls
+
+ for (DirectoryEntry entry : dir) {
+ if (!entry.name().equals(Name.SELF) && !entry.name().equals(Name.PARENT)) {
+ modifiedTimes.put(entry.name(), entry.file().getLastModifiedTime());
+ }
+ }
+
+ return modifiedTimes.build();
+ } finally {
+ store.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns whether or not the two given paths locate the same file. The second path is located
+ * using the given view rather than this file view.
+ */
+ public boolean isSameFile(JimfsPath path, FileSystemView view2, JimfsPath path2)
+ throws IOException {
+ if (!isSameFileSystem(view2)) {
+ return false;
+ }
+
+ store.readLock().lock();
+ try {
+ File file = lookUp(path, Options.FOLLOW_LINKS).fileOrNull();
+ File file2 = view2.lookUp(path2, Options.FOLLOW_LINKS).fileOrNull();
+ return file != null && Objects.equals(file, file2);
+ } finally {
+ store.readLock().unlock();
+ }
+ }
+
+ /**
+ * Gets the {@linkplain Path#toRealPath(LinkOption...) real path} to the file located by the given
+ * path.
+ */
+ public JimfsPath toRealPath(
+ JimfsPath path, PathService pathService, Set<? super LinkOption> options) throws IOException {
+ checkNotNull(path);
+ checkNotNull(options);
+
+ store.readLock().lock();
+ try {
+ DirectoryEntry entry = lookUp(path, options).requireExists(path);
+
+ List<Name> names = new ArrayList<>();
+ names.add(entry.name());
+ while (!entry.file().isRootDirectory()) {
+ entry = entry.directory().entryInParent();
+ names.add(entry.name());
+ }
+
+ // names are ordered last to first in the list, so get the reverse view
+ List<Name> reversed = Lists.reverse(names);
+ Name root = reversed.remove(0);
+ return pathService.createPath(root, reversed);
+ } finally {
+ store.readLock().unlock();
+ }
+ }
+
+ /**
+ * Creates a new directory at the given path. The given attributes will be set on the new file if
+ * possible.
+ */
+ public Directory createDirectory(JimfsPath path, FileAttribute<?>... attrs) throws IOException {
+ return (Directory) createFile(path, store.directoryCreator(), true, attrs);
+ }
+
+ /**
+ * Creates a new symbolic link at the given path with the given target. The given attributes will
+ * be set on the new file if possible.
+ */
+ public SymbolicLink createSymbolicLink(
+ JimfsPath path, JimfsPath target, FileAttribute<?>... attrs) throws IOException {
+ if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) {
+ throw new UnsupportedOperationException();
+ }
+ return (SymbolicLink) createFile(path, store.symbolicLinkCreator(target), true, attrs);
+ }
+
+ /**
+ * Creates a new file at the given path if possible, using the given supplier to create the file.
+ * Returns the new file. If {@code allowExisting} is {@code true} and a file already exists at the
+ * given path, returns that file. Otherwise, throws {@link FileAlreadyExistsException}.
+ */
+ private File createFile(
+ JimfsPath path,
+ Supplier<? extends File> fileCreator,
+ boolean failIfExists,
+ FileAttribute<?>... attrs)
+ throws IOException {
+ checkNotNull(path);
+ checkNotNull(fileCreator);
+
+ store.writeLock().lock();
+ try {
+ DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS);
+
+ if (entry.exists()) {
+ if (failIfExists) {
+ throw new FileAlreadyExistsException(path.toString());
+ }
+
+ // currently can only happen if getOrCreateFile doesn't find the file with the read lock
+ // and then the file is created between when it releases the read lock and when it
+ // acquires the write lock; so, very unlikely
+ return entry.file();
+ }
+
+ Directory parent = entry.directory();
+
+ File newFile = fileCreator.get();
+ store.setInitialAttributes(newFile, attrs);
+ parent.link(path.name(), newFile);
+ parent.updateModifiedTime();
+ return newFile;
+ } finally {
+ store.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Gets the regular file at the given path, creating it if it doesn't exist and the given options
+ * specify that it should be created.
+ */
+ public RegularFile getOrCreateRegularFile(
+ JimfsPath path, Set<OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ checkNotNull(path);
+
+ if (!options.contains(CREATE_NEW)) {
+ // assume file exists unless we're explicitly trying to create a new file
+ RegularFile file = lookUpRegularFile(path, options);
+ if (file != null) {
+ return file;
+ }
+ }
+
+ if (options.contains(CREATE) || options.contains(CREATE_NEW)) {
+ return getOrCreateRegularFileWithWriteLock(path, options, attrs);
+ } else {
+ throw new NoSuchFileException(path.toString());
+ }
+ }
+
+ /**
+ * Looks up the regular file at the given path, throwing an exception if the file isn't a regular
+ * file. Returns null if the file did not exist.
+ */
+ @NullableDecl
+ private RegularFile lookUpRegularFile(JimfsPath path, Set<OpenOption> options)
+ throws IOException {
+ store.readLock().lock();
+ try {
+ DirectoryEntry entry = lookUp(path, options);
+ if (entry.exists()) {
+ File file = entry.file();
+ if (!file.isRegularFile()) {
+ throw new FileSystemException(path.toString(), null, "not a regular file");
+ }
+ return open((RegularFile) file, options);
+ } else {
+ return null;
+ }
+ } finally {
+ store.readLock().unlock();
+ }
+ }
+
+ /** Gets or creates a new regular file with a write lock (assuming the file does not exist). */
+ private RegularFile getOrCreateRegularFileWithWriteLock(
+ JimfsPath path, Set<OpenOption> options, FileAttribute<?>[] attrs) throws IOException {
+ store.writeLock().lock();
+ try {
+ File file = createFile(path, store.regularFileCreator(), options.contains(CREATE_NEW), attrs);
+ // the file already existed but was not a regular file
+ if (!file.isRegularFile()) {
+ throw new FileSystemException(path.toString(), null, "not a regular file");
+ }
+ return open((RegularFile) file, options);
+ } finally {
+ store.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Opens the given regular file with the given options, truncating it if necessary and
+ * incrementing its open count. Returns the given file.
+ */
+ private static RegularFile open(RegularFile file, Set<OpenOption> options) {
+ if (options.contains(TRUNCATE_EXISTING) && options.contains(WRITE)) {
+ file.writeLock().lock();
+ try {
+ file.truncate(0);
+ } finally {
+ file.writeLock().unlock();
+ }
+ }
+
+ // must be opened while holding a file store lock to ensure no race between opening and
+ // deleting the file
+ file.opened();
+
+ return file;
+ }
+
+ /** Returns the target of the symbolic link at the given path. */
+ public JimfsPath readSymbolicLink(JimfsPath path) throws IOException {
+ if (!store.supportsFeature(Feature.SYMBOLIC_LINKS)) {
+ throw new UnsupportedOperationException();
+ }
+
+ SymbolicLink symbolicLink =
+ (SymbolicLink)
+ lookUpWithLock(path, Options.NOFOLLOW_LINKS).requireSymbolicLink(path).file();
+
+ return symbolicLink.target();
+ }
+
+ /**
+ * Checks access to the file at the given path for the given modes. Since access controls are not
+ * implemented for this file system, this just checks that the file exists.
+ */
+ public void checkAccess(JimfsPath path) throws IOException {
+ // just check that the file exists
+ lookUpWithLock(path, Options.FOLLOW_LINKS).requireExists(path);
+ }
+
+ /**
+ * Creates a hard link at the given link path to the regular file at the given path. The existing
+ * file must exist and must be a regular file. The given file system view must belong to the same
+ * file system as this view.
+ */
+ public void link(JimfsPath link, FileSystemView existingView, JimfsPath existing)
+ throws IOException {
+ checkNotNull(link);
+ checkNotNull(existingView);
+ checkNotNull(existing);
+
+ if (!store.supportsFeature(Feature.LINKS)) {
+ throw new UnsupportedOperationException();
+ }
+
+ if (!isSameFileSystem(existingView)) {
+ throw new FileSystemException(
+ link.toString(),
+ existing.toString(),
+ "can't link: source and target are in different file system instances");
+ }
+
+ Name linkName = link.name();
+
+ // existingView is in the same file system, so just one lock is needed
+ store.writeLock().lock();
+ try {
+ // we do want to follow links when finding the existing file
+ File existingFile =
+ existingView.lookUp(existing, Options.FOLLOW_LINKS).requireExists(existing).file();
+ if (!existingFile.isRegularFile()) {
+ throw new FileSystemException(
+ link.toString(), existing.toString(), "can't link: not a regular file");
+ }
+
+ Directory linkParent =
+ lookUp(link, Options.NOFOLLOW_LINKS).requireDoesNotExist(link).directory();
+
+ linkParent.link(linkName, existingFile);
+ linkParent.updateModifiedTime();
+ } finally {
+ store.writeLock().unlock();
+ }
+ }
+
+ /** Deletes the file at the given absolute path. */
+ public void deleteFile(JimfsPath path, DeleteMode deleteMode) throws IOException {
+ store.writeLock().lock();
+ try {
+ DirectoryEntry entry = lookUp(path, Options.NOFOLLOW_LINKS).requireExists(path);
+ delete(entry, deleteMode, path);
+ } finally {
+ store.writeLock().unlock();
+ }
+ }
+
+ /** Deletes the given directory entry from its parent directory. */
+ private void delete(DirectoryEntry entry, DeleteMode deleteMode, JimfsPath pathForException)
+ throws IOException {
+ Directory parent = entry.directory();
+ File file = entry.file();
+
+ checkDeletable(file, deleteMode, pathForException);
+ parent.unlink(entry.name());
+ parent.updateModifiedTime();
+
+ file.deleted();
+ }
+
+ /** Mode for deleting. Determines what types of files can be deleted. */
+ public enum DeleteMode {
+ /** Delete any file. */
+ ANY,
+ /** Only delete non-directory files. */
+ NON_DIRECTORY_ONLY,
+ /** Only delete directory files. */
+ DIRECTORY_ONLY
+ }
+
+ /** Checks that the given file can be deleted, throwing an exception if it can't. */
+ private void checkDeletable(File file, DeleteMode mode, Path path) throws IOException {
+ if (file.isRootDirectory()) {
+ throw new FileSystemException(path.toString(), null, "can't delete root directory");
+ }
+
+ if (file.isDirectory()) {
+ if (mode == DeleteMode.NON_DIRECTORY_ONLY) {
+ throw new FileSystemException(path.toString(), null, "can't delete: is a directory");
+ }
+
+ checkEmpty(((Directory) file), path);
+ } else if (mode == DeleteMode.DIRECTORY_ONLY) {
+ throw new FileSystemException(path.toString(), null, "can't delete: is not a directory");
+ }
+
+ if (file == workingDirectory && !path.isAbsolute()) {
+ // this is weird, but on Unix at least, the file system seems to be happy to delete the
+ // working directory if you give the absolute path to it but fail if you use a relative path
+ // that resolves to the working directory (e.g. "" or ".")
+ throw new FileSystemException(path.toString(), null, "invalid argument");
+ }
+ }
+
+ /** Checks that given directory is empty, throwing {@link DirectoryNotEmptyException} if not. */
+ private void checkEmpty(Directory dir, Path pathForException) throws FileSystemException {
+ if (!dir.isEmpty()) {
+ throw new DirectoryNotEmptyException(pathForException.toString());
+ }
+ }
+
+ /** Copies or moves the file at the given source path to the given dest path. */
+ public void copy(
+ JimfsPath source,
+ FileSystemView destView,
+ JimfsPath dest,
+ Set<CopyOption> options,
+ boolean move)
+ throws IOException {
+ checkNotNull(source);
+ checkNotNull(destView);
+ checkNotNull(dest);
+ checkNotNull(options);
+
+ boolean sameFileSystem = isSameFileSystem(destView);
+
+ File sourceFile;
+ File copyFile = null; // non-null after block completes iff source file was copied
+ lockBoth(store.writeLock(), destView.store.writeLock());
+ try {
+ DirectoryEntry sourceEntry = lookUp(source, options).requireExists(source);
+ DirectoryEntry destEntry = destView.lookUp(dest, Options.NOFOLLOW_LINKS);
+
+ Directory sourceParent = sourceEntry.directory();
+ sourceFile = sourceEntry.file();
+
+ Directory destParent = destEntry.directory();
+
+ if (move && sourceFile.isDirectory()) {
+ if (sameFileSystem) {
+ checkMovable(sourceFile, source);
+ checkNotAncestor(sourceFile, destParent, destView);
+ } else {
+ // move to another file system is accomplished by copy-then-delete, so the source file
+ // must be deletable to be moved
+ checkDeletable(sourceFile, DeleteMode.ANY, source);
+ }
+ }
+
+ if (destEntry.exists()) {
+ if (destEntry.file().equals(sourceFile)) {
+ return;
+ } else if (options.contains(REPLACE_EXISTING)) {
+ destView.delete(destEntry, DeleteMode.ANY, dest);
+ } else {
+ throw new FileAlreadyExistsException(dest.toString());
+ }
+ }
+
+ if (move && sameFileSystem) {
+ // Real move on the same file system.
+ sourceParent.unlink(source.name());
+ sourceParent.updateModifiedTime();
+
+ destParent.link(dest.name(), sourceFile);
+ destParent.updateModifiedTime();
+ } else {
+ // Doing a copy OR a move to a different file system, which must be implemented by copy and
+ // delete.
+
+ // By default, don't copy attributes.
+ AttributeCopyOption attributeCopyOption = AttributeCopyOption.NONE;
+ if (move) {
+ // Copy only the basic attributes of the file to the other file system, as it may not
+ // support all the attribute views that this file system does. This also matches the
+ // behavior of moving a file to a foreign file system with a different
+ // FileSystemProvider.
+ attributeCopyOption = AttributeCopyOption.BASIC;
+ } else if (options.contains(COPY_ATTRIBUTES)) {
+ // As with move, if we're copying the file to a different file system, only copy its
+ // basic attributes.
+ attributeCopyOption =
+ sameFileSystem ? AttributeCopyOption.ALL : AttributeCopyOption.BASIC;
+ }
+
+ // Copy the file, but don't copy its content while we're holding the file store locks.
+ copyFile = destView.store.copyWithoutContent(sourceFile, attributeCopyOption);
+ destParent.link(dest.name(), copyFile);
+ destParent.updateModifiedTime();
+
+ // In order for the copy to be atomic (not strictly necessary, but seems preferable since
+ // we can) lock both source and copy files before leaving the file store locks. This
+ // ensures that users cannot observe the copy's content until the content has been copied.
+ // This also marks the source file as opened, preventing its content from being deleted
+ // until after it's copied if the source file itself is deleted in the next step.
+ lockSourceAndCopy(sourceFile, copyFile);
+
+ if (move) {
+ // It should not be possible for delete to throw an exception here, because we already
+ // checked that the file was deletable above.
+ delete(sourceEntry, DeleteMode.ANY, source);
+ }
+ }
+ } finally {
+ destView.store.writeLock().unlock();
+ store.writeLock().unlock();
+ }
+
+ if (copyFile != null) {
+ // Copy the content. This is done outside the above block to minimize the time spent holding
+ // file store locks, since copying the content of a regular file could take a (relatively)
+ // long time. If done inside the above block, copying using Files.copy can be slower than
+ // copying with an InputStream and an OutputStream if many files are being copied on
+ // different threads.
+ try {
+ sourceFile.copyContentTo(copyFile);
+ } finally {
+ // Unlock the files, allowing the content of the copy to be observed by the user. This also
+ // closes the source file, allowing its content to be deleted if it was deleted.
+ unlockSourceAndCopy(sourceFile, copyFile);
+ }
+ }
+ }
+
+ private void checkMovable(File file, JimfsPath path) throws FileSystemException {
+ if (file.isRootDirectory()) {
+ throw new FileSystemException(path.toString(), null, "can't move root directory");
+ }
+ }
+
+ /**
+ * Acquires both write locks in a way that attempts to avoid the possibility of deadlock. Note
+ * that typically (when only one file system instance is involved), both locks will be the same
+ * lock and there will be no issue at all.
+ */
+ private static void lockBoth(Lock sourceWriteLock, Lock destWriteLock) {
+ while (true) {
+ sourceWriteLock.lock();
+ if (destWriteLock.tryLock()) {
+ return;
+ } else {
+ sourceWriteLock.unlock();
+ }
+
+ destWriteLock.lock();
+ if (sourceWriteLock.tryLock()) {
+ return;
+ } else {
+ destWriteLock.unlock();
+ }
+ }
+ }
+
+ /** Checks that source is not an ancestor of dest, throwing an exception if it is. */
+ private void checkNotAncestor(File source, Directory destParent, FileSystemView destView)
+ throws IOException {
+ // if dest is not in the same file system, it couldn't be in source's subdirectories
+ if (!isSameFileSystem(destView)) {
+ return;
+ }
+
+ Directory current = destParent;
+ while (true) {
+ if (current.equals(source)) {
+ throw new IOException(
+ "invalid argument: can't move directory into a subdirectory of itself");
+ }
+
+ if (current.isRootDirectory()) {
+ return;
+ } else {
+ current = current.parent();
+ }
+ }
+ }
+
+ /**
+ * Locks source and copy files before copying content. Also marks the source file as opened so
+ * that its content won't be deleted until after the copy if it is deleted.
+ */
+ private void lockSourceAndCopy(File sourceFile, File copyFile) {
+ sourceFile.opened();
+ ReadWriteLock sourceLock = sourceFile.contentLock();
+ if (sourceLock != null) {
+ sourceLock.readLock().lock();
+ }
+ ReadWriteLock copyLock = copyFile.contentLock();
+ if (copyLock != null) {
+ copyLock.writeLock().lock();
+ }
+ }
+
+ /**
+ * Unlocks source and copy files after copying content. Also closes the source file so its content
+ * can be deleted if it was deleted.
+ */
+ private void unlockSourceAndCopy(File sourceFile, File copyFile) {
+ ReadWriteLock sourceLock = sourceFile.contentLock();
+ if (sourceLock != null) {
+ sourceLock.readLock().unlock();
+ }
+ ReadWriteLock copyLock = copyFile.contentLock();
+ if (copyLock != null) {
+ copyLock.writeLock().unlock();
+ }
+ sourceFile.closed();
+ }
+
+ /** Returns a file attribute view using the given lookup callback. */
+ @NullableDecl
+ public <V extends FileAttributeView> V getFileAttributeView(FileLookup lookup, Class<V> type) {
+ return store.getFileAttributeView(lookup, type);
+ }
+
+ /** Returns a file attribute view for the given path in this view. */
+ @NullableDecl
+ public <V extends FileAttributeView> V getFileAttributeView(
+ final JimfsPath path, Class<V> type, final Set<? super LinkOption> options) {
+ return store.getFileAttributeView(
+ new FileLookup() {
+ @Override
+ public File lookup() throws IOException {
+ return lookUpWithLock(path, options).requireExists(path).file();
+ }
+ },
+ type);
+ }
+
+ /** Reads attributes of the file located by the given path in this view as an object. */
+ public <A extends BasicFileAttributes> A readAttributes(
+ JimfsPath path, Class<A> type, Set<? super LinkOption> options) throws IOException {
+ File file = lookUpWithLock(path, options).requireExists(path).file();
+ return store.readAttributes(file, type);
+ }
+
+ /** Reads attributes of the file located by the given path in this view as a map. */
+ public ImmutableMap<String, Object> readAttributes(
+ JimfsPath path, String attributes, Set<? super LinkOption> options) throws IOException {
+ File file = lookUpWithLock(path, options).requireExists(path).file();
+ return store.readAttributes(file, attributes);
+ }
+
+ /**
+ * Sets the given attribute to the given value on the file located by the given path in this view.
+ */
+ public void setAttribute(
+ JimfsPath path, String attribute, Object value, Set<? super LinkOption> options)
+ throws IOException {
+ File file = lookUpWithLock(path, options).requireExists(path).file();
+ store.setAttribute(file, attribute, value);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/FileTree.java b/jimfs/src/main/java/com/google/common/jimfs/FileTree.java
new file mode 100644
index 0000000..c480942
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/FileTree.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.ImmutableSortedSet;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * The tree of directories and files for the file system. Contains the file system root directories
+ * and provides the ability to look up files by path. One piece of the file store implementation.
+ *
+ * @author Colin Decker
+ */
+final class FileTree {
+
+ /**
+ * Doesn't much matter, but this number comes from MIN_ELOOP_THRESHOLD <a
+ * href="https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=blob_plain;f=sysdeps/generic/eloop-threshold.h;hb=HEAD">
+ * here</a>
+ */
+ private static final int MAX_SYMBOLIC_LINK_DEPTH = 40;
+
+ private static final ImmutableList<Name> EMPTY_PATH_NAMES = ImmutableList.of(Name.SELF);
+
+ /** Map of root names to root directories. */
+ private final ImmutableSortedMap<Name, Directory> roots;
+
+ /** Creates a new file tree with the given root directories. */
+ FileTree(Map<Name, Directory> roots) {
+ this.roots = ImmutableSortedMap.copyOf(roots, Name.canonicalOrdering());
+ }
+
+ /** Returns the names of the root directories in this tree. */
+ public ImmutableSortedSet<Name> getRootDirectoryNames() {
+ return roots.keySet();
+ }
+
+ /**
+ * Gets the directory entry for the root with the given name or {@code null} if no such root
+ * exists.
+ */
+ @NullableDecl
+ public DirectoryEntry getRoot(Name name) {
+ Directory dir = roots.get(name);
+ return dir == null ? null : dir.entryInParent();
+ }
+
+ /** Returns the result of the file lookup for the given path. */
+ public DirectoryEntry lookUp(
+ File workingDirectory, JimfsPath path, Set<? super LinkOption> options) throws IOException {
+ checkNotNull(path);
+ checkNotNull(options);
+
+ DirectoryEntry result = lookUp(workingDirectory, path, options, 0);
+ if (result == null) {
+ // an intermediate file in the path did not exist or was not a directory
+ throw new NoSuchFileException(path.toString());
+ }
+ return result;
+ }
+
+ @NullableDecl
+ private DirectoryEntry lookUp(
+ File dir, JimfsPath path, Set<? super LinkOption> options, int linkDepth) throws IOException {
+ ImmutableList<Name> names = path.names();
+
+ if (path.isAbsolute()) {
+ // look up the root directory
+ DirectoryEntry entry = getRoot(path.root());
+ if (entry == null) {
+ // root not found; always return null as no real parent directory exists
+ // this prevents new roots from being created in file systems supporting multiple roots
+ return null;
+ } else if (names.isEmpty()) {
+ // root found, no more names to look up
+ return entry;
+ } else {
+ // root found, more names to look up; set dir to the root directory for the path
+ dir = entry.file();
+ }
+ } else if (isEmpty(names)) {
+ // set names to the canonical list of names for an empty path (singleton list of ".")
+ names = EMPTY_PATH_NAMES;
+ }
+
+ return lookUp(dir, names, options, linkDepth);
+ }
+
+ /**
+ * Looks up the given names against the given base file. If the file is not a directory, the
+ * lookup fails.
+ */
+ @NullableDecl
+ private DirectoryEntry lookUp(
+ File dir, Iterable<Name> names, Set<? super LinkOption> options, int linkDepth)
+ throws IOException {
+ Iterator<Name> nameIterator = names.iterator();
+ Name name = nameIterator.next();
+ while (nameIterator.hasNext()) {
+ Directory directory = toDirectory(dir);
+ if (directory == null) {
+ return null;
+ }
+
+ DirectoryEntry entry = directory.get(name);
+ if (entry == null) {
+ return null;
+ }
+
+ File file = entry.file();
+ if (file.isSymbolicLink()) {
+ DirectoryEntry linkResult = followSymbolicLink(dir, (SymbolicLink) file, linkDepth);
+
+ if (linkResult == null) {
+ return null;
+ }
+
+ dir = linkResult.fileOrNull();
+ } else {
+ dir = file;
+ }
+
+ name = nameIterator.next();
+ }
+
+ return lookUpLast(dir, name, options, linkDepth);
+ }
+
+ /** Looks up the last element of a path. */
+ @NullableDecl
+ private DirectoryEntry lookUpLast(
+ @NullableDecl File dir, Name name, Set<? super LinkOption> options, int linkDepth)
+ throws IOException {
+ Directory directory = toDirectory(dir);
+ if (directory == null) {
+ return null;
+ }
+
+ DirectoryEntry entry = directory.get(name);
+ if (entry == null) {
+ return new DirectoryEntry(directory, name, null);
+ }
+
+ File file = entry.file();
+ if (!options.contains(LinkOption.NOFOLLOW_LINKS) && file.isSymbolicLink()) {
+ return followSymbolicLink(dir, (SymbolicLink) file, linkDepth);
+ }
+
+ return getRealEntry(entry);
+ }
+
+ /**
+ * Returns the directory entry located by the target path of the given symbolic link, resolved
+ * relative to the given directory.
+ */
+ @NullableDecl
+ private DirectoryEntry followSymbolicLink(File dir, SymbolicLink link, int linkDepth)
+ throws IOException {
+ if (linkDepth >= MAX_SYMBOLIC_LINK_DEPTH) {
+ throw new IOException("too many levels of symbolic links");
+ }
+
+ return lookUp(dir, link.target(), Options.FOLLOW_LINKS, linkDepth + 1);
+ }
+
+ /**
+ * Returns the entry for the file in its parent directory. This will be the given entry unless the
+ * name for the entry is "." or "..", in which the directory linking to the file is not the file's
+ * parent directory. In that case, we know the file must be a directory ("." and ".." can only
+ * link to directories), so we can just get the entry in the directory's parent directory that
+ * links to it. So, for example, if we have a directory "foo" that contains a directory "bar" and
+ * we find an entry [bar -> "." -> bar], we instead return the entry for bar in its parent, [foo
+ * -> "bar" -> bar].
+ */
+ @NullableDecl
+ private DirectoryEntry getRealEntry(DirectoryEntry entry) {
+ Name name = entry.name();
+
+ if (name.equals(Name.SELF) || name.equals(Name.PARENT)) {
+ Directory dir = toDirectory(entry.file());
+ assert dir != null;
+ return dir.entryInParent();
+ } else {
+ return entry;
+ }
+ }
+
+ @NullableDecl
+ private Directory toDirectory(@NullableDecl File file) {
+ return file == null || !file.isDirectory() ? null : (Directory) file;
+ }
+
+ private static boolean isEmpty(ImmutableList<Name> names) {
+ // the empty path (created by FileSystem.getPath("")), has no root and a single name, ""
+ return names.isEmpty() || (names.size() == 1 && names.get(0).toString().isEmpty());
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/GlobToRegex.java b/jimfs/src/main/java/com/google/common/jimfs/GlobToRegex.java
new file mode 100644
index 0000000..c3e463b
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/GlobToRegex.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Translates globs to regex patterns.
+ *
+ * @author Colin Decker
+ */
+final class GlobToRegex {
+
+ /**
+ * Converts the given glob to a regular expression pattern. The given separators determine what
+ * characters the resulting expression breaks on for glob expressions such as * which should not
+ * cross directory boundaries.
+ *
+ * <p>Basic conversions (assuming / as only separator):
+ *
+ * <pre>{@code
+ * ? = [^/]
+ * * = [^/]*
+ * ** = .*
+ * [a-z] = [[^/]&&[a-z]]
+ * [!a-z] = [[^/]&&[^a-z]]
+ * {a,b,c} = (a|b|c)
+ * }</pre>
+ */
+ public static String toRegex(String glob, String separators) {
+ return new GlobToRegex(glob, separators).convert();
+ }
+
+ private static final InternalCharMatcher REGEX_RESERVED =
+ InternalCharMatcher.anyOf("^$.?+*\\[]{}()");
+
+ private final String glob;
+ private final String separators;
+ private final InternalCharMatcher separatorMatcher;
+
+ private final StringBuilder builder = new StringBuilder();
+ private final Deque<State> states = new ArrayDeque<>();
+ private int index;
+
+ private GlobToRegex(String glob, String separators) {
+ this.glob = checkNotNull(glob);
+ this.separators = separators;
+ this.separatorMatcher = InternalCharMatcher.anyOf(separators);
+ }
+
+ /**
+ * Converts the glob to a regex one character at a time. A state stack (states) is maintained,
+ * with the state at the top of the stack being the current state at any given time. The current
+ * state is always used to process the next character. When a state processes a character, it may
+ * pop the current state or push a new state as the current state. The resulting regex is written
+ * to {@code builder}.
+ */
+ private String convert() {
+ pushState(NORMAL);
+ for (index = 0; index < glob.length(); index++) {
+ currentState().process(this, glob.charAt(index));
+ }
+ currentState().finish(this);
+ return builder.toString();
+ }
+
+ /** Enters the given state. The current state becomes the previous state. */
+ private void pushState(State state) {
+ states.push(state);
+ }
+
+ /** Returns to the previous state. */
+ private void popState() {
+ states.pop();
+ }
+
+ /** Returns the current state. */
+ private State currentState() {
+ return states.peek();
+ }
+
+ /** Throws a {@link PatternSyntaxException}. */
+ private PatternSyntaxException syntaxError(String desc) {
+ throw new PatternSyntaxException(desc, glob, index);
+ }
+
+ /** Appends the given character as-is to the regex. */
+ private void appendExact(char c) {
+ builder.append(c);
+ }
+
+ /** Appends the regex form of the given normal character or separator from the glob. */
+ private void append(char c) {
+ if (separatorMatcher.matches(c)) {
+ appendSeparator();
+ } else {
+ appendNormal(c);
+ }
+ }
+
+ /** Appends the regex form of the given normal character from the glob. */
+ private void appendNormal(char c) {
+ if (REGEX_RESERVED.matches(c)) {
+ builder.append('\\');
+ }
+ builder.append(c);
+ }
+
+ /** Appends the regex form matching the separators for the path type. */
+ private void appendSeparator() {
+ if (separators.length() == 1) {
+ appendNormal(separators.charAt(0));
+ } else {
+ builder.append('[');
+ for (int i = 0; i < separators.length(); i++) {
+ appendInBracket(separators.charAt(i));
+ }
+ builder.append("]");
+ }
+ }
+
+ /** Appends the regex form that matches anything except the separators for the path type. */
+ private void appendNonSeparator() {
+ builder.append("[^");
+ for (int i = 0; i < separators.length(); i++) {
+ appendInBracket(separators.charAt(i));
+ }
+ builder.append(']');
+ }
+
+ /** Appends the regex form of the glob ? character. */
+ private void appendQuestionMark() {
+ appendNonSeparator();
+ }
+
+ /** Appends the regex form of the glob * character. */
+ private void appendStar() {
+ appendNonSeparator();
+ builder.append('*');
+ }
+
+ /** Appends the regex form of the glob ** pattern. */
+ private void appendStarStar() {
+ builder.append(".*");
+ }
+
+ /** Appends the regex form of the start of a glob [] section. */
+ private void appendBracketStart() {
+ builder.append('[');
+ appendNonSeparator();
+ builder.append("&&[");
+ }
+
+ /** Appends the regex form of the end of a glob [] section. */
+ private void appendBracketEnd() {
+ builder.append("]]");
+ }
+
+ /** Appends the regex form of the given character within a glob [] section. */
+ private void appendInBracket(char c) {
+ // escape \ in regex character class
+ if (c == '\\') {
+ builder.append('\\');
+ }
+
+ builder.append(c);
+ }
+
+ /** Appends the regex form of the start of a glob {} section. */
+ private void appendCurlyBraceStart() {
+ builder.append('(');
+ }
+
+ /** Appends the regex form of the separator (,) within a glob {} section. */
+ private void appendSubpatternSeparator() {
+ builder.append('|');
+ }
+
+ /** Appends the regex form of the end of a glob {} section. */
+ private void appendCurlyBraceEnd() {
+ builder.append(')');
+ }
+
+ /** Converter state. */
+ private abstract static class State {
+ /**
+ * Process the next character with the current state, transitioning the converter to a new state
+ * if necessary.
+ */
+ abstract void process(GlobToRegex converter, char c);
+
+ /** Called after all characters have been read. */
+ void finish(GlobToRegex converter) {}
+ }
+
+ /** Normal state. */
+ private static final State NORMAL =
+ new State() {
+ @Override
+ void process(GlobToRegex converter, char c) {
+ switch (c) {
+ case '?':
+ converter.appendQuestionMark();
+ return;
+ case '[':
+ converter.appendBracketStart();
+ converter.pushState(BRACKET_FIRST_CHAR);
+ return;
+ case '{':
+ converter.appendCurlyBraceStart();
+ converter.pushState(CURLY_BRACE);
+ return;
+ case '*':
+ converter.pushState(STAR);
+ return;
+ case '\\':
+ converter.pushState(ESCAPE);
+ return;
+ default:
+ converter.append(c);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "NORMAL";
+ }
+ };
+
+ /** State following the reading of a single \. */
+ private static final State ESCAPE =
+ new State() {
+ @Override
+ void process(GlobToRegex converter, char c) {
+ converter.append(c);
+ converter.popState();
+ }
+
+ @Override
+ void finish(GlobToRegex converter) {
+ throw converter.syntaxError("Hanging escape (\\) at end of pattern");
+ }
+
+ @Override
+ public String toString() {
+ return "ESCAPE";
+ }
+ };
+
+ /** State following the reading of a single *. */
+ private static final State STAR =
+ new State() {
+ @Override
+ void process(GlobToRegex converter, char c) {
+ if (c == '*') {
+ converter.appendStarStar();
+ converter.popState();
+ } else {
+ converter.appendStar();
+ converter.popState();
+ converter.currentState().process(converter, c);
+ }
+ }
+
+ @Override
+ void finish(GlobToRegex converter) {
+ converter.appendStar();
+ }
+
+ @Override
+ public String toString() {
+ return "STAR";
+ }
+ };
+
+ /** State immediately following the reading of a [. */
+ private static final State BRACKET_FIRST_CHAR =
+ new State() {
+ @Override
+ void process(GlobToRegex converter, char c) {
+ if (c == ']') {
+ // A glob like "[]]" or "[]q]" is apparently fine in Unix (when used with ls for
+ // example) but doesn't work for the default java.nio.file implementations. In the cases
+ // of "[]]" it produces:
+ // java.util.regex.PatternSyntaxException: Unclosed character class near index 13
+ // ^[[^/]&&[]]\]$
+ // ^
+ // The error here is slightly different, but trying to make this work would require some
+ // kind of lookahead and break the simplicity of char-by-char conversion here. Also, if
+ // someone wants to include a ']' inside a character class, they should escape it.
+ throw converter.syntaxError("Empty []");
+ }
+ if (c == '!') {
+ converter.appendExact('^');
+ } else if (c == '-') {
+ converter.appendExact(c);
+ } else {
+ converter.appendInBracket(c);
+ }
+ converter.popState();
+ converter.pushState(BRACKET);
+ }
+
+ @Override
+ void finish(GlobToRegex converter) {
+ throw converter.syntaxError("Unclosed [");
+ }
+
+ @Override
+ public String toString() {
+ return "BRACKET_FIRST_CHAR";
+ }
+ };
+
+ /** State inside [brackets], but not at the first character inside the brackets. */
+ private static final State BRACKET =
+ new State() {
+ @Override
+ void process(GlobToRegex converter, char c) {
+ if (c == ']') {
+ converter.appendBracketEnd();
+ converter.popState();
+ } else {
+ converter.appendInBracket(c);
+ }
+ }
+
+ @Override
+ void finish(GlobToRegex converter) {
+ throw converter.syntaxError("Unclosed [");
+ }
+
+ @Override
+ public String toString() {
+ return "BRACKET";
+ }
+ };
+
+ /** State inside {curly braces}. */
+ private static final State CURLY_BRACE =
+ new State() {
+ @Override
+ void process(GlobToRegex converter, char c) {
+ switch (c) {
+ case '?':
+ converter.appendQuestionMark();
+ break;
+ case '[':
+ converter.appendBracketStart();
+ converter.pushState(BRACKET_FIRST_CHAR);
+ break;
+ case '{':
+ throw converter.syntaxError("{ not allowed in subpattern group");
+ case '*':
+ converter.pushState(STAR);
+ break;
+ case '\\':
+ converter.pushState(ESCAPE);
+ break;
+ case '}':
+ converter.appendCurlyBraceEnd();
+ converter.popState();
+ break;
+ case ',':
+ converter.appendSubpatternSeparator();
+ break;
+ default:
+ converter.append(c);
+ }
+ }
+
+ @Override
+ void finish(GlobToRegex converter) {
+ throw converter.syntaxError("Unclosed {");
+ }
+
+ @Override
+ public String toString() {
+ return "CURLY_BRACE";
+ }
+ };
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/GuardedBy.java b/jimfs/src/main/java/com/google/common/jimfs/GuardedBy.java
new file mode 100644
index 0000000..a653736
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/GuardedBy.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Error Prone Authors.
+ *
+ * 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.google.common.jimfs;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+// TODO(cpovirk): Delete this in favor of the copy in Error Prone once that has a module name.
+/** Indicates that the annotated element should be used only while holding the specified lock. */
+@Target({FIELD, METHOD})
+@Retention(CLASS)
+@interface GuardedBy {
+ /**
+ * The lock that should be held, specified in the format <a
+ * href="http://jcip.net/annotations/doc/net/jcip/annotations/GuardedBy.html">given in Java
+ * Concurrency in Practice</a>.
+ */
+ String value();
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Handler.java b/jimfs/src/main/java/com/google/common/jimfs/Handler.java
new file mode 100644
index 0000000..fd4ab74
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Handler.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+/**
+ * {@link URLStreamHandler} implementation for jimfs. Named {@code Handler} so that the class can be
+ * found by Java as described in the documentation for {@link URL#URL(String, String, int, String)
+ * URL}.
+ *
+ * <p>This class is only public because it is necessary for Java to find it. It is not intended to
+ * be used directly.
+ *
+ * @author Colin Decker
+ * @since 1.1
+ */
+public final class Handler extends URLStreamHandler {
+
+ private static final String JAVA_PROTOCOL_HANDLER_PACKAGES = "java.protocol.handler.pkgs";
+
+ /**
+ * Registers this handler by adding the package {@code com.google.common} to the system property
+ * {@code "java.protocol.handler.pkgs"}. Java will then look for this class in the {@code jimfs}
+ * (the name of the protocol) package of {@code com.google.common}.
+ *
+ * @throws SecurityException if the system property that needs to be set to register this handler
+ * can't be read or written.
+ */
+ static void register() {
+ register(Handler.class);
+ }
+
+ /** Generic method that would allow registration of any properly placed {@code Handler} class. */
+ static void register(Class<? extends URLStreamHandler> handlerClass) {
+ checkArgument("Handler".equals(handlerClass.getSimpleName()));
+
+ String pkg = handlerClass.getPackage().getName();
+ int lastDot = pkg.lastIndexOf('.');
+ checkArgument(lastDot > 0, "package for Handler (%s) must have a parent package", pkg);
+
+ String parentPackage = pkg.substring(0, lastDot);
+
+ String packages = System.getProperty(JAVA_PROTOCOL_HANDLER_PACKAGES);
+ if (packages == null) {
+ packages = parentPackage;
+ } else {
+ packages += "|" + parentPackage;
+ }
+ System.setProperty(JAVA_PROTOCOL_HANDLER_PACKAGES, packages);
+ }
+
+ /** @deprecated Not intended to be called directly; this class is only for use by Java itself. */
+ @Deprecated
+ public Handler() {} // a public, no-arg constructor is required
+
+ @Override
+ protected URLConnection openConnection(URL url) throws IOException {
+ return new PathURLConnection(url);
+ }
+
+ @Override
+ @SuppressWarnings("UnsynchronizedOverridesSynchronized") // no need to synchronize to return null
+ protected InetAddress getHostAddress(URL url) {
+ // jimfs uses the URI host to specify the name of the file system being used.
+ // In the default implementation of getHostAddress(URL), a non-null host would cause an attempt
+ // to look up the IP address, causing a slowdown on calling equals/hashCode methods on the URL
+ // object. By returning null, we speed up equality checks on URL's (since there isn't an IP to
+ // connect to).
+ return null;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/HeapDisk.java b/jimfs/src/main/java/com/google/common/jimfs/HeapDisk.java
new file mode 100644
index 0000000..ab06933
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/HeapDisk.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.math.LongMath;
+import java.io.IOException;
+import java.math.RoundingMode;
+
+/**
+ * A resizable pseudo-disk acting as a shared space for storing file data. A disk allocates fixed
+ * size blocks of bytes to files as needed and may cache blocks that have been freed for reuse. A
+ * memory disk has a fixed maximum number of blocks it will allocate at a time (which sets the total
+ * "size" of the disk) and a maximum number of unused blocks it will cache for reuse at a time
+ * (which sets the minimum amount of space the disk will use once
+ *
+ * @author Colin Decker
+ */
+final class HeapDisk {
+
+ /** Fixed size of each block for this disk. */
+ private final int blockSize;
+
+ /** Maximum total number of blocks that the disk may contain at any time. */
+ private final int maxBlockCount;
+
+ /** Maximum total number of unused blocks that may be cached for reuse at any time. */
+ private final int maxCachedBlockCount;
+
+ /**
+ * Cache of free blocks to be allocated to files. While this is stored as a file, it isn't used
+ * like a normal file: only the methods for accessing its blocks are used.
+ */
+ @VisibleForTesting final RegularFile blockCache;
+
+ /** The current total number of blocks that are currently allocated to files. */
+ private int allocatedBlockCount;
+
+ /** Creates a new disk using settings from the given configuration. */
+ public HeapDisk(Configuration config) {
+ this.blockSize = config.blockSize;
+ this.maxBlockCount = toBlockCount(config.maxSize, blockSize);
+ this.maxCachedBlockCount =
+ config.maxCacheSize == -1 ? maxBlockCount : toBlockCount(config.maxCacheSize, blockSize);
+ this.blockCache = createBlockCache(maxCachedBlockCount);
+ }
+
+ /**
+ * Creates a new disk with the given {@code blockSize}, {@code maxBlockCount} and {@code
+ * maxCachedBlockCount}.
+ */
+ public HeapDisk(int blockSize, int maxBlockCount, int maxCachedBlockCount) {
+ checkArgument(blockSize > 0, "blockSize (%s) must be positive", blockSize);
+ checkArgument(maxBlockCount > 0, "maxBlockCount (%s) must be positive", maxBlockCount);
+ checkArgument(
+ maxCachedBlockCount >= 0, "maxCachedBlockCount must be non-negative", maxCachedBlockCount);
+ this.blockSize = blockSize;
+ this.maxBlockCount = maxBlockCount;
+ this.maxCachedBlockCount = maxCachedBlockCount;
+ this.blockCache = createBlockCache(maxCachedBlockCount);
+ }
+
+ /** Returns the nearest multiple of {@code blockSize} that is <= {@code size}. */
+ private static int toBlockCount(long size, int blockSize) {
+ return (int) LongMath.divide(size, blockSize, RoundingMode.FLOOR);
+ }
+
+ private RegularFile createBlockCache(int maxCachedBlockCount) {
+ return new RegularFile(-1, this, new byte[Math.min(maxCachedBlockCount, 8192)][], 0, 0);
+ }
+
+ /** Returns the size of blocks created by this disk. */
+ public int blockSize() {
+ return blockSize;
+ }
+
+ /**
+ * Returns the total size of this disk. This is the maximum size of the disk and does not reflect
+ * the amount of data currently allocated or cached.
+ */
+ public synchronized long getTotalSpace() {
+ return maxBlockCount * (long) blockSize;
+ }
+
+ /**
+ * Returns the current number of unallocated bytes on this disk. This is the maximum number of
+ * additional bytes that could be allocated and does not reflect the number of bytes currently
+ * actually cached in the disk.
+ */
+ public synchronized long getUnallocatedSpace() {
+ return (maxBlockCount - allocatedBlockCount) * (long) blockSize;
+ }
+
+ /** Allocates the given number of blocks and adds them to the given file. */
+ public synchronized void allocate(RegularFile file, int count) throws IOException {
+ int newAllocatedBlockCount = allocatedBlockCount + count;
+ if (newAllocatedBlockCount > maxBlockCount) {
+ throw new IOException("out of disk space");
+ }
+
+ int newBlocksNeeded = Math.max(count - blockCache.blockCount(), 0);
+
+ for (int i = 0; i < newBlocksNeeded; i++) {
+ file.addBlock(new byte[blockSize]);
+ }
+
+ if (newBlocksNeeded != count) {
+ blockCache.transferBlocksTo(file, count - newBlocksNeeded);
+ }
+
+ allocatedBlockCount = newAllocatedBlockCount;
+ }
+
+ /** Frees all blocks in the given file. */
+ public void free(RegularFile file) {
+ free(file, file.blockCount());
+ }
+
+ /** Frees the last {@code count} blocks from the given file. */
+ public synchronized void free(RegularFile file, int count) {
+ int remainingCacheSpace = maxCachedBlockCount - blockCache.blockCount();
+ if (remainingCacheSpace > 0) {
+ file.copyBlocksTo(blockCache, Math.min(count, remainingCacheSpace));
+ }
+ file.truncateBlocks(file.blockCount() - count);
+
+ allocatedBlockCount -= count;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/InternalCharMatcher.java b/jimfs/src/main/java/com/google/common/jimfs/InternalCharMatcher.java
new file mode 100644
index 0000000..a3fba6a
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/InternalCharMatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import java.util.Arrays;
+
+/**
+ * Simple replacement for the real CharMatcher until it's out of @Beta.
+ *
+ * @author Colin Decker
+ */
+final class InternalCharMatcher {
+
+ public static InternalCharMatcher anyOf(String chars) {
+ return new InternalCharMatcher(chars);
+ }
+
+ private final char[] chars;
+
+ private InternalCharMatcher(String chars) {
+ this.chars = chars.toCharArray();
+ Arrays.sort(this.chars);
+ }
+
+ public boolean matches(char c) {
+ return Arrays.binarySearch(chars, c) >= 0;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java b/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java
new file mode 100644
index 0000000..a04ce46
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Jimfs.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.jimfs.SystemJimfsFileSystemProvider.FILE_SYSTEM_KEY;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.ProviderNotFoundException;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Static factory methods for creating new Jimfs file systems. File systems may either be created
+ * with a basic configuration matching the current operating system or by providing a specific
+ * {@link Configuration}. Basic {@linkplain Configuration#unix() UNIX}, {@linkplain
+ * Configuration#osX() Mac OS X} and {@linkplain Configuration#windows() Windows} configurations are
+ * provided.
+ *
+ * <p>Examples:
+ *
+ * <pre>
+ * // A file system with a configuration similar to the current OS
+ * FileSystem fileSystem = Jimfs.newFileSystem();
+ *
+ * // A file system with paths and behavior generally matching that of Windows
+ * FileSystem windows = Jimfs.newFileSystem(Configuration.windows()); </pre>
+ *
+ * <p>Additionally, various behavior of the file system can be customized by creating a custom
+ * {@link Configuration}. A modified version of one of the existing default configurations can be
+ * created using {@link Configuration#toBuilder()} or a new configuration can be created from
+ * scratch with {@link Configuration#builder(PathType)}. See {@link Configuration.Builder} for what
+ * can be configured.
+ *
+ * <p>Examples:
+ *
+ * <pre>
+ * // Modify the default UNIX configuration
+ * FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix()
+ * .toBuilder()
+ * .setAttributeViews("basic", "owner", "posix", "unix")
+ * .setWorkingDirectory("/home/user")
+ * .setBlockSize(4096)
+ * .build());
+ *
+ * // Create a custom configuration
+ * Configuration config = Configuration.builder(PathType.windows())
+ * .setRoots("C:\\", "D:\\", "E:\\")
+ * // ...
+ * .build(); </pre>
+ *
+ * @author Colin Decker
+ */
+public final class Jimfs {
+
+ /** The URI scheme for the Jimfs file system ("jimfs"). */
+ public static final String URI_SCHEME = "jimfs";
+
+ private static final Logger LOGGER = Logger.getLogger(Jimfs.class.getName());
+
+ private Jimfs() {}
+
+ /**
+ * Creates a new in-memory file system with a {@linkplain Configuration#forCurrentPlatform()
+ * default configuration} appropriate to the current operating system.
+ *
+ * <p>More specifically, if the operating system is Windows, {@link Configuration#windows()} is
+ * used; if the operating system is Mac OS X, {@link Configuration#osX()} is used; otherwise,
+ * {@link Configuration#unix()} is used.
+ */
+ public static FileSystem newFileSystem() {
+ return newFileSystem(newRandomFileSystemName());
+ }
+
+ /**
+ * Creates a new in-memory file system with a {@linkplain Configuration#forCurrentPlatform()
+ * default configuration} appropriate to the current operating system.
+ *
+ * <p>More specifically, if the operating system is Windows, {@link Configuration#windows()} is
+ * used; if the operating system is Mac OS X, {@link Configuration#osX()} is used; otherwise,
+ * {@link Configuration#unix()} is used.
+ *
+ * <p>The returned file system uses the given name as the host part of its URI and the URIs of
+ * paths in the file system. For example, given the name {@code my-file-system}, the file system's
+ * URI will be {@code jimfs://my-file-system} and the URI of the path {@code /foo/bar} will be
+ * {@code jimfs://my-file-system/foo/bar}.
+ */
+ public static FileSystem newFileSystem(String name) {
+ return newFileSystem(name, Configuration.forCurrentPlatform());
+ }
+
+ /** Creates a new in-memory file system with the given configuration. */
+ public static FileSystem newFileSystem(Configuration configuration) {
+ return newFileSystem(newRandomFileSystemName(), configuration);
+ }
+
+ /**
+ * Creates a new in-memory file system with the given configuration.
+ *
+ * <p>The returned file system uses the given name as the host part of its URI and the URIs of
+ * paths in the file system. For example, given the name {@code my-file-system}, the file system's
+ * URI will be {@code jimfs://my-file-system} and the URI of the path {@code /foo/bar} will be
+ * {@code jimfs://my-file-system/foo/bar}.
+ */
+ public static FileSystem newFileSystem(String name, Configuration configuration) {
+ try {
+ URI uri = new URI(URI_SCHEME, name, null, null);
+ return newFileSystem(uri, configuration);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @VisibleForTesting
+ static FileSystem newFileSystem(URI uri, Configuration config) {
+ checkArgument(
+ URI_SCHEME.equals(uri.getScheme()), "uri (%s) must have scheme %s", uri, URI_SCHEME);
+
+ try {
+ // Create the FileSystem. It uses JimfsFileSystemProvider as its provider, as that is
+ // the provider that actually implements the operations needed for Files methods to work.
+ JimfsFileSystem fileSystem =
+ JimfsFileSystems.newFileSystem(JimfsFileSystemProvider.instance(), uri, config);
+
+ /*
+ * Now, call FileSystems.newFileSystem, passing it the FileSystem we just created. This
+ * allows the system-loaded SystemJimfsFileSystemProvider instance to cache the FileSystem
+ * so that methods like Paths.get(URI) work.
+ * We do it in this awkward way to avoid issues when the classes in the API (this class
+ * and Configuration, for example) are loaded by a different classloader than the one that
+ * loads SystemJimfsFileSystemProvider using ServiceLoader. See
+ * https://github.com/google/jimfs/issues/18 for gory details.
+ */
+ try {
+ ImmutableMap<String, ?> env = ImmutableMap.of(FILE_SYSTEM_KEY, fileSystem);
+ FileSystems.newFileSystem(uri, env, SystemJimfsFileSystemProvider.class.getClassLoader());
+ } catch (ProviderNotFoundException | ServiceConfigurationError ignore) {
+ // See the similar catch block below for why we ignore this.
+ // We log there rather than here so that there's only typically one such message per VM.
+ }
+
+ return fileSystem;
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * The system-loaded instance of {@code SystemJimfsFileSystemProvider}, or {@code null} if it
+ * could not be found or loaded.
+ */
+ @NullableDecl static final FileSystemProvider systemProvider = getSystemJimfsProvider();
+
+ /**
+ * Returns the system-loaded instance of {@code SystemJimfsFileSystemProvider} or {@code null} if
+ * it could not be found or loaded.
+ *
+ * <p>Like {@link FileSystems#newFileSystem(URI, Map, ClassLoader)}, this method first looks in
+ * the list of {@linkplain FileSystemProvider#installedProviders() installed providers} and if not
+ * found there, attempts to load it from the {@code ClassLoader} with {@link ServiceLoader}.
+ *
+ * <p>The idea is that this method should return an instance of the same class (i.e. loaded by the
+ * same class loader) as the class whose static cache a {@code JimfsFileSystem} instance will be
+ * placed in when {@code FileSystems.newFileSystem} is called in {@code Jimfs.newFileSystem}.
+ */
+ @NullableDecl
+ private static FileSystemProvider getSystemJimfsProvider() {
+ try {
+ for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
+ if (provider.getScheme().equals(URI_SCHEME)) {
+ return provider;
+ }
+ }
+
+ /*
+ * Jimfs.newFileSystem passes SystemJimfsFileSystemProvider.class.getClassLoader() to
+ * FileSystems.newFileSystem so that it will fall back to loading from that classloader if
+ * the provider isn't found in the installed providers. So do the same fallback here to ensure
+ * that we can remove file systems from the static cache on SystemJimfsFileSystemProvider if
+ * it gets loaded that way.
+ */
+ ServiceLoader<FileSystemProvider> loader =
+ ServiceLoader.load(
+ FileSystemProvider.class, SystemJimfsFileSystemProvider.class.getClassLoader());
+ for (FileSystemProvider provider : loader) {
+ if (provider.getScheme().equals(URI_SCHEME)) {
+ return provider;
+ }
+ }
+ } catch (ProviderNotFoundException | ServiceConfigurationError e) {
+ /*
+ * This can apparently (https://github.com/google/jimfs/issues/31) occur in an environment
+ * where services are not loaded from META-INF/services, such as JBoss/Wildfly. In this
+ * case, FileSystems.newFileSystem will most likely fail in the same way when called from
+ * Jimfs.newFileSystem above, and there will be no way to make URI-based methods like
+ * Paths.get(URI) work. Rather than making the user completly unable to use Jimfs, just
+ * log this exception and continue.
+ *
+ * Note: Catching both ProviderNotFoundException, which would occur if no provider matching
+ * the "jimfs" URI scheme is found, and ServiceConfigurationError, which can occur if the
+ * ServiceLoader finds the META-INF/services entry for Jimfs (or some other
+ * FileSystemProvider!) but is then unable to load that class.
+ */
+ LOGGER.log(
+ Level.INFO,
+ "An exception occurred when attempting to find the system-loaded FileSystemProvider "
+ + "for Jimfs. This likely means that your environment does not support loading "
+ + "services via ServiceLoader or is not configured correctly. This does not prevent "
+ + "using Jimfs, but it will mean that methods that look up via URI such as "
+ + "Paths.get(URI) cannot work.",
+ e);
+ }
+
+ return null;
+ }
+
+ private static String newRandomFileSystemName() {
+ return UUID.randomUUID().toString();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsAsynchronousFileChannel.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsAsynchronousFileChannel.java
new file mode 100644
index 0000000..c59522c
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsAsynchronousFileChannel.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.FileLock;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * {@link AsynchronousFileChannel} implementation that delegates to a {@link JimfsFileChannel}.
+ *
+ * @author Colin Decker
+ */
+final class JimfsAsynchronousFileChannel extends AsynchronousFileChannel {
+
+ private final JimfsFileChannel channel;
+ private final ListeningExecutorService executor;
+
+ public JimfsAsynchronousFileChannel(JimfsFileChannel channel, ExecutorService executor) {
+ this.channel = checkNotNull(channel);
+ this.executor = MoreExecutors.listeningDecorator(executor);
+ }
+
+ @Override
+ public long size() throws IOException {
+ return channel.size();
+ }
+
+ private <R, A> void addCallback(
+ ListenableFuture<R> future,
+ CompletionHandler<R, ? super A> handler,
+ @NullableDecl A attachment) {
+ future.addListener(new CompletionHandlerCallback<>(future, handler, attachment), executor);
+ }
+
+ @Override
+ public AsynchronousFileChannel truncate(long size) throws IOException {
+ channel.truncate(size);
+ return this;
+ }
+
+ @Override
+ public void force(boolean metaData) throws IOException {
+ channel.force(metaData);
+ }
+
+ @Override
+ public <A> void lock(
+ long position,
+ long size,
+ boolean shared,
+ @NullableDecl A attachment,
+ CompletionHandler<FileLock, ? super A> handler) {
+ checkNotNull(handler);
+ addCallback(lock(position, size, shared), handler, attachment);
+ }
+
+ @Override
+ public ListenableFuture<FileLock> lock(
+ final long position, final long size, final boolean shared) {
+ Util.checkNotNegative(position, "position");
+ Util.checkNotNegative(size, "size");
+ if (!isOpen()) {
+ return closedChannelFuture();
+ }
+ if (shared) {
+ channel.checkReadable();
+ } else {
+ channel.checkWritable();
+ }
+ return executor.submit(
+ new Callable<FileLock>() {
+ @Override
+ public FileLock call() throws IOException {
+ return tryLock(position, size, shared);
+ }
+ });
+ }
+
+ @Override
+ public FileLock tryLock(long position, long size, boolean shared) throws IOException {
+ Util.checkNotNegative(position, "position");
+ Util.checkNotNegative(size, "size");
+ channel.checkOpen();
+ if (shared) {
+ channel.checkReadable();
+ } else {
+ channel.checkWritable();
+ }
+ return new JimfsFileChannel.FakeFileLock(this, position, size, shared);
+ }
+
+ @Override
+ public <A> void read(
+ ByteBuffer dst,
+ long position,
+ @NullableDecl A attachment,
+ CompletionHandler<Integer, ? super A> handler) {
+ addCallback(read(dst, position), handler, attachment);
+ }
+
+ @Override
+ public ListenableFuture<Integer> read(final ByteBuffer dst, final long position) {
+ checkArgument(!dst.isReadOnly(), "dst may not be read-only");
+ Util.checkNotNegative(position, "position");
+ if (!isOpen()) {
+ return closedChannelFuture();
+ }
+ channel.checkReadable();
+ return executor.submit(
+ new Callable<Integer>() {
+ @Override
+ public Integer call() throws IOException {
+ return channel.read(dst, position);
+ }
+ });
+ }
+
+ @Override
+ public <A> void write(
+ ByteBuffer src,
+ long position,
+ @NullableDecl A attachment,
+ CompletionHandler<Integer, ? super A> handler) {
+ addCallback(write(src, position), handler, attachment);
+ }
+
+ @Override
+ public ListenableFuture<Integer> write(final ByteBuffer src, final long position) {
+ Util.checkNotNegative(position, "position");
+ if (!isOpen()) {
+ return closedChannelFuture();
+ }
+ channel.checkWritable();
+ return executor.submit(
+ new Callable<Integer>() {
+ @Override
+ public Integer call() throws IOException {
+ return channel.write(src, position);
+ }
+ });
+ }
+
+ @Override
+ public boolean isOpen() {
+ return channel.isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ channel.close();
+ }
+
+ /** Immediate future indicating that the channel is closed. */
+ private static <V> ListenableFuture<V> closedChannelFuture() {
+ SettableFuture<V> future = SettableFuture.create();
+ future.setException(new ClosedChannelException());
+ return future;
+ }
+
+ /** Runnable callback that wraps a {@link CompletionHandler} and an attachment. */
+ private static final class CompletionHandlerCallback<R, A> implements Runnable {
+
+ private final ListenableFuture<R> future;
+ private final CompletionHandler<R, ? super A> completionHandler;
+ @NullableDecl private final A attachment;
+
+ private CompletionHandlerCallback(
+ ListenableFuture<R> future,
+ CompletionHandler<R, ? super A> completionHandler,
+ @NullableDecl A attachment) {
+ this.future = checkNotNull(future);
+ this.completionHandler = checkNotNull(completionHandler);
+ this.attachment = attachment;
+ }
+
+ @Override
+ public void run() {
+ R result;
+ try {
+ result = future.get();
+ } catch (ExecutionException e) {
+ onFailure(e.getCause());
+ return;
+ } catch (InterruptedException | RuntimeException | Error e) {
+ // get() shouldn't be interrupted since this should only be called when the result is
+ // ready, but just handle it anyway to be sure and to satisfy the compiler
+ onFailure(e);
+ return;
+ }
+
+ onSuccess(result);
+ }
+
+ private void onSuccess(R result) {
+ completionHandler.completed(result, attachment);
+ }
+
+ private void onFailure(Throwable t) {
+ completionHandler.failed(t, attachment);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileChannel.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileChannel.java
new file mode 100644
index 0000000..95863cc
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileChannel.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkPositionIndexes;
+import static java.nio.file.StandardOpenOption.APPEND;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.WRITE;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.ClosedByInterruptException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.FileLockInterruptionException;
+import java.nio.channels.NonReadableChannelException;
+import java.nio.channels.NonWritableChannelException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.OpenOption;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A {@link FileChannel} implementation that reads and writes to a {@link RegularFile} object. The
+ * read and write methods and other methods that read or change the position of the channel are
+ * locked because the {@link ReadableByteChannel} and {@link WritableByteChannel} interfaces specify
+ * that the read and write methods block when another thread is currently doing a read or write
+ * operation.
+ *
+ * @author Colin Decker
+ */
+final class JimfsFileChannel extends FileChannel {
+
+ /**
+ * Set of threads that are currently doing an interruptible blocking operation; that is, doing
+ * something that requires acquiring the file's lock. These threads must be interrupted if the
+ * channel is closed by another thread.
+ */
+ @GuardedBy("blockingThreads")
+ private final Set<Thread> blockingThreads = new HashSet<Thread>();
+
+ private final RegularFile file;
+ private final FileSystemState fileSystemState;
+
+ private final boolean read;
+ private final boolean write;
+ private final boolean append;
+
+ @GuardedBy("this")
+ private long position;
+
+ public JimfsFileChannel(
+ RegularFile file, Set<OpenOption> options, FileSystemState fileSystemState) {
+ this.file = file;
+ this.fileSystemState = fileSystemState;
+ this.read = options.contains(READ);
+ this.write = options.contains(WRITE);
+ this.append = options.contains(APPEND);
+
+ fileSystemState.register(this);
+ }
+
+ /**
+ * Returns an {@link AsynchronousFileChannel} view of this channel using the given executor for
+ * asynchronous operations.
+ */
+ public AsynchronousFileChannel asAsynchronousFileChannel(ExecutorService executor) {
+ return new JimfsAsynchronousFileChannel(this, executor);
+ }
+
+ void checkReadable() {
+ if (!read) {
+ throw new NonReadableChannelException();
+ }
+ }
+
+ void checkWritable() {
+ if (!write) {
+ throw new NonWritableChannelException();
+ }
+ }
+
+ void checkOpen() throws ClosedChannelException {
+ if (!isOpen()) {
+ throw new ClosedChannelException();
+ }
+ }
+
+ /**
+ * Begins a blocking operation, making the operation interruptible. Returns {@code true} if the
+ * channel was open and the thread was added as a blocking thread; returns {@code false} if the
+ * channel was closed.
+ */
+ private boolean beginBlocking() {
+ begin();
+ synchronized (blockingThreads) {
+ if (isOpen()) {
+ blockingThreads.add(Thread.currentThread());
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Ends a blocking operation, throwing an exception if the thread was interrupted while blocking
+ * or if the channel was closed from another thread.
+ */
+ private void endBlocking(boolean completed) throws AsynchronousCloseException {
+ synchronized (blockingThreads) {
+ blockingThreads.remove(Thread.currentThread());
+ }
+ end(completed);
+ }
+
+ @Override
+ public int read(ByteBuffer dst) throws IOException {
+ checkNotNull(dst);
+ checkOpen();
+ checkReadable();
+
+ int read = 0; // will definitely either be assigned or an exception will be thrown
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.readLock().lockInterruptibly();
+ try {
+ read = file.read(position, dst);
+ if (read != -1) {
+ position += read;
+ }
+ file.updateAccessTime();
+ completed = true;
+ } finally {
+ file.readLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return read;
+ }
+
+ @Override
+ public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
+ checkPositionIndexes(offset, offset + length, dsts.length);
+ List<ByteBuffer> buffers = Arrays.asList(dsts).subList(offset, offset + length);
+ Util.checkNoneNull(buffers);
+ checkOpen();
+ checkReadable();
+
+ long read = 0; // will definitely either be assigned or an exception will be thrown
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.readLock().lockInterruptibly();
+ try {
+ read = file.read(position, buffers);
+ if (read != -1) {
+ position += read;
+ }
+ file.updateAccessTime();
+ completed = true;
+ } finally {
+ file.readLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return read;
+ }
+
+ @Override
+ public int read(ByteBuffer dst, long position) throws IOException {
+ checkNotNull(dst);
+ Util.checkNotNegative(position, "position");
+ checkOpen();
+ checkReadable();
+
+ int read = 0; // will definitely either be assigned or an exception will be thrown
+
+ // no need to synchronize here; this method does not make use of the channel's position
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.readLock().lockInterruptibly();
+ try {
+ read = file.read(position, dst);
+ file.updateAccessTime();
+ completed = true;
+ } finally {
+ file.readLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+
+ return read;
+ }
+
+ @Override
+ public int write(ByteBuffer src) throws IOException {
+ checkNotNull(src);
+ checkOpen();
+ checkWritable();
+
+ int written = 0; // will definitely either be assigned or an exception will be thrown
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.writeLock().lockInterruptibly();
+ try {
+ if (append) {
+ position = file.size();
+ }
+ written = file.write(position, src);
+ position += written;
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return written;
+ }
+
+ @Override
+ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
+ checkPositionIndexes(offset, offset + length, srcs.length);
+ List<ByteBuffer> buffers = Arrays.asList(srcs).subList(offset, offset + length);
+ Util.checkNoneNull(buffers);
+ checkOpen();
+ checkWritable();
+
+ long written = 0; // will definitely either be assigned or an exception will be thrown
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.writeLock().lockInterruptibly();
+ try {
+ if (append) {
+ position = file.size();
+ }
+ written = file.write(position, buffers);
+ position += written;
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return written;
+ }
+
+ @Override
+ public int write(ByteBuffer src, long position) throws IOException {
+ checkNotNull(src);
+ Util.checkNotNegative(position, "position");
+ checkOpen();
+ checkWritable();
+
+ int written = 0; // will definitely either be assigned or an exception will be thrown
+
+ if (append) {
+ // synchronize because appending does update the channel's position
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+
+ file.writeLock().lockInterruptibly();
+ try {
+ position = file.sizeWithoutLocking();
+ written = file.write(position, src);
+ this.position = position + written;
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+ } else {
+ // don't synchronize because the channel's position is not involved
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.writeLock().lockInterruptibly();
+ try {
+ written = file.write(position, src);
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return written;
+ }
+
+ @Override
+ public long position() throws IOException {
+ checkOpen();
+
+ long pos;
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ begin(); // don't call beginBlocking() because this method doesn't block
+ if (!isOpen()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ pos = this.position;
+ completed = true;
+ } finally {
+ end(completed);
+ }
+ }
+
+ return pos;
+ }
+
+ @Override
+ public FileChannel position(long newPosition) throws IOException {
+ Util.checkNotNegative(newPosition, "newPosition");
+ checkOpen();
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ begin(); // don't call beginBlocking() because this method doesn't block
+ if (!isOpen()) {
+ return this; // AsynchronousCloseException will be thrown
+ }
+ this.position = newPosition;
+ completed = true;
+ } finally {
+ end(completed);
+ }
+ }
+
+ return this;
+ }
+
+ @Override
+ public long size() throws IOException {
+ checkOpen();
+
+ long size = 0; // will definitely either be assigned or an exception will be thrown
+
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.readLock().lockInterruptibly();
+ try {
+ size = file.sizeWithoutLocking();
+ completed = true;
+ } finally {
+ file.readLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+
+ return size;
+ }
+
+ @Override
+ public FileChannel truncate(long size) throws IOException {
+ Util.checkNotNegative(size, "size");
+ checkOpen();
+ checkWritable();
+
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return this; // AsynchronousCloseException will be thrown
+ }
+ file.writeLock().lockInterruptibly();
+ try {
+ file.truncate(size);
+ if (position > size) {
+ position = size;
+ }
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return this;
+ }
+
+ @Override
+ public void force(boolean metaData) throws IOException {
+ checkOpen();
+
+ // nothing to do since writes are all direct to the storage
+ // however, we should handle the thread being interrupted anyway
+ boolean completed = false;
+ try {
+ begin();
+ completed = true;
+ } finally {
+ end(completed);
+ }
+ }
+
+ @Override
+ public long transferTo(long position, long count, WritableByteChannel target) throws IOException {
+ checkNotNull(target);
+ Util.checkNotNegative(position, "position");
+ Util.checkNotNegative(count, "count");
+ checkOpen();
+ checkReadable();
+
+ long transferred = 0; // will definitely either be assigned or an exception will be thrown
+
+ // no need to synchronize here; this method does not make use of the channel's position
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.readLock().lockInterruptibly();
+ try {
+ transferred = file.transferTo(position, count, target);
+ file.updateAccessTime();
+ completed = true;
+ } finally {
+ file.readLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+
+ return transferred;
+ }
+
+ @Override
+ public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException {
+ checkNotNull(src);
+ Util.checkNotNegative(position, "position");
+ Util.checkNotNegative(count, "count");
+ checkOpen();
+ checkWritable();
+
+ long transferred = 0; // will definitely either be assigned or an exception will be thrown
+
+ if (append) {
+ // synchronize because appending does update the channel's position
+ synchronized (this) {
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+
+ file.writeLock().lockInterruptibly();
+ try {
+ position = file.sizeWithoutLocking();
+ transferred = file.transferFrom(src, position, count);
+ this.position = position + transferred;
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+ } else {
+ // don't synchronize because the channel's position is not involved
+ boolean completed = false;
+ try {
+ if (!beginBlocking()) {
+ return 0; // AsynchronousCloseException will be thrown
+ }
+ file.writeLock().lockInterruptibly();
+ try {
+ transferred = file.transferFrom(src, position, count);
+ file.updateModifiedTime();
+ completed = true;
+ } finally {
+ file.writeLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ endBlocking(completed);
+ }
+ }
+
+ return transferred;
+ }
+
+ @Override
+ public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
+ // would like this to pretend to work, but can't create an implementation of MappedByteBuffer
+ // well, a direct buffer could be cast to MappedByteBuffer, but it couldn't work in general
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public FileLock lock(long position, long size, boolean shared) throws IOException {
+ checkLockArguments(position, size, shared);
+
+ // lock is interruptible
+ boolean completed = false;
+ try {
+ begin();
+ completed = true;
+ return new FakeFileLock(this, position, size, shared);
+ } finally {
+ try {
+ end(completed);
+ } catch (ClosedByInterruptException e) {
+ throw new FileLockInterruptionException();
+ }
+ }
+ }
+
+ @Override
+ public FileLock tryLock(long position, long size, boolean shared) throws IOException {
+ checkLockArguments(position, size, shared);
+
+ // tryLock is not interruptible
+ return new FakeFileLock(this, position, size, shared);
+ }
+
+ private void checkLockArguments(long position, long size, boolean shared) throws IOException {
+ Util.checkNotNegative(position, "position");
+ Util.checkNotNegative(size, "size");
+ checkOpen();
+ if (shared) {
+ checkReadable();
+ } else {
+ checkWritable();
+ }
+ }
+
+ @Override
+ protected void implCloseChannel() {
+ // interrupt the current blocking threads, if any, causing them to throw
+ // ClosedByInterruptException
+ try {
+ synchronized (blockingThreads) {
+ for (Thread thread : blockingThreads) {
+ thread.interrupt();
+ }
+ }
+ } finally {
+ fileSystemState.unregister(this);
+ file.closed();
+ }
+ }
+
+ /** A file lock that does nothing, since only one JVM process has access to this file system. */
+ static final class FakeFileLock extends FileLock {
+
+ private final AtomicBoolean valid = new AtomicBoolean(true);
+
+ public FakeFileLock(FileChannel channel, long position, long size, boolean shared) {
+ super(channel, position, size, shared);
+ }
+
+ public FakeFileLock(AsynchronousFileChannel channel, long position, long size, boolean shared) {
+ super(channel, position, size, shared);
+ }
+
+ @Override
+ public boolean isValid() {
+ return valid.get();
+ }
+
+ @Override
+ public void release() throws IOException {
+ valid.set(false);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileStore.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileStore.java
new file mode 100644
index 0000000..910d231
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileStore.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * {@link FileStore} implementation which provides methods for file creation, lookup and attribute
+ * handling.
+ *
+ * <p>Most of these methods are actually implemented in another class: {@link FileTree} for lookup,
+ * {@link FileFactory} for creating and copying files and {@link AttributeService} for attribute
+ * handling. This class merely provides a single API through which to access the functionality of
+ * those classes.
+ *
+ * @author Colin Decker
+ */
+final class JimfsFileStore extends FileStore {
+
+ private final FileTree tree;
+ private final HeapDisk disk;
+ private final AttributeService attributes;
+ private final FileFactory factory;
+ private final ImmutableSet<Feature> supportedFeatures;
+ private final FileSystemState state;
+
+ private final Lock readLock;
+ private final Lock writeLock;
+
+ public JimfsFileStore(
+ FileTree tree,
+ FileFactory factory,
+ HeapDisk disk,
+ AttributeService attributes,
+ ImmutableSet<Feature> supportedFeatures,
+ FileSystemState state) {
+ this.tree = checkNotNull(tree);
+ this.factory = checkNotNull(factory);
+ this.disk = checkNotNull(disk);
+ this.attributes = checkNotNull(attributes);
+ this.supportedFeatures = checkNotNull(supportedFeatures);
+ this.state = checkNotNull(state);
+
+ ReadWriteLock lock = new ReentrantReadWriteLock();
+ this.readLock = lock.readLock();
+ this.writeLock = lock.writeLock();
+ }
+
+ // internal use methods
+
+ /** Returns the file system state object. */
+ FileSystemState state() {
+ return state;
+ }
+
+ /** Returns the read lock for this store. */
+ Lock readLock() {
+ return readLock;
+ }
+
+ /** Returns the write lock for this store. */
+ Lock writeLock() {
+ return writeLock;
+ }
+
+ /** Returns the names of the root directories in this store. */
+ ImmutableSortedSet<Name> getRootDirectoryNames() {
+ state.checkOpen();
+ return tree.getRootDirectoryNames();
+ }
+
+ /** Returns the root directory with the given name or {@code null} if no such directory exists. */
+ @NullableDecl
+ Directory getRoot(Name name) {
+ DirectoryEntry entry = tree.getRoot(name);
+ return entry == null ? null : (Directory) entry.file();
+ }
+
+ /** Returns whether or not the given feature is supported by this file store. */
+ boolean supportsFeature(Feature feature) {
+ return supportedFeatures.contains(feature);
+ }
+
+ /**
+ * Looks up the file at the given path using the given link options. If the path is relative, the
+ * lookup is relative to the given working directory.
+ *
+ * @throws NoSuchFileException if an element of the path other than the final element does not
+ * resolve to a directory or symbolic link (e.g. it doesn't exist or is a regular file)
+ * @throws IOException if a symbolic link cycle is detected or the depth of symbolic link
+ * recursion otherwise exceeds a threshold
+ */
+ DirectoryEntry lookUp(File workingDirectory, JimfsPath path, Set<? super LinkOption> options)
+ throws IOException {
+ state.checkOpen();
+ return tree.lookUp(workingDirectory, path, options);
+ }
+
+ /** Returns a supplier that creates a new regular file. */
+ Supplier<RegularFile> regularFileCreator() {
+ state.checkOpen();
+ return factory.regularFileCreator();
+ }
+
+ /** Returns a supplier that creates a new directory. */
+ Supplier<Directory> directoryCreator() {
+ state.checkOpen();
+ return factory.directoryCreator();
+ }
+
+ /** Returns a supplier that creates a new symbolic link with the given target. */
+ Supplier<SymbolicLink> symbolicLinkCreator(JimfsPath target) {
+ state.checkOpen();
+ return factory.symbolicLinkCreator(target);
+ }
+
+ /**
+ * Creates a copy of the given file, copying its attributes as well according to the given {@code
+ * attributeCopyOption}.
+ */
+ File copyWithoutContent(File file, AttributeCopyOption attributeCopyOption) throws IOException {
+ File copy = factory.copyWithoutContent(file);
+ setInitialAttributes(copy);
+ attributes.copyAttributes(file, copy, attributeCopyOption);
+ return copy;
+ }
+
+ /**
+ * Sets initial attributes on the given file. Sets default attributes first, then attempts to set
+ * the given user-provided attributes.
+ */
+ void setInitialAttributes(File file, FileAttribute<?>... attrs) {
+ state.checkOpen();
+ attributes.setInitialAttributes(file, attrs);
+ }
+
+ /**
+ * Returns an attribute view of the given type for the given file lookup callback, or {@code null}
+ * if the view type is not supported.
+ */
+ @NullableDecl
+ <V extends FileAttributeView> V getFileAttributeView(FileLookup lookup, Class<V> type) {
+ state.checkOpen();
+ return attributes.getFileAttributeView(lookup, type);
+ }
+
+ /**
+ * Returns a map containing the attributes described by the given string mapped to their values.
+ */
+ ImmutableMap<String, Object> readAttributes(File file, String attributes) {
+ state.checkOpen();
+ return this.attributes.readAttributes(file, attributes);
+ }
+
+ /**
+ * Returns attributes of the given file as an object of the given type.
+ *
+ * @throws UnsupportedOperationException if the given attributes type is not supported
+ */
+ <A extends BasicFileAttributes> A readAttributes(File file, Class<A> type) {
+ state.checkOpen();
+ return attributes.readAttributes(file, type);
+ }
+
+ /** Sets the given attribute to the given value for the given file. */
+ void setAttribute(File file, String attribute, Object value) {
+ state.checkOpen();
+ // TODO(cgdecker): Change attribute stuff to avoid the sad boolean parameter
+ attributes.setAttribute(file, attribute, value, false);
+ }
+
+ /** Returns the file attribute views supported by this store. */
+ ImmutableSet<String> supportedFileAttributeViews() {
+ state.checkOpen();
+ return attributes.supportedFileAttributeViews();
+ }
+
+ // methods implementing the FileStore API
+
+ @Override
+ public String name() {
+ return "jimfs";
+ }
+
+ @Override
+ public String type() {
+ return "jimfs";
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public long getTotalSpace() throws IOException {
+ state.checkOpen();
+ return disk.getTotalSpace();
+ }
+
+ @Override
+ public long getUsableSpace() throws IOException {
+ state.checkOpen();
+ return getUnallocatedSpace();
+ }
+
+ @Override
+ public long getUnallocatedSpace() throws IOException {
+ state.checkOpen();
+ return disk.getUnallocatedSpace();
+ }
+
+ @Override
+ public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
+ state.checkOpen();
+ return attributes.supportsFileAttributeView(type);
+ }
+
+ @Override
+ public boolean supportsFileAttributeView(String name) {
+ state.checkOpen();
+ return attributes.supportedFileAttributeViews().contains(name);
+ }
+
+ @Override
+ public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
+ state.checkOpen();
+ return null; // no supported views
+ }
+
+ @Override
+ public Object getAttribute(String attribute) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java
new file mode 100644
index 0000000..dd72146
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * {@link FileSystem} implementation for Jimfs. Most behavior for the file system is implemented by
+ * its {@linkplain #getDefaultView() default file system view}.
+ *
+ * <h3>Overview of file system design</h3>
+ *
+ * {@link com.google.common.jimfs.JimfsFileSystem JimfsFileSystem} instances are created by {@link
+ * com.google.common.jimfs.JimfsFileSystems JimfsFileSystems} using a user-provided {@link
+ * com.google.common.jimfs.Configuration Configuration}. The configuration is used to create the
+ * various classes that implement the file system with the correct settings and to create the file
+ * system root directories and working directory. The file system is then used to create the {@code
+ * Path} objects that all file system operations use.
+ *
+ * <p>Once created, the primary entry points to the file system are {@link
+ * com.google.common.jimfs.JimfsFileSystemProvider JimfsFileSystemProvider}, which handles calls to
+ * methods in {@link java.nio.file.Files}, and {@link
+ * com.google.common.jimfs.JimfsSecureDirectoryStream JimfsSecureDirectoryStream}, which provides
+ * methods that are similar to those of the file system provider but which treat relative paths as
+ * relative to the stream's directory rather than the file system's working directory.
+ *
+ * <p>The implementation of the methods on both of those classes is handled by the {@link
+ * com.google.common.jimfs.FileSystemView FileSystemView} class, which acts as a view of the file
+ * system with a specific working directory. The file system provider uses the file system's default
+ * view, while each secure directory stream uses a view specific to that stream.
+ *
+ * <p>File system views make use of the file system's singleton {@link
+ * com.google.common.jimfs.JimfsFileStore JimfsFileStore} which handles file creation, storage and
+ * attributes. The file store delegates to several other classes to handle each of these:
+ *
+ * <ul>
+ * <li>{@link com.google.common.jimfs.FileFactory FileFactory} handles creation of new file
+ * objects.
+ * <li>{@link com.google.common.jimfs.HeapDisk HeapDisk} handles allocation of blocks to {@link
+ * RegularFile RegularFile} instances.
+ * <li>{@link com.google.common.jimfs.FileTree FileTree} stores the root of the file hierarchy and
+ * handles file lookup.
+ * <li>{@link com.google.common.jimfs.AttributeService AttributeService} handles file attributes,
+ * using a set of {@link com.google.common.jimfs.AttributeProvider AttributeProvider}
+ * implementations to handle each supported file attribute view.
+ * </ul>
+ *
+ * <h3>Paths</h3>
+ *
+ * The implementation of {@link java.nio.file.Path} for the file system is {@link
+ * com.google.common.jimfs.JimfsPath JimfsPath}. Paths are created by a {@link
+ * com.google.common.jimfs.PathService PathService} with help from the file system's configured
+ * {@link com.google.common.jimfs.PathType PathType}.
+ *
+ * <p>Paths are made up of {@link com.google.common.jimfs.Name Name} objects, which also serve as
+ * the file names in directories. A name has two forms:
+ *
+ * <ul>
+ * <li>The <b>display form</b> is used in {@code Path} for {@code toString()}. It is also used for
+ * determining the equality and sort order of {@code Path} objects for most file systems.
+ * <li>The <b>canonical form</b> is used for equality of two {@code Name} objects. This affects
+ * the notion of name equality in the file system itself for file lookup. A file system may be
+ * configured to use the canonical form of the name for path equality (a Windows-like file
+ * system configuration does this, as the real Windows file system implementation uses
+ * case-insensitive equality for its path objects.
+ * </ul>
+ *
+ * <p>The canonical form of a name is created by applying a series of {@linkplain PathNormalization
+ * normalizations} to the original string. These normalization may be either a Unicode normalization
+ * (e.g. NFD) or case folding normalization for case-insensitivity. Normalizations may also be
+ * applied to the display form of a name, but this is currently only done for a Mac OS X type
+ * configuration.
+ *
+ * <h3>Files</h3>
+ *
+ * All files in the file system are an instance of {@link com.google.common.jimfs.File File}. A file
+ * object contains both the file's attributes and content.
+ *
+ * <p>There are three types of files:
+ *
+ * <ul>
+ * <li>{@link Directory Directory} - contains a table linking file names to {@linkplain
+ * com.google.common.jimfs.DirectoryEntry directory entries}.
+ * <li>{@link RegularFile RegularFile} - an in-memory store for raw bytes.
+ * <li>{@link com.google.common.jimfs.SymbolicLink SymbolicLink} - contains a path.
+ * </ul>
+ *
+ * <p>{@link com.google.common.jimfs.JimfsFileChannel JimfsFileChannel}, {@link
+ * com.google.common.jimfs.JimfsInputStream JimfsInputStream} and {@link
+ * com.google.common.jimfs.JimfsOutputStream JimfsOutputStream} implement the standard
+ * channel/stream APIs for regular files.
+ *
+ * <p>{@link com.google.common.jimfs.JimfsSecureDirectoryStream JimfsSecureDirectoryStream} handles
+ * reading the entries of a directory. The secure directory stream additionally contains a {@code
+ * FileSystemView} with its directory as the working directory, allowing for operations relative to
+ * the actual directory file rather than just the path to the file. This allows the operations to
+ * continue to work as expected even if the directory is moved.
+ *
+ * <p>A directory can be watched for changes using the {@link java.nio.file.WatchService}
+ * implementation, {@link com.google.common.jimfs.PollingWatchService PollingWatchService}.
+ *
+ * <h3>Regular files</h3>
+ *
+ * {@link RegularFile RegularFile} makes use of a singleton {@link com.google.common.jimfs.HeapDisk
+ * HeapDisk}. A disk is a resizable factory and cache for fixed size blocks of memory. These blocks
+ * are allocated to files as needed and returned to the disk when a file is deleted or truncated.
+ * When cached free blocks are available, those blocks are allocated to files first. If more blocks
+ * are needed, they are created.
+ *
+ * <h3>Linking</h3>
+ *
+ * When a file is mapped to a file name in a directory table, it is <i>linked</i>. Each type of file
+ * has different rules governing how it is linked.
+ *
+ * <ul>
+ * <li>Directory - A directory has two or more links to it. The first is the link from its parent
+ * directory to it. This link is the name of the directory. The second is the <i>self</i> link
+ * (".") which links the directory to itself. The directory may also have any number of
+ * additional <i>parent</i> links ("..") from child directories back to it.
+ * <li>Regular file - A regular file has one link from its parent directory by default. However,
+ * regular files are also allowed to have any number of additional user-created hard links,
+ * from the same directory with different names and/or from other directories with any names.
+ * <li>Symbolic link - A symbolic link can only have one link, from its parent directory.
+ * </ul>
+ *
+ * <h3>Thread safety</h3>
+ *
+ * All file system operations should be safe in a multithreaded environment. The file hierarchy
+ * itself is protected by a file system level read-write lock. This ensures safety of all
+ * modifications to directory tables as well as atomicity of operations like file moves. Regular
+ * files are each protected by a read-write lock which is obtained for each read or write operation.
+ * File attributes are protected by synchronization on the file object itself.
+ *
+ * @author Colin Decker
+ */
+final class JimfsFileSystem extends FileSystem {
+
+ private final JimfsFileSystemProvider provider;
+ private final URI uri;
+
+ private final JimfsFileStore fileStore;
+ private final PathService pathService;
+
+ private final UserPrincipalLookupService userLookupService = new UserLookupService(true);
+
+ private final FileSystemView defaultView;
+
+ private final WatchServiceConfiguration watchServiceConfig;
+
+ JimfsFileSystem(
+ JimfsFileSystemProvider provider,
+ URI uri,
+ JimfsFileStore fileStore,
+ PathService pathService,
+ FileSystemView defaultView,
+ WatchServiceConfiguration watchServiceConfig) {
+ this.provider = checkNotNull(provider);
+ this.uri = checkNotNull(uri);
+ this.fileStore = checkNotNull(fileStore);
+ this.pathService = checkNotNull(pathService);
+ this.defaultView = checkNotNull(defaultView);
+ this.watchServiceConfig = checkNotNull(watchServiceConfig);
+ }
+
+ @Override
+ public JimfsFileSystemProvider provider() {
+ return provider;
+ }
+
+ /** Returns the URI for this file system. */
+ public URI getUri() {
+ return uri;
+ }
+
+ /** Returns the default view for this file system. */
+ public FileSystemView getDefaultView() {
+ return defaultView;
+ }
+
+ @Override
+ public String getSeparator() {
+ return pathService.getSeparator();
+ }
+
+ @SuppressWarnings("unchecked") // safe cast of immutable set
+ @Override
+ public ImmutableSortedSet<Path> getRootDirectories() {
+ ImmutableSortedSet.Builder<JimfsPath> builder = ImmutableSortedSet.orderedBy(pathService);
+ for (Name name : fileStore.getRootDirectoryNames()) {
+ builder.add(pathService.createRoot(name));
+ }
+ return (ImmutableSortedSet<Path>) (ImmutableSortedSet<?>) builder.build();
+ }
+
+ /** Returns the working directory path for this file system. */
+ public JimfsPath getWorkingDirectory() {
+ return defaultView.getWorkingDirectoryPath();
+ }
+
+ /** Returns the path service for this file system. */
+ @VisibleForTesting
+ PathService getPathService() {
+ return pathService;
+ }
+
+ /** Returns the file store for this file system. */
+ public JimfsFileStore getFileStore() {
+ return fileStore;
+ }
+
+ @Override
+ public ImmutableSet<FileStore> getFileStores() {
+ fileStore.state().checkOpen();
+ return ImmutableSet.<FileStore>of(fileStore);
+ }
+
+ @Override
+ public ImmutableSet<String> supportedFileAttributeViews() {
+ return fileStore.supportedFileAttributeViews();
+ }
+
+ @Override
+ public JimfsPath getPath(String first, String... more) {
+ fileStore.state().checkOpen();
+ return pathService.parsePath(first, more);
+ }
+
+ /** Gets the URI of the given path in this file system. */
+ public URI toUri(JimfsPath path) {
+ fileStore.state().checkOpen();
+ return pathService.toUri(uri, path.toAbsolutePath());
+ }
+
+ /** Converts the given URI into a path in this file system. */
+ public JimfsPath toPath(URI uri) {
+ fileStore.state().checkOpen();
+ return pathService.fromUri(uri);
+ }
+
+ @Override
+ public PathMatcher getPathMatcher(String syntaxAndPattern) {
+ fileStore.state().checkOpen();
+ return pathService.createPathMatcher(syntaxAndPattern);
+ }
+
+ @Override
+ public UserPrincipalLookupService getUserPrincipalLookupService() {
+ fileStore.state().checkOpen();
+ return userLookupService;
+ }
+
+ @Override
+ public WatchService newWatchService() throws IOException {
+ return watchServiceConfig.newWatchService(defaultView, pathService);
+ }
+
+ @NullableDecl private ExecutorService defaultThreadPool;
+
+ /**
+ * Returns a default thread pool to use for asynchronous file channels when users do not provide
+ * an executor themselves. (This is required by the spec of newAsynchronousFileChannel in
+ * FileSystemProvider.)
+ */
+ public synchronized ExecutorService getDefaultThreadPool() {
+ if (defaultThreadPool == null) {
+ defaultThreadPool =
+ Executors.newCachedThreadPool(
+ new ThreadFactoryBuilder()
+ .setDaemon(true)
+ .setNameFormat("JimfsFileSystem-" + uri.getHost() + "-defaultThreadPool-%s")
+ .build());
+
+ // ensure thread pool is closed when file system is closed
+ fileStore
+ .state()
+ .register(
+ new Closeable() {
+ @Override
+ public void close() {
+ defaultThreadPool.shutdown();
+ }
+ });
+ }
+ return defaultThreadPool;
+ }
+
+ /**
+ * Returns {@code false}; currently, cannot create a read-only file system.
+ *
+ * @return {@code false}, always
+ */
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public boolean isOpen() {
+ return fileStore.state().isOpen();
+ }
+
+ @Override
+ public void close() throws IOException {
+ fileStore.state().close();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystemProvider.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystemProvider.java
new file mode 100644
index 0000000..8d487dd
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystemProvider.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.jimfs.Feature.FILE_CHANNEL;
+import static com.google.common.jimfs.Jimfs.URI_SCHEME;
+import static java.nio.file.StandardOpenOption.APPEND;
+
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.DosFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * {@link FileSystemProvider} implementation for Jimfs. This provider implements the actual file
+ * system operations but does not handle creation, caching or lookup of file systems. See {@link
+ * SystemJimfsFileSystemProvider}, which is the {@code META-INF/services/} entry for Jimfs, for
+ * those operations.
+ *
+ * @author Colin Decker
+ */
+final class JimfsFileSystemProvider extends FileSystemProvider {
+
+ private static final JimfsFileSystemProvider INSTANCE = new JimfsFileSystemProvider();
+
+ static {
+ // Register the URL stream handler implementation.
+ try {
+ Handler.register();
+ } catch (Throwable e) {
+ // Couldn't set the system property needed to register the handler. Nothing we can do really.
+ }
+ }
+
+ /** Returns the singleton instance of this provider. */
+ static JimfsFileSystemProvider instance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String getScheme() {
+ return URI_SCHEME;
+ }
+
+ @Override
+ public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+ throw new UnsupportedOperationException(
+ "This method should not be called directly;"
+ + "use an overload of Jimfs.newFileSystem() to create a FileSystem.");
+ }
+
+ @Override
+ public FileSystem newFileSystem(Path path, Map<String, ?> env) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ checkNotNull(env);
+
+ URI pathUri = checkedPath.toUri();
+ URI jarUri = URI.create("jar:" + pathUri);
+
+ try {
+ // pass the new jar:jimfs://... URI to be handled by ZipFileSystemProvider
+ return FileSystems.newFileSystem(jarUri, env);
+ } catch (Exception e) {
+ // if any exception occurred, assume the file wasn't a zip file and that we don't support
+ // viewing it as a file system
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ throw new UnsupportedOperationException(
+ "This method should not be called directly; "
+ + "use FileSystems.getFileSystem(URI) instead.");
+ }
+
+ /** Gets the file system for the given path. */
+ private static JimfsFileSystem getFileSystem(Path path) {
+ return (JimfsFileSystem) checkPath(path).getFileSystem();
+ }
+
+ @Override
+ public Path getPath(URI uri) {
+ throw new UnsupportedOperationException(
+ "This method should not be called directly; " + "use Paths.get(URI) instead.");
+ }
+
+ private static JimfsPath checkPath(Path path) {
+ if (path instanceof JimfsPath) {
+ return (JimfsPath) path;
+ }
+ throw new ProviderMismatchException(
+ "path " + path + " is not associated with a Jimfs file system");
+ }
+
+ /** Returns the default file system view for the given path. */
+ private static FileSystemView getDefaultView(JimfsPath path) {
+ return getFileSystem(path).getDefaultView();
+ }
+
+ @Override
+ public FileChannel newFileChannel(
+ Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ if (!checkedPath.getJimfsFileSystem().getFileStore().supportsFeature(FILE_CHANNEL)) {
+ throw new UnsupportedOperationException();
+ }
+ return newJimfsFileChannel(checkedPath, options, attrs);
+ }
+
+ private JimfsFileChannel newJimfsFileChannel(
+ JimfsPath path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
+ throws IOException {
+ ImmutableSet<OpenOption> opts = Options.getOptionsForChannel(options);
+ FileSystemView view = getDefaultView(path);
+ RegularFile file = view.getOrCreateRegularFile(path, opts, attrs);
+ return new JimfsFileChannel(file, opts, view.state());
+ }
+
+ @Override
+ public SeekableByteChannel newByteChannel(
+ Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ JimfsFileChannel channel = newJimfsFileChannel(checkedPath, options, attrs);
+ return checkedPath.getJimfsFileSystem().getFileStore().supportsFeature(FILE_CHANNEL)
+ ? channel
+ : new DowngradedSeekableByteChannel(channel);
+ }
+
+ @Override
+ public AsynchronousFileChannel newAsynchronousFileChannel(
+ Path path,
+ Set<? extends OpenOption> options,
+ @NullableDecl ExecutorService executor,
+ FileAttribute<?>... attrs)
+ throws IOException {
+ // call newFileChannel and cast so that FileChannel support is checked there
+ JimfsFileChannel channel = (JimfsFileChannel) newFileChannel(path, options, attrs);
+ if (executor == null) {
+ JimfsFileSystem fileSystem = (JimfsFileSystem) path.getFileSystem();
+ executor = fileSystem.getDefaultThreadPool();
+ }
+ return channel.asAsynchronousFileChannel(executor);
+ }
+
+ @Override
+ public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ ImmutableSet<OpenOption> opts = Options.getOptionsForInputStream(options);
+ FileSystemView view = getDefaultView(checkedPath);
+ RegularFile file = view.getOrCreateRegularFile(checkedPath, opts, NO_ATTRS);
+ return new JimfsInputStream(file, view.state());
+ }
+
+ private static final FileAttribute<?>[] NO_ATTRS = {};
+
+ @Override
+ public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ ImmutableSet<OpenOption> opts = Options.getOptionsForOutputStream(options);
+ FileSystemView view = getDefaultView(checkedPath);
+ RegularFile file = view.getOrCreateRegularFile(checkedPath, opts, NO_ATTRS);
+ return new JimfsOutputStream(file, opts.contains(APPEND), view.state());
+ }
+
+ @Override
+ public DirectoryStream<Path> newDirectoryStream(
+ Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
+ JimfsPath checkedPath = checkPath(dir);
+ return getDefaultView(checkedPath)
+ .newDirectoryStream(checkedPath, filter, Options.FOLLOW_LINKS, checkedPath);
+ }
+
+ @Override
+ public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+ JimfsPath checkedPath = checkPath(dir);
+ FileSystemView view = getDefaultView(checkedPath);
+ view.createDirectory(checkedPath, attrs);
+ }
+
+ @Override
+ public void createLink(Path link, Path existing) throws IOException {
+ JimfsPath linkPath = checkPath(link);
+ JimfsPath existingPath = checkPath(existing);
+ checkArgument(
+ linkPath.getFileSystem().equals(existingPath.getFileSystem()),
+ "link and existing paths must belong to the same file system instance");
+ FileSystemView view = getDefaultView(linkPath);
+ view.link(linkPath, getDefaultView(existingPath), existingPath);
+ }
+
+ @Override
+ public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs)
+ throws IOException {
+ JimfsPath linkPath = checkPath(link);
+ JimfsPath targetPath = checkPath(target);
+ checkArgument(
+ linkPath.getFileSystem().equals(targetPath.getFileSystem()),
+ "link and target paths must belong to the same file system instance");
+ FileSystemView view = getDefaultView(linkPath);
+ view.createSymbolicLink(linkPath, targetPath, attrs);
+ }
+
+ @Override
+ public Path readSymbolicLink(Path link) throws IOException {
+ JimfsPath checkedPath = checkPath(link);
+ return getDefaultView(checkedPath).readSymbolicLink(checkedPath);
+ }
+
+ @Override
+ public void delete(Path path) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ FileSystemView view = getDefaultView(checkedPath);
+ view.deleteFile(checkedPath, FileSystemView.DeleteMode.ANY);
+ }
+
+ @Override
+ public void copy(Path source, Path target, CopyOption... options) throws IOException {
+ copy(source, target, Options.getCopyOptions(options), false);
+ }
+
+ private void copy(Path source, Path target, ImmutableSet<CopyOption> options, boolean move)
+ throws IOException {
+ JimfsPath sourcePath = checkPath(source);
+ JimfsPath targetPath = checkPath(target);
+
+ FileSystemView sourceView = getDefaultView(sourcePath);
+ FileSystemView targetView = getDefaultView(targetPath);
+ sourceView.copy(sourcePath, targetView, targetPath, options, move);
+ }
+
+ @Override
+ public void move(Path source, Path target, CopyOption... options) throws IOException {
+ copy(source, target, Options.getMoveOptions(options), true);
+ }
+
+ @Override
+ public boolean isSameFile(Path path, Path path2) throws IOException {
+ if (path.equals(path2)) {
+ return true;
+ }
+
+ if (!(path instanceof JimfsPath && path2 instanceof JimfsPath)) {
+ return false;
+ }
+
+ JimfsPath checkedPath = (JimfsPath) path;
+ JimfsPath checkedPath2 = (JimfsPath) path2;
+
+ FileSystemView view = getDefaultView(checkedPath);
+ FileSystemView view2 = getDefaultView(checkedPath2);
+
+ return view.isSameFile(checkedPath, view2, checkedPath2);
+ }
+
+ @Override
+ public boolean isHidden(Path path) throws IOException {
+ // TODO(cgdecker): This should probably be configurable, but this seems fine for now
+ /*
+ * If the DOS view is supported, use the Windows isHidden method (check the dos:hidden
+ * attribute). Otherwise, use the Unix isHidden method (just check if the file name starts with
+ * ".").
+ */
+ JimfsPath checkedPath = checkPath(path);
+ FileSystemView view = getDefaultView(checkedPath);
+ if (getFileStore(path).supportsFileAttributeView("dos")) {
+ return view.readAttributes(checkedPath, DosFileAttributes.class, Options.NOFOLLOW_LINKS)
+ .isHidden();
+ }
+ return path.getNameCount() > 0 && path.getFileName().toString().startsWith(".");
+ }
+
+ @Override
+ public FileStore getFileStore(Path path) throws IOException {
+ return getFileSystem(path).getFileStore();
+ }
+
+ @Override
+ public void checkAccess(Path path, AccessMode... modes) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ getDefaultView(checkedPath).checkAccess(checkedPath);
+ }
+
+ @NullableDecl
+ @Override
+ public <V extends FileAttributeView> V getFileAttributeView(
+ Path path, Class<V> type, LinkOption... options) {
+ JimfsPath checkedPath = checkPath(path);
+ return getDefaultView(checkedPath)
+ .getFileAttributeView(checkedPath, type, Options.getLinkOptions(options));
+ }
+
+ @Override
+ public <A extends BasicFileAttributes> A readAttributes(
+ Path path, Class<A> type, LinkOption... options) throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ return getDefaultView(checkedPath)
+ .readAttributes(checkedPath, type, Options.getLinkOptions(options));
+ }
+
+ @Override
+ public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options)
+ throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ return getDefaultView(checkedPath)
+ .readAttributes(checkedPath, attributes, Options.getLinkOptions(options));
+ }
+
+ @Override
+ public void setAttribute(Path path, String attribute, Object value, LinkOption... options)
+ throws IOException {
+ JimfsPath checkedPath = checkPath(path);
+ getDefaultView(checkedPath)
+ .setAttribute(checkedPath, attribute, value, Options.getLinkOptions(options));
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java
new file mode 100644
index 0000000..bd36c8f
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Initializes and configures new file system instances.
+ *
+ * @author Colin Decker
+ */
+final class JimfsFileSystems {
+
+ private JimfsFileSystems() {}
+
+ private static final Runnable DO_NOTHING =
+ new Runnable() {
+ @Override
+ public void run() {}
+ };
+
+ /**
+ * Returns a {@code Runnable} that will remove the file system with the given {@code URI} from the
+ * system provider's cache when called.
+ */
+ private static Runnable removeFileSystemRunnable(URI uri) {
+ if (Jimfs.systemProvider == null) {
+ // TODO(cgdecker): Use Runnables.doNothing() when it's out of @Beta
+ return DO_NOTHING;
+ }
+
+ // We have to invoke the SystemJimfsFileSystemProvider.removeFileSystemRunnable(URI)
+ // method reflectively since the system-loaded instance of it may be a different class
+ // than the one we'd get if we tried to cast it and call it like normal here.
+ try {
+ Method method =
+ Jimfs.systemProvider.getClass().getDeclaredMethod("removeFileSystemRunnable", URI.class);
+ return (Runnable) method.invoke(null, uri);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(
+ "Unable to get Runnable for removing the FileSystem from the cache when it is closed", e);
+ }
+ }
+
+ /**
+ * Initialize and configure a new file system with the given provider and URI, using the given
+ * configuration.
+ */
+ public static JimfsFileSystem newFileSystem(
+ JimfsFileSystemProvider provider, URI uri, Configuration config) throws IOException {
+ PathService pathService = new PathService(config);
+ FileSystemState state = new FileSystemState(removeFileSystemRunnable(uri));
+
+ JimfsFileStore fileStore = createFileStore(config, pathService, state);
+ FileSystemView defaultView = createDefaultView(config, fileStore, pathService);
+ WatchServiceConfiguration watchServiceConfig = config.watchServiceConfig;
+
+ JimfsFileSystem fileSystem =
+ new JimfsFileSystem(provider, uri, fileStore, pathService, defaultView, watchServiceConfig);
+
+ pathService.setFileSystem(fileSystem);
+ return fileSystem;
+ }
+
+ /** Creates the file store for the file system. */
+ private static JimfsFileStore createFileStore(
+ Configuration config, PathService pathService, FileSystemState state) {
+ AttributeService attributeService = new AttributeService(config);
+
+ HeapDisk disk = new HeapDisk(config);
+ FileFactory fileFactory = new FileFactory(disk);
+
+ Map<Name, Directory> roots = new HashMap<>();
+
+ // create roots
+ for (String root : config.roots) {
+ JimfsPath path = pathService.parsePath(root);
+ if (!path.isAbsolute() && path.getNameCount() == 0) {
+ throw new IllegalArgumentException("Invalid root path: " + root);
+ }
+
+ Name rootName = path.root();
+
+ Directory rootDir = fileFactory.createRootDirectory(rootName);
+ attributeService.setInitialAttributes(rootDir);
+ roots.put(rootName, rootDir);
+ }
+
+ return new JimfsFileStore(
+ new FileTree(roots), fileFactory, disk, attributeService, config.supportedFeatures, state);
+ }
+
+ /** Creates the default view of the file system using the given working directory. */
+ private static FileSystemView createDefaultView(
+ Configuration config, JimfsFileStore fileStore, PathService pathService) throws IOException {
+ JimfsPath workingDirPath = pathService.parsePath(config.workingDirectory);
+
+ Directory dir = fileStore.getRoot(workingDirPath.root());
+ if (dir == null) {
+ throw new IllegalArgumentException("Invalid working dir path: " + workingDirPath);
+ }
+
+ for (Name name : workingDirPath.names()) {
+ Directory newDir = fileStore.directoryCreator().get();
+ fileStore.setInitialAttributes(newDir);
+ dir.link(name, newDir);
+
+ dir = newDir;
+ }
+
+ return new FileSystemView(fileStore, dir, workingDirPath);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsInputStream.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsInputStream.java
new file mode 100644
index 0000000..750530c
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsInputStream.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkPositionIndexes;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.Ints;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * {@link InputStream} for reading from a file's {@link RegularFile}.
+ *
+ * @author Colin Decker
+ */
+final class JimfsInputStream extends InputStream {
+
+ @GuardedBy("this")
+ @VisibleForTesting
+ RegularFile file;
+
+ @GuardedBy("this")
+ private long pos;
+
+ @GuardedBy("this")
+ private boolean finished;
+
+ private final FileSystemState fileSystemState;
+
+ public JimfsInputStream(RegularFile file, FileSystemState fileSystemState) {
+ this.file = checkNotNull(file);
+ this.fileSystemState = fileSystemState;
+ fileSystemState.register(this);
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ checkNotClosed();
+ if (finished) {
+ return -1;
+ }
+
+ file.readLock().lock();
+ try {
+
+ int b = file.read(pos++); // it's ok for pos to go beyond size()
+ if (b == -1) {
+ finished = true;
+ } else {
+ file.updateAccessTime();
+ }
+ return b;
+ } finally {
+ file.readLock().unlock();
+ }
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return readInternal(b, 0, b.length);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ checkPositionIndexes(off, off + len, b.length);
+ return readInternal(b, off, len);
+ }
+
+ private synchronized int readInternal(byte[] b, int off, int len) throws IOException {
+ checkNotClosed();
+ if (finished) {
+ return -1;
+ }
+
+ file.readLock().lock();
+ try {
+ int read = file.read(pos, b, off, len);
+ if (read == -1) {
+ finished = true;
+ } else {
+ pos += read;
+ }
+
+ file.updateAccessTime();
+ return read;
+ } finally {
+ file.readLock().unlock();
+ }
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (n <= 0) {
+ return 0;
+ }
+
+ synchronized (this) {
+ checkNotClosed();
+ if (finished) {
+ return 0;
+ }
+
+ // available() must be an int, so the min must be also
+ int skip = (int) Math.min(Math.max(file.size() - pos, 0), n);
+ pos += skip;
+ return skip;
+ }
+ }
+
+ @Override
+ public synchronized int available() throws IOException {
+ checkNotClosed();
+ if (finished) {
+ return 0;
+ }
+ long available = Math.max(file.size() - pos, 0);
+ return Ints.saturatedCast(available);
+ }
+
+ @GuardedBy("this")
+ private void checkNotClosed() throws IOException {
+ if (file == null) {
+ throw new IOException("stream is closed");
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (isOpen()) {
+ fileSystemState.unregister(this);
+ file.closed();
+
+ // file is set to null here and only here
+ file = null;
+ }
+ }
+
+ @GuardedBy("this")
+ private boolean isOpen() {
+ return file != null;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsOutputStream.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsOutputStream.java
new file mode 100644
index 0000000..0b88046
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsOutputStream.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkPositionIndexes;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * {@link OutputStream} for writing to a {@link RegularFile}.
+ *
+ * @author Colin Decker
+ */
+final class JimfsOutputStream extends OutputStream {
+
+ @GuardedBy("this")
+ @VisibleForTesting
+ RegularFile file;
+
+ @GuardedBy("this")
+ private long pos;
+
+ private final boolean append;
+ private final FileSystemState fileSystemState;
+
+ JimfsOutputStream(RegularFile file, boolean append, FileSystemState fileSystemState) {
+ this.file = checkNotNull(file);
+ this.append = append;
+ this.fileSystemState = fileSystemState;
+ fileSystemState.register(this);
+ }
+
+ @Override
+ public synchronized void write(int b) throws IOException {
+ checkNotClosed();
+
+ file.writeLock().lock();
+ try {
+ if (append) {
+ pos = file.sizeWithoutLocking();
+ }
+ file.write(pos++, (byte) b);
+
+ file.updateModifiedTime();
+ } finally {
+ file.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ writeInternal(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ checkPositionIndexes(off, off + len, b.length);
+ writeInternal(b, off, len);
+ }
+
+ private synchronized void writeInternal(byte[] b, int off, int len) throws IOException {
+ checkNotClosed();
+
+ file.writeLock().lock();
+ try {
+ if (append) {
+ pos = file.sizeWithoutLocking();
+ }
+ pos += file.write(pos, b, off, len);
+
+ file.updateModifiedTime();
+ } finally {
+ file.writeLock().unlock();
+ }
+ }
+
+ @GuardedBy("this")
+ private void checkNotClosed() throws IOException {
+ if (file == null) {
+ throw new IOException("stream is closed");
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (isOpen()) {
+ fileSystemState.unregister(this);
+ file.closed();
+
+ // file is set to null here and only here
+ file = null;
+ }
+ }
+
+ @GuardedBy("this")
+ private boolean isOpen() {
+ return file != null;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsPath.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsPath.java
new file mode 100644
index 0000000..7c6b115
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsPath.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.AbstractList;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Jimfs implementation of {@link Path}. Creation of new {@code Path} objects is delegated to the
+ * file system's {@link PathService}.
+ *
+ * @author Colin Decker
+ */
+final class JimfsPath implements Path {
+
+ @NullableDecl private final Name root;
+ private final ImmutableList<Name> names;
+ private final PathService pathService;
+
+ public JimfsPath(PathService pathService, @NullableDecl Name root, Iterable<Name> names) {
+ this.pathService = checkNotNull(pathService);
+ this.root = root;
+ this.names = ImmutableList.copyOf(names);
+ }
+
+ /** Returns the root name, or null if there is no root. */
+ @NullableDecl
+ public Name root() {
+ return root;
+ }
+
+ /** Returns the list of name elements. */
+ public ImmutableList<Name> names() {
+ return names;
+ }
+
+ /**
+ * Returns the file name of this path. Unlike {@link #getFileName()}, this may return the name of
+ * the root if this is a root path.
+ */
+ @NullableDecl
+ public Name name() {
+ if (!names.isEmpty()) {
+ return Iterables.getLast(names);
+ }
+ return root;
+ }
+
+ /**
+ * Returns whether or not this is the empty path, with no root and a single, empty string, name.
+ */
+ public boolean isEmptyPath() {
+ return root == null && names.size() == 1 && names.get(0).toString().isEmpty();
+ }
+
+ @Override
+ public FileSystem getFileSystem() {
+ return pathService.getFileSystem();
+ }
+
+ /**
+ * Equivalent to {@link #getFileSystem()} but with a return type of {@code JimfsFileSystem}.
+ * {@code getFileSystem()}'s return type is left as {@code FileSystem} to make testing paths
+ * easier (as long as methods that access the file system in some way are not called, the file
+ * system can be a fake file system instance).
+ */
+ public JimfsFileSystem getJimfsFileSystem() {
+ return (JimfsFileSystem) pathService.getFileSystem();
+ }
+
+ @Override
+ public boolean isAbsolute() {
+ return root != null;
+ }
+
+ @Override
+ public JimfsPath getRoot() {
+ if (root == null) {
+ return null;
+ }
+ return pathService.createRoot(root);
+ }
+
+ @Override
+ public JimfsPath getFileName() {
+ return names.isEmpty() ? null : getName(names.size() - 1);
+ }
+
+ @Override
+ public JimfsPath getParent() {
+ if (names.isEmpty() || (names.size() == 1 && root == null)) {
+ return null;
+ }
+
+ return pathService.createPath(root, names.subList(0, names.size() - 1));
+ }
+
+ @Override
+ public int getNameCount() {
+ return names.size();
+ }
+
+ @Override
+ public JimfsPath getName(int index) {
+ checkArgument(
+ index >= 0 && index < names.size(),
+ "index (%s) must be >= 0 and < name count (%s)",
+ index,
+ names.size());
+ return pathService.createFileName(names.get(index));
+ }
+
+ @Override
+ public JimfsPath subpath(int beginIndex, int endIndex) {
+ checkArgument(
+ beginIndex >= 0 && endIndex <= names.size() && endIndex > beginIndex,
+ "beginIndex (%s) must be >= 0; endIndex (%s) must be <= name count (%s) and > beginIndex",
+ beginIndex,
+ endIndex,
+ names.size());
+ return pathService.createRelativePath(names.subList(beginIndex, endIndex));
+ }
+
+ /** Returns true if list starts with all elements of other in the same order. */
+ private static boolean startsWith(List<?> list, List<?> other) {
+ return list.size() >= other.size() && list.subList(0, other.size()).equals(other);
+ }
+
+ @Override
+ public boolean startsWith(Path other) {
+ JimfsPath otherPath = checkPath(other);
+ return otherPath != null
+ && getFileSystem().equals(otherPath.getFileSystem())
+ && Objects.equals(root, otherPath.root)
+ && startsWith(names, otherPath.names);
+ }
+
+ @Override
+ public boolean startsWith(String other) {
+ return startsWith(pathService.parsePath(other));
+ }
+
+ @Override
+ public boolean endsWith(Path other) {
+ JimfsPath otherPath = checkPath(other);
+ if (otherPath == null) {
+ return false;
+ }
+
+ if (otherPath.isAbsolute()) {
+ return compareTo(otherPath) == 0;
+ }
+ return startsWith(names.reverse(), otherPath.names.reverse());
+ }
+
+ @Override
+ public boolean endsWith(String other) {
+ return endsWith(pathService.parsePath(other));
+ }
+
+ @Override
+ public JimfsPath normalize() {
+ if (isNormal()) {
+ return this;
+ }
+
+ Deque<Name> newNames = new ArrayDeque<>();
+ for (Name name : names) {
+ if (name.equals(Name.PARENT)) {
+ Name lastName = newNames.peekLast();
+ if (lastName != null && !lastName.equals(Name.PARENT)) {
+ newNames.removeLast();
+ } else if (!isAbsolute()) {
+ // if there's a root and we have an extra ".." that would go up above the root, ignore it
+ newNames.add(name);
+ }
+ } else if (!name.equals(Name.SELF)) {
+ newNames.add(name);
+ }
+ }
+
+ return Iterables.elementsEqual(newNames, names) ? this : pathService.createPath(root, newNames);
+ }
+
+ /**
+ * Returns whether or not this path is in a normalized form. It's normal if it both contains no
+ * "." names and contains no ".." names in a location other than the start of the path.
+ */
+ private boolean isNormal() {
+ if (getNameCount() == 0 || (getNameCount() == 1 && !isAbsolute())) {
+ return true;
+ }
+
+ boolean foundNonParentName = isAbsolute(); // if there's a root, the path doesn't start with ..
+ boolean normal = true;
+ for (Name name : names) {
+ if (name.equals(Name.PARENT)) {
+ if (foundNonParentName) {
+ normal = false;
+ break;
+ }
+ } else {
+ if (name.equals(Name.SELF)) {
+ normal = false;
+ break;
+ }
+
+ foundNonParentName = true;
+ }
+ }
+ return normal;
+ }
+
+ /** Resolves the given name against this path. The name is assumed not to be a root name. */
+ JimfsPath resolve(Name name) {
+ if (name.toString().isEmpty()) {
+ return this;
+ }
+ return pathService.createPathInternal(
+ root, ImmutableList.<Name>builder().addAll(names).add(name).build());
+ }
+
+ @Override
+ public JimfsPath resolve(Path other) {
+ JimfsPath otherPath = checkPath(other);
+ if (otherPath == null) {
+ throw new ProviderMismatchException(other.toString());
+ }
+
+ if (isEmptyPath() || otherPath.isAbsolute()) {
+ return otherPath;
+ }
+ if (otherPath.isEmptyPath()) {
+ return this;
+ }
+ return pathService.createPath(
+ root, ImmutableList.<Name>builder().addAll(names).addAll(otherPath.names).build());
+ }
+
+ @Override
+ public JimfsPath resolve(String other) {
+ return resolve(pathService.parsePath(other));
+ }
+
+ @Override
+ public JimfsPath resolveSibling(Path other) {
+ JimfsPath otherPath = checkPath(other);
+ if (otherPath == null) {
+ throw new ProviderMismatchException(other.toString());
+ }
+
+ if (otherPath.isAbsolute()) {
+ return otherPath;
+ }
+ JimfsPath parent = getParent();
+ if (parent == null) {
+ return otherPath;
+ }
+ return parent.resolve(other);
+ }
+
+ @Override
+ public JimfsPath resolveSibling(String other) {
+ return resolveSibling(pathService.parsePath(other));
+ }
+
+ @Override
+ public JimfsPath relativize(Path other) {
+ JimfsPath otherPath = checkPath(other);
+ if (otherPath == null) {
+ throw new ProviderMismatchException(other.toString());
+ }
+
+ checkArgument(
+ Objects.equals(root, otherPath.root), "Paths have different roots: %s, %s", this, other);
+
+ if (equals(other)) {
+ return pathService.emptyPath();
+ }
+
+ if (isEmptyPath()) {
+ return otherPath;
+ }
+
+ ImmutableList<Name> otherNames = otherPath.names;
+ int sharedSubsequenceLength = 0;
+ for (int i = 0; i < Math.min(getNameCount(), otherNames.size()); i++) {
+ if (names.get(i).equals(otherNames.get(i))) {
+ sharedSubsequenceLength++;
+ } else {
+ break;
+ }
+ }
+
+ int extraNamesInThis = Math.max(0, getNameCount() - sharedSubsequenceLength);
+
+ ImmutableList<Name> extraNamesInOther =
+ (otherNames.size() <= sharedSubsequenceLength)
+ ? ImmutableList.<Name>of()
+ : otherNames.subList(sharedSubsequenceLength, otherNames.size());
+
+ List<Name> parts = new ArrayList<>(extraNamesInThis + extraNamesInOther.size());
+
+ // add .. for each extra name in this path
+ parts.addAll(Collections.nCopies(extraNamesInThis, Name.PARENT));
+ // add each extra name in the other path
+ parts.addAll(extraNamesInOther);
+
+ return pathService.createRelativePath(parts);
+ }
+
+ @Override
+ public JimfsPath toAbsolutePath() {
+ return isAbsolute() ? this : getJimfsFileSystem().getWorkingDirectory().resolve(this);
+ }
+
+ @Override
+ public JimfsPath toRealPath(LinkOption... options) throws IOException {
+ return getJimfsFileSystem()
+ .getDefaultView()
+ .toRealPath(this, pathService, Options.getLinkOptions(options));
+ }
+
+ @Override
+ public WatchKey register(
+ WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
+ throws IOException {
+ checkNotNull(modifiers);
+ return register(watcher, events);
+ }
+
+ @Override
+ public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException {
+ checkNotNull(watcher);
+ checkNotNull(events);
+ if (!(watcher instanceof AbstractWatchService)) {
+ throw new IllegalArgumentException(
+ "watcher (" + watcher + ") is not associated with this file system");
+ }
+
+ AbstractWatchService service = (AbstractWatchService) watcher;
+ return service.register(this, Arrays.asList(events));
+ }
+
+ @Override
+ public URI toUri() {
+ return getJimfsFileSystem().toUri(this);
+ }
+
+ @Override
+ public File toFile() {
+ // documented as unsupported for anything but the default file system
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Iterator<Path> iterator() {
+ return asList().iterator();
+ }
+
+ private List<Path> asList() {
+ return new AbstractList<Path>() {
+ @Override
+ public Path get(int index) {
+ return getName(index);
+ }
+
+ @Override
+ public int size() {
+ return getNameCount();
+ }
+ };
+ }
+
+ @Override
+ public int compareTo(Path other) {
+ // documented to throw CCE if other is associated with a different FileSystemProvider
+ JimfsPath otherPath = (JimfsPath) other;
+ return ComparisonChain.start()
+ .compare(getJimfsFileSystem().getUri(), ((JimfsPath) other).getJimfsFileSystem().getUri())
+ .compare(this, otherPath, pathService)
+ .result();
+ }
+
+ @Override
+ public boolean equals(@NullableDecl Object obj) {
+ return obj instanceof JimfsPath && compareTo((JimfsPath) obj) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return pathService.hash(this);
+ }
+
+ @Override
+ public String toString() {
+ return pathService.toString(this);
+ }
+
+ @NullableDecl
+ private JimfsPath checkPath(Path other) {
+ if (checkNotNull(other) instanceof JimfsPath && other.getFileSystem().equals(getFileSystem())) {
+ return (JimfsPath) other;
+ }
+ return null;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsSecureDirectoryStream.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsSecureDirectoryStream.java
new file mode 100644
index 0000000..e3391b6
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsSecureDirectoryStream.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.ClosedDirectoryStreamException;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.ProviderMismatchException;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.Iterator;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Secure directory stream implementation that uses a {@link FileSystemView} with the stream's
+ * directory as its working directory.
+ *
+ * @author Colin Decker
+ */
+final class JimfsSecureDirectoryStream implements SecureDirectoryStream<Path> {
+
+ private final FileSystemView view;
+ private final Filter<? super Path> filter;
+ private final FileSystemState fileSystemState;
+
+ private boolean open = true;
+ private Iterator<Path> iterator = new DirectoryIterator();
+
+ public JimfsSecureDirectoryStream(
+ FileSystemView view, Filter<? super Path> filter, FileSystemState fileSystemState) {
+ this.view = checkNotNull(view);
+ this.filter = checkNotNull(filter);
+ this.fileSystemState = fileSystemState;
+ fileSystemState.register(this);
+ }
+
+ private JimfsPath path() {
+ return view.getWorkingDirectoryPath();
+ }
+
+ @Override
+ public synchronized Iterator<Path> iterator() {
+ checkOpen();
+ Iterator<Path> result = iterator;
+ checkState(result != null, "iterator() has already been called once");
+ iterator = null;
+ return result;
+ }
+
+ @Override
+ public synchronized void close() {
+ open = false;
+ fileSystemState.unregister(this);
+ }
+
+ protected synchronized void checkOpen() {
+ if (!open) {
+ throw new ClosedDirectoryStreamException();
+ }
+ }
+
+ private final class DirectoryIterator extends AbstractIterator<Path> {
+
+ @NullableDecl private Iterator<Name> fileNames;
+
+ @Override
+ protected synchronized Path computeNext() {
+ checkOpen();
+
+ try {
+ if (fileNames == null) {
+ fileNames = view.snapshotWorkingDirectoryEntries().iterator();
+ }
+
+ while (fileNames.hasNext()) {
+ Name name = fileNames.next();
+ Path path = view.getWorkingDirectoryPath().resolve(name);
+
+ if (filter.accept(path)) {
+ return path;
+ }
+ }
+
+ return endOfData();
+ } catch (IOException e) {
+ throw new DirectoryIteratorException(e);
+ }
+ }
+ }
+
+ /** A stream filter that always returns true. */
+ public static final Filter<Object> ALWAYS_TRUE_FILTER =
+ new Filter<Object>() {
+ @Override
+ public boolean accept(Object entry) throws IOException {
+ return true;
+ }
+ };
+
+ @Override
+ public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options)
+ throws IOException {
+ checkOpen();
+ JimfsPath checkedPath = checkPath(path);
+
+ // safe cast because a file system that supports SecureDirectoryStream always creates
+ // SecureDirectoryStreams
+ return (SecureDirectoryStream<Path>)
+ view.newDirectoryStream(
+ checkedPath,
+ ALWAYS_TRUE_FILTER,
+ Options.getLinkOptions(options),
+ path().resolve(checkedPath));
+ }
+
+ @Override
+ public SeekableByteChannel newByteChannel(
+ Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ checkOpen();
+ JimfsPath checkedPath = checkPath(path);
+ ImmutableSet<OpenOption> opts = Options.getOptionsForChannel(options);
+ return new JimfsFileChannel(
+ view.getOrCreateRegularFile(checkedPath, opts), opts, fileSystemState);
+ }
+
+ @Override
+ public void deleteFile(Path path) throws IOException {
+ checkOpen();
+ JimfsPath checkedPath = checkPath(path);
+ view.deleteFile(checkedPath, FileSystemView.DeleteMode.NON_DIRECTORY_ONLY);
+ }
+
+ @Override
+ public void deleteDirectory(Path path) throws IOException {
+ checkOpen();
+ JimfsPath checkedPath = checkPath(path);
+ view.deleteFile(checkedPath, FileSystemView.DeleteMode.DIRECTORY_ONLY);
+ }
+
+ @Override
+ public void move(Path srcPath, SecureDirectoryStream<Path> targetDir, Path targetPath)
+ throws IOException {
+ checkOpen();
+ JimfsPath checkedSrcPath = checkPath(srcPath);
+ JimfsPath checkedTargetPath = checkPath(targetPath);
+
+ if (!(targetDir instanceof JimfsSecureDirectoryStream)) {
+ throw new ProviderMismatchException(
+ "targetDir isn't a secure directory stream associated with this file system");
+ }
+
+ JimfsSecureDirectoryStream checkedTargetDir = (JimfsSecureDirectoryStream) targetDir;
+
+ view.copy(
+ checkedSrcPath,
+ checkedTargetDir.view,
+ checkedTargetPath,
+ ImmutableSet.<CopyOption>of(),
+ true);
+ }
+
+ @Override
+ public <V extends FileAttributeView> V getFileAttributeView(Class<V> type) {
+ return getFileAttributeView(path().getFileSystem().getPath("."), type);
+ }
+
+ @Override
+ public <V extends FileAttributeView> V getFileAttributeView(
+ Path path, Class<V> type, LinkOption... options) {
+ checkOpen();
+ final JimfsPath checkedPath = checkPath(path);
+ final ImmutableSet<LinkOption> optionsSet = Options.getLinkOptions(options);
+ return view.getFileAttributeView(
+ new FileLookup() {
+ @Override
+ public File lookup() throws IOException {
+ checkOpen(); // per the spec, must check that the stream is open for each view operation
+ return view.lookUpWithLock(checkedPath, optionsSet).requireExists(checkedPath).file();
+ }
+ },
+ type);
+ }
+
+ private static JimfsPath checkPath(Path path) {
+ if (path instanceof JimfsPath) {
+ return (JimfsPath) path;
+ }
+ throw new ProviderMismatchException(
+ "path " + path + " is not associated with a Jimfs file system");
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Name.java b/jimfs/src/main/java/com/google/common/jimfs/Name.java
new file mode 100644
index 0000000..327be75
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Name.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.Ordering;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Immutable representation of a file name. Used both for the name components of paths and as the
+ * keys for directory entries.
+ *
+ * <p>A name has both a display string (used in the {@code toString()} form of a {@code Path} as
+ * well as for {@code Path} equality and sort ordering) and a canonical string, which is used for
+ * determining equality of the name during file lookup.
+ *
+ * <p>Note: all factory methods return a constant name instance when given the original string "."
+ * or "..", ensuring that those names can be accessed statically elsewhere in the code while still
+ * being equal to any names created for those values, regardless of normalization settings.
+ *
+ * @author Colin Decker
+ */
+final class Name {
+
+ /** The empty name. */
+ static final Name EMPTY = new Name("", "");
+
+ /** The name to use for a link from a directory to itself. */
+ public static final Name SELF = new Name(".", ".");
+
+ /** The name to use for a link from a directory to its parent directory. */
+ public static final Name PARENT = new Name("..", "..");
+
+ /** Creates a new name with no normalization done on the given string. */
+ @VisibleForTesting
+ static Name simple(String name) {
+ switch (name) {
+ case ".":
+ return SELF;
+ case "..":
+ return PARENT;
+ default:
+ return new Name(name, name);
+ }
+ }
+
+ /**
+ * Creates a name with the given display representation and the given canonical representation.
+ */
+ public static Name create(String display, String canonical) {
+ return new Name(display, canonical);
+ }
+
+ private final String display;
+ private final String canonical;
+
+ private Name(String display, String canonical) {
+ this.display = checkNotNull(display);
+ this.canonical = checkNotNull(canonical);
+ }
+
+ @Override
+ public boolean equals(@NullableDecl Object obj) {
+ if (obj instanceof Name) {
+ Name other = (Name) obj;
+ return canonical.equals(other.canonical);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Util.smearHash(canonical.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ return display;
+ }
+
+ /** Returns an ordering that orders names by their display representation. */
+ public static Ordering<Name> displayOrdering() {
+ return DISPLAY_ORDERING;
+ }
+
+ /** Returns an ordering that orders names by their canonical representation. */
+ public static Ordering<Name> canonicalOrdering() {
+ return CANONICAL_ORDERING;
+ }
+
+ private static final Ordering<Name> DISPLAY_ORDERING =
+ Ordering.natural()
+ .onResultOf(
+ new Function<Name, String>() {
+ @Override
+ public String apply(Name name) {
+ return name.display;
+ }
+ });
+
+ private static final Ordering<Name> CANONICAL_ORDERING =
+ Ordering.natural()
+ .onResultOf(
+ new Function<Name, String>() {
+ @Override
+ public String apply(Name name) {
+ return name.canonical;
+ }
+ });
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Options.java b/jimfs/src/main/java/com/google/common/jimfs/Options.java
new file mode 100644
index 0000000..a575b88
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Options.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
+import static java.nio.file.StandardOpenOption.APPEND;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import java.nio.file.CopyOption;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Utility methods for normalizing user-provided options arrays and sets to canonical immutable sets
+ * of options.
+ *
+ * @author Colin Decker
+ */
+final class Options {
+
+ private Options() {}
+
+ /** Immutable set containing LinkOption.NOFOLLOW_LINKS. */
+ public static final ImmutableSet<LinkOption> NOFOLLOW_LINKS =
+ ImmutableSet.of(LinkOption.NOFOLLOW_LINKS);
+
+ /** Immutable empty LinkOption set. */
+ public static final ImmutableSet<LinkOption> FOLLOW_LINKS = ImmutableSet.of();
+
+ private static final ImmutableSet<OpenOption> DEFAULT_READ = ImmutableSet.<OpenOption>of(READ);
+
+ private static final ImmutableSet<OpenOption> DEFAULT_READ_NOFOLLOW_LINKS =
+ ImmutableSet.<OpenOption>of(READ, LinkOption.NOFOLLOW_LINKS);
+
+ private static final ImmutableSet<OpenOption> DEFAULT_WRITE =
+ ImmutableSet.<OpenOption>of(WRITE, CREATE, TRUNCATE_EXISTING);
+
+ /** Returns an immutable set of link options. */
+ public static ImmutableSet<LinkOption> getLinkOptions(LinkOption... options) {
+ return options.length == 0 ? FOLLOW_LINKS : NOFOLLOW_LINKS;
+ }
+
+ /** Returns an immutable set of open options for opening a new file channel. */
+ public static ImmutableSet<OpenOption> getOptionsForChannel(Set<? extends OpenOption> options) {
+ if (options.isEmpty()) {
+ return DEFAULT_READ;
+ }
+
+ boolean append = options.contains(APPEND);
+ boolean write = append || options.contains(WRITE);
+ boolean read = !write || options.contains(READ);
+
+ if (read) {
+ if (append) {
+ throw new UnsupportedOperationException("'READ' + 'APPEND' not allowed");
+ }
+
+ if (!write) {
+ // ignore all write related options
+ return options.contains(LinkOption.NOFOLLOW_LINKS)
+ ? DEFAULT_READ_NOFOLLOW_LINKS
+ : DEFAULT_READ;
+ }
+ }
+
+ // options contains write or append and may also contain read
+ // it does not contain both read and append
+ return addWrite(options);
+ }
+
+ /** Returns an immutable set of open options for opening a new input stream. */
+ @SuppressWarnings("unchecked") // safe covariant cast
+ public static ImmutableSet<OpenOption> getOptionsForInputStream(OpenOption... options) {
+ boolean nofollowLinks = false;
+ for (OpenOption option : options) {
+ if (checkNotNull(option) != READ) {
+ if (option == LinkOption.NOFOLLOW_LINKS) {
+ nofollowLinks = true;
+ } else {
+ throw new UnsupportedOperationException("'" + option + "' not allowed");
+ }
+ }
+ }
+
+ // just return the link options for finding the file, nothing else is needed
+ return (ImmutableSet<OpenOption>)
+ (ImmutableSet<?>) (nofollowLinks ? NOFOLLOW_LINKS : FOLLOW_LINKS);
+ }
+
+ /** Returns an immutable set of open options for opening a new output stream. */
+ public static ImmutableSet<OpenOption> getOptionsForOutputStream(OpenOption... options) {
+ if (options.length == 0) {
+ return DEFAULT_WRITE;
+ }
+
+ ImmutableSet<OpenOption> result = addWrite(Arrays.asList(options));
+ if (result.contains(READ)) {
+ throw new UnsupportedOperationException("'READ' not allowed");
+ }
+ return result;
+ }
+
+ /**
+ * Returns an {@link ImmutableSet} copy of the given {@code options}, adding {@link
+ * StandardOpenOption#WRITE} if it isn't already present.
+ */
+ private static ImmutableSet<OpenOption> addWrite(Collection<? extends OpenOption> options) {
+ return options.contains(WRITE)
+ ? ImmutableSet.copyOf(options)
+ : ImmutableSet.<OpenOption>builder().add(WRITE).addAll(options).build();
+ }
+
+ /** Returns an immutable set of the given options for a move. */
+ public static ImmutableSet<CopyOption> getMoveOptions(CopyOption... options) {
+ return ImmutableSet.copyOf(Lists.asList(LinkOption.NOFOLLOW_LINKS, options));
+ }
+
+ /** Returns an immutable set of the given options for a copy. */
+ public static ImmutableSet<CopyOption> getCopyOptions(CopyOption... options) {
+ ImmutableSet<CopyOption> result = ImmutableSet.copyOf(options);
+ if (result.contains(ATOMIC_MOVE)) {
+ throw new UnsupportedOperationException("'ATOMIC_MOVE' not allowed");
+ }
+ return result;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/OwnerAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/OwnerAttributeProvider.java
new file mode 100644
index 0000000..3408639
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/OwnerAttributeProvider.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.jimfs.UserLookupService.createUserPrincipal;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Attribute provider that provides the {@link FileOwnerAttributeView} ("owner").
+ *
+ * @author Colin Decker
+ */
+final class OwnerAttributeProvider extends AttributeProvider {
+
+ private static final ImmutableSet<String> ATTRIBUTES = ImmutableSet.of("owner");
+
+ private static final UserPrincipal DEFAULT_OWNER = createUserPrincipal("user");
+
+ @Override
+ public String name() {
+ return "owner";
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ return ATTRIBUTES;
+ }
+
+ @Override
+ public ImmutableMap<String, ?> defaultValues(Map<String, ?> userProvidedDefaults) {
+ Object userProvidedOwner = userProvidedDefaults.get("owner:owner");
+
+ UserPrincipal owner = DEFAULT_OWNER;
+ if (userProvidedOwner != null) {
+ if (userProvidedOwner instanceof String) {
+ owner = createUserPrincipal((String) userProvidedOwner);
+ } else {
+ throw invalidType("owner", "owner", userProvidedOwner, String.class, UserPrincipal.class);
+ }
+ }
+
+ return ImmutableMap.of("owner:owner", owner);
+ }
+
+ @NullableDecl
+ @Override
+ public Object get(File file, String attribute) {
+ if (attribute.equals("owner")) {
+ return file.getAttribute("owner", "owner");
+ }
+ return null;
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ if (attribute.equals("owner")) {
+ checkNotCreate(view, attribute, create);
+ UserPrincipal user = checkType(view, attribute, value, UserPrincipal.class);
+ // TODO(cgdecker): Do we really need to do this? Any reason not to allow any UserPrincipal?
+ if (!(user instanceof UserLookupService.JimfsUserPrincipal)) {
+ user = createUserPrincipal(user.getName());
+ }
+ file.setAttribute("owner", "owner", user);
+ }
+ }
+
+ @Override
+ public Class<FileOwnerAttributeView> viewType() {
+ return FileOwnerAttributeView.class;
+ }
+
+ @Override
+ public FileOwnerAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ return new View(lookup);
+ }
+
+ /** Implementation of {@link FileOwnerAttributeView}. */
+ private static final class View extends AbstractAttributeView implements FileOwnerAttributeView {
+
+ public View(FileLookup lookup) {
+ super(lookup);
+ }
+
+ @Override
+ public String name() {
+ return "owner";
+ }
+
+ @Override
+ public UserPrincipal getOwner() throws IOException {
+ return (UserPrincipal) lookupFile().getAttribute("owner", "owner");
+ }
+
+ @Override
+ public void setOwner(UserPrincipal owner) throws IOException {
+ lookupFile().setAttribute("owner", "owner", checkNotNull(owner));
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PathMatchers.java b/jimfs/src/main/java/com/google/common/jimfs/PathMatchers.java
new file mode 100644
index 0000000..38ba45a
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PathMatchers.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Ascii;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@link PathMatcher} factory for any file system.
+ *
+ * @author Colin Decker
+ */
+final class PathMatchers {
+
+ private PathMatchers() {}
+
+ /**
+ * Gets a {@link PathMatcher} for the given syntax and pattern as specified by {@link
+ * FileSystem#getPathMatcher}. The {@code separators} string contains the path name element
+ * separators (one character each) recognized by the file system. For a glob-syntax path matcher,
+ * any of the given separators will be recognized as a separator in the pattern, and any of them
+ * will be matched as a separator when checking a path.
+ */
+ // TODO(cgdecker): Should I be just canonicalizing separators rather than matching any separator?
+ // Perhaps so, assuming Path always canonicalizes its separators
+ public static PathMatcher getPathMatcher(
+ String syntaxAndPattern, String separators, ImmutableSet<PathNormalization> normalizations) {
+ int syntaxSeparator = syntaxAndPattern.indexOf(':');
+ checkArgument(
+ syntaxSeparator > 0, "Must be of the form 'syntax:pattern': %s", syntaxAndPattern);
+
+ String syntax = Ascii.toLowerCase(syntaxAndPattern.substring(0, syntaxSeparator));
+ String pattern = syntaxAndPattern.substring(syntaxSeparator + 1);
+
+ switch (syntax) {
+ case "glob":
+ pattern = GlobToRegex.toRegex(pattern, separators);
+ // fall through
+ case "regex":
+ return fromRegex(pattern, normalizations);
+ default:
+ throw new UnsupportedOperationException("Invalid syntax: " + syntaxAndPattern);
+ }
+ }
+
+ private static PathMatcher fromRegex(String regex, Iterable<PathNormalization> normalizations) {
+ return new RegexPathMatcher(PathNormalization.compilePattern(regex, normalizations));
+ }
+
+ /**
+ * {@code PathMatcher} that matches the {@code toString()} form of a {@code Path} against a regex
+ * {@code Pattern}.
+ */
+ @VisibleForTesting
+ static final class RegexPathMatcher implements PathMatcher {
+
+ private final Pattern pattern;
+
+ private RegexPathMatcher(Pattern pattern) {
+ this.pattern = checkNotNull(pattern);
+ }
+
+ @Override
+ public boolean matches(Path path) {
+ return pattern.matcher(path.toString()).matches();
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).addValue(pattern).toString();
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PathNormalization.java b/jimfs/src/main/java/com/google/common/jimfs/PathNormalization.java
new file mode 100644
index 0000000..40fd398
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PathNormalization.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import com.google.common.base.Ascii;
+import com.google.common.base.Function;
+import com.ibm.icu.lang.UCharacter;
+import java.text.Normalizer;
+import java.util.regex.Pattern;
+
+/**
+ * Normalizations that can be applied to names in paths. Includes Unicode normalizations and
+ * normalizations for case insensitive paths. These normalizations can be set in {@code
+ * Configuration.Builder} when creating a Jimfs file system instance and are automatically applied
+ * to paths in the file system.
+ *
+ * @author Colin Decker
+ */
+public enum PathNormalization implements Function<String, String> {
+
+ /** No normalization. */
+ NONE(0) {
+ @Override
+ public String apply(String string) {
+ return string;
+ }
+ },
+
+ /** Unicode composed normalization (form {@linkplain java.text.Normalizer.Form#NFC NFC}). */
+ NFC(Pattern.CANON_EQ) {
+ @Override
+ public String apply(String string) {
+ return Normalizer.normalize(string, Normalizer.Form.NFC);
+ }
+ },
+
+ /** Unicode decomposed normalization (form {@linkplain java.text.Normalizer.Form#NFD NFD}). */
+ NFD(Pattern.CANON_EQ) {
+ @Override
+ public String apply(String string) {
+ return Normalizer.normalize(string, Normalizer.Form.NFD);
+ }
+ },
+
+ /*
+ * Some notes on case folding/case insensitivity of file systems:
+ *
+ * In general (I don't have any counterexamples) case-insensitive file systems handle
+ * their case insensitivity in a locale-independent way. NTFS, for example, writes a
+ * special case mapping file ($UpCase) to the file system when it's first initialized,
+ * and this is not affected by the locale of either the user or the copy of Windows
+ * being used. This means that it will NOT handle i/I-variants in filenames as you'd
+ * expect for Turkic languages, even for a Turkish user who has installed a Turkish
+ * copy of Windows.
+ */
+
+ /** Unicode case folding for case insensitive paths. Requires ICU4J on the classpath. */
+ CASE_FOLD_UNICODE(Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) {
+ @Override
+ public String apply(String string) {
+ try {
+ return UCharacter.foldCase(string, true);
+ } catch (NoClassDefFoundError e) {
+ NoClassDefFoundError error =
+ new NoClassDefFoundError(
+ "PathNormalization.CASE_FOLD_UNICODE requires ICU4J. "
+ + "Did you forget to include it on your classpath?");
+ error.initCause(e);
+ throw error;
+ }
+ }
+ },
+
+ /** ASCII case folding for simple case insensitive paths. */
+ CASE_FOLD_ASCII(Pattern.CASE_INSENSITIVE) {
+ @Override
+ public String apply(String string) {
+ return Ascii.toLowerCase(string);
+ }
+ };
+
+ private final int patternFlags;
+
+ private PathNormalization(int patternFlags) {
+ this.patternFlags = patternFlags;
+ }
+
+ /** Applies this normalization to the given string, returning the normalized result. */
+ @Override
+ public abstract String apply(String string);
+
+ /**
+ * Returns the flags that should be used when creating a regex {@link Pattern} in order to
+ * approximate this normalization.
+ */
+ public int patternFlags() {
+ return patternFlags;
+ }
+
+ /**
+ * Applies the given normalizations to the given string in order, returning the normalized result.
+ */
+ public static String normalize(String string, Iterable<PathNormalization> normalizations) {
+ String result = string;
+ for (PathNormalization normalization : normalizations) {
+ result = normalization.apply(result);
+ }
+ return result;
+ }
+
+ /** Compiles a regex pattern using flags based on the given normalizations. */
+ public static Pattern compilePattern(String regex, Iterable<PathNormalization> normalizations) {
+ int flags = 0;
+ for (PathNormalization normalization : normalizations) {
+ flags |= normalization.patternFlags();
+ }
+ return Pattern.compile(regex, flags);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PathService.java b/jimfs/src/main/java/com/google/common/jimfs/PathService.java
new file mode 100644
index 0000000..49717bd
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PathService.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.jimfs.PathType.ParseResult;
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Functions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.PathMatcher;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Service for creating {@link JimfsPath} instances and handling other path-related operations.
+ *
+ * @author Colin Decker
+ */
+final class PathService implements Comparator<JimfsPath> {
+
+ private static final Ordering<Name> DISPLAY_ROOT_ORDERING = Name.displayOrdering().nullsLast();
+ private static final Ordering<Iterable<Name>> DISPLAY_NAMES_ORDERING =
+ Name.displayOrdering().lexicographical();
+
+ private static final Ordering<Name> CANONICAL_ROOT_ORDERING =
+ Name.canonicalOrdering().nullsLast();
+ private static final Ordering<Iterable<Name>> CANONICAL_NAMES_ORDERING =
+ Name.canonicalOrdering().lexicographical();
+
+ private final PathType type;
+
+ private final ImmutableSet<PathNormalization> displayNormalizations;
+ private final ImmutableSet<PathNormalization> canonicalNormalizations;
+ private final boolean equalityUsesCanonicalForm;
+
+ private final Ordering<Name> rootOrdering;
+ private final Ordering<Iterable<Name>> namesOrdering;
+
+ private volatile FileSystem fileSystem;
+ private volatile JimfsPath emptyPath;
+
+ PathService(Configuration config) {
+ this(
+ config.pathType,
+ config.nameDisplayNormalization,
+ config.nameCanonicalNormalization,
+ config.pathEqualityUsesCanonicalForm);
+ }
+
+ PathService(
+ PathType type,
+ Iterable<PathNormalization> displayNormalizations,
+ Iterable<PathNormalization> canonicalNormalizations,
+ boolean equalityUsesCanonicalForm) {
+ this.type = checkNotNull(type);
+ this.displayNormalizations = ImmutableSet.copyOf(displayNormalizations);
+ this.canonicalNormalizations = ImmutableSet.copyOf(canonicalNormalizations);
+ this.equalityUsesCanonicalForm = equalityUsesCanonicalForm;
+
+ this.rootOrdering = equalityUsesCanonicalForm ? CANONICAL_ROOT_ORDERING : DISPLAY_ROOT_ORDERING;
+ this.namesOrdering =
+ equalityUsesCanonicalForm ? CANONICAL_NAMES_ORDERING : DISPLAY_NAMES_ORDERING;
+ }
+
+ /** Sets the file system to use for created paths. */
+ public void setFileSystem(FileSystem fileSystem) {
+ // allowed to not be JimfsFileSystem for testing purposes only
+ checkState(this.fileSystem == null, "may not set fileSystem twice");
+ this.fileSystem = checkNotNull(fileSystem);
+ }
+
+ /** Returns the file system this service is for. */
+ public FileSystem getFileSystem() {
+ return fileSystem;
+ }
+
+ /** Returns the default path separator. */
+ public String getSeparator() {
+ return type.getSeparator();
+ }
+
+ /** Returns an empty path which has a single name, the empty string. */
+ public JimfsPath emptyPath() {
+ JimfsPath result = emptyPath;
+ if (result == null) {
+ // use createPathInternal to avoid recursive call from createPath()
+ result = createPathInternal(null, ImmutableList.of(Name.EMPTY));
+ emptyPath = result;
+ return result;
+ }
+ return result;
+ }
+
+ /** Returns the {@link Name} form of the given string. */
+ public Name name(String name) {
+ switch (name) {
+ case "":
+ return Name.EMPTY;
+ case ".":
+ return Name.SELF;
+ case "..":
+ return Name.PARENT;
+ default:
+ String display = PathNormalization.normalize(name, displayNormalizations);
+ String canonical = PathNormalization.normalize(name, canonicalNormalizations);
+ return Name.create(display, canonical);
+ }
+ }
+
+ /** Returns the {@link Name} forms of the given strings. */
+ @VisibleForTesting
+ List<Name> names(Iterable<String> names) {
+ List<Name> result = new ArrayList<>();
+ for (String name : names) {
+ result.add(name(name));
+ }
+ return result;
+ }
+
+ /** Returns a root path with the given name. */
+ public JimfsPath createRoot(Name root) {
+ return createPath(checkNotNull(root), ImmutableList.<Name>of());
+ }
+
+ /** Returns a single filename path with the given name. */
+ public JimfsPath createFileName(Name name) {
+ return createPath(null, ImmutableList.of(name));
+ }
+
+ /** Returns a relative path with the given names. */
+ public JimfsPath createRelativePath(Iterable<Name> names) {
+ return createPath(null, ImmutableList.copyOf(names));
+ }
+
+ /** Returns a path with the given root (or no root, if null) and the given names. */
+ public JimfsPath createPath(@NullableDecl Name root, Iterable<Name> names) {
+ ImmutableList<Name> nameList = ImmutableList.copyOf(Iterables.filter(names, NOT_EMPTY));
+ if (root == null && nameList.isEmpty()) {
+ // ensure the canonical empty path (one empty string name) is used rather than a path with
+ // no root and no names
+ return emptyPath();
+ }
+ return createPathInternal(root, nameList);
+ }
+
+ /** Returns a path with the given root (or no root, if null) and the given names. */
+ protected final JimfsPath createPathInternal(@NullableDecl Name root, Iterable<Name> names) {
+ return new JimfsPath(this, root, names);
+ }
+
+ /** Parses the given strings as a path. */
+ public JimfsPath parsePath(String first, String... more) {
+ String joined = type.joiner().join(Iterables.filter(Lists.asList(first, more), NOT_EMPTY));
+ return toPath(type.parsePath(joined));
+ }
+
+ private JimfsPath toPath(ParseResult parsed) {
+ Name root = parsed.root() == null ? null : name(parsed.root());
+ Iterable<Name> names = names(parsed.names());
+ return createPath(root, names);
+ }
+
+ /** Returns the string form of the given path. */
+ public String toString(JimfsPath path) {
+ Name root = path.root();
+ String rootString = root == null ? null : root.toString();
+ Iterable<String> names = Iterables.transform(path.names(), Functions.toStringFunction());
+ return type.toString(rootString, names);
+ }
+
+ /** Creates a hash code for the given path. */
+ public int hash(JimfsPath path) {
+ // Note: JimfsPath.equals() is implemented using the compare() method below;
+ // equalityUsesCanonicalForm is taken into account there via the namesOrdering, which is set
+ // at construction time.
+ int hash = 31;
+ hash = 31 * hash + getFileSystem().hashCode();
+
+ final Name root = path.root();
+ final ImmutableList<Name> names = path.names();
+
+ if (equalityUsesCanonicalForm) {
+ // use hash codes of names themselves, which are based on the canonical form
+ hash = 31 * hash + (root == null ? 0 : root.hashCode());
+ for (Name name : names) {
+ hash = 31 * hash + name.hashCode();
+ }
+ } else {
+ // use hash codes from toString() form of names
+ hash = 31 * hash + (root == null ? 0 : root.toString().hashCode());
+ for (Name name : names) {
+ hash = 31 * hash + name.toString().hashCode();
+ }
+ }
+ return hash;
+ }
+
+ @Override
+ public int compare(JimfsPath a, JimfsPath b) {
+ return ComparisonChain.start()
+ .compare(a.root(), b.root(), rootOrdering)
+ .compare(a.names(), b.names(), namesOrdering)
+ .result();
+ }
+
+ /**
+ * Returns the URI for the given path. The given file system URI is the base against which the
+ * path is resolved to create the returned URI.
+ */
+ public URI toUri(URI fileSystemUri, JimfsPath path) {
+ checkArgument(path.isAbsolute(), "path (%s) must be absolute", path);
+ String root = String.valueOf(path.root());
+ Iterable<String> names = Iterables.transform(path.names(), Functions.toStringFunction());
+ return type.toUri(fileSystemUri, root, names, Files.isDirectory(path, NOFOLLOW_LINKS));
+ }
+
+ /** Converts the path of the given URI into a path for this file system. */
+ public JimfsPath fromUri(URI uri) {
+ return toPath(type.fromUri(uri));
+ }
+
+ /**
+ * Returns a {@link PathMatcher} for the given syntax and pattern as specified by {@link
+ * FileSystem#getPathMatcher(String)}.
+ */
+ public PathMatcher createPathMatcher(String syntaxAndPattern) {
+ return PathMatchers.getPathMatcher(
+ syntaxAndPattern,
+ type.getSeparator() + type.getOtherSeparators(),
+ equalityUsesCanonicalForm ? canonicalNormalizations : displayNormalizations);
+ }
+
+ private static final Predicate<Object> NOT_EMPTY =
+ new Predicate<Object>() {
+ @Override
+ public boolean apply(Object input) {
+ return !input.toString().isEmpty();
+ }
+ };
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PathType.java b/jimfs/src/main/java/com/google/common/jimfs/PathType.java
new file mode 100644
index 0000000..4e4d30e
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PathType.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.InvalidPathException;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * An object defining a specific type of path. Knows how to parse strings to a path and how to
+ * render a path as a string as well as what the path separator is and what other separators are
+ * recognized when parsing paths.
+ *
+ * @author Colin Decker
+ */
+public abstract class PathType {
+
+ /**
+ * Returns a Unix-style path type. "/" is both the root and the only separator. Any path starting
+ * with "/" is considered absolute. The nul character ('\0') is disallowed in paths.
+ */
+ public static PathType unix() {
+ return UnixPathType.INSTANCE;
+ }
+
+ /**
+ * Returns a Windows-style path type. The canonical separator character is "\". "/" is also
+ * treated as a separator when parsing paths.
+ *
+ * <p>As much as possible, this implementation follows the information provided in <a
+ * href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">this
+ * article</a>. Paths with drive-letter roots (e.g. "C:\") and paths with UNC roots (e.g.
+ * "\\host\share\") are supported.
+ *
+ * <p>Two Windows path features are not currently supported as they are too Windows-specific:
+ *
+ * <ul>
+ * <li>Relative paths containing a drive-letter root, for example "C:" or "C:foo\bar". Such
+ * paths have a root component and optionally have names, but are <i>relative</i> paths,
+ * relative to the working directory of the drive identified by the root.
+ * <li>Absolute paths with no root, for example "\foo\bar". Such paths are absolute paths on the
+ * current drive.
+ * </ul>
+ */
+ public static PathType windows() {
+ return WindowsPathType.INSTANCE;
+ }
+
+ private final boolean allowsMultipleRoots;
+ private final String separator;
+ private final String otherSeparators;
+ private final Joiner joiner;
+ private final Splitter splitter;
+
+ protected PathType(boolean allowsMultipleRoots, char separator, char... otherSeparators) {
+ this.separator = String.valueOf(separator);
+ this.allowsMultipleRoots = allowsMultipleRoots;
+ this.otherSeparators = String.valueOf(otherSeparators);
+ this.joiner = Joiner.on(separator);
+ this.splitter = createSplitter(separator, otherSeparators);
+ }
+
+ private static final char[] regexReservedChars = "^$.?+*\\[]{}()".toCharArray();
+
+ static {
+ Arrays.sort(regexReservedChars);
+ }
+
+ private static boolean isRegexReserved(char c) {
+ return Arrays.binarySearch(regexReservedChars, c) >= 0;
+ }
+
+ private static Splitter createSplitter(char separator, char... otherSeparators) {
+ if (otherSeparators.length == 0) {
+ return Splitter.on(separator).omitEmptyStrings();
+ }
+
+ // TODO(cgdecker): When CharMatcher is out of @Beta, us Splitter.on(CharMatcher)
+ StringBuilder patternBuilder = new StringBuilder();
+ patternBuilder.append("[");
+ appendToRegex(separator, patternBuilder);
+ for (char other : otherSeparators) {
+ appendToRegex(other, patternBuilder);
+ }
+ patternBuilder.append("]");
+ return Splitter.onPattern(patternBuilder.toString()).omitEmptyStrings();
+ }
+
+ private static void appendToRegex(char separator, StringBuilder patternBuilder) {
+ if (isRegexReserved(separator)) {
+ patternBuilder.append("\\");
+ }
+ patternBuilder.append(separator);
+ }
+
+ /** Returns whether or not this type of path allows multiple root directories. */
+ public final boolean allowsMultipleRoots() {
+ return allowsMultipleRoots;
+ }
+
+ /**
+ * Returns the canonical separator for this path type. The returned string always has a length of
+ * one.
+ */
+ public final String getSeparator() {
+ return separator;
+ }
+
+ /**
+ * Returns the other separators that are recognized when parsing a path. If no other separators
+ * are recognized, the empty string is returned.
+ */
+ public final String getOtherSeparators() {
+ return otherSeparators;
+ }
+
+ /** Returns the path joiner for this path type. */
+ public final Joiner joiner() {
+ return joiner;
+ }
+
+ /** Returns the path splitter for this path type. */
+ public final Splitter splitter() {
+ return splitter;
+ }
+
+ /** Returns an empty path. */
+ protected final ParseResult emptyPath() {
+ return new ParseResult(null, ImmutableList.of(""));
+ }
+
+ /**
+ * Parses the given strings as a path.
+ *
+ * @throws InvalidPathException if the path isn't valid for this path type
+ */
+ public abstract ParseResult parsePath(String path);
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ /** Returns the string form of the given path. */
+ public abstract String toString(@NullableDecl String root, Iterable<String> names);
+
+ /**
+ * Returns the string form of the given path for use in the path part of a URI. The root element
+ * is not nullable as the path must be absolute. The elements of the returned path <i>do not</i>
+ * need to be escaped. The {@code directory} boolean indicates whether the file the URI is for is
+ * known to be a directory.
+ */
+ protected abstract String toUriPath(String root, Iterable<String> names, boolean directory);
+
+ /**
+ * Parses a path from the given URI path.
+ *
+ * @throws InvalidPathException if the given path isn't valid for this path type
+ */
+ protected abstract ParseResult parseUriPath(String uriPath);
+
+ /**
+ * Creates a URI for the path with the given root and names in the file system with the given URI.
+ */
+ public final URI toUri(
+ URI fileSystemUri, String root, Iterable<String> names, boolean directory) {
+ String path = toUriPath(root, names, directory);
+ try {
+ // it should not suck this much to create a new URI that's the same except with a path set =(
+ // need to do it this way for automatic path escaping
+ return new URI(
+ fileSystemUri.getScheme(),
+ fileSystemUri.getUserInfo(),
+ fileSystemUri.getHost(),
+ fileSystemUri.getPort(),
+ path,
+ null,
+ null);
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Parses a path from the given URI. */
+ public final ParseResult fromUri(URI uri) {
+ return parseUriPath(uri.getPath());
+ }
+
+ /** Simple result of parsing a path. */
+ public static final class ParseResult {
+
+ @NullableDecl private final String root;
+ private final Iterable<String> names;
+
+ public ParseResult(@NullableDecl String root, Iterable<String> names) {
+ this.root = root;
+ this.names = checkNotNull(names);
+ }
+
+ /** Returns whether or not this result is an absolute path. */
+ public boolean isAbsolute() {
+ return root != null;
+ }
+
+ /** Returns whether or not this result represents a root path. */
+ public boolean isRoot() {
+ return root != null && Iterables.isEmpty(names);
+ }
+
+ /** Returns the parsed root element, or null if there was no root. */
+ @NullableDecl
+ public String root() {
+ return root;
+ }
+
+ /** Returns the parsed name elements. */
+ public Iterable<String> names() {
+ return names;
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PathURLConnection.java b/jimfs/src/main/java/com/google/common/jimfs/PathURLConnection.java
new file mode 100644
index 0000000..4f71d33
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PathURLConnection.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Ascii;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * {@code URLConnection} implementation.
+ *
+ * @author Colin Decker
+ */
+final class PathURLConnection extends URLConnection {
+
+ /*
+ * This implementation should be able to work for any proper file system implementation... it
+ * might be useful to release it and make it usable by other file systems.
+ */
+
+ private static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss \'GMT\'";
+ private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
+
+ private InputStream stream;
+ private ImmutableListMultimap<String, String> headers = ImmutableListMultimap.of();
+
+ PathURLConnection(URL url) {
+ super(checkNotNull(url));
+ }
+
+ @Override
+ public void connect() throws IOException {
+ if (stream != null) {
+ return;
+ }
+
+ Path path = Paths.get(toUri(url));
+
+ long length;
+ if (Files.isDirectory(path)) {
+ // Match File URL behavior for directories by having the stream contain the filenames in
+ // the directory separated by newlines.
+ StringBuilder builder = new StringBuilder();
+ try (DirectoryStream<Path> files = Files.newDirectoryStream(path)) {
+ for (Path file : files) {
+ builder.append(file.getFileName()).append('\n');
+ }
+ }
+ byte[] bytes = builder.toString().getBytes(UTF_8);
+ stream = new ByteArrayInputStream(bytes);
+ length = bytes.length;
+ } else {
+ stream = Files.newInputStream(path);
+ length = Files.size(path);
+ }
+
+ FileTime lastModified = Files.getLastModifiedTime(path);
+ String contentType =
+ MoreObjects.firstNonNull(Files.probeContentType(path), DEFAULT_CONTENT_TYPE);
+
+ ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
+ builder.put("content-length", "" + length);
+ builder.put("content-type", contentType);
+ if (lastModified != null) {
+ DateFormat format = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
+ format.setTimeZone(TimeZone.getTimeZone("GMT"));
+ builder.put("last-modified", format.format(new Date(lastModified.toMillis())));
+ }
+
+ headers = builder.build();
+ }
+
+ private static URI toUri(URL url) throws IOException {
+ try {
+ return url.toURI();
+ } catch (URISyntaxException e) {
+ throw new IOException("URL " + url + " cannot be converted to a URI", e);
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ connect();
+ return stream;
+ }
+
+ @SuppressWarnings("unchecked") // safe by specification of ListMultimap.asMap()
+ @Override
+ public Map<String, List<String>> getHeaderFields() {
+ try {
+ connect();
+ } catch (IOException e) {
+ return ImmutableMap.of();
+ }
+ return (ImmutableMap<String, List<String>>) (ImmutableMap<String, ?>) headers.asMap();
+ }
+
+ @Override
+ public String getHeaderField(String name) {
+ try {
+ connect();
+ } catch (IOException e) {
+ return null;
+ }
+
+ // no header should have more than one value
+ return Iterables.getFirst(headers.get(Ascii.toLowerCase(name)), null);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java b/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java
new file mode 100644
index 0000000..d1f5b89
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchService;
+import java.nio.file.Watchable;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation of {@link WatchService} that polls for changes to directories at registered paths.
+ *
+ * @author Colin Decker
+ */
+final class PollingWatchService extends AbstractWatchService {
+
+ /**
+ * Thread factory for polling threads, which should be daemon threads so as not to keep the VM
+ * running if the user doesn't close the watch service or the file system.
+ */
+ private static final ThreadFactory THREAD_FACTORY =
+ new ThreadFactoryBuilder()
+ .setNameFormat("com.google.common.jimfs.PollingWatchService-thread-%d")
+ .setDaemon(true)
+ .build();
+
+ private final ScheduledExecutorService pollingService =
+ Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY);
+
+ /** Map of keys to the most recent directory snapshot for each key. */
+ private final ConcurrentMap<Key, Snapshot> snapshots = new ConcurrentHashMap<>();
+
+ private final FileSystemView view;
+ private final PathService pathService;
+ private final FileSystemState fileSystemState;
+
+ @VisibleForTesting final long interval;
+ @VisibleForTesting final TimeUnit timeUnit;
+
+ private ScheduledFuture<?> pollingFuture;
+
+ PollingWatchService(
+ FileSystemView view,
+ PathService pathService,
+ FileSystemState fileSystemState,
+ long interval,
+ TimeUnit timeUnit) {
+ this.view = checkNotNull(view);
+ this.pathService = checkNotNull(pathService);
+ this.fileSystemState = checkNotNull(fileSystemState);
+
+ checkArgument(interval >= 0, "interval (%s) may not be negative", interval);
+ this.interval = interval;
+ this.timeUnit = checkNotNull(timeUnit);
+
+ fileSystemState.register(this);
+ }
+
+ @Override
+ public Key register(Watchable watchable, Iterable<? extends WatchEvent.Kind<?>> eventTypes)
+ throws IOException {
+ JimfsPath path = checkWatchable(watchable);
+
+ Key key = super.register(path, eventTypes);
+
+ Snapshot snapshot = takeSnapshot(path);
+
+ synchronized (this) {
+ snapshots.put(key, snapshot);
+ if (pollingFuture == null) {
+ startPolling();
+ }
+ }
+
+ return key;
+ }
+
+ private JimfsPath checkWatchable(Watchable watchable) {
+ if (!(watchable instanceof JimfsPath) || !isSameFileSystem((Path) watchable)) {
+ throw new IllegalArgumentException(
+ "watchable ("
+ + watchable
+ + ") must be a Path "
+ + "associated with the same file system as this watch service");
+ }
+
+ return (JimfsPath) watchable;
+ }
+
+ private boolean isSameFileSystem(Path path) {
+ return ((JimfsFileSystem) path.getFileSystem()).getDefaultView() == view;
+ }
+
+ @VisibleForTesting
+ synchronized boolean isPolling() {
+ return pollingFuture != null;
+ }
+
+ @Override
+ public synchronized void cancelled(Key key) {
+ snapshots.remove(key);
+
+ if (snapshots.isEmpty()) {
+ stopPolling();
+ }
+ }
+
+ @Override
+ public void close() {
+ super.close();
+
+ synchronized (this) {
+ // synchronize to ensure no new
+ for (Key key : snapshots.keySet()) {
+ key.cancel();
+ }
+
+ pollingService.shutdown();
+ fileSystemState.unregister(this);
+ }
+ }
+
+ private void startPolling() {
+ pollingFuture = pollingService.scheduleAtFixedRate(pollingTask, interval, interval, timeUnit);
+ }
+
+ private void stopPolling() {
+ pollingFuture.cancel(false);
+ pollingFuture = null;
+ }
+
+ private final Runnable pollingTask =
+ new Runnable() {
+ @Override
+ public void run() {
+ synchronized (PollingWatchService.this) {
+ for (Map.Entry<Key, Snapshot> entry : snapshots.entrySet()) {
+ Key key = entry.getKey();
+ Snapshot previousSnapshot = entry.getValue();
+
+ JimfsPath path = (JimfsPath) key.watchable();
+ try {
+ Snapshot newSnapshot = takeSnapshot(path);
+ boolean posted = previousSnapshot.postChanges(newSnapshot, key);
+ entry.setValue(newSnapshot);
+ if (posted) {
+ key.signal();
+ }
+ } catch (IOException e) {
+ // snapshot failed; assume file does not exist or isn't a directory
+ // and cancel the key
+ key.cancel();
+ }
+ }
+ }
+ }
+ };
+
+ private Snapshot takeSnapshot(JimfsPath path) throws IOException {
+ return new Snapshot(view.snapshotModifiedTimes(path));
+ }
+
+ /** Snapshot of the state of a directory at a particular moment. */
+ private final class Snapshot {
+
+ /** Maps directory entry names to last modified times. */
+ private final ImmutableMap<Name, Long> modifiedTimes;
+
+ Snapshot(Map<Name, Long> modifiedTimes) {
+ this.modifiedTimes = ImmutableMap.copyOf(modifiedTimes);
+ }
+
+ /**
+ * Posts events to the given key based on the kinds of events it subscribes to and what events
+ * have occurred between this state and the given new state.
+ */
+ boolean postChanges(Snapshot newState, Key key) {
+ boolean changesPosted = false;
+
+ if (key.subscribesTo(ENTRY_CREATE)) {
+ Set<Name> created =
+ Sets.difference(newState.modifiedTimes.keySet(), modifiedTimes.keySet());
+
+ for (Name name : created) {
+ key.post(new Event<>(ENTRY_CREATE, 1, pathService.createFileName(name)));
+ changesPosted = true;
+ }
+ }
+
+ if (key.subscribesTo(ENTRY_DELETE)) {
+ Set<Name> deleted =
+ Sets.difference(modifiedTimes.keySet(), newState.modifiedTimes.keySet());
+
+ for (Name name : deleted) {
+ key.post(new Event<>(ENTRY_DELETE, 1, pathService.createFileName(name)));
+ changesPosted = true;
+ }
+ }
+
+ if (key.subscribesTo(ENTRY_MODIFY)) {
+ for (Map.Entry<Name, Long> entry : modifiedTimes.entrySet()) {
+ Name name = entry.getKey();
+ Long modifiedTime = entry.getValue();
+
+ Long newModifiedTime = newState.modifiedTimes.get(name);
+ if (newModifiedTime != null && !modifiedTime.equals(newModifiedTime)) {
+ key.post(new Event<>(ENTRY_MODIFY, 1, pathService.createFileName(name)));
+ changesPosted = true;
+ }
+ }
+ }
+
+ return changesPosted;
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/PosixAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/PosixAttributeProvider.java
new file mode 100644
index 0000000..9dcd887
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/PosixAttributeProvider.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.jimfs.UserLookupService.createGroupPrincipal;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Map;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Attribute provider that provides the {@link PosixFileAttributeView} ("posix") and allows reading
+ * of {@link PosixFileAttributes}.
+ *
+ * @author Colin Decker
+ */
+final class PosixAttributeProvider extends AttributeProvider {
+
+ private static final ImmutableSet<String> ATTRIBUTES = ImmutableSet.of("group", "permissions");
+
+ private static final ImmutableSet<String> INHERITED_VIEWS = ImmutableSet.of("basic", "owner");
+
+ private static final GroupPrincipal DEFAULT_GROUP = createGroupPrincipal("group");
+ private static final ImmutableSet<PosixFilePermission> DEFAULT_PERMISSIONS =
+ Sets.immutableEnumSet(PosixFilePermissions.fromString("rw-r--r--"));
+
+ @Override
+ public String name() {
+ return "posix";
+ }
+
+ @Override
+ public ImmutableSet<String> inherits() {
+ return INHERITED_VIEWS;
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ return ATTRIBUTES;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ImmutableMap<String, ?> defaultValues(Map<String, ?> userProvidedDefaults) {
+ Object userProvidedGroup = userProvidedDefaults.get("posix:group");
+
+ UserPrincipal group = DEFAULT_GROUP;
+ if (userProvidedGroup != null) {
+ if (userProvidedGroup instanceof String) {
+ group = createGroupPrincipal((String) userProvidedGroup);
+ } else {
+ throw new IllegalArgumentException(
+ "invalid type "
+ + userProvidedGroup.getClass().getName()
+ + " for attribute 'posix:group': should be one of "
+ + String.class
+ + " or "
+ + GroupPrincipal.class);
+ }
+ }
+
+ Object userProvidedPermissions = userProvidedDefaults.get("posix:permissions");
+
+ Set<PosixFilePermission> permissions = DEFAULT_PERMISSIONS;
+ if (userProvidedPermissions != null) {
+ if (userProvidedPermissions instanceof String) {
+ permissions =
+ Sets.immutableEnumSet(
+ PosixFilePermissions.fromString((String) userProvidedPermissions));
+ } else if (userProvidedPermissions instanceof Set) {
+ permissions = toPermissions((Set<?>) userProvidedPermissions);
+ } else {
+ throw new IllegalArgumentException(
+ "invalid type "
+ + userProvidedPermissions.getClass().getName()
+ + " for attribute 'posix:permissions': should be one of "
+ + String.class
+ + " or "
+ + Set.class);
+ }
+ }
+
+ return ImmutableMap.of(
+ "posix:group", group,
+ "posix:permissions", permissions);
+ }
+
+ @NullableDecl
+ @Override
+ public Object get(File file, String attribute) {
+ switch (attribute) {
+ case "group":
+ return file.getAttribute("posix", "group");
+ case "permissions":
+ return file.getAttribute("posix", "permissions");
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ switch (attribute) {
+ case "group":
+ checkNotCreate(view, attribute, create);
+
+ GroupPrincipal group = checkType(view, attribute, value, GroupPrincipal.class);
+ if (!(group instanceof UserLookupService.JimfsGroupPrincipal)) {
+ group = createGroupPrincipal(group.getName());
+ }
+ file.setAttribute("posix", "group", group);
+ break;
+ case "permissions":
+ file.setAttribute(
+ "posix", "permissions", toPermissions(checkType(view, attribute, value, Set.class)));
+ break;
+ default:
+ }
+ }
+
+ @SuppressWarnings("unchecked") // only cast after checking each element's type
+ private static ImmutableSet<PosixFilePermission> toPermissions(Set<?> set) {
+ ImmutableSet<?> copy = ImmutableSet.copyOf(set);
+ for (Object obj : copy) {
+ if (!(obj instanceof PosixFilePermission)) {
+ throw new IllegalArgumentException(
+ "invalid element for attribute 'posix:permissions': "
+ + "should be Set<PosixFilePermission>, found element of type "
+ + obj.getClass());
+ }
+ }
+
+ return Sets.immutableEnumSet((ImmutableSet<PosixFilePermission>) copy);
+ }
+
+ @Override
+ public Class<PosixFileAttributeView> viewType() {
+ return PosixFileAttributeView.class;
+ }
+
+ @Override
+ public PosixFileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ return new View(
+ lookup,
+ (BasicFileAttributeView) inheritedViews.get("basic"),
+ (FileOwnerAttributeView) inheritedViews.get("owner"));
+ }
+
+ @Override
+ public Class<PosixFileAttributes> attributesType() {
+ return PosixFileAttributes.class;
+ }
+
+ @Override
+ public PosixFileAttributes readAttributes(File file) {
+ return new Attributes(file);
+ }
+
+ /** Implementation of {@link PosixFileAttributeView}. */
+ private static class View extends AbstractAttributeView implements PosixFileAttributeView {
+
+ private final BasicFileAttributeView basicView;
+ private final FileOwnerAttributeView ownerView;
+
+ protected View(
+ FileLookup lookup, BasicFileAttributeView basicView, FileOwnerAttributeView ownerView) {
+ super(lookup);
+ this.basicView = checkNotNull(basicView);
+ this.ownerView = checkNotNull(ownerView);
+ }
+
+ @Override
+ public String name() {
+ return "posix";
+ }
+
+ @Override
+ public PosixFileAttributes readAttributes() throws IOException {
+ return new Attributes(lookupFile());
+ }
+
+ @Override
+ public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime)
+ throws IOException {
+ basicView.setTimes(lastModifiedTime, lastAccessTime, createTime);
+ }
+
+ @Override
+ public void setPermissions(Set<PosixFilePermission> perms) throws IOException {
+ lookupFile().setAttribute("posix", "permissions", ImmutableSet.copyOf(perms));
+ }
+
+ @Override
+ public void setGroup(GroupPrincipal group) throws IOException {
+ lookupFile().setAttribute("posix", "group", checkNotNull(group));
+ }
+
+ @Override
+ public UserPrincipal getOwner() throws IOException {
+ return ownerView.getOwner();
+ }
+
+ @Override
+ public void setOwner(UserPrincipal owner) throws IOException {
+ ownerView.setOwner(owner);
+ }
+ }
+
+ /** Implementation of {@link PosixFileAttributes}. */
+ static class Attributes extends BasicAttributeProvider.Attributes implements PosixFileAttributes {
+
+ private final UserPrincipal owner;
+ private final GroupPrincipal group;
+ private final ImmutableSet<PosixFilePermission> permissions;
+
+ @SuppressWarnings("unchecked")
+ protected Attributes(File file) {
+ super(file);
+ this.owner = (UserPrincipal) file.getAttribute("owner", "owner");
+ this.group = (GroupPrincipal) file.getAttribute("posix", "group");
+ this.permissions =
+ (ImmutableSet<PosixFilePermission>) file.getAttribute("posix", "permissions");
+ }
+
+ @Override
+ public UserPrincipal owner() {
+ return owner;
+ }
+
+ @Override
+ public GroupPrincipal group() {
+ return group;
+ }
+
+ @Override
+ public ImmutableSet<PosixFilePermission> permissions() {
+ return permissions;
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java b/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java
new file mode 100644
index 0000000..b8bb688
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/RegularFile.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.jimfs.Util.clear;
+import static com.google.common.jimfs.Util.nextPowerOf2;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.UnsignedBytes;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.Arrays;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * A mutable, resizable store for bytes. Bytes are stored in fixed-sized byte arrays (blocks)
+ * allocated by a {@link HeapDisk}.
+ *
+ * @author Colin Decker
+ */
+final class RegularFile extends File {
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ private final HeapDisk disk;
+
+ /** Block list for the file. */
+ private byte[][] blocks;
+ /** Block count for the the file, which also acts as the head of the block list. */
+ private int blockCount;
+
+ private long size;
+
+ /** Creates a new regular file with the given ID and using the given disk. */
+ public static RegularFile create(int id, HeapDisk disk) {
+ return new RegularFile(id, disk, new byte[32][], 0, 0);
+ }
+
+ RegularFile(int id, HeapDisk disk, byte[][] blocks, int blockCount, long size) {
+ super(id);
+ this.disk = checkNotNull(disk);
+ this.blocks = checkNotNull(blocks);
+ this.blockCount = blockCount;
+
+ checkArgument(size >= 0);
+ this.size = size;
+ }
+
+ private int openCount = 0;
+ private boolean deleted = false;
+
+ /** Returns the read lock for this file. */
+ public Lock readLock() {
+ return lock.readLock();
+ }
+
+ /** Returns the write lock for this file. */
+ public Lock writeLock() {
+ return lock.writeLock();
+ }
+
+ // lower-level methods dealing with the blocks array
+
+ private void expandIfNecessary(int minBlockCount) {
+ if (minBlockCount > blocks.length) {
+ this.blocks = Arrays.copyOf(blocks, nextPowerOf2(minBlockCount));
+ }
+ }
+
+ /** Returns the number of blocks this file contains. */
+ int blockCount() {
+ return blockCount;
+ }
+
+ /** Copies the last {@code count} blocks from this file to the end of the given target file. */
+ void copyBlocksTo(RegularFile target, int count) {
+ int start = blockCount - count;
+ int targetEnd = target.blockCount + count;
+ target.expandIfNecessary(targetEnd);
+
+ System.arraycopy(this.blocks, start, target.blocks, target.blockCount, count);
+ target.blockCount = targetEnd;
+ }
+
+ /** Transfers the last {@code count} blocks from this file to the end of the given target file. */
+ void transferBlocksTo(RegularFile target, int count) {
+ copyBlocksTo(target, count);
+ truncateBlocks(blockCount - count);
+ }
+
+ /** Truncates the blocks of this file to the given block count. */
+ void truncateBlocks(int count) {
+ clear(blocks, count, blockCount - count);
+ blockCount = count;
+ }
+
+ /** Adds the given block to the end of this file. */
+ void addBlock(byte[] block) {
+ expandIfNecessary(blockCount + 1);
+ blocks[blockCount++] = block;
+ }
+
+ /** Gets the block at the given index in this file. */
+ @VisibleForTesting
+ byte[] getBlock(int index) {
+ return blocks[index];
+ }
+
+ // end of lower-level methods dealing with the blocks array
+
+ /**
+ * Gets the current size of this file in bytes. Does not do locking, so should only be called when
+ * holding a lock.
+ */
+ public long sizeWithoutLocking() {
+ return size;
+ }
+
+ // need to lock in these methods since they're defined by an interface
+
+ @Override
+ public long size() {
+ readLock().lock();
+ try {
+ return size;
+ } finally {
+ readLock().unlock();
+ }
+ }
+
+ @Override
+ RegularFile copyWithoutContent(int id) {
+ byte[][] copyBlocks = new byte[Math.max(blockCount * 2, 32)][];
+ return new RegularFile(id, disk, copyBlocks, 0, size);
+ }
+
+ @Override
+ void copyContentTo(File file) throws IOException {
+ RegularFile copy = (RegularFile) file;
+ disk.allocate(copy, blockCount);
+
+ for (int i = 0; i < blockCount; i++) {
+ byte[] block = blocks[i];
+ byte[] copyBlock = copy.blocks[i];
+ System.arraycopy(block, 0, copyBlock, 0, block.length);
+ }
+ }
+
+ @Override
+ ReadWriteLock contentLock() {
+ return lock;
+ }
+
+ // opened/closed/delete don't use the read/write lock... they only need to ensure that they are
+ // synchronized among themselves
+
+ @Override
+ public synchronized void opened() {
+ openCount++;
+ }
+
+ @Override
+ public synchronized void closed() {
+ if (--openCount == 0 && deleted) {
+ deleteContents();
+ }
+ }
+
+ /**
+ * Marks this file as deleted. If there are no streams or channels open to the file, its contents
+ * are deleted if necessary.
+ */
+ @Override
+ public synchronized void deleted() {
+ if (links() == 0) {
+ deleted = true;
+ if (openCount == 0) {
+ deleteContents();
+ }
+ }
+ }
+
+ /**
+ * Deletes the contents of this file. Called when this file has been deleted and all open streams
+ * and channels to it have been closed.
+ */
+ private void deleteContents() {
+ disk.free(this);
+ size = 0;
+ }
+
+ /**
+ * Truncates this file to the given {@code size}. If the given size is less than the current size
+ * of this file, the size of the file is reduced to the given size and any bytes beyond that size
+ * are lost. If the given size is greater than the current size of the file, this method does
+ * nothing. Returns {@code true} if this file was modified by the call (its size changed) and
+ * {@code false} otherwise.
+ */
+ public boolean truncate(long size) {
+ if (size >= this.size) {
+ return false;
+ }
+
+ long lastPosition = size - 1;
+ this.size = size;
+
+ int newBlockCount = blockIndex(lastPosition) + 1;
+ int blocksToRemove = blockCount - newBlockCount;
+ if (blocksToRemove > 0) {
+ disk.free(this, blocksToRemove);
+ }
+
+ return true;
+ }
+
+ /** Prepares for a write of len bytes starting at position pos. */
+ private void prepareForWrite(long pos, long len) throws IOException {
+ long end = pos + len;
+
+ // allocate any additional blocks needed
+ int lastBlockIndex = blockCount - 1;
+ int endBlockIndex = blockIndex(end - 1);
+
+ if (endBlockIndex > lastBlockIndex) {
+ int additionalBlocksNeeded = endBlockIndex - lastBlockIndex;
+ disk.allocate(this, additionalBlocksNeeded);
+ }
+
+ // zero bytes between current size and pos
+ if (pos > size) {
+ long remaining = pos - size;
+
+ int blockIndex = blockIndex(size);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(size);
+
+ remaining -= zero(block, off, length(off, remaining));
+
+ while (remaining > 0) {
+ block = blocks[++blockIndex];
+
+ remaining -= zero(block, 0, length(remaining));
+ }
+
+ size = pos;
+ }
+ }
+
+ /**
+ * Writes the given byte to this file at position {@code pos}. {@code pos} may be greater than the
+ * current size of this file, in which case this file is resized and all bytes between the current
+ * size and {@code pos} are set to 0. Returns the number of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public int write(long pos, byte b) throws IOException {
+ prepareForWrite(pos, 1);
+
+ byte[] block = blocks[blockIndex(pos)];
+ int off = offsetInBlock(pos);
+ block[off] = b;
+
+ if (pos >= size) {
+ size = pos + 1;
+ }
+
+ return 1;
+ }
+
+ /**
+ * Writes {@code len} bytes starting at offset {@code off} in the given byte array to this file
+ * starting at position {@code pos}. {@code pos} may be greater than the current size of this
+ * file, in which case this file is resized and all bytes between the current size and {@code pos}
+ * are set to 0. Returns the number of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public int write(long pos, byte[] b, int off, int len) throws IOException {
+ prepareForWrite(pos, len);
+
+ if (len == 0) {
+ return 0;
+ }
+
+ int remaining = len;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int offInBlock = offsetInBlock(pos);
+
+ int written = put(block, offInBlock, b, off, length(offInBlock, remaining));
+ remaining -= written;
+ off += written;
+
+ while (remaining > 0) {
+ block = blocks[++blockIndex];
+
+ written = put(block, 0, b, off, length(remaining));
+ remaining -= written;
+ off += written;
+ }
+
+ long endPos = pos + len;
+ if (endPos > size) {
+ size = endPos;
+ }
+
+ return len;
+ }
+
+ /**
+ * Writes all available bytes from buffer {@code buf} to this file starting at position {@code
+ * pos}. {@code pos} may be greater than the current size of this file, in which case this file is
+ * resized and all bytes between the current size and {@code pos} are set to 0. Returns the number
+ * of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public int write(long pos, ByteBuffer buf) throws IOException {
+ int len = buf.remaining();
+
+ prepareForWrite(pos, len);
+
+ if (len == 0) {
+ return 0;
+ }
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(pos);
+
+ put(block, off, buf);
+
+ while (buf.hasRemaining()) {
+ block = blocks[++blockIndex];
+
+ put(block, 0, buf);
+ }
+
+ long endPos = pos + len;
+ if (endPos > size) {
+ size = endPos;
+ }
+
+ return len;
+ }
+
+ /**
+ * Writes all available bytes from each buffer in {@code bufs}, in order, to this file starting at
+ * position {@code pos}. {@code pos} may be greater than the current size of this file, in which
+ * case this file is resized and all bytes between the current size and {@code pos} are set to 0.
+ * Returns the number of bytes written.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full
+ */
+ public long write(long pos, Iterable<ByteBuffer> bufs) throws IOException {
+ long start = pos;
+ for (ByteBuffer buf : bufs) {
+ pos += write(pos, buf);
+ }
+ return pos - start;
+ }
+
+ /**
+ * Transfers up to {@code count} bytes from the given channel to this file starting at position
+ * {@code pos}. Returns the number of bytes transferred. If {@code pos} is greater than the
+ * current size of this file, the file is truncated up to size {@code pos} before writing.
+ *
+ * @throws IOException if the file needs more blocks but the disk is full or if reading from src
+ * throws an exception
+ */
+ public long transferFrom(ReadableByteChannel src, long pos, long count) throws IOException {
+ prepareForWrite(pos, 0); // don't assume the full count bytes will be written
+
+ if (count == 0) {
+ return 0;
+ }
+
+ long remaining = count;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blockForWrite(blockIndex);
+ int off = offsetInBlock(pos);
+
+ ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
+
+ long currentPos = pos;
+ int read = 0;
+ while (buf.hasRemaining()) {
+ read = src.read(buf);
+ if (read == -1) {
+ break;
+ }
+
+ currentPos += read;
+ remaining -= read;
+ }
+
+ // update size before trying to get next block in case the disk is out of space
+ if (currentPos > size) {
+ size = currentPos;
+ }
+
+ if (read != -1) {
+ outer:
+ while (remaining > 0) {
+ block = blockForWrite(++blockIndex);
+
+ buf = ByteBuffer.wrap(block, 0, length(remaining));
+ while (buf.hasRemaining()) {
+ read = src.read(buf);
+ if (read == -1) {
+ break outer;
+ }
+
+ currentPos += read;
+ remaining -= read;
+ }
+
+ if (currentPos > size) {
+ size = currentPos;
+ }
+ }
+ }
+
+ if (currentPos > size) {
+ size = currentPos;
+ }
+
+ return currentPos - pos;
+ }
+
+ /**
+ * Reads the byte at position {@code pos} in this file as an unsigned integer in the range 0-255.
+ * If {@code pos} is greater than or equal to the size of this file, returns -1 instead.
+ */
+ public int read(long pos) {
+ if (pos >= size) {
+ return -1;
+ }
+
+ byte[] block = blocks[blockIndex(pos)];
+ int off = offsetInBlock(pos);
+ return UnsignedBytes.toInt(block[off]);
+ }
+
+ /**
+ * Reads up to {@code len} bytes starting at position {@code pos} in this file to the given byte
+ * array starting at offset {@code off}. Returns the number of bytes actually read or -1 if {@code
+ * pos} is greater than or equal to the size of this file.
+ */
+ public int read(long pos, byte[] b, int off, int len) {
+ // since max is len (an int), result is guaranteed to be an int
+ int bytesToRead = (int) bytesToRead(pos, len);
+
+ if (bytesToRead > 0) {
+ int remaining = bytesToRead;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int offsetInBlock = offsetInBlock(pos);
+
+ int read = get(block, offsetInBlock, b, off, length(offsetInBlock, remaining));
+ remaining -= read;
+ off += read;
+
+ while (remaining > 0) {
+ int index = ++blockIndex;
+ block = blocks[index];
+
+ read = get(block, 0, b, off, length(remaining));
+ remaining -= read;
+ off += read;
+ }
+ }
+
+ return bytesToRead;
+ }
+
+ /**
+ * Reads up to {@code buf.remaining()} bytes starting at position {@code pos} in this file to the
+ * given buffer. Returns the number of bytes read or -1 if {@code pos} is greater than or equal to
+ * the size of this file.
+ */
+ public int read(long pos, ByteBuffer buf) {
+ // since max is buf.remaining() (an int), result is guaranteed to be an int
+ int bytesToRead = (int) bytesToRead(pos, buf.remaining());
+
+ if (bytesToRead > 0) {
+ int remaining = bytesToRead;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(pos);
+
+ remaining -= get(block, off, buf, length(off, remaining));
+
+ while (remaining > 0) {
+ int index = ++blockIndex;
+ block = blocks[index];
+ remaining -= get(block, 0, buf, length(remaining));
+ }
+ }
+
+ return bytesToRead;
+ }
+
+ /**
+ * Reads up to the total {@code remaining()} number of bytes in each of {@code bufs} starting at
+ * position {@code pos} in this file to the given buffers, in order. Returns the number of bytes
+ * read or -1 if {@code pos} is greater than or equal to the size of this file.
+ */
+ public long read(long pos, Iterable<ByteBuffer> bufs) {
+ if (pos >= size()) {
+ return -1;
+ }
+
+ long start = pos;
+ for (ByteBuffer buf : bufs) {
+ int read = read(pos, buf);
+ if (read == -1) {
+ break;
+ } else {
+ pos += read;
+ }
+ }
+
+ return pos - start;
+ }
+
+ /**
+ * Transfers up to {@code count} bytes to the given channel starting at position {@code pos} in
+ * this file. Returns the number of bytes transferred, possibly 0. Note that unlike all other read
+ * methods in this class, this method does not return -1 if {@code pos} is greater than or equal
+ * to the current size. This for consistency with {@link FileChannel#transferTo}, which this
+ * method is primarily intended as an implementation of.
+ */
+ public long transferTo(long pos, long count, WritableByteChannel dest) throws IOException {
+ long bytesToRead = bytesToRead(pos, count);
+
+ if (bytesToRead > 0) {
+ long remaining = bytesToRead;
+
+ int blockIndex = blockIndex(pos);
+ byte[] block = blocks[blockIndex];
+ int off = offsetInBlock(pos);
+
+ ByteBuffer buf = ByteBuffer.wrap(block, off, length(off, remaining));
+ while (buf.hasRemaining()) {
+ remaining -= dest.write(buf);
+ }
+ buf.clear();
+
+ while (remaining > 0) {
+ int index = ++blockIndex;
+ block = blocks[index];
+
+ buf = ByteBuffer.wrap(block, 0, length(remaining));
+ while (buf.hasRemaining()) {
+ remaining -= dest.write(buf);
+ }
+ buf.clear();
+ }
+ }
+
+ return Math.max(bytesToRead, 0); // don't return -1 for this method
+ }
+
+ /** Gets the block at the given index, expanding to create the block if necessary. */
+ private byte[] blockForWrite(int index) throws IOException {
+ if (index >= blockCount) {
+ int additionalBlocksNeeded = index - blockCount + 1;
+ disk.allocate(this, additionalBlocksNeeded);
+ }
+
+ return blocks[index];
+ }
+
+ private int blockIndex(long position) {
+ return (int) (position / disk.blockSize());
+ }
+
+ private int offsetInBlock(long position) {
+ return (int) (position % disk.blockSize());
+ }
+
+ private int length(long max) {
+ return (int) Math.min(disk.blockSize(), max);
+ }
+
+ private int length(int off, long max) {
+ return (int) Math.min(disk.blockSize() - off, max);
+ }
+
+ /**
+ * Returns the number of bytes that can be read starting at position {@code pos} (up to a maximum
+ * of {@code max}) or -1 if {@code pos} is greater than or equal to the current size.
+ */
+ private long bytesToRead(long pos, long max) {
+ long available = size - pos;
+ if (available <= 0) {
+ return -1;
+ }
+ return Math.min(available, max);
+ }
+
+ /** Zeroes len bytes in the given block starting at the given offset. Returns len. */
+ private static int zero(byte[] block, int offset, int len) {
+ Util.zero(block, offset, len);
+ return len;
+ }
+
+ /** Puts the given slice of the given array at the given offset in the given block. */
+ private static int put(byte[] block, int offset, byte[] b, int off, int len) {
+ System.arraycopy(b, off, block, offset, len);
+ return len;
+ }
+
+ /** Puts the contents of the given byte buffer at the given offset in the given block. */
+ private static int put(byte[] block, int offset, ByteBuffer buf) {
+ int len = Math.min(block.length - offset, buf.remaining());
+ buf.get(block, offset, len);
+ return len;
+ }
+
+ /**
+ * Reads len bytes starting at the given offset in the given block into the given slice of the
+ * given byte array.
+ */
+ private static int get(byte[] block, int offset, byte[] b, int off, int len) {
+ System.arraycopy(block, offset, b, off, len);
+ return len;
+ }
+
+ /** Reads len bytes starting at the given offset in the given block into the given byte buffer. */
+ private static int get(byte[] block, int offset, ByteBuffer buf, int len) {
+ buf.put(block, offset, len);
+ return len;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/StandardAttributeProviders.java b/jimfs/src/main/java/com/google/common/jimfs/StandardAttributeProviders.java
new file mode 100644
index 0000000..973c6bb
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/StandardAttributeProviders.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import com.google.common.collect.ImmutableMap;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Static registry of {@link AttributeProvider} implementations for the standard set of file
+ * attribute views Jimfs supports.
+ *
+ * @author Colin Decker
+ */
+final class StandardAttributeProviders {
+
+ private StandardAttributeProviders() {}
+
+ private static final ImmutableMap<String, AttributeProvider> PROVIDERS =
+ new ImmutableMap.Builder<String, AttributeProvider>()
+ .put("basic", new BasicAttributeProvider())
+ .put("owner", new OwnerAttributeProvider())
+ .put("posix", new PosixAttributeProvider())
+ .put("dos", new DosAttributeProvider())
+ .put("acl", new AclAttributeProvider())
+ .put("user", new UserDefinedAttributeProvider())
+ .build();
+
+ /**
+ * Returns the attribute provider for the given view, or {@code null} if the given view is not one
+ * of the attribute views this supports.
+ */
+ @NullableDecl
+ public static AttributeProvider get(String view) {
+ AttributeProvider provider = PROVIDERS.get(view);
+
+ if (provider == null && view.equals("unix")) {
+ // create a new UnixAttributeProvider per file system, as it does some caching that should be
+ // cleaned up when the file system is garbage collected
+ return new UnixAttributeProvider();
+ }
+
+ return provider;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/SymbolicLink.java b/jimfs/src/main/java/com/google/common/jimfs/SymbolicLink.java
new file mode 100644
index 0000000..29f4aa5
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/SymbolicLink.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A symbolic link file, containing a {@linkplain JimfsPath path}.
+ *
+ * @author Colin Decker
+ */
+final class SymbolicLink extends File {
+
+ private final JimfsPath target;
+
+ /** Creates a new symbolic link with the given ID and target. */
+ public static SymbolicLink create(int id, JimfsPath target) {
+ return new SymbolicLink(id, target);
+ }
+
+ private SymbolicLink(int id, JimfsPath target) {
+ super(id);
+ this.target = checkNotNull(target);
+ }
+
+ /** Returns the target path of this symbolic link. */
+ JimfsPath target() {
+ return target;
+ }
+
+ @Override
+ File copyWithoutContent(int id) {
+ return SymbolicLink.create(id, target);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/SystemJimfsFileSystemProvider.java b/jimfs/src/main/java/com/google/common/jimfs/SystemJimfsFileSystemProvider.java
new file mode 100644
index 0000000..dcf3d02
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/SystemJimfsFileSystemProvider.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.jimfs.Jimfs.URI_SCHEME;
+
+import com.google.auto.service.AutoService;
+import com.google.common.collect.MapMaker;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * {@link FileSystemProvider} implementation for Jimfs that is loaded by the system as a service.
+ * This implementation only serves as a cache for file system instances and does not implement
+ * actual file system operations.
+ *
+ * <p>While this class is public, it should not be used directly. To create a new file system
+ * instance, see {@link Jimfs}. For other operations, use the public APIs in {@code java.nio.file}.
+ *
+ * @author Colin Decker
+ * @since 1.1
+ */
+@AutoService(FileSystemProvider.class)
+public final class SystemJimfsFileSystemProvider extends FileSystemProvider {
+
+ /**
+ * Env map key that maps to the already-created {@code FileSystem} instance in {@code
+ * newFileSystem}.
+ */
+ static final String FILE_SYSTEM_KEY = "fileSystem";
+
+ /**
+ * Cache of file systems that have been created but not closed.
+ *
+ * <p>This cache is static to ensure that even when this provider isn't loaded by the system class
+ * loader, meaning that a new instance of it must be created each time one of the methods on
+ * {@link FileSystems} or {@link Paths#get(URI)} is called, cached file system instances are still
+ * available.
+ *
+ * <p>The cache uses weak values so that it doesn't prevent file systems that are created but not
+ * closed from being garbage collected if no references to them are held elsewhere. This is a
+ * compromise between ensuring that any file URI continues to work as long as the file system
+ * hasn't been closed (which is technically the correct thing to do but unlikely to be something
+ * that most users care about) and ensuring that users don't get unexpected leaks of large amounts
+ * of memory because they're creating many file systems in tests but forgetting to close them
+ * (which seems likely to happen sometimes). Users that want to ensure that a file system won't be
+ * garbage collected just need to ensure they hold a reference to it somewhere for as long as they
+ * need it to stick around.
+ */
+ private static final ConcurrentMap<URI, FileSystem> fileSystems =
+ new MapMaker().weakValues().makeMap();
+
+ /** @deprecated Not intended to be called directly; this class is only for use by Java itself. */
+ @Deprecated
+ public SystemJimfsFileSystemProvider() {} // a public, no-arg constructor is required
+
+ @Override
+ public String getScheme() {
+ return URI_SCHEME;
+ }
+
+ @Override
+ public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+ checkArgument(
+ uri.getScheme().equalsIgnoreCase(URI_SCHEME),
+ "uri (%s) scheme must be '%s'",
+ uri,
+ URI_SCHEME);
+ checkArgument(
+ isValidFileSystemUri(uri), "uri (%s) may not have a path, query or fragment", uri);
+ checkArgument(
+ env.get(FILE_SYSTEM_KEY) instanceof FileSystem,
+ "env map (%s) must contain key '%s' mapped to an instance of %s",
+ env,
+ FILE_SYSTEM_KEY,
+ FileSystem.class);
+
+ FileSystem fileSystem = (FileSystem) env.get(FILE_SYSTEM_KEY);
+ if (fileSystems.putIfAbsent(uri, fileSystem) != null) {
+ throw new FileSystemAlreadyExistsException(uri.toString());
+ }
+ return fileSystem;
+ }
+
+ @Override
+ public FileSystem getFileSystem(URI uri) {
+ FileSystem fileSystem = fileSystems.get(uri);
+ if (fileSystem == null) {
+ throw new FileSystemNotFoundException(uri.toString());
+ }
+ return fileSystem;
+ }
+
+ @Override
+ public Path getPath(URI uri) {
+ checkArgument(
+ URI_SCHEME.equalsIgnoreCase(uri.getScheme()),
+ "uri scheme does not match this provider: %s",
+ uri);
+
+ String path = uri.getPath();
+ checkArgument(!isNullOrEmpty(path), "uri must have a path: %s", uri);
+
+ return toPath(getFileSystem(toFileSystemUri(uri)), uri);
+ }
+
+ /**
+ * Returns whether or not the given URI is valid as a base file system URI. It must not have a
+ * path, query or fragment.
+ */
+ private static boolean isValidFileSystemUri(URI uri) {
+ // would like to just check null, but fragment appears to be the empty string when not present
+ return isNullOrEmpty(uri.getPath())
+ && isNullOrEmpty(uri.getQuery())
+ && isNullOrEmpty(uri.getFragment());
+ }
+
+ /** Returns the given URI with any path, query or fragment stripped off. */
+ private static URI toFileSystemUri(URI uri) {
+ try {
+ return new URI(
+ uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Invokes the {@code toPath(URI)} method on the given {@code FileSystem}. */
+ private static Path toPath(FileSystem fileSystem, URI uri) {
+ // We have to invoke this method by reflection because while the file system should be
+ // an instance of JimfsFileSystem, it may be loaded by a different class loader and as
+ // such appear to be a totally different class.
+ try {
+ Method toPath = fileSystem.getClass().getDeclaredMethod("toPath", URI.class);
+ return (Path) toPath.invoke(fileSystem, uri);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("invalid file system: " + fileSystem);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public FileSystem newFileSystem(Path path, Map<String, ?> env) throws IOException {
+ FileSystemProvider realProvider = path.getFileSystem().provider();
+ return realProvider.newFileSystem(path, env);
+ }
+
+ /**
+ * Returns a runnable that, when run, removes the file system with the given URI from this
+ * provider.
+ */
+ @SuppressWarnings("unused") // called via reflection
+ public static Runnable removeFileSystemRunnable(final URI uri) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ fileSystems.remove(uri);
+ }
+ };
+ }
+
+ @Override
+ public SeekableByteChannel newByteChannel(
+ Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DirectoryStream<Path> newDirectoryStream(
+ Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void delete(Path path) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void copy(Path source, Path target, CopyOption... options) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void move(Path source, Path target, CopyOption... options) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSameFile(Path path, Path path2) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isHidden(Path path) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public FileStore getFileStore(Path path) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void checkAccess(Path path, AccessMode... modes) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <V extends FileAttributeView> V getFileAttributeView(
+ Path path, Class<V> type, LinkOption... options) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <A extends BasicFileAttributes> A readAttributes(
+ Path path, Class<A> type, LinkOption... options) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options)
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setAttribute(Path path, String attribute, Object value, LinkOption... options)
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/UnixAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/UnixAttributeProvider.java
new file mode 100644
index 0000000..e314643
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/UnixAttributeProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Attribute provider that provides the "unix" attribute view.
+ *
+ * @author Colin Decker
+ */
+final class UnixAttributeProvider extends AttributeProvider {
+
+ private static final ImmutableSet<String> ATTRIBUTES =
+ ImmutableSet.of("uid", "ino", "dev", "nlink", "rdev", "ctime", "mode", "gid");
+
+ private static final ImmutableSet<String> INHERITED_VIEWS =
+ ImmutableSet.of("basic", "owner", "posix");
+
+ private final AtomicInteger uidGenerator = new AtomicInteger();
+ private final ConcurrentMap<Object, Integer> idCache = new ConcurrentHashMap<>();
+
+ @Override
+ public String name() {
+ return "unix";
+ }
+
+ @Override
+ public ImmutableSet<String> inherits() {
+ return INHERITED_VIEWS;
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ return ATTRIBUTES;
+ }
+
+ @Override
+ public Class<UnixFileAttributeView> viewType() {
+ return UnixFileAttributeView.class;
+ }
+
+ @Override
+ public UnixFileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ // This method should not be called... and it cannot be called through the public APIs in
+ // java.nio.file since there is no public UnixFileAttributeView type.
+ throw new UnsupportedOperationException();
+ }
+
+ // TODO(cgdecker): Since we can now guarantee that the owner/group for an file are our own
+ // implementation of UserPrincipal/GroupPrincipal, it would be nice to have them store a unique
+ // ID themselves and just get that rather than doing caching here. Then this could be a singleton
+ // like the rest of the AttributeProviders. However, that would require a way for the owner/posix
+ // providers to create their default principals using the lookup service for the specific file
+ // system.
+
+ /** Returns an ID that is guaranteed to be the same for any invocation with equal objects. */
+ private Integer getUniqueId(Object object) {
+ Integer id = idCache.get(object);
+ if (id == null) {
+ id = uidGenerator.incrementAndGet();
+ Integer existing = idCache.putIfAbsent(object, id);
+ if (existing != null) {
+ return existing;
+ }
+ }
+ return id;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object get(File file, String attribute) {
+ switch (attribute) {
+ case "uid":
+ UserPrincipal user = (UserPrincipal) file.getAttribute("owner", "owner");
+ return getUniqueId(user);
+ case "gid":
+ GroupPrincipal group = (GroupPrincipal) file.getAttribute("posix", "group");
+ return getUniqueId(group);
+ case "mode":
+ Set<PosixFilePermission> permissions =
+ (Set<PosixFilePermission>) file.getAttribute("posix", "permissions");
+ return toMode(permissions);
+ case "ctime":
+ return FileTime.fromMillis(file.getCreationTime());
+ case "rdev":
+ return 0L;
+ case "dev":
+ return 1L;
+ case "ino":
+ return file.id();
+ case "nlink":
+ return file.links();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ throw unsettable(view, attribute, create);
+ }
+
+ @SuppressWarnings("OctalInteger")
+ private static int toMode(Set<PosixFilePermission> permissions) {
+ int result = 0;
+ for (PosixFilePermission permission : permissions) {
+ checkNotNull(permission);
+ switch (permission) {
+ case OWNER_READ:
+ result |= 0400; // note: octal numbers
+ break;
+ case OWNER_WRITE:
+ result |= 0200;
+ break;
+ case OWNER_EXECUTE:
+ result |= 0100;
+ break;
+ case GROUP_READ:
+ result |= 0040;
+ break;
+ case GROUP_WRITE:
+ result |= 0020;
+ break;
+ case GROUP_EXECUTE:
+ result |= 0010;
+ break;
+ case OTHERS_READ:
+ result |= 0004;
+ break;
+ case OTHERS_WRITE:
+ result |= 0002;
+ break;
+ case OTHERS_EXECUTE:
+ result |= 0001;
+ break;
+ default:
+ throw new AssertionError(); // no other possible values
+ }
+ }
+ return result;
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/UnixFileAttributeView.java b/jimfs/src/main/java/com/google/common/jimfs/UnixFileAttributeView.java
new file mode 100644
index 0000000..9a51f72
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/UnixFileAttributeView.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import java.nio.file.attribute.FileAttributeView;
+
+/**
+ * Dummy view interface for the "unix" view, which doesn't have a public view interface.
+ *
+ * @author Colin Decker
+ */
+interface UnixFileAttributeView extends FileAttributeView {}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/UnixPathType.java b/jimfs/src/main/java/com/google/common/jimfs/UnixPathType.java
new file mode 100644
index 0000000..76f1339
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/UnixPathType.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.nio.file.InvalidPathException;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Unix-style path type.
+ *
+ * @author Colin Decker
+ */
+final class UnixPathType extends PathType {
+
+ /** Unix path type. */
+ static final PathType INSTANCE = new UnixPathType();
+
+ private UnixPathType() {
+ super(false, '/');
+ }
+
+ @Override
+ public ParseResult parsePath(String path) {
+ if (path.isEmpty()) {
+ return emptyPath();
+ }
+
+ checkValid(path);
+
+ String root = path.startsWith("/") ? "/" : null;
+ return new ParseResult(root, splitter().split(path));
+ }
+
+ private static void checkValid(String path) {
+ int nulIndex = path.indexOf('\0');
+ if (nulIndex != -1) {
+ throw new InvalidPathException(path, "nul character not allowed", nulIndex);
+ }
+ }
+
+ @Override
+ public String toString(@NullableDecl String root, Iterable<String> names) {
+ StringBuilder builder = new StringBuilder();
+ if (root != null) {
+ builder.append(root);
+ }
+ joiner().appendTo(builder, names);
+ return builder.toString();
+ }
+
+ @Override
+ public String toUriPath(String root, Iterable<String> names, boolean directory) {
+ StringBuilder builder = new StringBuilder();
+ for (String name : names) {
+ builder.append('/').append(name);
+ }
+
+ if (directory || builder.length() == 0) {
+ builder.append('/');
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public ParseResult parseUriPath(String uriPath) {
+ checkArgument(uriPath.startsWith("/"), "uriPath (%s) must start with /", uriPath);
+ return parsePath(uriPath);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/UserDefinedAttributeProvider.java b/jimfs/src/main/java/com/google/common/jimfs/UserDefinedAttributeProvider.java
new file mode 100644
index 0000000..51cbfa6
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/UserDefinedAttributeProvider.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.UserDefinedFileAttributeView;
+import java.util.List;
+
+/**
+ * Attribute provider that provides the {@link UserDefinedFileAttributeView} ("user"). Unlike most
+ * other attribute providers, this one has no pre-defined set of attributes. Rather, it allows
+ * arbitrary user defined attributes to be set (as {@code ByteBuffer} or {@code byte[]}) and read
+ * (as {@code byte[]}).
+ *
+ * @author Colin Decker
+ */
+final class UserDefinedAttributeProvider extends AttributeProvider {
+
+ UserDefinedAttributeProvider() {}
+
+ @Override
+ public String name() {
+ return "user";
+ }
+
+ @Override
+ public ImmutableSet<String> fixedAttributes() {
+ // no fixed set of attributes for this view
+ return ImmutableSet.of();
+ }
+
+ @Override
+ public boolean supports(String attribute) {
+ // any attribute name is supported
+ return true;
+ }
+
+ @Override
+ public ImmutableSet<String> attributes(File file) {
+ return userDefinedAttributes(file);
+ }
+
+ private static ImmutableSet<String> userDefinedAttributes(File file) {
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ for (String attribute : file.getAttributeNames("user")) {
+ builder.add(attribute);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public Object get(File file, String attribute) {
+ Object value = file.getAttribute("user", attribute);
+ if (value instanceof byte[]) {
+ byte[] bytes = (byte[]) value;
+ return bytes.clone();
+ }
+ return null;
+ }
+
+ @Override
+ public void set(File file, String view, String attribute, Object value, boolean create) {
+ checkNotNull(value);
+ checkNotCreate(view, attribute, create);
+
+ byte[] bytes;
+ if (value instanceof byte[]) {
+ bytes = ((byte[]) value).clone();
+ } else if (value instanceof ByteBuffer) {
+ // value instanceof ByteBuffer
+ ByteBuffer buffer = (ByteBuffer) value;
+ bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ } else {
+ throw invalidType(view, attribute, value, byte[].class, ByteBuffer.class);
+ }
+
+ file.setAttribute("user", attribute, bytes);
+ }
+
+ @Override
+ public Class<UserDefinedFileAttributeView> viewType() {
+ return UserDefinedFileAttributeView.class;
+ }
+
+ @Override
+ public UserDefinedFileAttributeView view(
+ FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+ return new View(lookup);
+ }
+
+ /** Implementation of {@link UserDefinedFileAttributeView}. */
+ private static class View extends AbstractAttributeView implements UserDefinedFileAttributeView {
+
+ public View(FileLookup lookup) {
+ super(lookup);
+ }
+
+ @Override
+ public String name() {
+ return "user";
+ }
+
+ @Override
+ public List<String> list() throws IOException {
+ return userDefinedAttributes(lookupFile()).asList();
+ }
+
+ private byte[] getStoredBytes(String name) throws IOException {
+ byte[] bytes = (byte[]) lookupFile().getAttribute(name(), name);
+ if (bytes == null) {
+ throw new IllegalArgumentException("attribute '" + name() + ":" + name + "' is not set");
+ }
+ return bytes;
+ }
+
+ @Override
+ public int size(String name) throws IOException {
+ return getStoredBytes(name).length;
+ }
+
+ @Override
+ public int read(String name, ByteBuffer dst) throws IOException {
+ byte[] bytes = getStoredBytes(name);
+ dst.put(bytes);
+ return bytes.length;
+ }
+
+ @Override
+ public int write(String name, ByteBuffer src) throws IOException {
+ byte[] bytes = new byte[src.remaining()];
+ src.get(bytes);
+ lookupFile().setAttribute(name(), name, bytes);
+ return bytes.length;
+ }
+
+ @Override
+ public void delete(String name) throws IOException {
+ lookupFile().deleteAttribute(name(), name);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/UserLookupService.java b/jimfs/src/main/java/com/google/common/jimfs/UserLookupService.java
new file mode 100644
index 0000000..419d71f
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/UserLookupService.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.IOException;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.nio.file.attribute.UserPrincipalNotFoundException;
+
+/**
+ * {@link UserPrincipalLookupService} implementation.
+ *
+ * @author Colin Decker
+ */
+final class UserLookupService extends UserPrincipalLookupService {
+
+ private final boolean supportsGroups;
+
+ public UserLookupService(boolean supportsGroups) {
+ this.supportsGroups = supportsGroups;
+ }
+
+ @Override
+ public UserPrincipal lookupPrincipalByName(String name) {
+ return createUserPrincipal(name);
+ }
+
+ @Override
+ public GroupPrincipal lookupPrincipalByGroupName(String group) throws IOException {
+ if (!supportsGroups) {
+ throw new UserPrincipalNotFoundException(group); // required by spec
+ }
+ return createGroupPrincipal(group);
+ }
+
+ /** Creates a {@link UserPrincipal} for the given user name. */
+ static UserPrincipal createUserPrincipal(String name) {
+ return new JimfsUserPrincipal(name);
+ }
+
+ /** Creates a {@link GroupPrincipal} for the given group name. */
+ static GroupPrincipal createGroupPrincipal(String name) {
+ return new JimfsGroupPrincipal(name);
+ }
+
+ /** Base class for {@link UserPrincipal} and {@link GroupPrincipal} implementations. */
+ private abstract static class NamedPrincipal implements UserPrincipal {
+
+ protected final String name;
+
+ private NamedPrincipal(String name) {
+ this.name = checkNotNull(name);
+ }
+
+ @Override
+ public final String getName() {
+ return name;
+ }
+
+ @Override
+ public final int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public final String toString() {
+ return name;
+ }
+ }
+
+ /** {@link UserPrincipal} implementation. */
+ static final class JimfsUserPrincipal extends NamedPrincipal {
+
+ private JimfsUserPrincipal(String name) {
+ super(name);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof JimfsUserPrincipal
+ && getName().equals(((JimfsUserPrincipal) obj).getName());
+ }
+ }
+
+ /** {@link GroupPrincipal} implementation. */
+ static final class JimfsGroupPrincipal extends NamedPrincipal implements GroupPrincipal {
+
+ private JimfsGroupPrincipal(String name) {
+ super(name);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof JimfsGroupPrincipal && ((JimfsGroupPrincipal) obj).name.equals(name);
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/Util.java b/jimfs/src/main/java/com/google/common/jimfs/Util.java
new file mode 100644
index 0000000..3d1ec5c
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/Util.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableCollection;
+
+/**
+ * Miscellaneous static utility methods.
+ *
+ * @author Colin Decker
+ * @author Austin Appleby
+ */
+final class Util {
+
+ private Util() {}
+
+ /** Returns the next power of 2 >= n. */
+ public static int nextPowerOf2(int n) {
+ if (n == 0) {
+ return 1;
+ }
+ int b = Integer.highestOneBit(n);
+ return b == n ? n : b << 1;
+ }
+
+ /**
+ * Checks that the given number is not negative, throwing IAE if it is. The given description
+ * describes the number in the exception message.
+ */
+ static void checkNotNegative(long n, String description) {
+ checkArgument(n >= 0, "%s must not be negative: %s", description, n);
+ }
+
+ /** Checks that no element in the given iterable is null, throwing NPE if any is. */
+ static void checkNoneNull(Iterable<?> objects) {
+ if (!(objects instanceof ImmutableCollection)) {
+ for (Object o : objects) {
+ checkNotNull(o);
+ }
+ }
+ }
+
+ private static final int C1 = 0xcc9e2d51;
+ private static final int C2 = 0x1b873593;
+
+ /*
+ * This method was rewritten in Java from an intermediate step of the Murmur hash function in
+ * http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp, which contained the
+ * following header:
+ *
+ * MurmurHash3 was written by Austin Appleby, and is placed in the public domain. The author
+ * hereby disclaims copyright to this source code.
+ */
+ static int smearHash(int hashCode) {
+ return C2 * Integer.rotateLeft(hashCode * C1, 15);
+ }
+
+ private static final int ARRAY_LEN = 8192;
+ private static final byte[] ZERO_ARRAY = new byte[ARRAY_LEN];
+ private static final byte[][] NULL_ARRAY = new byte[ARRAY_LEN][];
+
+ /** Zeroes all bytes between off (inclusive) and off + len (exclusive) in the given array. */
+ static void zero(byte[] bytes, int off, int len) {
+ // this is significantly faster than looping or Arrays.fill (which loops), particularly when
+ // the length of the slice to be zeroed is <= to ARRAY_LEN (in that case, it's faster by a
+ // factor of 2)
+ int remaining = len;
+ while (remaining > ARRAY_LEN) {
+ System.arraycopy(ZERO_ARRAY, 0, bytes, off, ARRAY_LEN);
+ off += ARRAY_LEN;
+ remaining -= ARRAY_LEN;
+ }
+
+ System.arraycopy(ZERO_ARRAY, 0, bytes, off, remaining);
+ }
+
+ /**
+ * Clears (sets to null) all blocks between off (inclusive) and off + len (exclusive) in the given
+ * array.
+ */
+ static void clear(byte[][] blocks, int off, int len) {
+ // this is significantly faster than looping or Arrays.fill (which loops), particularly when
+ // the length of the slice to be cleared is <= to ARRAY_LEN (in that case, it's faster by a
+ // factor of 2)
+ int remaining = len;
+ while (remaining > ARRAY_LEN) {
+ System.arraycopy(NULL_ARRAY, 0, blocks, off, ARRAY_LEN);
+ off += ARRAY_LEN;
+ remaining -= ARRAY_LEN;
+ }
+
+ System.arraycopy(NULL_ARRAY, 0, blocks, off, remaining);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java b/jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java
new file mode 100644
index 0000000..5a28627
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.nio.file.WatchService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Configuration for the {@link WatchService} implementation used by a file system.
+ *
+ * @author Colin Decker
+ * @since 1.1
+ */
+public abstract class WatchServiceConfiguration {
+
+ /** The default configuration that's used if the user doesn't provide anything more specific. */
+ static final WatchServiceConfiguration DEFAULT = polling(5, SECONDS);
+
+ /**
+ * Returns a configuration for a {@link WatchService} that polls watched directories for changes
+ * every {@code interval} of the given {@code timeUnit} (e.g. every 5 {@link TimeUnit#SECONDS
+ * seconds}).
+ */
+ @SuppressWarnings("GoodTime") // should accept a java.time.Duration
+ public static WatchServiceConfiguration polling(long interval, TimeUnit timeUnit) {
+ return new PollingConfig(interval, timeUnit);
+ }
+
+ WatchServiceConfiguration() {}
+
+ /** Creates a new {@link AbstractWatchService} implementation. */
+ // return type and parameters of this method subject to change if needed for any future
+ // implementations
+ abstract AbstractWatchService newWatchService(FileSystemView view, PathService pathService);
+
+ /** Implementation for {@link #polling}. */
+ private static final class PollingConfig extends WatchServiceConfiguration {
+
+ private final long interval;
+ private final TimeUnit timeUnit;
+
+ private PollingConfig(long interval, TimeUnit timeUnit) {
+ checkArgument(interval > 0, "interval (%s) must be positive", interval);
+ this.interval = interval;
+ this.timeUnit = checkNotNull(timeUnit);
+ }
+
+ @Override
+ AbstractWatchService newWatchService(FileSystemView view, PathService pathService) {
+ return new PollingWatchService(view, pathService, view.state(), interval, timeUnit);
+ }
+
+ @Override
+ public String toString() {
+ return "WatchServiceConfiguration.polling(" + interval + ", " + timeUnit + ")";
+ }
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/WindowsPathType.java b/jimfs/src/main/java/com/google/common/jimfs/WindowsPathType.java
new file mode 100644
index 0000000..7cdf0c4
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/WindowsPathType.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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.google.common.jimfs;
+
+import java.nio.file.InvalidPathException;
+import java.util.Iterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Windows-style path type.
+ *
+ * @author Colin Decker
+ */
+final class WindowsPathType extends PathType {
+
+ /** Windows path type. */
+ static final WindowsPathType INSTANCE = new WindowsPathType();
+
+ /**
+ * Matches the C:foo\bar path format, which has a root (C:) and names (foo\bar) and matches a path
+ * relative to the working directory on that drive. Currently can't support that format as it
+ * requires behavior that differs completely from Unix.
+ */
+ // TODO(cgdecker): Can probably support this at some point
+ // It would require:
+ // - A method like PathType.isAbsolute(Path) or something to that effect; this would allow
+ // WindowsPathType to distinguish between an absolute root path (C:\) and a relative root
+ // path (C:)
+ // - Special handling for relative paths that have a root. This handling would determine the
+ // root directory and then determine the working directory from there. The file system would
+ // still have one working directory; for the root that working directory is under, it is the
+ // working directory. For every other root, the root itself is the working directory.
+ private static final Pattern WORKING_DIR_WITH_DRIVE = Pattern.compile("^[a-zA-Z]:([^\\\\].*)?$");
+
+ /** Pattern for matching trailing spaces in file names. */
+ private static final Pattern TRAILING_SPACES = Pattern.compile("[ ]+(\\\\|$)");
+
+ private WindowsPathType() {
+ super(true, '\\', '/');
+ }
+
+ @Override
+ public ParseResult parsePath(String path) {
+ String original = path;
+ path = path.replace('/', '\\');
+
+ if (WORKING_DIR_WITH_DRIVE.matcher(path).matches()) {
+ throw new InvalidPathException(
+ original,
+ "Jimfs does not currently support the Windows syntax for a relative path "
+ + "on a specific drive (e.g. \"C:foo\\bar\")");
+ }
+
+ String root;
+ if (path.startsWith("\\\\")) {
+ root = parseUncRoot(path, original);
+ } else if (path.startsWith("\\")) {
+ throw new InvalidPathException(
+ original,
+ "Jimfs does not currently support the Windows syntax for an absolute path "
+ + "on the current drive (e.g. \"\\foo\\bar\")");
+ } else {
+ root = parseDriveRoot(path);
+ }
+
+ // check for root.length() > 3 because only "C:\" type roots are allowed to have :
+ int startIndex = root == null || root.length() > 3 ? 0 : root.length();
+ for (int i = startIndex; i < path.length(); i++) {
+ char c = path.charAt(i);
+ if (isReserved(c)) {
+ throw new InvalidPathException(original, "Illegal char <" + c + ">", i);
+ }
+ }
+
+ Matcher trailingSpaceMatcher = TRAILING_SPACES.matcher(path);
+ if (trailingSpaceMatcher.find()) {
+ throw new InvalidPathException(original, "Trailing char < >", trailingSpaceMatcher.start());
+ }
+
+ if (root != null) {
+ path = path.substring(root.length());
+
+ if (!root.endsWith("\\")) {
+ root = root + "\\";
+ }
+ }
+
+ return new ParseResult(root, splitter().split(path));
+ }
+
+ /** Pattern for matching UNC \\host\share root syntax. */
+ private static final Pattern UNC_ROOT = Pattern.compile("^(\\\\\\\\)([^\\\\]+)?(\\\\[^\\\\]+)?");
+
+ /**
+ * Parse the root of a UNC-style path, throwing an exception if the path does not start with a
+ * valid UNC root.
+ */
+ private String parseUncRoot(String path, String original) {
+ Matcher uncMatcher = UNC_ROOT.matcher(path);
+ if (uncMatcher.find()) {
+ String host = uncMatcher.group(2);
+ if (host == null) {
+ throw new InvalidPathException(original, "UNC path is missing hostname");
+ }
+ String share = uncMatcher.group(3);
+ if (share == null) {
+ throw new InvalidPathException(original, "UNC path is missing sharename");
+ }
+
+ return path.substring(uncMatcher.start(), uncMatcher.end());
+ } else {
+ // probably shouldn't ever reach this
+ throw new InvalidPathException(original, "Invalid UNC path");
+ }
+ }
+
+ /** Pattern for matching normal C:\ drive letter root syntax. */
+ private static final Pattern DRIVE_LETTER_ROOT = Pattern.compile("^[a-zA-Z]:\\\\");
+
+ /** Parses a normal drive-letter root, e.g. "C:\". */
+ @NullableDecl
+ private String parseDriveRoot(String path) {
+ Matcher drivePathMatcher = DRIVE_LETTER_ROOT.matcher(path);
+ if (drivePathMatcher.find()) {
+ return path.substring(drivePathMatcher.start(), drivePathMatcher.end());
+ }
+ return null;
+ }
+
+ /** Checks if c is one of the reserved characters that aren't allowed in Windows file names. */
+ private static boolean isReserved(char c) {
+ switch (c) {
+ case '<':
+ case '>':
+ case ':':
+ case '"':
+ case '|':
+ case '?':
+ case '*':
+ return true;
+ default:
+ return c <= 31;
+ }
+ }
+
+ @Override
+ public String toString(@NullableDecl String root, Iterable<String> names) {
+ StringBuilder builder = new StringBuilder();
+ if (root != null) {
+ builder.append(root);
+ }
+ joiner().appendTo(builder, names);
+ return builder.toString();
+ }
+
+ @Override
+ public String toUriPath(String root, Iterable<String> names, boolean directory) {
+ if (root.startsWith("\\\\")) {
+ root = root.replace('\\', '/');
+ } else {
+ root = "/" + root.replace('\\', '/');
+ }
+
+ StringBuilder builder = new StringBuilder();
+ builder.append(root);
+
+ Iterator<String> iter = names.iterator();
+ if (iter.hasNext()) {
+ builder.append(iter.next());
+ while (iter.hasNext()) {
+ builder.append('/').append(iter.next());
+ }
+ }
+
+ if (directory && builder.charAt(builder.length() - 1) != '/') {
+ builder.append('/');
+ }
+
+ return builder.toString();
+ }
+
+ @Override
+ public ParseResult parseUriPath(String uriPath) {
+ uriPath = uriPath.replace('/', '\\');
+ if (uriPath.charAt(0) == '\\' && uriPath.charAt(1) != '\\') {
+ // non-UNC path, so the leading / was just there for the URI path format and isn't part
+ // of what should be parsed
+ uriPath = uriPath.substring(1);
+ }
+ return parsePath(uriPath);
+ }
+}
diff --git a/jimfs/src/main/java/com/google/common/jimfs/package-info.java b/jimfs/src/main/java/com/google/common/jimfs/package-info.java
new file mode 100644
index 0000000..47a75b0
--- /dev/null
+++ b/jimfs/src/main/java/com/google/common/jimfs/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * 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 containing the Jimfs file system API and implementation. Most users should only need to
+ * use the {@link com.google.common.jimfs.Jimfs Jimfs} and {@link
+ * com.google.common.jimfs.Configuration Configuration} classes.
+ */
+@ParametersAreNonnullByDefault
+package com.google.common.jimfs;
+
+import javax.annotation.ParametersAreNonnullByDefault;