diff options
author | Yuexi Ma <yuexima@google.com> | 2020-03-04 18:43:18 -0800 |
---|---|---|
committer | Yuexi Ma <yuexima@google.com> | 2020-03-05 03:04:14 +0000 |
commit | cef92d673c81daab4b0dad931591947c86e0dc8a (patch) | |
tree | f8cbc802a806455c5fdfeab7f696c63641b9f9f5 | |
parent | 68591711a9034281d5fe11fc7a30e535bbce125c (diff) | |
parent | 93a6c6782a9fdf1365face2461876b8644b2a404 (diff) | |
download | jimfs-cef92d673c81daab4b0dad931591947c86e0dc8a.tar.gz |
Initial merge with upstreamandroid-r-preview-4android-r-preview-3android-r-preview-2
Test: n/a
Bug: 150784654
Change-Id: I6fb223f1bd657a6a3d0be1492f63a7774e21943e
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 + @@ -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 @@ -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"); + } +} @@ -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 |