Repository: netty/netty
Issue: Is there a way to create a ByteBuf backed by a read-only ByteBuffer?
Labels: defect

I'm mmaping a file (read-only), and would like to send it using Netty.

When I do Unpooled.wrappedBuffer(myByteBuffer) Netty throws because of these lines in  UnpooledUnsafeDirectByteBuf:

```
    if (initialBuffer.isReadOnly()) {
        throw new IllegalArgumentException("initialBuffer is a read-only buffer.");
    }
```

I presume there's a reason for the isReadOnly() test, but I can't really see why it is a problem to have a read-only ByteBuf as long as I don't try to write to it (?)


Issue comments:
`Unpooled.wrappedBuffer(ByteBuffer)` should not fail even if a user specified a read-only buffer.  However, it should return a different `ByteBuf` implementation than `UnpooledUnsafeDirectByteBuf` because it will make the read-only buffer writable.

@trustin will you take care ?

working on it....

Fixed via #1190 


Pull request review comments:

Patch that resolved the issue:
diff --git a/buffer/src/main/java/io/netty/buffer/ReadOnlyDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/ReadOnlyDirectByteBuf.java
new file mode 100644
index 00000000000..b1b4d8b2124
--- /dev/null
+++ b/buffer/src/main/java/io/netty/buffer/ReadOnlyDirectByteBuf.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2013 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.buffer;
+
+import io.netty.util.ResourceLeak;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ReadOnlyBufferException;
+import java.nio.channels.GatheringByteChannel;
+import java.nio.channels.ScatteringByteChannel;
+
+
+/**
+ * Read-only ByteBuf which wraps a read-only direct ByteBuffer.
+ */
+class ReadOnlyDirectByteBuf extends AbstractReferenceCountedByteBuf {
+    private final ResourceLeak leak = leakDetector.open(this);
+
+    protected final ByteBuffer buffer;
+    private final ByteBufAllocator allocator;
+    private ByteBuffer tmpNioBuf;
+
+    public ReadOnlyDirectByteBuf(ByteBufAllocator allocator, ByteBuffer buffer) {
+        super(buffer.remaining());
+        if (!buffer.isDirect()) {
+            throw new IllegalArgumentException("buffer must be readOnly");
+        }
+        this.allocator = allocator;
+        this.buffer = buffer.slice().order(ByteOrder.BIG_ENDIAN);
+        writerIndex(buffer.limit());
+    }
+
+    @Override
+    protected void deallocate() {
+        leak.close();
+    }
+
+    @Override
+    public byte getByte(int index) {
+        ensureAccessible();
+        return _getByte(index);
+    }
+
+    @Override
+    protected byte _getByte(int index) {
+        return buffer.get(index);
+    }
+
+    @Override
+    public short getShort(int index) {
+        ensureAccessible();
+        return _getShort(index);
+    }
+
+    @Override
+    protected short _getShort(int index) {
+        return buffer.getShort(index);
+    }
+
+    @Override
+    public int getUnsignedMedium(int index) {
+        ensureAccessible();
+        return _getUnsignedMedium(index);
+    }
+
+    @Override
+    protected int _getUnsignedMedium(int index) {
+        return (getByte(index) & 0xff) << 16 | (getByte(index + 1) & 0xff) << 8 | getByte(index + 2) & 0xff;
+    }
+
+    @Override
+    public int getInt(int index) {
+        ensureAccessible();
+        return _getInt(index);
+    }
+
+    @Override
+    protected int _getInt(int index) {
+        return buffer.getInt(index);
+    }
+
+    @Override
+    public long getLong(int index) {
+        ensureAccessible();
+        return _getLong(index);
+    }
+
+    @Override
+    protected long _getLong(int index) {
+        return buffer.getLong(index);
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) {
+        checkDstIndex(index, length, dstIndex, dst.capacity());
+        if (dst.hasArray()) {
+            getBytes(index, dst.array(), dst.arrayOffset() + dstIndex, length);
+        } else if (dst.nioBufferCount() > 0) {
+            for (ByteBuffer bb: dst.nioBuffers(dstIndex, length)) {
+                int bbLen = bb.remaining();
+                getBytes(index, bb);
+                index += bbLen;
+            }
+        } else {
+            dst.setBytes(dstIndex, this, index, length);
+        }
+        return this;
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
+        checkDstIndex(index, length, dstIndex, dst.length);
+        if (dst == null) {
+            throw new NullPointerException("dst");
+        }
+        if (dstIndex < 0 || dstIndex > dst.length - length) {
+            throw new IndexOutOfBoundsException(String.format(
+                    "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dst.length));
+        }
+
+        ByteBuffer tmpBuf = internalNioBuffer();
+        tmpBuf.clear().position(index).limit(index + length);
+        tmpBuf.get(dst, dstIndex, length);
+        return this;
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, ByteBuffer dst) {
+        checkIndex(index);
+        if (dst == null) {
+            throw new NullPointerException("dst");
+        }
+
+        int bytesToCopy = Math.min(capacity() - index, dst.remaining());
+        ByteBuffer tmpBuf = internalNioBuffer();
+        tmpBuf.clear().position(index).limit(index + bytesToCopy);
+        dst.put(tmpBuf);
+        return this;
+    }
+
+    @Override
+    protected void _setByte(int index, int value) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    protected void _setShort(int index, int value) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    protected void _setMedium(int index, int value) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    protected void _setInt(int index, int value) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    protected void _setLong(int index, long value) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public int capacity() {
+        return maxCapacity();
+    }
+
+    @Override
+    public ByteBuf capacity(int newCapacity) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public ByteBufAllocator alloc() {
+        return allocator;
+    }
+
+    @Override
+    public ByteOrder order() {
+        return ByteOrder.BIG_ENDIAN;
+    }
+
+    @Override
+    public ByteBuf unwrap() {
+        return null;
+    }
+
+    @Override
+    public boolean isDirect() {
+        return true;
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, OutputStream out, int length) throws IOException {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public int getBytes(int index, GatheringByteChannel out, int length) throws IOException {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public ByteBuf setBytes(int index, ByteBuffer src) {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public int setBytes(int index, InputStream in, int length) throws IOException {
+        throw new ReadOnlyBufferException();
+    }
+
+    @Override
+    public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
+        throw new ReadOnlyBufferException();
+    }
+
+    protected final ByteBuffer internalNioBuffer() {
+        ByteBuffer tmpNioBuf = this.tmpNioBuf;
+        if (tmpNioBuf == null) {
+            this.tmpNioBuf = tmpNioBuf = buffer.duplicate();
+        }
+        return tmpNioBuf;
+    }
+
+    @Override
+    public ByteBuf copy(int index, int length) {
+        ensureAccessible();
+        ByteBuffer src;
+        try {
+            src = (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
+        } catch (IllegalArgumentException e) {
+            throw new IndexOutOfBoundsException("Too many bytes to read - Need " + (index + length));
+        }
+
+        ByteBuffer dst = ByteBuffer.allocateDirect(length);
+        dst.put(src);
+        dst.order(order());
+        dst.clear();
+        return new UnpooledDirectByteBuf(alloc(), dst, maxCapacity());
+    }
+
+    @Override
+    public int nioBufferCount() {
+        return 1;
+    }
+
+    @Override
+    public ByteBuffer nioBuffer(int index, int length) {
+        ensureAccessible();
+        if (index == 0 && length == capacity()) {
+            return buffer.duplicate();
+        } else {
+            return ((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length)).slice();
+        }
+    }
+
+    @Override
+    public ByteBuffer[] nioBuffers(int index, int length) {
+        return new ByteBuffer[] { nioBuffer(index, length) };
+    }
+
+    @Override
+    public boolean hasArray() {
+        return false;
+    }
+
+    @Override
+    public byte[] array() {
+        throw new UnsupportedOperationException("direct buffer");
+    }
+
+    @Override
+    public int arrayOffset() {
+        throw new UnsupportedOperationException("direct buffer");
+    }
+
+    @Override
+    public boolean hasMemoryAddress() {
+        return false;
+    }
+
+    @Override
+    public long memoryAddress() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ByteBuf suspendIntermediaryDeallocations() {
+        return this;
+    }
+
+    @Override
+    public ByteBuf resumeIntermediaryDeallocations() {
+        return this;
+    }
+}
diff --git a/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java
new file mode 100644
index 00000000000..1607a6006bd
--- /dev/null
+++ b/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2013 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.buffer;
+
+
+import io.netty.util.internal.PlatformDependent;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+
+
+/**
+ * Read-only ByteBuf which wraps a read-only direct ByteBuffer and use unsafe for best performance.
+ */
+final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyDirectByteBuf {
+    private static final boolean NATIVE_ORDER = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN;
+    private final long memoryAddress;
+
+    public ReadOnlyUnsafeDirectByteBuf(ByteBufAllocator allocator, ByteBuffer buffer) {
+        super(allocator, buffer);
+        memoryAddress = PlatformDependent.directBufferAddress(buffer);
+    }
+
+    @Override
+    protected byte _getByte(int index) {
+        return PlatformDependent.getByte(addr(index));
+    }
+
+    @Override
+    protected short _getShort(int index) {
+        short v = PlatformDependent.getShort(addr(index));
+        return NATIVE_ORDER? v : Short.reverseBytes(v);
+    }
+
+    @Override
+    protected int _getUnsignedMedium(int index) {
+        long addr = addr(index);
+        return (PlatformDependent.getByte(addr) & 0xff) << 16 |
+                (PlatformDependent.getByte(addr + 1) & 0xff) << 8 |
+                PlatformDependent.getByte(addr + 2) & 0xff;
+    }
+
+    @Override
+    protected int _getInt(int index) {
+        int v = PlatformDependent.getInt(addr(index));
+        return NATIVE_ORDER? v : Integer.reverseBytes(v);
+    }
+
+    @Override
+    protected long _getLong(int index) {
+        long v = PlatformDependent.getLong(addr(index));
+        return NATIVE_ORDER? v : Long.reverseBytes(v);
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) {
+        checkIndex(index, length);
+        if (dst == null) {
+            throw new NullPointerException("dst");
+        }
+        if (dstIndex < 0 || dstIndex > dst.capacity() - length) {
+            throw new IndexOutOfBoundsException("dstIndex: " + dstIndex);
+        }
+
+        if (dst.hasMemoryAddress()) {
+            PlatformDependent.copyMemory(addr(index), dst.memoryAddress() + dstIndex, length);
+        } else if (dst.hasArray()) {
+            PlatformDependent.copyMemory(addr(index), dst.array(), dst.arrayOffset() + dstIndex, length);
+        } else {
+            dst.setBytes(dstIndex, this, index, length);
+        }
+        return this;
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
+        checkIndex(index, length);
+        if (dst == null) {
+            throw new NullPointerException("dst");
+        }
+        if (dstIndex < 0 || dstIndex > dst.length - length) {
+            throw new IndexOutOfBoundsException(String.format(
+                    "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dst.length));
+        }
+
+        if (length != 0) {
+            PlatformDependent.copyMemory(addr(index), dst, dstIndex, length);
+        }
+        return this;
+    }
+
+    @Override
+    public ByteBuf getBytes(int index, ByteBuffer dst) {
+        checkIndex(index);
+        if (dst == null) {
+            throw new NullPointerException("dst");
+        }
+
+        int bytesToCopy = Math.min(capacity() - index, dst.remaining());
+        ByteBuffer tmpBuf = internalNioBuffer();
+        tmpBuf.clear().position(index).limit(index + bytesToCopy);
+        dst.put(tmpBuf);
+        return this;
+    }
+
+    @Override
+    public ByteBuf copy(int index, int length) {
+        checkIndex(index, length);
+        UnpooledUnsafeDirectByteBuf copy = (UnpooledUnsafeDirectByteBuf) alloc().directBuffer(length, maxCapacity());
+        if (length != 0) {
+            PlatformDependent.copyMemory(addr(index), copy.addr(0), length);
+            copy.setIndex(0, length);
+        }
+        return copy;
+    }
+
+    private long addr(int index) {
+        return memoryAddress + index;
+    }
+}
diff --git a/buffer/src/main/java/io/netty/buffer/Unpooled.java b/buffer/src/main/java/io/netty/buffer/Unpooled.java
index 77303d4eb93..8a0f7d9565b 100644
--- a/buffer/src/main/java/io/netty/buffer/Unpooled.java
+++ b/buffer/src/main/java/io/netty/buffer/Unpooled.java
@@ -223,9 +223,17 @@ public static ByteBuf wrappedBuffer(ByteBuffer buffer) {
                     buffer.arrayOffset() + buffer.position(),
                     buffer.remaining()).order(buffer.order());
         } else if (PlatformDependent.hasUnsafe()) {
-            return new UnpooledUnsafeDirectByteBuf(ALLOC, buffer, buffer.remaining());
+            if (buffer.isReadOnly()) {
+                return new ReadOnlyUnsafeDirectByteBuf(ALLOC, buffer);
+            } else {
+                return new UnpooledUnsafeDirectByteBuf(ALLOC, buffer, buffer.remaining());
+            }
         } else {
-            return new UnpooledDirectByteBuf(ALLOC, buffer, buffer.remaining());
+            if (buffer.isReadOnly()) {
+                return new ReadOnlyDirectByteBuf(ALLOC, buffer);
+            }  else {
+                return new UnpooledDirectByteBuf(ALLOC, buffer, buffer.remaining());
+            }
         }
     }
 
diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java
index a0510dbf20f..815922dfc04 100644
--- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java
@@ -493,7 +493,7 @@ public ByteBuf unwrap() {
         return null;
     }
 
-    private long addr(int index) {
+    long addr(int index) {
         return memoryAddress + index;
     }
 }


Test patch:
diff --git a/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufTest.java b/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufTest.java
new file mode 100644
index 00000000000..5c8fc9d7c08
--- /dev/null
+++ b/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2013 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.buffer;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ReadOnlyBufferException;
+
+public class ReadOnlyDirectByteBufTest {
+
+    protected ByteBuf buffer(ByteBuffer buffer) {
+        return new ReadOnlyDirectByteBuf(UnpooledByteBufAllocator.DEFAULT, buffer.asReadOnlyBuffer());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testConstructWithNotDirectBuffer() {
+        buffer(ByteBuffer.allocate(1));
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetByte() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setByte(0, 1);
+    }
+
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetInt() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setInt(0, 1);
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetShort() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setShort(0, 1);
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetMedium() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setMedium(0, 1);
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetLong() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setLong(0, 1);
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetBytesViaArray() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setBytes(0, "test".getBytes());
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetBytesViaBuffer() {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setBytes(0, Unpooled.copyInt(1));
+    }
+
+    @Test(expected = ReadOnlyBufferException.class)
+    public void testSetBytesViaStream() throws IOException {
+        ByteBuf buf = buffer(ByteBuffer.allocateDirect(8).asReadOnlyBuffer());
+        buf.setBytes(0, new ByteArrayInputStream("test".getBytes()), 2);
+    }
+
+    @Test
+    public void testGetReadByte() {
+        ByteBuf buf = buffer(((ByteBuffer) ByteBuffer.allocateDirect(2).put(new byte[]{(byte) 1, (byte) 2}).flip()).asReadOnlyBuffer());
+        Assert.assertEquals(1, buf.getByte(0));
+        Assert.assertEquals(2, buf.getByte(1));
+
+        Assert.assertEquals(1, buf.readByte());
+        Assert.assertEquals(2, buf.readByte());
+        Assert.assertFalse(buf.isReadable());
+    }
+
+    @Test
+    public void testGetReadInt() {
+        ByteBuf buf = buffer(((ByteBuffer) ByteBuffer.allocateDirect(8).putInt(1).putInt(2).flip()).asReadOnlyBuffer());
+        Assert.assertEquals(1, buf.getInt(0));
+        Assert.assertEquals(2, buf.getInt(4));
+
+        Assert.assertEquals(1, buf.readInt());
+        Assert.assertEquals(2, buf.readInt());
+        Assert.assertFalse(buf.isReadable());
+    }
+
+
+    @Test
+    public void testGetReadShort() {
+        ByteBuf buf = buffer(((ByteBuffer) ByteBuffer.allocateDirect(8).putShort((short) 1).putShort((short) 2).flip()).asReadOnlyBuffer());
+        Assert.assertEquals(1, buf.getShort(0));
+        Assert.assertEquals(2, buf.getShort(2));
+
+        Assert.assertEquals(1, buf.readShort());
+        Assert.assertEquals(2, buf.readShort());
+        Assert.assertFalse(buf.isReadable());
+    }
+
+    @Test
+    public void testGetReadLong() {
+        ByteBuf buf = buffer(((ByteBuffer) ByteBuffer.allocateDirect(16).putLong(1).putLong(2).flip()).asReadOnlyBuffer());
+        Assert.assertEquals(1, buf.getLong(0));
+        Assert.assertEquals(2, buf.getLong(8));
+
+        Assert.assertEquals(1, buf.readLong());
+        Assert.assertEquals(2, buf.readLong());
+        Assert.assertFalse(buf.isReadable());
+    }
+
+    @Test
+    public void testCopy() {
+        ByteBuf buf = buffer(((ByteBuffer) ByteBuffer.allocateDirect(16).putLong(1).putLong(2).flip()).asReadOnlyBuffer());
+        Assert.assertEquals(buf, buf.copy());
+    }
+
+    @Test
+    public void testCopyWithOffset() {
+        ByteBuf buf = buffer(((ByteBuffer) ByteBuffer.allocateDirect(16).putLong(1).putLong(2).flip()).asReadOnlyBuffer());
+        Assert.assertEquals(buf.slice(1, 9), buf.copy(1, 9));
+    }
+}
diff --git a/buffer/src/test/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBufTest.java b/buffer/src/test/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBufTest.java
new file mode 100644
index 00000000000..4ce322660ca
--- /dev/null
+++ b/buffer/src/test/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBufTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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 io.netty.buffer;
+
+import io.netty.util.internal.PlatformDependent;
+import org.junit.BeforeClass;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assume.assumeTrue;
+
+public class ReadOnlyUnsafeDirectByteBufTest extends ReadOnlyDirectByteBufTest {
+
+    /**
+     * Needs unsafe to run
+     */
+    @BeforeClass
+    public static void assumeConditions(){
+        assumeTrue(PlatformDependent.hasUnsafe());
+    }
+
+    @Override
+    protected ByteBuf buffer(ByteBuffer buffer) {
+        return new ReadOnlyUnsafeDirectByteBuf(UnpooledByteBufAllocator.DEFAULT, buffer);
+    }
+}
diff --git a/buffer/src/test/java/io/netty/buffer/UnpooledTest.java b/buffer/src/test/java/io/netty/buffer/UnpooledTest.java
index e53f2b33ef9..9f61c6533e2 100644
--- a/buffer/src/test/java/io/netty/buffer/UnpooledTest.java
+++ b/buffer/src/test/java/io/netty/buffer/UnpooledTest.java
@@ -570,4 +570,17 @@ public void testWrapBoolean() {
         assertEquals(0, Unpooled.copyBoolean(new boolean[0]).capacity());
 
     }
+
+    @Test
+    public void wrappedReadOnlyDirectBuffer() {
+        ByteBuffer buffer = ByteBuffer.allocateDirect(12);
+        for (int i = 0; i < 12; i++) {
+            buffer.put((byte) i);
+        }
+        buffer.flip();
+        ByteBuf wrapped = wrappedBuffer(buffer.asReadOnlyBuffer());
+        for (int i = 0; i < 12; i++) {
+            assertEquals((byte) i, wrapped.readByte());
+        }
+    }
 }