Home > Blockchain >  How to make create a path in Java for which isReadable(path) returns false?
How to make create a path in Java for which isReadable(path) returns false?

Time:08-02

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.)

CodePudding user response:

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.

CodePudding user response:

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));
}
  • Related