Skip to content

Commit 09c7d03

Browse files
committed
Added an "unsafe" way to retrieve a byte array from SdkBytes and ResponseBytes without copying the data.
1 parent 495001b commit 09c7d03

File tree

7 files changed

+170
-11
lines changed

7 files changed

+170
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"description": "Added an \"unsafe\" way to retrieve a byte array from `SdkBytes` and `ResponseBytes` without copying the data."
5+
}

build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2" />
3333
</Match>
3434

35+
<Match>
36+
<!-- Explicitly an unsafe option that the customer must opt into. -->
37+
<Class name="software.amazon.awssdk.core.BytesWrapper" />
38+
<Method name="asByteArrayUnsafe" />
39+
<Bug pattern="EI_EXPOSE_REP" />
40+
</Match>
41+
3542
<!-- Delegate closes input stream. -->
3643
<Match>
3744
<Class name="software.amazon.awssdk.protocols.ion.internal.IonFactory" />

core/sdk-core/src/main/java/software/amazon/awssdk/core/BytesWrapper.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,19 @@
3636
*/
3737
@SdkPublicApi
3838
public abstract class BytesWrapper {
39-
private static final byte[] EMPTY_BYTES = new byte[0];
40-
4139
private final byte[] bytes;
4240

4341
// Needed for serialization
4442
@SdkInternalApi
4543
BytesWrapper() {
46-
this(EMPTY_BYTES);
44+
this(new byte[0]);
4745
}
4846

4947
@SdkInternalApi
5048
BytesWrapper(byte[] bytes) {
5149
this.bytes = Validate.paramNotNull(bytes, "bytes");
5250
}
5351

54-
final byte[] wrappedBytes() {
55-
return bytes;
56-
}
57-
5852
/**
5953
* @return The output as a read-only byte buffer.
6054
*/
@@ -70,6 +64,22 @@ public final byte[] asByteArray() {
7064
return Arrays.copyOf(bytes, bytes.length);
7165
}
7266

67+
/**
68+
* @return The output as a byte array. This <b>does not</b> create a copy of the underlying byte array. This introduces
69+
* concurrency risks, allowing: (1) the caller to modify the byte array stored in this object implementation AND
70+
* (2) the original creator of this object, if they created it using the unsafe method.
71+
*
72+
* <p>Consider using {@link #asByteBuffer()}, which is a safer method to avoid an additional array copy because it does not
73+
* provide a way to modify the underlying buffer. As the method name implies, this is unsafe. If you're not sure, don't use
74+
* this. The only guarantees given to the user of this method is that the SDK itself won't modify the underlying byte
75+
* array.</p>
76+
*
77+
* @see #asByteBuffer() to prevent creating an additional array copy safely.
78+
*/
79+
public final byte[] asByteArrayUnsafe() {
80+
return bytes;
81+
}
82+
7383
/**
7484
* Retrieve the output as a string.
7585
*

core/sdk-core/src/main/java/software/amazon/awssdk/core/ResponseBytes.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515

1616
package software.amazon.awssdk.core;
1717

18+
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
19+
20+
import java.io.InputStream;
21+
import java.io.UncheckedIOException;
1822
import java.util.Arrays;
1923
import software.amazon.awssdk.annotations.SdkPublicApi;
2024
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
2125
import software.amazon.awssdk.core.sync.ResponseTransformer;
26+
import software.amazon.awssdk.utils.IoUtils;
2227
import software.amazon.awssdk.utils.ToString;
2328
import software.amazon.awssdk.utils.Validate;
2429

@@ -31,15 +36,38 @@
3136
public final class ResponseBytes<ResponseT> extends BytesWrapper {
3237
private final ResponseT response;
3338

34-
ResponseBytes(ResponseT response, byte[] bytes) {
39+
private ResponseBytes(ResponseT response, byte[] bytes) {
3540
super(bytes);
3641
this.response = Validate.paramNotNull(response, "response");
3742
}
3843

44+
/**
45+
* Create {@link ResponseBytes} from a Byte array. This will copy the contents of the byte array.
46+
*/
47+
public static <ResponseT> ResponseBytes<ResponseT> fromInputStream(ResponseT response, InputStream stream)
48+
throws UncheckedIOException {
49+
return new ResponseBytes<>(response, invokeSafely(() -> IoUtils.toByteArray(stream)));
50+
}
51+
52+
/**
53+
* Create {@link ResponseBytes} from a Byte array. This will copy the contents of the byte array.
54+
*/
3955
public static <ResponseT> ResponseBytes<ResponseT> fromByteArray(ResponseT response, byte[] bytes) {
4056
return new ResponseBytes<>(response, Arrays.copyOf(bytes, bytes.length));
4157
}
4258

59+
/**
60+
* Create {@link ResponseBytes} from a Byte array <b>without</b> copying the contents of the byte array. This introduces
61+
* concurrency risks, allowing: (1) the caller to modify the byte array stored in this {@code SdkBytes} implementation AND
62+
* (2) any users of {@link #asByteArrayUnsafe()} to modify the byte array passed into this {@code SdkBytes} implementation.
63+
*
64+
* <p>As the method name implies, this is unsafe. Use {@link #fromByteArray(Object, byte[])} unless you're sure you know the
65+
* risks.
66+
*/
67+
public static <ResponseT> ResponseBytes<ResponseT> fromByteArrayUnsafe(ResponseT response, byte[] bytes) {
68+
return new ResponseBytes<>(response, bytes);
69+
}
70+
4371
/**
4472
* @return the unmarshalled response object from the service.
4573
*/
@@ -51,7 +79,7 @@ public ResponseT response() {
5179
public String toString() {
5280
return ToString.builder("ResponseBytes")
5381
.add("response", response)
54-
.add("bytes", wrappedBytes())
82+
.add("bytes", asByteArrayUnsafe())
5583
.build();
5684
}
5785

core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkBytes.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private SdkBytes() {
5252
* @see #fromUtf8String(String)
5353
* @see #fromString(String, Charset)
5454
*/
55-
SdkBytes(byte[] bytes) {
55+
private SdkBytes(byte[] bytes) {
5656
super(bytes);
5757
}
5858

@@ -72,6 +72,18 @@ public static SdkBytes fromByteArray(byte[] bytes) {
7272
return new SdkBytes(Arrays.copyOf(bytes, bytes.length));
7373
}
7474

75+
/**
76+
* Create {@link SdkBytes} from a Byte array <b>without</b> copying the contents of the byte array. This introduces
77+
* concurrency risks, allowing: (1) the caller to modify the byte array stored in this {@code SdkBytes} implementation AND
78+
* (2) any users of {@link #asByteArrayUnsafe()} to modify the byte array passed into this {@code SdkBytes} implementation.
79+
*
80+
* <p>As the method name implies, this is unsafe. Use {@link #fromByteArray(byte[])} unless you're sure you know the risks.
81+
*/
82+
public static SdkBytes fromByteArrayUnsafe(byte[] bytes) {
83+
Validate.paramNotNull(bytes, "bytes");
84+
return new SdkBytes(bytes);
85+
}
86+
7587
/**
7688
* Create {@link SdkBytes} from a string, using the provided charset.
7789
*/
@@ -100,7 +112,7 @@ public static SdkBytes fromInputStream(InputStream inputStream) {
100112
@Override
101113
public String toString() {
102114
return ToString.builder("SdkBytes")
103-
.add("bytes", wrappedBytes())
115+
.add("bytes", asByteArrayUnsafe())
104116
.build();
105117
}
106118
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import org.junit.Test;
21+
22+
public class ResponseBytesTest {
23+
private static final Object OBJECT = new Object();
24+
@Test
25+
public void fromByteArrayCreatesCopy() {
26+
byte[] input = new byte[] { 'a' };
27+
byte[] output = ResponseBytes.fromByteArray(OBJECT, input).asByteArrayUnsafe();
28+
29+
input[0] = 'b';
30+
assertThat(output).isNotEqualTo(input);
31+
}
32+
33+
@Test
34+
public void asByteArrayCreatesCopy() {
35+
byte[] input = new byte[] { 'a' };
36+
byte[] output = ResponseBytes.fromByteArrayUnsafe(OBJECT, input).asByteArray();
37+
38+
input[0] = 'b';
39+
assertThat(output).isNotEqualTo(input);
40+
}
41+
42+
@Test
43+
public void fromByteArrayUnsafeAndAsByteArrayUnsafeDoNotCopy() {
44+
byte[] input = new byte[] { 'a' };
45+
byte[] output = ResponseBytes.fromByteArrayUnsafe(OBJECT, input).asByteArrayUnsafe();
46+
47+
assertThat(output).isSameAs(input);
48+
}
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import org.junit.Test;
21+
22+
public class SdkBytesTest {
23+
@Test
24+
public void fromByteArrayCreatesCopy() {
25+
byte[] input = new byte[] { 'a' };
26+
byte[] output = SdkBytes.fromByteArray(input).asByteArrayUnsafe();
27+
28+
input[0] = 'b';
29+
assertThat(output).isNotEqualTo(input);
30+
}
31+
32+
@Test
33+
public void asByteArrayCreatesCopy() {
34+
byte[] input = new byte[] { 'a' };
35+
byte[] output = SdkBytes.fromByteArrayUnsafe(input).asByteArray();
36+
37+
input[0] = 'b';
38+
assertThat(output).isNotEqualTo(input);
39+
}
40+
41+
@Test
42+
public void fromByteArrayUnsafeAndAsByteArrayUnsafeDoNotCopy() {
43+
byte[] input = new byte[] { 'a' };
44+
byte[] output = SdkBytes.fromByteArrayUnsafe(input).asByteArrayUnsafe();
45+
46+
assertThat(output).isSameAs(input);
47+
}
48+
}

0 commit comments

Comments
 (0)