Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,12 @@ public Optional<SrtSigner> getSrtSigner() {
* Checks to see if this has the structure of a Z-TDF in that it is a zip file
* containing
* a `manifest.json` and a `0.payload`
*
* <p>
* The off-spec `0.manifest.json` this SDK wrote before the spec alignment is also
* accepted, matching {@link TDFReader}. Entries beyond those two are ignored rather
* than disqualifying: the spec fixes where the manifest lives, not what else the
* archive may hold.
*
* @param channel A channel containing the bytes of the potential Z-TDF
* @return `true` if
*/
Expand All @@ -169,11 +174,9 @@ public static boolean isTDF(SeekableByteChannel channel) {
return false;
}
var entries = zipReader.getEntries();
if (entries.size() != 2) {
return false;
}
return entries.stream().anyMatch(e -> "0.manifest.json".equals(e.getName()))
&& entries.stream().anyMatch(e -> "0.payload".equals(e.getName()));
return entries.stream().anyMatch(e -> TDFWriter.TDF_MANIFEST_FILE_NAME.equals(e.getName())
|| TDFWriter.TDF_MANIFEST_FILE_NAME_OFFSPEC.equals(e.getName()))
&& entries.stream().anyMatch(e -> TDFWriter.TDF_PAYLOAD_FILE_NAME.equals(e.getName()));
}

/**
Expand Down
10 changes: 8 additions & 2 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.stream.Collectors;

import static io.opentdf.platform.sdk.TDFWriter.TDF_MANIFEST_FILE_NAME;
import static io.opentdf.platform.sdk.TDFWriter.TDF_MANIFEST_FILE_NAME_OFFSPEC;
import static io.opentdf.platform.sdk.TDFWriter.TDF_PAYLOAD_FILE_NAME;

/**
Expand All @@ -26,14 +27,19 @@
.stream()
.collect(Collectors.toMap(ZipReader.Entry::getName, e -> e));

if (!entries.containsKey(TDF_MANIFEST_FILE_NAME)) {
// The spec name wins over the off-spec one when an archive carries both, so a
// conformant entry is never passed over for a superseded one.
var manifest = entries.containsKey(TDF_MANIFEST_FILE_NAME)
? entries.get(TDF_MANIFEST_FILE_NAME)
: entries.get(TDF_MANIFEST_FILE_NAME_OFFSPEC);

Check warning on line 34 in sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move the conditional expression inside this operation.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaCmeLZL1v8MDSs8OgZT&open=AaCmeLZL1v8MDSs8OgZT&pullRequest=405
if (manifest == null) {
throw new IllegalArgumentException("tdf doesn't contain a manifest");
}
if (!entries.containsKey(TDF_PAYLOAD_FILE_NAME)) {
throw new IllegalArgumentException("tdf doesn't contain a payload");
}

manifestEntry = entries.get(TDF_MANIFEST_FILE_NAME);
manifestEntry = manifest;
payload = entries.get(TDF_PAYLOAD_FILE_NAME).getData();
}

Expand Down
16 changes: 15 additions & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@
*/
public class TDFWriter {
public static final String TDF_PAYLOAD_FILE_NAME = "0.payload";
public static final String TDF_MANIFEST_FILE_NAME = "0.manifest.json";

/**
* The manifest entry name given by the OpenTDF spec:
* <a href="https://opentdf.io/spec#tdf-structure">opentdf.io/spec</a>.
*/
public static final String TDF_MANIFEST_FILE_NAME = "manifest.json";

/**
* The manifest entry name this SDK wrote before the spec alignment. The {@code 0.}
* prefix is a holdover from an early design that anticipated several payload/manifest
* pairs per archive and never shipped. Readers still accept it; the writer no longer
* emits it.
* See <a href="https://github.com/opentdf/platform/issues/3513">platform#3513</a>.
*/
public static final String TDF_MANIFEST_FILE_NAME_OFFSPEC = "0.manifest.json";
private final ZipWriter archiveWriter;

public TDFWriter(OutputStream destination) {
Expand Down
47 changes: 47 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.Random;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -29,6 +31,51 @@ void testExaminingValidZTDF() throws IOException {
}
}

/**
* The spec names the manifest entry {@code manifest.json}; the fixture above is an
* archive from before the spec alignment, which names it {@code 0.manifest.json}.
* Both are recognized.
* See <a href="https://github.com/opentdf/platform/issues/3513">platform#3513</a>.
*/
@Test
void testExaminingTDFWithSpecManifestName() throws IOException {
try (var chan = zipOf("0.payload", "manifest.json")) {
assertThat(SDK.isTDF(chan)).isTrue();
}
}

@Test
void testExaminingTDFWithAnExtraEntry() throws IOException {
try (var chan = zipOf("0.payload", "manifest.json", "something-else")) {
assertThat(SDK.isTDF(chan)).isTrue();
}
}

@Test
void testExaminingZipWithNoManifest() throws IOException {
try (var chan = zipOf("0.payload")) {
assertThat(SDK.isTDF(chan)).isFalse();
}
}

@Test
void testExaminingZipWithNoPayload() throws IOException {
try (var chan = zipOf("manifest.json")) {
assertThat(SDK.isTDF(chan)).isFalse();
}
}

/** Builds a zip holding the named entries; contents are irrelevant to {@link SDK#isTDF}. */
private static SeekableInMemoryByteChannel zipOf(String... names) throws IOException {
var out = new ByteArrayOutputStream();
var writer = new ZipWriter(out);
for (var name : names) {
writer.data(name, name.getBytes(StandardCharsets.UTF_8));
}
writer.finish();
return new SeekableInMemoryByteChannel(out.toByteArray());
}

@Test
void testReadingProtocolClient() {
var platformServicesClient = mock(ProtocolClient.class);
Expand Down
119 changes: 119 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/TDFReaderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.opentdf.platform.sdk;

import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* The OpenTDF spec names the manifest entry {@code manifest.json}; this SDK wrote
* {@code 0.manifest.json} before the spec alignment, so the reader accepts either.
* See <a href="https://github.com/opentdf/platform/issues/3513">platform#3513</a>.
*/
public class TDFReaderTest {

Check warning on line 20 in sdk/src/test/java/io/opentdf/platform/sdk/TDFReaderTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaCmeLaw1v8MDSs8OgZU&open=AaCmeLaw1v8MDSs8OgZU&pullRequest=405

/**
* Carries the fields the TDF manifest schema requires, so the fixtures below are
* manifests rather than arbitrary JSON. {@link TDFReader#manifest()} hands back the
* bytes without parsing them, so the literal is spelled out here.
*/
private static String manifestJson(String mimeType) {
return "{\"payload\":{\"type\":\"reference\",\"url\":\"" + TDFWriter.TDF_PAYLOAD_FILE_NAME
+ "\",\"protocol\":\"zip\",\"isEncrypted\":true,\"mimeType\":\"" + mimeType
+ "\"},\"encryptionInformation\":{\"type\":\"split\"}}";
}

/**
* What nearly every fixture here stores. These tests exercise the entry name, not
* manifest contents, so the same manifest serves whichever name it is filed under.
*/
private static final String MANIFEST = manifestJson("application/octet-stream");

/**
* Exists only for the test that must tell the two entries apart -- with identical
* content, it could not say which one the reader returned.
*/
private static final String OTHER_MANIFEST = manifestJson("text/plain");

private static final String PAYLOAD = "payload bytes";

/** Builds a zip holding exactly the given entries, in iteration order. */
private static SeekableInMemoryByteChannel archiveOf(Map<String, String> entries) throws IOException {
var out = new ByteArrayOutputStream();
var writer = new ZipWriter(out);
for (var entry : entries.entrySet()) {
writer.data(entry.getKey(), entry.getValue().getBytes(StandardCharsets.UTF_8));
}
writer.finish();
return new SeekableInMemoryByteChannel(out.toByteArray());
}

private static Map<String, String> entries(String... namesAndContents) {
var entries = new LinkedHashMap<String, String>();
for (int i = 0; i < namesAndContents.length; i += 2) {
entries.put(namesAndContents[i], namesAndContents[i + 1]);
}
return entries;
}

@Test
void readsManifestUnderTheSpecName() throws IOException {
try (var tdf = archiveOf(entries(
TDFWriter.TDF_PAYLOAD_FILE_NAME, PAYLOAD,
"manifest.json", MANIFEST))) {
assertThat(new TDFReader(tdf).manifest()).isEqualTo(MANIFEST);
}
}

@Test
void readsManifestUnderTheOffspecName() throws IOException {
try (var tdf = archiveOf(entries(
TDFWriter.TDF_PAYLOAD_FILE_NAME, PAYLOAD,
"0.manifest.json", MANIFEST))) {
assertThat(new TDFReader(tdf).manifest()).isEqualTo(MANIFEST);
}
}

/**
* The two entries hold different manifests, so this cannot pass by reading whichever
* one the reader happened to pick.
*/
@Test
void prefersTheSpecNameWhenAnArchiveCarriesBoth() throws IOException {
try (var tdf = archiveOf(entries(
TDFWriter.TDF_PAYLOAD_FILE_NAME, PAYLOAD,
"0.manifest.json", OTHER_MANIFEST,
"manifest.json", MANIFEST))) {
assertThat(new TDFReader(tdf).manifest()).isEqualTo(MANIFEST);
}
}

@Test
void rejectsAnArchiveWithNoManifestUnderEitherName() throws IOException {
try (var tdf = archiveOf(entries(TDFWriter.TDF_PAYLOAD_FILE_NAME, PAYLOAD))) {
assertThatThrownBy(() -> new TDFReader(tdf))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("tdf doesn't contain a manifest");
}
}

@Test
void readsThePayloadAlongsideASpecNamedManifest() throws IOException {
try (var tdf = archiveOf(entries(
TDFWriter.TDF_PAYLOAD_FILE_NAME, PAYLOAD,
"manifest.json", MANIFEST))) {
var reader = new TDFReader(tdf);
var buf = new byte[PAYLOAD.length()];

assertThat(reader.readPayloadBytes(buf)).isEqualTo(PAYLOAD.length());
assertThat(new String(buf, StandardCharsets.UTF_8)).isEqualTo(PAYLOAD);
}
}
}
27 changes: 26 additions & 1 deletion sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class TDFWriterTest {
@Test
Expand Down Expand Up @@ -74,6 +75,30 @@ void simpleTDFCreate() throws IOException {
fileOutStream.close();
}

/**
* The OpenTDF spec puts the manifest at the archive root under {@code manifest.json};
* this SDK wrote {@code 0.manifest.json} before the spec alignment. Conformance test
* for the entry name the writer emits.
* See <a href="https://github.com/opentdf/platform/issues/3513">platform#3513</a>.
*/
@Test
void writesTheManifestUnderTheSpecEntryName() throws IOException {
var out = new ByteArrayOutputStream();
var writer = new TDFWriter(out);
try (var p = writer.payload()) {
new ByteArrayInputStream("payload bytes".getBytes(StandardCharsets.UTF_8)).transferTo(p);
}
writer.appendManifest("{\"payload\":{\"url\":\"0.payload\"}}");
writer.finish();

try (var chan = new SeekableInMemoryByteChannel(out.toByteArray())) {
var names = new ZipReader(chan).getEntries().stream()
.map(ZipReader.Entry::getName)
.collect(Collectors.toList());
assertThat(names).containsExactlyInAnyOrder("0.payload", "manifest.json");
}
}

/**
* The manifest is appended after the payload, so in a large TDF its local header offset
* doesn't fit in a 32-bit central directory field. Uses the lowered zip64 threshold to run
Expand Down
Loading