oci-zero 0.2.1

A no-std, allocation-free streaming core for OCI registries
Documentation
use oci_zero::{
    digest::Digest,
    server::{serve, BufferTooSmall, Content, MemoryStore, Repository, Store, Tag},
};

#[test]
fn serves_borrowed_content_by_tag_and_digest_with_head_metadata() {
    let manifest = Content::new(b"{}", "application/vnd.oci.image.manifest.v1+json");
    let blob = Content::new(b"hello", "text/plain");
    let tags = [Tag {
        name: "latest",
        digest: manifest.digest,
    }];
    let repositories = [Repository {
        name: "a/nested",
        tags: &tags,
        manifests: &[manifest],
        blobs: &[blob],
    }];
    let store = MemoryStore(&repositories);
    let mut scratch = [];
    let result = serve(&store, "GET", "/v2/a/nested/manifests/latest", &mut scratch).unwrap();
    assert_eq!(result.status, 200);
    assert_eq!(result.body.as_ptr(), manifest.bytes.as_ptr());
    assert_eq!(result.media_type, manifest.media_type);
    assert_eq!(result.digest, Some(manifest.digest));
    let target = format!("/v2/a/nested/manifests/{}", manifest.digest);
    let head = serve(&store, "HEAD", &target, &mut scratch).unwrap();
    assert_eq!(head.status, 200);
    assert!(head.body.is_empty());
    assert_eq!(head.content_length, manifest.bytes.len());
    assert_eq!(head.digest, Some(manifest.digest));
    let target = format!("/v2/a/nested/blobs/{}", blob.digest);
    let result = serve(&store, "GET", &target, &mut scratch).unwrap();
    assert_eq!(result.body, b"hello");
    assert_eq!(result.media_type, "application/octet-stream");
}

#[test]
fn paginates_unsorted_repositories_and_tags_with_encoded_cursors() {
    let digest = Digest::from_bytes([0; 32]);
    let tags = [
        Tag { name: "v2", digest },
        Tag {
            name: "latest",
            digest,
        },
        Tag { name: "v1", digest },
    ];
    let store = MemoryStore(&[
        Repository {
            name: "z",
            tags: &[],
            manifests: &[],
            blobs: &[],
        },
        Repository {
            name: "a/nested",
            tags: &tags,
            manifests: &[],
            blobs: &[],
        },
    ]);
    let mut scratch = [0; 256];
    let result = serve(&store, "GET", "/v2/_catalog?n=1", &mut scratch).unwrap();
    assert_eq!(result.body, br#"{"repositories":["a/nested"]}"#);
    assert_eq!(
        result.next.unwrap().to_string(),
        "</v2/_catalog?n=1&last=a%2Fnested>; rel=\"next\""
    );
    let result = serve(
        &store,
        "GET",
        "/v2/_catalog?n=1&last=a%2Fnested",
        &mut scratch,
    )
    .unwrap();
    assert_eq!(result.body, br#"{"repositories":["z"]}"#);
    assert!(result.next.is_none());
    let result = serve(&store, "GET", "/v2/a/nested/tags/list?n=2", &mut scratch).unwrap();
    assert_eq!(
        result.body,
        br#"{"name":"a/nested","tags":["latest","v1"]}"#
    );
    assert_eq!(result.next.unwrap().last, "v1");
    let result = serve(
        &store,
        "GET",
        "/v2/a/nested/tags/list?n=2&last=v1",
        &mut scratch,
    )
    .unwrap();
    assert_eq!(result.body, br#"{"name":"a/nested","tags":["v2"]}"#);
    assert!(result.next.is_none());
}

#[test]
fn bounds_listing_output_and_rejects_malformed_pagination() {
    let store = MemoryStore(&[]);
    let mut scratch = [0; 128];
    assert!(matches!(
        serve(&store, "GET", "/v2/_catalog", &mut [0; 1]),
        Err(BufferTooSmall)
    ));
    for query in [
        "n=-1",
        "n=+1",
        "n=wat",
        "n=1&n=2",
        "last=%",
        "last=%gg",
        "last=%ff",
        "last=x&last=y",
        "n",
    ] {
        let target = format!("/v2/_catalog?{query}");
        let result = serve(&store, "GET", &target, &mut scratch).unwrap();
        assert_eq!(result.status, 400, "{query}");
    }
    let target = format!("/v2/_catalog?last={}", "a".repeat(257));
    assert_eq!(
        serve(&store, "GET", &target, &mut [0; 128]).unwrap().status,
        400
    );
    let result = serve(&store, "GET", "/v2/_catalog?other=value", &mut scratch).unwrap();
    assert_eq!(result.body, br#"{"repositories":[]}"#);
}

#[test]
fn errors_are_oci_json_and_blobs_are_repository_scoped() {
    let blob = Content::new(b"hello", "text/plain");
    let store = MemoryStore(&[
        Repository {
            name: "a",
            tags: &[],
            manifests: &[],
            blobs: &[blob],
        },
        Repository {
            name: "b",
            tags: &[],
            manifests: &[],
            blobs: &[],
        },
    ]);
    let mut scratch = [];
    for (target, status, code) in [
        ("/v2/no/tags/list".into(), 404, "NAME_UNKNOWN"),
        ("/v2/a/manifests/missing".into(), 404, "MANIFEST_UNKNOWN"),
        (format!("/v2/b/blobs/{}", blob.digest), 404, "BLOB_UNKNOWN"),
        ("/v2/a/blobs/sha256:bad".into(), 400, "DIGEST_INVALID"),
    ] {
        let result = serve(&store, "GET", &target, &mut scratch).unwrap();
        assert_eq!(result.status, status);
        assert!(std::str::from_utf8(result.body).unwrap().contains(code));
        assert_eq!(result.media_type, "application/json");
        let length = result.content_length;
        let head = serve(&store, "HEAD", &target, &mut scratch).unwrap();
        assert!(head.body.is_empty());
        assert_eq!(head.content_length, length);
    }
    assert!(store.blob("b", blob.digest).is_none());
}

#[test]
fn supports_probe_and_preflight_and_rejects_writes() {
    let store = MemoryStore(&[]);
    let mut scratch = [];
    assert_eq!(
        serve(&store, "GET", "/v2/", &mut scratch).unwrap().body,
        b"{}"
    );
    let response = serve(&store, "OPTIONS", "/v2/any/path", &mut scratch).unwrap();
    assert_eq!(response.status, 204);
    assert!(response.body.is_empty());
    for method in ["POST", "PUT", "DELETE", "PATCH"] {
        assert_eq!(
            serve(&store, method, "/v2/", &mut scratch).unwrap().status,
            405
        );
    }
}

#[test]
fn selects_the_terminal_operation_when_repository_components_are_endpoint_names() {
    let manifest = Content::new(b"{}", "application/vnd.oci.image.manifest.v1+json");
    let blob = Content::new(b"hello", "text/plain");
    let tags = [Tag {
        name: "latest",
        digest: manifest.digest,
    }];
    let manifests = [manifest];
    let blobs = [blob];
    let repositories =
        ["team/manifests/app", "team/blobs/app", "team/tags/list"].map(|name| Repository {
            name,
            tags: &tags,
            manifests: &manifests,
            blobs: &blobs,
        });
    let store = MemoryStore(&repositories);
    let mut scratch = [0; 256];
    for repository in repositories {
        let path = format!("/v2/{}/blobs/{}", repository.name, blob.digest);
        assert_eq!(
            serve(&store, "GET", &path, &mut scratch).unwrap().body,
            b"hello"
        );
        let path = format!("/v2/{}/manifests/latest", repository.name);
        assert_eq!(
            serve(&store, "GET", &path, &mut scratch).unwrap().body,
            b"{}"
        );
        let path = format!("/v2/{}/tags/list", repository.name);
        assert_eq!(
            serve(&store, "GET", &path, &mut scratch).unwrap().status,
            200
        );
    }
}

#[test]
fn zero_page_size_returns_an_empty_listing_without_a_link() {
    let tags = [Tag {
        name: "latest",
        digest: Digest::from_bytes([0; 32]),
    }];
    let store = MemoryStore(&[Repository {
        name: "demo",
        tags: &tags,
        manifests: &[],
        blobs: &[],
    }]);
    let mut scratch = [0; 256];
    for path in [
        "/v2/demo/tags/list?n=0",
        "/v2/demo/tags/list?n=0&last=latest",
    ] {
        let result = serve(&store, "GET", path, &mut scratch).unwrap();
        assert_eq!(result.status, 200);
        assert_eq!(result.body, br#"{"name":"demo","tags":[]}"#);
        assert!(result.next.is_none());
    }
    let result = serve(&store, "GET", "/v2/_catalog?n=0", &mut scratch).unwrap();
    assert_eq!(result.body, br#"{"repositories":[]}"#);
    assert!(result.next.is_none());
}