Skip to content

Commit 6df5469

Browse files
authored
feat: allow specifying a negative offset to ReadChannel (#1916)
GCS Supports negative offsets in ranged reads and is documented in https://cloud.google.com/storage/docs/json_api/v1/parameters#range Update the ReadChannel returned from Storage.reader to allow providing a negative value to `seek`. NOTE: `seek` to a negative offset will be ignored by GCS if `limit` is also specified
1 parent b4cdb37 commit 6df5469

6 files changed

Lines changed: 41 additions & 4 deletions

File tree

google-cloud-storage/src/main/java/com/google/cloud/storage/BaseStorageReadChannel.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ public final synchronized int read(ByteBuffer dst) throws IOException {
8888
throw new ClosedChannelException();
8989
}
9090
long diff = byteRangeSpec.length();
91-
if (diff <= 0) {
91+
// the check on beginOffset >= 0 used to be a precondition on seek(long)
92+
// move it here to preserve existing behavior while allowing new negative offsets
93+
if (diff <= 0 && byteRangeSpec.beginOffset() >= 0) {
9294
return -1;
9395
}
9496
try {

google-cloud-storage/src/main/java/com/google/cloud/storage/ByteRangeSpec.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,8 @@ public ReadObjectRequest.Builder seekReadObjectRequest(ReadObjectRequest.Builder
472472
protected String fmtAsHttpRangeHeader() throws ArithmeticException {
473473
if (beginOffset > 0) {
474474
return String.format("bytes=%d-", beginOffset);
475+
} else if (beginOffset < 0) {
476+
return String.format("bytes=%d", beginOffset);
475477
} else {
476478
return null;
477479
}
@@ -510,7 +512,7 @@ long length() throws ArithmeticException {
510512

511513
@Override
512514
ByteRangeSpec withNewBeginOffset(long beginOffset) {
513-
if (beginOffset > 0) {
515+
if (beginOffset != 0) {
514516
return new LeftClosedByteRangeSpec(beginOffset);
515517
} else {
516518
return this;

google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUnbufferedReadableByteChannel.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ private static final class ReadCursor {
173173
private final long limit;
174174

175175
private ReadCursor(long beginning, long limit) {
176-
checkArgument(0 <= beginning && beginning <= limit, "0 <= %d <= %d", beginning, limit);
177176
this.limit = limit;
178177
this.beginning = beginning;
179178
this.offset = beginning;

google-cloud-storage/src/main/java/com/google/cloud/storage/StorageReadChannel.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ default ByteRangeSpec getByteRangeSpec() {
4141
@SuppressWarnings("resource")
4242
@Override
4343
default void seek(long position) throws IOException {
44-
checkArgument(position >= 0, "position must be >= 0");
4544
try {
4645
setByteRangeSpec(getByteRangeSpec().withNewBeginOffset(position));
4746
} catch (StorageException e) {

google-cloud-storage/src/test/java/com/google/cloud/storage/ByteRangeSpecTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ public final class ByteRangeSpecTest {
4545

4646
public static final class Behavior {
4747

48+
@Test
49+
public void negativeBeginOffset() throws Exception {
50+
ByteRangeSpec rel = ByteRangeSpec.relativeLength(-5L, null);
51+
ByteRangeSpec exO = ByteRangeSpec.explicit(-5L, null);
52+
ByteRangeSpec exC = ByteRangeSpec.explicitClosed(-5L, null);
53+
threeWayEqual(exO, exC, rel);
54+
}
55+
56+
@Test
57+
public void negativeBeginOffset_fromNull() {
58+
ByteRangeSpec spec = ByteRangeSpec.nullRange().withNewBeginOffset(-5L);
59+
assertThat(spec.getHttpRangeHeader()).isEqualTo("bytes=-5");
60+
}
61+
4862
@Test
4963
public void beginNonNullZero_endNonNullNonInfinity() throws Exception {
5064
ByteRangeSpec rel = ByteRangeSpec.relativeLength(0L, 52L);

google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadChannelTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,27 @@ public void readingLastByteReturnsOneByte_seekOnly() throws IOException {
459459
}
460460
}
461461

462+
@Test
463+
public void readingLastByteReturnsOneByte_seekOnly_negativeOffset() throws IOException {
464+
int length = 10;
465+
byte[] bytes = DataGenerator.base64Characters().genBytes(length);
466+
467+
BlobInfo info1 = BlobInfo.newBuilder(bucket, generator.randomObjectName()).build();
468+
Blob gen1 = storage.create(info1, bytes, BlobTargetOption.doesNotExist());
469+
470+
byte[] expected1 = Arrays.copyOfRange(bytes, 9, 10);
471+
String xxdExpected1 = xxd(expected1);
472+
try (ReadChannel reader = storage.reader(gen1.getBlobId());
473+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
474+
WritableByteChannel writer = Channels.newChannel(baos)) {
475+
reader.seek(-1);
476+
ByteStreams.copy(reader, writer);
477+
byte[] bytes1 = baos.toByteArray();
478+
String xxd1 = xxd(bytes1);
479+
assertThat(xxd1).isEqualTo(xxdExpected1);
480+
}
481+
}
482+
462483
@Test
463484
public void readingLastByteReturnsOneByte_seekAndLimit() throws IOException {
464485
int length = 10;

0 commit comments

Comments
 (0)