Skip to content

Commit 2625e13

Browse files
committed
add ReadOnlyZipStore
1 parent 6737660 commit 2625e13

File tree

4 files changed

+209
-26
lines changed

4 files changed

+209
-26
lines changed

src/main/java/dev/zarr/zarrjava/store/BufferedZipStore.java

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import java.util.zip.CRC32;
1616
import java.util.zip.ZipEntry; // for STORED constant
1717

18+
import static dev.zarr.zarrjava.utils.ZipUtils.getZipCommentFromBuffer;
19+
1820

1921
/** A Store implementation that buffers reads and writes and flushes them to an underlying Store as a zip file.
2022
*/
@@ -112,32 +114,6 @@ private void writeBuffer() throws IOException{
112114
underlyingStore.set(ByteBuffer.wrap(zipBytes));
113115
}
114116

115-
// adopted from https://stackoverflow.com/a/9918966
116-
@Nullable
117-
private String getZipCommentFromBuffer(byte[] bufArray) throws IOException {
118-
// End of Central Directory (EOCD) record magic number
119-
byte[] EOCD = {0x50, 0x4b, 0x05, 0x06};
120-
int buffLen = bufArray.length;
121-
// Check the buffer from the end
122-
search:
123-
for (int i = buffLen - EOCD.length - 22; i >= 0; i--) {
124-
for (int k = 0; k < EOCD.length; k++) {
125-
if (bufArray[i + k] != EOCD[k]) {
126-
continue search;
127-
}
128-
}
129-
// End of Central Directory found!
130-
int commentLen = bufArray[i + 20] + bufArray[i + 21] * 256;
131-
int realLen = buffLen - i - 22;
132-
if (commentLen != realLen) {
133-
throw new IOException("ZIP comment size mismatch: "
134-
+ "directory says len is " + commentLen
135-
+ ", but file ends after " + realLen + " bytes!");
136-
}
137-
return new String(bufArray, i + 22, commentLen);
138-
}
139-
return null;
140-
}
141117

142118
private void loadBuffer() throws IOException{
143119
// read zip file bytes from underlying store and populate buffer store
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package dev.zarr.zarrjava.store;
2+
3+
import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream;
4+
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
5+
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
6+
7+
import javax.annotation.Nonnull;
8+
import javax.annotation.Nullable;
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.IOException;
11+
import java.nio.ByteBuffer;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.stream.Stream;
15+
16+
import static dev.zarr.zarrjava.utils.ZipUtils.getZipCommentFromBuffer;
17+
18+
public class ReadOnlyZipStore implements Store, Store.ListableStore {
19+
20+
private final StoreHandle underlyingStore;
21+
22+
String resolveKeys(String[] keys) {
23+
return String.join("/", keys);
24+
}
25+
26+
String[] resolveEntryKeys(String entryKey) {
27+
return entryKey.split("/");
28+
}
29+
30+
@Override
31+
public boolean exists(String[] keys) {
32+
return get(keys, 0, 0) != null;
33+
}
34+
35+
@Nullable
36+
@Override
37+
public ByteBuffer get(String[] keys) {
38+
return get(keys, 0);
39+
}
40+
41+
@Nullable
42+
@Override
43+
public ByteBuffer get(String[] keys, long start) {
44+
return get(keys, start, -1);
45+
}
46+
47+
public String getArchiveComment() throws IOException {
48+
ByteBuffer buffer = underlyingStore.read();
49+
if (buffer == null) {
50+
return null;
51+
}
52+
byte[] bufArray;
53+
if (buffer.hasArray()) {
54+
bufArray = buffer.array();
55+
} else {
56+
bufArray = new byte[buffer.remaining()];
57+
buffer.duplicate().get(bufArray);
58+
}
59+
return getZipCommentFromBuffer(bufArray);
60+
}
61+
62+
@Nullable
63+
@Override
64+
public ByteBuffer get(String[] keys, long start, long end) {
65+
ByteBuffer buffer = underlyingStore.read();
66+
if (buffer == null) {
67+
return null;
68+
}
69+
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new ByteBufferBackedInputStream(buffer))) {
70+
ZipArchiveEntry entry;
71+
while ((entry = zis.getNextEntry()) != null) {
72+
if (entry.isDirectory() || !entry.getName().equals(resolveKeys(keys))) {
73+
continue;
74+
}
75+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
76+
if (end == -1) {
77+
end = entry.getSize();
78+
}
79+
if (start > end) {
80+
throw new IllegalArgumentException("Start position can not be larger than end position. Got start=" + start + ", end=" + end);
81+
}
82+
if (start < 0 || end > entry.getSize()) {
83+
throw new IllegalArgumentException("Start and end positions must be within the bounds of the zip entry size. Entry size=" + entry.getSize() + ", got start=" + start + ", end=" + end);
84+
}
85+
zis.skip(start);
86+
long bytesToRead = end - start;
87+
byte[] bufferArray = new byte[8192];
88+
int len;
89+
while (bytesToRead > 0 && (len = zis.read(bufferArray, 0, (int) Math.min(bufferArray.length, bytesToRead))) != -1) {
90+
baos.write(bufferArray, 0, len);
91+
bytesToRead -= len;
92+
}
93+
byte[] bytes = baos.toByteArray();
94+
return ByteBuffer.wrap(bytes);
95+
}
96+
} catch (IOException e) {
97+
return null;
98+
}
99+
return null;
100+
}
101+
102+
@Override
103+
public void set(String[] keys, ByteBuffer bytes) {
104+
throw new UnsupportedOperationException("ReadOnlyZipStore does not support set operation.");
105+
}
106+
107+
@Override
108+
public void delete(String[] keys) {
109+
throw new UnsupportedOperationException("ReadOnlyZipStore does not support delete operation.");
110+
}
111+
112+
@Nonnull
113+
@Override
114+
public StoreHandle resolve(String... keys) {
115+
return new StoreHandle(this, keys);
116+
}
117+
118+
@Override
119+
public String toString() {
120+
return "ReadOnlyZipStore(" + underlyingStore.toString() + ")";
121+
}
122+
123+
public ReadOnlyZipStore(@Nonnull StoreHandle underlyingStore) {
124+
this.underlyingStore = underlyingStore;
125+
}
126+
127+
public ReadOnlyZipStore(@Nonnull Path underlyingStore) {
128+
this(new FilesystemStore(underlyingStore.getParent()).resolve(underlyingStore.getFileName().toString()));
129+
}
130+
131+
public ReadOnlyZipStore(@Nonnull String underlyingStorePath) {
132+
this(Paths.get(underlyingStorePath));
133+
}
134+
135+
@Override
136+
public Stream<String[]> list(String[] keys) {
137+
Stream.Builder<String[]> builder = Stream.builder();
138+
139+
ByteBuffer buffer = underlyingStore.read();
140+
if (buffer == null) {
141+
return builder.build();
142+
}
143+
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new ByteBufferBackedInputStream(buffer))) {
144+
ZipArchiveEntry entry;
145+
String prefix = resolveKeys(keys);
146+
while ((entry = zis.getNextEntry()) != null) {
147+
String entryKey = entry.getName();
148+
if (!entryKey.startsWith(prefix) || entryKey.equals(prefix)) {
149+
continue;
150+
}
151+
String[] entryKeys = resolveEntryKeys(entryKey.substring(prefix.length()));
152+
builder.add(entryKeys);
153+
}
154+
} catch (IOException e) {
155+
return null;
156+
}
157+
return builder.build();
158+
}
159+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.zarr.zarrjava.utils;
2+
3+
import javax.annotation.Nullable;
4+
import java.io.IOException;
5+
6+
public class ZipUtils {
7+
8+
// adopted from https://stackoverflow.com/a/9918966
9+
@Nullable
10+
public static String getZipCommentFromBuffer(byte[] bufArray) throws IOException {
11+
// End of Central Directory (EOCD) record magic number
12+
byte[] EOCD = {0x50, 0x4b, 0x05, 0x06};
13+
int buffLen = bufArray.length;
14+
// Check the buffer from the end
15+
search:
16+
for (int i = buffLen - EOCD.length - 22; i >= 0; i--) {
17+
for (int k = 0; k < EOCD.length; k++) {
18+
if (bufArray[i + k] != EOCD[k]) {
19+
continue search;
20+
}
21+
}
22+
// End of Central Directory found!
23+
int commentLen = bufArray[i + 20] + bufArray[i + 21] * 256;
24+
int realLen = buffLen - i - 22;
25+
if (commentLen != realLen) {
26+
throw new IOException("ZIP comment size mismatch: "
27+
+ "directory says len is " + commentLen
28+
+ ", but file ends after " + realLen + " bytes!");
29+
}
30+
return new String(bufArray, i + 22, commentLen);
31+
}
32+
return null;
33+
}
34+
35+
}

src/test/java/dev/zarr/zarrjava/ZarrStoreTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,19 @@ public void testZipStoreV2() throws ZarrException, IOException {
263263
assertIsTestGroupV2(dev.zarr.zarrjava.core.Group.open(fsStore.resolve()), true);
264264
}
265265

266+
@Test
267+
public void testReadOnlyZipStore() throws ZarrException, IOException {
268+
Path path = TESTOUTPUT.resolve("testReadOnlyZipStore.zip");
269+
String archiveComment = "This is a test ZIP archive comment.";
270+
BufferedZipStore zipStore = new BufferedZipStore(path, archiveComment);
271+
writeTestGroupV3(zipStore, true);
272+
zipStore.flush();
273+
274+
ReadOnlyZipStore readOnlyZipStore = new ReadOnlyZipStore(path);
275+
Assertions.assertEquals(archiveComment, readOnlyZipStore.getArchiveComment(), "ZIP archive comment from ReadOnlyZipStore does not match expected value.");
276+
assertIsTestGroupV3(Group.open(readOnlyZipStore.resolve()), true);
277+
}
278+
266279

267280
static Stream<Store> localStores() {
268281
return Stream.of(

0 commit comments

Comments
 (0)