Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and expand range checks when performing tile retrieval #11

Merged
merged 10 commits into from
Oct 3, 2024
56 changes: 30 additions & 26 deletions src/main/java/com/glencoesoftware/omero/zarr/ZarrPixelBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.IntStream;

Expand Down Expand Up @@ -138,7 +137,7 @@ public ZarrPixelBuffer(Pixels pixels, Path root, Integer maxPlaneWidth,
int h = key.get(7);
int[] shape = new int[] { 1, 1, 1, h, w };
byte[] innerBuffer =
new byte[length(shape) * getByteWidth()];
new byte[(int) length(shape) * getByteWidth()];
setResolutionLevel(resolutionLevel);
return getTileDirect(z, c, t, x, y, w, h, innerBuffer);
});
Expand Down Expand Up @@ -181,25 +180,17 @@ public int getPixelsType() {
* "https://numpy.org/doc/stable/reference/generated/numpy.shape.html">
* numpy.shape</a> documentation
*/
private int length(int[] shape) {
return IntStream.of(shape).reduce(1, Math::multiplyExact);
private long length(int[] shape) {
return IntStream.of(shape)
.mapToLong(a -> (long) a)
.reduce(1, Math::multiplyExact);
}

private void read(byte[] buffer, int[] shape, int[] offset)
throws IOException {
Integer sizeX = shape[4];
Integer sizeY = shape[3];
if((sizeX * sizeY) > (maxPlaneWidth*maxPlaneHeight)) {
throw new IllegalArgumentException(String.format(
"Requested Region Size %d * %d > max plane size %d * %d", sizeX,
sizeY, maxPlaneWidth, maxPlaneHeight));
}
if (sizeX < 0) {
throw new IllegalArgumentException("width < 0");
}
if (sizeY < 0) {
throw new IllegalArgumentException("height < 0");
}
// Check planar read size (sizeX and sizeY only)
checkReadSize(Arrays.copyOfRange(shape, 3, 5));

try {
ByteBuffer asByteBuffer = ByteBuffer.wrap(buffer);
DataType dataType = array.getDataType();
Expand Down Expand Up @@ -326,31 +317,42 @@ public void close() throws IOException {
* Implemented as specified by {@link PixelBuffer} I/F.
* @see PixelBuffer#checkBounds(Integer, Integer, Integer, Integer, Integer)
*/
@Override
public void checkBounds(Integer x, Integer y, Integer z, Integer c,
Integer t)
throws DimensionsOutOfBoundsException {
if (x != null && (x > getSizeX() - 1 || x < 0)) {
throw new DimensionsOutOfBoundsException("X '" + x
+ "' greater than sizeX '" + getSizeX() + "'.");
+ "' greater than sizeX '" + getSizeX() + "' or < '0'.");
}
if (y != null && (y > getSizeY() - 1 || y < 0)) {
throw new DimensionsOutOfBoundsException("Y '" + y
+ "' greater than sizeY '" + getSizeY() + "'.");
+ "' greater than sizeY '" + getSizeY() + "' or < '0'.");
}

if (z != null && (z > getSizeZ() - 1 || z < 0)) {
throw new DimensionsOutOfBoundsException("Z '" + z
+ "' greater than sizeZ '" + getSizeZ() + "'.");
+ "' greater than sizeZ '" + getSizeZ() + "' or < '0'.");
}

if (c != null && (c > getSizeC() - 1 || c < 0)) {
throw new DimensionsOutOfBoundsException("C '" + c
+ "' greater than sizeC '" + getSizeC() + "'.");
+ "' greater than sizeC '" + getSizeC() + "' or < '0'.");
}

if (t != null && (t > getSizeT() - 1 || t < 0)) {
throw new DimensionsOutOfBoundsException("T '" + t
+ "' greater than sizeT '" + getSizeT() + "'.");
+ "' greater than sizeT '" + getSizeT() + "' or < '0'.");
}
}

public void checkReadSize(int[] shape) {
long length = length(shape);
long maxLength = maxPlaneWidth * maxPlaneHeight;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth doing long multiplication here too? Or do we know for sure that maxPlaneWidth * maxPlaneHeight will always be less than Integer.MAX_VALUE?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't know for sure but if you were to configure your server like that all sorts of other stuff would break spectacularly.

if (length > maxLength) {
throw new IllegalArgumentException(String.format(
"Requested shape %s > max plane size %d * %d",
Arrays.toString(shape), maxPlaneWidth, maxPlaneHeight));
}
}

Expand Down Expand Up @@ -454,6 +456,11 @@ public PixelData getTile(
checkBounds(x, y, z, c, t);
//Check check bottom-right of tile in bounds
checkBounds(x + w - 1, y + h - 1, z, c, t);
//Check planar read size (sizeX and sizeY only), essential that this
//happens before the similar check in read(). Otherwise we will
//potentially allocate massive inner buffers in the tile cache
//asynchronous entry builder that will never be used.
checkReadSize(new int[] { w, h });

List<List<Integer>> keys = new ArrayList<List<Integer>>();
List<Integer> key = null;
Expand All @@ -476,10 +483,7 @@ public PixelData getTile(
// of channels can be unpredictable.
tileCache.synchronous().invalidateAll();
}
CompletableFuture<Map<List<Integer>, byte[]>> future =
tileCache.getAll(keys);
Map<List<Integer>, byte[]> values = future.join();
return toPixelData(values.get(key));
return toPixelData(tileCache.getAll(keys).join().get(key));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@

import com.bc.zarr.ZarrArray;
import com.bc.zarr.ZarrGroup;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Caffeine;

import picocli.CommandLine;
import com.glencoesoftware.bioformats2raw.Converter;
import com.glencoesoftware.omero.zarr.ZarrPixelsService;
import com.glencoesoftware.omero.zarr.ZarrPixelBuffer;

import loci.formats.FormatTools;
import loci.formats.in.FakeReader;
Expand Down Expand Up @@ -503,8 +502,39 @@ public void testGetTileLargerThanImage()
}
}

@Test
public void testTileExceedsMax() throws IOException, InvalidRangeException {
@Test(expected = IllegalArgumentException.class)
public void testTileIntegerOverflow()
throws IOException, InvalidRangeException {
int sizeT = 1;
int sizeC = 3;
int sizeZ = 1;
int sizeY = 1;
int sizeX = 1;
int resolutions = 1;
Pixels pixels = new Pixels(
null, null, sizeX, sizeY, sizeZ, sizeC, sizeT, "", null);
Path output = writeTestZarr(
sizeT, sizeC, sizeZ, sizeY, sizeX, "uint16",
resolutions);

// Hack the .zarray so we can appear as though we have more data than
// we actually have written above.
Copy link
Member

@sbesson sbesson Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the rationale if that is we want to generate an OME-Zarr of a given dimensions without actually writing the underlying test data (which increases the test time for no value).

@melissalinkert this makes me wonder whether we could introduce an option in bioformats2raw to generate Zarr datasets with sparse data

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the rationale if that is we want to generate an OME-Zarr of a given dimensions without actually writing the underlying test data (which increases the test time for no value).

Correct. Creating an array large enough to not end up with earlier out of bounds checks can take minutes.

ObjectMapper mapper = new ObjectMapper();
HashMap<String, Object> zArray = mapper.readValue(
Files.readAllBytes(output.resolve("0/0/.zarray")),
HashMap.class);
List<Integer> shape = (List<Integer>) zArray.get("shape");
shape.set(3, 50000);
shape.set(4, 50000);
mapper.writeValue(output.resolve("0/0/.zarray").toFile(), zArray);
try (ZarrPixelBuffer zpbuf =
createPixelBuffer(pixels, output.resolve("0"), 32, 32)) {
zpbuf.getTile(0, 0, 0, 0, 0, 50000, 50000);
}
}

@Test(expected = IllegalArgumentException.class)
public void testTileExceedsMinMax() throws IOException {
int sizeT = 1;
int sizeC = 3;
int sizeZ = 1;
Expand All @@ -519,8 +549,9 @@ public void testTileExceedsMax() throws IOException, InvalidRangeException {

try (ZarrPixelBuffer zpbuf =
createPixelBuffer(pixels, output.resolve("0"), 32, 32)) {
PixelData pixelData = zpbuf.getTile(0, 0, 0, 0, 0, 32, 33);
Assert.assertNull(pixelData);
Assert.assertNull(zpbuf.getTile(0, 0, 0, 0, 0, 32, 33));
// Throws exception
zpbuf.getTile(0, 0, 0, -1, 0, 1, 1);
}
}

Expand Down