aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-03-05 05:12:32 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-03-05 05:12:32 +0000
commit6f35239027aa2efad5fbbfe41ecad0923003112b (patch)
treef8cbc802a806455c5fdfeab7f696c63641b9f9f5
parent68591711a9034281d5fe11fc7a30e535bbce125c (diff)
parent089373269bfaa2212f2008ac7bda148cdd6945c9 (diff)
downloadjimfs-6f35239027aa2efad5fbbfe41ecad0923003112b.tar.gz
Initial merge with upstream am: cef92d673c am: c5f71e95df am: e50b3b7561 am: c47b5481de am: 089373269b
Change-Id: I00caf048ac2f81928690feb6ce0c26274339e84a
-rw-r--r--.gitignore12
-rw-r--r--.travis.yml31
-rw-r--r--CONTRIBUTING.md15
-rw-r--r--LICENSE202
-rw-r--r--METADATA22
-rw-r--r--MODULE_LICENSE_APACHE20
l---------NOTICE1
-rw-r--r--README.md86
-rw-r--r--jimfs/pom.xml130
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AbstractAttributeView.java41
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AbstractWatchService.java305
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AclAttributeProvider.java155
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AttributeCopyOption.java31
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AttributeProvider.java179
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/AttributeService.java423
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/BasicAttributeProvider.java239
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Configuration.java700
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Directory.java353
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DirectoryEntry.java167
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DosAttributeProvider.java200
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DowngradedDirectoryStream.java50
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/DowngradedSeekableByteChannel.java81
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Feature.java105
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/File.java280
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileFactory.java121
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileLookup.java34
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileSystemState.java129
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileSystemView.java737
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/FileTree.java221
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/GlobToRegex.java400
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/GuardedBy.java36
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Handler.java92
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/HeapDisk.java145
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/InternalCharMatcher.java42
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Jimfs.java245
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsAsynchronousFileChannel.java231
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileChannel.java675
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileStore.java267
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java337
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystemProvider.java355
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java132
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsInputStream.java158
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsOutputStream.java116
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsPath.java445
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/JimfsSecureDirectoryStream.java217
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Name.java127
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Options.java149
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/OwnerAttributeProvider.java123
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathMatchers.java96
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathNormalization.java133
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathService.java269
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathType.java243
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PathURLConnection.java147
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java250
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/PosixAttributeProvider.java270
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/RegularFile.java661
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/StandardAttributeProviders.java58
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/SymbolicLink.java49
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/SystemJimfsFileSystemProvider.java277
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UnixAttributeProvider.java170
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UnixFileAttributeView.java26
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UnixPathType.java85
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UserDefinedAttributeProvider.java162
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/UserLookupService.java114
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/Util.java111
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java76
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/WindowsPathType.java208
-rw-r--r--jimfs/src/main/java/com/google/common/jimfs/package-info.java25
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AbstractAttributeProviderTest.java133
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AbstractGlobMatcherTest.java154
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AbstractJimfsIntegrationTest.java115
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AbstractPathMatcherTest.java259
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AbstractWatchServiceTest.java253
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AclAttributeProviderTest.java119
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/AttributeServiceTest.java391
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/BasicAttributeProviderTest.java151
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/BasicFileAttribute.java43
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/ByteBufferChannel.java98
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/ClassLoaderTest.java120
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java366
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/DirectoryTest.java383
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/DosAttributeProviderTest.java123
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/FileFactoryTest.java74
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/FileSystemStateTest.java178
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/FileTest.java116
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/FileTreeTest.java463
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/HeapDiskTest.java229
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsAsynchronousFileChannelTest.java262
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsFileChannelTest.java1049
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsFileSystemCloseTest.java438
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsInputStreamTest.java240
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsOutputStreamTest.java205
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsPathTest.java385
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsUnixLikeFileSystemTest.java2401
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/JimfsWindowsLikeFileSystemTest.java491
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/NameTest.java41
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/OwnerAttributeProviderTest.java75
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PathNormalizationTest.java351
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PathServiceTest.java264
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PathSubject.java406
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PathTester.java187
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PathTypeTest.java157
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java247
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/PosixAttributeProviderTest.java124
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/RegexGlobMatcherTest.java101
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/RegularFileBlocksTest.java145
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/RegularFileTest.java892
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/TestAttributeProvider.java227
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/TestAttributeView.java30
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/TestAttributes.java29
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/TestUtils.java144
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/UnixAttributeProviderTest.java92
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/UnixPathTypeTest.java108
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/UrlTest.java127
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/UserDefinedAttributeProviderTest.java134
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/UserLookupServiceTest.java68
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java75
-rw-r--r--jimfs/src/test/java/com/google/common/jimfs/WindowsPathTypeTest.java222
-rw-r--r--pom.xml344
-rwxr-xr-xutil/deploy_snapshot.sh16
-rw-r--r--util/settings.xml11
-rwxr-xr-xutil/update_snapshot_docs.sh25
122 files changed, 26653 insertions, 0 deletions
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