Initial merge with upstream

Test: n/a
Bug: 150784654
Change-Id: I6fb223f1bd657a6a3d0be1492f63a7774e21943e
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..83c26b8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.idea/
+*.ims
+*.iml
+
+.classpath
+.project
+.settings/
+
+target/
+
+bin/
+out/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..0923403
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,31 @@
+sudo: false
+
+language: java
+
+jdk:
+  - openjdk8
+  - openjdk11
+
+install: mvn install -U -DskipTests=true
+
+script: mvn verify -U -Dmaven.javadoc.skip=true
+
+after_success:
+  - util/deploy_snapshot.sh
+  - util/update_snapshot_docs.sh
+
+cache:
+  directories:
+  - $HOME/.m2
+
+env:
+  global:
+    - secure: "YlCxYTG64KLbyyD2tvA7LwCrNMDCxBigClh8enVicY2Rw6EN9ZTE1YYZivsXAN42YtI1snpy4fTn1z42KUx6FhrlkXVnhLi9TO1lz1lVL4czhqj8MGew20+DJs7tlw3xWRJlRVhqGIXFfximqBsYskm7/+qnHga6uyyV59/VwEI="
+    - secure: "bTcwsovwxPXplZysfwgNkTR3hfHjb7UvWMlxeEkHHt3GQiZxIDKkiJbgW2mHAG/e/H0wfKQyujeCgQwxn1fa5ttR+UbGz+TIIY2tgjpIFkSbBRzlNGOO0Y23wQpFXXUv3lAY//cV1pa0HlCz+IWNq7ZqPZAoReDAkxExbbmydtE="
+    - secure: "JZnVEfpNSCLBZQg1MP7MuhzP9H8t2gGUU4salm5VsRKck27fgg1HwBxADolcVeON2k+2masSKLEQPkeYQizc/VN5hZsCZpTgYjuMke1ZLe1v0KsIdH3Rdt77fhhTqiT1BEkMV8tlBwiraYZz+41iLo+Ug5yjgfmXXayDjYm4h4w="
+
+
+branches:
+  only:
+    - master
+    - /^release.*$/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..58f5047
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,15 @@
+Contributing to Jimfs
+=====================
+
+Contributions to Jimfs can be made by forking the repostitory and sending
+a pull request. Before we can merge any pull requests from you, you must
+first sign the Google [Contributor License Agreement][1].
+
+When making changes to the code, please try to stay consistent with the
+style of the existing code, specified by the [Google Java Style Guide][2].
+Please also ensure that the code compiles and that changes have appropriate
+tests.
+
+[1]: https://developers.google.com/open-source/cla/individual
+[2]: https://google.github.io/styleguide/javaguide.html
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
\ No newline at end of file
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..5e2f5a7
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,22 @@
+name: "Jimfs"
+description:
+    "Jimfs is an in-memory file system for Java 7 and above, implementing the "
+    "java.nio.file abstract file system APIs. "
+    "Note that it may not directly mimic all file system behavior (e.g. SELinux "
+    "access control, emulated storage, etc.). The initial intention to add this "
+    "project to Android is to fake host side disk access in unit tests, and not "
+    "the device itself."
+
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://github.com/google/jimfs"
+  }
+  url {
+    type: GIT
+    value: "https://github.com/google/jimfs.git"
+  }
+  version: "v1.1"
+  last_upgrade_date { year: 2019 month: 12 day: 18 }
+  license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 120000
index 0000000..7a694c9
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+LICENSE
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d975958
--- /dev/null
+++ b/README.md
@@ -0,0 +1,86 @@
+Jimfs
+=====
+
+Jimfs is an in-memory file system for Java 7 and above, implementing the
+[java.nio.file](http://docs.oracle.com/javase/7/docs/api/java/nio/file/package-summary.html)
+abstract file system APIs.
+
+[![Build Status](https://travis-ci.org/google/jimfs.svg?branch=master)](https://travis-ci.org/google/jimfs)
+[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.google.jimfs/jimfs/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.google.jimfs/jimfs)
+
+Getting started
+---------------
+
+The latest release is [1.1](https://github.com/google/jimfs/releases/tag/v1.1).
+
+It is available in Maven Central as
+[com.google.jimfs:jimfs:1.1](http://search.maven.org/#artifactdetails%7Ccom.google.jimfs%7Cjimfs%7C1.1%7Cjar):
+
+```xml
+<dependency>
+  <groupId>com.google.jimfs</groupId>
+  <artifactId>jimfs</artifactId>
+  <version>1.1</version>
+</dependency>
+```
+
+Basic use
+---------
+
+The simplest way to use Jimfs is to just get a new `FileSystem` instance from the `Jimfs` class and
+start using it:
+
+```java
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+...
+
+// For a simple file system with Unix-style paths and behavior:
+FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+Path foo = fs.getPath("/foo");
+Files.createDirectory(foo);
+
+Path hello = foo.resolve("hello.txt"); // /foo/hello.txt
+Files.write(hello, ImmutableList.of("hello world"), StandardCharsets.UTF_8);
+```
+
+What's supported?
+-----------------
+
+Jimfs supports almost all the APIs under `java.nio.file`. It supports:
+
+- Creating, deleting, moving and copying files and directories.
+- Reading and writing files with `FileChannel` or `SeekableByteChannel`, `InputStream`,
+  `OutputStream`, etc.
+- Symbolic links.
+- Hard links to regular files.
+- `SecureDirectoryStream`, for operations relative to an _open_ directory.
+- Glob and regex path filtering with `PathMatcher`.
+- Watching for changes to a directory with a `WatchService`.
+- File attributes. Built-in attribute views that can be supported include "basic", "owner",
+  "posix", "unix", "dos", "acl" and "user". Do note, however, that not all attribute views provide
+  _useful_ attributes. For example, while setting and reading POSIX file permissions is possible
+  with the "posix" view, those permissions will not actually affect the behavior of the file system.
+
+Jimfs also supports creating file systems that, for example, use Windows-style paths and (to an
+extent) behavior. In general, however, file system behavior is modeled after UNIX and may not
+exactly match any particular real file system or platform.
+
+License
+-------
+
+```
+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.
+```
diff --git a/jimfs/pom.xml b/jimfs/pom.xml
new file mode 100644
index 0000000..d4b5ba1
--- /dev/null
+++ b/jimfs/pom.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.jimfs</groupId>
+    <artifactId>jimfs-parent</artifactId>
+    <version>HEAD-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>jimfs</artifactId>
+
+  <packaging>bundle</packaging>
+
+  <name>Jimfs</name>
+
+  <description>
+    Jimfs is an in-memory implementation of Java 7's java.nio.file abstract file system API.
+  </description>
+
+  <dependencies>
+    <!-- Required runtime dependencies -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <!-- Optional runtime dependencies -->
+    <dependency>
+      <groupId>com.ibm.icu</groupId>
+      <artifactId>icu4j</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <!-- Compile-time dependencies -->
+    <dependency>
+      <groupId>com.google.auto.service</groupId>
+      <artifactId>auto-service-annotations</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.checkerframework</groupId>
+      <artifactId>checker-compat-qual</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava-testlib</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <phase>post-integration-test</phase>
+            <goals>
+              <goal>jar-no-fork</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <configuration>
+          <excludePackageNames>com.google.jimfs.internal</excludePackageNames>
+        </configuration>
+        <executions>
+          <execution>
+            <id>attach-docs</id>
+            <phase>post-integration-test</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Export-Package>com.google.common.jimfs.*</Export-Package>
+            <Include-Resource>
+              META-INF/services=target/classes/META-INF/services
+            </Include-Resource>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
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;
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AbstractAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/AbstractAttributeProviderTest.java
new file mode 100644
index 0000000..7e2bdf9
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AbstractAttributeProviderTest.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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Before;
+
+/**
+ * Abstract base class for tests of individual {@link AttributeProvider} implementations.
+ *
+ * @author Colin Decker
+ */
+public abstract class AbstractAttributeProviderTest<P extends AttributeProvider> {
+
+  protected static final ImmutableMap<String, FileAttributeView> NO_INHERITED_VIEWS =
+      ImmutableMap.of();
+
+  protected P provider;
+  protected File file;
+
+  /** Create the provider being tested. */
+  protected abstract P createProvider();
+
+  /** Creates the set of providers the provider being tested depends on. */
+  protected abstract Set<? extends AttributeProvider> createInheritedProviders();
+
+  protected FileLookup fileLookup() {
+    return new FileLookup() {
+      @Override
+      public File lookup() throws IOException {
+        return file;
+      }
+    };
+  }
+
+  @Before
+  public void setUp() {
+    this.provider = createProvider();
+    this.file = Directory.create(0);
+
+    Map<String, ?> defaultValues = createDefaultValues();
+    setDefaultValues(file, provider, defaultValues);
+
+    Set<? extends AttributeProvider> inheritedProviders = createInheritedProviders();
+    for (AttributeProvider inherited : inheritedProviders) {
+      setDefaultValues(file, inherited, defaultValues);
+    }
+  }
+
+  private static void setDefaultValues(
+      File file, AttributeProvider provider, Map<String, ?> defaultValues) {
+    Map<String, ?> defaults = provider.defaultValues(defaultValues);
+    for (Map.Entry<String, ?> entry : defaults.entrySet()) {
+      int separatorIndex = entry.getKey().indexOf(':');
+      String view = entry.getKey().substring(0, separatorIndex);
+      String attr = entry.getKey().substring(separatorIndex + 1);
+      file.setAttribute(view, attr, entry.getValue());
+    }
+  }
+
+  protected Map<String, ?> createDefaultValues() {
+    return ImmutableMap.of();
+  }
+
+  // assertions
+
+  protected void assertSupportsAll(String... attributes) {
+    for (String attribute : attributes) {
+      assertThat(provider.supports(attribute)).isTrue();
+    }
+  }
+
+  protected void assertContainsAll(File file, ImmutableMap<String, Object> expectedAttributes) {
+    for (Map.Entry<String, Object> entry : expectedAttributes.entrySet()) {
+      String attribute = entry.getKey();
+      Object value = entry.getValue();
+
+      assertThat(provider.get(file, attribute)).isEqualTo(value);
+    }
+  }
+
+  protected void assertSetAndGetSucceeds(String attribute, Object value) {
+    assertSetAndGetSucceeds(attribute, value, false);
+  }
+
+  protected void assertSetAndGetSucceeds(String attribute, Object value, boolean create) {
+    provider.set(file, provider.name(), attribute, value, create);
+    assertThat(provider.get(file, attribute)).isEqualTo(value);
+  }
+
+  protected void assertSetAndGetSucceedsOnCreate(String attribute, Object value) {
+    assertSetAndGetSucceeds(attribute, value, true);
+  }
+
+  @SuppressWarnings("EmptyCatchBlock")
+  protected void assertSetFails(String attribute, Object value) {
+    try {
+      provider.set(file, provider.name(), attribute, value, false);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @SuppressWarnings("EmptyCatchBlock")
+  protected void assertSetFailsOnCreate(String attribute, Object value) {
+    try {
+      provider.set(file, provider.name(), attribute, value, true);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AbstractGlobMatcherTest.java b/jimfs/src/test/java/com/google/common/jimfs/AbstractGlobMatcherTest.java
new file mode 100644
index 0000000..57936e1
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AbstractGlobMatcherTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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 org.junit.Test;
+
+/** @author Colin Decker */
+public abstract class AbstractGlobMatcherTest extends AbstractPathMatcherTest {
+
+  @Test
+  public void testMatching_literal() {
+    assertThat("foo").matches("foo");
+    assertThat("/foo").matches("/foo");
+    assertThat("/foo/bar/baz").matches("/foo/bar/baz");
+  }
+
+  @Test
+  public void testMatching_questionMark() {
+    assertThat("?").matches("a", "A", "$", "5", "_").doesNotMatch("/", "ab", "");
+    assertThat("??").matches("ab");
+    assertThat("????").matches("1234");
+    assertThat("?oo?").matches("book", "doom").doesNotMatch("/oom");
+    assertThat("/?oo/ba?").matches("/foo/bar");
+    assertThat("foo.?").matches("foo.h");
+    assertThat("foo.??").matches("foo.cc");
+  }
+
+  @Test
+  public void testMatching_star() {
+    assertThat("*")
+        .matches("a", "abc", "298347829473928423", "abc12345", "")
+        .doesNotMatch("/", "/abc");
+    assertThat("/*").matches("/a", "/abcd", "/abc123", "/").doesNotMatch("/foo/bar");
+    assertThat("/*/*/*")
+        .matches("/a/b/c", "/foo/bar/baz")
+        .doesNotMatch("/foo/bar", "/foo/bar/baz/abc");
+    assertThat("/*/bar").matches("/foo/bar", "/abc/bar").doesNotMatch("/bar");
+    assertThat("/foo/*")
+        .matches("/foo/bar", "/foo/baz")
+        .doesNotMatch("/foo", "foo/bar", "/foo/bar/baz");
+    assertThat("/foo*/ba*")
+        .matches("/food/bar", "/fool/bat", "/foo/ba", "/foot/ba", "/foo/bar", "/foods/bartender")
+        .doesNotMatch("/food/baz/bar");
+    assertThat("*.java")
+        .matches("Foo.java", "Bar.java", "GlobPatternTest.java", "Foo.java.java", ".java")
+        .doesNotMatch("Foo.jav", "Foo", "java.Foo", "Foo.java.");
+    assertThat("Foo.*")
+        .matches("Foo.java", "Foo.txt", "Foo.tar.gz", "Foo.Foo.", "Foo.")
+        .doesNotMatch("Foo", ".Foo");
+    assertThat("*/*.java").matches("foo/Bar.java", "foo/.java");
+    assertThat("*/Bar.*").matches("foo/Bar.java");
+    assertThat(".*").matches(".bashrc", ".bash_profile");
+    assertThat("*.............").matches(
+        "............a............a..............a.............a............a.........." +
+        ".........................................................a....................");
+    assertThat("*.............*..").matches(
+        "............a............a..............a.............a............a.........." +
+        "..........a...................................................................");
+    assertThat(".................*........*.*.....*....................*..............*").matches(
+        ".................................abc.........................................." +
+        ".............................................................................." +
+        ".............................................................................." +
+        ".............................................12..............................." +
+        ".........................................................................hello" +
+        "..............................................................................");
+  }
+
+  @Test
+  public void testMatching_starStar() {
+    assertThat("**")
+        .matches("", "a", "abc", "293874982374913794141", "/foo/bar/baz", "foo/bar.txt");
+    assertThat("**foo")
+        .matches("foo", "barfoo", "/foo", "/a/b/c/foo", "c.foo", "a/b/c.foo")
+        .doesNotMatch("foo.bar", "/a/b/food");
+    assertThat("/foo/**/bar.txt")
+        .matches("/foo/baz/bar.txt", "/foo/bar/asdf/bar.txt")
+        .doesNotMatch("/foo/bar.txt", "/foo/baz/bar");
+    assertThat("**/*.java").matches("/Foo.java", "foo/Bar.java", "/.java", "foo/.java");
+  }
+
+  @Test
+  public void testMatching_brackets() {
+    assertThat("[ab]").matches("a", "b").doesNotMatch("ab", "ba", "aa", "bb", "c", "", "/");
+    assertThat("[a-d]")
+        .matches("a", "b", "c", "d")
+        .doesNotMatch("e", "f", "z", "aa", "ab", "abcd", "", "/");
+    assertThat("[a-dz]")
+        .matches("a", "b", "c", "d", "z")
+        .doesNotMatch("e", "f", "aa", "ab", "dz", "", "/");
+    assertThat("[!b]").matches("a", "c", "d", "0", "!", "$").doesNotMatch("b", "/", "", "ac");
+    assertThat("[!b-d3]")
+        .matches("a", "e", "f", "0", "1", "2", "4")
+        .doesNotMatch("b", "c", "d", "3");
+    assertThat("[-]").matches("-");
+    assertThat("[-a-c]").matches("-", "a", "b", "c");
+    assertThat("[!-a-c]").matches("d", "e", "0").doesNotMatch("a", "b", "c", "-");
+    assertThat("[\\d]").matches("\\", "d").doesNotMatch("0", "1");
+    assertThat("[\\s]").matches("\\", "s").doesNotMatch(" ");
+    assertThat("[\\]").matches("\\").doesNotMatch("]");
+  }
+
+  @Test
+  public void testMatching_curlyBraces() {
+    assertThat("{a,b}").matches("a", "b").doesNotMatch("/", "c", "0", "", ",", "{", "}");
+    assertThat("{ab,cd}").matches("ab", "cd").doesNotMatch("bc", "ac", "ad", "ba", "dc", ",");
+    assertThat(".{h,cc}").matches(".h", ".cc").doesNotMatch("h", "cc");
+    assertThat("{?oo,ba?}").matches("foo", "boo", "moo", "bat", "bar", "baz");
+    assertThat("{[Ff]oo*,[Bb]a*,[A-Ca-c]*/[!z]*.txt}")
+        .matches("foo", "Foo", "fools", "ba", "Ba", "bar", "Bar", "Bart", "c/y.txt", "Cat/foo.txt")
+        .doesNotMatch("Cat", "Cat/foo", "blah", "bAr", "c/z.txt", "c/.txt", "*");
+  }
+
+  @Test
+  public void testMatching_escapes() {
+    assertThat("\\\\").matches("\\");
+    assertThat("\\*").matches("*");
+    assertThat("\\*\\*").matches("**");
+    assertThat("\\[").matches("[");
+    assertThat("\\{").matches("{");
+    assertThat("\\a").matches("a");
+    assertThat("{a,\\}}").matches("a", "}");
+    assertThat("{a\\,,b}").matches("a,", "b").doesNotMatch("a", ",");
+  }
+
+  @Test
+  public void testMatching_various() {
+    assertThat("**/[A-Z]*.{[Jj][Aa][Vv][Aa],[Tt][Xx][Tt]}")
+        .matches("/foo/bar/Baz.java", "/A.java", "bar/Test.JAVA", "foo/Foo.tXt");
+  }
+
+  @Test
+  public void testInvalidSyntax() {
+    assertSyntaxError("\\");
+    assertSyntaxError("[");
+    assertSyntaxError("[]");
+    assertSyntaxError("{");
+    assertSyntaxError("{{}");
+    assertSyntaxError("{a,b,a{b,c},d}");
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AbstractJimfsIntegrationTest.java b/jimfs/src/test/java/com/google/common/jimfs/AbstractJimfsIntegrationTest.java
new file mode 100644
index 0000000..11a0944
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AbstractJimfsIntegrationTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.jimfs.PathSubject.paths;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import java.io.IOException;
+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.FileTime;
+import org.junit.After;
+import org.junit.Before;
+
+/** @author Colin Decker */
+public abstract class AbstractJimfsIntegrationTest {
+
+  protected FileSystem fs;
+
+  @Before
+  public void setUp() throws IOException {
+    fs = createFileSystem();
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    fs.close();
+  }
+
+  /** Creates the file system to use in the tests. */
+  protected abstract FileSystem createFileSystem();
+
+  // helpers
+
+  protected Path path(String first, String... more) {
+    return fs.getPath(first, more);
+  }
+
+  protected Object getFileKey(String path, LinkOption... options) throws IOException {
+    return Files.getAttribute(path(path), "fileKey", options);
+  }
+
+  protected PathSubject assertThatPath(String path, LinkOption... options) {
+    return assertThatPath(path(path), options);
+  }
+
+  protected static PathSubject assertThatPath(Path path, LinkOption... options) {
+    PathSubject subject = assert_().about(paths()).that(path);
+    if (options.length != 0) {
+      subject = subject.noFollowLinks();
+    }
+    return subject;
+  }
+
+  /** Tester for testing changes in file times. */
+  protected static final class FileTimeTester {
+
+    private final Path path;
+
+    private FileTime accessTime;
+    private FileTime modifiedTime;
+
+    FileTimeTester(Path path) throws IOException {
+      this.path = path;
+
+      BasicFileAttributes attrs = attrs();
+      accessTime = attrs.lastAccessTime();
+      modifiedTime = attrs.lastModifiedTime();
+    }
+
+    private BasicFileAttributes attrs() throws IOException {
+      return Files.readAttributes(path, BasicFileAttributes.class);
+    }
+
+    public void assertAccessTimeChanged() throws IOException {
+      FileTime t = attrs().lastAccessTime();
+      assertThat(t).isNotEqualTo(accessTime);
+      accessTime = t;
+    }
+
+    public void assertAccessTimeDidNotChange() throws IOException {
+      FileTime t = attrs().lastAccessTime();
+      assertThat(t).isEqualTo(accessTime);
+    }
+
+    public void assertModifiedTimeChanged() throws IOException {
+      FileTime t = attrs().lastModifiedTime();
+      assertThat(t).isNotEqualTo(modifiedTime);
+      modifiedTime = t;
+    }
+
+    public void assertModifiedTimeDidNotChange() throws IOException {
+      FileTime t = attrs().lastModifiedTime();
+      assertThat(t).isEqualTo(modifiedTime);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AbstractPathMatcherTest.java b/jimfs/src/test/java/com/google/common/jimfs/AbstractPathMatcherTest.java
new file mode 100644
index 0000000..70ac0e9
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AbstractPathMatcherTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+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.PathMatcher;
+import java.nio.file.Paths;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Iterator;
+import java.util.regex.PatternSyntaxException;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Abstract base class for tests of {@link PathMatcher} implementations.
+ *
+ * @author Colin Decker
+ */
+public abstract class AbstractPathMatcherTest {
+
+  /**
+   * Creates a new {@code PathMatcher} using the given pattern in the syntax this test is testing.
+   */
+  protected abstract PathMatcher matcher(String pattern);
+
+  /** Override to return a real matcher for the given pattern. */
+  @NullableDecl
+  protected PathMatcher realMatcher(String pattern) {
+    return null;
+  }
+
+  protected void assertSyntaxError(String pattern) {
+    try {
+      matcher(pattern);
+      fail();
+    } catch (PatternSyntaxException expected) {
+    }
+
+    try {
+      PathMatcher real = realMatcher(pattern);
+      if (real != null) {
+        fail();
+      }
+    } catch (PatternSyntaxException expected) {
+    }
+  }
+
+  protected final PatternAsserter assertThat(String pattern) {
+    return new PatternAsserter(pattern);
+  }
+
+  protected final class PatternAsserter {
+
+    private final PathMatcher matcher;
+
+    @NullableDecl private final PathMatcher realMatcher;
+
+    PatternAsserter(String pattern) {
+      this.matcher = matcher(pattern);
+      this.realMatcher = realMatcher(pattern);
+    }
+
+    PatternAsserter matches(String... paths) {
+      for (String path : paths) {
+        assertTrue(
+            "matcher '" + matcher + "' did not match '" + path + "'", matcher.matches(fake(path)));
+        if (realMatcher != null) {
+          Path realPath = Paths.get(path);
+          assertTrue(
+              "real matcher '" + realMatcher + "' did not match '" + realPath + "'",
+              realMatcher.matches(realPath));
+        }
+      }
+      return this;
+    }
+
+    PatternAsserter doesNotMatch(String... paths) {
+      for (String path : paths) {
+        assertFalse(
+            "glob '" + matcher + "' should not have matched '" + path + "'",
+            matcher.matches(fake(path)));
+        if (realMatcher != null) {
+          Path realPath = Paths.get(path);
+          assertFalse(
+              "real matcher '" + realMatcher + "' matched '" + realPath + "'",
+              realMatcher.matches(realPath));
+        }
+      }
+      return this;
+    }
+  }
+
+  /** Path that only provides toString(). */
+  private static Path fake(final String path) {
+    return new Path() {
+      @Override
+      public FileSystem getFileSystem() {
+        return null;
+      }
+
+      @Override
+      public boolean isAbsolute() {
+        return false;
+      }
+
+      @Override
+      public Path getRoot() {
+        return null;
+      }
+
+      @Override
+      public Path getFileName() {
+        return null;
+      }
+
+      @Override
+      public Path getParent() {
+        return null;
+      }
+
+      @Override
+      public int getNameCount() {
+        return 0;
+      }
+
+      @Override
+      public Path getName(int index) {
+        return null;
+      }
+
+      @Override
+      public Path subpath(int beginIndex, int endIndex) {
+        return null;
+      }
+
+      @Override
+      public boolean startsWith(Path other) {
+        return false;
+      }
+
+      @Override
+      public boolean startsWith(String other) {
+        return false;
+      }
+
+      @Override
+      public boolean endsWith(Path other) {
+        return false;
+      }
+
+      @Override
+      public boolean endsWith(String other) {
+        return false;
+      }
+
+      @Override
+      public Path normalize() {
+        return null;
+      }
+
+      @Override
+      public Path resolve(Path other) {
+        return null;
+      }
+
+      @Override
+      public Path resolve(String other) {
+        return null;
+      }
+
+      @Override
+      public Path resolveSibling(Path other) {
+        return null;
+      }
+
+      @Override
+      public Path resolveSibling(String other) {
+        return null;
+      }
+
+      @Override
+      public Path relativize(Path other) {
+        return null;
+      }
+
+      @Override
+      public URI toUri() {
+        return null;
+      }
+
+      @Override
+      public Path toAbsolutePath() {
+        return null;
+      }
+
+      @Override
+      public Path toRealPath(LinkOption... options) throws IOException {
+        return null;
+      }
+
+      @Override
+      public File toFile() {
+        return null;
+      }
+
+      @Override
+      public WatchKey register(
+          WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
+          throws IOException {
+        return null;
+      }
+
+      @Override
+      public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events)
+          throws IOException {
+        return null;
+      }
+
+      @Override
+      public Iterator<Path> iterator() {
+        return null;
+      }
+
+      @Override
+      public int compareTo(Path other) {
+        return 0;
+      }
+
+      @Override
+      public String toString() {
+        return path;
+      }
+    };
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AbstractWatchServiceTest.java b/jimfs/src/test/java/com/google/common/jimfs/AbstractWatchServiceTest.java
new file mode 100644
index 0000000..61ddeb8
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AbstractWatchServiceTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.jimfs.AbstractWatchService.Key.State.READY;
+import static com.google.common.jimfs.AbstractWatchService.Key.State.SIGNALLED;
+import static com.google.common.truth.Truth.assertThat;
+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 static java.nio.file.StandardWatchEventKinds.OVERFLOW;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.fail;
+
+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.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.Watchable;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link AbstractWatchService}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class AbstractWatchServiceTest {
+
+  private AbstractWatchService watcher;
+
+  @Before
+  public void setUp() throws IOException {
+    watcher = new AbstractWatchService() {};
+  }
+
+  @Test
+  public void testNewWatcher() throws IOException {
+    assertThat(watcher.isOpen()).isTrue();
+    assertThat(watcher.poll()).isNull();
+    assertThat(watcher.queuedKeys()).isEmpty();
+    watcher.close();
+    assertThat(watcher.isOpen()).isFalse();
+  }
+
+  @Test
+  public void testRegister() throws IOException {
+    Watchable watchable = new StubWatchable();
+    AbstractWatchService.Key key = watcher.register(watchable, ImmutableSet.of(ENTRY_CREATE));
+    assertThat(key.isValid()).isTrue();
+    assertThat(key.pollEvents()).isEmpty();
+    assertThat(key.subscribesTo(ENTRY_CREATE)).isTrue();
+    assertThat(key.subscribesTo(ENTRY_DELETE)).isFalse();
+    assertThat(key.watchable()).isEqualTo(watchable);
+    assertThat(key.state()).isEqualTo(READY);
+  }
+
+  @Test
+  public void testPostEvent() throws IOException {
+    AbstractWatchService.Key key =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_CREATE));
+
+    AbstractWatchService.Event<Path> event =
+        new AbstractWatchService.Event<>(ENTRY_CREATE, 1, null);
+    key.post(event);
+    key.signal();
+
+    assertThat(watcher.queuedKeys()).containsExactly(key);
+
+    WatchKey retrievedKey = watcher.poll();
+    assertThat(retrievedKey).isEqualTo(key);
+
+    List<WatchEvent<?>> events = retrievedKey.pollEvents();
+    assertThat(events).hasSize(1);
+    assertThat(events.get(0)).isEqualTo(event);
+
+    // polling should have removed all events
+    assertThat(retrievedKey.pollEvents()).isEmpty();
+  }
+
+  @Test
+  public void testKeyStates() throws IOException {
+    AbstractWatchService.Key key =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_CREATE));
+
+    AbstractWatchService.Event<Path> event =
+        new AbstractWatchService.Event<>(ENTRY_CREATE, 1, null);
+    assertThat(key.state()).isEqualTo(READY);
+    key.post(event);
+    key.signal();
+    assertThat(key.state()).isEqualTo(SIGNALLED);
+
+    AbstractWatchService.Event<Path> event2 =
+        new AbstractWatchService.Event<>(ENTRY_CREATE, 1, null);
+    key.post(event2);
+    assertThat(key.state()).isEqualTo(SIGNALLED);
+
+    // key was not queued twice
+    assertThat(watcher.queuedKeys()).containsExactly(key);
+    assertThat(watcher.poll().pollEvents()).containsExactly(event, event2);
+
+    assertThat(watcher.poll()).isNull();
+
+    key.post(event);
+
+    // still not added to queue; already signalled
+    assertThat(watcher.poll()).isNull();
+    assertThat(key.pollEvents()).containsExactly(event);
+
+    key.reset();
+    assertThat(key.state()).isEqualTo(READY);
+
+    key.post(event2);
+    key.signal();
+
+    // now that it's reset it can be requeued
+    assertThat(watcher.poll()).isEqualTo(key);
+  }
+
+  @Test
+  public void testKeyRequeuedOnResetIfEventsArePending() throws IOException {
+    AbstractWatchService.Key key =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_CREATE));
+    key.post(new AbstractWatchService.Event<>(ENTRY_CREATE, 1, null));
+    key.signal();
+
+    key = (AbstractWatchService.Key) watcher.poll();
+    assertThat(watcher.queuedKeys()).isEmpty();
+
+    assertThat(key.pollEvents()).hasSize(1);
+
+    key.post(new AbstractWatchService.Event<>(ENTRY_CREATE, 1, null));
+    assertThat(watcher.queuedKeys()).isEmpty();
+
+    key.reset();
+    assertThat(key.state()).isEqualTo(SIGNALLED);
+    assertThat(watcher.queuedKeys()).hasSize(1);
+  }
+
+  @Test
+  public void testOverflow() throws IOException {
+    AbstractWatchService.Key key =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_CREATE));
+    for (int i = 0; i < AbstractWatchService.Key.MAX_QUEUE_SIZE + 10; i++) {
+      key.post(new AbstractWatchService.Event<>(ENTRY_CREATE, 1, null));
+    }
+    key.signal();
+
+    List<WatchEvent<?>> events = key.pollEvents();
+
+    assertThat(events).hasSize(AbstractWatchService.Key.MAX_QUEUE_SIZE + 1);
+    for (int i = 0; i < AbstractWatchService.Key.MAX_QUEUE_SIZE; i++) {
+      assertThat(events.get(i).kind()).isEqualTo(ENTRY_CREATE);
+    }
+
+    WatchEvent<?> lastEvent = events.get(AbstractWatchService.Key.MAX_QUEUE_SIZE);
+    assertThat(lastEvent.kind()).isEqualTo(OVERFLOW);
+    assertThat(lastEvent.count()).isEqualTo(10);
+  }
+
+  @Test
+  public void testResetAfterCancelReturnsFalse() throws IOException {
+    AbstractWatchService.Key key =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_CREATE));
+    key.signal();
+    key.cancel();
+    assertThat(key.reset()).isFalse();
+  }
+
+  @Test
+  public void testClosedWatcher() throws IOException, InterruptedException {
+    AbstractWatchService.Key key1 =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_CREATE));
+    AbstractWatchService.Key key2 =
+        watcher.register(new StubWatchable(), ImmutableSet.of(ENTRY_MODIFY));
+
+    assertThat(key1.isValid()).isTrue();
+    assertThat(key2.isValid()).isTrue();
+
+    watcher.close();
+
+    assertThat(key1.isValid()).isFalse();
+    assertThat(key2.isValid()).isFalse();
+    assertThat(key1.reset()).isFalse();
+    assertThat(key2.reset()).isFalse();
+
+    try {
+      watcher.poll();
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+
+    try {
+      watcher.poll(10, SECONDS);
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+
+    try {
+      watcher.take();
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+
+    try {
+      watcher.register(new StubWatchable(), ImmutableList.<WatchEvent.Kind<?>>of());
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+  }
+
+  // TODO(cgdecker): Test concurrent use of Watcher
+
+  /** A fake {@link Watchable} for testing. */
+  private static final class StubWatchable implements Watchable {
+
+    @Override
+    public WatchKey register(
+        WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers)
+        throws IOException {
+      return register(watcher, events);
+    }
+
+    @Override
+    public WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events)
+        throws IOException {
+      return ((AbstractWatchService) watcher).register(this, Arrays.asList(events));
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AclAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/AclAttributeProviderTest.java
new file mode 100644
index 0000000..f8a9445
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AclAttributeProviderTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.jimfs.UserLookupService.createUserPrincipal;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.file.attribute.AclEntryFlag.DIRECTORY_INHERIT;
+import static java.nio.file.attribute.AclEntryPermission.APPEND_DATA;
+import static java.nio.file.attribute.AclEntryPermission.DELETE;
+import static java.nio.file.attribute.AclEntryType.ALLOW;
+import static org.junit.Assert.assertNotNull;
+
+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.UserPrincipal;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link AclAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class AclAttributeProviderTest extends AbstractAttributeProviderTest<AclAttributeProvider> {
+
+  private static final UserPrincipal USER = createUserPrincipal("user");
+  private static final UserPrincipal FOO = createUserPrincipal("foo");
+
+  private static final ImmutableList<AclEntry> defaultAcl =
+      new ImmutableList.Builder<AclEntry>()
+          .add(
+              AclEntry.newBuilder()
+                  .setType(ALLOW)
+                  .setFlags(DIRECTORY_INHERIT)
+                  .setPermissions(DELETE, APPEND_DATA)
+                  .setPrincipal(USER)
+                  .build())
+          .add(
+              AclEntry.newBuilder()
+                  .setType(ALLOW)
+                  .setFlags(DIRECTORY_INHERIT)
+                  .setPermissions(DELETE, APPEND_DATA)
+                  .setPrincipal(FOO)
+                  .build())
+          .build();
+
+  @Override
+  protected AclAttributeProvider createProvider() {
+    return new AclAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of(new BasicAttributeProvider(), new OwnerAttributeProvider());
+  }
+
+  @Override
+  protected Map<String, ?> createDefaultValues() {
+    return ImmutableMap.of("acl:acl", defaultAcl);
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    assertThat(provider.get(file, "acl")).isEqualTo(defaultAcl);
+  }
+
+  @Test
+  public void testSet() {
+    assertSetAndGetSucceeds("acl", ImmutableList.of());
+    assertSetFailsOnCreate("acl", ImmutableList.of());
+    assertSetFails("acl", ImmutableSet.of());
+    assertSetFails("acl", ImmutableList.of("hello"));
+  }
+
+  @Test
+  public void testView() throws IOException {
+    AclFileAttributeView view =
+        provider.view(
+            fileLookup(),
+            ImmutableMap.<String, FileAttributeView>of(
+                "owner", new OwnerAttributeProvider().view(fileLookup(), NO_INHERITED_VIEWS)));
+    assertNotNull(view);
+
+    assertThat(view.name()).isEqualTo("acl");
+
+    assertThat(view.getAcl()).isEqualTo(defaultAcl);
+
+    view.setAcl(ImmutableList.<AclEntry>of());
+    view.setOwner(FOO);
+
+    assertThat(view.getAcl()).isEqualTo(ImmutableList.<AclEntry>of());
+    assertThat(view.getOwner()).isEqualTo(FOO);
+
+    assertThat(file.getAttribute("acl", "acl")).isEqualTo(ImmutableList.<AclEntry>of());
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/AttributeServiceTest.java b/jimfs/src/test/java/com/google/common/jimfs/AttributeServiceTest.java
new file mode 100644
index 0000000..80b0191
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/AttributeServiceTest.java
@@ -0,0 +1,391 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link AttributeService}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class AttributeServiceTest {
+
+  private AttributeService service;
+
+  @Before
+  public void setUp() {
+    ImmutableSet<AttributeProvider> providers =
+        ImmutableSet.of(
+            StandardAttributeProviders.get("basic"),
+            StandardAttributeProviders.get("owner"),
+            new TestAttributeProvider());
+    service = new AttributeService(providers, ImmutableMap.<String, Object>of());
+  }
+
+  @Test
+  public void testSupportedFileAttributeViews() {
+    assertThat(service.supportedFileAttributeViews())
+        .isEqualTo(ImmutableSet.of("basic", "test", "owner"));
+  }
+
+  @Test
+  public void testSupportsFileAttributeView() {
+    assertThat(service.supportsFileAttributeView(BasicFileAttributeView.class)).isTrue();
+    assertThat(service.supportsFileAttributeView(TestAttributeView.class)).isTrue();
+    assertThat(service.supportsFileAttributeView(PosixFileAttributeView.class)).isFalse();
+  }
+
+  @Test
+  public void testSetInitialAttributes() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+
+    assertThat(file.getAttributeNames("test")).containsExactly("bar", "baz");
+    assertThat(file.getAttributeNames("owner")).containsExactly("owner");
+
+    assertThat(service.getAttribute(file, "basic:lastModifiedTime")).isInstanceOf(FileTime.class);
+    assertThat(file.getAttribute("test", "bar")).isEqualTo(0L);
+    assertThat(file.getAttribute("test", "baz")).isEqualTo(1);
+  }
+
+  @Test
+  public void testGetAttribute() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+
+    assertThat(service.getAttribute(file, "test:foo")).isEqualTo("hello");
+    assertThat(service.getAttribute(file, "test", "foo")).isEqualTo("hello");
+    assertThat(service.getAttribute(file, "basic:isRegularFile")).isEqualTo(false);
+    assertThat(service.getAttribute(file, "isDirectory")).isEqualTo(true);
+    assertThat(service.getAttribute(file, "test:baz")).isEqualTo(1);
+  }
+
+  @Test
+  public void testGetAttribute_fromInheritedProvider() {
+    File file = Directory.create(0);
+    assertThat(service.getAttribute(file, "test:isRegularFile")).isEqualTo(false);
+    assertThat(service.getAttribute(file, "test:isDirectory")).isEqualTo(true);
+    assertThat(service.getAttribute(file, "test", "fileKey")).isEqualTo(0);
+  }
+
+  @Test
+  public void testGetAttribute_failsForAttributesNotDefinedByProvider() {
+    File file = Directory.create(0);
+    try {
+      service.getAttribute(file, "test:blah");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      // baz is defined by "test", but basic doesn't inherit test
+      service.getAttribute(file, "basic", "baz");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testSetAttribute() {
+    File file = Directory.create(0);
+    service.setAttribute(file, "test:bar", 10L, false);
+    assertThat(file.getAttribute("test", "bar")).isEqualTo(10L);
+
+    service.setAttribute(file, "test:baz", 100, false);
+    assertThat(file.getAttribute("test", "baz")).isEqualTo(100);
+  }
+
+  @Test
+  public void testSetAttribute_forInheritedProvider() {
+    File file = Directory.create(0);
+    service.setAttribute(file, "test:lastModifiedTime", FileTime.fromMillis(0), false);
+    assertThat(file.getAttribute("test", "lastModifiedTime")).isNull();
+    assertThat(service.getAttribute(file, "basic:lastModifiedTime"))
+        .isEqualTo(FileTime.fromMillis(0));
+  }
+
+  @Test
+  public void testSetAttribute_withAlternateAcceptedType() {
+    File file = Directory.create(0);
+    service.setAttribute(file, "test:bar", 10F, false);
+    assertThat(file.getAttribute("test", "bar")).isEqualTo(10L);
+
+    service.setAttribute(file, "test:bar", BigInteger.valueOf(123), false);
+    assertThat(file.getAttribute("test", "bar")).isEqualTo(123L);
+  }
+
+  @Test
+  public void testSetAttribute_onCreate() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file, new BasicFileAttribute<>("test:baz", 123));
+    assertThat(file.getAttribute("test", "baz")).isEqualTo(123);
+  }
+
+  @Test
+  public void testSetAttribute_failsForAttributesNotDefinedByProvider() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+
+    try {
+      service.setAttribute(file, "test:blah", "blah", false);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    try {
+      // baz is defined by "test", but basic doesn't inherit test
+      service.setAttribute(file, "basic:baz", 5, false);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    assertThat(file.getAttribute("test", "baz")).isEqualTo(1);
+  }
+
+  @Test
+  public void testSetAttribute_failsForArgumentThatIsNotOfCorrectType() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+    try {
+      service.setAttribute(file, "test:bar", "wrong", false);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    assertThat(file.getAttribute("test", "bar")).isEqualTo(0L);
+  }
+
+  @Test
+  public void testSetAttribute_failsForNullArgument() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+    try {
+      service.setAttribute(file, "test:bar", null, false);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+
+    assertThat(file.getAttribute("test", "bar")).isEqualTo(0L);
+  }
+
+  @Test
+  public void testSetAttribute_failsForAttributeThatIsNotSettable() {
+    File file = Directory.create(0);
+    try {
+      service.setAttribute(file, "test:foo", "world", false);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    assertThat(file.getAttribute("test", "foo")).isNull();
+  }
+
+  @Test
+  public void testSetAttribute_onCreate_failsForAttributeThatIsNotSettableOnCreate() {
+    File file = Directory.create(0);
+    try {
+      service.setInitialAttributes(file, new BasicFileAttribute<>("test:foo", "world"));
+      fail();
+    } catch (UnsupportedOperationException expected) {
+      // it turns out that UOE should be thrown on create even if the attribute isn't settable
+      // under any circumstances
+    }
+
+    try {
+      service.setInitialAttributes(file, new BasicFileAttribute<>("test:bar", 5));
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @SuppressWarnings("ConstantConditions")
+  @Test
+  public void testGetFileAttributeView() throws IOException {
+    final File file = Directory.create(0);
+    service.setInitialAttributes(file);
+
+    FileLookup fileLookup =
+        new FileLookup() {
+          @Override
+          public File lookup() throws IOException {
+            return file;
+          }
+        };
+
+    assertThat(service.getFileAttributeView(fileLookup, TestAttributeView.class)).isNotNull();
+    assertThat(service.getFileAttributeView(fileLookup, BasicFileAttributeView.class)).isNotNull();
+
+    TestAttributes attrs =
+        service.getFileAttributeView(fileLookup, TestAttributeView.class).readAttributes();
+    assertThat(attrs.foo()).isEqualTo("hello");
+    assertThat(attrs.bar()).isEqualTo(0);
+    assertThat(attrs.baz()).isEqualTo(1);
+  }
+
+  @Test
+  public void testGetFileAttributeView_isNullForUnsupportedView() {
+    final File file = Directory.create(0);
+    FileLookup fileLookup =
+        new FileLookup() {
+          @Override
+          public File lookup() throws IOException {
+            return file;
+          }
+        };
+    assertThat(service.getFileAttributeView(fileLookup, PosixFileAttributeView.class)).isNull();
+  }
+
+  @Test
+  public void testReadAttributes_asMap() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+
+    ImmutableMap<String, Object> map = service.readAttributes(file, "test:foo,bar,baz");
+    assertThat(map).isEqualTo(ImmutableMap.of("foo", "hello", "bar", 0L, "baz", 1));
+
+    FileTime time = (FileTime) service.getAttribute(file, "basic:creationTime");
+
+    map = service.readAttributes(file, "test:*");
+    assertThat(map)
+        .isEqualTo(
+            ImmutableMap.<String, Object>builder()
+                .put("foo", "hello")
+                .put("bar", 0L)
+                .put("baz", 1)
+                .put("fileKey", 0)
+                .put("isDirectory", true)
+                .put("isRegularFile", false)
+                .put("isSymbolicLink", false)
+                .put("isOther", false)
+                .put("size", 0L)
+                .put("lastModifiedTime", time)
+                .put("lastAccessTime", time)
+                .put("creationTime", time)
+                .build());
+
+    map = service.readAttributes(file, "basic:*");
+    assertThat(map)
+        .isEqualTo(
+            ImmutableMap.<String, Object>builder()
+                .put("fileKey", 0)
+                .put("isDirectory", true)
+                .put("isRegularFile", false)
+                .put("isSymbolicLink", false)
+                .put("isOther", false)
+                .put("size", 0L)
+                .put("lastModifiedTime", time)
+                .put("lastAccessTime", time)
+                .put("creationTime", time)
+                .build());
+  }
+
+  @Test
+  public void testReadAttributes_asMap_failsForInvalidAttributes() {
+    File file = Directory.create(0);
+    try {
+      service.readAttributes(file, "basic:fileKey,isOther,*,creationTime");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("invalid attributes");
+    }
+
+    try {
+      service.readAttributes(file, "basic:fileKey,isOther,foo");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("invalid attribute");
+    }
+  }
+
+  @Test
+  public void testReadAttributes_asObject() {
+    File file = Directory.create(0);
+    service.setInitialAttributes(file);
+
+    BasicFileAttributes basicAttrs = service.readAttributes(file, BasicFileAttributes.class);
+    assertThat(basicAttrs.fileKey()).isEqualTo(0);
+    assertThat(basicAttrs.isDirectory()).isTrue();
+    assertThat(basicAttrs.isRegularFile()).isFalse();
+
+    TestAttributes testAttrs = service.readAttributes(file, TestAttributes.class);
+    assertThat(testAttrs.foo()).isEqualTo("hello");
+    assertThat(testAttrs.bar()).isEqualTo(0);
+    assertThat(testAttrs.baz()).isEqualTo(1);
+
+    file.setAttribute("test", "baz", 100);
+    assertThat(service.readAttributes(file, TestAttributes.class).baz()).isEqualTo(100);
+  }
+
+  @Test
+  public void testReadAttributes_failsForUnsupportedAttributesType() {
+    File file = Directory.create(0);
+    try {
+      service.readAttributes(file, PosixFileAttributes.class);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test
+  public void testIllegalAttributeFormats() {
+    File file = Directory.create(0);
+    try {
+      service.getAttribute(file, ":bar");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("attribute format");
+    }
+
+    try {
+      service.getAttribute(file, "test:");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("attribute format");
+    }
+
+    try {
+      service.getAttribute(file, "basic:test:isDirectory");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("attribute format");
+    }
+
+    try {
+      service.getAttribute(file, "basic:fileKey,size");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("single attribute");
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/BasicAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/BasicAttributeProviderTest.java
new file mode 100644
index 0000000..a101b78
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/BasicAttributeProviderTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.truth.Truth.assertThat;
+
+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.FileTime;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link BasicAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class BasicAttributeProviderTest
+    extends AbstractAttributeProviderTest<BasicAttributeProvider> {
+
+  @Override
+  protected BasicAttributeProvider createProvider() {
+    return new BasicAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of();
+  }
+
+  @Test
+  public void testSupportedAttributes() {
+    assertSupportsAll(
+        "fileKey",
+        "size",
+        "isDirectory",
+        "isRegularFile",
+        "isSymbolicLink",
+        "isOther",
+        "creationTime",
+        "lastModifiedTime",
+        "lastAccessTime");
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    long time = file.getCreationTime();
+    assertThat(time).isNotEqualTo(0L);
+    assertThat(time).isEqualTo(file.getLastAccessTime());
+    assertThat(time).isEqualTo(file.getLastModifiedTime());
+
+    assertContainsAll(
+        file,
+        ImmutableMap.<String, Object>builder()
+            .put("fileKey", 0)
+            .put("size", 0L)
+            .put("isDirectory", true)
+            .put("isRegularFile", false)
+            .put("isSymbolicLink", false)
+            .put("isOther", false)
+            .build());
+  }
+
+  @Test
+  public void testSet() {
+    FileTime time = FileTime.fromMillis(0L);
+
+    // settable
+    assertSetAndGetSucceeds("creationTime", time);
+    assertSetAndGetSucceeds("lastModifiedTime", time);
+    assertSetAndGetSucceeds("lastAccessTime", time);
+
+    // unsettable
+    assertSetFails("fileKey", 3L);
+    assertSetFails("size", 1L);
+    assertSetFails("isRegularFile", true);
+    assertSetFails("isDirectory", true);
+    assertSetFails("isSymbolicLink", true);
+    assertSetFails("isOther", true);
+
+    // invalid type
+    assertSetFails("creationTime", "foo");
+  }
+
+  @Test
+  public void testSetOnCreate() {
+    FileTime time = FileTime.fromMillis(0L);
+
+    assertSetFailsOnCreate("creationTime", time);
+    assertSetFailsOnCreate("lastModifiedTime", time);
+    assertSetFailsOnCreate("lastAccessTime", time);
+  }
+
+  @Test
+  public void testView() throws IOException {
+    BasicFileAttributeView view = provider.view(fileLookup(), NO_INHERITED_VIEWS);
+
+    assertThat(view).isNotNull();
+    assertThat(view.name()).isEqualTo("basic");
+
+    BasicFileAttributes attrs = view.readAttributes();
+    assertThat(attrs.fileKey()).isEqualTo(0);
+
+    FileTime time = attrs.creationTime();
+    assertThat(attrs.lastAccessTime()).isEqualTo(time);
+    assertThat(attrs.lastModifiedTime()).isEqualTo(time);
+
+    view.setTimes(null, null, null);
+
+    attrs = view.readAttributes();
+    assertThat(attrs.creationTime()).isEqualTo(time);
+    assertThat(attrs.lastAccessTime()).isEqualTo(time);
+    assertThat(attrs.lastModifiedTime()).isEqualTo(time);
+
+    view.setTimes(FileTime.fromMillis(0L), null, null);
+
+    attrs = view.readAttributes();
+    assertThat(attrs.creationTime()).isEqualTo(time);
+    assertThat(attrs.lastAccessTime()).isEqualTo(time);
+    assertThat(attrs.lastModifiedTime()).isEqualTo(FileTime.fromMillis(0L));
+  }
+
+  @Test
+  public void testAttributes() {
+    BasicFileAttributes attrs = provider.readAttributes(file);
+    assertThat(attrs.fileKey()).isEqualTo(0);
+    assertThat(attrs.isDirectory()).isTrue();
+    assertThat(attrs.isRegularFile()).isFalse();
+    assertThat(attrs.creationTime()).isNotNull();
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/BasicFileAttribute.java b/jimfs/src/test/java/com/google/common/jimfs/BasicFileAttribute.java
new file mode 100644
index 0000000..8311a35
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/BasicFileAttribute.java
@@ -0,0 +1,43 @@
+/*
+ * 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.nio.file.attribute.FileAttribute;
+
+/** @author Colin Decker */
+public class BasicFileAttribute<T> implements FileAttribute<T> {
+
+  private final String name;
+  private final T value;
+
+  public BasicFileAttribute(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/test/java/com/google/common/jimfs/ByteBufferChannel.java b/jimfs/src/test/java/com/google/common/jimfs/ByteBufferChannel.java
new file mode 100644
index 0000000..7428975
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/ByteBufferChannel.java
@@ -0,0 +1,98 @@
+/*
+ * 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.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+
+/** @author Colin Decker */
+public class ByteBufferChannel implements SeekableByteChannel {
+
+  private final ByteBuffer buffer;
+
+  public ByteBufferChannel(byte[] bytes) {
+    this.buffer = ByteBuffer.wrap(bytes);
+  }
+
+  public ByteBufferChannel(byte[] bytes, int offset, int length) {
+    this.buffer = ByteBuffer.wrap(bytes, offset, length);
+  }
+
+  public ByteBufferChannel(int capacity) {
+    this.buffer = ByteBuffer.allocate(capacity);
+  }
+
+  public ByteBufferChannel(ByteBuffer buffer) {
+    this.buffer = buffer;
+  }
+
+  public ByteBuffer buffer() {
+    return buffer;
+  }
+
+  @Override
+  public int read(ByteBuffer dst) throws IOException {
+    if (buffer.remaining() == 0) {
+      return -1;
+    }
+    int length = Math.min(dst.remaining(), buffer.remaining());
+    for (int i = 0; i < length; i++) {
+      dst.put(buffer.get());
+    }
+    return length;
+  }
+
+  @Override
+  public int write(ByteBuffer src) throws IOException {
+    int length = Math.min(src.remaining(), buffer.remaining());
+    for (int i = 0; i < length; i++) {
+      buffer.put(src.get());
+    }
+    return length;
+  }
+
+  @Override
+  public long position() throws IOException {
+    return buffer.position();
+  }
+
+  @Override
+  public SeekableByteChannel position(long newPosition) throws IOException {
+    buffer.position((int) newPosition);
+    return this;
+  }
+
+  @Override
+  public long size() throws IOException {
+    return buffer.limit();
+  }
+
+  @Override
+  public SeekableByteChannel truncate(long size) throws IOException {
+    buffer.limit((int) size);
+    return this;
+  }
+
+  @Override
+  public boolean isOpen() {
+    return true;
+  }
+
+  @Override
+  public void close() throws IOException {}
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/ClassLoaderTest.java b/jimfs/src/test/java/com/google/common/jimfs/ClassLoaderTest.java
new file mode 100644
index 0000000..671a566
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/ClassLoaderTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URLClassLoader;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests behavior when user code loads Jimfs in a separate class loader from the system class loader
+ * (which is what {@link FileSystemProvider#installedProviders()} uses to load {@link
+ * FileSystemProvider}s as services from the classpath).
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class ClassLoaderTest {
+
+  @Test
+  public void separateClassLoader() throws Exception {
+    ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+    ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
+
+    ClassLoader loader = MoreObjects.firstNonNull(contextLoader, systemLoader);
+
+    if (loader instanceof URLClassLoader) {
+      // Anything we can do if it isn't a URLClassLoader?
+      URLClassLoader urlLoader = (URLClassLoader) loader;
+
+      ClassLoader separateLoader =
+          new URLClassLoader(
+              urlLoader.getURLs(), systemLoader.getParent()); // either null or the boostrap loader
+
+      Thread.currentThread().setContextClassLoader(separateLoader);
+      try {
+        Class<?> thisClass = separateLoader.loadClass(getClass().getName());
+        Method createFileSystem = thisClass.getDeclaredMethod("createFileSystem");
+
+        // First, the call to Jimfs.newFileSystem in createFileSystem needs to succeed
+        Object fs = createFileSystem.invoke(null);
+
+        // Next, some sanity checks:
+
+        // The file system is a JimfsFileSystem
+        assertEquals("com.google.common.jimfs.JimfsFileSystem", fs.getClass().getName());
+
+        // But it is not seen as an instance of JimfsFileSystem here because it was loaded by a
+        // different ClassLoader
+        assertFalse(fs instanceof JimfsFileSystem);
+
+        // But it should be an instance of FileSystem regardless, which is the important thing.
+        assertTrue(fs instanceof FileSystem);
+
+        // And normal file operations should work on it despite its provenance from a different
+        // ClassLoader
+        writeAndRead((FileSystem) fs, "bar.txt", "blah blah");
+
+        // And for the heck of it, test the contents of the file that was created in
+        // createFileSystem too
+        assertEquals(
+            "blah", Files.readAllLines(((FileSystem) fs).getPath("foo.txt"), UTF_8).get(0));
+      } finally {
+        Thread.currentThread().setContextClassLoader(contextLoader);
+      }
+    }
+  }
+
+  /**
+   * This method is really just testing that {@code Jimfs.newFileSystem()} succeeds. Without special
+   * handling, when the system class loader loads our {@code FileSystemProvider} implementation as a
+   * service and this code (the user code) is loaded in a separate class loader, the system-loaded
+   * provider won't see the instance of {@code Configuration} we give it as being an instance of the
+   * {@code Configuration} it's expecting (they're completely separate classes) and creation of the
+   * file system will fail.
+   */
+  public static FileSystem createFileSystem() throws IOException {
+    FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+
+    // Just some random operations to verify that basic things work on the created file system.
+    writeAndRead(fs, "foo.txt", "blah");
+
+    return fs;
+  }
+
+  private static void writeAndRead(FileSystem fs, String path, String text) throws IOException {
+    Path p = fs.getPath(path);
+    Files.write(p, ImmutableList.of(text), UTF_8);
+    List<String> lines = Files.readAllLines(p, UTF_8);
+    assertEquals(text, lines.get(0));
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java b/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java
new file mode 100644
index 0000000..0404f57
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java
@@ -0,0 +1,366 @@
+/*
+ * 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.jimfs.PathNormalization.CASE_FOLD_ASCII;
+import static com.google.common.jimfs.PathNormalization.CASE_FOLD_UNICODE;
+import static com.google.common.jimfs.PathNormalization.NFC;
+import static com.google.common.jimfs.PathNormalization.NFD;
+import static com.google.common.jimfs.PathSubject.paths;
+import static com.google.common.jimfs.WatchServiceConfiguration.polling;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.PosixFilePermissions;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Configuration}, {@link Configuration.Builder} and file systems created from
+ * them.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class ConfigurationTest {
+
+  private static PathSubject assertThatPath(Path path) {
+    return assert_().about(paths()).that(path);
+  }
+
+  @Test
+  public void testDefaultUnixConfiguration() {
+    Configuration config = Configuration.unix();
+
+    assertThat(config.pathType).isEqualTo(PathType.unix());
+    assertThat(config.roots).containsExactly("/");
+    assertThat(config.workingDirectory).isEqualTo("/work");
+    assertThat(config.nameCanonicalNormalization).isEmpty();
+    assertThat(config.nameDisplayNormalization).isEmpty();
+    assertThat(config.pathEqualityUsesCanonicalForm).isFalse();
+    assertThat(config.blockSize).isEqualTo(8192);
+    assertThat(config.maxSize).isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(config.maxCacheSize).isEqualTo(-1);
+    assertThat(config.attributeViews).containsExactly("basic");
+    assertThat(config.attributeProviders).isEmpty();
+    assertThat(config.defaultAttributeValues).isEmpty();
+  }
+
+  @Test
+  public void testFileSystemForDefaultUnixConfiguration() throws IOException {
+    FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+
+    assertThat(fs.getRootDirectories())
+        .containsExactlyElementsIn(ImmutableList.of(fs.getPath("/")))
+        .inOrder();
+    assertThatPath(fs.getPath("").toRealPath()).isEqualTo(fs.getPath("/work"));
+    assertThat(Iterables.getOnlyElement(fs.getFileStores()).getTotalSpace())
+        .isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(fs.supportedFileAttributeViews()).containsExactly("basic");
+
+    Files.createFile(fs.getPath("/foo"));
+    Files.createFile(fs.getPath("/FOO"));
+  }
+
+  @Test
+  public void testDefaultOsXConfiguration() {
+    Configuration config = Configuration.osX();
+
+    assertThat(config.pathType).isEqualTo(PathType.unix());
+    assertThat(config.roots).containsExactly("/");
+    assertThat(config.workingDirectory).isEqualTo("/work");
+    assertThat(config.nameCanonicalNormalization).containsExactly(NFD, CASE_FOLD_ASCII);
+    assertThat(config.nameDisplayNormalization).containsExactly(NFC);
+    assertThat(config.pathEqualityUsesCanonicalForm).isFalse();
+    assertThat(config.blockSize).isEqualTo(8192);
+    assertThat(config.maxSize).isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(config.maxCacheSize).isEqualTo(-1);
+    assertThat(config.attributeViews).containsExactly("basic");
+    assertThat(config.attributeProviders).isEmpty();
+    assertThat(config.defaultAttributeValues).isEmpty();
+  }
+
+  @Test
+  public void testFileSystemForDefaultOsXConfiguration() throws IOException {
+    FileSystem fs = Jimfs.newFileSystem(Configuration.osX());
+
+    assertThat(fs.getRootDirectories())
+        .containsExactlyElementsIn(ImmutableList.of(fs.getPath("/")))
+        .inOrder();
+    assertThatPath(fs.getPath("").toRealPath()).isEqualTo(fs.getPath("/work"));
+    assertThat(Iterables.getOnlyElement(fs.getFileStores()).getTotalSpace())
+        .isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(fs.supportedFileAttributeViews()).containsExactly("basic");
+
+    Files.createFile(fs.getPath("/foo"));
+
+    try {
+      Files.createFile(fs.getPath("/FOO"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+    }
+  }
+
+  @Test
+  public void testDefaultWindowsConfiguration() {
+    Configuration config = Configuration.windows();
+
+    assertThat(config.pathType).isEqualTo(PathType.windows());
+    assertThat(config.roots).containsExactly("C:\\");
+    assertThat(config.workingDirectory).isEqualTo("C:\\work");
+    assertThat(config.nameCanonicalNormalization).containsExactly(CASE_FOLD_ASCII);
+    assertThat(config.nameDisplayNormalization).isEmpty();
+    assertThat(config.pathEqualityUsesCanonicalForm).isTrue();
+    assertThat(config.blockSize).isEqualTo(8192);
+    assertThat(config.maxSize).isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(config.maxCacheSize).isEqualTo(-1);
+    assertThat(config.attributeViews).containsExactly("basic");
+    assertThat(config.attributeProviders).isEmpty();
+    assertThat(config.defaultAttributeValues).isEmpty();
+  }
+
+  @Test
+  public void testFileSystemForDefaultWindowsConfiguration() throws IOException {
+    FileSystem fs = Jimfs.newFileSystem(Configuration.windows());
+
+    assertThat(fs.getRootDirectories())
+        .containsExactlyElementsIn(ImmutableList.of(fs.getPath("C:\\")))
+        .inOrder();
+    assertThatPath(fs.getPath("").toRealPath()).isEqualTo(fs.getPath("C:\\work"));
+    assertThat(Iterables.getOnlyElement(fs.getFileStores()).getTotalSpace())
+        .isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(fs.supportedFileAttributeViews()).containsExactly("basic");
+
+    Files.createFile(fs.getPath("C:\\foo"));
+
+    try {
+      Files.createFile(fs.getPath("C:\\FOO"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+    }
+  }
+
+  @Test
+  public void testBuilder() {
+    AttributeProvider unixProvider = StandardAttributeProviders.get("unix");
+
+    Configuration config =
+        Configuration.builder(PathType.unix())
+            .setRoots("/")
+            .setWorkingDirectory("/hello/world")
+            .setNameCanonicalNormalization(NFD, CASE_FOLD_UNICODE)
+            .setNameDisplayNormalization(NFC)
+            .setPathEqualityUsesCanonicalForm(true)
+            .setBlockSize(10)
+            .setMaxSize(100)
+            .setMaxCacheSize(50)
+            .setAttributeViews("basic", "posix")
+            .addAttributeProvider(unixProvider)
+            .setDefaultAttributeValue(
+                "posix:permissions", PosixFilePermissions.fromString("---------"))
+            .build();
+
+    assertThat(config.pathType).isEqualTo(PathType.unix());
+    assertThat(config.roots).containsExactly("/");
+    assertThat(config.workingDirectory).isEqualTo("/hello/world");
+    assertThat(config.nameCanonicalNormalization).containsExactly(NFD, CASE_FOLD_UNICODE);
+    assertThat(config.nameDisplayNormalization).containsExactly(NFC);
+    assertThat(config.pathEqualityUsesCanonicalForm).isTrue();
+    assertThat(config.blockSize).isEqualTo(10);
+    assertThat(config.maxSize).isEqualTo(100);
+    assertThat(config.maxCacheSize).isEqualTo(50);
+    assertThat(config.attributeViews).containsExactly("basic", "posix");
+    assertThat(config.attributeProviders).containsExactly(unixProvider);
+    assertThat(config.defaultAttributeValues)
+        .containsEntry("posix:permissions", PosixFilePermissions.fromString("---------"));
+  }
+
+  @Test
+  public void testFileSystemForCustomConfiguration() throws IOException {
+    Configuration config =
+        Configuration.builder(PathType.unix())
+            .setRoots("/")
+            .setWorkingDirectory("/hello/world")
+            .setNameCanonicalNormalization(NFD, CASE_FOLD_UNICODE)
+            .setNameDisplayNormalization(NFC)
+            .setPathEqualityUsesCanonicalForm(true)
+            .setBlockSize(10)
+            .setMaxSize(100)
+            .setMaxCacheSize(50)
+            .setAttributeViews("unix")
+            .setDefaultAttributeValue(
+                "posix:permissions", PosixFilePermissions.fromString("---------"))
+            .build();
+
+    FileSystem fs = Jimfs.newFileSystem(config);
+
+    assertThat(fs.getRootDirectories())
+        .containsExactlyElementsIn(ImmutableList.of(fs.getPath("/")))
+        .inOrder();
+    assertThatPath(fs.getPath("").toRealPath()).isEqualTo(fs.getPath("/hello/world"));
+    assertThat(Iterables.getOnlyElement(fs.getFileStores()).getTotalSpace()).isEqualTo(100);
+    assertThat(fs.supportedFileAttributeViews()).containsExactly("basic", "owner", "posix", "unix");
+
+    Files.createFile(fs.getPath("/foo"));
+    assertThat(Files.getAttribute(fs.getPath("/foo"), "posix:permissions"))
+        .isEqualTo(PosixFilePermissions.fromString("---------"));
+
+    try {
+      Files.createFile(fs.getPath("/FOO"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+    }
+  }
+
+  @Test
+  public void testToBuilder() {
+    Configuration config =
+        Configuration.unix().toBuilder()
+            .setWorkingDirectory("/hello/world")
+            .setAttributeViews("basic", "posix")
+            .build();
+
+    assertThat(config.pathType).isEqualTo(PathType.unix());
+    assertThat(config.roots).containsExactly("/");
+    assertThat(config.workingDirectory).isEqualTo("/hello/world");
+    assertThat(config.nameCanonicalNormalization).isEmpty();
+    assertThat(config.nameDisplayNormalization).isEmpty();
+    assertThat(config.pathEqualityUsesCanonicalForm).isFalse();
+    assertThat(config.blockSize).isEqualTo(8192);
+    assertThat(config.maxSize).isEqualTo(4L * 1024 * 1024 * 1024);
+    assertThat(config.maxCacheSize).isEqualTo(-1);
+    assertThat(config.attributeViews).containsExactly("basic", "posix");
+    assertThat(config.attributeProviders).isEmpty();
+    assertThat(config.defaultAttributeValues).isEmpty();
+  }
+
+  @Test
+  public void testSettingRootsUnsupportedByPathType() {
+    assertIllegalRoots(PathType.unix(), "\\");
+    assertIllegalRoots(PathType.unix(), "/", "\\");
+    assertIllegalRoots(PathType.windows(), "/");
+    assertIllegalRoots(PathType.windows(), "C:"); // must have a \ (or a /)
+  }
+
+  private static void assertIllegalRoots(PathType type, String first, String... more) {
+    try {
+      Configuration.builder(type).setRoots(first, more); // wrong root
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testSettingWorkingDirectoryWithRelativePath() {
+    try {
+      Configuration.unix().toBuilder().setWorkingDirectory("foo/bar");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      Configuration.windows().toBuilder().setWorkingDirectory("foo\\bar");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testSettingNormalizationWhenNormalizationAlreadySet() {
+    assertIllegalNormalizations(NFC, NFC);
+    assertIllegalNormalizations(NFC, NFD);
+    assertIllegalNormalizations(CASE_FOLD_ASCII, CASE_FOLD_ASCII);
+    assertIllegalNormalizations(CASE_FOLD_ASCII, CASE_FOLD_UNICODE);
+  }
+
+  private static void assertIllegalNormalizations(
+      PathNormalization first, PathNormalization... more) {
+    try {
+      Configuration.builder(PathType.unix()).setNameCanonicalNormalization(first, more);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      Configuration.builder(PathType.unix()).setNameDisplayNormalization(first, more);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testSetDefaultAttributeValue_illegalAttributeFormat() {
+    try {
+      Configuration.unix().toBuilder().setDefaultAttributeValue("foo", 1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test // how's that for a name?
+  public void testCreateFileSystemFromConfigurationWithWorkingDirectoryNotUnderConfiguredRoot() {
+    try {
+      Jimfs.newFileSystem(
+          Configuration.windows().toBuilder()
+              .setRoots("C:\\", "D:\\")
+              .setWorkingDirectory("E:\\foo")
+              .build());
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testFileSystemWithDefaultWatchService() throws IOException {
+    FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+
+    WatchService watchService = fs.newWatchService();
+    assertThat(watchService).isInstanceOf(PollingWatchService.class);
+
+    PollingWatchService pollingWatchService = (PollingWatchService) watchService;
+    assertThat(pollingWatchService.interval).isEqualTo(5);
+    assertThat(pollingWatchService.timeUnit).isEqualTo(SECONDS);
+  }
+
+  @Test
+  public void testFileSystemWithCustomWatchServicePollingInterval() throws IOException {
+    FileSystem fs =
+        Jimfs.newFileSystem(
+            Configuration.unix().toBuilder()
+                .setWatchServiceConfiguration(polling(10, MILLISECONDS))
+                .build());
+
+    WatchService watchService = fs.newWatchService();
+    assertThat(watchService).isInstanceOf(PollingWatchService.class);
+
+    PollingWatchService pollingWatchService = (PollingWatchService) watchService;
+    assertThat(pollingWatchService.interval).isEqualTo(10);
+    assertThat(pollingWatchService.timeUnit).isEqualTo(MILLISECONDS);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/DirectoryTest.java b/jimfs/src/test/java/com/google/common/jimfs/DirectoryTest.java
new file mode 100644
index 0000000..217509d
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/DirectoryTest.java
@@ -0,0 +1,383 @@
+/*
+ * 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.jimfs.Name.PARENT;
+import static com.google.common.jimfs.Name.SELF;
+import static com.google.common.jimfs.TestUtils.regularFile;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterables;
+import java.util.HashSet;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Directory}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class DirectoryTest {
+
+  private Directory root;
+  private Directory dir;
+
+  @Before
+  public void setUp() {
+    root = Directory.createRoot(0, Name.simple("/"));
+
+    dir = Directory.create(1);
+    root.link(Name.simple("foo"), dir);
+  }
+
+  @Test
+  public void testRootDirectory() {
+    assertThat(root.entryCount()).isEqualTo(3); // two for parent/self, one for dir
+    assertThat(root.isEmpty()).isFalse();
+    assertThat(root.entryInParent()).isEqualTo(entry(root, "/", root));
+    assertThat(root.entryInParent().name()).isEqualTo(Name.simple("/"));
+
+    assertParentAndSelf(root, root, root);
+  }
+
+  @Test
+  public void testEmptyDirectory() {
+    assertThat(dir.entryCount()).isEqualTo(2);
+    assertThat(dir.isEmpty()).isTrue();
+
+    assertParentAndSelf(dir, root, dir);
+  }
+
+  @Test
+  public void testGet() {
+    assertThat(root.get(Name.simple("foo"))).isEqualTo(entry(root, "foo", dir));
+    assertThat(dir.get(Name.simple("foo"))).isNull();
+    assertThat(root.get(Name.simple("Foo"))).isNull();
+  }
+
+  @Test
+  public void testLink() {
+    assertThat(dir.get(Name.simple("bar"))).isNull();
+
+    File bar = Directory.create(2);
+    dir.link(Name.simple("bar"), bar);
+
+    assertThat(dir.get(Name.simple("bar"))).isEqualTo(entry(dir, "bar", bar));
+  }
+
+  @Test
+  public void testLink_existingNameFails() {
+    try {
+      root.link(Name.simple("foo"), Directory.create(2));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testLink_parentAndSelfNameFails() {
+    try {
+      dir.link(Name.simple("."), Directory.create(2));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      dir.link(Name.simple(".."), Directory.create(2));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testGet_normalizingCaseInsensitive() {
+    File bar = Directory.create(2);
+    Name barName = caseInsensitive("bar");
+
+    dir.link(barName, bar);
+
+    DirectoryEntry expected = new DirectoryEntry(dir, barName, bar);
+    assertThat(dir.get(caseInsensitive("bar"))).isEqualTo(expected);
+    assertThat(dir.get(caseInsensitive("BAR"))).isEqualTo(expected);
+    assertThat(dir.get(caseInsensitive("Bar"))).isEqualTo(expected);
+    assertThat(dir.get(caseInsensitive("baR"))).isEqualTo(expected);
+  }
+
+  @Test
+  public void testUnlink() {
+    assertThat(root.get(Name.simple("foo"))).isNotNull();
+
+    root.unlink(Name.simple("foo"));
+
+    assertThat(root.get(Name.simple("foo"))).isNull();
+  }
+
+  @Test
+  public void testUnlink_nonExistentNameFails() {
+    try {
+      dir.unlink(Name.simple("bar"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testUnlink_parentAndSelfNameFails() {
+    try {
+      dir.unlink(Name.simple("."));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      dir.unlink(Name.simple(".."));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testUnlink_normalizingCaseInsensitive() {
+    dir.link(caseInsensitive("bar"), Directory.create(2));
+
+    assertThat(dir.get(caseInsensitive("bar"))).isNotNull();
+
+    dir.unlink(caseInsensitive("BAR"));
+
+    assertThat(dir.get(caseInsensitive("bar"))).isNull();
+  }
+
+  @Test
+  public void testLinkDirectory() {
+    Directory newDir = Directory.create(10);
+
+    assertThat(newDir.entryInParent()).isNull();
+    assertThat(newDir.get(Name.SELF).file()).isEqualTo(newDir);
+    assertThat(newDir.get(Name.PARENT)).isNull();
+    assertThat(newDir.links()).isEqualTo(1);
+
+    dir.link(Name.simple("foo"), newDir);
+
+    assertThat(newDir.entryInParent()).isEqualTo(entry(dir, "foo", newDir));
+    assertThat(newDir.parent()).isEqualTo(dir);
+    assertThat(newDir.entryInParent().name()).isEqualTo(Name.simple("foo"));
+    assertThat(newDir.get(Name.SELF)).isEqualTo(entry(newDir, ".", newDir));
+    assertThat(newDir.get(Name.PARENT)).isEqualTo(entry(newDir, "..", dir));
+    assertThat(newDir.links()).isEqualTo(2);
+  }
+
+  @Test
+  public void testUnlinkDirectory() {
+    Directory newDir = Directory.create(10);
+
+    dir.link(Name.simple("foo"), newDir);
+
+    assertThat(dir.links()).isEqualTo(3);
+
+    assertThat(newDir.entryInParent()).isEqualTo(entry(dir, "foo", newDir));
+    assertThat(newDir.links()).isEqualTo(2);
+
+    dir.unlink(Name.simple("foo"));
+
+    assertThat(dir.links()).isEqualTo(2);
+
+    assertThat(newDir.entryInParent()).isEqualTo(entry(dir, "foo", newDir));
+    assertThat(newDir.get(Name.SELF).file()).isEqualTo(newDir);
+    assertThat(newDir.get(Name.PARENT)).isEqualTo(entry(newDir, "..", dir));
+    assertThat(newDir.links()).isEqualTo(1);
+  }
+
+  @Test
+  public void testSnapshot() {
+    root.link(Name.simple("bar"), regularFile(10));
+    root.link(Name.simple("abc"), regularFile(10));
+
+    /*
+     * If we inline this into the assertThat call below, javac resolves it to assertThat(SortedSet),
+     * which isn't available publicly. Our @GoogleInternal checks consider that to be an error, even
+     * though the code will compile fine externally by resolving to assertThat(Iterable) instead. So
+     * we avoid that by assigning to a non-SortedSet type here.
+     */
+    ImmutableSet<Name> snapshot = root.snapshot();
+    // does not include . or .. and is sorted by the name
+    assertThat(snapshot)
+        .containsExactly(Name.simple("abc"), Name.simple("bar"), Name.simple("foo"))
+        .inOrder();
+  }
+
+  @Test
+  public void testSnapshot_sortsUsingStringAndNotCanonicalValueOfNames() {
+    dir.link(caseInsensitive("FOO"), regularFile(10));
+    dir.link(caseInsensitive("bar"), regularFile(10));
+
+    ImmutableSortedSet<Name> snapshot = dir.snapshot();
+    Iterable<String> strings = Iterables.transform(snapshot, Functions.toStringFunction());
+
+    // "FOO" comes before "bar"
+    // if the order were based on the normalized, canonical form of the names ("foo" and "bar"),
+    // "bar" would come first
+    assertThat(strings).containsExactly("FOO", "bar").inOrder();
+  }
+
+  // Tests for internal hash table implementation
+
+  private static final Directory A = Directory.create(0);
+
+  @Test
+  public void testInitialState() {
+    assertThat(dir.entryCount()).isEqualTo(2);
+    assertThat(ImmutableSet.copyOf(dir))
+        .containsExactly(
+            new DirectoryEntry(dir, Name.SELF, dir), new DirectoryEntry(dir, Name.PARENT, root));
+    assertThat(dir.get(Name.simple("foo"))).isNull();
+  }
+
+  @Test
+  public void testPutAndGet() {
+    dir.put(entry("foo"));
+
+    assertThat(dir.entryCount()).isEqualTo(3);
+    assertThat(ImmutableSet.copyOf(dir)).contains(entry("foo"));
+    assertThat(dir.get(Name.simple("foo"))).isEqualTo(entry("foo"));
+
+    dir.put(entry("bar"));
+
+    assertThat(dir.entryCount()).isEqualTo(4);
+    assertThat(ImmutableSet.copyOf(dir)).containsAtLeast(entry("foo"), entry("bar"));
+    assertThat(dir.get(Name.simple("foo"))).isEqualTo(entry("foo"));
+    assertThat(dir.get(Name.simple("bar"))).isEqualTo(entry("bar"));
+  }
+
+  @Test
+  public void testPutEntryForExistingNameIsIllegal() {
+    dir.put(entry("foo"));
+
+    try {
+      dir.put(entry("foo"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testRemove() {
+    dir.put(entry("foo"));
+    dir.put(entry("bar"));
+
+    dir.remove(Name.simple("foo"));
+
+    assertThat(dir.entryCount()).isEqualTo(3);
+    assertThat(ImmutableSet.copyOf(dir))
+        .containsExactly(
+            entry("bar"),
+            new DirectoryEntry(dir, Name.SELF, dir),
+            new DirectoryEntry(dir, Name.PARENT, root));
+    assertThat(dir.get(Name.simple("foo"))).isNull();
+    assertThat(dir.get(Name.simple("bar"))).isEqualTo(entry("bar"));
+
+    dir.remove(Name.simple("bar"));
+
+    assertThat(dir.entryCount()).isEqualTo(2);
+
+    dir.put(entry("bar"));
+    dir.put(entry("foo")); // these should just succeeded
+  }
+
+  @Test
+  public void testManyPutsAndRemoves() {
+    // test resizing/rehashing
+
+    Set<DirectoryEntry> entriesInDir = new HashSet<>();
+    entriesInDir.add(new DirectoryEntry(dir, Name.SELF, dir));
+    entriesInDir.add(new DirectoryEntry(dir, Name.PARENT, root));
+
+    // add 1000 entries
+    for (int i = 0; i < 1000; i++) {
+      DirectoryEntry entry = entry(String.valueOf(i));
+      dir.put(entry);
+      entriesInDir.add(entry);
+
+      assertThat(ImmutableSet.copyOf(dir)).isEqualTo(entriesInDir);
+
+      for (DirectoryEntry expected : entriesInDir) {
+        assertThat(dir.get(expected.name())).isEqualTo(expected);
+      }
+    }
+
+    // remove 1000 entries
+    for (int i = 0; i < 1000; i++) {
+      dir.remove(Name.simple(String.valueOf(i)));
+      entriesInDir.remove(entry(String.valueOf(i)));
+
+      assertThat(ImmutableSet.copyOf(dir)).isEqualTo(entriesInDir);
+
+      for (DirectoryEntry expected : entriesInDir) {
+        assertThat(dir.get(expected.name())).isEqualTo(expected);
+      }
+    }
+
+    // mixed adds and removes
+    for (int i = 0; i < 10000; i++) {
+      DirectoryEntry entry = entry(String.valueOf(i));
+      dir.put(entry);
+      entriesInDir.add(entry);
+
+      if (i > 0 && i % 20 == 0) {
+        String nameToRemove = String.valueOf(i / 2);
+        dir.remove(Name.simple(nameToRemove));
+        entriesInDir.remove(entry(nameToRemove));
+      }
+    }
+
+    // for this one, only test that the end result is correct
+    // takes too long to test at each iteration
+    assertThat(ImmutableSet.copyOf(dir)).isEqualTo(entriesInDir);
+
+    for (DirectoryEntry expected : entriesInDir) {
+      assertThat(dir.get(expected.name())).isEqualTo(expected);
+    }
+  }
+
+  private static DirectoryEntry entry(String name) {
+    return new DirectoryEntry(A, Name.simple(name), A);
+  }
+
+  private static DirectoryEntry entry(Directory dir, String name, @NullableDecl File file) {
+    return new DirectoryEntry(dir, Name.simple(name), file);
+  }
+
+  private static void assertParentAndSelf(Directory dir, File parent, File self) {
+    assertThat(dir).isEqualTo(self);
+    assertThat(dir.parent()).isEqualTo(parent);
+
+    assertThat(dir.get(PARENT)).isEqualTo(entry((Directory) self, "..", parent));
+    assertThat(dir.get(SELF)).isEqualTo(entry((Directory) self, ".", self));
+  }
+
+  private static Name caseInsensitive(String name) {
+    return Name.create(name, PathNormalization.CASE_FOLD_UNICODE.apply(name));
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/DosAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/DosAttributeProviderTest.java
new file mode 100644
index 0000000..2781263
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/DosAttributeProviderTest.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.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+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.DosFileAttributeView;
+import java.nio.file.attribute.DosFileAttributes;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link DosAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class DosAttributeProviderTest extends AbstractAttributeProviderTest<DosAttributeProvider> {
+
+  private static final ImmutableList<String> DOS_ATTRIBUTES =
+      ImmutableList.of("hidden", "archive", "readonly", "system");
+
+  @Override
+  protected DosAttributeProvider createProvider() {
+    return new DosAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of(new BasicAttributeProvider(), new OwnerAttributeProvider());
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    for (String attribute : DOS_ATTRIBUTES) {
+      assertThat(provider.get(file, attribute)).isEqualTo(false);
+    }
+  }
+
+  @Test
+  public void testSet() {
+    for (String attribute : DOS_ATTRIBUTES) {
+      assertSetAndGetSucceeds(attribute, true);
+      assertSetFailsOnCreate(attribute, true);
+    }
+  }
+
+  @Test
+  public void testView() throws IOException {
+    DosFileAttributeView view =
+        provider.view(
+            fileLookup(),
+            ImmutableMap.<String, FileAttributeView>of(
+                "basic", new BasicAttributeProvider().view(fileLookup(), NO_INHERITED_VIEWS)));
+    assertNotNull(view);
+
+    assertThat(view.name()).isEqualTo("dos");
+
+    DosFileAttributes attrs = view.readAttributes();
+    assertThat(attrs.isHidden()).isFalse();
+    assertThat(attrs.isArchive()).isFalse();
+    assertThat(attrs.isReadOnly()).isFalse();
+    assertThat(attrs.isSystem()).isFalse();
+
+    view.setArchive(true);
+    view.setReadOnly(true);
+    view.setHidden(true);
+    view.setSystem(false);
+
+    assertThat(attrs.isHidden()).isFalse();
+    assertThat(attrs.isArchive()).isFalse();
+    assertThat(attrs.isReadOnly()).isFalse();
+
+    attrs = view.readAttributes();
+    assertThat(attrs.isHidden()).isTrue();
+    assertThat(attrs.isArchive()).isTrue();
+    assertThat(attrs.isReadOnly()).isTrue();
+    assertThat(attrs.isSystem()).isFalse();
+
+    view.setTimes(FileTime.fromMillis(0L), null, null);
+    assertThat(view.readAttributes().lastModifiedTime()).isEqualTo(FileTime.fromMillis(0L));
+  }
+
+  @Test
+  public void testAttributes() {
+    DosFileAttributes attrs = provider.readAttributes(file);
+    assertThat(attrs.isHidden()).isFalse();
+    assertThat(attrs.isArchive()).isFalse();
+    assertThat(attrs.isReadOnly()).isFalse();
+    assertThat(attrs.isSystem()).isFalse();
+
+    file.setAttribute("dos", "hidden", true);
+
+    attrs = provider.readAttributes(file);
+    assertThat(attrs.isHidden()).isTrue();
+    assertThat(attrs.isArchive()).isFalse();
+    assertThat(attrs.isReadOnly()).isFalse();
+    assertThat(attrs.isSystem()).isFalse();
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/FileFactoryTest.java b/jimfs/src/test/java/com/google/common/jimfs/FileFactoryTest.java
new file mode 100644
index 0000000..9e3cb40
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/FileFactoryTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link FileFactory}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class FileFactoryTest {
+
+  private FileFactory factory;
+
+  @Before
+  public void setUp() {
+    factory = new FileFactory(new HeapDisk(2, 2, 0));
+  }
+
+  @Test
+  public void testCreateFiles_basic() {
+    File file = factory.createDirectory();
+    assertThat(file.id()).isEqualTo(0L);
+    assertThat(file.isDirectory()).isTrue();
+
+    file = factory.createRegularFile();
+    assertThat(file.id()).isEqualTo(1L);
+    assertThat(file.isRegularFile()).isTrue();
+
+    file = factory.createSymbolicLink(fakePath());
+    assertThat(file.id()).isEqualTo(2L);
+    assertThat(file.isSymbolicLink()).isTrue();
+  }
+
+  @Test
+  public void testCreateFiles_withSupplier() {
+    File file = factory.directoryCreator().get();
+    assertThat(file.id()).isEqualTo(0L);
+    assertThat(file.isDirectory()).isTrue();
+
+    file = factory.regularFileCreator().get();
+    assertThat(file.id()).isEqualTo(1L);
+    assertThat(file.isRegularFile()).isTrue();
+
+    file = factory.symbolicLinkCreator(fakePath()).get();
+    assertThat(file.id()).isEqualTo(2L);
+    assertThat(file.isSymbolicLink()).isTrue();
+  }
+
+  static JimfsPath fakePath() {
+    return PathServiceTest.fakeUnixPathService().emptyPath();
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/FileSystemStateTest.java b/jimfs/src/test/java/com/google/common/jimfs/FileSystemStateTest.java
new file mode 100644
index 0000000..f8143b6
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/FileSystemStateTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.ClosedFileSystemException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link FileSystemState}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class FileSystemStateTest {
+
+  private final TestRunnable onClose = new TestRunnable();
+  private final FileSystemState state = new FileSystemState(onClose);
+
+  @Test
+  public void testIsOpen() throws IOException {
+    assertTrue(state.isOpen());
+    state.close();
+    assertFalse(state.isOpen());
+  }
+
+  @Test
+  public void testCheckOpen() throws IOException {
+    state.checkOpen(); // does not throw
+    state.close();
+    try {
+      state.checkOpen();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+  }
+
+  @Test
+  public void testClose_callsOnCloseRunnable() throws IOException {
+    assertEquals(0, onClose.runCount);
+    state.close();
+    assertEquals(1, onClose.runCount);
+  }
+
+  @Test
+  public void testClose_multipleTimesDoNothing() throws IOException {
+    state.close();
+    assertEquals(1, onClose.runCount);
+    state.close();
+    state.close();
+    assertEquals(1, onClose.runCount);
+  }
+
+  @Test
+  public void testClose_registeredResourceIsClosed() throws IOException {
+    TestCloseable resource = new TestCloseable();
+    state.register(resource);
+    assertFalse(resource.closed);
+    state.close();
+    assertTrue(resource.closed);
+  }
+
+  @Test
+  public void testClose_unregisteredResourceIsNotClosed() throws IOException {
+    TestCloseable resource = new TestCloseable();
+    state.register(resource);
+    assertFalse(resource.closed);
+    state.unregister(resource);
+    state.close();
+    assertFalse(resource.closed);
+  }
+
+  @Test
+  public void testClose_multipleRegisteredResourcesAreClosed() throws IOException {
+    List<TestCloseable> resources =
+        ImmutableList.of(new TestCloseable(), new TestCloseable(), new TestCloseable());
+    for (TestCloseable resource : resources) {
+      state.register(resource);
+      assertFalse(resource.closed);
+    }
+    state.close();
+    for (TestCloseable resource : resources) {
+      assertTrue(resource.closed);
+    }
+  }
+
+  @Test
+  public void testClose_resourcesThatThrowOnClose() {
+    List<TestCloseable> resources =
+        ImmutableList.of(
+            new TestCloseable(),
+            new ThrowsOnClose("a"),
+            new TestCloseable(),
+            new ThrowsOnClose("b"),
+            new ThrowsOnClose("c"),
+            new TestCloseable(),
+            new TestCloseable());
+    for (TestCloseable resource : resources) {
+      state.register(resource);
+      assertFalse(resource.closed);
+    }
+
+    try {
+      state.close();
+      fail();
+    } catch (IOException expected) {
+      Throwable[] suppressed = expected.getSuppressed();
+      assertEquals(2, suppressed.length);
+      ImmutableSet<String> messages =
+          ImmutableSet.of(
+              expected.getMessage(), suppressed[0].getMessage(), suppressed[1].getMessage());
+      assertEquals(ImmutableSet.of("a", "b", "c"), messages);
+    }
+
+    for (TestCloseable resource : resources) {
+      assertTrue(resource.closed);
+    }
+  }
+
+  private static class TestCloseable implements Closeable {
+
+    boolean closed = false;
+
+    @Override
+    public void close() throws IOException {
+      closed = true;
+    }
+  }
+
+  private static final class TestRunnable implements Runnable {
+    int runCount = 0;
+
+    @Override
+    public void run() {
+      runCount++;
+    }
+  }
+
+  private static class ThrowsOnClose extends TestCloseable {
+
+    private final String string;
+
+    private ThrowsOnClose(String string) {
+      this.string = string;
+    }
+
+    @Override
+    public void close() throws IOException {
+      super.close();
+      throw new IOException(string);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/FileTest.java b/jimfs/src/test/java/com/google/common/jimfs/FileTest.java
new file mode 100644
index 0000000..83cda00
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/FileTest.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.jimfs.FileFactoryTest.fakePath;
+import static com.google.common.jimfs.TestUtils.regularFile;
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link File}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class FileTest {
+
+  @Test
+  public void testAttributes() {
+    // these methods are basically just thin wrappers around a map, so no need to test too
+    // thoroughly
+
+    File file = RegularFile.create(0, new HeapDisk(10, 10, 10));
+
+    assertThat(file.getAttributeKeys()).isEmpty();
+    assertThat(file.getAttribute("foo", "foo")).isNull();
+
+    file.deleteAttribute("foo", "foo"); // doesn't throw
+
+    file.setAttribute("foo", "foo", "foo");
+
+    assertThat(file.getAttributeKeys()).containsExactly("foo:foo");
+    assertThat(file.getAttribute("foo", "foo")).isEqualTo("foo");
+
+    file.deleteAttribute("foo", "foo");
+
+    assertThat(file.getAttributeKeys()).isEmpty();
+    assertThat(file.getAttribute("foo", "foo")).isNull();
+  }
+
+  @Test
+  public void testFileBasics() {
+    File file = regularFile(0);
+
+    assertThat(file.id()).isEqualTo(0);
+    assertThat(file.links()).isEqualTo(0);
+  }
+
+  @Test
+  public void testDirectory() {
+    File file = Directory.create(0);
+    assertThat(file.isDirectory()).isTrue();
+    assertThat(file.isRegularFile()).isFalse();
+    assertThat(file.isSymbolicLink()).isFalse();
+  }
+
+  @Test
+  public void testRegularFile() {
+    File file = regularFile(10);
+    assertThat(file.isDirectory()).isFalse();
+    assertThat(file.isRegularFile()).isTrue();
+    assertThat(file.isSymbolicLink()).isFalse();
+  }
+
+  @Test
+  public void testSymbolicLink() {
+    File file = SymbolicLink.create(0, fakePath());
+    assertThat(file.isDirectory()).isFalse();
+    assertThat(file.isRegularFile()).isFalse();
+    assertThat(file.isSymbolicLink()).isTrue();
+  }
+
+  @Test
+  public void testRootDirectory() {
+    Directory file = Directory.createRoot(0, Name.simple("/"));
+    assertThat(file.isRootDirectory()).isTrue();
+
+    Directory otherFile = Directory.createRoot(1, Name.simple("$"));
+    assertThat(otherFile.isRootDirectory()).isTrue();
+  }
+
+  @Test
+  public void testLinkAndUnlink() {
+    File file = regularFile(0);
+    assertThat(file.links()).isEqualTo(0);
+
+    file.incrementLinkCount();
+    assertThat(file.links()).isEqualTo(1);
+
+    file.incrementLinkCount();
+    assertThat(file.links()).isEqualTo(2);
+
+    file.decrementLinkCount();
+    assertThat(file.links()).isEqualTo(1);
+
+    file.decrementLinkCount();
+    assertThat(file.links()).isEqualTo(0);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/FileTreeTest.java b/jimfs/src/test/java/com/google/common/jimfs/FileTreeTest.java
new file mode 100644
index 0000000..54f590d
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/FileTreeTest.java
@@ -0,0 +1,463 @@
+/*
+ * 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.TestUtils.regularFile;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link FileTree}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class FileTreeTest {
+
+  /*
+   * Directory structure. Each file should have a unique name.
+   *
+   * /
+   *   work/
+   *     one/
+   *       two/
+   *         three/
+   *       eleven
+   *     four/
+   *       five -> /foo
+   *       six -> ../one
+   *       loop -> ../four/loop
+   *   foo/
+   *     bar/
+   * $
+   *   a/
+   *     b/
+   *       c/
+   */
+
+  /**
+   * This path service is for unix-like paths, with the exception that it recognizes $ and ! as
+   * roots in addition to /, allowing for up to three roots. When creating a {@linkplain
+   * PathType#toUriPath URI path}, we prefix the path with / to differentiate between a path like
+   * "$foo/bar" and one like "/$foo/bar". They would become "/$foo/bar" and "//$foo/bar"
+   * respectively.
+   */
+  private final PathService pathService =
+      PathServiceTest.fakePathService(
+          new PathType(true, '/') {
+            @Override
+            public ParseResult parsePath(String path) {
+              String root = null;
+              if (path.matches("^[/$!].*")) {
+                root = path.substring(0, 1);
+                path = path.substring(1);
+              }
+              return new ParseResult(root, Splitter.on('/').omitEmptyStrings().split(path));
+            }
+
+            @Override
+            public String toString(@NullableDecl String root, Iterable<String> names) {
+              root = Strings.nullToEmpty(root);
+              return root + Joiner.on('/').join(names);
+            }
+
+            @Override
+            public String toUriPath(String root, Iterable<String> names, boolean directory) {
+              // need to add extra / to differentiate between paths "/$foo/bar" and "$foo/bar".
+              return "/" + toString(root, names);
+            }
+
+            @Override
+            public ParseResult parseUriPath(String uriPath) {
+              checkArgument(
+                  uriPath.matches("^/[/$!].*"), "uriPath (%s) must start with // or /$ or /!");
+              return parsePath(uriPath.substring(1)); // skip leading /
+            }
+          },
+          false);
+
+  private FileTree fileTree;
+  private File workingDirectory;
+  private final Map<String, File> files = new HashMap<>();
+
+  @Before
+  public void setUp() {
+    Directory root = Directory.createRoot(0, Name.simple("/"));
+    files.put("/", root);
+
+    Directory otherRoot = Directory.createRoot(2, Name.simple("$"));
+    files.put("$", otherRoot);
+
+    Map<Name, Directory> roots = new HashMap<>();
+    roots.put(Name.simple("/"), root);
+    roots.put(Name.simple("$"), otherRoot);
+
+    fileTree = new FileTree(roots);
+
+    workingDirectory = createDirectory("/", "work");
+
+    createDirectory("work", "one");
+    createDirectory("one", "two");
+    createFile("one", "eleven");
+    createDirectory("two", "three");
+    createDirectory("work", "four");
+    createSymbolicLink("four", "five", "/foo");
+    createSymbolicLink("four", "six", "../one");
+    createSymbolicLink("four", "loop", "../four/loop");
+    createDirectory("/", "foo");
+    createDirectory("foo", "bar");
+    createDirectory("$", "a");
+    createDirectory("a", "b");
+    createDirectory("b", "c");
+  }
+
+  // absolute lookups
+
+  @Test
+  public void testLookup_root() throws IOException {
+    assertExists(lookup("/"), "/", "/");
+    assertExists(lookup("$"), "$", "$");
+  }
+
+  @Test
+  public void testLookup_nonExistentRoot() throws IOException {
+    try {
+      lookup("!");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("!a");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_absolute() throws IOException {
+    assertExists(lookup("/work"), "/", "work");
+    assertExists(lookup("/work/one/two/three"), "two", "three");
+    assertExists(lookup("$a"), "$", "a");
+    assertExists(lookup("$a/b/c"), "b", "c");
+  }
+
+  @Test
+  public void testLookup_absolute_notExists() throws IOException {
+    try {
+      lookup("/a/b");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("/work/one/foo/bar");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("$c/d");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("$a/b/c/d/e");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_absolute_parentExists() throws IOException {
+    assertParentExists(lookup("/a"), "/");
+    assertParentExists(lookup("/foo/baz"), "foo");
+    assertParentExists(lookup("$c"), "$");
+    assertParentExists(lookup("$a/b/c/d"), "c");
+  }
+
+  @Test
+  public void testLookup_absolute_nonDirectoryIntermediateFile() throws IOException {
+    try {
+      lookup("/work/one/eleven/twelve");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("/work/one/eleven/twelve/thirteen/fourteen");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_absolute_intermediateSymlink() throws IOException {
+    assertExists(lookup("/work/four/five/bar"), "foo", "bar");
+    assertExists(lookup("/work/four/six/two/three"), "two", "three");
+
+    // NOFOLLOW_LINKS doesn't affect intermediate symlinks
+    assertExists(lookup("/work/four/five/bar", NOFOLLOW_LINKS), "foo", "bar");
+    assertExists(lookup("/work/four/six/two/three", NOFOLLOW_LINKS), "two", "three");
+  }
+
+  @Test
+  public void testLookup_absolute_intermediateSymlink_parentExists() throws IOException {
+    assertParentExists(lookup("/work/four/five/baz"), "foo");
+    assertParentExists(lookup("/work/four/six/baz"), "one");
+  }
+
+  @Test
+  public void testLookup_absolute_finalSymlink() throws IOException {
+    assertExists(lookup("/work/four/five"), "/", "foo");
+    assertExists(lookup("/work/four/six"), "work", "one");
+  }
+
+  @Test
+  public void testLookup_absolute_finalSymlink_nofollowLinks() throws IOException {
+    assertExists(lookup("/work/four/five", NOFOLLOW_LINKS), "four", "five");
+    assertExists(lookup("/work/four/six", NOFOLLOW_LINKS), "four", "six");
+    assertExists(lookup("/work/four/loop", NOFOLLOW_LINKS), "four", "loop");
+  }
+
+  @Test
+  public void testLookup_absolute_symlinkLoop() {
+    try {
+      lookup("/work/four/loop");
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      lookup("/work/four/loop/whatever");
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_absolute_withDotsInPath() throws IOException {
+    assertExists(lookup("/."), "/", "/");
+    assertExists(lookup("/./././."), "/", "/");
+    assertExists(lookup("/work/./one/./././two/three"), "two", "three");
+    assertExists(lookup("/work/./one/./././two/././three"), "two", "three");
+    assertExists(lookup("/work/./one/./././two/three/././."), "two", "three");
+  }
+
+  @Test
+  public void testLookup_absolute_withDotDotsInPath() throws IOException {
+    assertExists(lookup("/.."), "/", "/");
+    assertExists(lookup("/../../.."), "/", "/");
+    assertExists(lookup("/work/.."), "/", "/");
+    assertExists(lookup("/work/../work/one/two/../two/three"), "two", "three");
+    assertExists(lookup("/work/one/two/../../four/../one/two/three/../three"), "two", "three");
+    assertExists(lookup("/work/one/two/three/../../two/three/.."), "one", "two");
+    assertExists(lookup("/work/one/two/three/../../two/three/../.."), "work", "one");
+  }
+
+  @Test
+  public void testLookup_absolute_withDotDotsInPath_afterSymlink() throws IOException {
+    assertExists(lookup("/work/four/five/.."), "/", "/");
+    assertExists(lookup("/work/four/six/.."), "/", "work");
+  }
+
+  // relative lookups
+
+  @Test
+  public void testLookup_relative() throws IOException {
+    assertExists(lookup("one"), "work", "one");
+    assertExists(lookup("one/two/three"), "two", "three");
+  }
+
+  @Test
+  public void testLookup_relative_notExists() throws IOException {
+    try {
+      lookup("a/b");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("one/foo/bar");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_relative_parentExists() throws IOException {
+    assertParentExists(lookup("a"), "work");
+    assertParentExists(lookup("one/two/four"), "two");
+  }
+
+  @Test
+  public void testLookup_relative_nonDirectoryIntermediateFile() throws IOException {
+    try {
+      lookup("one/eleven/twelve");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    try {
+      lookup("one/eleven/twelve/thirteen/fourteen");
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_relative_intermediateSymlink() throws IOException {
+    assertExists(lookup("four/five/bar"), "foo", "bar");
+    assertExists(lookup("four/six/two/three"), "two", "three");
+
+    // NOFOLLOW_LINKS doesn't affect intermediate symlinks
+    assertExists(lookup("four/five/bar", NOFOLLOW_LINKS), "foo", "bar");
+    assertExists(lookup("four/six/two/three", NOFOLLOW_LINKS), "two", "three");
+  }
+
+  @Test
+  public void testLookup_relative_intermediateSymlink_parentExists() throws IOException {
+    assertParentExists(lookup("four/five/baz"), "foo");
+    assertParentExists(lookup("four/six/baz"), "one");
+  }
+
+  @Test
+  public void testLookup_relative_finalSymlink() throws IOException {
+    assertExists(lookup("four/five"), "/", "foo");
+    assertExists(lookup("four/six"), "work", "one");
+  }
+
+  @Test
+  public void testLookup_relative_finalSymlink_nofollowLinks() throws IOException {
+    assertExists(lookup("four/five", NOFOLLOW_LINKS), "four", "five");
+    assertExists(lookup("four/six", NOFOLLOW_LINKS), "four", "six");
+    assertExists(lookup("four/loop", NOFOLLOW_LINKS), "four", "loop");
+  }
+
+  @Test
+  public void testLookup_relative_symlinkLoop() {
+    try {
+      lookup("four/loop");
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      lookup("four/loop/whatever");
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testLookup_relative_emptyPath() throws IOException {
+    assertExists(lookup(""), "/", "work");
+  }
+
+  @Test
+  public void testLookup_relative_withDotsInPath() throws IOException {
+    assertExists(lookup("."), "/", "work");
+    assertExists(lookup("././."), "/", "work");
+    assertExists(lookup("./one/./././two/three"), "two", "three");
+    assertExists(lookup("./one/./././two/././three"), "two", "three");
+    assertExists(lookup("./one/./././two/three/././."), "two", "three");
+  }
+
+  @Test
+  public void testLookup_relative_withDotDotsInPath() throws IOException {
+    assertExists(lookup(".."), "/", "/");
+    assertExists(lookup("../../.."), "/", "/");
+    assertExists(lookup("../work"), "/", "work");
+    assertExists(lookup("../../work"), "/", "work");
+    assertExists(lookup("../foo"), "/", "foo");
+    assertExists(lookup("../work/one/two/../two/three"), "two", "three");
+    assertExists(lookup("one/two/../../four/../one/two/three/../three"), "two", "three");
+    assertExists(lookup("one/two/three/../../two/three/.."), "one", "two");
+    assertExists(lookup("one/two/three/../../two/three/../.."), "work", "one");
+  }
+
+  @Test
+  public void testLookup_relative_withDotDotsInPath_afterSymlink() throws IOException {
+    assertExists(lookup("four/five/.."), "/", "/");
+    assertExists(lookup("four/six/.."), "/", "work");
+  }
+
+  private DirectoryEntry lookup(String path, LinkOption... options) throws IOException {
+    JimfsPath pathObj = pathService.parsePath(path);
+    return fileTree.lookUp(workingDirectory, pathObj, Options.getLinkOptions(options));
+  }
+
+  private void assertExists(DirectoryEntry entry, String parent, String file) {
+    assertThat(entry.exists()).isTrue();
+    assertThat(entry.name()).isEqualTo(Name.simple(file));
+    assertThat(entry.directory()).isEqualTo(files.get(parent));
+    assertThat(entry.file()).isEqualTo(files.get(file));
+  }
+
+  private void assertParentExists(DirectoryEntry entry, String parent) {
+    assertThat(entry.exists()).isFalse();
+    assertThat(entry.directory()).isEqualTo(files.get(parent));
+
+    try {
+      entry.file();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  private File createDirectory(String parent, String name) {
+    Directory dir = (Directory) files.get(parent);
+    Directory newFile = Directory.create(new Random().nextInt());
+    dir.link(Name.simple(name), newFile);
+    files.put(name, newFile);
+    return newFile;
+  }
+
+  private File createFile(String parent, String name) {
+    Directory dir = (Directory) files.get(parent);
+    File newFile = regularFile(0);
+    dir.link(Name.simple(name), newFile);
+    files.put(name, newFile);
+    return newFile;
+  }
+
+  private File createSymbolicLink(String parent, String name, String target) {
+    Directory dir = (Directory) files.get(parent);
+    File newFile = SymbolicLink.create(new Random().nextInt(), pathService.parsePath(target));
+    dir.link(Name.simple(name), newFile);
+    files.put(name, newFile);
+    return newFile;
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/HeapDiskTest.java b/jimfs/src/test/java/com/google/common/jimfs/HeapDiskTest.java
new file mode 100644
index 0000000..af09b85
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/HeapDiskTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link HeapDisk}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class HeapDiskTest {
+
+  private RegularFile blocks;
+
+  @Before
+  public void setUp() {
+    // the HeapDisk of this file is unused; it's passed to other HeapDisks to test operations
+    blocks = RegularFile.create(-1, new HeapDisk(2, 2, 2));
+  }
+
+  @Test
+  public void testInitialSettings_basic() {
+    HeapDisk disk = new HeapDisk(8192, 100, 100);
+
+    assertThat(disk.blockSize()).isEqualTo(8192);
+    assertThat(disk.getTotalSpace()).isEqualTo(819200);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(819200);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(0);
+  }
+
+  @Test
+  public void testInitialSettings_fromConfiguration() {
+    Configuration config =
+        Configuration.unix().toBuilder()
+            .setBlockSize(4)
+            .setMaxSize(99) // not a multiple of 4
+            .setMaxCacheSize(25)
+            .build();
+
+    HeapDisk disk = new HeapDisk(config);
+
+    assertThat(disk.blockSize()).isEqualTo(4);
+    assertThat(disk.getTotalSpace()).isEqualTo(96);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(96);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(0);
+  }
+
+  @Test
+  public void testAllocate() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 0);
+
+    disk.allocate(blocks, 1);
+
+    assertThat(blocks.blockCount()).isEqualTo(1);
+    assertThat(blocks.getBlock(0).length).isEqualTo(4);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(36);
+
+    disk.allocate(blocks, 5);
+
+    assertThat(blocks.blockCount()).isEqualTo(6);
+    for (int i = 0; i < blocks.blockCount(); i++) {
+      assertThat(blocks.getBlock(i).length).isEqualTo(4);
+    }
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(16);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(0);
+  }
+
+  @Test
+  public void testFree_noCaching() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 0);
+    disk.allocate(blocks, 6);
+
+    disk.free(blocks, 2);
+    assertThat(blocks.blockCount()).isEqualTo(4);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(24);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(0);
+
+    disk.free(blocks);
+
+    assertThat(blocks.blockCount()).isEqualTo(0);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(40);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(0);
+  }
+
+  @Test
+  public void testFree_fullCaching() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 10);
+    disk.allocate(blocks, 6);
+
+    disk.free(blocks, 2);
+
+    assertThat(blocks.blockCount()).isEqualTo(4);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(24);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(2);
+
+    disk.free(blocks);
+
+    assertThat(blocks.blockCount()).isEqualTo(0);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(40);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(6);
+  }
+
+  @Test
+  public void testFree_partialCaching() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 4);
+    disk.allocate(blocks, 6);
+
+    disk.free(blocks, 2);
+
+    assertThat(blocks.blockCount()).isEqualTo(4);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(24);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(2);
+
+    disk.free(blocks);
+
+    assertThat(blocks.blockCount()).isEqualTo(0);
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(40);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(4);
+  }
+
+  @Test
+  public void testAllocateFromCache_fullAllocationFromCache() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 10);
+    disk.allocate(blocks, 10);
+
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(0);
+
+    disk.free(blocks);
+
+    assertThat(blocks.blockCount()).isEqualTo(0);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(10);
+
+    List<byte[]> cachedBlocks = new ArrayList<>();
+    for (int i = 0; i < 10; i++) {
+      cachedBlocks.add(disk.blockCache.getBlock(i));
+    }
+
+    disk.allocate(blocks, 6);
+
+    assertThat(blocks.blockCount()).isEqualTo(6);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(4);
+
+    // the 6 arrays in blocks are the last 6 arrays that were cached
+    for (int i = 0; i < 6; i++) {
+      assertThat(blocks.getBlock(i)).isEqualTo(cachedBlocks.get(i + 4));
+    }
+  }
+
+  @Test
+  public void testAllocateFromCache_partialAllocationFromCache() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 4);
+    disk.allocate(blocks, 10);
+
+    assertThat(disk.getUnallocatedSpace()).isEqualTo(0);
+
+    disk.free(blocks);
+
+    assertThat(blocks.blockCount()).isEqualTo(0);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(4);
+
+    List<byte[]> cachedBlocks = new ArrayList<>();
+    for (int i = 0; i < 4; i++) {
+      cachedBlocks.add(disk.blockCache.getBlock(i));
+    }
+
+    disk.allocate(blocks, 6);
+
+    assertThat(blocks.blockCount()).isEqualTo(6);
+    assertThat(disk.blockCache.blockCount()).isEqualTo(0);
+
+    // the last 4 arrays in blocks are the 4 arrays that were cached
+    for (int i = 2; i < 6; i++) {
+      assertThat(blocks.getBlock(i)).isEqualTo(cachedBlocks.get(i - 2));
+    }
+  }
+
+  @Test
+  public void testFullDisk() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 4);
+    disk.allocate(blocks, 10);
+
+    try {
+      disk.allocate(blocks, 1);
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testFullDisk_doesNotAllocatePartiallyWhenTooManyBlocksRequested() throws IOException {
+    HeapDisk disk = new HeapDisk(4, 10, 4);
+    disk.allocate(blocks, 6);
+
+    RegularFile blocks2 = RegularFile.create(-2, disk);
+
+    try {
+      disk.allocate(blocks2, 5);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    assertThat(blocks2.blockCount()).isEqualTo(0);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsAsynchronousFileChannelTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsAsynchronousFileChannelTest.java
new file mode 100644
index 0000000..7d47588
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsAsynchronousFileChannelTest.java
@@ -0,0 +1,262 @@
+/*
+ * 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.jimfs.TestUtils.buffer;
+import static com.google.common.jimfs.TestUtils.regularFile;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Runnables;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.FileLock;
+import java.nio.file.OpenOption;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link JimfsAsynchronousFileChannel}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsAsynchronousFileChannelTest {
+
+  private static JimfsAsynchronousFileChannel channel(
+      RegularFile file, ExecutorService executor, OpenOption... options) throws IOException {
+    JimfsFileChannel channel =
+        new JimfsFileChannel(
+            file,
+            Options.getOptionsForChannel(ImmutableSet.copyOf(options)),
+            new FileSystemState(Runnables.doNothing()));
+    return new JimfsAsynchronousFileChannel(channel, executor);
+  }
+
+  /**
+   * Just tests the main read/write methods... the methods all delegate to the non-async channel
+   * anyway.
+   */
+  @Test
+  public void testAsyncChannel() throws Throwable {
+    RegularFile file = regularFile(15);
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    JimfsAsynchronousFileChannel channel = channel(file, executor, READ, WRITE);
+
+    try {
+      assertEquals(15, channel.size());
+
+      assertSame(channel, channel.truncate(5));
+      assertEquals(5, channel.size());
+
+      file.write(5, new byte[5], 0, 5);
+      checkAsyncRead(channel);
+      checkAsyncWrite(channel);
+      checkAsyncLock(channel);
+
+      channel.close();
+      assertFalse(channel.isOpen());
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  @Test
+  public void testClosedChannel() throws Throwable {
+    RegularFile file = regularFile(15);
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    try {
+      JimfsAsynchronousFileChannel channel = channel(file, executor, READ, WRITE);
+      channel.close();
+
+      assertClosed(channel.read(ByteBuffer.allocate(10), 0));
+      assertClosed(channel.write(ByteBuffer.allocate(10), 15));
+      assertClosed(channel.lock());
+      assertClosed(channel.lock(0, 10, true));
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  @Test
+  public void testAsyncClose_write() throws Throwable {
+    RegularFile file = regularFile(15);
+    ExecutorService executor = Executors.newFixedThreadPool(4);
+
+    try {
+      JimfsAsynchronousFileChannel channel = channel(file, executor, READ, WRITE);
+
+      file.writeLock().lock(); // cause another thread trying to write to block
+
+      // future-returning write
+      Future<Integer> future = channel.write(ByteBuffer.allocate(10), 0);
+
+      // completion handler write
+      SettableFuture<Integer> completionHandlerFuture = SettableFuture.create();
+      channel.write(ByteBuffer.allocate(10), 0, null, setFuture(completionHandlerFuture));
+
+      // Despite this 10ms sleep to allow plenty of time, it's possible, though very rare, for a
+      // race to cause the channel to be closed before the asynchronous calls get to the initial
+      // check that the channel is open, causing ClosedChannelException to be thrown rather than
+      // AsynchronousCloseException. This is not a problem in practice, just a quirk of how these
+      // tests work and that we don't have a way of waiting for the operations to get past that
+      // check.
+      Uninterruptibles.sleepUninterruptibly(10, MILLISECONDS);
+
+      channel.close();
+
+      assertAsynchronousClose(future);
+      assertAsynchronousClose(completionHandlerFuture);
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  @Test
+  public void testAsyncClose_read() throws Throwable {
+    RegularFile file = regularFile(15);
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+
+    try {
+      JimfsAsynchronousFileChannel channel = channel(file, executor, READ, WRITE);
+
+      file.writeLock().lock(); // cause another thread trying to read to block
+
+      // future-returning read
+      Future<Integer> future = channel.read(ByteBuffer.allocate(10), 0);
+
+      // completion handler read
+      SettableFuture<Integer> completionHandlerFuture = SettableFuture.create();
+      channel.read(ByteBuffer.allocate(10), 0, null, setFuture(completionHandlerFuture));
+
+      // Despite this 10ms sleep to allow plenty of time, it's possible, though very rare, for a
+      // race to cause the channel to be closed before the asynchronous calls get to the initial
+      // check that the channel is open, causing ClosedChannelException to be thrown rather than
+      // AsynchronousCloseException. This is not a problem in practice, just a quirk of how these
+      // tests work and that we don't have a way of waiting for the operations to get past that
+      // check.
+      Uninterruptibles.sleepUninterruptibly(10, MILLISECONDS);
+
+      channel.close();
+
+      assertAsynchronousClose(future);
+      assertAsynchronousClose(completionHandlerFuture);
+    } finally {
+      executor.shutdown();
+    }
+  }
+
+  private static void checkAsyncRead(AsynchronousFileChannel channel) throws Throwable {
+    ByteBuffer buf = buffer("1234567890");
+    assertEquals(10, (int) channel.read(buf, 0).get());
+
+    buf.flip();
+
+    SettableFuture<Integer> future = SettableFuture.create();
+    channel.read(buf, 0, null, setFuture(future));
+
+    assertThat(future.get(10, SECONDS)).isEqualTo(10);
+  }
+
+  private static void checkAsyncWrite(AsynchronousFileChannel asyncChannel) throws Throwable {
+    ByteBuffer buf = buffer("1234567890");
+    assertEquals(10, (int) asyncChannel.write(buf, 0).get());
+
+    buf.flip();
+    SettableFuture<Integer> future = SettableFuture.create();
+    asyncChannel.write(buf, 0, null, setFuture(future));
+
+    assertThat(future.get(10, SECONDS)).isEqualTo(10);
+  }
+
+  private static void checkAsyncLock(AsynchronousFileChannel channel) throws Throwable {
+    assertNotNull(channel.lock().get());
+    assertNotNull(channel.lock(0, 10, true).get());
+
+    SettableFuture<FileLock> future = SettableFuture.create();
+    channel.lock(0, 10, true, null, setFuture(future));
+
+    assertNotNull(future.get(10, SECONDS));
+  }
+
+  /**
+   * Returns a {@code CompletionHandler} that sets the appropriate result or exception on the given
+   * {@code future} on completion.
+   */
+  private static <T> CompletionHandler<T, Object> setFuture(final SettableFuture<T> future) {
+    return new CompletionHandler<T, Object>() {
+      @Override
+      public void completed(T result, Object attachment) {
+        future.set(result);
+      }
+
+      @Override
+      public void failed(Throwable exc, Object attachment) {
+        future.setException(exc);
+      }
+    };
+  }
+
+  /** Assert that the future fails, with the failure caused by {@code ClosedChannelException}. */
+  private static void assertClosed(Future<?> future) throws Throwable {
+    try {
+      future.get(10, SECONDS);
+      fail("ChannelClosedException was not thrown");
+    } catch (ExecutionException expected) {
+      assertThat(expected.getCause()).isInstanceOf(ClosedChannelException.class);
+    }
+  }
+
+  /**
+   * Assert that the future fails, with the failure caused by either {@code
+   * AsynchronousCloseException} or (rarely) {@code ClosedChannelException}.
+   */
+  private static void assertAsynchronousClose(Future<?> future) throws Throwable {
+    try {
+      future.get(10, SECONDS);
+      fail("no exception was thrown");
+    } catch (ExecutionException expected) {
+      Throwable t = expected.getCause();
+      if (!(t instanceof AsynchronousCloseException || t instanceof ClosedChannelException)) {
+        fail(
+            "expected AsynchronousCloseException (or in rare cases ClosedChannelException); got "
+                + t);
+      }
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsFileChannelTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsFileChannelTest.java
new file mode 100644
index 0000000..c525ef1
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsFileChannelTest.java
@@ -0,0 +1,1049 @@
+/*
+ * 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.jimfs.TestUtils.assertNotEquals;
+import static com.google.common.jimfs.TestUtils.buffer;
+import static com.google.common.jimfs.TestUtils.bytes;
+import static com.google.common.jimfs.TestUtils.regularFile;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.file.StandardOpenOption.APPEND;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.NullPointerTester;
+import com.google.common.util.concurrent.Runnables;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
+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.file.OpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Most of the behavior of {@link JimfsFileChannel} is handled by the {@link RegularFile}
+ * implementations, so the thorough tests of that are in {@link RegularFileTest}. This mostly tests
+ * interactions with the file and channel positions.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsFileChannelTest {
+
+  private static FileChannel channel(RegularFile file, OpenOption... options) throws IOException {
+    return new JimfsFileChannel(
+        file,
+        Options.getOptionsForChannel(ImmutableSet.copyOf(options)),
+        new FileSystemState(Runnables.doNothing()));
+  }
+
+  @Test
+  public void testPosition() throws IOException {
+    FileChannel channel = channel(regularFile(10), READ);
+    assertEquals(0, channel.position());
+    assertSame(channel, channel.position(100));
+    assertEquals(100, channel.position());
+  }
+
+  @Test
+  public void testSize() throws IOException {
+    RegularFile file = regularFile(10);
+    FileChannel channel = channel(file, READ);
+
+    assertEquals(10, channel.size());
+
+    file.write(10, new byte[90], 0, 90);
+    assertEquals(100, channel.size());
+  }
+
+  @Test
+  public void testRead() throws IOException {
+    RegularFile file = regularFile(20);
+    FileChannel channel = channel(file, READ);
+    assertEquals(0, channel.position());
+
+    ByteBuffer buf = buffer("1234567890");
+    ByteBuffer buf2 = buffer("123457890");
+    assertEquals(10, channel.read(buf));
+    assertEquals(10, channel.position());
+
+    buf.flip();
+    assertEquals(10, channel.read(new ByteBuffer[] {buf, buf2}));
+    assertEquals(20, channel.position());
+
+    buf.flip();
+    buf2.flip();
+    file.write(20, new byte[10], 0, 10);
+    assertEquals(10, channel.read(new ByteBuffer[] {buf, buf2}, 0, 2));
+    assertEquals(30, channel.position());
+
+    buf.flip();
+    assertEquals(10, channel.read(buf, 5));
+    assertEquals(30, channel.position());
+
+    buf.flip();
+    assertEquals(-1, channel.read(buf));
+    assertEquals(30, channel.position());
+  }
+
+  @Test
+  public void testWrite() throws IOException {
+    RegularFile file = regularFile(0);
+    FileChannel channel = channel(file, WRITE);
+    assertEquals(0, channel.position());
+
+    ByteBuffer buf = buffer("1234567890");
+    ByteBuffer buf2 = buffer("1234567890");
+    assertEquals(10, channel.write(buf));
+    assertEquals(10, channel.position());
+
+    buf.flip();
+    assertEquals(20, channel.write(new ByteBuffer[] {buf, buf2}));
+    assertEquals(30, channel.position());
+
+    buf.flip();
+    buf2.flip();
+    assertEquals(20, channel.write(new ByteBuffer[] {buf, buf2}, 0, 2));
+    assertEquals(50, channel.position());
+
+    buf.flip();
+    assertEquals(10, channel.write(buf, 5));
+    assertEquals(50, channel.position());
+  }
+
+  @Test
+  public void testAppend() throws IOException {
+    RegularFile file = regularFile(0);
+    FileChannel channel = channel(file, WRITE, APPEND);
+    assertEquals(0, channel.position());
+
+    ByteBuffer buf = buffer("1234567890");
+    ByteBuffer buf2 = buffer("1234567890");
+
+    assertEquals(10, channel.write(buf));
+    assertEquals(10, channel.position());
+
+    buf.flip();
+    channel.position(0);
+    assertEquals(20, channel.write(new ByteBuffer[] {buf, buf2}));
+    assertEquals(30, channel.position());
+
+    buf.flip();
+    buf2.flip();
+    channel.position(0);
+    assertEquals(20, channel.write(new ByteBuffer[] {buf, buf2}, 0, 2));
+    assertEquals(50, channel.position());
+
+    buf.flip();
+    channel.position(0);
+    assertEquals(10, channel.write(buf, 5));
+    assertEquals(60, channel.position());
+
+    buf.flip();
+    channel.position(0);
+    assertEquals(10, channel.transferFrom(new ByteBufferChannel(buf), 0, 10));
+    assertEquals(70, channel.position());
+  }
+
+  @Test
+  public void testTransferTo() throws IOException {
+    RegularFile file = regularFile(10);
+    FileChannel channel = channel(file, READ);
+
+    ByteBufferChannel writeChannel = new ByteBufferChannel(buffer("1234567890"));
+    assertEquals(10, channel.transferTo(0, 100, writeChannel));
+    assertEquals(0, channel.position());
+  }
+
+  @Test
+  public void testTransferFrom() throws IOException {
+    RegularFile file = regularFile(0);
+    FileChannel channel = channel(file, WRITE);
+
+    ByteBufferChannel readChannel = new ByteBufferChannel(buffer("1234567890"));
+    assertEquals(10, channel.transferFrom(readChannel, 0, 100));
+    assertEquals(0, channel.position());
+  }
+
+  @Test
+  public void testTruncate() throws IOException {
+    RegularFile file = regularFile(10);
+    FileChannel channel = channel(file, WRITE);
+
+    channel.truncate(10); // no resize, >= size
+    assertEquals(10, file.size());
+    channel.truncate(11); // no resize, > size
+    assertEquals(10, file.size());
+    channel.truncate(5); // resize down to 5
+    assertEquals(5, file.size());
+
+    channel.position(20);
+    channel.truncate(10);
+    assertEquals(10, channel.position());
+    channel.truncate(2);
+    assertEquals(2, channel.position());
+  }
+
+  @Test
+  public void testFileTimeUpdates() throws IOException {
+    RegularFile file = regularFile(10);
+    FileChannel channel =
+        new JimfsFileChannel(
+            file,
+            ImmutableSet.<OpenOption>of(READ, WRITE),
+            new FileSystemState(Runnables.doNothing()));
+
+    // accessed
+    long accessTime = file.getLastAccessTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.read(ByteBuffer.allocate(10));
+    assertNotEquals(accessTime, file.getLastAccessTime());
+
+    accessTime = file.getLastAccessTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.read(ByteBuffer.allocate(10), 0);
+    assertNotEquals(accessTime, file.getLastAccessTime());
+
+    accessTime = file.getLastAccessTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.read(new ByteBuffer[] {ByteBuffer.allocate(10)});
+    assertNotEquals(accessTime, file.getLastAccessTime());
+
+    accessTime = file.getLastAccessTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.read(new ByteBuffer[] {ByteBuffer.allocate(10)}, 0, 1);
+    assertNotEquals(accessTime, file.getLastAccessTime());
+
+    accessTime = file.getLastAccessTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.transferTo(0, 10, new ByteBufferChannel(10));
+    assertNotEquals(accessTime, file.getLastAccessTime());
+
+    // modified
+    long modifiedTime = file.getLastModifiedTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.write(ByteBuffer.allocate(10));
+    assertNotEquals(modifiedTime, file.getLastModifiedTime());
+
+    modifiedTime = file.getLastModifiedTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.write(ByteBuffer.allocate(10), 0);
+    assertNotEquals(modifiedTime, file.getLastModifiedTime());
+
+    modifiedTime = file.getLastModifiedTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.write(new ByteBuffer[] {ByteBuffer.allocate(10)});
+    assertNotEquals(modifiedTime, file.getLastModifiedTime());
+
+    modifiedTime = file.getLastModifiedTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.write(new ByteBuffer[] {ByteBuffer.allocate(10)}, 0, 1);
+    assertNotEquals(modifiedTime, file.getLastModifiedTime());
+
+    modifiedTime = file.getLastModifiedTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.truncate(0);
+    assertNotEquals(modifiedTime, file.getLastModifiedTime());
+
+    modifiedTime = file.getLastModifiedTime();
+    Uninterruptibles.sleepUninterruptibly(2, MILLISECONDS);
+
+    channel.transferFrom(new ByteBufferChannel(10), 0, 10);
+    assertNotEquals(modifiedTime, file.getLastModifiedTime());
+  }
+
+  @Test
+  public void testClose() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    assertTrue(channel.isOpen());
+    channel.close();
+    assertFalse(channel.isOpen());
+
+    try {
+      channel.position();
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.position(0);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.lock();
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.lock(0, 10, true);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.tryLock();
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.tryLock(0, 10, true);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.force(true);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.write(buffer("111"));
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.write(buffer("111"), 10);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.write(new ByteBuffer[] {buffer("111"), buffer("111")});
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.write(new ByteBuffer[] {buffer("111"), buffer("111")}, 0, 2);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.transferFrom(new ByteBufferChannel(bytes("1111")), 0, 4);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.truncate(0);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.read(buffer("111"));
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.read(buffer("111"), 10);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.read(new ByteBuffer[] {buffer("111"), buffer("111")});
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.read(new ByteBuffer[] {buffer("111"), buffer("111")}, 0, 2);
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      channel.transferTo(0, 10, new ByteBufferChannel(buffer("111")));
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    executor.shutdown();
+  }
+
+  @Test
+  public void testWritesInReadOnlyMode() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ);
+
+    try {
+      channel.write(buffer("111"));
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+
+    try {
+      channel.write(buffer("111"), 10);
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+
+    try {
+      channel.write(new ByteBuffer[] {buffer("111"), buffer("111")});
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+
+    try {
+      channel.write(new ByteBuffer[] {buffer("111"), buffer("111")}, 0, 2);
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+
+    try {
+      channel.transferFrom(new ByteBufferChannel(bytes("1111")), 0, 4);
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+
+    try {
+      channel.truncate(0);
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+
+    try {
+      channel.lock(0, 10, false);
+      fail();
+    } catch (NonWritableChannelException expected) {
+    }
+  }
+
+  @Test
+  public void testReadsInWriteOnlyMode() throws IOException {
+    FileChannel channel = channel(regularFile(0), WRITE);
+
+    try {
+      channel.read(buffer("111"));
+      fail();
+    } catch (NonReadableChannelException expected) {
+    }
+
+    try {
+      channel.read(buffer("111"), 10);
+      fail();
+    } catch (NonReadableChannelException expected) {
+    }
+
+    try {
+      channel.read(new ByteBuffer[] {buffer("111"), buffer("111")});
+      fail();
+    } catch (NonReadableChannelException expected) {
+    }
+
+    try {
+      channel.read(new ByteBuffer[] {buffer("111"), buffer("111")}, 0, 2);
+      fail();
+    } catch (NonReadableChannelException expected) {
+    }
+
+    try {
+      channel.transferTo(0, 10, new ByteBufferChannel(buffer("111")));
+      fail();
+    } catch (NonReadableChannelException expected) {
+    }
+
+    try {
+      channel.lock(0, 10, true);
+      fail();
+    } catch (NonReadableChannelException expected) {
+    }
+  }
+
+  @Test
+  public void testPositionNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.position(-1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testTruncateNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.truncate(-1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testWriteNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.write(buffer("111"), -1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    ByteBuffer[] bufs = {buffer("111"), buffer("111")};
+    try {
+      channel.write(bufs, -1, 10);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+
+    try {
+      channel.write(bufs, 0, -1);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test
+  public void testReadNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.read(buffer("111"), -1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    ByteBuffer[] bufs = {buffer("111"), buffer("111")};
+    try {
+      channel.read(bufs, -1, 10);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+
+    try {
+      channel.read(bufs, 0, -1);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test
+  public void testTransferToNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.transferTo(-1, 0, new ByteBufferChannel(10));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      channel.transferTo(0, -1, new ByteBufferChannel(10));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testTransferFromNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.transferFrom(new ByteBufferChannel(10), -1, 0);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      channel.transferFrom(new ByteBufferChannel(10), 0, -1);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testLockNegative() throws IOException {
+    FileChannel channel = channel(regularFile(0), READ, WRITE);
+
+    try {
+      channel.lock(-1, 10, true);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      channel.lock(0, -1, true);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      channel.tryLock(-1, 10, true);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      channel.tryLock(0, -1, true);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testNullPointerExceptions() throws IOException {
+    FileChannel channel = channel(regularFile(100), READ, WRITE);
+
+    NullPointerTester tester = new NullPointerTester();
+    tester.testAllPublicInstanceMethods(channel);
+  }
+
+  @Test
+  public void testLock() throws IOException {
+    FileChannel channel = channel(regularFile(10), READ, WRITE);
+
+    assertNotNull(channel.lock());
+    assertNotNull(channel.lock(0, 10, false));
+    assertNotNull(channel.lock(0, 10, true));
+
+    assertNotNull(channel.tryLock());
+    assertNotNull(channel.tryLock(0, 10, false));
+    assertNotNull(channel.tryLock(0, 10, true));
+
+    FileLock lock = channel.lock();
+    assertTrue(lock.isValid());
+    lock.release();
+    assertFalse(lock.isValid());
+  }
+
+  @Test
+  public void testAsynchronousClose() throws Exception {
+    RegularFile file = regularFile(10);
+    final FileChannel channel = channel(file, READ, WRITE);
+
+    file.writeLock().lock(); // ensure all operations on the channel will block
+
+    ExecutorService executor = Executors.newCachedThreadPool();
+
+    CountDownLatch latch = new CountDownLatch(BLOCKING_OP_COUNT);
+    List<Future<?>> futures = queueAllBlockingOperations(channel, executor, latch);
+
+    // wait for all the threads to have started running
+    latch.await();
+    // then ensure time for operations to start blocking
+    Uninterruptibles.sleepUninterruptibly(20, MILLISECONDS);
+
+    // close channel on this thread
+    channel.close();
+
+    // the blocking operations are running on different threads, so they all get
+    // AsynchronousCloseException
+    for (Future<?> future : futures) {
+      try {
+        future.get();
+        fail();
+      } catch (ExecutionException expected) {
+        assertWithMessage("blocking thread exception")
+            .that(expected.getCause())
+            .isInstanceOf(AsynchronousCloseException.class);
+      }
+    }
+  }
+
+  @Test
+  public void testCloseByInterrupt() throws Exception {
+    RegularFile file = regularFile(10);
+    final FileChannel channel = channel(file, READ, WRITE);
+
+    file.writeLock().lock(); // ensure all operations on the channel will block
+
+    ExecutorService executor = Executors.newCachedThreadPool();
+
+    final CountDownLatch threadStartLatch = new CountDownLatch(1);
+    final SettableFuture<Throwable> interruptException = SettableFuture.create();
+
+    // This thread, being the first to run, will be blocking on the interruptible lock (the byte
+    // file's write lock) and as such will be interrupted properly... the other threads will be
+    // blocked on the lock that guards the position field and the specification that only one method
+    // on the channel will be in progress at a time. That lock is not interruptible, so we must
+    // interrupt this thread.
+    Thread thread =
+        new Thread(
+            new Runnable() {
+              @Override
+              public void run() {
+                threadStartLatch.countDown();
+                try {
+                  channel.write(ByteBuffer.allocate(20));
+                  interruptException.set(null);
+                } catch (Throwable e) {
+                  interruptException.set(e);
+                }
+              }
+            });
+    thread.start();
+
+    // let the thread start running
+    threadStartLatch.await();
+    // then ensure time for thread to start blocking on the write lock
+    Uninterruptibles.sleepUninterruptibly(10, MILLISECONDS);
+
+    CountDownLatch blockingStartLatch = new CountDownLatch(BLOCKING_OP_COUNT);
+    List<Future<?>> futures = queueAllBlockingOperations(channel, executor, blockingStartLatch);
+
+    // wait for all blocking threads to start
+    blockingStartLatch.await();
+    // then ensure time for the operations to start blocking
+    Uninterruptibles.sleepUninterruptibly(20, MILLISECONDS);
+
+    // interrupting this blocking thread closes the channel and makes all the other threads
+    // throw AsynchronousCloseException... the operation on this thread should throw
+    // ClosedByInterruptException
+    thread.interrupt();
+
+    // get the exception that caused the interrupted operation to terminate
+    assertWithMessage("interrupted thread exception")
+        .that(interruptException.get(200, MILLISECONDS))
+        .isInstanceOf(ClosedByInterruptException.class);
+
+    // check that each other thread got AsynchronousCloseException (since the interrupt, on a
+    // different thread, closed the channel)
+    for (Future<?> future : futures) {
+      try {
+        future.get();
+        fail();
+      } catch (ExecutionException expected) {
+        assertWithMessage("blocking thread exception")
+            .that(expected.getCause())
+            .isInstanceOf(AsynchronousCloseException.class);
+      }
+    }
+  }
+
+  private static final int BLOCKING_OP_COUNT = 10;
+
+  /**
+   * Queues blocking operations on the channel in separate threads using the given executor. The
+   * given latch should have a count of BLOCKING_OP_COUNT to allow the caller wants to wait for all
+   * threads to start executing.
+   */
+  private List<Future<?>> queueAllBlockingOperations(
+      final FileChannel channel, ExecutorService executor, final CountDownLatch startLatch) {
+    List<Future<?>> futures = new ArrayList<>();
+
+    final ByteBuffer buffer = ByteBuffer.allocate(10);
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.write(buffer);
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.write(buffer, 0);
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.write(new ByteBuffer[] {buffer, buffer});
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.write(new ByteBuffer[] {buffer, buffer, buffer}, 0, 2);
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.read(buffer);
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.read(buffer, 0);
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.read(new ByteBuffer[] {buffer, buffer});
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.read(new ByteBuffer[] {buffer, buffer, buffer}, 0, 2);
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.transferTo(0, 10, new ByteBufferChannel(buffer));
+                return null;
+              }
+            }));
+
+    futures.add(
+        executor.submit(
+            new Callable<Object>() {
+              @Override
+              public Object call() throws Exception {
+                startLatch.countDown();
+                channel.transferFrom(new ByteBufferChannel(buffer), 0, 10);
+                return null;
+              }
+            }));
+
+    return futures;
+  }
+
+  /**
+   * Tests that the methods on the default FileChannel that support InterruptibleChannel behavior
+   * also support it on JimfsFileChannel, by just interrupting the thread before calling the method.
+   */
+  @Test
+  public void testInterruptedThreads() throws IOException {
+    final ByteBuffer buf = ByteBuffer.allocate(10);
+    final ByteBuffer[] bufArray = {buf};
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.size();
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.position();
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.position(0);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.write(buf);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.write(bufArray, 0, 1);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.read(buf);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.read(bufArray, 0, 1);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.write(buf, 0);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.read(buf, 0);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.transferTo(0, 1, channel(regularFile(10), READ, WRITE));
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.transferFrom(channel(regularFile(10), READ, WRITE), 0, 1);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.force(true);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.truncate(0);
+          }
+        });
+
+    assertClosedByInterrupt(
+        new FileChannelMethod() {
+          @Override
+          public void call(FileChannel channel) throws IOException {
+            channel.lock(0, 1, true);
+          }
+        });
+
+    // tryLock() does not handle interruption
+    // map() always throws UOE; it doesn't make sense for it to try to handle interruption
+  }
+
+  private interface FileChannelMethod {
+    void call(FileChannel channel) throws IOException;
+  }
+
+  /**
+   * Asserts that when the given operation is run on an interrupted thread, {@code
+   * ClosedByInterruptException} is thrown, the channel is closed and the thread is no longer
+   * interrupted.
+   */
+  private static void assertClosedByInterrupt(FileChannelMethod method) throws IOException {
+    FileChannel channel = channel(regularFile(10), READ, WRITE);
+    Thread.currentThread().interrupt();
+    try {
+      method.call(channel);
+      fail(
+          "expected the method to throw ClosedByInterruptException or "
+              + "FileLockInterruptionException");
+    } catch (ClosedByInterruptException | FileLockInterruptionException expected) {
+      assertFalse("expected the channel to be closed", channel.isOpen());
+      assertTrue("expected the thread to still be interrupted", Thread.interrupted());
+    } finally {
+      Thread.interrupted(); // ensure the thread isn't interrupted when this method returns
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsFileSystemCloseTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsFileSystemCloseTest.java
new file mode 100644
index 0000000..2e05714
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsFileSystemCloseTest.java
@@ -0,0 +1,438 @@
+/*
+ * 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 java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.WRITE;
+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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+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.ClosedChannelException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.ClosedDirectoryStreamException;
+import java.nio.file.ClosedFileSystemException;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for what happens when a file system is closed.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsFileSystemCloseTest {
+
+  private JimfsFileSystem fs = (JimfsFileSystem) Jimfs.newFileSystem(Configuration.unix());
+
+  @Test
+  public void testIsNotOpen() throws IOException {
+    assertTrue(fs.isOpen());
+    fs.close();
+    assertFalse(fs.isOpen());
+  }
+
+  @Test
+  public void testIsNotAvailableFromProvider() throws IOException {
+    URI uri = fs.getUri();
+    assertEquals(fs, FileSystems.getFileSystem(uri));
+
+    fs.close();
+
+    try {
+      FileSystems.getFileSystem(uri);
+      fail();
+    } catch (FileSystemNotFoundException expected) {
+    }
+  }
+
+  @Test
+  public void testOpenStreamsClosed() throws IOException {
+    Path p = fs.getPath("/foo");
+    OutputStream out = Files.newOutputStream(p);
+    InputStream in = Files.newInputStream(p);
+
+    out.write(1);
+    assertEquals(1, in.read());
+
+    fs.close();
+
+    try {
+      out.write(1);
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream is closed", expected.getMessage());
+    }
+
+    try {
+      in.read();
+      fail();
+    } catch (IOException expected) {
+      assertEquals("stream is closed", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testOpenChannelsClosed() throws IOException {
+    Path p = fs.getPath("/foo");
+    FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE);
+    SeekableByteChannel sbc = Files.newByteChannel(p, READ);
+    AsynchronousFileChannel afc = AsynchronousFileChannel.open(p, READ, WRITE);
+
+    assertTrue(fc.isOpen());
+    assertTrue(sbc.isOpen());
+    assertTrue(afc.isOpen());
+
+    fs.close();
+
+    assertFalse(fc.isOpen());
+    assertFalse(sbc.isOpen());
+    assertFalse(afc.isOpen());
+
+    try {
+      fc.size();
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      sbc.size();
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+
+    try {
+      afc.size();
+      fail();
+    } catch (ClosedChannelException expected) {
+    }
+  }
+
+  @Test
+  public void testOpenDirectoryStreamsClosed() throws IOException {
+    Path p = fs.getPath("/foo");
+    Files.createDirectory(p);
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(p)) {
+
+      fs.close();
+
+      try {
+        stream.iterator();
+        fail();
+      } catch (ClosedDirectoryStreamException expected) {
+      }
+    }
+  }
+
+  @Test
+  public void testOpenWatchServicesClosed() throws IOException {
+    WatchService ws1 = fs.newWatchService();
+    WatchService ws2 = fs.newWatchService();
+
+    assertNull(ws1.poll());
+    assertNull(ws2.poll());
+
+    fs.close();
+
+    try {
+      ws1.poll();
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+
+    try {
+      ws2.poll();
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+  }
+
+  @Test
+  public void testPathMethodsThrow() throws IOException {
+    Path p = fs.getPath("/foo");
+    Files.createDirectory(p);
+
+    WatchService ws = fs.newWatchService();
+
+    fs.close();
+
+    try {
+      p.register(ws, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
+      fail();
+    } catch (ClosedWatchServiceException expected) {
+    }
+
+    try {
+      p = p.toRealPath();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    // While technically (according to the FileSystem.close() spec) all methods on Path should
+    // probably throw, we only throw for methods that access the file system itself in some way...
+    // path manipulation methods seem totally harmless to keep working, and I don't see any need to
+    // add the overhead of checking that the file system is open for each of those method calls.
+  }
+
+  @Test
+  public void testOpenFileAttributeViewsThrow() throws IOException {
+    Path p = fs.getPath("/foo");
+    Files.createFile(p);
+
+    BasicFileAttributeView view = Files.getFileAttributeView(p, BasicFileAttributeView.class);
+
+    fs.close();
+
+    try {
+      view.readAttributes();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      view.setTimes(null, null, null);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+  }
+
+  @Test
+  public void testFileSystemMethodsThrow() throws IOException {
+    fs.close();
+
+    try {
+      fs.getPath("/foo");
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      fs.getRootDirectories();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      fs.getFileStores();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      fs.getPathMatcher("glob:*.java");
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      fs.getUserPrincipalLookupService();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      fs.newWatchService();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      fs.supportedFileAttributeViews();
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+  }
+
+  @Test
+  public void testFilesMethodsThrow() throws IOException {
+    Path file = fs.getPath("/file");
+    Path dir = fs.getPath("/dir");
+    Path nothing = fs.getPath("/nothing");
+
+    Files.createDirectory(dir);
+    Files.createFile(file);
+
+    fs.close();
+
+    // not exhaustive, but should cover every major type of functionality accessible through Files
+    // TODO(cgdecker): reflectively invoke all methods with default arguments?
+
+    try {
+      Files.delete(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.createDirectory(nothing);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.createFile(nothing);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.write(nothing, ImmutableList.of("hello world"), UTF_8);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.newInputStream(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.newOutputStream(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.newByteChannel(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.newDirectoryStream(dir);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.copy(file, nothing);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.move(file, nothing);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.copy(dir, nothing);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.move(dir, nothing);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.createSymbolicLink(nothing, file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.createLink(nothing, file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.exists(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.getAttribute(file, "size");
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.setAttribute(file, "lastModifiedTime", FileTime.fromMillis(0));
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.getFileAttributeView(file, BasicFileAttributeView.class);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.readAttributes(file, "basic:size,lastModifiedTime");
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.readAttributes(file, BasicFileAttributes.class);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.isDirectory(dir);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.readAllBytes(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+
+    try {
+      Files.isReadable(file);
+      fail();
+    } catch (ClosedFileSystemException expected) {
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsInputStreamTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsInputStreamTest.java
new file mode 100644
index 0000000..94cc5f4
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsInputStreamTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.jimfs.TestUtils.bytes;
+import static com.google.common.jimfs.TestUtils.regularFile;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.util.concurrent.Runnables;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link JimfsInputStream}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("ResultOfMethodCallIgnored")
+public class JimfsInputStreamTest {
+
+  @Test
+  public void testRead_singleByte() throws IOException {
+    JimfsInputStream in = newInputStream(2);
+    assertThat(in.read()).isEqualTo(2);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_wholeArray() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    byte[] bytes = new byte[8];
+    assertThat(in.read(bytes)).isEqualTo(8);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6, 7, 8), bytes);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_wholeArray_arrayLarger() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    byte[] bytes = new byte[12];
+    assertThat(in.read(bytes)).isEqualTo(8);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0), bytes);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_wholeArray_arraySmaller() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    byte[] bytes = new byte[6];
+    assertThat(in.read(bytes)).isEqualTo(6);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6), bytes);
+    bytes = new byte[6];
+    assertThat(in.read(bytes)).isEqualTo(2);
+    assertArrayEquals(bytes(7, 8, 0, 0, 0, 0), bytes);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_partialArray() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    byte[] bytes = new byte[12];
+    assertThat(in.read(bytes, 0, 8)).isEqualTo(8);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0), bytes);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_partialArray_sliceLarger() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    byte[] bytes = new byte[12];
+    assertThat(in.read(bytes, 0, 10)).isEqualTo(8);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0), bytes);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_partialArray_sliceSmaller() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    byte[] bytes = new byte[12];
+    assertThat(in.read(bytes, 0, 6)).isEqualTo(6);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0), bytes);
+    assertThat(in.read(bytes, 6, 6)).isEqualTo(2);
+    assertArrayEquals(bytes(1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0), bytes);
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testRead_partialArray_invalidInput() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5);
+
+    try {
+      in.read(new byte[3], -1, 1);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+
+    try {
+      in.read(new byte[3], 0, 4);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+
+    try {
+      in.read(new byte[3], 1, 3);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test
+  public void testAvailable() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    assertThat(in.available()).isEqualTo(8);
+    assertThat(in.read()).isEqualTo(1);
+    assertThat(in.available()).isEqualTo(7);
+    assertThat(in.read(new byte[3])).isEqualTo(3);
+    assertThat(in.available()).isEqualTo(4);
+    assertThat(in.read(new byte[10], 1, 2)).isEqualTo(2);
+    assertThat(in.available()).isEqualTo(2);
+    assertThat(in.read(new byte[10])).isEqualTo(2);
+    assertThat(in.available()).isEqualTo(0);
+  }
+
+  @Test
+  public void testSkip() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5, 6, 7, 8);
+    assertThat(in.skip(0)).isEqualTo(0);
+    assertThat(in.skip(-10)).isEqualTo(0);
+    assertThat(in.skip(2)).isEqualTo(2);
+    assertThat(in.read()).isEqualTo(3);
+    assertThat(in.skip(3)).isEqualTo(3);
+    assertThat(in.read()).isEqualTo(7);
+    assertThat(in.skip(10)).isEqualTo(1);
+    assertEmpty(in);
+    assertThat(in.skip(10)).isEqualTo(0);
+    assertEmpty(in);
+  }
+
+  @SuppressWarnings("GuardedByChecker")
+  @Test
+  public void testFullyReadInputStream_doesNotChangeStateWhenStoreChanges() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3, 4, 5);
+    assertThat(in.read(new byte[5])).isEqualTo(5);
+    assertEmpty(in);
+
+    in.file.write(5, new byte[10], 0, 10); // append more bytes to file
+    assertEmpty(in);
+  }
+
+  @Test
+  public void testMark_unsupported() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3);
+    assertThat(in.markSupported()).isFalse();
+
+    // mark does nothing
+    in.mark(1);
+
+    try {
+      // reset throws IOException when unsupported
+      in.reset();
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testClosedInputStream_throwsException() throws IOException {
+    JimfsInputStream in = newInputStream(1, 2, 3);
+    in.close();
+
+    try {
+      in.read();
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      in.read(new byte[3]);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      in.read(new byte[10], 0, 2);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      in.skip(10);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      in.available();
+      fail();
+    } catch (IOException expected) {
+    }
+
+    in.close(); // does nothing
+  }
+
+  private static JimfsInputStream newInputStream(int... bytes) throws IOException {
+    byte[] b = new byte[bytes.length];
+    for (int i = 0; i < bytes.length; i++) {
+      b[i] = (byte) bytes[i];
+    }
+
+    RegularFile file = regularFile(0);
+    file.write(0, b, 0, b.length);
+    return new JimfsInputStream(file, new FileSystemState(Runnables.doNothing()));
+  }
+
+  private static void assertEmpty(JimfsInputStream in) throws IOException {
+    assertThat(in.read()).isEqualTo(-1);
+    assertThat(in.read(new byte[3])).isEqualTo(-1);
+    assertThat(in.read(new byte[10], 1, 5)).isEqualTo(-1);
+    assertThat(in.available()).isEqualTo(0);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsOutputStreamTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsOutputStreamTest.java
new file mode 100644
index 0000000..3c230a7
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsOutputStreamTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.jimfs.TestUtils.bytes;
+import static com.google.common.jimfs.TestUtils.regularFile;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.util.concurrent.Runnables;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link JimfsOutputStream}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsOutputStreamTest {
+
+  @Test
+  public void testWrite_singleByte() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    out.write(1);
+    out.write(2);
+    out.write(3);
+    assertStoreContains(out, 1, 2, 3);
+  }
+
+  @Test
+  public void testWrite_wholeArray() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    out.write(new byte[] {1, 2, 3, 4});
+    assertStoreContains(out, 1, 2, 3, 4);
+  }
+
+  @Test
+  public void testWrite_partialArray() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    out.write(new byte[] {1, 2, 3, 4, 5, 6}, 1, 3);
+    assertStoreContains(out, 2, 3, 4);
+  }
+
+  @Test
+  public void testWrite_partialArray_invalidInput() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+
+    try {
+      out.write(new byte[3], -1, 1);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+
+    try {
+      out.write(new byte[3], 0, 4);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+
+    try {
+      out.write(new byte[3], 1, 3);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  @Test
+  public void testWrite_singleByte_appendMode() throws IOException {
+    JimfsOutputStream out = newOutputStream(true);
+    addBytesToStore(out, 9, 8, 7);
+    out.write(1);
+    out.write(2);
+    out.write(3);
+    assertStoreContains(out, 9, 8, 7, 1, 2, 3);
+  }
+
+  @Test
+  public void testWrite_wholeArray_appendMode() throws IOException {
+    JimfsOutputStream out = newOutputStream(true);
+    addBytesToStore(out, 9, 8, 7);
+    out.write(new byte[] {1, 2, 3, 4});
+    assertStoreContains(out, 9, 8, 7, 1, 2, 3, 4);
+  }
+
+  @Test
+  public void testWrite_partialArray_appendMode() throws IOException {
+    JimfsOutputStream out = newOutputStream(true);
+    addBytesToStore(out, 9, 8, 7);
+    out.write(new byte[] {1, 2, 3, 4, 5, 6}, 1, 3);
+    assertStoreContains(out, 9, 8, 7, 2, 3, 4);
+  }
+
+  @Test
+  public void testWrite_singleByte_overwriting() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    addBytesToStore(out, 9, 8, 7, 6, 5, 4, 3);
+    out.write(1);
+    out.write(2);
+    out.write(3);
+    assertStoreContains(out, 1, 2, 3, 6, 5, 4, 3);
+  }
+
+  @Test
+  public void testWrite_wholeArray_overwriting() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    addBytesToStore(out, 9, 8, 7, 6, 5, 4, 3);
+    out.write(new byte[] {1, 2, 3, 4});
+    assertStoreContains(out, 1, 2, 3, 4, 5, 4, 3);
+  }
+
+  @Test
+  public void testWrite_partialArray_overwriting() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    addBytesToStore(out, 9, 8, 7, 6, 5, 4, 3);
+    out.write(new byte[] {1, 2, 3, 4, 5, 6}, 1, 3);
+    assertStoreContains(out, 2, 3, 4, 6, 5, 4, 3);
+  }
+
+  @Test
+  public void testClosedOutputStream_throwsException() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    out.close();
+
+    try {
+      out.write(1);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      out.write(new byte[3]);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      out.write(new byte[10], 1, 3);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    out.close(); // does nothing
+  }
+
+  @Test
+  public void testClosedOutputStream_doesNotThrowOnFlush() throws IOException {
+    JimfsOutputStream out = newOutputStream(false);
+    out.close();
+    out.flush(); // does nothing
+
+    try (JimfsOutputStream out2 = newOutputStream(false);
+        BufferedOutputStream bout = new BufferedOutputStream(out2);
+        OutputStreamWriter writer = new OutputStreamWriter(bout, UTF_8)) {
+      /*
+       * This specific scenario is why flush() shouldn't throw when the stream is already closed.
+       * Nesting try-with-resources like this will cause close() to be called on the
+       * BufferedOutputStream multiple times. Each time, BufferedOutputStream will first call
+       * out2.flush(), then call out2.close(). If out2.flush() throws when the stream is already
+       * closed, the second flush() will throw an exception. Prior to JDK8, this exception would be
+       * swallowed and ignored completely; in JDK8, the exception is thrown from close().
+       */
+    }
+  }
+
+  private static JimfsOutputStream newOutputStream(boolean append) {
+    RegularFile file = regularFile(0);
+    return new JimfsOutputStream(file, append, new FileSystemState(Runnables.doNothing()));
+  }
+
+  @SuppressWarnings("GuardedByChecker")
+  private static void addBytesToStore(JimfsOutputStream out, int... bytes) throws IOException {
+    RegularFile file = out.file;
+    long pos = file.sizeWithoutLocking();
+    for (int b : bytes) {
+      file.write(pos++, (byte) b);
+    }
+  }
+
+  @SuppressWarnings("GuardedByChecker")
+  private static void assertStoreContains(JimfsOutputStream out, int... bytes) {
+    byte[] actualBytes = new byte[bytes.length];
+    out.file.read(0, actualBytes, 0, actualBytes.length);
+    assertArrayEquals(bytes(bytes), actualBytes);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsPathTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsPathTest.java
new file mode 100644
index 0000000..da7d43c
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsPathTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.testing.EqualsTester;
+import com.google.common.testing.NullPointerTester;
+import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link JimfsPath}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsPathTest {
+
+  private final PathService pathService = PathServiceTest.fakeUnixPathService();
+
+  @Test
+  public void testPathParsing() {
+    assertPathEquals("/", "/");
+    assertPathEquals("/foo", "/foo");
+    assertPathEquals("/foo", "/", "foo");
+    assertPathEquals("/foo/bar", "/foo/bar");
+    assertPathEquals("/foo/bar", "/", "foo", "bar");
+    assertPathEquals("/foo/bar", "/foo", "bar");
+    assertPathEquals("/foo/bar", "/", "foo/bar");
+    assertPathEquals("foo/bar/baz", "foo/bar/baz");
+    assertPathEquals("foo/bar/baz", "foo", "bar", "baz");
+    assertPathEquals("foo/bar/baz", "foo/bar", "baz");
+    assertPathEquals("foo/bar/baz", "foo", "bar/baz");
+  }
+
+  @Test
+  public void testPathParsing_withExtraSeparators() {
+    assertPathEquals("/foo/bar", "///foo/bar");
+    assertPathEquals("/foo/bar", "/foo///bar//");
+    assertPathEquals("/foo/bar/baz", "/foo", "/bar", "baz/");
+    // assertPathEquals("/foo/bar/baz", "/foo\\/bar//\\\\/baz\\/");
+  }
+
+  @Test
+  public void testPathParsing_windowsStylePaths() throws IOException {
+    PathService windowsPathService = PathServiceTest.fakeWindowsPathService();
+    assertEquals("C:\\", pathService.parsePath("C:\\").toString());
+    assertEquals("C:\\foo", windowsPathService.parsePath("C:\\foo").toString());
+    assertEquals("C:\\foo", windowsPathService.parsePath("C:\\", "foo").toString());
+    assertEquals("C:\\foo", windowsPathService.parsePath("C:", "\\foo").toString());
+    assertEquals("C:\\foo", windowsPathService.parsePath("C:", "foo").toString());
+    assertEquals("C:\\foo\\bar", windowsPathService.parsePath("C:", "foo/bar").toString());
+  }
+
+  @Test
+  public void testParsing_windowsStylePaths_invalidPaths() {
+    PathService windowsPathService = PathServiceTest.fakeWindowsPathService();
+
+    try {
+      // The actual windows implementation seems to allow "C:" but treat it as a *name*, not a root
+      // despite the fact that a : is illegal except in a root... a : at any position other than
+      // index 1 in the string will produce an exception.
+      // Here, I choose to be more strict
+      windowsPathService.parsePath("C:");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      // "1:\" isn't a root because 1 isn't a letter
+      windowsPathService.parsePath("1:\\foo");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      // < and > are reserved characters
+      windowsPathService.parsePath("foo<bar>");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+  }
+
+  @Test
+  public void testPathParsing_withAlternateSeparator() {
+    // windows recognizes / as an alternate separator
+    PathService windowsPathService = PathServiceTest.fakeWindowsPathService();
+    assertEquals(
+        windowsPathService.parsePath("foo\\bar\\baz"), windowsPathService.parsePath("foo/bar/baz"));
+    assertEquals(
+        windowsPathService.parsePath("C:\\foo\\bar"), windowsPathService.parsePath("C:\\foo/bar"));
+    assertEquals(
+        windowsPathService.parsePath("c:\\foo\\bar\\baz"),
+        windowsPathService.parsePath("c:", "foo/", "bar/baz"));
+  }
+
+  @Test
+  public void testRootPath() {
+    new PathTester(pathService, "/").root("/").test("/");
+  }
+
+  @Test
+  public void testRelativePath_singleName() {
+    new PathTester(pathService, "test").names("test").test("test");
+
+    Path path = pathService.parsePath("test");
+    assertEquals(path, path.getFileName());
+  }
+
+  @Test
+  public void testRelativePath_twoNames() {
+    PathTester tester = new PathTester(pathService, "foo/bar").names("foo", "bar");
+
+    tester.test("foo/bar");
+  }
+
+  @Test
+  public void testRelativePath_fourNames() {
+    new PathTester(pathService, "foo/bar/baz/test")
+        .names("foo", "bar", "baz", "test")
+        .test("foo/bar/baz/test");
+  }
+
+  @Test
+  public void testAbsolutePath_singleName() {
+    new PathTester(pathService, "/foo").root("/").names("foo").test("/foo");
+  }
+
+  @Test
+  public void testAbsolutePath_twoNames() {
+    new PathTester(pathService, "/foo/bar").root("/").names("foo", "bar").test("/foo/bar");
+  }
+
+  @Test
+  public void testAbsoluteMultiNamePath_fourNames() {
+    new PathTester(pathService, "/foo/bar/baz/test")
+        .root("/")
+        .names("foo", "bar", "baz", "test")
+        .test("/foo/bar/baz/test");
+  }
+
+  @Test
+  public void testResolve_fromRoot() {
+    Path root = pathService.parsePath("/");
+
+    assertResolvedPathEquals("/foo", root, "foo");
+    assertResolvedPathEquals("/foo/bar", root, "foo/bar");
+    assertResolvedPathEquals("/foo/bar", root, "foo", "bar");
+    assertResolvedPathEquals("/foo/bar/baz/test", root, "foo/bar/baz/test");
+    assertResolvedPathEquals("/foo/bar/baz/test", root, "foo", "bar/baz", "test");
+  }
+
+  @Test
+  public void testResolve_fromAbsolute() {
+    Path path = pathService.parsePath("/foo");
+
+    assertResolvedPathEquals("/foo/bar", path, "bar");
+    assertResolvedPathEquals("/foo/bar/baz/test", path, "bar/baz/test");
+    assertResolvedPathEquals("/foo/bar/baz/test", path, "bar/baz", "test");
+    assertResolvedPathEquals("/foo/bar/baz/test", path, "bar", "baz", "test");
+  }
+
+  @Test
+  public void testResolve_fromRelative() {
+    Path path = pathService.parsePath("foo");
+
+    assertResolvedPathEquals("foo/bar", path, "bar");
+    assertResolvedPathEquals("foo/bar/baz/test", path, "bar/baz/test");
+    assertResolvedPathEquals("foo/bar/baz/test", path, "bar", "baz", "test");
+    assertResolvedPathEquals("foo/bar/baz/test", path, "bar/baz", "test");
+  }
+
+  @Test
+  public void testResolve_withThisAndParentDirNames() {
+    Path path = pathService.parsePath("/foo");
+
+    assertResolvedPathEquals("/foo/bar/../baz", path, "bar/../baz");
+    assertResolvedPathEquals("/foo/bar/../baz", path, "bar", "..", "baz");
+    assertResolvedPathEquals("/foo/./bar/baz", path, "./bar/baz");
+    assertResolvedPathEquals("/foo/./bar/baz", path, ".", "bar/baz");
+  }
+
+  @Test
+  public void testResolve_givenAbsolutePath() {
+    assertResolvedPathEquals("/test", pathService.parsePath("/foo"), "/test");
+    assertResolvedPathEquals("/test", pathService.parsePath("foo"), "/test");
+  }
+
+  @Test
+  public void testResolve_givenEmptyPath() {
+    assertResolvedPathEquals("/foo", pathService.parsePath("/foo"), "");
+    assertResolvedPathEquals("foo", pathService.parsePath("foo"), "");
+  }
+
+  @Test
+  public void testResolve_againstEmptyPath() {
+    assertResolvedPathEquals("foo/bar", pathService.emptyPath(), "foo/bar");
+  }
+
+  @Test
+  public void testResolveSibling_givenEmptyPath() {
+    Path path = pathService.parsePath("foo/bar");
+    Path resolved = path.resolveSibling("");
+    assertPathEquals("foo", resolved);
+
+    path = pathService.parsePath("foo");
+    resolved = path.resolveSibling("");
+    assertPathEquals("", resolved);
+  }
+
+  @Test
+  public void testResolveSibling_againstEmptyPath() {
+    Path path = pathService.parsePath("");
+    Path resolved = path.resolveSibling("foo");
+    assertPathEquals("foo", resolved);
+
+    path = pathService.parsePath("");
+    resolved = path.resolveSibling("");
+    assertPathEquals("", resolved);
+  }
+
+  @Test
+  public void testRelativize_bothAbsolute() {
+    // TODO(cgdecker): When the paths have different roots, how should this work?
+    // Should it work at all?
+    assertRelativizedPathEquals("b/c", pathService.parsePath("/a"), "/a/b/c");
+    assertRelativizedPathEquals("c/d", pathService.parsePath("/a/b"), "/a/b/c/d");
+  }
+
+  @Test
+  public void testRelativize_bothRelative() {
+    assertRelativizedPathEquals("b/c", pathService.parsePath("a"), "a/b/c");
+    assertRelativizedPathEquals("d", pathService.parsePath("a/b/c"), "a/b/c/d");
+  }
+
+  @Test
+  public void testRelativize_againstEmptyPath() {
+    assertRelativizedPathEquals("foo/bar", pathService.emptyPath(), "foo/bar");
+  }
+
+  @Test
+  public void testRelativize_oneAbsoluteOneRelative() {
+    try {
+      pathService.parsePath("/foo/bar").relativize(pathService.parsePath("foo"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      pathService.parsePath("foo").relativize(pathService.parsePath("/foo/bar"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testNormalize_withParentDirName() {
+    assertNormalizedPathEquals("/foo/baz", "/foo/bar/../baz");
+    assertNormalizedPathEquals("/foo/baz", "/foo", "bar", "..", "baz");
+  }
+
+  @Test
+  public void testNormalize_withThisDirName() {
+    assertNormalizedPathEquals("/foo/bar/baz", "/foo/bar/./baz");
+    assertNormalizedPathEquals("/foo/bar/baz", "/foo", "bar", ".", "baz");
+  }
+
+  @Test
+  public void testNormalize_withThisAndParentDirNames() {
+    assertNormalizedPathEquals("foo/test", "foo/./bar/../././baz/../test");
+  }
+
+  @Test
+  public void testNormalize_withLeadingParentDirNames() {
+    assertNormalizedPathEquals("../../foo/baz", "../../foo/bar/../baz");
+  }
+
+  @Test
+  public void testNormalize_withLeadingThisAndParentDirNames() {
+    assertNormalizedPathEquals("../../foo/baz", "./.././.././foo/bar/../baz");
+  }
+
+  @Test
+  public void testNormalize_withExtraParentDirNamesAtRoot() {
+    assertNormalizedPathEquals("/", "/..");
+    assertNormalizedPathEquals("/", "/../../..");
+    assertNormalizedPathEquals("/", "/foo/../../..");
+    assertNormalizedPathEquals("/", "/../foo/../../bar/baz/../../../..");
+  }
+
+  @Test
+  public void testPathWithExtraSlashes() {
+    assertPathEquals("/foo/bar/baz", pathService.parsePath("/foo/bar/baz/"));
+    assertPathEquals("/foo/bar/baz", pathService.parsePath("/foo//bar///baz"));
+    assertPathEquals("/foo/bar/baz", pathService.parsePath("///foo/bar/baz"));
+  }
+
+  @Test
+  public void testEqualityBasedOnStringNotName() {
+    Name a1 = Name.create("a", "a");
+    Name a2 = Name.create("A", "a");
+    Name a3 = Name.create("a", "A");
+
+    Path path1 = pathService.createFileName(a1);
+    Path path2 = pathService.createFileName(a2);
+    Path path3 = pathService.createFileName(a3);
+
+    new EqualsTester().addEqualityGroup(path1, path3).addEqualityGroup(path2).testEquals();
+  }
+
+  @Test
+  public void testNullPointerExceptions() throws NoSuchMethodException {
+    NullPointerTester tester =
+        new NullPointerTester().ignore(JimfsPath.class.getMethod("toRealPath", LinkOption[].class));
+    // ignore toRealPath because the pathService creates fake paths that do not have a
+    // JimfsFileSystem instance, causing it to fail since it needs to access the file system
+
+    tester.testAllPublicInstanceMethods(pathService.parsePath("/"));
+    tester.testAllPublicInstanceMethods(pathService.parsePath(""));
+    tester.testAllPublicInstanceMethods(pathService.parsePath("/foo"));
+    tester.testAllPublicInstanceMethods(pathService.parsePath("/foo/bar/baz"));
+    tester.testAllPublicInstanceMethods(pathService.parsePath("foo"));
+    tester.testAllPublicInstanceMethods(pathService.parsePath("foo/bar"));
+    tester.testAllPublicInstanceMethods(pathService.parsePath("foo/bar/baz"));
+    tester.testAllPublicInstanceMethods(pathService.parsePath("."));
+    tester.testAllPublicInstanceMethods(pathService.parsePath(".."));
+  }
+
+  private void assertResolvedPathEquals(
+      String expected, Path path, String firstResolvePath, String... moreResolvePaths) {
+    Path resolved = path.resolve(firstResolvePath);
+    for (String additionalPath : moreResolvePaths) {
+      resolved = resolved.resolve(additionalPath);
+    }
+    assertPathEquals(expected, resolved);
+
+    Path relative = pathService.parsePath(firstResolvePath, moreResolvePaths);
+    resolved = path.resolve(relative);
+    assertPathEquals(expected, resolved);
+
+    // assert the invariant that p.relativize(p.resolve(q)).equals(q) when q does not have a root
+    // p = path, q = relative, p.resolve(q) = resolved
+    if (relative.getRoot() == null) {
+      assertEquals(relative, path.relativize(resolved));
+    }
+  }
+
+  private void assertRelativizedPathEquals(String expected, Path path, String relativizePath) {
+    Path relativized = path.relativize(pathService.parsePath(relativizePath));
+    assertPathEquals(expected, relativized);
+  }
+
+  private void assertNormalizedPathEquals(String expected, String first, String... more) {
+    assertPathEquals(expected, pathService.parsePath(first, more).normalize());
+  }
+
+  private void assertPathEquals(String expected, String first, String... more) {
+    assertPathEquals(expected, pathService.parsePath(first, more));
+  }
+
+  private void assertPathEquals(String expected, Path path) {
+    assertEquals(pathService.parsePath(expected), path);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsUnixLikeFileSystemTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsUnixLikeFileSystemTest.java
new file mode 100644
index 0000000..a839d6a
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsUnixLikeFileSystemTest.java
@@ -0,0 +1,2401 @@
+/*
+ * 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.jimfs.TestUtils.bytes;
+import static com.google.common.jimfs.TestUtils.permutations;
+import static com.google.common.jimfs.TestUtils.preFilledBytes;
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
+import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.nio.file.StandardOpenOption.APPEND;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.CREATE_NEW;
+import static java.nio.file.StandardOpenOption.DSYNC;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.SPARSE;
+import static java.nio.file.StandardOpenOption.SYNC;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Ordering;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.CharStreams;
+import com.google.common.primitives.Bytes;
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.NonReadableChannelException;
+import java.nio.channels.NonWritableChannelException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.ClosedDirectoryStreamException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.NotLinkException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.regex.PatternSyntaxException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests an in-memory file system through the public APIs in {@link Files}, etc. This also acts as
+ * the tests for {@code FileSystemView}, as each public API method is (mostly) implemented by a
+ * method in {@code FileSystemView}.
+ *
+ * <p>These tests uses a Unix-like file system, but most of what they test applies to any file
+ * system configuration.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsUnixLikeFileSystemTest extends AbstractJimfsIntegrationTest {
+
+  private static final Configuration UNIX_CONFIGURATION =
+      Configuration.unix().toBuilder()
+          .setAttributeViews("basic", "owner", "posix", "unix")
+          .setMaxSize(1024 * 1024 * 1024) // 1 GB
+          .setMaxCacheSize(256 * 1024 * 1024) // 256 MB
+          .build();
+
+  @Override
+  protected FileSystem createFileSystem() {
+    return Jimfs.newFileSystem("unix", UNIX_CONFIGURATION);
+  }
+
+  @Test
+  public void testFileSystem() {
+    assertThat(fs.getSeparator()).isEqualTo("/");
+    assertThat(fs.getRootDirectories())
+        .containsExactlyElementsIn(ImmutableSet.of(path("/")))
+        .inOrder();
+    assertThat(fs.isOpen()).isTrue();
+    assertThat(fs.isReadOnly()).isFalse();
+    assertThat(fs.supportedFileAttributeViews()).containsExactly("basic", "owner", "posix", "unix");
+    assertThat(fs.provider()).isInstanceOf(JimfsFileSystemProvider.class);
+  }
+
+  @Test
+  public void testFileStore() throws IOException {
+    FileStore fileStore = Iterables.getOnlyElement(fs.getFileStores());
+    assertThat(fileStore.name()).isEqualTo("jimfs");
+    assertThat(fileStore.type()).isEqualTo("jimfs");
+    assertThat(fileStore.isReadOnly()).isFalse();
+
+    long totalSpace = 1024 * 1024 * 1024; // 1 GB
+    assertThat(fileStore.getTotalSpace()).isEqualTo(totalSpace);
+    assertThat(fileStore.getUnallocatedSpace()).isEqualTo(totalSpace);
+    assertThat(fileStore.getUsableSpace()).isEqualTo(totalSpace);
+
+    Files.write(fs.getPath("/foo"), new byte[10000]);
+
+    assertThat(fileStore.getTotalSpace()).isEqualTo(totalSpace);
+
+    // We wrote 10000 bytes, but since the file system allocates fixed size blocks, more than 10k
+    // bytes may have been allocated. As such, the unallocated space after the write can be at most
+    // maxUnallocatedSpace.
+    assertThat(fileStore.getUnallocatedSpace() <= totalSpace - 10000).isTrue();
+
+    // Usable space is at most unallocated space. (In this case, it's currently exactly unallocated
+    // space, but that's not required.)
+    assertThat(fileStore.getUsableSpace() <= fileStore.getUnallocatedSpace()).isTrue();
+
+    Files.delete(fs.getPath("/foo"));
+    assertThat(fileStore.getTotalSpace()).isEqualTo(totalSpace);
+    assertThat(fileStore.getUnallocatedSpace()).isEqualTo(totalSpace);
+    assertThat(fileStore.getUsableSpace()).isEqualTo(totalSpace);
+  }
+
+  @Test
+  public void testPaths() {
+    assertThatPath("/").isAbsolute().and().hasRootComponent("/").and().hasNoNameComponents();
+    assertThatPath("foo").isRelative().and().hasNameComponents("foo");
+    assertThatPath("foo/bar").isRelative().and().hasNameComponents("foo", "bar");
+    assertThatPath("/foo/bar/baz")
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNameComponents("foo", "bar", "baz");
+  }
+
+  @Test
+  public void testPaths_equalityIsCaseSensitive() {
+    assertThatPath("foo").isNotEqualTo(path("FOO"));
+  }
+
+  @Test
+  public void testPaths_areSortedCaseSensitive() {
+    Path p1 = path("a");
+    Path p2 = path("B");
+    Path p3 = path("c");
+    Path p4 = path("D");
+
+    assertThat(Ordering.natural().immutableSortedCopy(Arrays.asList(p3, p4, p1, p2)))
+        .isEqualTo(ImmutableList.of(p2, p4, p1, p3));
+
+    // would be p1, p2, p3, p4 if sorting were case insensitive
+  }
+
+  @Test
+  public void testPaths_resolve() {
+    assertThatPath(path("/").resolve("foo/bar"))
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNameComponents("foo", "bar");
+    assertThatPath(path("foo/bar").resolveSibling("baz"))
+        .isRelative()
+        .and()
+        .hasNameComponents("foo", "baz");
+    assertThatPath(path("foo/bar").resolve("/one/two"))
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNameComponents("one", "two");
+  }
+
+  @Test
+  public void testPaths_normalize() {
+    assertThatPath(path("foo/bar/..").normalize()).isRelative().and().hasNameComponents("foo");
+    assertThatPath(path("foo/./bar/../baz/test/./../stuff").normalize())
+        .isRelative()
+        .and()
+        .hasNameComponents("foo", "baz", "stuff");
+    assertThatPath(path("../../foo/./bar").normalize())
+        .isRelative()
+        .and()
+        .hasNameComponents("..", "..", "foo", "bar");
+    assertThatPath(path("foo/../../bar").normalize())
+        .isRelative()
+        .and()
+        .hasNameComponents("..", "bar");
+    assertThatPath(path(".././..").normalize()).isRelative().and().hasNameComponents("..", "..");
+  }
+
+  @Test
+  public void testPaths_relativize() {
+    assertThatPath(path("/foo/bar").relativize(path("/foo/bar/baz")))
+        .isRelative()
+        .and()
+        .hasNameComponents("baz");
+    assertThatPath(path("/foo/bar/baz").relativize(path("/foo/bar")))
+        .isRelative()
+        .and()
+        .hasNameComponents("..");
+    assertThatPath(path("/foo/bar/baz").relativize(path("/foo/baz/bar")))
+        .isRelative()
+        .and()
+        .hasNameComponents("..", "..", "baz", "bar");
+    assertThatPath(path("foo/bar").relativize(path("foo")))
+        .isRelative()
+        .and()
+        .hasNameComponents("..");
+    assertThatPath(path("foo").relativize(path("foo/bar")))
+        .isRelative()
+        .and()
+        .hasNameComponents("bar");
+
+    try {
+      Path unused = path("/foo/bar").relativize(path("bar"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      Path unused = path("bar").relativize(path("/foo/bar"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testPaths_startsWith_endsWith() {
+    assertThat(path("/foo/bar").startsWith("/")).isTrue();
+    assertThat(path("/foo/bar").startsWith("/foo")).isTrue();
+    assertThat(path("/foo/bar").startsWith("/foo/bar")).isTrue();
+    assertThat(path("/foo/bar").endsWith("bar")).isTrue();
+    assertThat(path("/foo/bar").endsWith("foo/bar")).isTrue();
+    assertThat(path("/foo/bar").endsWith("/foo/bar")).isTrue();
+    assertThat(path("/foo/bar").endsWith("/foo")).isFalse();
+    assertThat(path("/foo/bar").startsWith("foo/bar")).isFalse();
+  }
+
+  @Test
+  public void testPaths_toAbsolutePath() {
+    assertThatPath(path("/foo/bar").toAbsolutePath())
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNameComponents("foo", "bar")
+        .and()
+        .isEqualTo(path("/foo/bar"));
+
+    assertThatPath(path("foo/bar").toAbsolutePath())
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNameComponents("work", "foo", "bar")
+        .and()
+        .isEqualTo(path("/work/foo/bar"));
+  }
+
+  @Test
+  public void testPaths_toRealPath() throws IOException {
+    Files.createDirectories(path("/foo/bar"));
+    Files.createSymbolicLink(path("/link"), path("/"));
+
+    assertThatPath(path("/link/foo/bar").toRealPath()).isEqualTo(path("/foo/bar"));
+
+    assertThatPath(path("").toRealPath()).isEqualTo(path("/work"));
+    assertThatPath(path(".").toRealPath()).isEqualTo(path("/work"));
+    assertThatPath(path("..").toRealPath()).isEqualTo(path("/"));
+    assertThatPath(path("../..").toRealPath()).isEqualTo(path("/"));
+    assertThatPath(path("./.././..").toRealPath()).isEqualTo(path("/"));
+    assertThatPath(path("./.././../.").toRealPath()).isEqualTo(path("/"));
+  }
+
+  @Test
+  public void testPaths_toUri() {
+    assertThat(path("/").toUri()).isEqualTo(URI.create("jimfs://unix/"));
+    assertThat(path("/foo").toUri()).isEqualTo(URI.create("jimfs://unix/foo"));
+    assertThat(path("/foo/bar").toUri()).isEqualTo(URI.create("jimfs://unix/foo/bar"));
+    assertThat(path("foo").toUri()).isEqualTo(URI.create("jimfs://unix/work/foo"));
+    assertThat(path("foo/bar").toUri()).isEqualTo(URI.create("jimfs://unix/work/foo/bar"));
+    assertThat(path("").toUri()).isEqualTo(URI.create("jimfs://unix/work/"));
+    assertThat(path("./../.").toUri()).isEqualTo(URI.create("jimfs://unix/work/./.././"));
+  }
+
+  @Test
+  public void testPaths_getFromUri() {
+    assertThatPath(Paths.get(URI.create("jimfs://unix/"))).isEqualTo(path("/"));
+    assertThatPath(Paths.get(URI.create("jimfs://unix/foo"))).isEqualTo(path("/foo"));
+    assertThatPath(Paths.get(URI.create("jimfs://unix/foo%20bar"))).isEqualTo(path("/foo bar"));
+    assertThatPath(Paths.get(URI.create("jimfs://unix/foo/./bar"))).isEqualTo(path("/foo/./bar"));
+    assertThatPath(Paths.get(URI.create("jimfs://unix/foo/bar/"))).isEqualTo(path("/foo/bar"));
+  }
+
+  @Test
+  public void testPathMatchers_regex() {
+    assertThatPath("bar").matches("regex:.*");
+    assertThatPath("bar").matches("regex:bar");
+    assertThatPath("bar").matches("regex:[a-z]+");
+    assertThatPath("/foo/bar").matches("regex:/.*");
+    assertThatPath("/foo/bar").matches("regex:/.*/bar");
+  }
+
+  @Test
+  public void testPathMatchers_glob() {
+    assertThatPath("bar").matches("glob:bar");
+    assertThatPath("bar").matches("glob:*");
+    assertThatPath("/foo").doesNotMatch("glob:*");
+    assertThatPath("/foo/bar").doesNotMatch("glob:*");
+    assertThatPath("/foo/bar").matches("glob:**");
+    assertThatPath("/foo/bar").matches("glob:/**");
+    assertThatPath("foo/bar").doesNotMatch("glob:/**");
+    assertThatPath("/foo/bar/baz/stuff").matches("glob:/foo/**");
+    assertThatPath("/foo/bar/baz/stuff").matches("glob:/**/stuff");
+    assertThatPath("/foo").matches("glob:/[a-z]*");
+    assertThatPath("/Foo").doesNotMatch("glob:/[a-z]*");
+    assertThatPath("/foo/bar/baz/Stuff.java").matches("glob:**/*.java");
+    assertThatPath("/foo/bar/baz/Stuff.java").matches("glob:**/*.{java,class}");
+    assertThatPath("/foo/bar/baz/Stuff.class").matches("glob:**/*.{java,class}");
+    assertThatPath("/foo/bar/baz/Stuff.java").matches("glob:**/*.*");
+
+    try {
+      fs.getPathMatcher("glob:**/*.{java,class");
+      fail();
+    } catch (PatternSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testPathMatchers_invalid() {
+    try {
+      fs.getPathMatcher("glob");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      fs.getPathMatcher("foo:foo");
+      fail();
+    } catch (UnsupportedOperationException expected) {
+      assertThat(expected.getMessage()).contains("syntax");
+    }
+  }
+
+  @Test
+  public void testNewFileSystem_hasRootAndWorkingDirectory() throws IOException {
+    assertThatPath("/").hasChildren("work");
+    assertThatPath("/work").hasNoChildren();
+  }
+
+  @Test
+  public void testCreateDirectory_absolute() throws IOException {
+    Files.createDirectory(path("/test"));
+
+    assertThatPath("/test").exists();
+    assertThatPath("/").hasChildren("test", "work");
+
+    Files.createDirectory(path("/foo"));
+    Files.createDirectory(path("/foo/bar"));
+
+    assertThatPath("/foo/bar").exists();
+    assertThatPath("/foo").hasChildren("bar");
+  }
+
+  @Test
+  public void testCreateFile_absolute() throws IOException {
+    Files.createFile(path("/test.txt"));
+
+    assertThatPath("/test.txt").isRegularFile();
+    assertThatPath("/").hasChildren("test.txt", "work");
+
+    Files.createDirectory(path("/foo"));
+    Files.createFile(path("/foo/test.txt"));
+
+    assertThatPath("/foo/test.txt").isRegularFile();
+    assertThatPath("/foo").hasChildren("test.txt");
+  }
+
+  @Test
+  public void testCreateSymbolicLink_absolute() throws IOException {
+    Files.createSymbolicLink(path("/link.txt"), path("test.txt"));
+
+    assertThatPath("/link.txt", NOFOLLOW_LINKS).isSymbolicLink().withTarget("test.txt");
+    assertThatPath("/").hasChildren("link.txt", "work");
+
+    Files.createDirectory(path("/foo"));
+    Files.createSymbolicLink(path("/foo/link.txt"), path("test.txt"));
+
+    assertThatPath("/foo/link.txt").noFollowLinks().isSymbolicLink().withTarget("test.txt");
+    assertThatPath("/foo").hasChildren("link.txt");
+  }
+
+  @Test
+  public void testCreateLink_absolute() throws IOException {
+    Files.createFile(path("/test.txt"));
+    Files.createLink(path("/link.txt"), path("/test.txt"));
+
+    // don't assert that the link is the same file here, just that it was created
+    // later tests check that linking works correctly
+    assertThatPath("/link.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("/").hasChildren("link.txt", "test.txt", "work");
+
+    Files.createDirectory(path("/foo"));
+    Files.createLink(path("/foo/link.txt"), path("/test.txt"));
+
+    assertThatPath("/foo/link.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("/foo").hasChildren("link.txt");
+  }
+
+  @Test
+  public void testCreateDirectory_relative() throws IOException {
+    Files.createDirectory(path("test"));
+
+    assertThatPath("/work/test", NOFOLLOW_LINKS).isDirectory();
+    assertThatPath("test", NOFOLLOW_LINKS).isDirectory();
+    assertThatPath("/work").hasChildren("test");
+    assertThatPath("test").isSameFileAs("/work/test");
+
+    Files.createDirectory(path("foo"));
+    Files.createDirectory(path("foo/bar"));
+
+    assertThatPath("/work/foo/bar", NOFOLLOW_LINKS).isDirectory();
+    assertThatPath("foo/bar", NOFOLLOW_LINKS).isDirectory();
+    assertThatPath("/work/foo").hasChildren("bar");
+    assertThatPath("foo").hasChildren("bar");
+    assertThatPath("foo/bar").isSameFileAs("/work/foo/bar");
+  }
+
+  @Test
+  public void testCreateFile_relative() throws IOException {
+    Files.createFile(path("test.txt"));
+
+    assertThatPath("/work/test.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("test.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("/work").hasChildren("test.txt");
+    assertThatPath("test.txt").isSameFileAs("/work/test.txt");
+
+    Files.createDirectory(path("foo"));
+    Files.createFile(path("foo/test.txt"));
+
+    assertThatPath("/work/foo/test.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("foo/test.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("/work/foo").hasChildren("test.txt");
+    assertThatPath("foo").hasChildren("test.txt");
+    assertThatPath("foo/test.txt").isSameFileAs("/work/foo/test.txt");
+  }
+
+  @Test
+  public void testCreateSymbolicLink_relative() throws IOException {
+    Files.createSymbolicLink(path("link.txt"), path("test.txt"));
+
+    assertThatPath("/work/link.txt", NOFOLLOW_LINKS).isSymbolicLink().withTarget("test.txt");
+    assertThatPath("link.txt", NOFOLLOW_LINKS).isSymbolicLink().withTarget("test.txt");
+    assertThatPath("/work").hasChildren("link.txt");
+
+    Files.createDirectory(path("foo"));
+    Files.createSymbolicLink(path("foo/link.txt"), path("test.txt"));
+
+    assertThatPath("/work/foo/link.txt", NOFOLLOW_LINKS).isSymbolicLink().withTarget("test.txt");
+    assertThatPath("foo/link.txt", NOFOLLOW_LINKS).isSymbolicLink().withTarget("test.txt");
+    assertThatPath("/work/foo").hasChildren("link.txt");
+    assertThatPath("foo").hasChildren("link.txt");
+  }
+
+  @Test
+  public void testCreateLink_relative() throws IOException {
+    Files.createFile(path("test.txt"));
+    Files.createLink(path("link.txt"), path("test.txt"));
+
+    // don't assert that the link is the same file here, just that it was created
+    // later tests check that linking works correctly
+    assertThatPath("/work/link.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("link.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("/work").hasChildren("link.txt", "test.txt");
+
+    Files.createDirectory(path("foo"));
+    Files.createLink(path("foo/link.txt"), path("test.txt"));
+
+    assertThatPath("/work/foo/link.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("foo/link.txt", NOFOLLOW_LINKS).isRegularFile();
+    assertThatPath("foo").hasChildren("link.txt");
+  }
+
+  @Test
+  public void testCreateFile_existing() throws IOException {
+    Files.createFile(path("/test"));
+    try {
+      Files.createFile(path("/test"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/test", expected.getMessage());
+    }
+
+    try {
+      Files.createDirectory(path("/test"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/test", expected.getMessage());
+    }
+
+    try {
+      Files.createSymbolicLink(path("/test"), path("/foo"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/test", expected.getMessage());
+    }
+
+    Files.createFile(path("/foo"));
+    try {
+      Files.createLink(path("/test"), path("/foo"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/test", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCreateFile_parentDoesNotExist() throws IOException {
+    try {
+      Files.createFile(path("/foo/test"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo/test", expected.getMessage());
+    }
+
+    try {
+      Files.createDirectory(path("/foo/test"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo/test", expected.getMessage());
+    }
+
+    try {
+      Files.createSymbolicLink(path("/foo/test"), path("/bar"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo/test", expected.getMessage());
+    }
+
+    Files.createFile(path("/bar"));
+    try {
+      Files.createLink(path("/foo/test"), path("/bar"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo/test", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCreateFile_parentIsNotDirectory() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createFile(path("/foo/bar"));
+
+    try {
+      Files.createFile(path("/foo/bar/baz"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo/bar/baz");
+    }
+  }
+
+  @Test
+  public void testCreateFile_nonDirectoryHigherInPath() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createFile(path("/foo/bar"));
+
+    try {
+      Files.createFile(path("/foo/bar/baz/stuff"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo/bar/baz/stuff");
+    }
+  }
+
+  @Test
+  public void testCreateFile_parentSymlinkDoesNotExist() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createSymbolicLink(path("/foo/bar"), path("/foo/nope"));
+
+    try {
+      Files.createFile(path("/foo/bar/baz"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo/bar/baz");
+    }
+  }
+
+  @Test
+  public void testCreateFile_symlinkHigherInPathDoesNotExist() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createSymbolicLink(path("/foo/bar"), path("nope"));
+
+    try {
+      Files.createFile(path("/foo/bar/baz/stuff"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo/bar/baz/stuff");
+    }
+  }
+
+  @Test
+  public void testCreateFile_parentSymlinkDoesPointsToNonDirectory() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createFile(path("/foo/file"));
+    Files.createSymbolicLink(path("/foo/bar"), path("/foo/file"));
+
+    try {
+      Files.createFile(path("/foo/bar/baz"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo/bar/baz");
+    }
+  }
+
+  @Test
+  public void testCreateFile_symlinkHigherInPathPointsToNonDirectory() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createFile(path("/foo/file"));
+    Files.createSymbolicLink(path("/foo/bar"), path("file"));
+
+    try {
+      Files.createFile(path("/foo/bar/baz/stuff"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo/bar/baz/stuff");
+    }
+  }
+
+  @Test
+  public void testCreateFile_withInitialAttributes() throws IOException {
+    Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rwxrwxrwx");
+    FileAttribute<?> permissionsAttr = PosixFilePermissions.asFileAttribute(permissions);
+
+    Files.createFile(path("/normal"));
+    Files.createFile(path("/foo"), permissionsAttr);
+
+    assertThatPath("/normal").attribute("posix:permissions").isNot(permissions);
+    assertThatPath("/foo").attribute("posix:permissions").is(permissions);
+  }
+
+  @Test
+  public void testCreateFile_withInitialAttributes_illegalInitialAttribute() throws IOException {
+    try {
+      Files.createFile(
+          path("/foo"),
+          new BasicFileAttribute<>("basic:lastModifiedTime", FileTime.fromMillis(0L)));
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    assertThatPath("/foo").doesNotExist();
+
+    try {
+      Files.createFile(path("/foo"), new BasicFileAttribute<>("basic:noSuchAttribute", "foo"));
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    assertThatPath("/foo").doesNotExist();
+  }
+
+  @Test
+  public void testOpenChannel_withInitialAttributes_createNewFile() throws IOException {
+    FileAttribute<Set<PosixFilePermission>> permissions =
+        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
+    Files.newByteChannel(path("/foo"), ImmutableSet.of(WRITE, CREATE), permissions).close();
+
+    assertThatPath("/foo")
+        .isRegularFile()
+        .and()
+        .attribute("posix:permissions")
+        .is(permissions.value());
+  }
+
+  @Test
+  public void testOpenChannel_withInitialAttributes_fileExists() throws IOException {
+    Files.createFile(path("/foo"));
+
+    FileAttribute<Set<PosixFilePermission>> permissions =
+        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
+    Files.newByteChannel(path("/foo"), ImmutableSet.of(WRITE, CREATE), permissions).close();
+
+    assertThatPath("/foo")
+        .isRegularFile()
+        .and()
+        .attribute("posix:permissions")
+        .isNot(permissions.value());
+  }
+
+  @Test
+  public void testCreateDirectory_withInitialAttributes() throws IOException {
+    FileAttribute<Set<PosixFilePermission>> permissions =
+        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
+
+    Files.createDirectory(path("/foo"), permissions);
+
+    assertThatPath("/foo")
+        .isDirectory()
+        .and()
+        .attribute("posix:permissions")
+        .is(permissions.value());
+
+    Files.createDirectory(path("/normal"));
+
+    assertThatPath("/normal")
+        .isDirectory()
+        .and()
+        .attribute("posix:permissions")
+        .isNot(permissions.value());
+  }
+
+  @Test
+  public void testCreateSymbolicLink_withInitialAttributes() throws IOException {
+    FileAttribute<Set<PosixFilePermission>> permissions =
+        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
+
+    Files.createSymbolicLink(path("/foo"), path("bar"), permissions);
+
+    assertThatPath("/foo", NOFOLLOW_LINKS)
+        .isSymbolicLink()
+        .and()
+        .attribute("posix:permissions")
+        .is(permissions.value());
+
+    Files.createSymbolicLink(path("/normal"), path("bar"));
+
+    assertThatPath("/normal", NOFOLLOW_LINKS)
+        .isSymbolicLink()
+        .and()
+        .attribute("posix:permissions")
+        .isNot(permissions.value());
+  }
+
+  @Test
+  public void testCreateDirectories() throws IOException {
+    Files.createDirectories(path("/foo/bar/baz"));
+
+    assertThatPath("/foo").isDirectory();
+    assertThatPath("/foo/bar").isDirectory();
+    assertThatPath("/foo/bar/baz").isDirectory();
+
+    Files.createDirectories(path("/foo/asdf/jkl"));
+
+    assertThatPath("/foo/asdf").isDirectory();
+    assertThatPath("/foo/asdf/jkl").isDirectory();
+
+    Files.createDirectories(path("bar/baz"));
+
+    assertThatPath("bar/baz").isDirectory();
+    assertThatPath("/work/bar/baz").isDirectory();
+  }
+
+  @Test
+  public void testDirectories_newlyCreatedDirectoryHasTwoLinks() throws IOException {
+    // one link from its parent to it; one from it to itself
+
+    Files.createDirectory(path("/foo"));
+
+    assertThatPath("/foo").hasLinkCount(2);
+  }
+
+  @Test
+  public void testDirectories_creatingDirectoryAddsOneLinkToParent() throws IOException {
+    // from the .. direntry
+
+    Files.createDirectory(path("/foo"));
+    Files.createDirectory(path("/foo/bar"));
+
+    assertThatPath("/foo").hasLinkCount(3);
+
+    Files.createDirectory(path("/foo/baz"));
+
+    assertThatPath("/foo").hasLinkCount(4);
+  }
+
+  @Test
+  public void testDirectories_creatingNonDirectoryDoesNotAddLinkToParent() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createFile(path("/foo/file"));
+    Files.createSymbolicLink(path("/foo/fileSymlink"), path("file"));
+    Files.createLink(path("/foo/link"), path("/foo/file"));
+    Files.createSymbolicLink(path("/foo/fooSymlink"), path("/foo"));
+
+    assertThatPath("/foo").hasLinkCount(2);
+  }
+
+  @Test
+  public void testSize_forNewFile_isZero() throws IOException {
+    Files.createFile(path("/test"));
+
+    assertThatPath("/test").hasSize(0);
+  }
+
+  @Test
+  public void testRead_forNewFile_isEmpty() throws IOException {
+    Files.createFile(path("/test"));
+
+    assertThatPath("/test").containsNoBytes();
+  }
+
+  @Test
+  public void testWriteFile_succeeds() throws IOException {
+    Files.createFile(path("/test"));
+    Files.write(path("/test"), new byte[] {0, 1, 2, 3});
+  }
+
+  @Test
+  public void testSize_forFileAfterWrite_isNumberOfBytesWritten() throws IOException {
+    Files.write(path("/test"), new byte[] {0, 1, 2, 3});
+
+    assertThatPath("/test").hasSize(4);
+  }
+
+  @Test
+  public void testRead_forFileAfterWrite_isBytesWritten() throws IOException {
+    byte[] bytes = {0, 1, 2, 3};
+    Files.write(path("/test"), bytes);
+
+    assertThatPath("/test").containsBytes(bytes);
+  }
+
+  @Test
+  public void testWriteFile_withStandardOptions() throws IOException {
+    Path test = path("/test");
+    byte[] bytes = {0, 1, 2, 3};
+
+    try {
+      // CREATE and CREATE_NEW not specified
+      Files.write(test, bytes, WRITE);
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals(test.toString(), expected.getMessage());
+    }
+
+    Files.write(test, bytes, CREATE_NEW); // succeeds, file does not exist
+    assertThatPath("/test").containsBytes(bytes);
+
+    try {
+      Files.write(test, bytes, CREATE_NEW); // CREATE_NEW requires file not exist
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals(test.toString(), expected.getMessage());
+    }
+
+    Files.write(test, new byte[] {4, 5}, CREATE); // succeeds, ok for file to already exist
+    assertThatPath("/test").containsBytes(4, 5, 2, 3); // did not truncate or append, so overwrote
+
+    Files.write(test, bytes, WRITE, CREATE, TRUNCATE_EXISTING); // default options
+    assertThatPath("/test").containsBytes(bytes);
+
+    Files.write(test, bytes, WRITE, APPEND);
+    assertThatPath("/test").containsBytes(0, 1, 2, 3, 0, 1, 2, 3);
+
+    Files.write(test, bytes, WRITE, CREATE, TRUNCATE_EXISTING, APPEND, SPARSE, DSYNC, SYNC);
+    assertThatPath("/test").containsBytes(bytes);
+
+    try {
+      Files.write(test, bytes, READ, WRITE); // READ not allowed
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test
+  public void testWriteLines_succeeds() throws IOException {
+    Files.write(path("/test.txt"), ImmutableList.of("hello", "world"), UTF_8);
+  }
+
+  @Test
+  public void testOpenFile_withReadAndTruncateExisting_doesNotTruncateFile() throws IOException {
+    byte[] bytes = bytes(1, 2, 3, 4);
+    Files.write(path("/test"), bytes);
+
+    try (FileChannel channel = FileChannel.open(path("/test"), READ, TRUNCATE_EXISTING)) {
+      // TRUNCATE_EXISTING ignored when opening for read
+      byte[] readBytes = new byte[4];
+      channel.read(ByteBuffer.wrap(readBytes));
+
+      assertThat(Bytes.asList(readBytes)).isEqualTo(Bytes.asList(bytes));
+    }
+  }
+
+  @Test
+  public void testRead_forFileAfterWriteLines_isLinesWritten() throws IOException {
+    Files.write(path("/test.txt"), ImmutableList.of("hello", "world"), UTF_8);
+
+    assertThatPath("/test.txt").containsLines("hello", "world");
+  }
+
+  @Test
+  public void testWriteLines_withStandardOptions() throws IOException {
+    Path test = path("/test.txt");
+    ImmutableList<String> lines = ImmutableList.of("hello", "world");
+
+    try {
+      // CREATE and CREATE_NEW not specified
+      Files.write(test, lines, UTF_8, WRITE);
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals(test.toString(), expected.getMessage());
+    }
+
+    Files.write(test, lines, UTF_8, CREATE_NEW); // succeeds, file does not exist
+    assertThatPath(test).containsLines(lines);
+
+    try {
+      Files.write(test, lines, UTF_8, CREATE_NEW); // CREATE_NEW requires file not exist
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+    }
+
+    // succeeds, ok for file to already exist
+    Files.write(test, ImmutableList.of("foo"), UTF_8, CREATE);
+    // did not truncate or append, so overwrote
+    if (System.getProperty("line.separator").length() == 2) {
+      // on Windows, an extra character is overwritten by the \r\n line separator
+      assertThatPath(test).containsLines("foo", "", "world");
+    } else {
+      assertThatPath(test).containsLines("foo", "o", "world");
+    }
+
+    Files.write(test, lines, UTF_8, WRITE, CREATE, TRUNCATE_EXISTING); // default options
+    assertThatPath(test).containsLines(lines);
+
+    Files.write(test, lines, UTF_8, WRITE, APPEND);
+    assertThatPath(test).containsLines("hello", "world", "hello", "world");
+
+    Files.write(test, lines, UTF_8, WRITE, CREATE, TRUNCATE_EXISTING, APPEND, SPARSE, DSYNC, SYNC);
+    assertThatPath(test).containsLines(lines);
+
+    try {
+      Files.write(test, lines, UTF_8, READ, WRITE); // READ not allowed
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test
+  public void testWrite_fileExistsButIsNotRegularFile() throws IOException {
+    Files.createDirectory(path("/foo"));
+
+    try {
+      // non-CREATE mode
+      Files.write(path("/foo"), preFilledBytes(10), WRITE);
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo");
+      assertThat(expected.getMessage()).contains("regular file");
+    }
+
+    try {
+      // CREATE mode
+      Files.write(path("/foo"), preFilledBytes(10));
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo");
+      assertThat(expected.getMessage()).contains("regular file");
+    }
+  }
+
+  @Test
+  public void testDelete_file() throws IOException {
+    try {
+      Files.delete(path("/test"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/test", expected.getMessage());
+    }
+
+    try {
+      Files.delete(path("/foo/bar"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo/bar", expected.getMessage());
+    }
+
+    assertFalse(Files.deleteIfExists(path("/test")));
+    assertFalse(Files.deleteIfExists(path("/foo/bar")));
+
+    Files.createFile(path("/test"));
+    assertThatPath("/test").isRegularFile();
+
+    Files.delete(path("/test"));
+    assertThatPath("/test").doesNotExist();
+
+    Files.createFile(path("/test"));
+
+    assertTrue(Files.deleteIfExists(path("/test")));
+    assertThatPath("/test").doesNotExist();
+  }
+
+  @Test
+  public void testDelete_file_whenOpenReferencesRemain() throws IOException {
+    // the open streams should continue to function normally despite the deletion
+
+    Path foo = path("/foo");
+    byte[] bytes = preFilledBytes(100);
+    Files.write(foo, bytes);
+
+    InputStream in = Files.newInputStream(foo);
+    OutputStream out = Files.newOutputStream(foo, APPEND);
+    FileChannel channel = FileChannel.open(foo, READ, WRITE);
+
+    assertThat(channel.size()).isEqualTo(100L);
+
+    Files.delete(foo);
+    assertThatPath("/foo").doesNotExist();
+
+    assertThat(channel.size()).isEqualTo(100L);
+
+    ByteBuffer buf = ByteBuffer.allocate(100);
+    while (buf.hasRemaining()) {
+      channel.read(buf);
+    }
+
+    assertArrayEquals(bytes, buf.array());
+
+    byte[] moreBytes = {1, 2, 3, 4, 5};
+    out.write(moreBytes);
+
+    assertThat(channel.size()).isEqualTo(105L);
+    buf.clear();
+    assertThat(channel.read(buf)).isEqualTo(5);
+
+    buf.flip();
+    byte[] b = new byte[5];
+    buf.get(b);
+    assertArrayEquals(moreBytes, b);
+
+    byte[] allBytes = new byte[105];
+    int off = 0;
+    int read;
+    while ((read = in.read(allBytes, off, allBytes.length - off)) != -1) {
+      off += read;
+    }
+    assertArrayEquals(concat(bytes, moreBytes), allBytes);
+
+    channel.close();
+    out.close();
+    in.close();
+  }
+
+  @Test
+  public void testDelete_directory() throws IOException {
+    Files.createDirectories(path("/foo/bar"));
+    assertThatPath("/foo").isDirectory();
+    assertThatPath("/foo/bar").isDirectory();
+
+    Files.delete(path("/foo/bar"));
+    assertThatPath("/foo/bar").doesNotExist();
+
+    assertTrue(Files.deleteIfExists(path("/foo")));
+    assertThatPath("/foo").doesNotExist();
+  }
+
+  @Test
+  public void testDelete_pathPermutations() throws IOException {
+    Path bar = path("/work/foo/bar");
+    Files.createDirectories(bar);
+    for (Path path : permutations(bar)) {
+      Files.createDirectories(bar);
+      assertThatPath(path).isSameFileAs(bar);
+      Files.delete(path);
+      assertThatPath(bar).doesNotExist();
+      assertThatPath(path).doesNotExist();
+    }
+
+    Path baz = path("/test/baz");
+    Files.createDirectories(baz);
+    Path hello = baz.resolve("hello.txt");
+    for (Path path : permutations(hello)) {
+      Files.createFile(hello);
+      assertThatPath(path).isSameFileAs(hello);
+      Files.delete(path);
+      assertThatPath(hello).doesNotExist();
+      assertThatPath(path).doesNotExist();
+    }
+  }
+
+  @Test
+  public void testDelete_directory_cantDeleteNonEmptyDirectory() throws IOException {
+    Files.createDirectories(path("/foo/bar"));
+
+    try {
+      Files.delete(path("/foo"));
+      fail();
+    } catch (DirectoryNotEmptyException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo");
+    }
+
+    try {
+      Files.deleteIfExists(path("/foo"));
+      fail();
+    } catch (DirectoryNotEmptyException expected) {
+      assertThat(expected.getFile()).isEqualTo("/foo");
+    }
+  }
+
+  @Test
+  public void testDelete_directory_canDeleteWorkingDirectoryByAbsolutePath() throws IOException {
+    assertThatPath("/work").exists();
+    assertThatPath("").exists();
+    assertThatPath(".").exists();
+
+    Files.delete(path("/work"));
+
+    assertThatPath("/work").doesNotExist();
+    assertThatPath("").exists();
+    assertThatPath(".").exists();
+  }
+
+  @Test
+  public void testDelete_directory_cantDeleteWorkingDirectoryByRelativePath() throws IOException {
+    try {
+      Files.delete(path(""));
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo("");
+    }
+
+    try {
+      Files.delete(path("."));
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo(".");
+    }
+
+    try {
+      Files.delete(path("../../work"));
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo("../../work");
+    }
+
+    try {
+      Files.delete(path("./../work/.././../work/."));
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo("./../work/.././../work/.");
+    }
+  }
+
+  @Test
+  public void testDelete_directory_cantDeleteRoot() throws IOException {
+    // delete working directory so that root is empty
+    // don't want to just be testing the "can't delete when not empty" logic
+    Files.delete(path("/work"));
+
+    try {
+      Files.delete(path("/"));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("root");
+    }
+
+    Files.createDirectories(path("/foo/bar"));
+
+    try {
+      Files.delete(path("/foo/bar/../.."));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("root");
+    }
+
+    try {
+      Files.delete(path("/foo/./../foo/bar/./../bar/.././../../.."));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("root");
+    }
+  }
+
+  @Test
+  public void testSymbolicLinks() throws IOException {
+    Files.createSymbolicLink(path("/link.txt"), path("/file.txt"));
+    assertThatPath("/link.txt", NOFOLLOW_LINKS).isSymbolicLink().withTarget("/file.txt");
+    assertThatPath("/link.txt").doesNotExist(); // following the link; target doesn't exist
+
+    try {
+      Files.createFile(path("/link.txt"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+    }
+
+    try {
+      Files.readAllBytes(path("/link.txt"));
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+
+    Files.createFile(path("/file.txt"));
+    assertThatPath("/link.txt").isRegularFile(); // following the link; target does exist
+    assertThatPath("/link.txt").containsNoBytes();
+
+    Files.createSymbolicLink(path("/foo"), path("/bar/baz"));
+    assertThatPath("/foo", NOFOLLOW_LINKS).isSymbolicLink().withTarget("/bar/baz");
+    assertThatPath("/foo").doesNotExist(); // following the link; target doesn't exist
+
+    Files.createDirectories(path("/bar/baz"));
+    assertThatPath("/foo").isDirectory(); // following the link; target does exist
+
+    Files.createFile(path("/bar/baz/test.txt"));
+    assertThatPath("/foo/test.txt", NOFOLLOW_LINKS).isRegularFile(); // follow intermediate link
+
+    try {
+      Files.readSymbolicLink(path("/none"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/none", expected.getMessage());
+    }
+
+    try {
+      Files.readSymbolicLink(path("/file.txt"));
+      fail();
+    } catch (NotLinkException expected) {
+      assertEquals("/file.txt", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testSymbolicLinks_symlinkCycle() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createSymbolicLink(path("/foo/bar"), path("baz"));
+    Files.createSymbolicLink(path("/foo/baz"), path("bar"));
+
+    try {
+      Files.createFile(path("/foo/bar/file"));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("symbolic link");
+    }
+
+    try {
+      Files.write(path("/foo/bar"), preFilledBytes(10));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("symbolic link");
+    }
+  }
+
+  @Test
+  public void testSymbolicLinks_lookupOfAbsoluteSymlinkPathFromRelativePath() throws IOException {
+    // relative path lookups are in the FileSystemView for the working directory
+    // this tests that when an absolute path is encountered, the lookup handles it correctly
+
+    Files.createDirectories(path("/foo/bar/baz"));
+    Files.createFile(path("/foo/bar/baz/file"));
+    Files.createDirectories(path("one/two/three"));
+    Files.createSymbolicLink(path("/work/one/two/three/link"), path("/foo/bar"));
+
+    assertThatPath("one/two/three/link/baz/file").isSameFileAs("/foo/bar/baz/file");
+  }
+
+  @Test
+  public void testLink() throws IOException {
+    Files.createFile(path("/file.txt"));
+    // checking link count requires "unix" attribute support, which we're using here
+    assertThatPath("/file.txt").hasLinkCount(1);
+
+    Files.createLink(path("/link.txt"), path("/file.txt"));
+
+    assertThatPath("/link.txt").isSameFileAs("/file.txt");
+
+    assertThatPath("/file.txt").hasLinkCount(2);
+    assertThatPath("/link.txt").hasLinkCount(2);
+
+    assertThatPath("/file.txt").containsNoBytes();
+    assertThatPath("/link.txt").containsNoBytes();
+
+    byte[] bytes = {0, 1, 2, 3};
+    Files.write(path("/file.txt"), bytes);
+
+    assertThatPath("/file.txt").containsBytes(bytes);
+    assertThatPath("/link.txt").containsBytes(bytes);
+
+    Files.write(path("/link.txt"), bytes, APPEND);
+
+    assertThatPath("/file.txt").containsBytes(0, 1, 2, 3, 0, 1, 2, 3);
+    assertThatPath("/link.txt").containsBytes(0, 1, 2, 3, 0, 1, 2, 3);
+
+    Files.delete(path("/file.txt"));
+    assertThatPath("/link.txt").hasLinkCount(1);
+
+    assertThatPath("/link.txt").containsBytes(0, 1, 2, 3, 0, 1, 2, 3);
+  }
+
+  @Test
+  public void testLink_forSymbolicLink_usesSymbolicLinkTarget() throws IOException {
+    Files.createFile(path("/file"));
+    Files.createSymbolicLink(path("/symlink"), path("/file"));
+
+    Object key = getFileKey("/file");
+
+    Files.createLink(path("/link"), path("/symlink"));
+
+    assertThatPath("/link")
+        .isRegularFile()
+        .and()
+        .hasLinkCount(2)
+        .and()
+        .attribute("fileKey")
+        .is(key);
+  }
+
+  @Test
+  public void testLink_failsWhenTargetDoesNotExist() throws IOException {
+    try {
+      Files.createLink(path("/link"), path("/foo"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo", expected.getFile());
+    }
+
+    Files.createSymbolicLink(path("/foo"), path("/bar"));
+
+    try {
+      Files.createLink(path("/link"), path("/foo"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/foo", expected.getFile());
+    }
+  }
+
+  @Test
+  public void testLink_failsForNonRegularFile() throws IOException {
+    Files.createDirectory(path("/dir"));
+
+    try {
+      Files.createLink(path("/link"), path("/dir"));
+      fail();
+    } catch (FileSystemException expected) {
+      assertEquals("/link", expected.getFile());
+      assertEquals("/dir", expected.getOtherFile());
+    }
+
+    assertThatPath("/link").doesNotExist();
+  }
+
+  @Test
+  public void testLinks_failsWhenTargetFileAlreadyExists() throws IOException {
+    Files.createFile(path("/file"));
+    Files.createFile(path("/link"));
+
+    try {
+      Files.createLink(path("/link"), path("/file"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/link", expected.getFile());
+    }
+  }
+
+  @Test
+  public void testStreams() throws IOException {
+    try (OutputStream out = Files.newOutputStream(path("/test"))) {
+      for (int i = 0; i < 100; i++) {
+        out.write(i);
+      }
+    }
+
+    byte[] expected = new byte[100];
+    for (byte i = 0; i < 100; i++) {
+      expected[i] = i;
+    }
+
+    try (InputStream in = Files.newInputStream(path("/test"))) {
+      byte[] bytes = new byte[100];
+      ByteStreams.readFully(in, bytes);
+      assertArrayEquals(expected, bytes);
+    }
+
+    try (Writer writer = Files.newBufferedWriter(path("/test.txt"), UTF_8)) {
+      writer.write("hello");
+    }
+
+    try (Reader reader = Files.newBufferedReader(path("/test.txt"), UTF_8)) {
+      assertEquals("hello", CharStreams.toString(reader));
+    }
+
+    try (Writer writer = Files.newBufferedWriter(path("/test.txt"), UTF_8, APPEND)) {
+      writer.write(" world");
+    }
+
+    try (Reader reader = Files.newBufferedReader(path("/test.txt"), UTF_8)) {
+      assertEquals("hello world", CharStreams.toString(reader));
+    }
+  }
+
+  @Test
+  public void testOutputStream_withTruncateExistingAndNotWrite_truncatesFile() throws IOException {
+    // https://github.com/google/jimfs/pull/77
+    Path path = path("/test");
+    Files.write(path, new byte[] {1, 2, 3});
+    assertThatPath(path).containsBytes(1, 2, 3);
+
+    try (OutputStream out = Files.newOutputStream(path, CREATE, TRUNCATE_EXISTING)) {
+      out.write(new byte[] {1, 2});
+    }
+
+    assertThatPath(path).containsBytes(1, 2);
+  }
+
+  @Test
+  public void testChannels() throws IOException {
+    try (FileChannel channel = FileChannel.open(path("/test.txt"), CREATE_NEW, WRITE)) {
+      ByteBuffer buf1 = UTF_8.encode("hello");
+      ByteBuffer buf2 = UTF_8.encode(" world");
+      while (buf1.hasRemaining() || buf2.hasRemaining()) {
+        channel.write(new ByteBuffer[] {buf1, buf2});
+      }
+
+      assertEquals(11, channel.position());
+      assertEquals(11, channel.size());
+
+      channel.write(UTF_8.encode("!"));
+
+      assertEquals(12, channel.position());
+      assertEquals(12, channel.size());
+    }
+
+    try (SeekableByteChannel channel = Files.newByteChannel(path("/test.txt"), READ)) {
+      assertEquals(0, channel.position());
+      assertEquals(12, channel.size());
+
+      ByteBuffer buffer = ByteBuffer.allocate(100);
+      while (channel.read(buffer) != -1) {}
+      buffer.flip();
+      assertEquals("hello world!", UTF_8.decode(buffer).toString());
+    }
+
+    byte[] bytes = preFilledBytes(100);
+
+    Files.write(path("/test"), bytes);
+
+    try (SeekableByteChannel channel = Files.newByteChannel(path("/test"), READ, WRITE)) {
+      ByteBuffer buffer = ByteBuffer.wrap(preFilledBytes(50));
+
+      channel.position(50);
+      channel.write(buffer);
+      buffer.flip();
+      channel.write(buffer);
+
+      channel.position(0);
+      ByteBuffer readBuffer = ByteBuffer.allocate(150);
+      while (readBuffer.hasRemaining()) {
+        channel.read(readBuffer);
+      }
+
+      byte[] expected = Bytes.concat(preFilledBytes(50), preFilledBytes(50), preFilledBytes(50));
+
+      assertArrayEquals(expected, readBuffer.array());
+    }
+
+    try (FileChannel channel = FileChannel.open(path("/test"), READ, WRITE)) {
+      assertEquals(150, channel.size());
+
+      channel.truncate(10);
+      assertEquals(10, channel.size());
+
+      ByteBuffer buffer = ByteBuffer.allocate(20);
+      assertEquals(10, channel.read(buffer));
+      buffer.flip();
+
+      byte[] expected = new byte[20];
+      System.arraycopy(preFilledBytes(10), 0, expected, 0, 10);
+      assertArrayEquals(expected, buffer.array());
+    }
+  }
+
+  @Test
+  public void testCopy_inputStreamToFile() throws IOException {
+    byte[] bytes = preFilledBytes(512);
+
+    Files.copy(new ByteArrayInputStream(bytes), path("/test"));
+    assertThatPath("/test").containsBytes(bytes);
+
+    try {
+      Files.copy(new ByteArrayInputStream(bytes), path("/test"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/test", expected.getMessage());
+    }
+
+    Files.copy(new ByteArrayInputStream(bytes), path("/test"), REPLACE_EXISTING);
+    assertThatPath("/test").containsBytes(bytes);
+
+    Files.copy(new ByteArrayInputStream(bytes), path("/foo"), REPLACE_EXISTING);
+    assertThatPath("/foo").containsBytes(bytes);
+  }
+
+  @Test
+  public void testCopy_fileToOutputStream() throws IOException {
+    byte[] bytes = preFilledBytes(512);
+    Files.write(path("/test"), bytes);
+
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    Files.copy(path("/test"), out);
+    assertArrayEquals(bytes, out.toByteArray());
+  }
+
+  @Test
+  public void testCopy_fileToPath() throws IOException {
+    byte[] bytes = preFilledBytes(512);
+    Files.write(path("/foo"), bytes);
+
+    assertThatPath("/bar").doesNotExist();
+    Files.copy(path("/foo"), path("/bar"));
+    assertThatPath("/bar").containsBytes(bytes);
+
+    byte[] moreBytes = preFilledBytes(2048);
+    Files.write(path("/baz"), moreBytes);
+
+    Files.copy(path("/baz"), path("/bar"), REPLACE_EXISTING);
+    assertThatPath("/bar").containsBytes(moreBytes);
+
+    try {
+      Files.copy(path("/none"), path("/bar"));
+      fail();
+    } catch (NoSuchFileException expected) {
+      assertEquals("/none", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCopy_withCopyAttributes() throws IOException {
+    Path foo = path("/foo");
+    Files.createFile(foo);
+
+    Files.getFileAttributeView(foo, BasicFileAttributeView.class)
+        .setTimes(FileTime.fromMillis(100), FileTime.fromMillis(1000), FileTime.fromMillis(10000));
+
+    assertThat(Files.getAttribute(foo, "lastModifiedTime")).isEqualTo(FileTime.fromMillis(100));
+
+    UserPrincipal zero = fs.getUserPrincipalLookupService().lookupPrincipalByName("zero");
+    Files.setAttribute(foo, "owner:owner", zero);
+
+    Path bar = path("/bar");
+    Files.copy(foo, bar, COPY_ATTRIBUTES);
+
+    BasicFileAttributes attributes = Files.readAttributes(bar, BasicFileAttributes.class);
+    assertThat(attributes.lastModifiedTime()).isEqualTo(FileTime.fromMillis(100));
+    assertThat(attributes.lastAccessTime()).isEqualTo(FileTime.fromMillis(1000));
+    assertThat(attributes.creationTime()).isEqualTo(FileTime.fromMillis(10000));
+    assertThat(Files.getAttribute(bar, "owner:owner")).isEqualTo(zero);
+
+    Path baz = path("/baz");
+    Files.copy(foo, baz);
+
+    // test that attributes are not copied when COPY_ATTRIBUTES is not specified
+    attributes = Files.readAttributes(baz, BasicFileAttributes.class);
+    assertThat(attributes.lastModifiedTime()).isNotEqualTo(FileTime.fromMillis(100));
+    assertThat(attributes.lastAccessTime()).isNotEqualTo(FileTime.fromMillis(1000));
+    assertThat(attributes.creationTime()).isNotEqualTo(FileTime.fromMillis(10000));
+    assertThat(Files.getAttribute(baz, "owner:owner")).isNotEqualTo(zero);
+  }
+
+  @Test
+  public void testCopy_doesNotSupportAtomicMove() throws IOException {
+    try {
+      Files.copy(path("/foo"), path("/bar"), ATOMIC_MOVE);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+  }
+
+  @Test
+  public void testCopy_directoryToPath() throws IOException {
+    Files.createDirectory(path("/foo"));
+
+    assertThatPath("/bar").doesNotExist();
+    Files.copy(path("/foo"), path("/bar"));
+    assertThatPath("/bar").isDirectory();
+  }
+
+  @Test
+  public void testCopy_withoutReplaceExisting_failsWhenTargetExists() throws IOException {
+    Files.createFile(path("/bar"));
+    Files.createDirectory(path("/foo"));
+
+    // dir -> file
+    try {
+      Files.copy(path("/foo"), path("/bar"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/bar", expected.getMessage());
+    }
+
+    Files.delete(path("/foo"));
+    Files.createFile(path("/foo"));
+
+    // file -> file
+    try {
+      Files.copy(path("/foo"), path("/bar"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/bar", expected.getMessage());
+    }
+
+    Files.delete(path("/bar"));
+    Files.createDirectory(path("/bar"));
+
+    // file -> dir
+    try {
+      Files.copy(path("/foo"), path("/bar"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/bar", expected.getMessage());
+    }
+
+    Files.delete(path("/foo"));
+    Files.createDirectory(path("/foo"));
+
+    // dir -> dir
+    try {
+      Files.copy(path("/foo"), path("/bar"));
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/bar", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testCopy_withReplaceExisting() throws IOException {
+    Files.createFile(path("/bar"));
+    Files.createDirectory(path("/test"));
+
+    assertThatPath("/bar").isRegularFile();
+
+    // overwrite regular file w/ directory
+    Files.copy(path("/test"), path("/bar"), REPLACE_EXISTING);
+
+    assertThatPath("/bar").isDirectory();
+
+    byte[] bytes = {0, 1, 2, 3};
+    Files.write(path("/baz"), bytes);
+
+    // overwrite directory w/ regular file
+    Files.copy(path("/baz"), path("/bar"), REPLACE_EXISTING);
+
+    assertThatPath("/bar").containsSameBytesAs("/baz");
+  }
+
+  @Test
+  public void testCopy_withReplaceExisting_cantReplaceNonEmptyDirectory() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createDirectory(path("/foo/bar"));
+    Files.createFile(path("/foo/baz"));
+
+    Files.createDirectory(path("/test"));
+
+    try {
+      Files.copy(path("/test"), path("/foo"), REPLACE_EXISTING);
+      fail();
+    } catch (DirectoryNotEmptyException expected) {
+      assertEquals("/foo", expected.getMessage());
+    }
+
+    Files.delete(path("/test"));
+    Files.createFile(path("/test"));
+
+    try {
+      Files.copy(path("/test"), path("/foo"), REPLACE_EXISTING);
+      fail();
+    } catch (DirectoryNotEmptyException expected) {
+      assertEquals("/foo", expected.getMessage());
+    }
+
+    Files.delete(path("/foo/baz"));
+    Files.delete(path("/foo/bar"));
+
+    Files.copy(path("/test"), path("/foo"), REPLACE_EXISTING);
+    assertThatPath("/foo").isRegularFile(); // replaced
+  }
+
+  @Test
+  public void testCopy_directoryToPath_doesNotCopyDirectoryContents() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createDirectory(path("/foo/baz"));
+    Files.createFile(path("/foo/test"));
+
+    Files.copy(path("/foo"), path("/bar"));
+    assertThatPath("/bar").hasNoChildren();
+  }
+
+  @Test
+  public void testCopy_symbolicLinkToPath() throws IOException {
+    byte[] bytes = preFilledBytes(128);
+    Files.write(path("/test"), bytes);
+    Files.createSymbolicLink(path("/link"), path("/test"));
+
+    assertThatPath("/bar").doesNotExist();
+    Files.copy(path("/link"), path("/bar"));
+    assertThatPath("/bar", NOFOLLOW_LINKS).containsBytes(bytes);
+
+    Files.delete(path("/bar"));
+
+    Files.copy(path("/link"), path("/bar"), NOFOLLOW_LINKS);
+    assertThatPath("/bar", NOFOLLOW_LINKS).isSymbolicLink().withTarget("/test");
+    assertThatPath("/bar").isRegularFile();
+    assertThatPath("/bar").containsBytes(bytes);
+
+    Files.delete(path("/test"));
+    assertThatPath("/bar", NOFOLLOW_LINKS).isSymbolicLink();
+    assertThatPath("/bar").doesNotExist();
+  }
+
+  @Test
+  public void testCopy_toDifferentFileSystem() throws IOException {
+    try (FileSystem fs2 = Jimfs.newFileSystem(UNIX_CONFIGURATION)) {
+      Path foo = fs.getPath("/foo");
+      byte[] bytes = {0, 1, 2, 3, 4};
+      Files.write(foo, bytes);
+
+      Path foo2 = fs2.getPath("/foo");
+      Files.copy(foo, foo2);
+
+      assertThatPath(foo).exists();
+      assertThatPath(foo2).exists().and().containsBytes(bytes);
+    }
+  }
+
+  @Test
+  public void testCopy_toDifferentFileSystem_copyAttributes() throws IOException {
+    try (FileSystem fs2 = Jimfs.newFileSystem(UNIX_CONFIGURATION)) {
+      Path foo = fs.getPath("/foo");
+      byte[] bytes = {0, 1, 2, 3, 4};
+      Files.write(foo, bytes);
+      Files.getFileAttributeView(foo, BasicFileAttributeView.class)
+          .setTimes(FileTime.fromMillis(0), FileTime.fromMillis(1), FileTime.fromMillis(2));
+
+      UserPrincipal owner = fs.getUserPrincipalLookupService().lookupPrincipalByName("foobar");
+      Files.setOwner(foo, owner);
+
+      assertThatPath(foo).attribute("owner:owner").is(owner);
+
+      Path foo2 = fs2.getPath("/foo");
+      Files.copy(foo, foo2, COPY_ATTRIBUTES);
+
+      assertThatPath(foo).exists();
+
+      // when copying with COPY_ATTRIBUTES to a different FileSystem, only basic attributes (that
+      // is, file times) can actually be copied
+      assertThatPath(foo2)
+          .exists()
+          .and()
+          .attribute("lastModifiedTime")
+          .is(FileTime.fromMillis(0))
+          .and()
+          .attribute("lastAccessTime")
+          .is(FileTime.fromMillis(1))
+          .and()
+          .attribute("creationTime")
+          .is(FileTime.fromMillis(2))
+          .and()
+          .attribute("owner:owner")
+          .isNot(owner)
+          .and()
+          .attribute("owner:owner")
+          .isNot(fs2.getUserPrincipalLookupService().lookupPrincipalByName("foobar"))
+          .and()
+          .containsBytes(bytes); // do this last; it updates the access time
+    }
+  }
+
+  @Test
+  public void testMove() throws IOException {
+    byte[] bytes = preFilledBytes(100);
+    Files.write(path("/foo"), bytes);
+
+    Object fooKey = getFileKey("/foo");
+
+    Files.move(path("/foo"), path("/bar"));
+    assertThatPath("/foo").doesNotExist();
+    assertThatPath("/bar").containsBytes(bytes).and().attribute("fileKey").is(fooKey);
+
+    Files.createDirectory(path("/foo"));
+    Files.move(path("/bar"), path("/foo/bar"));
+
+    assertThatPath("/bar").doesNotExist();
+    assertThatPath("/foo/bar").isRegularFile();
+
+    Files.move(path("/foo"), path("/baz"));
+    assertThatPath("/foo").doesNotExist();
+    assertThatPath("/baz").isDirectory();
+    assertThatPath("/baz/bar").isRegularFile();
+  }
+
+  @Test
+  public void testMove_movesSymbolicLinkNotTarget() throws IOException {
+    byte[] bytes = preFilledBytes(100);
+    Files.write(path("/foo.txt"), bytes);
+
+    Files.createSymbolicLink(path("/link"), path("foo.txt"));
+
+    Files.move(path("/link"), path("/link.txt"));
+
+    assertThatPath("/foo.txt").noFollowLinks().isRegularFile().and().containsBytes(bytes);
+
+    assertThatPath(path("/link")).doesNotExist();
+
+    assertThatPath(path("/link.txt")).noFollowLinks().isSymbolicLink();
+
+    assertThatPath(path("/link.txt")).isRegularFile().and().containsBytes(bytes);
+  }
+
+  @Test
+  public void testMove_cannotMoveDirIntoOwnSubtree() throws IOException {
+    Files.createDirectories(path("/foo"));
+
+    try {
+      Files.move(path("/foo"), path("/foo/bar"));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("sub");
+    }
+
+    Files.createDirectories(path("/foo/bar/baz/stuff"));
+    Files.createDirectories(path("/hello/world"));
+    Files.createSymbolicLink(path("/hello/world/link"), path("../../foo/bar/baz"));
+
+    try {
+      Files.move(path("/foo/bar"), path("/hello/world/link/bar"));
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected.getMessage()).contains("sub");
+    }
+  }
+
+  @Test
+  public void testMove_withoutReplaceExisting_failsWhenTargetExists() throws IOException {
+    byte[] bytes = preFilledBytes(50);
+    Files.write(path("/test"), bytes);
+
+    Object testKey = getFileKey("/test");
+
+    Files.createFile(path("/bar"));
+
+    try {
+      Files.move(path("/test"), path("/bar"), ATOMIC_MOVE);
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/bar", expected.getMessage());
+    }
+
+    assertThatPath("/test").containsBytes(bytes).and().attribute("fileKey").is(testKey);
+
+    Files.delete(path("/bar"));
+    Files.createDirectory(path("/bar"));
+
+    try {
+      Files.move(path("/test"), path("/bar"), ATOMIC_MOVE);
+      fail();
+    } catch (FileAlreadyExistsException expected) {
+      assertEquals("/bar", expected.getMessage());
+    }
+
+    assertThatPath("/test").containsBytes(bytes).and().attribute("fileKey").is(testKey);
+  }
+
+  @Test
+  public void testMove_toDifferentFileSystem() throws IOException {
+    try (FileSystem fs2 = Jimfs.newFileSystem(Configuration.unix())) {
+      Path foo = fs.getPath("/foo");
+      byte[] bytes = {0, 1, 2, 3, 4};
+      Files.write(foo, bytes);
+      Files.getFileAttributeView(foo, BasicFileAttributeView.class)
+          .setTimes(FileTime.fromMillis(0), FileTime.fromMillis(1), FileTime.fromMillis(2));
+
+      Path foo2 = fs2.getPath("/foo");
+      Files.move(foo, foo2);
+
+      assertThatPath(foo).doesNotExist();
+      assertThatPath(foo2)
+          .exists()
+          .and()
+          .attribute("lastModifiedTime")
+          .is(FileTime.fromMillis(0))
+          .and()
+          .attribute("lastAccessTime")
+          .is(FileTime.fromMillis(1))
+          .and()
+          .attribute("creationTime")
+          .is(FileTime.fromMillis(2))
+          .and()
+          .containsBytes(bytes); // do this last; it updates the access time
+    }
+  }
+
+  @Test
+  public void testIsSameFile() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createSymbolicLink(path("/bar"), path("/foo"));
+    Files.createFile(path("/bar/test"));
+
+    assertThatPath("/foo").isSameFileAs("/foo");
+    assertThatPath("/bar").isSameFileAs("/bar");
+    assertThatPath("/foo/test").isSameFileAs("/foo/test");
+    assertThatPath("/bar/test").isSameFileAs("/bar/test");
+    assertThatPath("/foo").isNotSameFileAs("test");
+    assertThatPath("/bar").isNotSameFileAs("/test");
+    assertThatPath("/foo").isSameFileAs("/bar");
+    assertThatPath("/foo/test").isSameFileAs("/bar/test");
+
+    Files.createSymbolicLink(path("/baz"), path("bar")); // relative path
+    assertThatPath("/baz").isSameFileAs("/foo");
+    assertThatPath("/baz/test").isSameFileAs("/foo/test");
+  }
+
+  @Test
+  public void testIsSameFile_forPathFromDifferentFileSystemProvider() throws IOException {
+    Path defaultFileSystemRoot = FileSystems.getDefault().getRootDirectories().iterator().next();
+
+    assertThat(Files.isSameFile(path("/"), defaultFileSystemRoot)).isFalse();
+  }
+
+  @Test
+  public void testPathLookups() throws IOException {
+    assertThatPath("/").isSameFileAs("/");
+    assertThatPath("/..").isSameFileAs("/");
+    assertThatPath("/../../..").isSameFileAs("/");
+    assertThatPath("../../../..").isSameFileAs("/");
+    assertThatPath("").isSameFileAs("/work");
+
+    Files.createDirectories(path("/foo/bar/baz"));
+    Files.createSymbolicLink(path("/foo/bar/link1"), path("../link2"));
+    Files.createSymbolicLink(path("/foo/link2"), path("/"));
+
+    assertThatPath("/foo/bar/link1/foo/bar/link1/foo").isSameFileAs("/foo");
+  }
+
+  @Test
+  public void testSecureDirectoryStream() throws IOException {
+    Files.createDirectories(path("/foo/bar"));
+    Files.createFile(path("/foo/a"));
+    Files.createFile(path("/foo/b"));
+    Files.createSymbolicLink(path("/foo/barLink"), path("bar"));
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path("/foo"))) {
+      if (!(stream instanceof SecureDirectoryStream)) {
+        fail("should be a secure directory stream");
+      }
+
+      SecureDirectoryStream<Path> secureStream = (SecureDirectoryStream<Path>) stream;
+
+      assertThat(ImmutableList.copyOf(secureStream))
+          .isEqualTo(
+              ImmutableList.of(
+                  path("/foo/a"), path("/foo/b"), path("/foo/bar"), path("/foo/barLink")));
+
+      secureStream.deleteFile(path("b"));
+      assertThatPath("/foo/b").doesNotExist();
+
+      secureStream.newByteChannel(path("b"), ImmutableSet.of(WRITE, CREATE_NEW)).close();
+      assertThatPath("/foo/b").isRegularFile();
+
+      assertThatPath("/foo").hasChildren("a", "b", "bar", "barLink");
+
+      Files.createDirectory(path("/baz"));
+      Files.move(path("/foo"), path("/baz/stuff"));
+
+      assertThatPath(path("/foo")).doesNotExist();
+
+      assertThatPath("/baz/stuff").hasChildren("a", "b", "bar", "barLink");
+
+      secureStream.deleteFile(path("b"));
+
+      assertThatPath("/baz/stuff/b").doesNotExist();
+      assertThatPath("/baz/stuff").hasChildren("a", "bar", "barLink");
+
+      assertThat(
+              secureStream
+                  .getFileAttributeView(BasicFileAttributeView.class)
+                  .readAttributes()
+                  .isDirectory())
+          .isTrue();
+
+      assertThat(
+              secureStream
+                  .getFileAttributeView(path("a"), BasicFileAttributeView.class)
+                  .readAttributes()
+                  .isRegularFile())
+          .isTrue();
+
+      try {
+        secureStream.deleteFile(path("bar"));
+        fail();
+      } catch (FileSystemException expected) {
+        assertThat(expected.getFile()).isEqualTo("bar");
+      }
+
+      try {
+        secureStream.deleteDirectory(path("a"));
+        fail();
+      } catch (FileSystemException expected) {
+        assertThat(expected.getFile()).isEqualTo("a");
+      }
+
+      try (SecureDirectoryStream<Path> barStream = secureStream.newDirectoryStream(path("bar"))) {
+        barStream.newByteChannel(path("stuff"), ImmutableSet.of(WRITE, CREATE_NEW)).close();
+        assertThat(
+                barStream
+                    .getFileAttributeView(path("stuff"), BasicFileAttributeView.class)
+                    .readAttributes()
+                    .isRegularFile())
+            .isTrue();
+
+        assertThat(
+                secureStream
+                    .getFileAttributeView(path("bar/stuff"), BasicFileAttributeView.class)
+                    .readAttributes()
+                    .isRegularFile())
+            .isTrue();
+      }
+
+      try (SecureDirectoryStream<Path> barLinkStream =
+          secureStream.newDirectoryStream(path("barLink"))) {
+        assertThat(
+                barLinkStream
+                    .getFileAttributeView(path("stuff"), BasicFileAttributeView.class)
+                    .readAttributes()
+                    .isRegularFile())
+            .isTrue();
+
+        assertThat(
+                barLinkStream
+                    .getFileAttributeView(path(".."), BasicFileAttributeView.class)
+                    .readAttributes()
+                    .isDirectory())
+            .isTrue();
+      }
+
+      try {
+        secureStream.newDirectoryStream(path("barLink"), NOFOLLOW_LINKS);
+        fail();
+      } catch (NotDirectoryException expected) {
+        assertThat(expected.getFile()).isEqualTo("barLink");
+      }
+
+      try (SecureDirectoryStream<Path> barStream = secureStream.newDirectoryStream(path("bar"))) {
+        secureStream.move(path("a"), barStream, path("moved"));
+
+        assertThatPath(path("/baz/stuff/a")).doesNotExist();
+        assertThatPath(path("/baz/stuff/bar/moved")).isRegularFile();
+
+        assertThat(
+                barStream
+                    .getFileAttributeView(path("moved"), BasicFileAttributeView.class)
+                    .readAttributes()
+                    .isRegularFile())
+            .isTrue();
+      }
+    }
+  }
+
+  @Test
+  public void testSecureDirectoryStreamBasedOnRelativePath() throws IOException {
+    Files.createDirectories(path("foo"));
+    Files.createFile(path("foo/a"));
+    Files.createFile(path("foo/b"));
+    Files.createDirectory(path("foo/c"));
+    Files.createFile(path("foo/c/d"));
+    Files.createFile(path("foo/c/e"));
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path("foo"))) {
+      SecureDirectoryStream<Path> secureStream = (SecureDirectoryStream<Path>) stream;
+
+      assertThat(ImmutableList.copyOf(secureStream))
+          .containsExactly(path("foo/a"), path("foo/b"), path("foo/c"));
+
+      try (DirectoryStream<Path> stream2 = secureStream.newDirectoryStream(path("c"))) {
+        assertThat(ImmutableList.copyOf(stream2)).containsExactly(path("foo/c/d"), path("foo/c/e"));
+      }
+    }
+  }
+
+  @SuppressWarnings("StreamResourceLeak")
+  @Test
+  public void testClosedSecureDirectoryStream() throws IOException {
+    Files.createDirectory(path("/foo"));
+    SecureDirectoryStream<Path> stream =
+        (SecureDirectoryStream<Path>) Files.newDirectoryStream(path("/foo"));
+
+    stream.close();
+
+    try {
+      stream.iterator();
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.deleteDirectory(fs.getPath("a"));
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.deleteFile(fs.getPath("a"));
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.newByteChannel(fs.getPath("a"), ImmutableSet.of(CREATE, WRITE));
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.newDirectoryStream(fs.getPath("a"));
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.move(fs.getPath("a"), stream, fs.getPath("b"));
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.getFileAttributeView(BasicFileAttributeView.class);
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      stream.getFileAttributeView(fs.getPath("a"), BasicFileAttributeView.class);
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+  }
+
+  @SuppressWarnings("StreamResourceLeak")
+  @Test
+  public void testClosedSecureDirectoryStreamAttributeViewAndIterator() throws IOException {
+    Files.createDirectory(path("/foo"));
+    Files.createDirectory(path("/foo/bar"));
+    SecureDirectoryStream<Path> stream =
+        (SecureDirectoryStream<Path>) Files.newDirectoryStream(path("/foo"));
+
+    Iterator<Path> iter = stream.iterator();
+    BasicFileAttributeView view1 = stream.getFileAttributeView(BasicFileAttributeView.class);
+    BasicFileAttributeView view2 =
+        stream.getFileAttributeView(path("bar"), BasicFileAttributeView.class);
+
+    try {
+      stream.iterator();
+      fail("expected IllegalStateException");
+    } catch (IllegalStateException expected) {
+    }
+
+    stream.close();
+
+    try {
+      iter.next();
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      view1.readAttributes();
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      view2.readAttributes();
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      view1.setTimes(null, null, null);
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+
+    try {
+      view2.setTimes(null, null, null);
+      fail("expected ClosedDirectoryStreamException");
+    } catch (ClosedDirectoryStreamException expected) {
+    }
+  }
+
+  @Test
+  public void testDirectoryAccessAndModifiedTimeUpdates() throws IOException {
+    Files.createDirectories(path("/foo/bar"));
+    FileTimeTester tester = new FileTimeTester(path("/foo/bar"));
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeDidNotChange();
+
+    // TODO(cgdecker): Use a Clock for file times so I can test this reliably without sleeping
+    Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    Files.createFile(path("/foo/bar/baz.txt"));
+
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeChanged();
+
+    Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    // access time is updated by reading the full contents of the directory
+    // not just by doing a lookup in it
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path("/foo/bar"))) {
+      // iterate the stream, forcing the directory to actually be read
+      Iterators.advance(stream.iterator(), Integer.MAX_VALUE);
+    }
+
+    tester.assertAccessTimeChanged();
+    tester.assertModifiedTimeDidNotChange();
+
+    Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    Files.move(path("/foo/bar/baz.txt"), path("/foo/bar/baz2.txt"));
+
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeChanged();
+
+    Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    Files.delete(path("/foo/bar/baz2.txt"));
+
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeChanged();
+  }
+
+  @Test
+  public void testRegularFileAccessAndModifiedTimeUpdates() throws IOException {
+    Path foo = path("foo");
+    Files.createFile(foo);
+
+    FileTimeTester tester = new FileTimeTester(foo);
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeDidNotChange();
+
+    Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    try (FileChannel channel = FileChannel.open(foo, READ)) {
+      // opening READ channel does not change times
+      tester.assertAccessTimeDidNotChange();
+      tester.assertModifiedTimeDidNotChange();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+      channel.read(ByteBuffer.allocate(100));
+
+      // read call on channel does
+      tester.assertAccessTimeChanged();
+      tester.assertModifiedTimeDidNotChange();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+      channel.read(ByteBuffer.allocate(100));
+
+      tester.assertAccessTimeChanged();
+      tester.assertModifiedTimeDidNotChange();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+      try {
+        channel.write(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}));
+      } catch (NonWritableChannelException ignore) {
+      }
+
+      // failed write on non-readable channel does not change times
+      tester.assertAccessTimeDidNotChange();
+      tester.assertModifiedTimeDidNotChange();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    }
+
+    // closing channel does not change times
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeDidNotChange();
+
+    Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    try (FileChannel channel = FileChannel.open(foo, WRITE)) {
+      // opening WRITE channel does not change times
+      tester.assertAccessTimeDidNotChange();
+      tester.assertModifiedTimeDidNotChange();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+      channel.write(ByteBuffer.wrap(new byte[] {0, 1, 2, 3}));
+
+      // write call on channel does
+      tester.assertAccessTimeDidNotChange();
+      tester.assertModifiedTimeChanged();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+      channel.write(ByteBuffer.wrap(new byte[] {4, 5, 6, 7}));
+
+      tester.assertAccessTimeDidNotChange();
+      tester.assertModifiedTimeChanged();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+      try {
+        channel.read(ByteBuffer.allocate(100));
+      } catch (NonReadableChannelException ignore) {
+      }
+
+      // failed read on non-readable channel does not change times
+      tester.assertAccessTimeDidNotChange();
+      tester.assertModifiedTimeDidNotChange();
+
+      Uninterruptibles.sleepUninterruptibly(1, MILLISECONDS);
+    }
+
+    // closing channel does not change times
+    tester.assertAccessTimeDidNotChange();
+    tester.assertModifiedTimeDidNotChange();
+  }
+
+  @Test
+  public void testUnsupportedFeatures() throws IOException {
+    FileSystem fileSystem =
+        Jimfs.newFileSystem(
+            Configuration.unix().toBuilder()
+                .setSupportedFeatures() // none
+                .build());
+
+    Path foo = fileSystem.getPath("foo");
+    Path bar = foo.resolveSibling("bar");
+
+    try {
+      Files.createLink(foo, bar);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    try {
+      Files.createSymbolicLink(foo, bar);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    try {
+      Files.readSymbolicLink(foo);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    try {
+      FileChannel.open(foo);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    try {
+      AsynchronousFileChannel.open(foo);
+      fail();
+    } catch (UnsupportedOperationException expected) {
+    }
+
+    Files.createDirectory(foo);
+    Files.createFile(bar);
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(foo)) {
+      assertThat(stream).isNotInstanceOf(SecureDirectoryStream.class);
+    }
+
+    try (SeekableByteChannel channel = Files.newByteChannel(bar)) {
+      assertThat(channel).isNotInstanceOf(FileChannel.class);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/JimfsWindowsLikeFileSystemTest.java b/jimfs/src/test/java/com/google/common/jimfs/JimfsWindowsLikeFileSystemTest.java
new file mode 100644
index 0000000..a3b7ad2
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/JimfsWindowsLikeFileSystemTest.java
@@ -0,0 +1,491 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.regex.PatternSyntaxException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests a Windows-like file system through the public methods in {@link Files}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class JimfsWindowsLikeFileSystemTest extends AbstractJimfsIntegrationTest {
+
+  @Override
+  protected FileSystem createFileSystem() {
+    return Jimfs.newFileSystem(
+        "win",
+        Configuration.windows().toBuilder()
+            .setRoots("C:\\", "E:\\")
+            .setAttributeViews("basic", "owner", "dos", "acl", "user")
+            .build());
+  }
+
+  @Test
+  public void testFileSystem() {
+    assertThat(fs.getSeparator()).isEqualTo("\\");
+    assertThat(fs.getRootDirectories())
+        .containsExactlyElementsIn(ImmutableSet.of(path("C:\\"), path("E:\\")))
+        .inOrder();
+    assertThat(fs.isOpen()).isTrue();
+    assertThat(fs.isReadOnly()).isFalse();
+    assertThat(fs.supportedFileAttributeViews())
+        .containsExactly("basic", "owner", "dos", "acl", "user");
+    assertThat(fs.provider()).isInstanceOf(JimfsFileSystemProvider.class);
+  }
+
+  @Test
+  public void testPaths() {
+    assertThatPath("C:\\").isAbsolute().and().hasRootComponent("C:\\").and().hasNoNameComponents();
+    assertThatPath("foo").isRelative().and().hasNameComponents("foo");
+    assertThatPath("foo\\bar").isRelative().and().hasNameComponents("foo", "bar");
+    assertThatPath("C:\\foo\\bar\\baz")
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("foo", "bar", "baz");
+  }
+
+  @Test
+  public void testPaths_equalityIsCaseInsensitive() {
+    assertThatPath("C:\\").isEqualTo(path("c:\\"));
+    assertThatPath("foo").isEqualTo(path("FOO"));
+  }
+
+  @Test
+  public void testPaths_areSortedCaseInsensitive() {
+    Path p1 = path("a");
+    Path p2 = path("B");
+    Path p3 = path("c");
+    Path p4 = path("D");
+
+    assertThat(Ordering.natural().immutableSortedCopy(Arrays.asList(p3, p4, p1, p2)))
+        .isEqualTo(ImmutableList.of(p1, p2, p3, p4));
+
+    // would be p2, p4, p1, p3 if sorting were case sensitive
+  }
+
+  @Test
+  public void testPaths_withSlash() {
+    assertThatPath("foo/bar")
+        .isRelative()
+        .and()
+        .hasNameComponents("foo", "bar")
+        .and()
+        .isEqualTo(path("foo\\bar"));
+    assertThatPath("C:/foo/bar/baz")
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("foo", "bar", "baz")
+        .and()
+        .isEqualTo(path("C:\\foo\\bar\\baz"));
+    assertThatPath("C:/foo\\bar/baz")
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("foo", "bar", "baz")
+        .and()
+        .isEqualTo(path("C:\\foo\\bar\\baz"));
+  }
+
+  @Test
+  public void testPaths_resolve() {
+    assertThatPath(path("C:\\").resolve("foo\\bar"))
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("foo", "bar");
+    assertThatPath(path("foo\\bar").resolveSibling("baz"))
+        .isRelative()
+        .and()
+        .hasNameComponents("foo", "baz");
+    assertThatPath(path("foo\\bar").resolve("C:\\one\\two"))
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("one", "two");
+  }
+
+  @Test
+  public void testPaths_normalize() {
+    assertThatPath(path("foo\\bar\\..").normalize()).isRelative().and().hasNameComponents("foo");
+    assertThatPath(path("foo\\.\\bar\\..\\baz\\test\\.\\..\\stuff").normalize())
+        .isRelative()
+        .and()
+        .hasNameComponents("foo", "baz", "stuff");
+    assertThatPath(path("..\\..\\foo\\.\\bar").normalize())
+        .isRelative()
+        .and()
+        .hasNameComponents("..", "..", "foo", "bar");
+    assertThatPath(path("foo\\..\\..\\bar").normalize())
+        .isRelative()
+        .and()
+        .hasNameComponents("..", "bar");
+    assertThatPath(path("..\\.\\..").normalize()).isRelative().and().hasNameComponents("..", "..");
+  }
+
+  @Test
+  public void testPaths_relativize() {
+    assertThatPath(path("C:\\foo\\bar").relativize(path("C:\\foo\\bar\\baz")))
+        .isRelative()
+        .and()
+        .hasNameComponents("baz");
+    assertThatPath(path("C:\\foo\\bar\\baz").relativize(path("C:\\foo\\bar")))
+        .isRelative()
+        .and()
+        .hasNameComponents("..");
+    assertThatPath(path("C:\\foo\\bar\\baz").relativize(path("C:\\foo\\baz\\bar")))
+        .isRelative()
+        .and()
+        .hasNameComponents("..", "..", "baz", "bar");
+    assertThatPath(path("foo\\bar").relativize(path("foo")))
+        .isRelative()
+        .and()
+        .hasNameComponents("..");
+    assertThatPath(path("foo").relativize(path("foo\\bar")))
+        .isRelative()
+        .and()
+        .hasNameComponents("bar");
+
+    try {
+      Path unused = path("C:\\foo\\bar").relativize(path("bar"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+
+    try {
+      Path unused = path("bar").relativize(path("C:\\foo\\bar"));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testPaths_startsWith_endsWith() {
+    assertThat(path("C:\\foo\\bar").startsWith("C:\\")).isTrue();
+    assertThat(path("C:\\foo\\bar").startsWith("C:\\foo")).isTrue();
+    assertThat(path("C:\\foo\\bar").startsWith("C:\\foo\\bar")).isTrue();
+    assertThat(path("C:\\foo\\bar").endsWith("bar")).isTrue();
+    assertThat(path("C:\\foo\\bar").endsWith("foo\\bar")).isTrue();
+    assertThat(path("C:\\foo\\bar").endsWith("C:\\foo\\bar")).isTrue();
+    assertThat(path("C:\\foo\\bar").endsWith("C:\\foo")).isFalse();
+    assertThat(path("C:\\foo\\bar").startsWith("foo\\bar")).isFalse();
+  }
+
+  @Test
+  public void testPaths_toAbsolutePath() {
+    assertThatPath(path("C:\\foo\\bar").toAbsolutePath())
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("foo", "bar")
+        .and()
+        .isEqualTo(path("C:\\foo\\bar"));
+
+    assertThatPath(path("foo\\bar").toAbsolutePath())
+        .isAbsolute()
+        .and()
+        .hasRootComponent("C:\\")
+        .and()
+        .hasNameComponents("work", "foo", "bar")
+        .and()
+        .isEqualTo(path("C:\\work\\foo\\bar"));
+  }
+
+  @Test
+  public void testPaths_toRealPath() throws IOException {
+    Files.createDirectories(path("C:\\foo\\bar"));
+    Files.createSymbolicLink(path("C:\\link"), path("C:\\"));
+
+    assertThatPath(path("C:\\link\\foo\\bar").toRealPath()).isEqualTo(path("C:\\foo\\bar"));
+
+    assertThatPath(path("").toRealPath()).isEqualTo(path("C:\\work"));
+    assertThatPath(path(".").toRealPath()).isEqualTo(path("C:\\work"));
+    assertThatPath(path("..").toRealPath()).isEqualTo(path("C:\\"));
+    assertThatPath(path("..\\..").toRealPath()).isEqualTo(path("C:\\"));
+    assertThatPath(path(".\\..\\.\\..").toRealPath()).isEqualTo(path("C:\\"));
+    assertThatPath(path(".\\..\\.\\..\\.").toRealPath()).isEqualTo(path("C:\\"));
+  }
+
+  @Test
+  public void testPaths_toUri() {
+    assertThat(fs.getPath("C:\\").toUri()).isEqualTo(URI.create("jimfs://win/C:/"));
+    assertThat(fs.getPath("C:\\foo").toUri()).isEqualTo(URI.create("jimfs://win/C:/foo"));
+    assertThat(fs.getPath("C:\\foo\\bar").toUri()).isEqualTo(URI.create("jimfs://win/C:/foo/bar"));
+    assertThat(fs.getPath("foo").toUri()).isEqualTo(URI.create("jimfs://win/C:/work/foo"));
+    assertThat(fs.getPath("foo\\bar").toUri()).isEqualTo(URI.create("jimfs://win/C:/work/foo/bar"));
+    assertThat(fs.getPath("").toUri()).isEqualTo(URI.create("jimfs://win/C:/work/"));
+    assertThat(fs.getPath(".\\..\\.").toUri()).isEqualTo(URI.create("jimfs://win/C:/work/./.././"));
+  }
+
+  @Test
+  public void testPaths_toUri_unc() {
+    assertThat(fs.getPath("\\\\host\\share\\").toUri())
+        .isEqualTo(URI.create("jimfs://win//host/share/"));
+    assertThat(fs.getPath("\\\\host\\share\\foo").toUri())
+        .isEqualTo(URI.create("jimfs://win//host/share/foo"));
+    assertThat(fs.getPath("\\\\host\\share\\foo\\bar").toUri())
+        .isEqualTo(URI.create("jimfs://win//host/share/foo/bar"));
+  }
+
+  @Test
+  public void testPaths_getFromUri() {
+    assertThatPath(Paths.get(URI.create("jimfs://win/C:/"))).isEqualTo(fs.getPath("C:\\"));
+    assertThatPath(Paths.get(URI.create("jimfs://win/C:/foo"))).isEqualTo(fs.getPath("C:\\foo"));
+    assertThatPath(Paths.get(URI.create("jimfs://win/C:/foo%20bar")))
+        .isEqualTo(fs.getPath("C:\\foo bar"));
+    assertThatPath(Paths.get(URI.create("jimfs://win/C:/foo/./bar")))
+        .isEqualTo(fs.getPath("C:\\foo\\.\\bar"));
+    assertThatPath(Paths.get(URI.create("jimfs://win/C:/foo/bar/")))
+        .isEqualTo(fs.getPath("C:\\foo\\bar"));
+  }
+
+  @Test
+  public void testPaths_getFromUri_unc() {
+    assertThatPath(Paths.get(URI.create("jimfs://win//host/share/")))
+        .isEqualTo(fs.getPath("\\\\host\\share\\"));
+    assertThatPath(Paths.get(URI.create("jimfs://win//host/share/foo")))
+        .isEqualTo(fs.getPath("\\\\host\\share\\foo"));
+    assertThatPath(Paths.get(URI.create("jimfs://win//host/share/foo%20bar")))
+        .isEqualTo(fs.getPath("\\\\host\\share\\foo bar"));
+    assertThatPath(Paths.get(URI.create("jimfs://win//host/share/foo/./bar")))
+        .isEqualTo(fs.getPath("\\\\host\\share\\foo\\.\\bar"));
+    assertThatPath(Paths.get(URI.create("jimfs://win//host/share/foo/bar/")))
+        .isEqualTo(fs.getPath("\\\\host\\share\\foo\\bar"));
+  }
+
+  @Test
+  public void testPathMatchers_glob() {
+    assertThatPath("bar").matches("glob:bar");
+    assertThatPath("bar").matches("glob:*");
+    assertThatPath("C:\\foo").doesNotMatch("glob:*");
+    assertThatPath("C:\\foo\\bar").doesNotMatch("glob:*");
+    assertThatPath("C:\\foo\\bar").matches("glob:**");
+    assertThatPath("C:\\foo\\bar").matches("glob:C:\\\\**");
+    assertThatPath("foo\\bar").doesNotMatch("glob:C:\\\\**");
+    assertThatPath("C:\\foo\\bar\\baz\\stuff").matches("glob:C:\\\\foo\\\\**");
+    assertThatPath("C:\\foo\\bar\\baz\\stuff").matches("glob:C:\\\\**\\\\stuff");
+    assertThatPath("C:\\foo").matches("glob:C:\\\\[a-z]*");
+    assertThatPath("C:\\Foo").matches("glob:C:\\\\[a-z]*");
+    assertThatPath("C:\\foo").matches("glob:C:\\\\[A-Z]*");
+    assertThatPath("C:\\Foo").matches("glob:C:\\\\[A-Z]*");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.java").matches("glob:**\\\\*.java");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.java").matches("glob:**\\\\*.{java,class}");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.class").matches("glob:**\\\\*.{java,class}");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.java").matches("glob:**\\\\*.*");
+
+    try {
+      fs.getPathMatcher("glob:**\\*.{java,class");
+      fail();
+    } catch (PatternSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testPathMatchers_glob_alternateSeparators() {
+    // only need to test / in the glob pattern; tests above check that / in a path is changed to
+    // \ automatically
+    assertThatPath("C:\\foo").doesNotMatch("glob:*");
+    assertThatPath("C:\\foo\\bar").doesNotMatch("glob:*");
+    assertThatPath("C:\\foo\\bar").matches("glob:**");
+    assertThatPath("C:\\foo\\bar").matches("glob:C:/**");
+    assertThatPath("foo\\bar").doesNotMatch("glob:C:/**");
+    assertThatPath("C:\\foo\\bar\\baz\\stuff").matches("glob:C:/foo/**");
+    assertThatPath("C:\\foo\\bar\\baz\\stuff").matches("glob:C:/**/stuff");
+    assertThatPath("C:\\foo").matches("glob:C:/[a-z]*");
+    assertThatPath("C:\\Foo").matches("glob:C:/[a-z]*");
+    assertThatPath("C:\\foo").matches("glob:C:/[A-Z]*");
+    assertThatPath("C:\\Foo").matches("glob:C:/[A-Z]*");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.java").matches("glob:**/*.java");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.java").matches("glob:**/*.{java,class}");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.class").matches("glob:**/*.{java,class}");
+    assertThatPath("C:\\foo\\bar\\baz\\Stuff.java").matches("glob:**/*.*");
+
+    try {
+      fs.getPathMatcher("glob:**/*.{java,class");
+      fail();
+    } catch (PatternSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testCreateFileOrDirectory_forNonExistentRootPath_fails() throws IOException {
+    try {
+      Files.createDirectory(path("Z:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.createFile(path("Z:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.createSymbolicLink(path("Z:\\"), path("foo"));
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testCopyFile_toNonExistentRootPath_fails() throws IOException {
+    Files.createFile(path("foo"));
+    Files.createDirectory(path("bar"));
+
+    try {
+      Files.copy(path("foo"), path("Z:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.copy(path("bar"), path("Z:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testMoveFile_toNonExistentRootPath_fails() throws IOException {
+    Files.createFile(path("foo"));
+    Files.createDirectory(path("bar"));
+
+    try {
+      Files.move(path("foo"), path("Z:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.move(path("bar"), path("Z:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testDelete_directory_cantDeleteRoot() throws IOException {
+    // test with E:\ because it is empty
+    try {
+      Files.delete(path("E:\\"));
+      fail();
+    } catch (FileSystemException expected) {
+      assertThat(expected.getFile()).isEqualTo("E:\\");
+      assertThat(expected.getMessage()).contains("root");
+    }
+  }
+
+  @Test
+  public void testCreateFileOrDirectory_forExistingRootPath_fails() throws IOException {
+    try {
+      Files.createDirectory(path("E:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.createFile(path("E:\\"));
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.createSymbolicLink(path("E:\\"), path("foo"));
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testCopyFile_toExistingRootPath_fails() throws IOException {
+    Files.createFile(path("foo"));
+    Files.createDirectory(path("bar"));
+
+    try {
+      Files.copy(path("foo"), path("E:\\"), REPLACE_EXISTING);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.copy(path("bar"), path("E:\\"), REPLACE_EXISTING);
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testMoveFile_toExistingRootPath_fails() throws IOException {
+    Files.createFile(path("foo"));
+    Files.createDirectory(path("bar"));
+
+    try {
+      Files.move(path("foo"), path("E:\\"), REPLACE_EXISTING);
+      fail();
+    } catch (IOException expected) {
+    }
+
+    try {
+      Files.move(path("bar"), path("E:\\"), REPLACE_EXISTING);
+      fail();
+    } catch (IOException expected) {
+    }
+  }
+
+  @Test
+  public void testMove_rootDirectory_fails() throws IOException {
+    try {
+      Files.move(path("E:\\"), path("Z:\\"));
+      fail();
+    } catch (FileSystemException expected) {
+    }
+
+    try {
+      Files.move(path("E:\\"), path("C:\\bar"));
+      fail();
+    } catch (FileSystemException expected) {
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/NameTest.java b/jimfs/src/test/java/com/google/common/jimfs/NameTest.java
new file mode 100644
index 0000000..1953076
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/NameTest.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.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link Name}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class NameTest {
+
+  @Test
+  public void testNames() {
+    assertThat(Name.create("foo", "foo")).isEqualTo(Name.create("foo", "foo"));
+    assertThat(Name.create("FOO", "foo")).isEqualTo(Name.create("foo", "foo"));
+    assertThat(Name.create("FOO", "foo")).isNotEqualTo(Name.create("FOO", "FOO"));
+
+    assertThat(Name.create("a", "b").toString()).isEqualTo("a");
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/OwnerAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/OwnerAttributeProviderTest.java
new file mode 100644
index 0000000..fc6192b
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/OwnerAttributeProviderTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.jimfs.UserLookupService.createUserPrincipal;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link OwnerAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class OwnerAttributeProviderTest
+    extends AbstractAttributeProviderTest<OwnerAttributeProvider> {
+
+  @Override
+  protected OwnerAttributeProvider createProvider() {
+    return new OwnerAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of();
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    assertThat(provider.get(file, "owner")).isEqualTo(createUserPrincipal("user"));
+  }
+
+  @Test
+  public void testSet() {
+    assertSetAndGetSucceeds("owner", createUserPrincipal("user"));
+    assertSetFailsOnCreate("owner", createUserPrincipal("user"));
+
+    // invalid type
+    assertSetFails("owner", "root");
+  }
+
+  @Test
+  public void testView() throws IOException {
+    FileOwnerAttributeView view = provider.view(fileLookup(), NO_INHERITED_VIEWS);
+    assertThat(view).isNotNull();
+
+    assertThat(view.name()).isEqualTo("owner");
+    assertThat(view.getOwner()).isEqualTo(createUserPrincipal("user"));
+
+    view.setOwner(createUserPrincipal("root"));
+    assertThat(view.getOwner()).isEqualTo(createUserPrincipal("root"));
+    assertThat(file.getAttribute("owner", "owner")).isEqualTo(createUserPrincipal("root"));
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PathNormalizationTest.java b/jimfs/src/test/java/com/google/common/jimfs/PathNormalizationTest.java
new file mode 100644
index 0000000..23b28b4
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PathNormalizationTest.java
@@ -0,0 +1,351 @@
+/*
+ * 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.jimfs.PathNormalization.CASE_FOLD_ASCII;
+import static com.google.common.jimfs.PathNormalization.CASE_FOLD_UNICODE;
+import static com.google.common.jimfs.PathNormalization.NFC;
+import static com.google.common.jimfs.PathNormalization.NFD;
+import static com.google.common.jimfs.TestUtils.assertNotEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.regex.Pattern;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PathNormalization}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class PathNormalizationTest {
+
+  private ImmutableSet<PathNormalization> normalizations;
+
+  @Test
+  public void testNone() {
+    normalizations = ImmutableSet.of();
+
+    assertNormalizedEqual("foo", "foo");
+    assertNormalizedUnequal("Foo", "foo");
+    assertNormalizedUnequal("\u00c5", "\u212b");
+    assertNormalizedUnequal("Am\u00e9lie", "Ame\u0301lie");
+  }
+
+  private static final String[][] CASE_FOLD_TEST_DATA = {
+    {"foo", "fOo", "foO", "Foo", "FOO"},
+    {"efficient", "efficient", "efficient", "Efficient", "EFFICIENT"},
+    {"flour", "flour", "flour", "Flour", "FLOUR"},
+    {"poſt", "post", "poſt", "Poſt", "POST"},
+    {"poſt", "post", "poſt", "Poſt", "POST"},
+    {"ſtop", "stop", "ſtop", "Stop", "STOP"},
+    {"tschüß", "tschüss", "tschüß", "Tschüß", "TSCHÜSS"},
+    {"weiß", "weiss", "weiß", "Weiß", "WEISS"},
+    {"WEIẞ", "weiss", "weiß", "Weiß", "WEIẞ"},
+    {"στιγμας", "στιγμασ", "στιγμας", "Στιγμας", "ΣΤΙΓΜΑΣ"},
+    {"á¾² στο διάολο", "á½°ι στο διάολο", "á¾² στο διάολο", "Ὰͅ Στο Διάολο", "á¾ºΙ ΣΤΟ ΔΙΆΟΛΟ"},
+    {"Henry â…§", "henry â…·", "henry â…·", "Henry â…§", "HENRY â…§"},
+    {"I Work At â“€", "i work at â“š", "i work at â“š", "I Work At â“€", "I WORK AT â“€"},
+    {"ʀᴀʀᴇ", "ʀᴀʀᴇ", "ʀᴀʀᴇ", "Ʀᴀʀᴇ", "ƦᴀƦᴇ"},
+    {"Ὰͅ", "á½°ι", "á¾²", "Ὰͅ", "ᾺΙ"}
+  };
+
+  @Test
+  public void testCaseFold() {
+    normalizations = ImmutableSet.of(CASE_FOLD_UNICODE);
+
+    for (String[] row : CASE_FOLD_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i; j < row.length; j++) {
+          assertNormalizedEqual(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testCaseInsensitiveAscii() {
+    normalizations = ImmutableSet.of(CASE_FOLD_ASCII);
+
+    String[] row = {"foo", "FOO", "fOo", "Foo"};
+    for (int i = 0; i < row.length; i++) {
+      for (int j = i; j < row.length; j++) {
+        assertNormalizedEqual(row[i], row[j]);
+      }
+    }
+
+    assertNormalizedUnequal("weiß", "weiss");
+  }
+
+  private static final String[][] NORMALIZE_TEST_DATA = {
+    {"\u00c5", "\u212b"}, // two forms of Å (one code point each)
+    {"Am\u00e9lie", "Ame\u0301lie"} // two forms of Amélie (one composed, one decomposed)
+  };
+
+  @Test
+  public void testNormalizeNfc() {
+    normalizations = ImmutableSet.of(NFC);
+
+    for (String[] row : NORMALIZE_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i; j < row.length; j++) {
+          assertNormalizedEqual(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testNormalizeNfd() {
+    normalizations = ImmutableSet.of(NFD);
+
+    for (String[] row : NORMALIZE_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i; j < row.length; j++) {
+          assertNormalizedEqual(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  private static final String[][] NORMALIZE_CASE_FOLD_TEST_DATA = {
+    {"\u00c5", "\u00e5", "\u212b"},
+    {"Am\u00e9lie", "Am\u00c9lie", "Ame\u0301lie", "AME\u0301LIE"}
+  };
+
+  @Test
+  public void testNormalizeNfcCaseFold() {
+    normalizations = ImmutableSet.of(NFC, CASE_FOLD_UNICODE);
+
+    for (String[] row : NORMALIZE_CASE_FOLD_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i; j < row.length; j++) {
+          assertNormalizedEqual(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testNormalizeNfdCaseFold() {
+    normalizations = ImmutableSet.of(NFD, CASE_FOLD_UNICODE);
+
+    for (String[] row : NORMALIZE_CASE_FOLD_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i; j < row.length; j++) {
+          assertNormalizedEqual(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  private static final String[][] NORMALIZED_CASE_INSENSITIVE_ASCII_TEST_DATA = {
+    {"\u00e5", "\u212b"},
+    {"Am\u00e9lie", "AME\u0301LIE"}
+  };
+
+  @Test
+  public void testNormalizeNfcCaseFoldAscii() {
+    normalizations = ImmutableSet.of(NFC, CASE_FOLD_ASCII);
+
+    for (String[] row : NORMALIZED_CASE_INSENSITIVE_ASCII_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i + 1; j < row.length; j++) {
+          assertNormalizedUnequal(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testNormalizeNfdCaseFoldAscii() {
+    normalizations = ImmutableSet.of(NFD, CASE_FOLD_ASCII);
+
+    for (String[] row : NORMALIZED_CASE_INSENSITIVE_ASCII_TEST_DATA) {
+      for (int i = 0; i < row.length; i++) {
+        for (int j = i + 1; j < row.length; j++) {
+          // since decomposition happens before case folding, the strings are equal when the
+          // decomposed ASCII letter is folded
+          assertNormalizedEqual(row[i], row[j]);
+        }
+      }
+    }
+  }
+
+  // regex patterns offer loosely similar matching, but that's all
+
+  @Test
+  public void testNone_pattern() {
+    normalizations = ImmutableSet.of();
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternDoesNotMatch("foo", "FOO");
+    assertNormalizedPatternDoesNotMatch("FOO", "foo");
+  }
+
+  @Test
+  public void testCaseFold_pattern() {
+    normalizations = ImmutableSet.of(CASE_FOLD_UNICODE);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternMatches("foo", "FOO");
+    assertNormalizedPatternMatches("FOO", "foo");
+    assertNormalizedPatternMatches("Am\u00e9lie", "AM\u00c9LIE");
+    assertNormalizedPatternMatches("Ame\u0301lie", "AME\u0301LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternDoesNotMatch("AM\u00c9LIE", "AME\u0301LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testCaseFoldAscii_pattern() {
+    normalizations = ImmutableSet.of(CASE_FOLD_ASCII);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternMatches("foo", "FOO");
+    assertNormalizedPatternMatches("FOO", "foo");
+    assertNormalizedPatternMatches("Ame\u0301lie", "AME\u0301LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AM\u00c9LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternDoesNotMatch("AM\u00c9LIE", "AME\u0301LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testNormalizeNfc_pattern() {
+    normalizations = ImmutableSet.of(NFC);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternDoesNotMatch("foo", "FOO");
+    assertNormalizedPatternDoesNotMatch("FOO", "foo");
+    assertNormalizedPatternMatches("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testNormalizeNfd_pattern() {
+    normalizations = ImmutableSet.of(NFD);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternDoesNotMatch("foo", "FOO");
+    assertNormalizedPatternDoesNotMatch("FOO", "foo");
+    assertNormalizedPatternMatches("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testNormalizeNfcCaseFold_pattern() {
+    normalizations = ImmutableSet.of(NFC, CASE_FOLD_UNICODE);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternMatches("foo", "FOO");
+    assertNormalizedPatternMatches("FOO", "foo");
+    assertNormalizedPatternMatches("Am\u00e9lie", "AM\u00c9LIE");
+    assertNormalizedPatternMatches("Ame\u0301lie", "AME\u0301LIE");
+    assertNormalizedPatternMatches("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternMatches("AM\u00c9LIE", "AME\u0301LIE");
+    assertNormalizedPatternMatches("Am\u00e9lie", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testNormalizeNfdCaseFold_pattern() {
+    normalizations = ImmutableSet.of(NFD, CASE_FOLD_UNICODE);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternMatches("foo", "FOO");
+    assertNormalizedPatternMatches("FOO", "foo");
+    assertNormalizedPatternMatches("Am\u00e9lie", "AM\u00c9LIE");
+    assertNormalizedPatternMatches("Ame\u0301lie", "AME\u0301LIE");
+    assertNormalizedPatternMatches("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternMatches("AM\u00c9LIE", "AME\u0301LIE");
+    assertNormalizedPatternMatches("Am\u00e9lie", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testNormalizeNfcCaseFoldAscii_pattern() {
+    normalizations = ImmutableSet.of(NFC, CASE_FOLD_ASCII);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternMatches("foo", "FOO");
+    assertNormalizedPatternMatches("FOO", "foo");
+
+    // these are all a bit fuzzy as when CASE_INSENSITIVE is present but not UNICODE_CASE, ASCII
+    // only strings are expected
+    assertNormalizedPatternMatches("Ame\u0301lie", "AME\u0301LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AM\u00c9LIE");
+    assertNormalizedPatternMatches("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternMatches("AM\u00c9LIE", "AME\u0301LIE");
+  }
+
+  @Test
+  public void testNormalizeNfdCaseFoldAscii_pattern() {
+    normalizations = ImmutableSet.of(NFD, CASE_FOLD_ASCII);
+    assertNormalizedPatternMatches("foo", "foo");
+    assertNormalizedPatternMatches("foo", "FOO");
+    assertNormalizedPatternMatches("FOO", "foo");
+
+    // these are all a bit fuzzy as when CASE_INSENSITIVE is present but not UNICODE_CASE, ASCII
+    // only strings are expected
+    assertNormalizedPatternMatches("Ame\u0301lie", "AME\u0301LIE");
+    assertNormalizedPatternDoesNotMatch("Am\u00e9lie", "AM\u00c9LIE");
+    assertNormalizedPatternMatches("Am\u00e9lie", "Ame\u0301lie");
+    assertNormalizedPatternMatches("AM\u00c9LIE", "AME\u0301LIE");
+  }
+
+  /** Asserts that the given strings normalize to the same string using the current normalizer. */
+  private void assertNormalizedEqual(String first, String second) {
+    assertEquals(
+        PathNormalization.normalize(first, normalizations),
+        PathNormalization.normalize(second, normalizations));
+  }
+
+  /** Asserts that the given strings normalize to different strings using the current normalizer. */
+  private void assertNormalizedUnequal(String first, String second) {
+    assertNotEquals(
+        PathNormalization.normalize(first, normalizations),
+        PathNormalization.normalize(second, normalizations));
+  }
+
+  /**
+   * Asserts that the given strings match when one is compiled as a regex pattern using the current
+   * normalizer and matched against the other.
+   */
+  private void assertNormalizedPatternMatches(String first, String second) {
+    Pattern pattern = PathNormalization.compilePattern(first, normalizations);
+    assertTrue(
+        "pattern '" + pattern + "' does not match '" + second + "'",
+        pattern.matcher(second).matches());
+
+    pattern = PathNormalization.compilePattern(second, normalizations);
+    assertTrue(
+        "pattern '" + pattern + "' does not match '" + first + "'",
+        pattern.matcher(first).matches());
+  }
+
+  /**
+   * Asserts that the given strings do not match when one is compiled as a regex pattern using the
+   * current normalizer and matched against the other.
+   */
+  private void assertNormalizedPatternDoesNotMatch(String first, String second) {
+    Pattern pattern = PathNormalization.compilePattern(first, normalizations);
+    assertFalse(
+        "pattern '" + pattern + "' should not match '" + second + "'",
+        pattern.matcher(second).matches());
+
+    pattern = PathNormalization.compilePattern(second, normalizations);
+    assertFalse(
+        "pattern '" + pattern + "' should not match '" + first + "'",
+        pattern.matcher(first).matches());
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PathServiceTest.java b/jimfs/src/test/java/com/google/common/jimfs/PathServiceTest.java
new file mode 100644
index 0000000..65349c7
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PathServiceTest.java
@@ -0,0 +1,264 @@
+/*
+ * 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.jimfs.PathNormalization.CASE_FOLD_ASCII;
+import static com.google.common.jimfs.PathSubject.paths;
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.PathMatcher;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PathService}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class PathServiceTest {
+
+  private static final ImmutableSet<PathNormalization> NO_NORMALIZATIONS = ImmutableSet.of();
+
+  private final PathService service = fakeUnixPathService();
+
+  @Test
+  public void testBasicProperties() {
+    assertThat(service.getSeparator()).isEqualTo("/");
+    assertThat(fakeWindowsPathService().getSeparator()).isEqualTo("\\");
+  }
+
+  @Test
+  public void testPathCreation() {
+    assertAbout(paths())
+        .that(service.emptyPath())
+        .hasRootComponent(null)
+        .and()
+        .hasNameComponents("");
+
+    assertAbout(paths())
+        .that(service.createRoot(service.name("/")))
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNoNameComponents();
+
+    assertAbout(paths())
+        .that(service.createFileName(service.name("foo")))
+        .hasRootComponent(null)
+        .and()
+        .hasNameComponents("foo");
+
+    JimfsPath relative = service.createRelativePath(service.names(ImmutableList.of("foo", "bar")));
+    assertAbout(paths())
+        .that(relative)
+        .hasRootComponent(null)
+        .and()
+        .hasNameComponents("foo", "bar");
+
+    JimfsPath absolute =
+        service.createPath(service.name("/"), service.names(ImmutableList.of("foo", "bar")));
+    assertAbout(paths())
+        .that(absolute)
+        .isAbsolute()
+        .and()
+        .hasRootComponent("/")
+        .and()
+        .hasNameComponents("foo", "bar");
+  }
+
+  @Test
+  public void testPathCreation_emptyPath() {
+    // normalized to empty path with single empty string name
+    assertAbout(paths())
+        .that(service.createPath(null, ImmutableList.<Name>of()))
+        .hasRootComponent(null)
+        .and()
+        .hasNameComponents("");
+  }
+
+  @Test
+  public void testPathCreation_parseIgnoresEmptyString() {
+    // if the empty string wasn't ignored, the resulting path would be "/foo" since the empty
+    // string would be joined with foo
+    assertAbout(paths())
+        .that(service.parsePath("", "foo"))
+        .hasRootComponent(null)
+        .and()
+        .hasNameComponents("foo");
+  }
+
+  @Test
+  public void testToString() {
+    // not much to test for this since it just delegates to PathType anyway
+    JimfsPath path =
+        new JimfsPath(service, null, ImmutableList.of(Name.simple("foo"), Name.simple("bar")));
+    assertThat(service.toString(path)).isEqualTo("foo/bar");
+
+    path = new JimfsPath(service, Name.simple("/"), ImmutableList.of(Name.simple("foo")));
+    assertThat(service.toString(path)).isEqualTo("/foo");
+  }
+
+  @Test
+  public void testHash_usingDisplayForm() {
+    PathService pathService = fakePathService(PathType.unix(), false);
+
+    JimfsPath path1 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("FOO", "foo")));
+    JimfsPath path2 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("FOO", "FOO")));
+    JimfsPath path3 =
+        new JimfsPath(
+            pathService, null, ImmutableList.of(Name.create("FOO", "9874238974897189741")));
+
+    assertThat(pathService.hash(path1)).isEqualTo(pathService.hash(path2));
+    assertThat(pathService.hash(path2)).isEqualTo(pathService.hash(path3));
+  }
+
+  @Test
+  public void testHash_usingCanonicalForm() {
+    PathService pathService = fakePathService(PathType.unix(), true);
+
+    JimfsPath path1 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("foo", "foo")));
+    JimfsPath path2 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("FOO", "foo")));
+    JimfsPath path3 =
+        new JimfsPath(
+            pathService, null, ImmutableList.of(Name.create("28937497189478912374897", "foo")));
+
+    assertThat(pathService.hash(path1)).isEqualTo(pathService.hash(path2));
+    assertThat(pathService.hash(path2)).isEqualTo(pathService.hash(path3));
+  }
+
+  @Test
+  public void testCompareTo_usingDisplayForm() {
+    PathService pathService = fakePathService(PathType.unix(), false);
+
+    JimfsPath path1 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("a", "z")));
+    JimfsPath path2 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("b", "y")));
+    JimfsPath path3 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("c", "x")));
+
+    assertThat(pathService.compare(path1, path2)).isEqualTo(-1);
+    assertThat(pathService.compare(path2, path3)).isEqualTo(-1);
+  }
+
+  @Test
+  public void testCompareTo_usingCanonicalForm() {
+    PathService pathService = fakePathService(PathType.unix(), true);
+
+    JimfsPath path1 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("a", "z")));
+    JimfsPath path2 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("b", "y")));
+    JimfsPath path3 = new JimfsPath(pathService, null, ImmutableList.of(Name.create("c", "x")));
+
+    assertThat(pathService.compare(path1, path2)).isEqualTo(1);
+    assertThat(pathService.compare(path2, path3)).isEqualTo(1);
+  }
+
+  @Test
+  public void testPathMatcher() {
+    assertThat(service.createPathMatcher("regex:foo"))
+        .isInstanceOf(PathMatchers.RegexPathMatcher.class);
+    assertThat(service.createPathMatcher("glob:foo"))
+        .isInstanceOf(PathMatchers.RegexPathMatcher.class);
+  }
+
+  @Test
+  public void testPathMatcher_usingCanonicalForm_usesCanonicalNormalizations() {
+    // https://github.com/google/jimfs/issues/91
+    // This matches the behavior of Windows (the only built-in configuration that uses canonical
+    // form for equality). There, PathMatchers should do case-insensitive matching despite Windows
+    // not normalizing case for display.
+    assertCaseInsensitiveMatches(
+        new PathService(
+            PathType.unix(), NO_NORMALIZATIONS, ImmutableSet.of(CASE_FOLD_ASCII), true));
+    assertCaseSensitiveMatches(
+        new PathService(
+            PathType.unix(), ImmutableSet.of(CASE_FOLD_ASCII), NO_NORMALIZATIONS, true));
+  }
+
+  @Test
+  public void testPathMatcher_usingDisplayForm_usesDisplayNormalizations() {
+    assertCaseInsensitiveMatches(
+        new PathService(
+            PathType.unix(), ImmutableSet.of(CASE_FOLD_ASCII), NO_NORMALIZATIONS, false));
+    assertCaseSensitiveMatches(
+        new PathService(
+            PathType.unix(), NO_NORMALIZATIONS, ImmutableSet.of(CASE_FOLD_ASCII), false));
+  }
+
+  private static void assertCaseInsensitiveMatches(PathService service) {
+    ImmutableList<PathMatcher> matchers =
+        ImmutableList.of(
+            service.createPathMatcher("glob:foo"), service.createPathMatcher("glob:FOO"));
+
+    JimfsPath lowerCasePath = singleNamePath(service, "foo");
+    JimfsPath upperCasePath = singleNamePath(service, "FOO");
+    JimfsPath nonMatchingPath = singleNamePath(service, "bar");
+
+    for (PathMatcher matcher : matchers) {
+      assertThat(matcher.matches(lowerCasePath)).isTrue();
+      assertThat(matcher.matches(upperCasePath)).isTrue();
+      assertThat(matcher.matches(nonMatchingPath)).isFalse();
+    }
+  }
+
+  private static void assertCaseSensitiveMatches(PathService service) {
+    PathMatcher matcher = service.createPathMatcher("glob:foo");
+
+    JimfsPath lowerCasePath = singleNamePath(service, "foo");
+    JimfsPath upperCasePath = singleNamePath(service, "FOO");
+
+    assertThat(matcher.matches(lowerCasePath)).isTrue();
+    assertThat(matcher.matches(upperCasePath)).isFalse();
+  }
+
+  public static PathService fakeUnixPathService() {
+    return fakePathService(PathType.unix(), false);
+  }
+
+  public static PathService fakeWindowsPathService() {
+    return fakePathService(PathType.windows(), false);
+  }
+
+  public static PathService fakePathService(PathType type, boolean equalityUsesCanonicalForm) {
+    PathService service =
+        new PathService(type, NO_NORMALIZATIONS, NO_NORMALIZATIONS, equalityUsesCanonicalForm);
+    service.setFileSystem(FILE_SYSTEM);
+    return service;
+  }
+
+  private static JimfsPath singleNamePath(PathService service, String name) {
+    return new JimfsPath(service, null, ImmutableList.of(Name.create(name, name)));
+  }
+
+  private static final FileSystem FILE_SYSTEM;
+
+  static {
+    try {
+      FILE_SYSTEM =
+          JimfsFileSystems.newFileSystem(
+              new JimfsFileSystemProvider(), URI.create("jimfs://foo"), Configuration.unix());
+    } catch (IOException e) {
+      throw new AssertionError(e);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PathSubject.java b/jimfs/src/test/java/com/google/common/jimfs/PathSubject.java
new file mode 100644
index 0000000..f6927a5
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PathSubject.java
@@ -0,0 +1,406 @@
+/*
+ * 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.truth.Fact.fact;
+import static com.google.common.truth.Fact.simpleFact;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.asList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/**
+ * Subject for doing assertions on file system paths.
+ *
+ * @author Colin Decker
+ */
+public final class PathSubject extends Subject {
+
+  /** Returns the subject factory for doing assertions on paths. */
+  public static Subject.Factory<PathSubject, Path> paths() {
+    return new PathSubjectFactory();
+  }
+
+  private static final LinkOption[] FOLLOW_LINKS = new LinkOption[0];
+  private static final LinkOption[] NOFOLLOW_LINKS = {LinkOption.NOFOLLOW_LINKS};
+
+  private final Path actual;
+  protected LinkOption[] linkOptions = FOLLOW_LINKS;
+  private Charset charset = UTF_8;
+
+  private PathSubject(FailureMetadata failureMetadata, Path subject) {
+    super(failureMetadata, subject);
+    this.actual = subject;
+  }
+
+  private Path toPath(String path) {
+    return actual.getFileSystem().getPath(path);
+  }
+
+  /** Returns this, for readability of chained assertions. */
+  public PathSubject and() {
+    return this;
+  }
+
+  /** Do not follow links when looking up the path. */
+  public PathSubject noFollowLinks() {
+    this.linkOptions = NOFOLLOW_LINKS;
+    return this;
+  }
+
+  /**
+   * Set the given charset to be used when reading the file at this path as text. Default charset if
+   * not set is UTF-8.
+   */
+  public PathSubject withCharset(Charset charset) {
+    this.charset = checkNotNull(charset);
+    return this;
+  }
+
+  /** Asserts that the path is absolute (it has a root component). */
+  public PathSubject isAbsolute() {
+    if (!actual.isAbsolute()) {
+      failWithActual(simpleFact("expected to be absolute"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path is relative (it has no root component). */
+  public PathSubject isRelative() {
+    if (actual.isAbsolute()) {
+      failWithActual(simpleFact("expected to be relative"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path has the given root component. */
+  public PathSubject hasRootComponent(@NullableDecl String root) {
+    Path rootComponent = actual.getRoot();
+    if (root == null && rootComponent != null) {
+      failWithActual("expected to have root component", root);
+    } else if (root != null && !root.equals(rootComponent.toString())) {
+      failWithActual("expected to have root component", root);
+    }
+    return this;
+  }
+
+  /** Asserts that the path has no name components. */
+  public PathSubject hasNoNameComponents() {
+    check("getNameCount()").that(actual.getNameCount()).isEqualTo(0);
+    return this;
+  }
+
+  /** Asserts that the path has the given name components. */
+  public PathSubject hasNameComponents(String... names) {
+    ImmutableList.Builder<String> builder = ImmutableList.builder();
+    for (Path name : actual) {
+      builder.add(name.toString());
+    }
+
+    if (!builder.build().equals(ImmutableList.copyOf(names))) {
+      failWithActual("expected components", asList(names));
+    }
+    return this;
+  }
+
+  /** Asserts that the path matches the given syntax and pattern. */
+  public PathSubject matches(String syntaxAndPattern) {
+    PathMatcher matcher = actual.getFileSystem().getPathMatcher(syntaxAndPattern);
+    if (!matcher.matches(actual)) {
+      failWithActual("expected to match ", syntaxAndPattern);
+    }
+    return this;
+  }
+
+  /** Asserts that the path does not match the given syntax and pattern. */
+  public PathSubject doesNotMatch(String syntaxAndPattern) {
+    PathMatcher matcher = actual.getFileSystem().getPathMatcher(syntaxAndPattern);
+    if (matcher.matches(actual)) {
+      failWithActual("expected not to match", syntaxAndPattern);
+    }
+    return this;
+  }
+
+  /** Asserts that the path exists. */
+  public PathSubject exists() {
+    if (!Files.exists(actual, linkOptions)) {
+      failWithActual(simpleFact("expected to exist"));
+    }
+    if (Files.notExists(actual, linkOptions)) {
+      failWithActual(simpleFact("expected to exist"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path does not exist. */
+  public PathSubject doesNotExist() {
+    if (!Files.notExists(actual, linkOptions)) {
+      failWithActual(simpleFact("expected not to exist"));
+    }
+    if (Files.exists(actual, linkOptions)) {
+      failWithActual(simpleFact("expected not to exist"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path is a directory. */
+  public PathSubject isDirectory() {
+    exists(); // check for directoryness should imply check for existence
+
+    if (!Files.isDirectory(actual, linkOptions)) {
+      failWithActual(simpleFact("expected to be directory"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path is a regular file. */
+  public PathSubject isRegularFile() {
+    exists(); // check for regular fileness should imply check for existence
+
+    if (!Files.isRegularFile(actual, linkOptions)) {
+      failWithActual(simpleFact("expected to be regular file"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path is a symbolic link. */
+  public PathSubject isSymbolicLink() {
+    exists(); // check for symbolic linkness should imply check for existence
+
+    if (!Files.isSymbolicLink(actual)) {
+      failWithActual(simpleFact("expected to be symbolic link"));
+    }
+    return this;
+  }
+
+  /** Asserts that the path, which is a symbolic link, has the given path as a target. */
+  public PathSubject withTarget(String targetPath) throws IOException {
+    Path actualTarget = Files.readSymbolicLink(actual);
+    if (!actualTarget.equals(toPath(targetPath))) {
+      failWithoutActual(
+          fact("expected link target", targetPath),
+          fact("but target was", actualTarget),
+          fact("for path", actual));
+    }
+    return this;
+  }
+
+  /**
+   * Asserts that the file the path points to exists and has the given number of links to it. Fails
+   * on a file system that does not support the "unix" view.
+   */
+  public PathSubject hasLinkCount(int count) throws IOException {
+    exists();
+
+    int linkCount = (int) Files.getAttribute(actual, "unix:nlink", linkOptions);
+    if (linkCount != count) {
+      failWithActual("expected to have link count", count);
+    }
+    return this;
+  }
+
+  /** Asserts that the path resolves to the same file as the given path. */
+  public PathSubject isSameFileAs(String path) throws IOException {
+    return isSameFileAs(toPath(path));
+  }
+
+  /** Asserts that the path resolves to the same file as the given path. */
+  public PathSubject isSameFileAs(Path path) throws IOException {
+    if (!Files.isSameFile(actual, path)) {
+      failWithActual("expected to be same file as", path);
+    }
+    return this;
+  }
+
+  /** Asserts that the path does not resolve to the same file as the given path. */
+  public PathSubject isNotSameFileAs(String path) throws IOException {
+    if (Files.isSameFile(actual, toPath(path))) {
+      failWithActual("expected not to be same file as", path);
+    }
+    return this;
+  }
+
+  /** Asserts that the directory has no children. */
+  public PathSubject hasNoChildren() throws IOException {
+    isDirectory();
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(actual)) {
+      if (stream.iterator().hasNext()) {
+        failWithActual(simpleFact("expected to have no children"));
+      }
+    }
+    return this;
+  }
+
+  /** Asserts that the directory has children with the given names, in the given order. */
+  public PathSubject hasChildren(String... children) throws IOException {
+    isDirectory();
+
+    List<Path> expectedNames = new ArrayList<>();
+    for (String child : children) {
+      expectedNames.add(actual.getFileSystem().getPath(child));
+    }
+
+    try (DirectoryStream<Path> stream = Files.newDirectoryStream(actual)) {
+      List<Path> actualNames = new ArrayList<>();
+      for (Path path : stream) {
+        actualNames.add(path.getFileName());
+      }
+
+      if (!actualNames.equals(expectedNames)) {
+        failWithoutActual(
+            fact("expected to have children", expectedNames),
+            fact("but had children", actualNames),
+            fact("for path", actual));
+      }
+    }
+    return this;
+  }
+
+  /** Asserts that the file has the given size. */
+  public PathSubject hasSize(long size) throws IOException {
+    if (Files.size(actual) != size) {
+      failWithActual("expected to have size", size);
+    }
+    return this;
+  }
+
+  /** Asserts that the file is a regular file containing no bytes. */
+  public PathSubject containsNoBytes() throws IOException {
+    return containsBytes(new byte[0]);
+  }
+
+  /**
+   * Asserts that the file is a regular file containing exactly the byte values of the given ints.
+   */
+  public PathSubject containsBytes(int... bytes) throws IOException {
+    byte[] realBytes = new byte[bytes.length];
+    for (int i = 0; i < bytes.length; i++) {
+      realBytes[i] = (byte) bytes[i];
+    }
+    return containsBytes(realBytes);
+  }
+
+  /** Asserts that the file is a regular file containing exactly the given bytes. */
+  public PathSubject containsBytes(byte[] bytes) throws IOException {
+    isRegularFile();
+    hasSize(bytes.length);
+
+    byte[] actual = Files.readAllBytes(this.actual);
+    if (!Arrays.equals(bytes, actual)) {
+      System.out.println(BaseEncoding.base16().encode(actual));
+      System.out.println(BaseEncoding.base16().encode(bytes));
+      failWithActual("expected to contain bytes", BaseEncoding.base16().encode(bytes));
+    }
+    return this;
+  }
+
+  /**
+   * Asserts that the file is a regular file containing the same bytes as the regular file at the
+   * given path.
+   */
+  public PathSubject containsSameBytesAs(String path) throws IOException {
+    isRegularFile();
+
+    byte[] expectedBytes = Files.readAllBytes(toPath(path));
+    if (!Arrays.equals(expectedBytes, Files.readAllBytes(actual))) {
+      failWithActual("expected to contain same bytes as", path);
+    }
+    return this;
+  }
+
+  /**
+   * Asserts that the file is a regular file containing the given lines of text. By default, the
+   * bytes are decoded as UTF-8; for a different charset, use {@link #withCharset(Charset)}.
+   */
+  public PathSubject containsLines(String... lines) throws IOException {
+    return containsLines(Arrays.asList(lines));
+  }
+
+  /**
+   * Asserts that the file is a regular file containing the given lines of text. By default, the
+   * bytes are decoded as UTF-8; for a different charset, use {@link #withCharset(Charset)}.
+   */
+  public PathSubject containsLines(Iterable<String> lines) throws IOException {
+    isRegularFile();
+
+    List<String> expected = ImmutableList.copyOf(lines);
+    List<String> actual = Files.readAllLines(this.actual, charset);
+    check("lines()").that(actual).isEqualTo(expected);
+    return this;
+  }
+
+  /** Returns an object for making assertions about the given attribute. */
+  public Attribute attribute(final String attribute) {
+    return new Attribute() {
+      @Override
+      public Attribute is(Object value) throws IOException {
+        Object actualValue = Files.getAttribute(actual, attribute, linkOptions);
+        check("attribute(%s)", attribute).that(actualValue).isEqualTo(value);
+        return this;
+      }
+
+      @Override
+      public Attribute isNot(Object value) throws IOException {
+        Object actualValue = Files.getAttribute(actual, attribute, linkOptions);
+        check("attribute(%s)", attribute).that(actualValue).isNotEqualTo(value);
+        return this;
+      }
+
+      @Override
+      public PathSubject and() {
+        return PathSubject.this;
+      }
+    };
+  }
+
+  private static class PathSubjectFactory implements Subject.Factory<PathSubject, Path> {
+
+    @Override
+    public PathSubject createSubject(FailureMetadata failureMetadata, Path that) {
+      return new PathSubject(failureMetadata, that);
+    }
+  }
+
+  /** Interface for assertions about a file attribute. */
+  public interface Attribute {
+
+    /** Asserts that the value of this attribute is equal to the given value. */
+    Attribute is(Object value) throws IOException;
+
+    /** Asserts that the value of this attribute is not equal to the given value. */
+    Attribute isNot(Object value) throws IOException;
+
+    /** Returns the path subject for further chaining. */
+    PathSubject and();
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PathTester.java b/jimfs/src/test/java/com/google/common/jimfs/PathTester.java
new file mode 100644
index 0000000..b96d77e
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PathTester.java
@@ -0,0 +1,187 @@
+/*
+ * 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.Functions.toStringFunction;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+/** @author Colin Decker */
+public final class PathTester {
+
+  private final PathService pathService;
+  private final String string;
+  private String root;
+  private ImmutableList<String> names = ImmutableList.of();
+
+  public PathTester(PathService pathService, String string) {
+    this.pathService = pathService;
+    this.string = string;
+  }
+
+  public PathTester root(String root) {
+    this.root = root;
+    return this;
+  }
+
+  public PathTester names(Iterable<String> names) {
+    this.names = ImmutableList.copyOf(names);
+    return this;
+  }
+
+  public PathTester names(String... names) {
+    return names(Arrays.asList(names));
+  }
+
+  public void test(String first, String... more) {
+    Path path = pathService.parsePath(first, more);
+    test(path);
+  }
+
+  public void test(Path path) {
+    assertEquals(string, path.toString());
+
+    testRoot(path);
+    testNames(path);
+    testParents(path);
+    testStartsWith(path);
+    testEndsWith(path);
+    testSubpaths(path);
+  }
+
+  private void testRoot(Path path) {
+    if (root != null) {
+      assertTrue(path + ".isAbsolute() should be true", path.isAbsolute());
+      assertNotNull(path + ".getRoot() should not be null", path.getRoot());
+      assertEquals(root, path.getRoot().toString());
+    } else {
+      assertFalse(path + ".isAbsolute() should be false", path.isAbsolute());
+      assertNull(path + ".getRoot() should be null", path.getRoot());
+    }
+  }
+
+  private void testNames(Path path) {
+    assertEquals(names.size(), path.getNameCount());
+    assertEquals(names, names(path));
+    for (int i = 0; i < names.size(); i++) {
+      assertEquals(names.get(i), path.getName(i).toString());
+      // don't test individual names if this is an individual name
+      if (names.size() > 1) {
+        new PathTester(pathService, names.get(i)).names(names.get(i)).test(path.getName(i));
+      }
+    }
+    if (names.size() > 0) {
+      String fileName = names.get(names.size() - 1);
+      assertEquals(fileName, path.getFileName().toString());
+      // don't test individual names if this is an individual name
+      if (names.size() > 1) {
+        new PathTester(pathService, fileName).names(fileName).test(path.getFileName());
+      }
+    }
+  }
+
+  private void testParents(Path path) {
+    Path parent = path.getParent();
+
+    if (root != null && names.size() >= 1 || names.size() > 1) {
+      assertNotNull(parent);
+    }
+
+    if (parent != null) {
+      String parentName = names.size() == 1 ? root : string.substring(0, string.lastIndexOf('/'));
+      new PathTester(pathService, parentName)
+          .root(root)
+          .names(names.subList(0, names.size() - 1))
+          .test(parent);
+    }
+  }
+
+  private void testSubpaths(Path path) {
+    if (path.getRoot() == null) {
+      assertEquals(path, path.subpath(0, path.getNameCount()));
+    }
+
+    if (path.getNameCount() > 1) {
+      String stringWithoutRoot = root == null ? string : string.substring(root.length());
+
+      // test start + 1 to end and start to end - 1 subpaths... this recursively tests all subpaths
+      // actually tests most possible subpaths multiple times but... eh
+      Path startSubpath = path.subpath(1, path.getNameCount());
+      List<String> startNames =
+          ImmutableList.copyOf(Splitter.on('/').split(stringWithoutRoot))
+              .subList(1, path.getNameCount());
+
+      new PathTester(pathService, Joiner.on('/').join(startNames))
+          .names(startNames)
+          .test(startSubpath);
+
+      Path endSubpath = path.subpath(0, path.getNameCount() - 1);
+      List<String> endNames =
+          ImmutableList.copyOf(Splitter.on('/').split(stringWithoutRoot))
+              .subList(0, path.getNameCount() - 1);
+
+      new PathTester(pathService, Joiner.on('/').join(endNames)).names(endNames).test(endSubpath);
+    }
+  }
+
+  private void testStartsWith(Path path) {
+    // empty path doesn't start with any path
+    if (root != null || !names.isEmpty()) {
+      Path other = path;
+      while (other != null) {
+        assertTrue(path + ".startsWith(" + other + ") should be true", path.startsWith(other));
+        assertTrue(
+            path + ".startsWith(" + other + ") should be true", path.startsWith(other.toString()));
+        other = other.getParent();
+      }
+    }
+  }
+
+  private void testEndsWith(Path path) {
+    // empty path doesn't start with any path
+    if (root != null || !names.isEmpty()) {
+      Path other = path;
+      while (other != null) {
+        assertTrue(path + ".endsWith(" + other + ") should be true", path.endsWith(other));
+        assertTrue(
+            path + ".endsWith(" + other + ") should be true", path.endsWith(other.toString()));
+        if (other.getRoot() != null && other.getNameCount() > 0) {
+          other = other.subpath(0, other.getNameCount());
+        } else if (other.getNameCount() > 1) {
+          other = other.subpath(1, other.getNameCount());
+        } else {
+          other = null;
+        }
+      }
+    }
+  }
+
+  private static List<String> names(Path path) {
+    return FluentIterable.from(path).transform(toStringFunction()).toList();
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PathTypeTest.java b/jimfs/src/test/java/com/google/common/jimfs/PathTypeTest.java
new file mode 100644
index 0000000..59fc114
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PathTypeTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.PathType.ParseResult;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import java.net.URI;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PathType}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class PathTypeTest {
+
+  private static final FakePathType type = new FakePathType();
+  static final URI fileSystemUri = URI.create("jimfs://foo");
+
+  @Test
+  public void testBasicProperties() {
+    assertThat(type.getSeparator()).isEqualTo("/");
+    assertThat(type.getOtherSeparators()).isEqualTo("\\");
+  }
+
+  @Test
+  public void testParsePath() {
+    ParseResult path = type.parsePath("foo/bar/baz/one\\two");
+    assertParseResult(path, null, "foo", "bar", "baz", "one", "two");
+
+    ParseResult path2 = type.parsePath("$one//\\two");
+    assertParseResult(path2, "$", "one", "two");
+  }
+
+  @Test
+  public void testToString() {
+    ParseResult path = type.parsePath("foo/bar\\baz");
+    assertThat(type.toString(path.root(), path.names())).isEqualTo("foo/bar/baz");
+
+    ParseResult path2 = type.parsePath("$/foo/bar");
+    assertThat(type.toString(path2.root(), path2.names())).isEqualTo("$foo/bar");
+  }
+
+  @Test
+  public void testToUri() {
+    URI fileUri = type.toUri(fileSystemUri, "$", ImmutableList.of("foo", "bar"), false);
+    assertThat(fileUri.toString()).isEqualTo("jimfs://foo/$/foo/bar");
+    assertThat(fileUri.getPath()).isEqualTo("/$/foo/bar");
+
+    URI directoryUri = type.toUri(fileSystemUri, "$", ImmutableList.of("foo", "bar"), true);
+    assertThat(directoryUri.toString()).isEqualTo("jimfs://foo/$/foo/bar/");
+    assertThat(directoryUri.getPath()).isEqualTo("/$/foo/bar/");
+
+    URI rootUri = type.toUri(fileSystemUri, "$", ImmutableList.<String>of(), true);
+    assertThat(rootUri.toString()).isEqualTo("jimfs://foo/$/");
+    assertThat(rootUri.getPath()).isEqualTo("/$/");
+  }
+
+  @Test
+  public void testToUri_escaping() {
+    URI fileUri = type.toUri(fileSystemUri, "$", ImmutableList.of("foo", "bar baz"), false);
+    assertThat(fileUri.toString()).isEqualTo("jimfs://foo/$/foo/bar%20baz");
+    assertThat(fileUri.getRawPath()).isEqualTo("/$/foo/bar%20baz");
+    assertThat(fileUri.getPath()).isEqualTo("/$/foo/bar baz");
+  }
+
+  @Test
+  public void testUriRoundTrips() {
+    assertUriRoundTripsCorrectly(type, "$");
+    assertUriRoundTripsCorrectly(type, "$foo");
+    assertUriRoundTripsCorrectly(type, "$foo/bar/baz");
+    assertUriRoundTripsCorrectly(type, "$foo bar");
+    assertUriRoundTripsCorrectly(type, "$foo/bar baz");
+  }
+
+  static void assertParseResult(ParseResult result, @NullableDecl String root, String... names) {
+    assertThat(result.root()).isEqualTo(root);
+    assertThat(result.names()).containsExactly((Object[]) names).inOrder();
+  }
+
+  static void assertUriRoundTripsCorrectly(PathType type, String path) {
+    ParseResult result = type.parsePath(path);
+    URI uri = type.toUri(fileSystemUri, result.root(), result.names(), false);
+    ParseResult parsedUri = type.fromUri(uri);
+    assertThat(parsedUri.root()).isEqualTo(result.root());
+    assertThat(parsedUri.names()).containsExactlyElementsIn(result.names()).inOrder();
+  }
+
+  /** Arbitrary path type with $ as the root, / as the separator and \ as an alternate separator. */
+  private static final class FakePathType extends PathType {
+
+    protected FakePathType() {
+      super(false, '/', '\\');
+    }
+
+    @Override
+    public ParseResult parsePath(String path) {
+      String root = null;
+      if (path.startsWith("$")) {
+        root = "$";
+        path = path.substring(1);
+      }
+
+      return new ParseResult(root, splitter().split(path));
+    }
+
+    @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();
+      builder.append('/').append(root);
+      for (String name : names) {
+        builder.append('/').append(name);
+      }
+      if (directory) {
+        builder.append('/');
+      }
+      return builder.toString();
+    }
+
+    @Override
+    public ParseResult parseUriPath(String uriPath) {
+      checkArgument(uriPath.startsWith("/$"), "uriPath (%s) must start with /$", uriPath);
+      return parsePath(uriPath.substring(1));
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java b/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java
new file mode 100644
index 0000000..86f9832
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.truth.Truth.assertThat;
+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 static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.jimfs.AbstractWatchService.Event;
+import com.google.common.jimfs.AbstractWatchService.Key;
+import com.google.common.util.concurrent.Runnables;
+import com.google.common.util.concurrent.Uninterruptibles;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PollingWatchService}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class PollingWatchServiceTest {
+
+  private JimfsFileSystem fs;
+  private PollingWatchService watcher;
+
+  @Before
+  public void setUp() {
+    fs = (JimfsFileSystem) Jimfs.newFileSystem(Configuration.unix());
+    watcher =
+        new PollingWatchService(
+            fs.getDefaultView(),
+            fs.getPathService(),
+            new FileSystemState(Runnables.doNothing()),
+            4,
+            MILLISECONDS);
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    watcher.close();
+    fs.close();
+    watcher = null;
+    fs = null;
+  }
+
+  @Test
+  public void testNewWatcher() {
+    assertThat(watcher.isOpen()).isTrue();
+    assertThat(watcher.isPolling()).isFalse();
+  }
+
+  @Test
+  public void testRegister() throws IOException {
+    Key key = watcher.register(createDirectory(), ImmutableList.of(ENTRY_CREATE));
+    assertThat(key.isValid()).isTrue();
+
+    assertThat(watcher.isPolling()).isTrue();
+  }
+
+  @Test
+  public void testRegister_fileDoesNotExist() throws IOException {
+    try {
+      watcher.register(fs.getPath("/a/b/c"), ImmutableList.of(ENTRY_CREATE));
+      fail();
+    } catch (NoSuchFileException expected) {
+    }
+  }
+
+  @Test
+  public void testRegister_fileIsNotDirectory() throws IOException {
+    Path path = fs.getPath("/a.txt");
+    Files.createFile(path);
+    try {
+      watcher.register(path, ImmutableList.of(ENTRY_CREATE));
+      fail();
+    } catch (NotDirectoryException expected) {
+    }
+  }
+
+  @Test
+  public void testCancellingLastKeyStopsPolling() throws IOException {
+    Key key = watcher.register(createDirectory(), ImmutableList.of(ENTRY_CREATE));
+    key.cancel();
+    assertThat(key.isValid()).isFalse();
+
+    assertThat(watcher.isPolling()).isFalse();
+
+    Key key2 = watcher.register(createDirectory(), ImmutableList.of(ENTRY_CREATE));
+    Key key3 = watcher.register(createDirectory(), ImmutableList.of(ENTRY_DELETE));
+
+    assertThat(watcher.isPolling()).isTrue();
+
+    key2.cancel();
+
+    assertThat(watcher.isPolling()).isTrue();
+
+    key3.cancel();
+
+    assertThat(watcher.isPolling()).isFalse();
+  }
+
+  @Test
+  public void testCloseCancelsAllKeysAndStopsPolling() throws IOException {
+    Key key1 = watcher.register(createDirectory(), ImmutableList.of(ENTRY_CREATE));
+    Key key2 = watcher.register(createDirectory(), ImmutableList.of(ENTRY_DELETE));
+
+    assertThat(key1.isValid()).isTrue();
+    assertThat(key2.isValid()).isTrue();
+    assertThat(watcher.isPolling()).isTrue();
+
+    watcher.close();
+
+    assertThat(key1.isValid()).isFalse();
+    assertThat(key2.isValid()).isFalse();
+    assertThat(watcher.isPolling()).isFalse();
+  }
+
+  @Test(timeout = 2000)
+  public void testWatchForOneEventType() throws IOException, InterruptedException {
+    JimfsPath path = createDirectory();
+    watcher.register(path, ImmutableList.of(ENTRY_CREATE));
+
+    Files.createFile(path.resolve("foo"));
+
+    assertWatcherHasEvents(new Event<>(ENTRY_CREATE, 1, fs.getPath("foo")));
+
+    Files.createFile(path.resolve("bar"));
+    Files.createFile(path.resolve("baz"));
+
+    assertWatcherHasEvents(
+        new Event<>(ENTRY_CREATE, 1, fs.getPath("bar")),
+        new Event<>(ENTRY_CREATE, 1, fs.getPath("baz")));
+  }
+
+  @Test(timeout = 2000)
+  public void testWatchForMultipleEventTypes() throws IOException, InterruptedException {
+    JimfsPath path = createDirectory();
+    watcher.register(path, ImmutableList.of(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY));
+
+    Files.createDirectory(path.resolve("foo"));
+    Files.createFile(path.resolve("bar"));
+
+    assertWatcherHasEvents(
+        new Event<>(ENTRY_CREATE, 1, fs.getPath("bar")),
+        new Event<>(ENTRY_CREATE, 1, fs.getPath("foo")));
+
+    Files.createFile(path.resolve("baz"));
+    Files.delete(path.resolve("bar"));
+    Files.createFile(path.resolve("foo/bar"));
+
+    assertWatcherHasEvents(
+        new Event<>(ENTRY_CREATE, 1, fs.getPath("baz")),
+        new Event<>(ENTRY_DELETE, 1, fs.getPath("bar")),
+        new Event<>(ENTRY_MODIFY, 1, fs.getPath("foo")));
+
+    Files.delete(path.resolve("foo/bar"));
+    ensureTimeToPoll(); // watcher polls, seeing modification, then polls again, seeing delete
+    Files.delete(path.resolve("foo"));
+
+    assertWatcherHasEvents(
+        new Event<>(ENTRY_MODIFY, 1, fs.getPath("foo")),
+        new Event<>(ENTRY_DELETE, 1, fs.getPath("foo")));
+
+    Files.createDirectories(path.resolve("foo/bar"));
+
+    // polling here may either see just the creation of foo, or may first see the creation of foo
+    // and then the creation of foo/bar (modification of foo) since those don't happen atomically
+    assertWatcherHasEvents(
+        ImmutableList.<WatchEvent<?>>of(new Event<>(ENTRY_CREATE, 1, fs.getPath("foo"))),
+        // or
+        ImmutableList.<WatchEvent<?>>of(
+            new Event<>(ENTRY_CREATE, 1, fs.getPath("foo")),
+            new Event<>(ENTRY_MODIFY, 1, fs.getPath("foo"))));
+
+    Files.delete(path.resolve("foo/bar"));
+    Files.delete(path.resolve("foo"));
+
+    // polling here may either just see the deletion of foo, or may first see the deletion of bar
+    // (modification of foo) and then the deletion of foo
+    assertWatcherHasEvents(
+        ImmutableList.<WatchEvent<?>>of(new Event<>(ENTRY_DELETE, 1, fs.getPath("foo"))),
+        // or
+        ImmutableList.<WatchEvent<?>>of(
+            new Event<>(ENTRY_MODIFY, 1, fs.getPath("foo")),
+            new Event<>(ENTRY_DELETE, 1, fs.getPath("foo"))));
+  }
+
+  private void assertWatcherHasEvents(WatchEvent<?>... events) throws InterruptedException {
+    assertWatcherHasEvents(Arrays.asList(events), ImmutableList.<WatchEvent<?>>of());
+  }
+
+  private void assertWatcherHasEvents(List<WatchEvent<?>> expected, List<WatchEvent<?>> alternate)
+      throws InterruptedException {
+    ensureTimeToPoll(); // otherwise we could read 1 event but not all the events we're expecting
+    WatchKey key = watcher.take();
+    List<WatchEvent<?>> keyEvents = key.pollEvents();
+
+    if (keyEvents.size() == expected.size() || alternate.isEmpty()) {
+      assertThat(keyEvents).containsExactlyElementsIn(expected);
+    } else {
+      assertThat(keyEvents).containsExactlyElementsIn(alternate);
+    }
+    key.reset();
+  }
+
+  private static void ensureTimeToPoll() {
+    Uninterruptibles.sleepUninterruptibly(40, MILLISECONDS);
+  }
+
+  private JimfsPath createDirectory() throws IOException {
+    JimfsPath path = fs.getPath("/" + UUID.randomUUID().toString());
+    Files.createDirectory(path);
+    return path;
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/PosixAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/PosixAttributeProviderTest.java
new file mode 100644
index 0000000..baef1f9
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/PosixAttributeProviderTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.jimfs.UserLookupService.createGroupPrincipal;
+import static com.google.common.jimfs.UserLookupService.createUserPrincipal;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+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.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PosixAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class PosixAttributeProviderTest
+    extends AbstractAttributeProviderTest<PosixAttributeProvider> {
+
+  @Override
+  protected PosixAttributeProvider createProvider() {
+    return new PosixAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of(new BasicAttributeProvider(), new OwnerAttributeProvider());
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    assertContainsAll(
+        file,
+        ImmutableMap.of(
+            "group", createGroupPrincipal("group"),
+            "permissions", PosixFilePermissions.fromString("rw-r--r--")));
+  }
+
+  @Test
+  public void testSet() {
+    assertSetAndGetSucceeds("group", createGroupPrincipal("foo"));
+    assertSetAndGetSucceeds("permissions", PosixFilePermissions.fromString("rwxrwxrwx"));
+
+    // invalid types
+    assertSetFails("permissions", ImmutableList.of(PosixFilePermission.GROUP_EXECUTE));
+    assertSetFails("permissions", ImmutableSet.of("foo"));
+  }
+
+  @Test
+  public void testSetOnCreate() {
+    assertSetAndGetSucceedsOnCreate("permissions", PosixFilePermissions.fromString("rwxrwxrwx"));
+    assertSetFailsOnCreate("group", createGroupPrincipal("foo"));
+  }
+
+  @Test
+  public void testView() throws IOException {
+    file.setAttribute("owner", "owner", createUserPrincipal("user"));
+
+    PosixFileAttributeView view =
+        provider.view(
+            fileLookup(),
+            ImmutableMap.of(
+                "basic", new BasicAttributeProvider().view(fileLookup(), NO_INHERITED_VIEWS),
+                "owner", new OwnerAttributeProvider().view(fileLookup(), NO_INHERITED_VIEWS)));
+    assertNotNull(view);
+
+    assertThat(view.name()).isEqualTo("posix");
+    assertThat(view.getOwner()).isEqualTo(createUserPrincipal("user"));
+
+    PosixFileAttributes attrs = view.readAttributes();
+    assertThat(attrs.fileKey()).isEqualTo(0);
+    assertThat(attrs.owner()).isEqualTo(createUserPrincipal("user"));
+    assertThat(attrs.group()).isEqualTo(createGroupPrincipal("group"));
+    assertThat(attrs.permissions()).isEqualTo(PosixFilePermissions.fromString("rw-r--r--"));
+
+    view.setOwner(createUserPrincipal("root"));
+    assertThat(view.getOwner()).isEqualTo(createUserPrincipal("root"));
+    assertThat(file.getAttribute("owner", "owner")).isEqualTo(createUserPrincipal("root"));
+
+    view.setGroup(createGroupPrincipal("root"));
+    assertThat(view.readAttributes().group()).isEqualTo(createGroupPrincipal("root"));
+    assertThat(file.getAttribute("posix", "group")).isEqualTo(createGroupPrincipal("root"));
+
+    view.setPermissions(PosixFilePermissions.fromString("rwx------"));
+    assertThat(view.readAttributes().permissions())
+        .isEqualTo(PosixFilePermissions.fromString("rwx------"));
+    assertThat(file.getAttribute("posix", "permissions"))
+        .isEqualTo(PosixFilePermissions.fromString("rwx------"));
+  }
+
+  @Test
+  public void testAttributes() {
+    PosixFileAttributes attrs = provider.readAttributes(file);
+    assertThat(attrs.permissions()).isEqualTo(PosixFilePermissions.fromString("rw-r--r--"));
+    assertThat(attrs.group()).isEqualTo(createGroupPrincipal("group"));
+    assertThat(attrs.fileKey()).isEqualTo(0);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/RegexGlobMatcherTest.java b/jimfs/src/test/java/com/google/common/jimfs/RegexGlobMatcherTest.java
new file mode 100644
index 0000000..5836e78
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/RegexGlobMatcherTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.PathMatcher;
+import java.util.regex.Pattern;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PathMatcher} instances created by {@link GlobToRegex}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class RegexGlobMatcherTest extends AbstractGlobMatcherTest {
+
+  @Override
+  protected PathMatcher matcher(String pattern) {
+    return PathMatchers.getPathMatcher(
+        "glob:" + pattern, "/", ImmutableSet.<PathNormalization>of());
+  }
+
+  @Override
+  protected PathMatcher realMatcher(String pattern) {
+    FileSystem defaultFileSystem = FileSystems.getDefault();
+    if ("/".equals(defaultFileSystem.getSeparator())) {
+      return defaultFileSystem.getPathMatcher("glob:" + pattern);
+    }
+    return null;
+  }
+
+  @Test
+  public void testRegexTranslation() {
+    assertGlobRegexIs("foo", "foo");
+    assertGlobRegexIs("/", "/");
+    assertGlobRegexIs("?", "[^/]");
+    assertGlobRegexIs("*", "[^/]*");
+    assertGlobRegexIs("**", ".*");
+    assertGlobRegexIs("/foo", "/foo");
+    assertGlobRegexIs("?oo", "[^/]oo");
+    assertGlobRegexIs("*oo", "[^/]*oo");
+    assertGlobRegexIs("**/*.java", ".*/[^/]*\\.java");
+    assertGlobRegexIs("[a-z]", "[[^/]&&[a-z]]");
+    assertGlobRegexIs("[!a-z]", "[[^/]&&[^a-z]]");
+    assertGlobRegexIs("[-a-z]", "[[^/]&&[-a-z]]");
+    assertGlobRegexIs("[!-a-z]", "[[^/]&&[^-a-z]]");
+    assertGlobRegexIs("{a,b,c}", "(a|b|c)");
+    assertGlobRegexIs("{?oo,[A-Z]*,foo/**}", "([^/]oo|[[^/]&&[A-Z]][^/]*|foo/.*)");
+  }
+
+  @Test
+  public void testRegexEscaping() {
+    assertGlobRegexIs("(", "\\(");
+    assertGlobRegexIs(".", "\\.");
+    assertGlobRegexIs("^", "\\^");
+    assertGlobRegexIs("$", "\\$");
+    assertGlobRegexIs("+", "\\+");
+    assertGlobRegexIs("\\\\", "\\\\");
+    assertGlobRegexIs("]", "\\]");
+    assertGlobRegexIs(")", "\\)");
+    assertGlobRegexIs("}", "\\}");
+  }
+
+  @Test
+  public void testRegexTranslationWithMultipleSeparators() {
+    assertGlobRegexIs("?", "[^\\\\/]", "\\/");
+    assertGlobRegexIs("*", "[^\\\\/]*", "\\/");
+    assertGlobRegexIs("/", "[\\\\/]", "\\/");
+    assertGlobRegexIs("\\\\", "[\\\\/]", "\\/");
+  }
+
+  private static void assertGlobRegexIs(String glob, String regex) {
+    assertGlobRegexIs(glob, regex, "/");
+  }
+
+  private static void assertGlobRegexIs(String glob, String regex, String separators) {
+    assertEquals(regex, GlobToRegex.toRegex(glob, separators));
+    Pattern.compile(regex); // ensure the regex syntax is valid
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/RegularFileBlocksTest.java b/jimfs/src/test/java/com/google/common/jimfs/RegularFileBlocksTest.java
new file mode 100644
index 0000000..1dc9df2
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/RegularFileBlocksTest.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.truth.Truth.assertThat;
+
+import com.google.common.primitives.Bytes;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for the lower-level operations dealing with the blocks of a {@link RegularFile}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class RegularFileBlocksTest {
+
+  private RegularFile file;
+
+  @Before
+  public void setUp() {
+    file = createFile();
+  }
+
+  private static RegularFile createFile() {
+    return RegularFile.create(-1, new HeapDisk(2, 2, 2));
+  }
+
+  @Test
+  public void testInitialState() {
+    assertThat(file.blockCount()).isEqualTo(0);
+
+    // no bounds checking, but there should never be a block at an index >= size
+    assertThat(file.getBlock(0)).isNull();
+  }
+
+  @Test
+  public void testAddAndGet() {
+    file.addBlock(new byte[] {1});
+
+    assertThat(file.blockCount()).isEqualTo(1);
+    assertThat(Bytes.asList(file.getBlock(0))).isEqualTo(Bytes.asList(new byte[] {1}));
+    assertThat(file.getBlock(1)).isNull();
+
+    file.addBlock(new byte[] {1, 2});
+
+    assertThat(file.blockCount()).isEqualTo(2);
+    assertThat(Bytes.asList(file.getBlock(1))).isEqualTo(Bytes.asList(new byte[] {1, 2}));
+    assertThat(file.getBlock(2)).isNull();
+  }
+
+  @Test
+  public void testTruncate() {
+    file.addBlock(new byte[0]);
+    file.addBlock(new byte[0]);
+    file.addBlock(new byte[0]);
+    file.addBlock(new byte[0]);
+
+    assertThat(file.blockCount()).isEqualTo(4);
+
+    file.truncateBlocks(2);
+
+    assertThat(file.blockCount()).isEqualTo(2);
+    assertThat(file.getBlock(2)).isNull();
+    assertThat(file.getBlock(3)).isNull();
+    assertThat(file.getBlock(0)).isNotNull();
+
+    file.truncateBlocks(0);
+    assertThat(file.blockCount()).isEqualTo(0);
+    assertThat(file.getBlock(0)).isNull();
+  }
+
+  @Test
+  public void testCopyTo() {
+    file.addBlock(new byte[] {1});
+    file.addBlock(new byte[] {1, 2});
+    RegularFile other = createFile();
+
+    assertThat(other.blockCount()).isEqualTo(0);
+
+    file.copyBlocksTo(other, 2);
+
+    assertThat(other.blockCount()).isEqualTo(2);
+    assertThat(other.getBlock(0)).isEqualTo(file.getBlock(0));
+    assertThat(other.getBlock(1)).isEqualTo(file.getBlock(1));
+
+    file.copyBlocksTo(other, 1); // should copy the last block
+
+    assertThat(other.blockCount()).isEqualTo(3);
+    assertThat(other.getBlock(2)).isEqualTo(file.getBlock(1));
+
+    other.copyBlocksTo(file, 3);
+
+    assertThat(file.blockCount()).isEqualTo(5);
+    assertThat(file.getBlock(2)).isEqualTo(other.getBlock(0));
+    assertThat(file.getBlock(3)).isEqualTo(other.getBlock(1));
+    assertThat(file.getBlock(4)).isEqualTo(other.getBlock(2));
+  }
+
+  @Test
+  public void testTransferTo() {
+    file.addBlock(new byte[] {1});
+    file.addBlock(new byte[] {1, 2});
+    file.addBlock(new byte[] {1, 2, 3});
+    RegularFile other = createFile();
+
+    assertThat(file.blockCount()).isEqualTo(3);
+    assertThat(other.blockCount()).isEqualTo(0);
+
+    file.transferBlocksTo(other, 3);
+
+    assertThat(file.blockCount()).isEqualTo(0);
+    assertThat(other.blockCount()).isEqualTo(3);
+
+    assertThat(file.getBlock(0)).isNull();
+    assertThat(Bytes.asList(other.getBlock(0))).isEqualTo(Bytes.asList(new byte[] {1}));
+    assertThat(Bytes.asList(other.getBlock(1))).isEqualTo(Bytes.asList(new byte[] {1, 2}));
+    assertThat(Bytes.asList(other.getBlock(2))).isEqualTo(Bytes.asList(new byte[] {1, 2, 3}));
+
+    other.transferBlocksTo(file, 1);
+
+    assertThat(file.blockCount()).isEqualTo(1);
+    assertThat(other.blockCount()).isEqualTo(2);
+    assertThat(other.getBlock(2)).isNull();
+    assertThat(Bytes.asList(file.getBlock(0))).isEqualTo(Bytes.asList(new byte[] {1, 2, 3}));
+    assertThat(file.getBlock(1)).isNull();
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java b/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java
new file mode 100644
index 0000000..f581690
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java
@@ -0,0 +1,892 @@
+/*
+ * 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.jimfs.TestUtils.buffer;
+import static com.google.common.jimfs.TestUtils.buffers;
+import static com.google.common.jimfs.TestUtils.bytes;
+import static com.google.common.primitives.Bytes.concat;
+import static org.junit.Assert.assertArrayEquals;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Tests for {@link RegularFile} and by extension for {@link HeapDisk}. These tests test files
+ * created by a heap disk in a number of different states.
+ *
+ * @author Colin Decker
+ */
+public class RegularFileTest {
+
+  /**
+   * Returns a test suite for testing file methods with a variety of {@code HeapDisk}
+   * configurations.
+   */
+  public static TestSuite suite() {
+    TestSuite suite = new TestSuite();
+
+    for (ReuseStrategy reuseStrategy : EnumSet.allOf(ReuseStrategy.class)) {
+      TestSuite suiteForReuseStrategy = new TestSuite(reuseStrategy.toString());
+      Set<List<Integer>> sizeOptions =
+          Sets.cartesianProduct(ImmutableList.of(BLOCK_SIZES, CACHE_SIZES));
+      for (List<Integer> options : sizeOptions) {
+        int blockSize = options.get(0);
+        int cacheSize = options.get(1);
+        if (cacheSize > 0 && cacheSize < blockSize) {
+          // skip cases where the cache size is not -1 (all) or 0 (none) but it is < blockSize,
+          // because this is equivalent to a cache size of 0
+          continue;
+        }
+
+        TestConfiguration state = new TestConfiguration(blockSize, cacheSize, reuseStrategy);
+        TestSuite suiteForTest = new TestSuite(state.toString());
+        for (Method method : TEST_METHODS) {
+          RegularFileTestRunner tester = new RegularFileTestRunner(method.getName(), state);
+          suiteForTest.addTest(tester);
+        }
+        suiteForReuseStrategy.addTest(suiteForTest);
+      }
+      suite.addTest(suiteForReuseStrategy);
+    }
+
+    return suite;
+  }
+
+  public static final ImmutableSet<Integer> BLOCK_SIZES = ImmutableSet.of(2, 8, 128, 8192);
+  public static final ImmutableSet<Integer> CACHE_SIZES = ImmutableSet.of(0, 4, 16, 128, -1);
+
+  private static final ImmutableList<Method> TEST_METHODS =
+      FluentIterable.from(Arrays.asList(RegularFileTestRunner.class.getDeclaredMethods()))
+          .filter(
+              new Predicate<Method>() {
+                @Override
+                public boolean apply(Method method) {
+                  return method.getName().startsWith("test")
+                      && Modifier.isPublic(method.getModifiers())
+                      && method.getParameterTypes().length == 0;
+                }
+              })
+          .toList();
+
+  /**
+   * Different strategies for handling reuse of disks and/or files between tests, intended to ensure
+   * that {@link HeapDisk} operates properly in a variety of usage states including newly created,
+   * having created files that have not been deleted yet, having created files that have been
+   * deleted, and having created files some of which have been deleted and some of which have not.
+   */
+  public enum ReuseStrategy {
+    /** Creates a new disk for each test. */
+    NEW_DISK,
+    /** Retains files after each test, forcing new blocks to be allocated. */
+    KEEP_FILES,
+    /** Deletes files after each test, allowing caching to be used if enabled. */
+    DELETE_FILES,
+    /** Randomly keeps or deletes a file after each test. */
+    KEEP_OR_DELETE_FILES
+  }
+
+  /** Configuration for a set of test cases. */
+  public static final class TestConfiguration {
+
+    private final int blockSize;
+    private final int cacheSize;
+    private final ReuseStrategy reuseStrategy;
+
+    private HeapDisk disk;
+
+    public TestConfiguration(int blockSize, int cacheSize, ReuseStrategy reuseStrategy) {
+      this.blockSize = blockSize;
+      this.cacheSize = cacheSize;
+      this.reuseStrategy = reuseStrategy;
+
+      if (reuseStrategy != ReuseStrategy.NEW_DISK) {
+        this.disk = createDisk();
+      }
+    }
+
+    private HeapDisk createDisk() {
+      int maxCachedBlockCount = cacheSize == -1 ? Integer.MAX_VALUE : (cacheSize / blockSize);
+      return new HeapDisk(blockSize, Integer.MAX_VALUE, maxCachedBlockCount);
+    }
+
+    public RegularFile createRegularFile() {
+      if (reuseStrategy == ReuseStrategy.NEW_DISK) {
+        disk = createDisk();
+      }
+      return RegularFile.create(0, disk);
+    }
+
+    public void tearDown(RegularFile file) {
+      switch (reuseStrategy) {
+        case DELETE_FILES:
+          file.deleted();
+          break;
+        case KEEP_OR_DELETE_FILES:
+          if (new Random().nextBoolean()) {
+            file.deleted();
+          }
+          break;
+        case KEEP_FILES:
+          break;
+        default:
+          break;
+      }
+    }
+
+    @Override
+    public String toString() {
+      return reuseStrategy + " [" + blockSize + ", " + cacheSize + "]";
+    }
+  }
+
+  /** Actual test cases for testing RegularFiles. */
+  public static class RegularFileTestRunner extends TestCase {
+
+    private final TestConfiguration configuration;
+
+    protected RegularFile file;
+
+    public RegularFileTestRunner(String methodName, TestConfiguration configuration) {
+      super(methodName);
+      this.configuration = configuration;
+    }
+
+    @Override
+    public String getName() {
+      return super.getName() + " [" + configuration + "]";
+    }
+
+    @Override
+    public void setUp() {
+      file = configuration.createRegularFile();
+    }
+
+    @Override
+    public void tearDown() {
+      configuration.tearDown(file);
+    }
+
+    private void fillContent(String fill) throws IOException {
+      file.write(0, buffer(fill));
+    }
+
+    public void testEmpty() {
+      assertEquals(0, file.size());
+      assertContentEquals("", file);
+    }
+
+    public void testEmpty_read_singleByte() {
+      assertEquals(-1, file.read(0));
+      assertEquals(-1, file.read(1));
+    }
+
+    public void testEmpty_read_byteArray() {
+      byte[] array = new byte[10];
+      assertEquals(-1, file.read(0, array, 0, array.length));
+      assertArrayEquals(bytes("0000000000"), array);
+    }
+
+    public void testEmpty_read_singleBuffer() {
+      ByteBuffer buffer = ByteBuffer.allocate(10);
+      int read = file.read(0, buffer);
+      assertEquals(-1, read);
+      assertEquals(0, buffer.position());
+    }
+
+    public void testEmpty_read_multipleBuffers() {
+      ByteBuffer buf1 = ByteBuffer.allocate(5);
+      ByteBuffer buf2 = ByteBuffer.allocate(5);
+      long read = file.read(0, ImmutableList.of(buf1, buf2));
+      assertEquals(-1, read);
+      assertEquals(0, buf1.position());
+      assertEquals(0, buf2.position());
+    }
+
+    public void testEmpty_write_singleByte_atStart() throws IOException {
+      file.write(0, (byte) 1);
+      assertContentEquals("1", file);
+    }
+
+    public void testEmpty_write_byteArray_atStart() throws IOException {
+      byte[] bytes = bytes("111111");
+      file.write(0, bytes, 0, bytes.length);
+      assertContentEquals(bytes, file);
+    }
+
+    public void testEmpty_write_partialByteArray_atStart() throws IOException {
+      byte[] bytes = bytes("2211111122");
+      file.write(0, bytes, 2, 6);
+      assertContentEquals("111111", file);
+    }
+
+    public void testEmpty_write_singleBuffer_atStart() throws IOException {
+      file.write(0, buffer("111111"));
+      assertContentEquals("111111", file);
+    }
+
+    public void testEmpty_write_multipleBuffers_atStart() throws IOException {
+      file.write(0, buffers("111", "111"));
+      assertContentEquals("111111", file);
+    }
+
+    public void testEmpty_write_singleByte_atNonZeroPosition() throws IOException {
+      file.write(5, (byte) 1);
+      assertContentEquals("000001", file);
+    }
+
+    public void testEmpty_write_byteArray_atNonZeroPosition() throws IOException {
+      byte[] bytes = bytes("111111");
+      file.write(5, bytes, 0, bytes.length);
+      assertContentEquals("00000111111", file);
+    }
+
+    public void testEmpty_write_partialByteArray_atNonZeroPosition() throws IOException {
+      byte[] bytes = bytes("2211111122");
+      file.write(5, bytes, 2, 6);
+      assertContentEquals("00000111111", file);
+    }
+
+    public void testEmpty_write_singleBuffer_atNonZeroPosition() throws IOException {
+      file.write(5, buffer("111"));
+      assertContentEquals("00000111", file);
+    }
+
+    public void testEmpty_write_multipleBuffers_atNonZeroPosition() throws IOException {
+      file.write(5, buffers("111", "222"));
+      assertContentEquals("00000111222", file);
+    }
+
+    public void testEmpty_write_noBytesArray_atStart() throws IOException {
+      file.write(0, bytes(), 0, 0);
+      assertContentEquals(bytes(), file);
+    }
+
+    public void testEmpty_write_noBytesArray_atNonZeroPosition() throws IOException {
+      file.write(5, bytes(), 0, 0);
+      assertContentEquals(bytes("00000"), file);
+    }
+
+    public void testEmpty_write_noBytesBuffer_atStart() throws IOException {
+      file.write(0, buffer(""));
+      assertContentEquals(bytes(), file);
+    }
+
+    public void testEmpty_write_noBytesBuffer_atNonZeroPosition() throws IOException {
+      ByteBuffer buffer = ByteBuffer.allocate(0);
+      file.write(5, buffer);
+      assertContentEquals(bytes("00000"), file);
+    }
+
+    public void testEmpty_write_noBytesBuffers_atStart() throws IOException {
+      file.write(0, ImmutableList.of(buffer(""), buffer(""), buffer("")));
+      assertContentEquals(bytes(), file);
+    }
+
+    public void testEmpty_write_noBytesBuffers_atNonZeroPosition() throws IOException {
+      file.write(5, ImmutableList.of(buffer(""), buffer(""), buffer("")));
+      assertContentEquals(bytes("00000"), file);
+    }
+
+    public void testEmpty_transferFrom_fromStart_countEqualsSrcSize() throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 0, 6);
+      assertEquals(6, transferred);
+      assertContentEquals("111111", file);
+    }
+
+    public void testEmpty_transferFrom_fromStart_countLessThanSrcSize() throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 0, 3);
+      assertEquals(3, transferred);
+      assertContentEquals("111", file);
+    }
+
+    public void testEmpty_transferFrom_fromStart_countGreaterThanSrcSize() throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 0, 12);
+      assertEquals(6, transferred);
+      assertContentEquals("111111", file);
+    }
+
+    public void testEmpty_transferFrom_fromBeyondStart_countEqualsSrcSize() throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 4, 6);
+      assertEquals(6, transferred);
+      assertContentEquals("0000111111", file);
+    }
+
+    public void testEmpty_transferFrom_fromBeyondStart_countLessThanSrcSize() throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 4, 3);
+      assertEquals(3, transferred);
+      assertContentEquals("0000111", file);
+    }
+
+    public void testEmpty_transferFrom_fromBeyondStart_countGreaterThanSrcSize()
+        throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("111111")), 4, 12);
+      assertEquals(6, transferred);
+      assertContentEquals("0000111111", file);
+    }
+
+    public void testEmpty_transferFrom_fromStart_noBytes_countEqualsSrcSize() throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 0, 0);
+      assertEquals(0, transferred);
+      assertContentEquals(bytes(), file);
+    }
+
+    public void testEmpty_transferFrom_fromStart_noBytes_countGreaterThanSrcSize()
+        throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 0, 10);
+      assertEquals(0, transferred);
+      assertContentEquals(bytes(), file);
+    }
+
+    public void testEmpty_transferFrom_fromBeyondStart_noBytes_countEqualsSrcSize()
+        throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 5, 0);
+      assertEquals(0, transferred);
+      assertContentEquals(bytes("00000"), file);
+    }
+
+    public void testEmpty_transferFrom_fromBeyondStart_noBytes_countGreaterThanSrcSize()
+        throws IOException {
+      long transferred = file.transferFrom(new ByteBufferChannel(buffer("")), 5, 10);
+      assertEquals(0, transferred);
+      assertContentEquals(bytes("00000"), file);
+    }
+
+    public void testEmpty_transferTo() throws IOException {
+      ByteBufferChannel channel = new ByteBufferChannel(100);
+      assertEquals(0, file.transferTo(0, 100, channel));
+    }
+
+    public void testEmpty_copy() throws IOException {
+      RegularFile copy = file.copyWithoutContent(1);
+      assertContentEquals("", copy);
+    }
+
+    public void testEmpty_truncate_toZero() throws IOException {
+      file.truncate(0);
+      assertContentEquals("", file);
+    }
+
+    public void testEmpty_truncate_sizeUp() throws IOException {
+      file.truncate(10);
+      assertContentEquals("", file);
+    }
+
+    public void testNonEmpty() throws IOException {
+      fillContent("222222");
+      assertContentEquals("222222", file);
+    }
+
+    public void testNonEmpty_read_singleByte() throws IOException {
+      fillContent("123456");
+      assertEquals(1, file.read(0));
+      assertEquals(2, file.read(1));
+      assertEquals(6, file.read(5));
+      assertEquals(-1, file.read(6));
+      assertEquals(-1, file.read(100));
+    }
+
+    public void testNonEmpty_read_all_byteArray() throws IOException {
+      fillContent("222222");
+      byte[] array = new byte[6];
+      assertEquals(6, file.read(0, array, 0, array.length));
+      assertArrayEquals(bytes("222222"), array);
+    }
+
+    public void testNonEmpty_read_all_singleBuffer() throws IOException {
+      fillContent("222222");
+      ByteBuffer buffer = ByteBuffer.allocate(6);
+      assertEquals(6, file.read(0, buffer));
+      assertBufferEquals("222222", 0, buffer);
+    }
+
+    public void testNonEmpty_read_all_multipleBuffers() throws IOException {
+      fillContent("223334");
+      ByteBuffer buf1 = ByteBuffer.allocate(3);
+      ByteBuffer buf2 = ByteBuffer.allocate(3);
+      assertEquals(6, file.read(0, ImmutableList.of(buf1, buf2)));
+      assertBufferEquals("223", 0, buf1);
+      assertBufferEquals("334", 0, buf2);
+    }
+
+    public void testNonEmpty_read_all_byteArray_largerThanContent() throws IOException {
+      fillContent("222222");
+      byte[] array = new byte[10];
+      assertEquals(6, file.read(0, array, 0, array.length));
+      assertArrayEquals(bytes("2222220000"), array);
+      array = new byte[10];
+      assertEquals(6, file.read(0, array, 2, 6));
+      assertArrayEquals(bytes("0022222200"), array);
+    }
+
+    public void testNonEmpty_read_all_singleBuffer_largerThanContent() throws IOException {
+      fillContent("222222");
+      ByteBuffer buffer = ByteBuffer.allocate(16);
+      assertBufferEquals("0000000000000000", 16, buffer);
+      assertEquals(6, file.read(0, buffer));
+      assertBufferEquals("2222220000000000", 10, buffer);
+    }
+
+    public void testNonEmpty_read_all_multipleBuffers_largerThanContent() throws IOException {
+      fillContent("222222");
+      ByteBuffer buf1 = ByteBuffer.allocate(4);
+      ByteBuffer buf2 = ByteBuffer.allocate(8);
+      assertEquals(6, file.read(0, ImmutableList.of(buf1, buf2)));
+      assertBufferEquals("2222", 0, buf1);
+      assertBufferEquals("22000000", 6, buf2);
+    }
+
+    public void testNonEmpty_read_all_multipleBuffers_extraBuffers() throws IOException {
+      fillContent("222222");
+      ByteBuffer buf1 = ByteBuffer.allocate(4);
+      ByteBuffer buf2 = ByteBuffer.allocate(8);
+      ByteBuffer buf3 = ByteBuffer.allocate(4);
+      assertEquals(6, file.read(0, ImmutableList.of(buf1, buf2, buf3)));
+      assertBufferEquals("2222", 0, buf1);
+      assertBufferEquals("22000000", 6, buf2);
+      assertBufferEquals("0000", 4, buf3);
+    }
+
+    public void testNonEmpty_read_partial_fromStart_byteArray() throws IOException {
+      fillContent("222222");
+      byte[] array = new byte[3];
+      assertEquals(3, file.read(0, array, 0, array.length));
+      assertArrayEquals(bytes("222"), array);
+      array = new byte[10];
+      assertEquals(3, file.read(0, array, 1, 3));
+      assertArrayEquals(bytes("0222000000"), array);
+    }
+
+    public void testNonEmpty_read_partial_fromMiddle_byteArray() throws IOException {
+      fillContent("22223333");
+      byte[] array = new byte[3];
+      assertEquals(3, file.read(3, array, 0, array.length));
+      assertArrayEquals(bytes("233"), array);
+      array = new byte[10];
+      assertEquals(3, file.read(3, array, 1, 3));
+      assertArrayEquals(bytes("0233000000"), array);
+    }
+
+    public void testNonEmpty_read_partial_fromEnd_byteArray() throws IOException {
+      fillContent("2222222222");
+      byte[] array = new byte[3];
+      assertEquals(2, file.read(8, array, 0, array.length));
+      assertArrayEquals(bytes("220"), array);
+      array = new byte[10];
+      assertEquals(2, file.read(8, array, 1, 3));
+      assertArrayEquals(bytes("0220000000"), array);
+    }
+
+    public void testNonEmpty_read_partial_fromStart_singleBuffer() throws IOException {
+      fillContent("222222");
+      ByteBuffer buffer = ByteBuffer.allocate(3);
+      assertEquals(3, file.read(0, buffer));
+      assertBufferEquals("222", 0, buffer);
+    }
+
+    public void testNonEmpty_read_partial_fromMiddle_singleBuffer() throws IOException {
+      fillContent("22223333");
+      ByteBuffer buffer = ByteBuffer.allocate(3);
+      assertEquals(3, file.read(3, buffer));
+      assertBufferEquals("233", 0, buffer);
+    }
+
+    public void testNonEmpty_read_partial_fromEnd_singleBuffer() throws IOException {
+      fillContent("2222222222");
+      ByteBuffer buffer = ByteBuffer.allocate(3);
+      assertEquals(2, file.read(8, buffer));
+      assertBufferEquals("220", 1, buffer);
+    }
+
+    public void testNonEmpty_read_partial_fromStart_multipleBuffers() throws IOException {
+      fillContent("12345678");
+      ByteBuffer buf1 = ByteBuffer.allocate(2);
+      ByteBuffer buf2 = ByteBuffer.allocate(2);
+      assertEquals(4, file.read(0, ImmutableList.of(buf1, buf2)));
+      assertBufferEquals("12", 0, buf1);
+      assertBufferEquals("34", 0, buf2);
+    }
+
+    public void testNonEmpty_read_partial_fromMiddle_multipleBuffers() throws IOException {
+      fillContent("12345678");
+      ByteBuffer buf1 = ByteBuffer.allocate(2);
+      ByteBuffer buf2 = ByteBuffer.allocate(2);
+      assertEquals(4, file.read(3, ImmutableList.of(buf1, buf2)));
+      assertBufferEquals("45", 0, buf1);
+      assertBufferEquals("67", 0, buf2);
+    }
+
+    public void testNonEmpty_read_partial_fromEnd_multipleBuffers() throws IOException {
+      fillContent("123456789");
+      ByteBuffer buf1 = ByteBuffer.allocate(2);
+      ByteBuffer buf2 = ByteBuffer.allocate(2);
+      assertEquals(3, file.read(6, ImmutableList.of(buf1, buf2)));
+      assertBufferEquals("78", 0, buf1);
+      assertBufferEquals("90", 1, buf2);
+    }
+
+    public void testNonEmpty_read_fromPastEnd_byteArray() throws IOException {
+      fillContent("123");
+      byte[] array = new byte[3];
+      assertEquals(-1, file.read(3, array, 0, array.length));
+      assertArrayEquals(bytes("000"), array);
+      assertEquals(-1, file.read(3, array, 0, 2));
+      assertArrayEquals(bytes("000"), array);
+    }
+
+    public void testNonEmpty_read_fromPastEnd_singleBuffer() throws IOException {
+      fillContent("123");
+      ByteBuffer buffer = ByteBuffer.allocate(3);
+      file.read(3, buffer);
+      assertBufferEquals("000", 3, buffer);
+    }
+
+    public void testNonEmpty_read_fromPastEnd_multipleBuffers() throws IOException {
+      fillContent("123");
+      ByteBuffer buf1 = ByteBuffer.allocate(2);
+      ByteBuffer buf2 = ByteBuffer.allocate(2);
+      assertEquals(-1, file.read(6, ImmutableList.of(buf1, buf2)));
+      assertBufferEquals("00", 2, buf1);
+      assertBufferEquals("00", 2, buf2);
+    }
+
+    public void testNonEmpty_write_partial_fromStart_singleByte() throws IOException {
+      fillContent("222222");
+      assertEquals(1, file.write(0, (byte) 1));
+      assertContentEquals("122222", file);
+    }
+
+    public void testNonEmpty_write_partial_fromMiddle_singleByte() throws IOException {
+      fillContent("222222");
+      assertEquals(1, file.write(3, (byte) 1));
+      assertContentEquals("222122", file);
+    }
+
+    public void testNonEmpty_write_partial_fromEnd_singleByte() throws IOException {
+      fillContent("222222");
+      assertEquals(1, file.write(6, (byte) 1));
+      assertContentEquals("2222221", file);
+    }
+
+    public void testNonEmpty_write_partial_fromStart_byteArray() throws IOException {
+      fillContent("222222");
+      assertEquals(3, file.write(0, bytes("111"), 0, 3));
+      assertContentEquals("111222", file);
+      assertEquals(2, file.write(0, bytes("333333"), 0, 2));
+      assertContentEquals("331222", file);
+    }
+
+    public void testNonEmpty_write_partial_fromMiddle_byteArray() throws IOException {
+      fillContent("22222222");
+      assertEquals(3, file.write(3, buffer("111")));
+      assertContentEquals("22211122", file);
+      assertEquals(2, file.write(5, bytes("333333"), 1, 2));
+      assertContentEquals("22211332", file);
+    }
+
+    public void testNonEmpty_write_partial_fromBeforeEnd_byteArray() throws IOException {
+      fillContent("22222222");
+      assertEquals(3, file.write(6, bytes("111"), 0, 3));
+      assertContentEquals("222222111", file);
+      assertEquals(2, file.write(8, bytes("333333"), 2, 2));
+      assertContentEquals("2222221133", file);
+    }
+
+    public void testNonEmpty_write_partial_fromEnd_byteArray() throws IOException {
+      fillContent("222222");
+      assertEquals(3, file.write(6, bytes("111"), 0, 3));
+      assertContentEquals("222222111", file);
+      assertEquals(2, file.write(9, bytes("333333"), 3, 2));
+      assertContentEquals("22222211133", file);
+    }
+
+    public void testNonEmpty_write_partial_fromPastEnd_byteArray() throws IOException {
+      fillContent("222222");
+      assertEquals(3, file.write(8, bytes("111"), 0, 3));
+      assertContentEquals("22222200111", file);
+      assertEquals(2, file.write(13, bytes("333333"), 4, 2));
+      assertContentEquals("222222001110033", file);
+    }
+
+    public void testNonEmpty_write_partial_fromStart_singleBuffer() throws IOException {
+      fillContent("222222");
+      assertEquals(3, file.write(0, buffer("111")));
+      assertContentEquals("111222", file);
+    }
+
+    public void testNonEmpty_write_partial_fromMiddle_singleBuffer() throws IOException {
+      fillContent("22222222");
+      assertEquals(3, file.write(3, buffer("111")));
+      assertContentEquals("22211122", file);
+    }
+
+    public void testNonEmpty_write_partial_fromBeforeEnd_singleBuffer() throws IOException {
+      fillContent("22222222");
+      assertEquals(3, file.write(6, buffer("111")));
+      assertContentEquals("222222111", file);
+    }
+
+    public void testNonEmpty_write_partial_fromEnd_singleBuffer() throws IOException {
+      fillContent("222222");
+      assertEquals(3, file.write(6, buffer("111")));
+      assertContentEquals("222222111", file);
+    }
+
+    public void testNonEmpty_write_partial_fromPastEnd_singleBuffer() throws IOException {
+      fillContent("222222");
+      assertEquals(3, file.write(8, buffer("111")));
+      assertContentEquals("22222200111", file);
+    }
+
+    public void testNonEmpty_write_partial_fromStart_multipleBuffers() throws IOException {
+      fillContent("222222");
+      assertEquals(4, file.write(0, buffers("11", "33")));
+      assertContentEquals("113322", file);
+    }
+
+    public void testNonEmpty_write_partial_fromMiddle_multipleBuffers() throws IOException {
+      fillContent("22222222");
+      assertEquals(4, file.write(2, buffers("11", "33")));
+      assertContentEquals("22113322", file);
+    }
+
+    public void testNonEmpty_write_partial_fromBeforeEnd_multipleBuffers() throws IOException {
+      fillContent("22222222");
+      assertEquals(6, file.write(6, buffers("111", "333")));
+      assertContentEquals("222222111333", file);
+    }
+
+    public void testNonEmpty_write_partial_fromEnd_multipleBuffers() throws IOException {
+      fillContent("222222");
+      assertEquals(6, file.write(6, buffers("111", "333")));
+      assertContentEquals("222222111333", file);
+    }
+
+    public void testNonEmpty_write_partial_fromPastEnd_multipleBuffers() throws IOException {
+      fillContent("222222");
+      assertEquals(4, file.write(10, buffers("11", "33")));
+      assertContentEquals("22222200001133", file);
+    }
+
+    public void testNonEmpty_write_overwrite_sameLength() throws IOException {
+      fillContent("2222");
+      assertEquals(4, file.write(0, buffer("1234")));
+      assertContentEquals("1234", file);
+    }
+
+    public void testNonEmpty_write_overwrite_greaterLength() throws IOException {
+      fillContent("2222");
+      assertEquals(8, file.write(0, buffer("12345678")));
+      assertContentEquals("12345678", file);
+    }
+
+    public void testNonEmpty_transferTo_fromStart_countEqualsSize() throws IOException {
+      fillContent("123456");
+      ByteBufferChannel channel = new ByteBufferChannel(10);
+      assertEquals(6, file.transferTo(0, 6, channel));
+      assertBufferEquals("1234560000", 4, channel.buffer());
+    }
+
+    public void testNonEmpty_transferTo_fromStart_countLessThanSize() throws IOException {
+      fillContent("123456");
+      ByteBufferChannel channel = new ByteBufferChannel(10);
+      assertEquals(4, file.transferTo(0, 4, channel));
+      assertBufferEquals("1234000000", 6, channel.buffer());
+    }
+
+    public void testNonEmpty_transferTo_fromMiddle_countEqualsSize() throws IOException {
+      fillContent("123456");
+      ByteBufferChannel channel = new ByteBufferChannel(10);
+      assertEquals(2, file.transferTo(4, 6, channel));
+      assertBufferEquals("5600000000", 8, channel.buffer());
+    }
+
+    public void testNonEmpty_transferTo_fromMiddle_countLessThanSize() throws IOException {
+      fillContent("12345678");
+      ByteBufferChannel channel = new ByteBufferChannel(10);
+      assertEquals(4, file.transferTo(3, 4, channel));
+      assertBufferEquals("4567000000", 6, channel.buffer());
+    }
+
+    public void testNonEmpty_transferFrom_toStart_countEqualsSrcSize() throws IOException {
+      fillContent("22222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("11111"));
+      assertEquals(5, file.transferFrom(channel, 0, 5));
+      assertContentEquals("11111222", file);
+    }
+
+    public void testNonEmpty_transferFrom_toStart_countLessThanSrcSize() throws IOException {
+      fillContent("22222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("11111"));
+      assertEquals(3, file.transferFrom(channel, 0, 3));
+      assertContentEquals("11122222", file);
+    }
+
+    public void testNonEmpty_transferFrom_toStart_countGreaterThanSrcSize() throws IOException {
+      fillContent("22222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("11111"));
+      assertEquals(5, file.transferFrom(channel, 0, 10));
+      assertContentEquals("11111222", file);
+    }
+
+    public void testNonEmpty_transferFrom_toMiddle_countEqualsSrcSize() throws IOException {
+      fillContent("22222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("1111"));
+      assertEquals(4, file.transferFrom(channel, 2, 4));
+      assertContentEquals("22111122", file);
+    }
+
+    public void testNonEmpty_transferFrom_toMiddle_countLessThanSrcSize() throws IOException {
+      fillContent("22222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("11111"));
+      assertEquals(3, file.transferFrom(channel, 2, 3));
+      assertContentEquals("22111222", file);
+    }
+
+    public void testNonEmpty_transferFrom_toMiddle_countGreaterThanSrcSize() throws IOException {
+      fillContent("22222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("1111"));
+      assertEquals(4, file.transferFrom(channel, 2, 100));
+      assertContentEquals("22111122", file);
+    }
+
+    public void testNonEmpty_transferFrom_toMiddle_transferGoesBeyondContentSize()
+        throws IOException {
+      fillContent("222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("111111"));
+      assertEquals(6, file.transferFrom(channel, 4, 6));
+      assertContentEquals("2222111111", file);
+    }
+
+    public void testNonEmpty_transferFrom_toEnd() throws IOException {
+      fillContent("222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("111111"));
+      assertEquals(6, file.transferFrom(channel, 6, 6));
+      assertContentEquals("222222111111", file);
+    }
+
+    public void testNonEmpty_transferFrom_toPastEnd() throws IOException {
+      fillContent("222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("111111"));
+      assertEquals(6, file.transferFrom(channel, 10, 6));
+      assertContentEquals("2222220000111111", file);
+    }
+
+    public void testNonEmpty_transferFrom_hugeOverestimateCount() throws IOException {
+      fillContent("222222");
+      ByteBufferChannel channel = new ByteBufferChannel(buffer("111111"));
+      assertEquals(6, file.transferFrom(channel, 6, 1024 * 1024 * 10));
+      assertContentEquals("222222111111", file);
+    }
+
+    public void testNonEmpty_copy() throws IOException {
+      fillContent("123456");
+      RegularFile copy = file.copyWithoutContent(1);
+      file.copyContentTo(copy);
+      assertContentEquals("123456", copy);
+    }
+
+    public void testNonEmpty_copy_multipleTimes() throws IOException {
+      fillContent("123456");
+      RegularFile copy = file.copyWithoutContent(1);
+      file.copyContentTo(copy);
+      RegularFile copy2 = copy.copyWithoutContent(2);
+      copy.copyContentTo(copy2);
+      assertContentEquals("123456", copy);
+    }
+
+    public void testNonEmpty_truncate_toZero() throws IOException {
+      fillContent("123456");
+      file.truncate(0);
+      assertContentEquals("", file);
+    }
+
+    public void testNonEmpty_truncate_partial() throws IOException {
+      fillContent("12345678");
+      file.truncate(5);
+      assertContentEquals("12345", file);
+    }
+
+    public void testNonEmpty_truncate_sizeUp() throws IOException {
+      fillContent("123456");
+      file.truncate(12);
+      assertContentEquals("123456", file);
+    }
+
+    public void testDeletedStoreRemainsUsableWhileOpen() throws IOException {
+      byte[] bytes = bytes("1234567890");
+      file.write(0, bytes, 0, bytes.length);
+
+      file.opened();
+      file.opened();
+
+      file.deleted();
+
+      assertContentEquals(bytes, file);
+
+      byte[] moreBytes = bytes("1234");
+      file.write(bytes.length, moreBytes, 0, 4);
+
+      byte[] totalBytes = concat(bytes, bytes("1234"));
+      assertContentEquals(totalBytes, file);
+
+      file.closed();
+
+      assertContentEquals(totalBytes, file);
+
+      file.closed();
+
+      // don't check anything else; no guarantee of what if anything will happen once the file is
+      // deleted and completely closed
+    }
+
+    private static void assertBufferEquals(String expected, ByteBuffer actual) {
+      assertEquals(expected.length(), actual.capacity());
+      assertArrayEquals(bytes(expected), actual.array());
+    }
+
+    private static void assertBufferEquals(String expected, int remaining, ByteBuffer actual) {
+      assertBufferEquals(expected, actual);
+      assertEquals(remaining, actual.remaining());
+    }
+
+    private static void assertContentEquals(String expected, RegularFile actual) {
+      assertContentEquals(bytes(expected), actual);
+    }
+
+    protected static void assertContentEquals(byte[] expected, RegularFile actual) {
+      assertEquals(expected.length, actual.sizeWithoutLocking());
+      byte[] actualBytes = new byte[(int) actual.sizeWithoutLocking()];
+      actual.read(0, ByteBuffer.wrap(actualBytes));
+      assertArrayEquals(expected, actualBytes);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/TestAttributeProvider.java b/jimfs/src/test/java/com/google/common/jimfs/TestAttributeProvider.java
new file mode 100644
index 0000000..4518132
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/TestAttributeProvider.java
@@ -0,0 +1,227 @@
+/*
+ * 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.FileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.util.HashMap;
+import java.util.Map;
+import org.checkerframework.checker.nullness.compatqual.NullableDecl;
+
+/** @author Colin Decker */
+public final class TestAttributeProvider extends AttributeProvider {
+
+  private static final ImmutableSet<String> ATTRIBUTES = ImmutableSet.of("foo", "bar", "baz");
+
+  @Override
+  public String name() {
+    return "test";
+  }
+
+  @Override
+  public ImmutableSet<String> inherits() {
+    return ImmutableSet.of("basic");
+  }
+
+  @Override
+  public ImmutableSet<String> fixedAttributes() {
+    return ATTRIBUTES;
+  }
+
+  @Override
+  public ImmutableMap<String, ?> defaultValues(Map<String, ?> userDefaults) {
+    Map<String, Object> result = new HashMap<>();
+
+    Long bar = 0L;
+    Integer baz = 1;
+    if (userDefaults.containsKey("test:bar")) {
+      bar = checkType("test", "bar", userDefaults.get("test:bar"), Number.class).longValue();
+    }
+    if (userDefaults.containsKey("test:baz")) {
+      baz = checkType("test", "baz", userDefaults.get("test:baz"), Integer.class);
+    }
+
+    result.put("test:bar", bar);
+    result.put("test:baz", baz);
+    return ImmutableMap.copyOf(result);
+  }
+
+  @Override
+  public void set(File file, String view, String attribute, Object value, boolean create) {
+    switch (attribute) {
+      case "bar":
+        checkNotCreate(view, attribute, create);
+        file.setAttribute(
+            "test", "bar", checkType(view, attribute, value, Number.class).longValue());
+        break;
+      case "baz":
+        file.setAttribute("test", "baz", checkType(view, attribute, value, Integer.class));
+        break;
+      default:
+        throw unsettable(view, attribute, create);
+    }
+  }
+
+  @Override
+  public Object get(File file, String attribute) {
+    if (attribute.equals("foo")) {
+      return "hello";
+    }
+    return file.getAttribute("test", attribute);
+  }
+
+  @Override
+  public Class<TestAttributeView> viewType() {
+    return TestAttributeView.class;
+  }
+
+  @Override
+  public TestAttributeView view(
+      FileLookup lookup, ImmutableMap<String, FileAttributeView> inheritedViews) {
+    return new View(lookup, (BasicFileAttributeView) inheritedViews.get("basic"));
+  }
+
+  @Override
+  public Class<TestAttributes> attributesType() {
+    return TestAttributes.class;
+  }
+
+  @Override
+  public TestAttributes readAttributes(File file) {
+    return new Attributes(file);
+  }
+
+  static final class View implements TestAttributeView {
+
+    private final FileLookup lookup;
+    private final BasicFileAttributeView basicView;
+
+    public View(FileLookup lookup, BasicFileAttributeView basicView) {
+      this.lookup = checkNotNull(lookup);
+      this.basicView = checkNotNull(basicView);
+    }
+
+    @Override
+    public String name() {
+      return "test";
+    }
+
+    @Override
+    public Attributes readAttributes() throws IOException {
+      return new Attributes(lookup.lookup());
+    }
+
+    @Override
+    public void setTimes(
+        @NullableDecl FileTime lastModifiedTime,
+        @NullableDecl FileTime lastAccessTime,
+        @NullableDecl FileTime createTime)
+        throws IOException {
+      basicView.setTimes(lastModifiedTime, lastAccessTime, createTime);
+    }
+
+    @Override
+    public void setBar(long bar) throws IOException {
+      lookup.lookup().setAttribute("test", "bar", bar);
+    }
+
+    @Override
+    public void setBaz(int baz) throws IOException {
+      lookup.lookup().setAttribute("test", "baz", baz);
+    }
+  }
+
+  static final class Attributes implements TestAttributes {
+
+    private final Long bar;
+    private final Integer baz;
+
+    public Attributes(File file) {
+      this.bar = (Long) file.getAttribute("test", "bar");
+      this.baz = (Integer) file.getAttribute("test", "baz");
+    }
+
+    @Override
+    public String foo() {
+      return "hello";
+    }
+
+    @Override
+    public long bar() {
+      return bar;
+    }
+
+    @Override
+    public int baz() {
+      return baz;
+    }
+
+    // BasicFileAttributes is just implemented here because readAttributes requires a subtype of
+    // BasicFileAttributes -- methods are not implemented
+
+    @Override
+    public FileTime lastModifiedTime() {
+      return null;
+    }
+
+    @Override
+    public FileTime lastAccessTime() {
+      return null;
+    }
+
+    @Override
+    public FileTime creationTime() {
+      return null;
+    }
+
+    @Override
+    public boolean isRegularFile() {
+      return false;
+    }
+
+    @Override
+    public boolean isDirectory() {
+      return false;
+    }
+
+    @Override
+    public boolean isSymbolicLink() {
+      return false;
+    }
+
+    @Override
+    public boolean isOther() {
+      return false;
+    }
+
+    @Override
+    public long size() {
+      return 0;
+    }
+
+    @Override
+    public Object fileKey() {
+      return null;
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/TestAttributeView.java b/jimfs/src/test/java/com/google/common/jimfs/TestAttributeView.java
new file mode 100644
index 0000000..c9c4cd5
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/TestAttributeView.java
@@ -0,0 +1,30 @@
+/*
+ * 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.nio.file.attribute.BasicFileAttributeView;
+
+/** @author Colin Decker */
+public interface TestAttributeView extends BasicFileAttributeView {
+
+  TestAttributes readAttributes() throws IOException;
+
+  void setBar(long bar) throws IOException;
+
+  void setBaz(int baz) throws IOException;
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/TestAttributes.java b/jimfs/src/test/java/com/google/common/jimfs/TestAttributes.java
new file mode 100644
index 0000000..fc66f80
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/TestAttributes.java
@@ -0,0 +1,29 @@
+/*
+ * 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.BasicFileAttributes;
+
+/** @author Colin Decker */
+public interface TestAttributes extends BasicFileAttributes {
+
+  String foo();
+
+  long bar();
+
+  int baz();
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/TestUtils.java b/jimfs/src/test/java/com/google/common/jimfs/TestUtils.java
new file mode 100644
index 0000000..30e0930
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/TestUtils.java
@@ -0,0 +1,144 @@
+/*
+ * 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 java.nio.file.LinkOption.NOFOLLOW_LINKS;
+import static org.junit.Assert.assertFalse;
+
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/** @author Colin Decker */
+public final class TestUtils {
+
+  private TestUtils() {}
+
+  public static byte[] bytes(int... bytes) {
+    byte[] result = new byte[bytes.length];
+    for (int i = 0; i < bytes.length; i++) {
+      result[i] = (byte) bytes[i];
+    }
+    return result;
+  }
+
+  public static byte[] bytes(String bytes) {
+    byte[] result = new byte[bytes.length()];
+    for (int i = 0; i < bytes.length(); i++) {
+      String digit = bytes.substring(i, i + 1);
+      result[i] = Byte.parseByte(digit);
+    }
+    return result;
+  }
+
+  public static byte[] preFilledBytes(int length, int fillValue) {
+    byte[] result = new byte[length];
+    Arrays.fill(result, (byte) fillValue);
+    return result;
+  }
+
+  public static byte[] preFilledBytes(int length) {
+    byte[] bytes = new byte[length];
+    for (int i = 0; i < length; i++) {
+      bytes[i] = (byte) i;
+    }
+    return bytes;
+  }
+
+  public static ByteBuffer buffer(String bytes) {
+    return ByteBuffer.wrap(bytes(bytes));
+  }
+
+  public static Iterable<ByteBuffer> buffers(String... bytes) {
+    List<ByteBuffer> result = new ArrayList<>();
+    for (String b : bytes) {
+      result.add(buffer(b));
+    }
+    return result;
+  }
+
+  /** Returns a number of permutations of the given path that should all locate the same file. */
+  public static Iterable<Path> permutations(Path path) throws IOException {
+    Path workingDir = path.getFileSystem().getPath("").toRealPath();
+    boolean directory = Files.isDirectory(path);
+
+    Set<Path> results = new HashSet<>();
+    results.add(path);
+    if (path.isAbsolute()) {
+      results.add(workingDir.relativize(path));
+    } else {
+      results.add(workingDir.resolve(path));
+    }
+    if (directory) {
+      for (Path p : ImmutableList.copyOf(results)) {
+        results.add(p.resolve("."));
+        results.add(p.resolve(".").resolve("."));
+        Path fileName = p.getFileName();
+        if (fileName != null
+            && !fileName.toString().equals(".")
+            && !fileName.toString().equals("..")) {
+          results.add(p.resolve("..").resolve(fileName));
+          results.add(p.resolve("..").resolve(".").resolve(fileName));
+          results.add(p.resolve("..").resolve(".").resolve(fileName).resolve("."));
+          results.add(p.resolve(".").resolve("..").resolve(".").resolve(fileName));
+        }
+      }
+
+      try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
+        for (Path child : stream) {
+          if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
+            Path childName = child.getFileName();
+            for (Path p : ImmutableList.copyOf(results)) {
+              results.add(p.resolve(childName).resolve(".."));
+              results.add(p.resolve(childName).resolve(".").resolve(".").resolve(".."));
+              results.add(p.resolve(childName).resolve("..").resolve("."));
+              results.add(
+                  p.resolve(childName).resolve("..").resolve(childName).resolve(".").resolve(".."));
+            }
+            break; // no need to add more than one child
+          }
+        }
+      }
+    }
+    return results;
+  }
+
+  // equivalent to the Junit 4.11 method.
+  public static void assertNotEquals(Object unexpected, Object actual) {
+    assertFalse(
+        "Values should be different. Actual: " + actual, Objects.equals(unexpected, actual));
+  }
+
+  static RegularFile regularFile(int size) {
+    RegularFile file = RegularFile.create(0, new HeapDisk(8096, 1000, 1000));
+    try {
+      file.write(0, new byte[size], 0, size);
+      return file;
+    } catch (IOException e) {
+      throw new AssertionError(e);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/UnixAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/UnixAttributeProviderTest.java
new file mode 100644
index 0000000..dc32d20
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/UnixAttributeProviderTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.jimfs.UserLookupService.createGroupPrincipal;
+import static com.google.common.jimfs.UserLookupService.createUserPrincipal;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link UnixAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("OctalInteger")
+public class UnixAttributeProviderTest
+    extends AbstractAttributeProviderTest<UnixAttributeProvider> {
+
+  @Override
+  protected UnixAttributeProvider createProvider() {
+    return new UnixAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of(
+        new BasicAttributeProvider(), new OwnerAttributeProvider(), new PosixAttributeProvider());
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    // unix provider relies on other providers to set their initial attributes
+    file.setAttribute("owner", "owner", createUserPrincipal("foo"));
+    file.setAttribute("posix", "group", createGroupPrincipal("bar"));
+    file.setAttribute(
+        "posix", "permissions", ImmutableSet.copyOf(PosixFilePermissions.fromString("rw-r--r--")));
+
+    // these are pretty much meaningless here since they aren't properties this
+    // file system actually has, so don't really care about the exact value of these
+    assertThat(provider.get(file, "uid")).isInstanceOf(Integer.class);
+    assertThat(provider.get(file, "gid")).isInstanceOf(Integer.class);
+    assertThat(provider.get(file, "rdev")).isEqualTo(0L);
+    assertThat(provider.get(file, "dev")).isEqualTo(1L);
+    assertThat(provider.get(file, "ino")).isInstanceOf(Integer.class);
+
+    // these have logical origins in attributes from other views
+    assertThat(provider.get(file, "mode")).isEqualTo(0644); // rw-r--r--
+    assertThat(provider.get(file, "ctime")).isEqualTo(FileTime.fromMillis(file.getCreationTime()));
+
+    // this is based on a property this file system does actually have
+    assertThat(provider.get(file, "nlink")).isEqualTo(1);
+
+    file.incrementLinkCount();
+    assertThat(provider.get(file, "nlink")).isEqualTo(2);
+    file.decrementLinkCount();
+    assertThat(provider.get(file, "nlink")).isEqualTo(1);
+  }
+
+  @Test
+  public void testSet() {
+    assertSetFails("unix:uid", 1);
+    assertSetFails("unix:gid", 1);
+    assertSetFails("unix:rdev", 1L);
+    assertSetFails("unix:dev", 1L);
+    assertSetFails("unix:ino", 1);
+    assertSetFails("unix:mode", 1);
+    assertSetFails("unix:ctime", 1L);
+    assertSetFails("unix:nlink", 1);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/UnixPathTypeTest.java b/jimfs/src/test/java/com/google/common/jimfs/UnixPathTypeTest.java
new file mode 100644
index 0000000..5bc6cb5
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/UnixPathTypeTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.jimfs.PathTypeTest.assertParseResult;
+import static com.google.common.jimfs.PathTypeTest.assertUriRoundTripsCorrectly;
+import static com.google.common.jimfs.PathTypeTest.fileSystemUri;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import java.net.URI;
+import java.nio.file.InvalidPathException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link UnixPathType}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class UnixPathTypeTest {
+
+  @Test
+  public void testUnix() {
+    PathType unix = PathType.unix();
+    assertThat(unix.getSeparator()).isEqualTo("/");
+    assertThat(unix.getOtherSeparators()).isEqualTo("");
+
+    // "//foo/bar" is what will be passed to parsePath if "/", "foo", "bar" is passed to getPath
+    PathType.ParseResult path = unix.parsePath("//foo/bar");
+    assertParseResult(path, "/", "foo", "bar");
+    assertThat(unix.toString(path.root(), path.names())).isEqualTo("/foo/bar");
+
+    PathType.ParseResult path2 = unix.parsePath("foo/bar/");
+    assertParseResult(path2, null, "foo", "bar");
+    assertThat(unix.toString(path2.root(), path2.names())).isEqualTo("foo/bar");
+  }
+
+  @Test
+  public void testUnix_toUri() {
+    URI fileUri = PathType.unix().toUri(fileSystemUri, "/", ImmutableList.of("foo", "bar"), false);
+    assertThat(fileUri.toString()).isEqualTo("jimfs://foo/foo/bar");
+    assertThat(fileUri.getPath()).isEqualTo("/foo/bar");
+
+    URI directoryUri =
+        PathType.unix().toUri(fileSystemUri, "/", ImmutableList.of("foo", "bar"), true);
+    assertThat(directoryUri.toString()).isEqualTo("jimfs://foo/foo/bar/");
+    assertThat(directoryUri.getPath()).isEqualTo("/foo/bar/");
+
+    URI rootUri = PathType.unix().toUri(fileSystemUri, "/", ImmutableList.<String>of(), true);
+    assertThat(rootUri.toString()).isEqualTo("jimfs://foo/");
+    assertThat(rootUri.getPath()).isEqualTo("/");
+  }
+
+  @Test
+  public void testUnix_toUri_escaping() {
+    URI uri = PathType.unix().toUri(fileSystemUri, "/", ImmutableList.of("foo bar"), false);
+    assertThat(uri.toString()).isEqualTo("jimfs://foo/foo%20bar");
+    assertThat(uri.getRawPath()).isEqualTo("/foo%20bar");
+    assertThat(uri.getPath()).isEqualTo("/foo bar");
+  }
+
+  @Test
+  public void testUnix_uriRoundTrips() {
+    assertUriRoundTripsCorrectly(PathType.unix(), "/");
+    assertUriRoundTripsCorrectly(PathType.unix(), "/foo");
+    assertUriRoundTripsCorrectly(PathType.unix(), "/foo/bar/baz");
+    assertUriRoundTripsCorrectly(PathType.unix(), "/foo/bar baz/one/two");
+    assertUriRoundTripsCorrectly(PathType.unix(), "/foo bar");
+    assertUriRoundTripsCorrectly(PathType.unix(), "/foo bar/");
+    assertUriRoundTripsCorrectly(PathType.unix(), "/foo bar/baz/one");
+  }
+
+  @Test
+  public void testUnix_illegalCharacters() {
+    try {
+      PathType.unix().parsePath("/foo/bar\0");
+      fail();
+    } catch (InvalidPathException expected) {
+      assertEquals(8, expected.getIndex());
+    }
+
+    try {
+      PathType.unix().parsePath("/\u00001/foo");
+      fail();
+    } catch (InvalidPathException expected) {
+      assertEquals(1, expected.getIndex());
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/UrlTest.java b/jimfs/src/test/java/com/google/common/jimfs/UrlTest.java
new file mode 100644
index 0000000..f724c7f
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/UrlTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.StandardSystemProperty.LINE_SEPARATOR;
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+import com.google.common.io.Resources;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests that {@link URL} instances can be created and used from jimfs URIs.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class UrlTest {
+
+  private final FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+  private Path path = fs.getPath("foo");
+
+  @Test
+  public void creatUrl() throws MalformedURLException {
+    URL url = path.toUri().toURL();
+    assertThat(url).isNotNull();
+  }
+
+  @Test
+  public void readFromUrl() throws IOException {
+    Files.write(path, ImmutableList.of("Hello World"), UTF_8);
+
+    URL url = path.toUri().toURL();
+    assertThat(Resources.asCharSource(url, UTF_8).read())
+        .isEqualTo("Hello World" + LINE_SEPARATOR.value());
+  }
+
+  @Test
+  public void readDirectoryContents() throws IOException {
+    Files.createDirectory(path);
+    Files.createFile(path.resolve("a.txt"));
+    Files.createFile(path.resolve("b.txt"));
+    Files.createDirectory(path.resolve("c"));
+
+    URL url = path.toUri().toURL();
+    assertThat(Resources.asCharSource(url, UTF_8).read()).isEqualTo("a.txt\nb.txt\nc\n");
+  }
+
+  @Test
+  public void headers() throws IOException {
+    byte[] bytes = {1, 2, 3};
+    Files.write(path, bytes);
+    FileTime lastModified = Files.getLastModifiedTime(path);
+
+    URL url = path.toUri().toURL();
+    URLConnection conn = url.openConnection();
+
+    // read header fields directly
+    assertThat(conn.getHeaderFields()).containsEntry("content-length", ImmutableList.of("3"));
+    assertThat(conn.getHeaderFields())
+        .containsEntry("content-type", ImmutableList.of("application/octet-stream"));
+
+    if (lastModified != null) {
+      assertThat(conn.getHeaderFields()).containsKey("last-modified");
+      assertThat(conn.getHeaderFields()).hasSize(3);
+    } else {
+      assertThat(conn.getHeaderFields()).hasSize(2);
+    }
+
+    // use the specific methods for reading the expected headers
+    assertThat(conn.getContentLengthLong()).isEqualTo(Files.size(path));
+    assertThat(conn.getContentType()).isEqualTo("application/octet-stream");
+
+    if (lastModified != null) {
+      // The HTTP date format does not include milliseconds, which means that the last modified time
+      // returned from the connection may not be exactly the same as that of the file system itself.
+      // The difference should less than 1000ms though, and should never be greater.
+      long difference = lastModified.toMillis() - conn.getLastModified();
+      assertThat(difference).isIn(Range.closedOpen(0L, 1000L));
+    } else {
+      assertThat(conn.getLastModified()).isEqualTo(0L);
+    }
+  }
+
+  @Test
+  public void contentType() throws IOException {
+    path = fs.getPath("foo.txt");
+    Files.write(path, ImmutableList.of("Hello World"), UTF_8);
+
+    URL url = path.toUri().toURL();
+    URLConnection conn = url.openConnection();
+
+    // Should be text/plain, but this is entirely dependent on the installed FileTypeDetectors
+    String detectedContentType = Files.probeContentType(path);
+    if (detectedContentType == null) {
+      assertThat(conn.getContentType()).isEqualTo("application/octet-stream");
+    } else {
+      assertThat(conn.getContentType()).isEqualTo(detectedContentType);
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/UserDefinedAttributeProviderTest.java b/jimfs/src/test/java/com/google/common/jimfs/UserDefinedAttributeProviderTest.java
new file mode 100644
index 0000000..67a95a8
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/UserDefinedAttributeProviderTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.attribute.UserDefinedFileAttributeView;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link UserDefinedAttributeProvider}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class UserDefinedAttributeProviderTest
+    extends AbstractAttributeProviderTest<UserDefinedAttributeProvider> {
+
+  @Override
+  protected UserDefinedAttributeProvider createProvider() {
+    return new UserDefinedAttributeProvider();
+  }
+
+  @Override
+  protected Set<? extends AttributeProvider> createInheritedProviders() {
+    return ImmutableSet.of();
+  }
+
+  @Test
+  public void testInitialAttributes() {
+    // no initial attributes
+    assertThat(ImmutableList.copyOf(file.getAttributeKeys())).isEmpty();
+    assertThat(provider.attributes(file)).isEmpty();
+  }
+
+  @Test
+  public void testGettingAndSetting() {
+    byte[] bytes = {0, 1, 2, 3};
+    provider.set(file, "user", "one", bytes, false);
+    provider.set(file, "user", "two", ByteBuffer.wrap(bytes), false);
+
+    byte[] one = (byte[]) provider.get(file, "one");
+    byte[] two = (byte[]) provider.get(file, "two");
+    assertThat(Arrays.equals(one, bytes)).isTrue();
+    assertThat(Arrays.equals(two, bytes)).isTrue();
+
+    assertSetFails("foo", "hello");
+
+    assertThat(provider.attributes(file)).containsExactly("one", "two");
+  }
+
+  @Test
+  public void testSetOnCreate() {
+    assertSetFailsOnCreate("anything", new byte[0]);
+  }
+
+  @Test
+  public void testView() throws IOException {
+    UserDefinedFileAttributeView view = provider.view(fileLookup(), NO_INHERITED_VIEWS);
+    assertNotNull(view);
+
+    assertThat(view.name()).isEqualTo("user");
+    assertThat(view.list()).isEmpty();
+
+    byte[] b1 = {0, 1, 2};
+    byte[] b2 = {0, 1, 2, 3, 4};
+
+    view.write("b1", ByteBuffer.wrap(b1));
+    view.write("b2", ByteBuffer.wrap(b2));
+
+    assertThat(view.list()).containsAtLeast("b1", "b2");
+    assertThat(file.getAttributeKeys()).containsExactly("user:b1", "user:b2");
+
+    assertThat(view.size("b1")).isEqualTo(3);
+    assertThat(view.size("b2")).isEqualTo(5);
+
+    ByteBuffer buf1 = ByteBuffer.allocate(view.size("b1"));
+    ByteBuffer buf2 = ByteBuffer.allocate(view.size("b2"));
+
+    view.read("b1", buf1);
+    view.read("b2", buf2);
+
+    assertThat(Arrays.equals(b1, buf1.array())).isTrue();
+    assertThat(Arrays.equals(b2, buf2.array())).isTrue();
+
+    view.delete("b2");
+
+    assertThat(view.list()).containsExactly("b1");
+    assertThat(file.getAttributeKeys()).containsExactly("user:b1");
+
+    try {
+      view.size("b2");
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("not set");
+    }
+
+    try {
+      view.read("b2", ByteBuffer.allocate(10));
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected.getMessage()).contains("not set");
+    }
+
+    view.write("b1", ByteBuffer.wrap(b2));
+    assertThat(view.size("b1")).isEqualTo(5);
+
+    view.delete("b2"); // succeeds
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/UserLookupServiceTest.java b/jimfs/src/test/java/com/google/common/jimfs/UserLookupServiceTest.java
new file mode 100644
index 0000000..04594ca
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/UserLookupServiceTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+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;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link UserLookupService}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class UserLookupServiceTest {
+
+  @Test
+  public void testUserLookupService() throws IOException {
+    UserPrincipalLookupService service = new UserLookupService(true);
+    UserPrincipal bob1 = service.lookupPrincipalByName("bob");
+    UserPrincipal bob2 = service.lookupPrincipalByName("bob");
+    UserPrincipal alice = service.lookupPrincipalByName("alice");
+
+    assertThat(bob1).isEqualTo(bob2);
+    assertThat(bob1).isNotEqualTo(alice);
+
+    GroupPrincipal group1 = service.lookupPrincipalByGroupName("group");
+    GroupPrincipal group2 = service.lookupPrincipalByGroupName("group");
+    GroupPrincipal foo = service.lookupPrincipalByGroupName("foo");
+
+    assertThat(group1).isEqualTo(group2);
+    assertThat(group1).isNotEqualTo(foo);
+  }
+
+  @Test
+  public void testServiceNotSupportingGroups() throws IOException {
+    UserPrincipalLookupService service = new UserLookupService(false);
+
+    try {
+      service.lookupPrincipalByGroupName("group");
+      fail();
+    } catch (UserPrincipalNotFoundException expected) {
+      assertThat(expected.getName()).isEqualTo("group");
+    }
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java b/jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java
new file mode 100644
index 0000000..7a98a7d
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.io.IOException;
+import java.nio.file.WatchService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link WatchServiceConfiguration}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class WatchServiceConfigurationTest {
+
+  private JimfsFileSystem fs;
+
+  @Before
+  public void setUp() {
+    // kind of putting the cart before the horse maybe, but it's the easiest way to get valid
+    // instances of both a FileSystemView and a PathService
+    fs = (JimfsFileSystem) Jimfs.newFileSystem();
+  }
+
+  @After
+  public void tearDown() throws IOException {
+    fs.close();
+    fs = null;
+  }
+
+  @Test
+  public void testPollingConfig() {
+    WatchServiceConfiguration polling = WatchServiceConfiguration.polling(50, MILLISECONDS);
+    WatchService watchService = polling.newWatchService(fs.getDefaultView(), fs.getPathService());
+    assertThat(watchService).isInstanceOf(PollingWatchService.class);
+
+    PollingWatchService pollingWatchService = (PollingWatchService) watchService;
+    assertThat(pollingWatchService.interval).isEqualTo(50);
+    assertThat(pollingWatchService.timeUnit).isEqualTo(MILLISECONDS);
+  }
+
+  @Test
+  public void testDefaultConfig() {
+    WatchService watchService =
+        WatchServiceConfiguration.DEFAULT.newWatchService(fs.getDefaultView(), fs.getPathService());
+    assertThat(watchService).isInstanceOf(PollingWatchService.class);
+
+    PollingWatchService pollingWatchService = (PollingWatchService) watchService;
+    assertThat(pollingWatchService.interval).isEqualTo(5);
+    assertThat(pollingWatchService.timeUnit).isEqualTo(SECONDS);
+  }
+}
diff --git a/jimfs/src/test/java/com/google/common/jimfs/WindowsPathTypeTest.java b/jimfs/src/test/java/com/google/common/jimfs/WindowsPathTypeTest.java
new file mode 100644
index 0000000..2da1280
--- /dev/null
+++ b/jimfs/src/test/java/com/google/common/jimfs/WindowsPathTypeTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.jimfs.PathType.windows;
+import static com.google.common.jimfs.PathTypeTest.assertParseResult;
+import static com.google.common.jimfs.PathTypeTest.assertUriRoundTripsCorrectly;
+import static com.google.common.jimfs.PathTypeTest.fileSystemUri;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import java.net.URI;
+import java.nio.file.InvalidPathException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link WindowsPathType}.
+ *
+ * @author Colin Decker
+ */
+@RunWith(JUnit4.class)
+public class WindowsPathTypeTest {
+
+  @Test
+  public void testWindows() {
+    PathType windows = PathType.windows();
+    assertThat(windows.getSeparator()).isEqualTo("\\");
+    assertThat(windows.getOtherSeparators()).isEqualTo("/");
+
+    // "C:\\foo\bar" results from "C:\", "foo", "bar" passed to getPath
+    PathType.ParseResult path = windows.parsePath("C:\\\\foo\\bar");
+    assertParseResult(path, "C:\\", "foo", "bar");
+    assertThat(windows.toString(path.root(), path.names())).isEqualTo("C:\\foo\\bar");
+
+    PathType.ParseResult path2 = windows.parsePath("foo/bar/");
+    assertParseResult(path2, null, "foo", "bar");
+    assertThat(windows.toString(path2.root(), path2.names())).isEqualTo("foo\\bar");
+
+    PathType.ParseResult path3 = windows.parsePath("hello world/foo/bar");
+    assertParseResult(path3, null, "hello world", "foo", "bar");
+    assertThat(windows.toString(null, path3.names())).isEqualTo("hello world\\foo\\bar");
+  }
+
+  @Test
+  public void testWindows_relativePathsWithDriveRoot_unsupported() {
+    try {
+      windows().parsePath("C:");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      windows().parsePath("C:foo\\bar");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+  }
+
+  @Test
+  public void testWindows_absolutePathOnCurrentDrive_unsupported() {
+    try {
+      windows().parsePath("\\foo\\bar");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      windows().parsePath("\\");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+  }
+
+  @Test
+  public void testWindows_uncPaths() {
+    PathType windows = PathType.windows();
+    PathType.ParseResult path = windows.parsePath("\\\\host\\share");
+    assertParseResult(path, "\\\\host\\share\\");
+
+    path = windows.parsePath("\\\\HOST\\share\\foo\\bar");
+    assertParseResult(path, "\\\\HOST\\share\\", "foo", "bar");
+
+    try {
+      windows.parsePath("\\\\");
+      fail();
+    } catch (InvalidPathException expected) {
+      assertThat(expected.getInput()).isEqualTo("\\\\");
+      assertThat(expected.getReason()).isEqualTo("UNC path is missing hostname");
+    }
+
+    try {
+      windows.parsePath("\\\\host");
+      fail();
+    } catch (InvalidPathException expected) {
+      assertThat(expected.getInput()).isEqualTo("\\\\host");
+      assertThat(expected.getReason()).isEqualTo("UNC path is missing sharename");
+    }
+
+    try {
+      windows.parsePath("\\\\host\\");
+      fail();
+    } catch (InvalidPathException expected) {
+      assertThat(expected.getInput()).isEqualTo("\\\\host\\");
+      assertThat(expected.getReason()).isEqualTo("UNC path is missing sharename");
+    }
+
+    try {
+      windows.parsePath("//host");
+      fail();
+    } catch (InvalidPathException expected) {
+      assertThat(expected.getInput()).isEqualTo("//host");
+      assertThat(expected.getReason()).isEqualTo("UNC path is missing sharename");
+    }
+  }
+
+  @Test
+  public void testWindows_illegalNames() {
+    try {
+      windows().parsePath("foo<bar");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      windows().parsePath("foo?");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      windows().parsePath("foo ");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+
+    try {
+      windows().parsePath("foo \\bar");
+      fail();
+    } catch (InvalidPathException expected) {
+    }
+  }
+
+  @Test
+  public void testWindows_toUri_normal() {
+    URI fileUri =
+        PathType.windows().toUri(fileSystemUri, "C:\\", ImmutableList.of("foo", "bar"), false);
+    assertThat(fileUri.toString()).isEqualTo("jimfs://foo/C:/foo/bar");
+    assertThat(fileUri.getPath()).isEqualTo("/C:/foo/bar");
+
+    URI directoryUri =
+        PathType.windows().toUri(fileSystemUri, "C:\\", ImmutableList.of("foo", "bar"), true);
+    assertThat(directoryUri.toString()).isEqualTo("jimfs://foo/C:/foo/bar/");
+    assertThat(directoryUri.getPath()).isEqualTo("/C:/foo/bar/");
+
+    URI rootUri = PathType.windows().toUri(fileSystemUri, "C:\\", ImmutableList.<String>of(), true);
+    assertThat(rootUri.toString()).isEqualTo("jimfs://foo/C:/");
+    assertThat(rootUri.getPath()).isEqualTo("/C:/");
+  }
+
+  @Test
+  public void testWindows_toUri_unc() {
+    URI fileUri =
+        PathType.windows()
+            .toUri(fileSystemUri, "\\\\host\\share\\", ImmutableList.of("foo", "bar"), false);
+    assertThat(fileUri.toString()).isEqualTo("jimfs://foo//host/share/foo/bar");
+    assertThat(fileUri.getPath()).isEqualTo("//host/share/foo/bar");
+
+    URI rootUri =
+        PathType.windows()
+            .toUri(fileSystemUri, "\\\\host\\share\\", ImmutableList.<String>of(), true);
+    assertThat(rootUri.toString()).isEqualTo("jimfs://foo//host/share/");
+    assertThat(rootUri.getPath()).isEqualTo("//host/share/");
+  }
+
+  @Test
+  public void testWindows_toUri_escaping() {
+    URI uri =
+        PathType.windows()
+            .toUri(fileSystemUri, "C:\\", ImmutableList.of("Users", "foo", "My Documents"), true);
+    assertThat(uri.toString()).isEqualTo("jimfs://foo/C:/Users/foo/My%20Documents/");
+    assertThat(uri.getRawPath()).isEqualTo("/C:/Users/foo/My%20Documents/");
+    assertThat(uri.getPath()).isEqualTo("/C:/Users/foo/My Documents/");
+  }
+
+  @Test
+  public void testWindows_uriRoundTrips_normal() {
+    assertUriRoundTripsCorrectly(PathType.windows(), "C:\\");
+    assertUriRoundTripsCorrectly(PathType.windows(), "C:\\foo");
+    assertUriRoundTripsCorrectly(PathType.windows(), "C:\\foo\\bar\\baz");
+    assertUriRoundTripsCorrectly(PathType.windows(), "C:\\Users\\foo\\My Documents\\");
+    assertUriRoundTripsCorrectly(PathType.windows(), "C:\\foo bar");
+    assertUriRoundTripsCorrectly(PathType.windows(), "C:\\foo bar\\baz");
+  }
+
+  @Test
+  public void testWindows_uriRoundTrips_unc() {
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share");
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share\\");
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share\\foo");
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share\\foo\\bar\\baz");
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share\\Users\\foo\\My Documents\\");
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share\\foo bar");
+    assertUriRoundTripsCorrectly(PathType.windows(), "\\\\host\\share\\foo bar\\baz");
+  }
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..d35a4c3
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,344 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.sonatype.oss</groupId>
+    <artifactId>oss-parent</artifactId>
+    <version>7</version>
+  </parent>
+
+  <groupId>com.google.jimfs</groupId>
+  <artifactId>jimfs-parent</artifactId>
+  <packaging>pom</packaging>
+  <version>HEAD-SNAPSHOT</version>
+
+  <modules>
+    <module>jimfs</module>
+  </modules>
+
+  <name>Jimfs Parent</name>
+
+  <description>
+    Jimfs is an in-memory implementation of Java 7's java.nio.file abstract file system API.
+  </description>
+
+  <url>https://github.com/google/jimfs</url>
+
+  <inceptionYear>2013</inceptionYear>
+
+  <organization>
+    <name>Google Inc.</name>
+    <url>http://www.google.com/</url>
+  </organization>
+
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+
+  <developers>
+    <developer>
+      <id>cgdecker</id>
+      <name>Colin Decker</name>
+      <email>cgdecker@google.com</email>
+      <organization>Google Inc.</organization>
+      <organizationUrl>http://www.google.com/</organizationUrl>
+      <roles>
+        <role>owner</role>
+        <role>developer</role>
+      </roles>
+      <timezone>-5</timezone>
+    </developer>
+  </developers>
+
+  <scm>
+    <url>http://github.com/google/jimfs/</url>
+    <connection>scm:git:git://github.com/google/jimfs.git</connection>
+    <developerConnection>scm:git:ssh://git@github.com/google/jimfs.git</developerConnection>
+    <tag>HEAD</tag>
+  </scm>
+
+  <issueManagement>
+    <system>GitHub Issues</system>
+    <url>http://github.com/google/jimfs/issues</url>
+  </issueManagement>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <auto-service.version>1.0-rc6</auto-service.version>
+    <java.version>1.7</java.version>
+    <guava.version>27.0.1-android</guava.version>
+    <surefire.version>3.0.0-M3</surefire.version>
+    <!--
+      NOTE: When updating errorprone.version, also update javac.version to the
+      version used by the new error-prone version. You should be able to find
+      it in the properties section of
+      https://github.com/google/error-prone/blob/v${errorprone.version}/pom.xml
+      -->
+    <errorprone.version>2.3.3</errorprone.version>
+    <javac.version>9+181-r4173-1</javac.version>
+  </properties>
+
+  <dependencyManagement>
+    <dependencies>
+      <!-- Required runtime dependencies -->
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>${guava.version}</version>
+      </dependency>
+
+      <!-- Optional runtime dependencies -->
+      <dependency>
+        <groupId>com.ibm.icu</groupId>
+        <artifactId>icu4j</artifactId>
+        <version>65.1</version>
+      </dependency>
+
+      <!-- Compile-time dependencies -->
+      <dependency>
+        <groupId>com.google.auto.service</groupId>
+        <artifactId>auto-service-annotations</artifactId>
+        <version>${auto-service.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.code.findbugs</groupId>
+        <artifactId>jsr305</artifactId>
+        <version>3.0.2</version>
+      </dependency>
+      <dependency>
+        <groupId>org.checkerframework</groupId>
+        <artifactId>checker-compat-qual</artifactId>
+        <version>2.5.5</version>
+      </dependency>
+
+      <!-- Test dependencies -->
+      <dependency>
+        <groupId>junit</groupId>
+        <artifactId>junit</artifactId>
+        <version>4.12</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava-testlib</artifactId>
+        <version>${guava.version}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.google.truth</groupId>
+        <artifactId>truth</artifactId>
+        <version>0.45</version>
+        <scope>test</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.8.1</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>3.0.1</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>3.1.1</version>
+          <configuration>
+            <debug>true</debug>
+            <encoding>UTF-8</encoding>
+            <docencoding>UTF-8</docencoding>
+            <charset>UTF-8</charset>
+            <detectJavaApiLink>false</detectJavaApiLink>
+            <links>
+              <link>https://checkerframework.org/api/</link>
+              <link>https://guava.dev/releases/${guava.version}/api/docs/</link>
+              <link>https://unicode-org.github.io/icu-docs/apidoc/released/icu4j</link>
+              <!-- When building against Java 8, the Java 11 link below is overridden to point to an older version (Java 9, the newest one that works). -->
+              <link>https://docs.oracle.com/en/java/javase/11/docs/api/</link>
+            </links>
+          </configuration>
+        </plugin>
+        <plugin>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>1.6</version>
+        </plugin>
+        <plugin>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>${surefire.version}</version>
+          <!-- For some reason, we need this for our internal tests that run in offline mode: -->
+          <dependencies>
+            <dependency>
+              <groupId>org.apache.maven.surefire</groupId>
+              <artifactId>surefire-junit4</artifactId>
+              <version>${surefire.version}</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.felix</groupId>
+          <artifactId>maven-bundle-plugin</artifactId>
+          <version>3.5.0</version>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>${java.version}</source>
+          <target>${java.version}</target>
+          <compilerArgs>
+            <arg>-XDcompilePolicy=simple</arg>
+            <arg>-Xplugin:ErrorProne</arg>
+          </compilerArgs>
+          <annotationProcessorPaths>
+            <path>
+              <groupId>com.google.errorprone</groupId>
+              <artifactId>error_prone_core</artifactId>
+              <version>${errorprone.version}</version>
+            </path>
+            <path>
+              <groupId>com.google.guava</groupId>
+              <artifactId>guava-beta-checker</artifactId>
+              <version>1.0</version>
+            </path>
+            <path>
+              <groupId>com.google.auto.service</groupId>
+              <artifactId>auto-service</artifactId>
+              <version>${auto-service.version}</version>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+        <executions>
+          <execution>
+            <id>default-testCompile</id>
+            <phase>test-compile</phase>
+            <goals>
+              <goal>testCompile</goal>
+            </goals>
+            <configuration>
+              <compilerArgs>
+                <arg>-XDcompilePolicy=simple</arg>
+                <arg>-Xplugin:ErrorProne -Xep:BetaApi:OFF</arg> <!-- Disable Beta Checker for tests -->
+              </compilerArgs>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>jdk8plus</id>
+      <activation>
+        <jdk>[1.8,)</jdk>
+      </activation>
+      <!-- Disable HTML checking in doclint under JDK 8 and higher -->
+      <reporting>
+        <plugins>
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <additionalOptions>
+                <additionalOption>-Xdoclint:none</additionalOption>
+              </additionalOptions>
+            </configuration>
+          </plugin>
+        </plugins>
+      </reporting>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <additionalOptions>
+                <additionalOption>-Xdoclint:none</additionalOption>
+              </additionalOptions>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <!-- https://errorprone.info/docs/installation#maven -->
+    <!-- using github.com/google/error-prone-javac is required when running on JDK 8 -->
+    <profile>
+      <id>jdk8exactly</id>
+      <activation>
+        <jdk>1.8</jdk>
+      </activation>
+      <reporting>
+        <plugins>
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <links>
+                <link>https://checkerframework.org/api/</link>
+                <link>https://guava.dev/releases/${guava.version}/api/docs/</link>
+                <link>https://unicode-org.github.io/icu-docs/apidoc/released/icu4j</link>
+                <link>https://docs.oracle.com/javase/9/docs/api/</link>
+              </links>
+            </configuration>
+          </plugin>
+        </plugins>
+      </reporting>
+      <build>
+        <plugins>
+          <!-- https://errorprone.info/docs/installation#maven -->
+          <!-- using github.com/google/error-prone-javac is required when running on JDK 8 -->
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <fork>true</fork>
+              <compilerArgs combine.children="append">
+                <arg>-J-Xbootclasspath/p:${settings.localRepository}/com/google/errorprone/javac/${javac.version}/javac-${javac.version}.jar</arg>
+              </compilerArgs>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <configuration>
+              <links>
+                <link>https://checkerframework.org/api/</link>
+                <link>https://guava.dev/releases/${guava.version}/api/docs/</link>
+                <link>https://unicode-org.github.io/icu-docs/apidoc/released/icu4j</link>
+                <link>https://docs.oracle.com/javase/9/docs/api/</link>
+              </links>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+</project>
diff --git a/util/deploy_snapshot.sh b/util/deploy_snapshot.sh
new file mode 100755
index 0000000..2a2db4c
--- /dev/null
+++ b/util/deploy_snapshot.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# see https://coderwall.com/p/9b_lfq
+
+set -e -u
+
+if [ "$TRAVIS_REPO_SLUG" == "google/jimfs" ] && \
+   [ "$TRAVIS_JDK_VERSION" == "oraclejdk7" ] && \
+   [ "$TRAVIS_PULL_REQUEST" == "false" ] && \
+   [ "$TRAVIS_BRANCH" == "master" ]; then
+  echo "Publishing Maven snapshot..."
+
+  mvn clean source:jar javadoc:jar deploy --settings="util/settings.xml" -DskipTests=true
+
+  echo "Maven snapshot published."
+fi
diff --git a/util/settings.xml b/util/settings.xml
new file mode 100644
index 0000000..306d14a
--- /dev/null
+++ b/util/settings.xml
@@ -0,0 +1,11 @@
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
+  <servers>
+    <server>
+      <id>sonatype-nexus-snapshots</id>
+      <username>${env.CI_DEPLOY_USERNAME}</username>
+      <password>${env.CI_DEPLOY_PASSWORD}</password>
+    </server>
+  </servers>
+</settings>
diff --git a/util/update_snapshot_docs.sh b/util/update_snapshot_docs.sh
new file mode 100755
index 0000000..aae5fa0
--- /dev/null
+++ b/util/update_snapshot_docs.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# see http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ for details
+
+set -e -u
+
+if [ "$TRAVIS_REPO_SLUG" == "google/jimfs" ] && \
+   [ "$TRAVIS_JDK_VERSION" == "oraclejdk7" ] && \
+   [ "$TRAVIS_PULL_REQUEST" == "false" ] && \
+   [ "$TRAVIS_BRANCH" == "master" ]; then
+  echo "Publishing Javadoc and JDiff..."
+
+  cd $HOME
+  git clone -q -b gh-pages https://${GH_TOKEN}@github.com/google/jimfs gh-pages > /dev/null
+  cd gh-pages
+
+  git config --global user.email "travis@travis-ci.org"
+  git config --global user.name "travis-ci"
+
+  ./updaterelease.sh snapshot
+
+  git push -fq origin gh-pages > /dev/null
+
+  echo "Javadoc published to gh-pages."
+fi