Accessing Windows disks directly with Java NIO

2020-07-06 09:01发布

问题:

I am using a library that uses Java NIO in order to directly map files to memory, but I am having trouble reading disks directly.

I can read the disks directly using FileInputStream with UNC, such as

File disk = new File("\\\\.\\PhysicalDrive0\\");
try (FileInputStream fis = new FileInputStream(disk);
    BufferedInputStream bis = new BufferedInputStream(fis)) {
    byte[] somebytes = new byte[10];
    bis.read(somebytes);
} catch (Exception ex) {
    System.out.println("Oh bother");
}

However, I can't extend this to NIO:

File disk = new File("\\\\.\\PhysicalDrive0\\");
Path path = disk.toPath();
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)){
    System.out.println("No exceptions! Yay!");
} catch (Exception ex) {
    System.out.println("Oh bother");
}

The stacktrace (up to the cause) is:

java.nio.file.FileSystemException: \\.\PhysicalDrive0\: The parameter is incorrect.

at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
at sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:115)
at java.nio.channels.FileChannel.open(FileChannel.java:287)
at java.nio.channels.FileChannel.open(FileChannel.java:334)
at hdreader.HDReader.testcode(HDReader.java:147)

I haven't been able to find a solution, though I saw something close on How to access specific raw data on disk from java. The answer by Daniel Alder suggesting the use of GLOBALROOT seems to be relevant, as the answer uses FileChannel in the answer, but I can't seem to find the drive using this pattern. Is there a way to list all devices under GLOBALROOT or something like that?

At the moment I am looking at replacing uses of NIO with straight InputStreams, but I want to avoid this if I can. Firstly, NIO was used for a reason, and secondly, it runs through a lot of code and will require a lot of work. Finally, I'd like to know how to implement something like Daniel's solution so that I can write to devices or use NIO in the future.

So in summary: how can I access drives directly with Java NIO (not InputStreams), and/or is there a way to list all devices accessible through GLOBALROOT so that I might use Daniel Alser's solution?

Summary of Answers: I have kept the past edits (below) to avoid confusion. With the help of EJP and Apangin I think I have a workable solution. Something like

private void rafMethod(long posn) {
    ByteBuffer buffer = ByteBuffer.allocate(512);
    buffer.rewind();
    try (RandomAccessFile raf = new RandomAccessFile(disk.getPath(), "r");
            SeekableByteChannel sbc = raf.getChannel()) {
        sbc.read(buffer);
    } catch (Exception ex) {
        System.out.println("Oh bother: " + ex);
        ex.printStackTrace();
    }

    return buffer;
}

This will work as long as the posn parameter is a multiple of the sector size (set at 512 in this case). Note that this also works with the Channels.newChannel(FileInputStream), which seems to always return a SeekableByteStream in this case and it appears it is safe to cast it to one.

From quick and dirty testing it appears that these methods truly do seek and don't just skip. I searched for a thousand locations at the start of my drive and it read them. I did the same but added an offset of half of the disk size (to search the back of the disk). I found:

  • Both methods took almost the same time.
  • Searching the start or the end of the disk did not affect time.
  • Reducing the range of the addresses did reduce time.
  • Sorting the addresses did reduce time, but not by much.

This suggests to me that this is truly seeking and not merely reading and skipping (as a stream tends to). The speed is still terrible at this stage and it makes my hard drive sound like a washing machine, but the code was designed for a quick test and has yet to be made pretty. It may still work fine.

Thanks to both EJP and Apangin for the help. Read more in their respective answers.

Edit: I have since run my code on a Windows 7 machine (I didn't have one originally), and I get a slightly different exception (see below). This was run with admin privileges, and the first piece of code still works under the same conditions.

java.nio.file.FileSystemException: \\.\PhysicalDrive0\: A device attached to the system is not functioning.

    at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
    at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
    at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
    at sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:115)
    at java.nio.channels.FileChannel.open(FileChannel.java:287)
    at java.nio.channels.FileChannel.open(FileChannel.java:335)
    at testapp.TestApp.doStuff(TestApp.java:30)
    at testapp.TestApp.main(TestApp.java:24)

Edit 2: In response to EJP, I have tried:

    byte[] bytes = new byte[20];
    ByteBuffer bb = ByteBuffer.wrap(bytes);
    bb.rewind();

    File disk = new File("\\\\.\\PhysicalDrive0\\");
    try (FileInputStream fis = new FileInputStream(disk);
            ReadableByteChannel rbc = Channels.newChannel(new FileInputStream(disk))) {

        System.out.println("Channel created");
        int read = rbc.read(bb);
        System.out.println("Read " + read + " bytes");
        System.out.println("No exceptions! Yay!");
    } catch (Exception ex) {
        System.out.println("Oh bother: " + ex);
    }

When I try this I get the following output:

Channel created
Oh bother: java.io.IOException: The parameter is incorrect

So it appears that I can create a FileChannel or ReadableByteChannel, but I can't use it; that is, the error is simply deferred.

回答1:

When accessing physical drive without buffering, you can read only complete sectors. This means, if a sector size is 512 bytes, you can read only multiple of 512 bytes. Change your buffer length to 512 or 4096 (whatever your sector size is) and FileChannel will work fine:

ByteBuffer buf = ByteBuffer.allocate(512);

try (RandomAccessFile raf = new RandomAccessFile("\\\\.\\PhysicalDrive0", "r");
     FileChannel fc = raf.getChannel()) {
    fc.read(buf);
    System.out.println("It worked! Read bytes: " + buf.position());
} catch (Exception e) {
    e.printStackTrace();
}

See Alignment and File Access Requirements.

Your original FileInputStream code works obviously because of BufferedInputStream which has the default buffer size of 8192. Take it away - and the code will fail with the same exception.



回答2:

Using NIO your original code only needs to change very slightly.

Path disk = Paths.get("d:\\.");
try (ByteChannel bc = Files.newByteChannel(disk, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    bc.read(buffer);
} catch (Exception e){
    e.printStackTrace();
}

Is fine, workable code, but I get an access denied error in both your version and mine.



回答3:

Run this as administrator. It really does work, as it's only a thin wrapper over java.io:

    try (FileInputStream fis = new FileInputStream(disk);
        ReadableByteChannel fc = Channels.newChannel(fis))
    {
        System.out.println("No exceptions! Yay!");
        ByteBuffer  bb = ByteBuffer.allocate(4096);
        int count = fc.read(bb);
        System.out.println("read count="+count);
    }
    catch (Exception ex)
    {
        System.out.println("Oh bother: "+ex);
        ex.printStackTrace();
    }

EDIT If you need random access, you're stuck with RandomAccessFile. There's no mapping from that via Channels. But the solution above isn't NIO anyway, just a Java NIO layer over FileInput/OutputStream.