Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/main/java/land/oras/Annotations.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.stream.Collectors;
import land.oras.utils.JsonUtils;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Record for annotations
Expand All @@ -44,7 +45,10 @@ public record Annotations(
* @param manifestAnnotations The manifest annotations
* @return The annotations
*/
public static Annotations ofManifest(Map<String, String> manifestAnnotations) {
public static Annotations ofManifest(@Nullable Map<String, String> manifestAnnotations) {
if (manifestAnnotations == null) {
return empty();
}
return new Annotations(new HashMap<>(), manifestAnnotations, new HashMap<>());
}

Expand Down
10 changes: 7 additions & 3 deletions src/main/java/land/oras/ContainerRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ public String getApiRegistry() {
* @return The namespace
*/
public @Nullable String getNamespace() {
String registry = getRegistry();
if (namespace == null && registry.equals("docker.io")) {
return "library";
}
return namespace;
}

Expand Down Expand Up @@ -147,7 +151,7 @@ public String getTag() {
* @return The new container reference
*/
public ContainerRef withDigest(String digest) {
return new ContainerRef(registry, namespace, repository, tag, digest);
return new ContainerRef(registry, getNamespace(), repository, tag, digest);
}

/**
Expand All @@ -169,9 +173,9 @@ public SupportedAlgorithm getAlgorithm() {
*/
private String getApiPrefix() {
if (namespace != null) {
return "%s/v2/%s/%s".formatted(registry, namespace, repository);
return "%s/v2/%s/%s".formatted(getApiRegistry(), getNamespace(), repository);
}
return "%s/v2/%s".formatted(registry, repository);
return "%s/v2/%s".formatted(getApiRegistry(), repository);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/land/oras/Manifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final class Manifest {
/**
* The manifest descriptor
*/
private transient ManifestDescriptor descriptor;
private final transient ManifestDescriptor descriptor;

private Manifest(
int schemaVersion,
Expand Down Expand Up @@ -127,7 +127,7 @@ public Subject getSubject() {
* @return The layers
*/
public List<Layer> getLayers() {
return Collections.unmodifiableList(layers);
return layers != null ? Collections.unmodifiableList(layers) : List.of();
}

/**
Expand Down
35 changes: 19 additions & 16 deletions src/main/java/land/oras/Registry.java
Original file line number Diff line number Diff line change
Expand Up @@ -397,20 +397,22 @@ public void copy(ContainerRef containerRef, Path folder) {
Files.writeString(indexFile, index.toJson());

// Write config as any blob
String configDigest = sourceConfig.getDigest();
SupportedAlgorithm configAlgorithm = SupportedAlgorithm.fromDigest(configDigest);
Path configFile = blobs.resolve(configAlgorithm.getPrefix())
.resolve(SupportedAlgorithm.getDigest(sourceConfig.getDigest()));
Path configPrefixDirectory = blobs.resolve(configAlgorithm.getPrefix());
if (!Files.exists(configPrefixDirectory)) {
Files.createDirectory(configPrefixDirectory);
}
// Write the data from data or fetch the blob
if (sourceConfig.getData() != null) {
Files.write(configFile, sourceConfig.getDataBytes());
} else {
try (InputStream is = fetchBlob(containerRef.withDigest(configDigest))) {
Files.copy(is, configFile);
if (sourceConfig != null) {
String configDigest = sourceConfig.getDigest();
SupportedAlgorithm configAlgorithm = SupportedAlgorithm.fromDigest(configDigest);
Path configFile = blobs.resolve(configAlgorithm.getPrefix())
.resolve(SupportedAlgorithm.getDigest(sourceConfig.getDigest()));
Path configPrefixDirectory = blobs.resolve(configAlgorithm.getPrefix());
if (!Files.exists(configPrefixDirectory)) {
Files.createDirectory(configPrefixDirectory);
}
// Write the data from data or fetch the blob
if (sourceConfig.getData() != null) {
Files.write(configFile, sourceConfig.getDataBytes());
} else {
try (InputStream is = fetchBlob(containerRef.withDigest(configDigest))) {
Files.copy(is, configFile);
}
}
}

Expand Down Expand Up @@ -737,8 +739,9 @@ public Manifest getManifest(ContainerRef containerRef) {
String contentType = response.headers().get(Const.CONTENT_TYPE_HEADER.toLowerCase());
String size = response.headers().get(Const.CONTENT_LENGTH_HEADER.toLowerCase());
String digest = response.headers().get(Const.DOCKER_CONTENT_DIGEST_HEADER.toLowerCase());
return JsonUtils.fromJson(response.response(), Manifest.class)
.withDescriptor(ManifestDescriptor.of(contentType, digest, size == null ? 0 : Long.parseLong(size)));
ManifestDescriptor descriptor =
ManifestDescriptor.of(contentType, digest, size == null ? 0 : Long.parseLong(size));
return JsonUtils.fromJson(response.response(), Manifest.class).withDescriptor(descriptor);
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/land/oras/AnnotationsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public void fromJson() {
assertEquals("more cream", annotations.getFileAnnotations("cake.txt").get("fun"));
}

@Test
public void nullAnnotations() {
Annotations annotations = Annotations.ofManifest(null);
assertEquals(0, annotations.configAnnotations().size());
assertEquals(0, annotations.manifestAnnotations().size());
assertEquals(0, annotations.filesAnnotations().size());
}

@Test
public void toJson() {
Annotations annotations = new Annotations(
Expand Down
72 changes: 51 additions & 21 deletions src/test/java/land/oras/ContainerRefTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ public class ContainerRefTest {

@Test
void shouldParseImageWithAllParts() {
ContainerRef containerRef =
ContainerRef.parse("docker.io/library/foo/hello-world:latest@sha256:1234567890abcdef");
ContainerRef containerRef = ContainerRef.parse("docker.io/library/foo/alpine:latest@sha256:1234567890abcdef");
assertEquals("docker.io", containerRef.getRegistry());
assertEquals("registry-1.docker.io", containerRef.getApiRegistry());
assertEquals("library/foo", containerRef.getNamespace());
assertEquals("hello-world", containerRef.getRepository());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertEquals(SupportedAlgorithm.SHA256, containerRef.getAlgorithm());
assertEquals("sha256:1234567890abcdef", containerRef.getDigest());
Expand All @@ -48,66 +47,97 @@ void shouldParseImageWithAllParts() {
void shouldFailWithUnSupportedAlgorithm() {
assertThrows(
OrasException.class,
() -> ContainerRef.parse("docker.io/library/foo/hello-world:latest@test:1234567890abcdef"),
() -> ContainerRef.parse("docker.io/library/foo/alpine:latest@test:1234567890abcdef"),
"Unsupported algorithm: test");
}

@Test
void shouldParseImageWithNoNamespace() {
ContainerRef containerRef = ContainerRef.parse("docker.io/hello-world:latest@sha256:1234567890abcdef");
ContainerRef containerRef = ContainerRef.parse("docker.io/alpine:latest@sha256:1234567890abcdef");
assertEquals("docker.io", containerRef.getRegistry());
assertEquals("library", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertEquals("sha256:1234567890abcdef", containerRef.getDigest());

containerRef = ContainerRef.parse("demo.goharbor.com/alpine:latest@sha256:1234567890abcdef");
assertEquals("demo.goharbor.com", containerRef.getRegistry());
assertEquals("demo.goharbor.com/v2/alpine/tags/list", containerRef.getTagsPath());
assertNull(containerRef.getNamespace());
assertEquals("hello-world", containerRef.getRepository());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertEquals("sha256:1234567890abcdef", containerRef.getDigest());
}

@Test
void shouldParseImageWithNoTag() {
ContainerRef containerRef = ContainerRef.parse("docker.io/hello-world@sha256:1234567890abcdef");
ContainerRef containerRef = ContainerRef.parse("docker.io/alpine@sha256:1234567890abcdef");
assertEquals("docker.io", containerRef.getRegistry());
assertNull(containerRef.getNamespace());
assertEquals("hello-world", containerRef.getRepository());
assertEquals("registry-1.docker.io/v2/alpine/tags/list", containerRef.getTagsPath());
assertEquals("library", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertEquals("sha256:1234567890abcdef", containerRef.getDigest());

containerRef = ContainerRef.parse("docker.io/foobar/alpine@sha256:1234567890abcdef");
assertEquals("registry-1.docker.io/v2/foobar/alpine/tags/list", containerRef.getTagsPath());
assertEquals("docker.io", containerRef.getRegistry());
assertEquals("foobar", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertEquals("sha256:1234567890abcdef", containerRef.getDigest());
}

@Test
void shouldParseImageWithNoDigest() {
ContainerRef containerRef = ContainerRef.parse("docker.io/hello-world:latest");
ContainerRef containerRef = ContainerRef.parse("docker.io/alpine:latest");
assertEquals("docker.io", containerRef.getRegistry());
assertNull(containerRef.getNamespace());
assertEquals("hello-world", containerRef.getRepository());
assertEquals("library", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertNull(containerRef.getDigest());
}

@Test
void shouldParseImageWithNoRegistry() {
ContainerRef containerRef = ContainerRef.parse("hello-world:latest");
ContainerRef containerRef = ContainerRef.parse("alpine:latest");
assertEquals("docker.io", containerRef.getRegistry());
assertEquals("registry-1.docker.io", containerRef.getApiRegistry());
assertNull(containerRef.getNamespace());
assertEquals("hello-world", containerRef.getRepository());
assertEquals("registry-1.docker.io/v2/alpine/tags/list", containerRef.getTagsPath());
assertEquals("library", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertNull(containerRef.getDigest());
}

@Test
void shouldParseImageWithNoTagAndNoRegistry() {
ContainerRef containerRef = ContainerRef.parse("hello-world");
ContainerRef containerRef = ContainerRef.parse("alpine");
assertEquals("docker.io", containerRef.getRegistry());
assertNull(containerRef.getNamespace());
assertEquals("hello-world", containerRef.getRepository());
assertEquals("library", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertNull(containerRef.getDigest());

containerRef = ContainerRef.parse("foobar/alpine");
assertEquals("docker.io", containerRef.getRegistry());
assertEquals("foobar", containerRef.getNamespace());
assertEquals("alpine", containerRef.getRepository());
assertEquals("latest", containerRef.getTag());
assertNull(containerRef.getDigest());
}

@Test
void shouldGetTagsPathDockerIo() {
ContainerRef containerRef = ContainerRef.parse("docker.io/library/foo/alpine:latest@sha256:1234567890abcdef");
assertEquals("registry-1.docker.io/v2/library/foo/alpine/tags/list", containerRef.getTagsPath());
}

@Test
void shouldGetTagsPath() {
void shouldGetTagsPathOtherRegistry() {
ContainerRef containerRef =
ContainerRef.parse("docker.io/library/foo/hello-world:latest@sha256:1234567890abcdef");
assertEquals("docker.io/v2/library/foo/hello-world/tags/list", containerRef.getTagsPath());
ContainerRef.parse("demo.goharbor.io/library/foo/alpine:latest@sha256:1234567890abcdef");
assertEquals("demo.goharbor.io/v2/library/foo/alpine/tags/list", containerRef.getTagsPath());
}

@Test
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/land/oras/ManifestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ void shouldHaveEmptyManifest() {
Manifest.fromJson(emptyManifest()).toJson(), Manifest.empty().toJson());
}

@Test
void shouldHaveNoLayerForIndex() {
String json =
"{\"schemaVersion\":2,\"mediaType\":\"application/vnd.oci.image.index.v1+json\",\"annotations\":{}}";
Manifest manifest = Manifest.fromJson(json);
assertEquals(0, manifest.getLayers().size());
}

@Test
void shouldGetArtifactTest() {
Manifest manifest1 = Manifest.empty().withArtifactType(ArtifactType.from("test/plain"));
Expand Down