1

Other questions in this forum ask how to make a file not readable in Java. This question is different. The other questions I have found point out how to lock a file from being read, but Files.isReadable(Path) still returns true!

Using Java 17 on Windows 10 I have a method that has a contract to do (or not do) certain things if Files.isReadable() returns false for a certain path. I need to test that method, so I need to artificially create a file or directory (preferably one of each) for which Files.isReadable() returns false.

I have tried creating an exclusive lock on the file:

Path lockedFile = writeString(directory.resolve("locked.txt"), "locked");
try (final FileChannel channel = FileChannel.open(lockedFile, StandardOpenOption.APPEND);
    FileLock lock = channel.lock()) {
  System.out.println("is locked file readable? " + isReadable(lockedFile));
}

Yet isReadable() still returns true.

I have tried using File.setReadable():

Path lockedFile = writeString(directory.resolve("locked.txt"), "locked");
lockedFile.toFile().setReadable(false);
System.out.println("is locked file readable? " + isReadable(lockedFile));

Still Files.isReadable() returns true.

What can I do to a file to make isReadable() return false? (Something Windows-specific would be acceptable, as I could set up my unit test to run only on Windows, but at least I would be able to test the method.)

Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • lockedFile.toFile().setReadable(false) return true if you have access permission to lock this files readability. Can you check what is that returning ? seems like an access Exception or Security Exception due to read-write permission on Windows – VMi Aug 01 '22 at 22:06
  • It works for me. I'm not sure what locking the file has to do with anything here. – President James K. Polk Aug 01 '22 at 22:08
  • `lockedFile.toFile().setReadable(false)` returns `false`. This is inside a unit test on Windows 10 for which JUnit 5 is passing me a `@TempDir`. – Garret Wilson Aug 01 '22 at 22:08
  • Mr. Polk, https://www.baeldung.com/java-lock-files indicates that `fileChannel.lock()` will create an exclusive lock, and also states that, "… an exclusive lock prevents all other operations – including reads – while a write operation completes." Naturally I was hopeful that preventing reads would make `Files.isReadable()` return `false`, but that does not seem to be the case. – Garret Wilson Aug 01 '22 at 22:10
  • https://stackoverflow.com/questions/41935829/can-not-change-permission-of-folder-through-java-code..FYI – VMi Aug 01 '22 at 22:13
  • VMi, so it says I cannot change certain permissions of the file from Java. OK. The question remains: how can I get `Files.isReadable()` to return `false`? I don't even mind mocking if there is a way around it. Hmmm … let me try Mockito static mocking. – Garret Wilson Aug 01 '22 at 22:18
  • Unbelievably I can use static mocking on `Files` to get `Files.isReadable()` to return `false` just for one file. But it involves mocking lots of `Files` methods and makes the test brittle because it depends so much on which other `Files` methods are being called. Any better solutions? – Garret Wilson Aug 01 '22 at 22:37
  • 1
    This worked for me in Windows 10: `AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class); view.setAcl(Collections.emptyList());` – VGR Aug 01 '22 at 22:55
  • Ooh, VGR, the `getFileAttributeView.setAcl(emptyList()` approach seems to be working! (I can restrict the unit test to Windows, no problem.) I'll continue testing, but in the meantime, add this as an official answer so I can mark it as accepted! – Garret Wilson Aug 01 '22 at 23:17
  • VGR, the wonderful news is that `getFileAttributeView(unreadableFile, AclFileAttributeView.class).setAcl(List.of())` works for files on Windows! Unfortunately `getFileAttributeView(unreadableDirectory, AclFileAttributeView.class).setAcl(List.of())` for directories gives a `StackOverflowError()` be recursively calling `Files.walkFileTree()` for some reason. But this is a great and welcome start; it will have to do for now. – Garret Wilson Aug 02 '22 at 00:01

2 Answers2

1

The solution that 'covers the field' so to speak, when it comes to testing anything filesystem related of any sort, is to write your own filesystem. Yeah, okay. It's perhaps a bit of a bazooka to take out a mosquito, but, this principle extends to virtually all things you want to do and test with filesystems.

For example, wanna have some code that writes a file out and you want to check that it does, indeed, write the correct data? You could just make that file, but now you do need to worry about some directory to write to during your tests. File system access is also slow and also means you need to worry about cleaning up after the test. It's also hard to isolate them for parallellizable purposes. All of this stuff is minor annoyance at best, but it starts to feel like death with a thousand cuts.

Another major downside of what you're doing is that file systems by their nature are highly varied between architectures/OSes. You can for example use AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class); view.setAcl(Collections.emptyList()); which will probably result in .isReadable returning false, if you're on an OS that supports and setting a file on a filesystem that supports the notion of an acl-based access system.

Which results in the rather nasty result of you having code that works perfectly every time on your hardware, but your buddy working on a different platform can't use the code. If you don't want that, virtual file systems can fix this for you.

Write my own.. what?

One of the advantages of the 'new' file IO stuff (java.nio.file, vs. the old java.io stuff) is that filesystems are mostly abstracted out.

The concept 'a file system' is something you get to define. Thus, you can define one that operates entirely in memory - you now no longer need to worry about cleanup, it'll be as fast as it can be, and it'll be independent of any other tests if you want them to be. It also makes mocking stuff out ('mocking', the english word, not 'mocking' such as JMock) a lot easier.

A bit like the time stuff, you do have to always use the right calls (i.e. if you make a habit of invoking LocalDate.now() in your source, you need to switch that over to LocalDate.now(clock) so that you can set up dummy clocks for stable testing). You can't just use Paths.get() everywhere - the Path object 'encodes' which filesystem is in use (and you want it to 'encode' that your test filesystem is in use).

Instead of Paths.get("/path/stuff") you have to call:

someFileSystem.getPath("/path/stuff") where someFileSystem can be FileSystems.getDefault() which then gets you identical behaviour to Paths.get, but it needs to be injected, so that under test conditions, you can have that be your test filesystem.

Now all you need is a fake file system. Unfortunately, the FileSystem API is quite complicated. Making your own is not exactly trivial. Fortunately, you don't have to shave this particular yak; someone's (Google's dev team, to be specific) has already done that for you. Presenting JimFS!

Downsides

  • Integrating JimFS is still a bit complicated.
  • Paths.get("/x/y") needs to be put on your linter's ban list; stamping this out may require quite a bit of rewrites. Especially if you do not using a dependency injection framework. It can be worthwhile to set up a simple singleton that can be asked for the filesystem; in basis it can return FileSystems.getDefault() and then all code works identically. You can then expand it and e.g. add a global 'override with my dummy file system' option, or use ThreadLocal to allow individual threads to each get their own, whatever you need.
  • Any usage of the old API (java.io), notably including attempts to use path.toFile() aren't going to work. The old API, as far as I know, simply can only interact with the actual filesystem. Thus, you also need to go on a search spree for any usage of old j.i.File anywhere in your code base and replace it with fileSystem.getPath based code instead.

It's understandable if this feels like a more arduous path vs. your current solution of mocking out the j.n.f.Files class. You'd know best, it's your project. However, I think this covers your 2 main options: Mock all the things, or, use a test file system.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Sure, many people have though of writing their own `FileSystem` implementation (see https://stackoverflow.com/q/30394737 ), and Google even has [Jimfs](https://github.com/google/jimfs). But that doesn't mean it will help me with this problem. A new file system implementation is not trivial. And even the example you gave completely ignores the `FileSystemProvider` layer, which is used by `java.nio.Files`. This means my code will have to call special methods, avoiding `Files` and many other standard Java NIO methods, at which point I might as well create my own API layer above NIO. – Garret Wilson Aug 02 '22 at 00:05
  • `Files` is fine. `java.io.File` is not. JimFS takes care of the 'a new file system impl is not trivial'. At any rate, the point of this answer is: Your spidey senses are tingling and telling you that there must be a non-hacky way to do this. Write a file system is the answer. If that answer is itself then not suitable, that's of course possible. But it answers the 'spidey senses'. Yeah, there's another way, and you decided it isn't good for this case. Now you can just write that mock out, and gnash some teeth, but stop looking. – rzwitserloot Aug 02 '22 at 01:01
  • What I was saying about `Files` is that the new `FileSystem` API requires something called a `FileSystemProvider` to be implemented as well; it's not straightforward how everything is all connected. After reading your answer again I think you were saying basically the same thing, and saying that one can use JimFS. The first time I read it I thought you were advocating writing a file system from scratch. Mocking isn't easy either because of all the indirections, making the tests brittle. Luckily in this case VGR found a solution, and I can use JUnit 5 annotations to only have it run on Windows. – Garret Wilson Aug 02 '22 at 03:55
1

This worked for me in Windows 10:

AclFileAttributeView view =
    Files.getFileAttributeView(path, AclFileAttributeView.class);
view.setAcl(Collections.emptyList());

For completeness across operating systems, you can guard it with the appropriate checks, and add a similar thing for (most) Unix file systems:

FileStore store = Files.getFileStore(path);
if (store.supportsFileAttributeView(AclFileAttributeView.class)) {
    AclFileAttributeView view =
        Files.getFileAttributeView(path, AclFileAttributeView.class);
    view.setAcl(Collections.emptyList());
} else if (store.supportsFileAttributeView(PosixFileAttributeView.class)) {
    Files.setPosixFilePermissions(path,
        EnumSet.noneOf(PosixFilePermission.class));
}
VGR
  • 40,506
  • 4
  • 48
  • 63