From 062879ea7a6662b9b2c41761cf998f4db54bd012 Mon Sep 17 00:00:00 2001 From: hahmann Date: Mon, 21 Sep 2026 14:57:26 +0200 Subject: [PATCH] Support gzip and zlib 'level' of -1 in v2 metadata Other implementations write zlib's Z_DEFAULT_COMPRESSION (-1) as the 'level' of gzip and zlib, meaning "let the library choose". Both codecs rejected it while parsing .zarray, so such an array could not be opened. Deflater takes -1 as-is, so only the lower bound of the check moves. Reported for gzip in https://github.com/mobie/mobie-io/issues/181. Co-Authored-By: Claude Opus 5 (1M context) --- .../zarrjava/v2/codec/core/GzipCodec.java | 5 +-- .../zarrjava/v2/codec/core/ZlibCodec.java | 4 +-- .../java/dev/zarr/zarrjava/ZarrV2Test.java | 35 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/java/dev/zarr/zarrjava/v2/codec/core/GzipCodec.java b/src/main/java/dev/zarr/zarrjava/v2/codec/core/GzipCodec.java index 42e8f505..3579c22f 100644 --- a/src/main/java/dev/zarr/zarrjava/v2/codec/core/GzipCodec.java +++ b/src/main/java/dev/zarr/zarrjava/v2/codec/core/GzipCodec.java @@ -8,6 +8,7 @@ import dev.zarr.zarrjava.v2.codec.Codec; import java.nio.ByteBuffer; +import java.util.zip.Deflater; public class GzipCodec extends dev.zarr.zarrjava.core.codec.core.GzipCodec implements Codec { @@ -20,8 +21,8 @@ public class GzipCodec extends dev.zarr.zarrjava.core.codec.core.GzipCodec imple public GzipCodec( @JsonProperty(value = "level", defaultValue = "" + DEFAULT_LEVEL) int level) throws ZarrException { - if (level < 0 || level > 9) { - throw new ZarrException("'level' needs to be between 0 and 9."); + if (level < Deflater.DEFAULT_COMPRESSION || level > 9) { + throw new ZarrException("'level' needs to be between -1 and 9."); } this.level = level; } diff --git a/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZlibCodec.java b/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZlibCodec.java index 4e62817e..4117952c 100644 --- a/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZlibCodec.java +++ b/src/main/java/dev/zarr/zarrjava/v2/codec/core/ZlibCodec.java @@ -27,8 +27,8 @@ public class ZlibCodec extends BytesBytesCodec implements Codec { @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ZlibCodec( @JsonProperty(value = "level", defaultValue = "1") int level) throws ZarrException { - if (level < 0 || level > 9) { - throw new ZarrException("'level' needs to be between 0 and 9."); + if (level < Deflater.DEFAULT_COMPRESSION || level > 9) { + throw new ZarrException("'level' needs to be between -1 and 9."); } this.level = level; } diff --git a/src/test/java/dev/zarr/zarrjava/ZarrV2Test.java b/src/test/java/dev/zarr/zarrjava/ZarrV2Test.java index 2d7c4bcc..310fae62 100644 --- a/src/test/java/dev/zarr/zarrjava/ZarrV2Test.java +++ b/src/test/java/dev/zarr/zarrjava/ZarrV2Test.java @@ -108,6 +108,41 @@ public void testReadBloscAutoShuffle(String dataType, String expectedShuffle) Assertions.assertEquals(127, readData.getInt(127)); } + @ParameterizedTest + @ValueSource(strings = {"gzip", "zlib"}) + public void testReadDefaultCompressionLevel(String compressorId) + throws IOException, ZarrException { + String arrayName = "v2_default_level_" + compressorId; + StoreHandle storeHandle = new FilesystemStore(TESTOUTPUT).resolve(arrayName); + ucar.ma2.Array testData = + ucar.ma2.Array.factory(ucar.ma2.DataType.UBYTE, new int[]{16, 16}); + for (int i = 0; i < testData.getSize(); i++) { + testData.setInt(i, i % 128); + } + ArrayMetadataBuilder metadataBuilder = Array.metadataBuilder() + .withShape(16, 16) + .withDataType(DataType.UINT8) + .withChunks(8, 8); + Array array = Array.create( + storeHandle, + (compressorId.equals("gzip") + ? metadataBuilder.withGzipCompressor(1) + : metadataBuilder.withZlibCompressor(1)).build() + ); + array.write(testData); + + Path zarrayPath = TESTOUTPUT.resolve(arrayName).resolve(ZARRAY); + String zarray = new String(Files.readAllBytes(zarrayPath)); + String defaultLevelZarray = zarray.replaceFirst("\"level\"\\s*:\\s*1", "\"level\": -1"); + Assertions.assertTrue(defaultLevelZarray.contains("\"level\": -1"), zarray); + Files.write(zarrayPath, defaultLevelZarray.getBytes()); + + Array reopenedArray = Array.open(storeHandle); + ucar.ma2.Array readData = reopenedArray.read(); + Assertions.assertEquals(16 * 16, readData.getSize()); + Assertions.assertEquals(127, readData.getInt(127)); + } + @ParameterizedTest @CsvSource({ "BOOL", "FLOAT64"