Skip to content

Commit a87110e

Browse files
committed
addressed copilot agent feedback
1 parent 8a8b217 commit a87110e

6 files changed

Lines changed: 68 additions & 14 deletions

File tree

azure-blob-payloads/src/main/java/com/microsoft/durabletask/azureblobpayloads/BlobPayloadStore.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ public boolean isKnownPayloadToken(String value) {
138138
return value.startsWith(TOKEN_PREFIX);
139139
}
140140

141+
/**
142+
* Ensures the blob container exists, creating it if necessary.
143+
* The check-then-act on {@code containerEnsured} is intentionally non-atomic:
144+
* concurrent callers may race through to {@code create()}, but the 409 Conflict
145+
* handler makes this benign.
146+
*/
141147
private void ensureContainerExists() {
142148
if (this.containerEnsured) {
143149
return;
@@ -194,13 +200,25 @@ private static byte[] gzipCompress(byte[] data) {
194200
}
195201
}
196202

203+
/**
204+
* Maximum decompressed size (20 MiB) to guard against decompression bombs.
205+
* This is 2x the default max externalized payload size of 10 MiB.
206+
*/
207+
private static final int MAX_DECOMPRESSED_BYTES = 20 * 1024 * 1024;
208+
197209
private static byte[] gzipDecompress(byte[] compressed) {
198210
try {
199211
ByteArrayOutputStream baos = new ByteArrayOutputStream();
200212
try (GZIPInputStream gzipIn = new GZIPInputStream(new ByteArrayInputStream(compressed))) {
201213
byte[] buffer = new byte[8192];
202214
int len;
215+
int totalRead = 0;
203216
while ((len = gzipIn.read(buffer)) != -1) {
217+
totalRead += len;
218+
if (totalRead > MAX_DECOMPRESSED_BYTES) {
219+
throw new IOException(
220+
"Decompressed payload exceeds safety limit of " + MAX_DECOMPRESSED_BYTES + " bytes");
221+
}
204222
baos.write(buffer, 0, len);
205223
}
206224
}

azure-blob-payloads/src/main/java/com/microsoft/durabletask/azureblobpayloads/BlobPayloadStoreOptions.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public static final class Builder {
138138
/**
139139
* Sets the Azure Storage connection string. Mutually exclusive with
140140
* {@link #setBlobServiceEndpoint(String)} and {@link #setBlobServiceClient(BlobServiceClient)}.
141+
* Setting this clears any previously set endpoint or pre-configured client.
141142
*
142143
* @param connectionString the connection string
143144
* @return this builder
@@ -147,12 +148,16 @@ public Builder setConnectionString(String connectionString) {
147148
throw new IllegalArgumentException("connectionString must not be null or empty");
148149
}
149150
this.connectionString = connectionString;
151+
this.blobServiceEndpoint = null;
152+
this.credential = null;
153+
this.blobServiceClient = null;
150154
return this;
151155
}
152156

153157
/**
154158
* Sets the Azure Blob Storage service endpoint. Use with {@link #setCredential(TokenCredential)}.
155159
* Mutually exclusive with {@link #setConnectionString(String)} and {@link #setBlobServiceClient(BlobServiceClient)}.
160+
* Setting this clears any previously set connection string or pre-configured client.
156161
*
157162
* @param blobServiceEndpoint the blob service endpoint URL
158163
* @return this builder
@@ -162,6 +167,8 @@ public Builder setBlobServiceEndpoint(String blobServiceEndpoint) {
162167
throw new IllegalArgumentException("blobServiceEndpoint must not be null or empty");
163168
}
164169
this.blobServiceEndpoint = blobServiceEndpoint;
170+
this.connectionString = null;
171+
this.blobServiceClient = null;
165172
return this;
166173
}
167174

@@ -183,6 +190,7 @@ public Builder setCredential(TokenCredential credential) {
183190
/**
184191
* Sets a pre-configured BlobServiceClient. Mutually exclusive with
185192
* {@link #setConnectionString(String)} and {@link #setBlobServiceEndpoint(String)}.
193+
* Setting this clears any previously set connection string or endpoint.
186194
*
187195
* @param blobServiceClient the pre-configured client
188196
* @return this builder
@@ -192,6 +200,9 @@ public Builder setBlobServiceClient(BlobServiceClient blobServiceClient) {
192200
throw new IllegalArgumentException("blobServiceClient must not be null");
193201
}
194202
this.blobServiceClient = blobServiceClient;
203+
this.connectionString = null;
204+
this.blobServiceEndpoint = null;
205+
this.credential = null;
195206
return this;
196207
}
197208

client/src/main/java/com/microsoft/durabletask/LargePayloadOptions.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ public final class LargePayloadOptions {
2424
/**
2525
* Default externalization threshold in bytes (900,000 bytes, matching .NET SDK).
2626
*/
27-
static final int DEFAULT_THRESHOLD_BYTES = 900_000;
27+
public static final int DEFAULT_THRESHOLD_BYTES = 900_000;
2828

2929
/**
3030
* Default maximum externalized payload size in bytes (10 MiB, matching .NET SDK).
3131
*/
32-
static final int DEFAULT_MAX_EXTERNALIZED_PAYLOAD_BYTES = 10 * 1024 * 1024;
32+
public static final int DEFAULT_MAX_EXTERNALIZED_PAYLOAD_BYTES = 10 * 1024 * 1024;
3333

3434
private final int thresholdBytes;
3535
private final int maxExternalizedPayloadBytes;
@@ -40,8 +40,8 @@ private LargePayloadOptions(Builder builder) {
4040
}
4141

4242
/**
43-
* Gets the size threshold in bytes above which payloads will be externalized.
44-
* Payloads at or below this size are sent inline. The comparison uses UTF-8 byte length.
43+
* Gets the size threshold in bytes at or above which payloads will be externalized.
44+
* Payloads below this size are sent inline. The comparison uses UTF-8 byte length.
4545
*
4646
* @return the externalization threshold in bytes
4747
*/

client/src/main/java/com/microsoft/durabletask/PayloadHelper.java

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// Licensed under the MIT License.
33
package com.microsoft.durabletask;
44

5-
import java.nio.charset.StandardCharsets;
6-
75
/**
86
* Internal utility for externalizing and resolving payloads using a {@link PayloadStore}.
97
* <p>
@@ -40,24 +38,24 @@ final class PayloadHelper {
4038
*
4139
* @param value the payload string to potentially externalize
4240
* @return the original value if below threshold, or an opaque token if externalized
43-
* @throws IllegalArgumentException if the payload exceeds the maximum externalized payload size
41+
* @throws PayloadTooLargeException if the payload exceeds the maximum externalized payload size
4442
*/
4543
String maybeExternalize(String value) {
4644
// (1) null/empty guard
4745
if (value == null || value.isEmpty()) {
4846
return value;
4947
}
5048

51-
// Fast path: if char count is below threshold, byte count is too
52-
// (each Java char encodes to 1-3 UTF-8 bytes, so length() <= UTF-8 byte length)
53-
if (value.length() <= this.options.getThresholdBytes()) {
49+
// Fast path: each Java char contributes at least 1 UTF-8 byte,
50+
// so length() is always <= UTF-8 byte length.
51+
if (value.length() < this.options.getThresholdBytes()) {
5452
return value;
5553
}
5654

57-
int byteSize = value.getBytes(StandardCharsets.UTF_8).length;
55+
int byteSize = utf8ByteLength(value);
5856

59-
// (2) below-threshold guard
60-
if (byteSize <= this.options.getThresholdBytes()) {
57+
// (2) below-threshold guard (strict less-than, matching .NET)
58+
if (byteSize < this.options.getThresholdBytes()) {
6159
return value;
6260
}
6361

@@ -96,4 +94,26 @@ String maybeResolve(String value) {
9694
}
9795
return resolved;
9896
}
97+
98+
/**
99+
* Counts the number of UTF-8 bytes needed to encode the given string,
100+
* without allocating a byte array (unlike {@code String.getBytes(UTF_8).length}).
101+
*/
102+
private static int utf8ByteLength(String s) {
103+
int count = 0;
104+
for (int i = 0; i < s.length(); i++) {
105+
char c = s.charAt(i);
106+
if (c <= 0x7F) {
107+
count++;
108+
} else if (c <= 0x7FF) {
109+
count += 2;
110+
} else if (Character.isHighSurrogate(c)) {
111+
count += 4;
112+
i++; // skip low surrogate
113+
} else {
114+
count += 3;
115+
}
116+
}
117+
return count;
118+
}
99119
}

client/src/main/java/com/microsoft/durabletask/PayloadStore.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
* Blob Storage, configure
2020
* <a href="https://learn.microsoft.com/azure/storage/blobs/lifecycle-management-overview">
2121
* lifecycle management policies</a> to automatically expire old payloads.
22+
* <p>
23+
* <b>Performance note:</b> All methods on this interface are synchronous. Implementations
24+
* that perform network I/O (e.g., Azure Blob Storage) should ensure that latency is
25+
* acceptable on the calling thread. The framework calls these methods on the worker's
26+
* processing thread.
2227
*
2328
* @see LargePayloadOptions
2429
*/

client/src/test/java/com/microsoft/durabletask/PayloadHelperTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ void maybeExternalize_exceedsMaxCap_throwsException() {
106106

107107
// Create a payload larger than 50 bytes
108108
String hugePayload = "x".repeat(100);
109-
assertThrows(IllegalArgumentException.class, () -> helper.maybeExternalize(hugePayload));
109+
assertThrows(PayloadTooLargeException.class, () -> helper.maybeExternalize(hugePayload));
110110
assertEquals(0, store.getUploadCount());
111111
}
112112

0 commit comments

Comments
 (0)