Skip to content

Commit 2a1fa7f

Browse files
committed
Make buffers read-only for all paths
This also moves the responsibility of reading from the file to the BufferHolder. This seems to make the responsibilities a bit clearer, although I also method on the buffers could work.
1 parent 1a26137 commit 2a1fa7f

File tree

5 files changed

+101
-141
lines changed

5 files changed

+101
-141
lines changed

src/main/java/com/maxmind/db/Buffer.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.maxmind.db;
22

3-
import java.io.IOException;
4-
import java.nio.channels.FileChannel;
53
import java.nio.charset.CharacterCodingException;
64
import java.nio.charset.CharsetDecoder;
75

@@ -12,6 +10,9 @@
1210
*
1311
* <p>This interface is designed to provide a long-based API while
1412
* remaining compatible with the limitations of underlying storage.
13+
*
14+
* <p>All underlying {@link java.nio.ByteBuffer}s are read-only to prevent
15+
* accidental modification of shared data.
1516
*/
1617
interface Buffer {
1718
/**
@@ -96,16 +97,6 @@ interface Buffer {
9697
*/
9798
Buffer duplicate();
9899

99-
/**
100-
* Reads data from the given channel into this buffer starting at the
101-
* current position.
102-
*
103-
* @param channel the file channel
104-
* @return the number of bytes read
105-
* @throws IOException if an I/O error occurs
106-
*/
107-
long readFrom(FileChannel channel) throws IOException;
108-
109100
/**
110101
* Decodes the buffer's content into a string using the given decoder.
111102
*

src/main/java/com/maxmind/db/BufferHolder.java

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,48 @@ final class BufferHolder {
2323
FileChannel channel = file.getChannel()) {
2424
long size = channel.size();
2525
if (mode == FileMode.MEMORY) {
26-
Buffer buf;
2726
if (size <= chunkSize) {
28-
buf = new SingleBuffer(size);
27+
// Allocate, read, and make read-only
28+
ByteBuffer buffer = ByteBuffer.allocate((int) size);
29+
if (channel.read(buffer) != size) {
30+
throw new IOException("Unable to read "
31+
+ database.getName()
32+
+ " into memory. Unexpected end of stream.");
33+
}
34+
buffer.flip();
35+
this.buffer = new SingleBuffer(buffer);
2936
} else {
30-
buf = new MultiBuffer(size);
31-
}
32-
if (buf.readFrom(channel) != buf.capacity()) {
33-
throw new IOException("Unable to read "
34-
+ database.getName()
35-
+ " into memory. Unexpected end of stream.");
37+
// Allocate chunks, read, and make read-only
38+
int fullChunks = (int) (size / chunkSize);
39+
int remainder = (int) (size % chunkSize);
40+
int totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
41+
ByteBuffer[] buffers = new ByteBuffer[totalChunks];
42+
43+
for (int i = 0; i < fullChunks; i++) {
44+
buffers[i] = ByteBuffer.allocate(chunkSize);
45+
}
46+
if (remainder > 0) {
47+
buffers[totalChunks - 1] = ByteBuffer.allocate(remainder);
48+
}
49+
50+
long totalRead = 0;
51+
for (ByteBuffer buffer : buffers) {
52+
int read = channel.read(buffer);
53+
if (read == -1) {
54+
break;
55+
}
56+
totalRead += read;
57+
buffer.flip();
58+
}
59+
60+
if (totalRead != size) {
61+
throw new IOException("Unable to read "
62+
+ database.getName()
63+
+ " into memory. Unexpected end of stream.");
64+
}
65+
66+
this.buffer = new MultiBuffer(buffers, chunkSize);
3667
}
37-
this.buffer = buf;
3868
} else {
3969
if (size <= chunkSize) {
4070
this.buffer = SingleBuffer.mapFromChannel(channel);

src/main/java/com/maxmind/db/MultiBuffer.java

Lines changed: 10 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
* a single logical position and limit across them.
1818
*
1919
* <p>Use this when working with databases/files that may exceed 2GB.
20+
*
21+
* <p>All underlying {@link ByteBuffer}s are read-only to prevent accidental
22+
* modification of shared data.
2023
*/
2124
class MultiBuffer implements Buffer {
2225

@@ -30,16 +33,6 @@ class MultiBuffer implements Buffer {
3033
private long position = 0;
3134
private long limit;
3235

33-
/**
34-
* Creates a new {@code MultiBuffer} with the given capacity, backed by
35-
* heap-allocated {@link ByteBuffer}s.
36-
*
37-
* @param capacity the total capacity in bytes
38-
*/
39-
public MultiBuffer(long capacity) {
40-
this(capacity, DEFAULT_CHUNK_SIZE);
41-
}
42-
4336
/**
4437
* Creates a new {@code MultiBuffer} backed by the given
4538
* {@link ByteBuffer}s.
@@ -64,43 +57,19 @@ public MultiBuffer(long capacity) {
6457
+ " is smaller than expected chunk size");
6558
}
6659

67-
this.buffers = buffers.clone();
68-
long capacity = 0;
69-
for (ByteBuffer buffer : buffers) {
70-
capacity += buffer.capacity();
60+
// Make all buffers read-only
61+
this.buffers = new ByteBuffer[buffers.length];
62+
for (int i = 0; i < buffers.length; i++) {
63+
this.buffers[i] = buffers[i].asReadOnlyBuffer();
7164
}
72-
this.capacity = capacity;
73-
this.limit = capacity;
74-
this.chunkSize = chunkSize;
75-
}
7665

77-
/**
78-
* Creates a new {@code MultiBuffer} with the given capacity, backed by
79-
* heap-allocated {@link ByteBuffer}s with the given chunk size.
80-
*
81-
* @param capacity the total capacity in bytes
82-
* @param chunkSize the size of each buffer chunk
83-
*/
84-
MultiBuffer(long capacity, int chunkSize) {
85-
if (capacity <= 0) {
86-
throw new IllegalArgumentException("Capacity must be positive");
66+
long capacity = 0;
67+
for (ByteBuffer buffer : this.buffers) {
68+
capacity += buffer.capacity();
8769
}
8870
this.capacity = capacity;
8971
this.limit = capacity;
9072
this.chunkSize = chunkSize;
91-
92-
int fullChunks = (int) (capacity / chunkSize);
93-
int remainder = (int) (capacity % chunkSize);
94-
int totalChunks = fullChunks + (remainder > 0 ? 1 : 0);
95-
96-
this.buffers = new ByteBuffer[totalChunks];
97-
98-
for (int i = 0; i < fullChunks; i++) {
99-
buffers[i] = ByteBuffer.allocate(chunkSize);
100-
}
101-
if (remainder > 0) {
102-
buffers[totalChunks - 1] = ByteBuffer.allocate(remainder);
103-
}
10473
}
10574

10675
/** {@inheritDoc} */
@@ -240,41 +209,6 @@ public Buffer duplicate() {
240209
return copy;
241210
}
242211

243-
/** {@inheritDoc} */
244-
@Override
245-
public long readFrom(FileChannel channel) throws IOException {
246-
return this.readFrom(channel, DEFAULT_CHUNK_SIZE);
247-
}
248-
249-
/**
250-
* Reads data from the given channel into this buffer starting at the
251-
* current position.
252-
*
253-
* @param channel the file channel
254-
* @param chunkSize the chunk size to use for positioning reads
255-
* @return the number of bytes read
256-
* @throws IOException if an I/O error occurs
257-
*/
258-
long readFrom(FileChannel channel, int chunkSize) throws IOException {
259-
long totalRead = 0;
260-
long pos = position;
261-
for (int i = (int) (pos / chunkSize); i < buffers.length; i++) {
262-
ByteBuffer buf = buffers[i];
263-
buf.position((int) (pos % chunkSize));
264-
int read = channel.read(buf);
265-
if (read == -1) {
266-
break;
267-
}
268-
totalRead += read;
269-
pos += read;
270-
if (pos >= limit) {
271-
break;
272-
}
273-
}
274-
position = pos;
275-
return totalRead;
276-
}
277-
278212
/** {@inheritDoc} */
279213
@Override
280214
public String decode(CharsetDecoder decoder)

src/main/java/com/maxmind/db/SingleBuffer.java

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,22 @@
1212
*
1313
* <p>This implementation is limited to capacities up to
1414
* {@link Integer#MAX_VALUE}, as {@link ByteBuffer} cannot exceed that size.
15+
*
16+
* <p>The underlying {@link ByteBuffer} is read-only to prevent accidental
17+
* modification of shared data.
1518
*/
1619
class SingleBuffer implements Buffer {
1720

1821
private final ByteBuffer buffer;
1922

20-
/**
21-
* Creates a new {@code SingleBuffer} with the given capacity.
22-
*
23-
* @param capacity the capacity in bytes (must be <= Integer.MAX_VALUE)
24-
* @throws IllegalArgumentException if the capacity exceeds
25-
* {@link Integer#MAX_VALUE}
26-
*/
27-
public SingleBuffer(long capacity) {
28-
if (capacity > Integer.MAX_VALUE) {
29-
throw new IllegalArgumentException(
30-
"SingleBuffer cannot exceed Integer.MAX_VALUE capacity"
31-
);
32-
}
33-
this.buffer = ByteBuffer.allocate((int) capacity);
34-
}
35-
3623
/**
3724
* Creates a new {@code SingleBuffer} wrapping the given {@link ByteBuffer}.
25+
* The buffer is made read-only.
3826
*
3927
* @param buffer the underlying buffer
4028
*/
41-
private SingleBuffer(ByteBuffer buffer) {
42-
this.buffer = buffer;
29+
SingleBuffer(ByteBuffer buffer) {
30+
this.buffer = buffer.asReadOnlyBuffer();
4331
}
4432

4533
/** {@inheritDoc} */
@@ -111,12 +99,6 @@ public SingleBuffer duplicate() {
11199
return new SingleBuffer(this.buffer.duplicate());
112100
}
113101

114-
/** {@inheritDoc} */
115-
@Override
116-
public long readFrom(FileChannel channel) throws IOException {
117-
return channel.read(buffer);
118-
}
119-
120102
/** {@inheritDoc} */
121103
@Override
122104
public String decode(CharsetDecoder decoder)
@@ -145,6 +127,6 @@ public static SingleBuffer wrap(byte[] array) {
145127
public static SingleBuffer mapFromChannel(FileChannel channel)
146128
throws IOException {
147129
ByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size());
148-
return new SingleBuffer(buffer.asReadOnlyBuffer());
130+
return new SingleBuffer(buffer);
149131
}
150132
}

0 commit comments

Comments
 (0)