diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentWriter.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentWriter.java index abab3d4..ab762f7 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentWriter.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentWriter.java @@ -595,7 +595,12 @@ public class SegmentWriter { byte[] data = reference.getBytes(Charsets.UTF_8); int length = data.length; - checkArgument(length < 8192); + // When writing a binary ID, the four most significant bits of the + // length field should be "1110", leaving 12 other bits to store the + // length itself. This means that the values of the length field can + // only range between 0 and 2^12 - 1. + + checkArgument(length < 4096); RecordId id = prepare(RecordType.VALUE, 2 + length); int len = length | 0xE000; diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/segment/file/ExternalBlobReferenceTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/segment/file/ExternalBlobReferenceTest.java new file mode 100644 index 0000000..afb5a0f --- /dev/null +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/segment/file/ExternalBlobReferenceTest.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.oak.plugins.segment.file; + +import com.google.common.base.Strings; +import org.apache.jackrabbit.oak.plugins.segment.Segment; +import org.apache.jackrabbit.oak.plugins.segment.SegmentBlob; +import org.apache.jackrabbit.oak.spi.blob.BlobStore; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.InputStream; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class ExternalBlobReferenceTest { + + @Rule + public TemporaryFolder segmentFolder = new TemporaryFolder(); + + private FileStore fileStore; + + private BlobStore blobStore; + + @Before + public void createFileStore() throws Exception { + blobStore = mock(BlobStore.class); + fileStore = FileStore.newFileStore(segmentFolder.getRoot()).withBlobStore(blobStore).create(); + } + + @After + public void destroyFileStore() throws Exception { + fileStore.close(); + } + + /** + * The {@code SegmentWriter} should be able to write blob IDs whose length + * is between 0 and 4095 bytes. It should be possible to correctly read the + * blob ID back and pass it to the {@code BlobStore} to obtain information + * about the blob. + *
+ * This code path executes only if the written stream is {@code + * Segment.MEDIUM_LIMIT} bytes long (or more). If the length of the stream + * is smaller, the binary value is inlined in the segment and the {@code + * BlobStore} is never called. + * + * See OAK-3105. + */ + @Test + public void testBlobIdLengthUpperLimit() throws Exception { + String blobId = Strings.repeat("x", 4095); + long blobLength = Segment.MEDIUM_LIMIT; + + doReturn(blobId).when(blobStore).writeBlob(any(InputStream.class)); + doReturn(blobLength).when(blobStore).getBlobLength(blobId); + + SegmentBlob blob = fileStore.getTracker().getWriter().writeStream(newRandomInputStream(blobLength)); + + assertEquals(blobLength, blob.length()); + } + + /** + * If the {@code BlobStore} returns a blob ID whose length is 4096 byes long + * (or more), writing the stream should throw an {@code + * IllegalArgumentException}. + * + * This code path executes only if the written stream is {@code + * Segment.MEDIUM_LIMIT} bytes long (or more). If the length of the stream + * is smaller, the binary value is inlined in the segment and the {@code + * BlobStore} is never called. + * + * See OAK-3105. + */ + @Test(expected = IllegalArgumentException.class) + public void testBlobIdLengthLongerThanUpperLimit() throws Exception { + String blobId = Strings.repeat("x", 4096); + long blobLength = Segment.MEDIUM_LIMIT; + + doReturn(blobId).when(blobStore).writeBlob(any(InputStream.class)); + + fileStore.getTracker().getWriter().writeStream(newRandomInputStream(blobLength)); + } + + private InputStream newRandomInputStream(final long size) { + return new InputStream() { + + private Random random = new Random(); + + private int read = 0; + + @Override + public int read() { + if (read >= size) { + return -1; + } + + read += 1; + + return random.nextInt() & 0xFF; + } + + }; + } + +}