Skip to content

Commit 24b52bd

Browse files
authored
Handle reading more than 2GiB part data correctly. (#1205)
Fixes #1204 Signed-off-by: Bala.FA <bala.gluster@gmail.com>
1 parent c27005f commit 24b52bd

File tree

8 files changed

+506
-198
lines changed

8 files changed

+506
-198
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2021 MinIO, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.InputStream;
23+
24+
/** ByteArrayOutputStream exposes underneath buffer as input stream. */
25+
class ByteBufferStream extends ByteArrayOutputStream {
26+
public ByteBufferStream() {
27+
super();
28+
}
29+
30+
public InputStream inputStream() {
31+
return new ByteArrayInputStream(this.buf, 0, this.count);
32+
}
33+
}

api/src/main/java/io/minio/Digest.java

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,51 +25,48 @@
2525
import java.nio.charset.StandardCharsets;
2626
import java.security.MessageDigest;
2727
import java.security.NoSuchAlgorithmException;
28+
import java.util.Base64;
2829
import java.util.Locale;
2930

3031
/** Various global static functions used. */
3132
public class Digest {
33+
// MD5 hash of zero length byte array.
34+
public static final String ZERO_MD5_HASH = "1B2M2Y8AsgTpgAmY7PhCfg==";
35+
// SHA-256 hash of zero length byte array.
36+
public static final String ZERO_SHA256_HASH =
37+
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
38+
3239
/** Private constructor. */
3340
private Digest() {}
3441

35-
/** Returns SHA-256 hash of given string. */
36-
public static String sha256Hash(String string) throws NoSuchAlgorithmException {
37-
byte[] data = string.getBytes(StandardCharsets.UTF_8);
38-
MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
39-
sha256Digest.update((byte[]) data, 0, data.length);
40-
return BaseEncoding.base16().encode(sha256Digest.digest()).toLowerCase(Locale.US);
42+
/** Returns MD5 hash of byte array. */
43+
public static String md5Hash(byte[] data, int length) throws NoSuchAlgorithmException {
44+
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
45+
md5Digest.update(data, 0, length);
46+
return Base64.getEncoder().encodeToString(md5Digest.digest());
4147
}
4248

43-
/**
44-
* Returns SHA-256 hash of given data and it's length.
45-
*
46-
* @param data must be {@link RandomAccessFile}, {@link BufferedInputStream} or byte array.
47-
* @param len length of data to be read for hash calculation.
48-
*/
49-
public static String sha256Hash(Object data, int len)
50-
throws NoSuchAlgorithmException, IOException, InsufficientDataException, InternalException {
49+
/** Returns SHA-256 hash of byte array. */
50+
public static String sha256Hash(byte[] data, int length) throws NoSuchAlgorithmException {
5151
MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
52-
53-
if (data instanceof BufferedInputStream || data instanceof RandomAccessFile) {
54-
updateDigests(data, len, sha256Digest, null);
55-
} else if (data instanceof byte[]) {
56-
sha256Digest.update((byte[]) data, 0, len);
57-
} else {
58-
throw new InternalException(
59-
"Unknown data source to calculate SHA-256 hash. This should not happen, "
60-
+ "please report this issue at https://github.com/minio/minio-java/issues",
61-
null);
62-
}
63-
52+
sha256Digest.update((byte[]) data, 0, length);
6453
return BaseEncoding.base16().encode(sha256Digest.digest()).toLowerCase(Locale.US);
6554
}
6655

56+
/** Returns SHA-256 hash of given string. */
57+
public static String sha256Hash(String string) throws NoSuchAlgorithmException {
58+
byte[] data = string.getBytes(StandardCharsets.UTF_8);
59+
return sha256Hash(data, data.length);
60+
}
61+
6762
/**
6863
* Returns SHA-256 and MD5 hashes of given data and it's length.
6964
*
7065
* @param data must be {@link RandomAccessFile}, {@link BufferedInputStream} or byte array.
7166
* @param len length of data to be read for hash calculation.
67+
* @deprecated This method is no longer supported.
7268
*/
69+
@Deprecated
7370
public static String[] sha256Md5Hashes(Object data, int len)
7471
throws NoSuchAlgorithmException, IOException, InsufficientDataException, InternalException {
7572
MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
@@ -93,30 +90,6 @@ public static String[] sha256Md5Hashes(Object data, int len)
9390
};
9491
}
9592

96-
/**
97-
* Returns MD5 hash of given data and it's length.
98-
*
99-
* @param data must be {@link RandomAccessFile}, {@link BufferedInputStream} or byte array.
100-
* @param len length of data to be read for hash calculation.
101-
*/
102-
public static String md5Hash(Object data, int len)
103-
throws NoSuchAlgorithmException, IOException, InsufficientDataException, InternalException {
104-
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
105-
106-
if (data instanceof BufferedInputStream || data instanceof RandomAccessFile) {
107-
updateDigests(data, len, null, md5Digest);
108-
} else if (data instanceof byte[]) {
109-
md5Digest.update((byte[]) data, 0, len);
110-
} else {
111-
throw new InternalException(
112-
"Unknown data source to calculate MD5 hash. This should not happen, "
113-
+ "please report this issue at https://github.com/minio/minio-java/issues",
114-
null);
115-
}
116-
117-
return BaseEncoding.base64().encode(md5Digest.digest());
118-
}
119-
12093
/** Updated MessageDigest with bytes read from file and stream. */
12194
private static int updateDigests(
12295
Object inputStream, int len, MessageDigest sha256Digest, MessageDigest md5Digest)

api/src/main/java/io/minio/HttpRequestBody.java

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,20 @@
1616

1717
package io.minio;
1818

19-
import java.io.BufferedInputStream;
2019
import java.io.IOException;
21-
import java.io.RandomAccessFile;
22-
import java.nio.channels.Channels;
2320
import okhttp3.MediaType;
2421
import okhttp3.RequestBody;
2522
import okio.BufferedSink;
26-
import okio.Okio;
2723

2824
/** RequestBody that wraps a single data object. */
2925
class HttpRequestBody extends RequestBody {
30-
private RandomAccessFile file = null;
31-
private BufferedInputStream stream = null;
32-
private byte[] bytes = null;
33-
private int length = -1;
34-
private String contentType = null;
26+
private PartSource partSource;
27+
private byte[] bytes;
28+
private int length;
29+
private String contentType;
3530

36-
HttpRequestBody(final RandomAccessFile file, final int length, final String contentType) {
37-
this.file = file;
38-
this.length = length;
39-
this.contentType = contentType;
40-
}
41-
42-
HttpRequestBody(final BufferedInputStream stream, final int length, final String contentType) {
43-
this.stream = stream;
44-
this.length = length;
31+
HttpRequestBody(final PartSource partSource, final String contentType) {
32+
this.partSource = partSource;
4533
this.contentType = contentType;
4634
}
4735

@@ -54,28 +42,19 @@ class HttpRequestBody extends RequestBody {
5442
@Override
5543
public MediaType contentType() {
5644
MediaType mediaType = null;
57-
58-
if (contentType != null) {
59-
mediaType = MediaType.parse(contentType);
60-
}
61-
if (mediaType == null) {
62-
mediaType = MediaType.parse("application/octet-stream");
63-
}
64-
65-
return mediaType;
45+
if (contentType != null) mediaType = MediaType.parse(contentType);
46+
return (mediaType == null) ? MediaType.parse("application/octet-stream") : mediaType;
6647
}
6748

6849
@Override
6950
public long contentLength() {
70-
return length;
51+
return (partSource != null) ? partSource.size() : length;
7152
}
7253

7354
@Override
7455
public void writeTo(BufferedSink sink) throws IOException {
75-
if (file != null) {
76-
sink.write(Okio.source(Channels.newInputStream(file.getChannel())), length);
77-
} else if (stream != null) {
78-
sink.write(Okio.source(stream), length);
56+
if (partSource != null) {
57+
sink.write(partSource.source(), partSource.size());
7958
} else {
8059
sink.write(bytes, 0, length);
8160
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage,
3+
* (C) 2021 MinIO, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package io.minio;
19+
20+
import com.google.common.io.BaseEncoding;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.io.RandomAccessFile;
24+
import java.security.MessageDigest;
25+
import java.security.NoSuchAlgorithmException;
26+
import java.util.Base64;
27+
import java.util.Locale;
28+
import java.util.Objects;
29+
import javax.annotation.Nonnull;
30+
31+
/** PartReader reads part data from file or input stream sequentially and returns PartSource. */
32+
class PartReader {
33+
private static final long CHUNK_SIZE = Integer.MAX_VALUE;
34+
35+
private byte[] buf16k = new byte[16384]; // 16KiB buffer for optimization.
36+
37+
private RandomAccessFile file;
38+
private InputStream stream;
39+
40+
private long objectSize;
41+
private long partSize;
42+
private int partCount;
43+
44+
private int partNumber;
45+
private long totalDataRead;
46+
47+
private ByteBufferStream[] buffers;
48+
private byte[] oneByte = null;
49+
boolean eof;
50+
51+
private PartReader(long objectSize, long partSize, int partCount) {
52+
this.objectSize = objectSize;
53+
this.partSize = partSize;
54+
this.partCount = partCount;
55+
56+
long bufferCount = partSize / CHUNK_SIZE;
57+
if ((partSize - (bufferCount * CHUNK_SIZE)) > 0) bufferCount++;
58+
if (bufferCount == 0) bufferCount++;
59+
60+
this.buffers = new ByteBufferStream[(int) bufferCount];
61+
}
62+
63+
public PartReader(@Nonnull RandomAccessFile file, long objectSize, long partSize, int partCount) {
64+
this(objectSize, partSize, partCount);
65+
this.file = Objects.requireNonNull(file, "file must not be null");
66+
if (this.objectSize < 0) throw new IllegalArgumentException("object size must be provided");
67+
}
68+
69+
public PartReader(@Nonnull InputStream stream, long objectSize, long partSize, int partCount) {
70+
this(objectSize, partSize, partCount);
71+
this.stream = Objects.requireNonNull(stream, "stream must not be null");
72+
for (int i = 0; i < this.buffers.length; i++) this.buffers[i] = new ByteBufferStream();
73+
}
74+
75+
private long readStreamChunk(
76+
ByteBufferStream buffer, long size, MessageDigest md5, MessageDigest sha256)
77+
throws IOException {
78+
long totalBytesRead = 0;
79+
80+
if (this.oneByte != null) {
81+
buffer.write(this.oneByte);
82+
md5.update(this.oneByte);
83+
if (sha256 != null) sha256.update(this.oneByte);
84+
totalBytesRead++;
85+
this.oneByte = null;
86+
}
87+
88+
while (totalBytesRead < size) {
89+
long bytesToRead = size - totalBytesRead;
90+
if (bytesToRead > this.buf16k.length) bytesToRead = this.buf16k.length;
91+
int bytesRead = this.stream.read(this.buf16k, 0, (int) bytesToRead);
92+
this.eof = (bytesRead < 0);
93+
if (this.eof) {
94+
if (this.objectSize < 0) break;
95+
throw new IOException("unexpected EOF");
96+
}
97+
buffer.write(this.buf16k, 0, bytesRead);
98+
md5.update(this.buf16k, 0, bytesRead);
99+
if (sha256 != null) sha256.update(this.buf16k, 0, bytesRead);
100+
totalBytesRead += bytesRead;
101+
}
102+
103+
return totalBytesRead;
104+
}
105+
106+
private long readStream(long size, MessageDigest md5, MessageDigest sha256) throws IOException {
107+
long count = size / CHUNK_SIZE;
108+
long lastChunkSize = size - (count * CHUNK_SIZE);
109+
if (lastChunkSize > 0) {
110+
count++;
111+
} else {
112+
lastChunkSize = CHUNK_SIZE;
113+
}
114+
115+
long totalBytesRead = 0;
116+
for (int i = 0; i < buffers.length; i++) buffers[i].reset();
117+
for (long i = 1; i <= count && !this.eof; i++) {
118+
long chunkSize = (i != count) ? CHUNK_SIZE : lastChunkSize;
119+
long bytesRead = this.readStreamChunk(buffers[(int) (i - 1)], chunkSize, md5, sha256);
120+
totalBytesRead += bytesRead;
121+
}
122+
123+
if (!this.eof && this.objectSize < 0) {
124+
this.oneByte = new byte[1];
125+
this.eof = this.stream.read(this.oneByte) < 0;
126+
}
127+
128+
return totalBytesRead;
129+
}
130+
131+
private long readFile(long size, MessageDigest md5, MessageDigest sha256) throws IOException {
132+
long position = this.file.getFilePointer();
133+
long totalBytesRead = 0;
134+
135+
while (totalBytesRead < size) {
136+
long bytesToRead = size - totalBytesRead;
137+
if (bytesToRead > this.buf16k.length) bytesToRead = this.buf16k.length;
138+
int bytesRead = this.file.read(this.buf16k, 0, (int) bytesToRead);
139+
if (bytesRead < 0) throw new IOException("unexpected EOF");
140+
md5.update(this.buf16k, 0, bytesRead);
141+
if (sha256 != null) sha256.update(this.buf16k, 0, bytesRead);
142+
totalBytesRead += bytesRead;
143+
}
144+
145+
this.file.seek(position);
146+
return totalBytesRead;
147+
}
148+
149+
private long read(long size, MessageDigest md5, MessageDigest sha256) throws IOException {
150+
return (this.file != null) ? readFile(size, md5, sha256) : readStream(size, md5, sha256);
151+
}
152+
153+
public PartSource getPart(boolean computeSha256) throws NoSuchAlgorithmException, IOException {
154+
if (this.partNumber == this.partCount) return null;
155+
156+
this.partNumber++;
157+
158+
MessageDigest md5 = MessageDigest.getInstance("MD5");
159+
MessageDigest sha256 = computeSha256 ? MessageDigest.getInstance("SHA-256") : null;
160+
161+
long partSize = this.partSize;
162+
if (this.partNumber == this.partCount) partSize = this.objectSize - this.totalDataRead;
163+
long bytesRead = this.read(partSize, md5, sha256);
164+
this.totalDataRead += bytesRead;
165+
if (this.objectSize < 0 && this.eof) this.partCount = this.partNumber;
166+
167+
String md5Hash = Base64.getEncoder().encodeToString(md5.digest());
168+
String sha256Hash = null;
169+
if (computeSha256) {
170+
sha256Hash = BaseEncoding.base16().encode(sha256.digest()).toLowerCase(Locale.US);
171+
}
172+
173+
if (this.file != null) {
174+
return new PartSource(this.partNumber, this.file, bytesRead, md5Hash, sha256Hash);
175+
}
176+
177+
return new PartSource(this.partNumber, this.buffers, bytesRead, md5Hash, sha256Hash);
178+
}
179+
180+
public int partCount() {
181+
return this.partCount;
182+
}
183+
}

0 commit comments

Comments
 (0)