use axum::body::{Body, Bytes};
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use ferro_blob_store::Digest;
use serde_json::Value;
use crate::error::{OciError, OciErrorCode};
use crate::media_types::{ManifestKind, classify_manifest_media_type};
use crate::reference::{Reference, validate_name};
use crate::registry::ReferrerDescriptor;
use crate::router::AppState;
fn parse_reference(s: &str) -> Result<Reference, OciError> {
s.parse::<Reference>()
}
fn manifest_response_headers(digest: &Digest, media_type: &str, size: usize) -> HeaderMap {
let mut headers = HeaderMap::new();
let digest_str = digest.to_string();
if let Ok(v) = HeaderValue::from_str(&digest_str) {
headers.insert("Docker-Content-Digest", v);
if let Ok(etag) = HeaderValue::from_str(&format!("\"{digest_str}\"")) {
headers.insert(header::ETAG, etag);
}
}
if let Ok(v) = HeaderValue::from_str(media_type) {
headers.insert(header::CONTENT_TYPE, v);
}
headers.insert(header::CONTENT_LENGTH, HeaderValue::from(size as u64));
headers
}
pub async fn get_manifest(
state: &AppState,
name: &str,
reference_str: &str,
request_headers: &HeaderMap,
) -> Response {
if let Err(e) = validate_name(name) {
return e.into_response();
}
let Ok(reference) = parse_reference(reference_str) else {
return manifest_not_found(name, reference_str);
};
match state.registry.get_manifest(name, &reference).await {
Ok(Some((digest, media_type, body))) => {
range_or_full_response(request_headers, &digest, &media_type, &body)
}
Ok(None) => manifest_not_found(name, reference_str),
Err(e) => OciError::from(e).into_response(),
}
}
fn range_or_full_response(
request_headers: &HeaderMap,
digest: &Digest,
media_type: &str,
body: &Bytes,
) -> Response {
let total = body.len();
let raw_range = request_headers
.get(header::RANGE)
.and_then(|v| v.to_str().ok());
let parse_outcome = raw_range.map(parse_byte_range);
match parse_outcome {
None => {
let mut headers = manifest_response_headers(digest, media_type, total);
headers.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
(StatusCode::OK, headers, Body::from(body.clone())).into_response()
}
Some(ByteRangeOutcome::Ignore) => {
let mut headers = manifest_response_headers(digest, media_type, total);
headers.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
(StatusCode::OK, headers, Body::from(body.clone())).into_response()
}
Some(ByteRangeOutcome::Unsatisfiable) => unsatisfiable_response(total),
Some(ByteRangeOutcome::Range { start, end }) => {
let last = total.saturating_sub(1);
if total == 0 || start > last {
return unsatisfiable_response(total);
}
let clamped_end = end.min(last);
let slice_len = clamped_end - start + 1;
let slice = body.slice(start..=clamped_end);
let mut headers = HeaderMap::new();
let digest_str = digest.to_string();
if let Ok(v) = HeaderValue::from_str(&digest_str) {
headers.insert("Docker-Content-Digest", v);
if let Ok(etag) = HeaderValue::from_str(&format!("\"{digest_str}\"")) {
headers.insert(header::ETAG, etag);
}
}
if let Ok(v) = HeaderValue::from_str(media_type) {
headers.insert(header::CONTENT_TYPE, v);
}
headers.insert(header::CONTENT_LENGTH, HeaderValue::from(slice_len as u64));
headers.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
if let Ok(v) = HeaderValue::from_str(&format!("bytes {start}-{clamped_end}/{total}")) {
headers.insert(header::CONTENT_RANGE, v);
}
(StatusCode::PARTIAL_CONTENT, headers, Body::from(slice)).into_response()
}
}
}
fn unsatisfiable_response(total: usize) -> Response {
let mut headers = HeaderMap::new();
if let Ok(v) = HeaderValue::from_str(&format!("bytes */{total}")) {
headers.insert(header::CONTENT_RANGE, v);
}
headers.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
(StatusCode::RANGE_NOT_SATISFIABLE, headers).into_response()
}
#[derive(Debug, PartialEq, Eq)]
enum ByteRangeOutcome {
Range { start: usize, end: usize },
Unsatisfiable,
Ignore,
}
fn parse_byte_range(raw: &str) -> ByteRangeOutcome {
let Some(spec) = raw.strip_prefix("bytes=") else {
return ByteRangeOutcome::Ignore;
};
if spec.contains(',') {
return ByteRangeOutcome::Ignore;
}
let Some((lhs, rhs)) = spec.split_once('-') else {
return ByteRangeOutcome::Ignore;
};
if lhs.is_empty() {
return ByteRangeOutcome::Ignore;
}
let Ok(start) = lhs.parse::<usize>() else {
return ByteRangeOutcome::Ignore;
};
let end = if rhs.is_empty() {
usize::MAX
} else {
match rhs.parse::<usize>() {
Ok(v) => v,
Err(_) => return ByteRangeOutcome::Ignore,
}
};
if start > end {
return ByteRangeOutcome::Unsatisfiable;
}
ByteRangeOutcome::Range { start, end }
}
pub async fn head_manifest(state: &AppState, name: &str, reference_str: &str) -> Response {
if let Err(e) = validate_name(name) {
return e.into_response();
}
let Ok(reference) = parse_reference(reference_str) else {
return manifest_not_found(name, reference_str);
};
match state.registry.get_manifest(name, &reference).await {
Ok(Some((digest, media_type, body))) => {
let headers = manifest_response_headers(&digest, &media_type, body.len());
(StatusCode::OK, headers).into_response()
}
Ok(None) => manifest_not_found(name, reference_str),
Err(e) => OciError::from(e).into_response(),
}
}
fn manifest_not_found(name: &str, reference_str: &str) -> Response {
OciError::new(
OciErrorCode::ManifestUnknown,
format!("manifest {reference_str} not found in {name}"),
)
.into_response()
}
pub async fn put_manifest(
state: &AppState,
name: &str,
reference_str: &str,
headers: &HeaderMap,
body: Bytes,
) -> Response {
if let Err(e) = validate_name(name) {
return e.into_response();
}
let reference = match parse_reference(reference_str) {
Ok(r) => r,
Err(e) => return e.into_response(),
};
let content_type = match headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
{
Some(s) => s.to_owned(),
None => {
return OciError::new(OciErrorCode::ManifestInvalid, "missing Content-Type")
.into_response();
}
};
let Some(kind) = classify_manifest_media_type(&content_type) else {
return OciError::new(
OciErrorCode::ManifestInvalid,
format!("unsupported manifest media type `{content_type}`"),
)
.into_response();
};
let parsed: Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(e) => {
return OciError::new(
OciErrorCode::ManifestInvalid,
format!("manifest is not valid JSON: {e}"),
)
.into_response();
}
};
if let Err(e) = verify_referenced_blobs(state, &parsed, kind).await {
return e.into_response();
}
let digest = Digest::sha256_of(&body);
let subject_digest = parsed
.get("subject")
.and_then(|s| s.get("digest"))
.and_then(Value::as_str)
.and_then(|s| s.parse::<Digest>().ok());
let artifact_type = parsed
.get("artifactType")
.and_then(Value::as_str)
.map(str::to_owned);
let annotations = parsed
.get("annotations")
.and_then(Value::as_object)
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_owned())))
.collect::<std::collections::BTreeMap<_, _>>()
});
if let Err(e) = state
.registry
.put_manifest(name, &reference, &digest, &content_type, body.clone())
.await
{
return OciError::from(e).into_response();
}
if let Some(subj) = subject_digest {
let descriptor = ReferrerDescriptor {
media_type: content_type.clone(),
digest: digest.clone(),
size: body.len() as u64,
artifact_type,
annotations,
};
if let Err(e) = state
.registry
.register_referrer(name, &subj, descriptor)
.await
{
return OciError::from(e).into_response();
}
}
let mut out = HeaderMap::new();
let location = format!("/v2/{name}/manifests/{digest}");
if let Ok(v) = HeaderValue::from_str(&location) {
out.insert(header::LOCATION, v);
}
if let Ok(v) = HeaderValue::from_str(&digest.to_string()) {
out.insert("Docker-Content-Digest", v);
}
if let Some(subj) = parsed
.get("subject")
.and_then(|s| s.get("digest"))
.and_then(Value::as_str)
&& let Ok(v) = HeaderValue::from_str(subj)
{
out.insert("OCI-Subject", v);
}
out.insert(header::CONTENT_LENGTH, HeaderValue::from(0u64));
(StatusCode::CREATED, out).into_response()
}
pub async fn delete_manifest(state: &AppState, name: &str, reference_str: &str) -> Response {
if let Err(e) = validate_name(name) {
return e.into_response();
}
let reference = match parse_reference(reference_str) {
Ok(r) => r,
Err(e) => return e.into_response(),
};
if reference.is_tag() {
return OciError::new(
OciErrorCode::Unsupported,
"DELETE manifest by tag is not supported; use digest",
)
.with_status(StatusCode::METHOD_NOT_ALLOWED)
.into_response();
}
match state.registry.delete_manifest(name, &reference).await {
Ok(true) => (StatusCode::ACCEPTED, HeaderMap::new()).into_response(),
Ok(false) => OciError::new(
OciErrorCode::ManifestUnknown,
format!("manifest {reference_str} not found in {name}"),
)
.into_response(),
Err(e) => OciError::from(e).into_response(),
}
}
async fn verify_referenced_blobs(
state: &AppState,
parsed: &Value,
kind: ManifestKind,
) -> Result<(), OciError> {
match kind {
ManifestKind::ImageManifest | ManifestKind::Artifact => {
if let Some(config) = parsed.get("config").and_then(Value::as_object)
&& let Some(d) = config.get("digest").and_then(Value::as_str)
{
check_blob_present(state, d).await?;
}
if let Some(layers) = parsed.get("layers").and_then(Value::as_array) {
for layer in layers {
if let Some(d) = layer.get("digest").and_then(Value::as_str) {
check_blob_present(state, d).await?;
}
}
}
}
ManifestKind::ImageIndex => {
if let Some(manifests) = parsed.get("manifests").and_then(Value::as_array) {
for manifest in manifests {
if let Some(d) = manifest.get("digest").and_then(Value::as_str) {
let digest = d.parse::<Digest>().map_err(|e| {
OciError::new(
OciErrorCode::ManifestInvalid,
format!("invalid digest in manifests[]: {e}"),
)
})?;
let present = state
.blob_store
.contains(&digest)
.await
.map_err(OciError::from)?;
if !present {
return Err(OciError::new(
OciErrorCode::ManifestBlobUnknown,
format!("referenced manifest digest {d} not present"),
));
}
}
}
}
}
}
Ok(())
}
const OCI_EMPTY_DESCRIPTOR_DIGEST: &str =
"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
async fn check_blob_present(state: &AppState, digest_str: &str) -> Result<(), OciError> {
if digest_str == OCI_EMPTY_DESCRIPTOR_DIGEST {
return Ok(());
}
let digest = digest_str.parse::<Digest>().map_err(|e| {
OciError::new(
OciErrorCode::ManifestInvalid,
format!("invalid digest `{digest_str}`: {e}"),
)
})?;
let present = state
.blob_store
.contains(&digest)
.await
.map_err(OciError::from)?;
if !present {
return Err(OciError::new(
OciErrorCode::ManifestBlobUnknown,
format!("referenced blob {digest_str} not present"),
));
}
Ok(())
}