1use core::fmt::{self, Write};
11
12use sha2::{Digest as _, Sha256};
13
14use crate::{buffer::BufferWriter, digest::Digest};
15
16#[derive(Clone, Copy, Debug)]
18pub struct Content<'a> {
19 pub bytes: &'a [u8],
20 pub media_type: &'a str,
21 pub digest: Digest,
22}
23
24impl<'a> Content<'a> {
25 pub fn new(bytes: &'a [u8], media_type: &'a str) -> Self {
27 Self {
28 bytes,
29 media_type,
30 digest: Digest::from_bytes(Sha256::digest(bytes).into()),
31 }
32 }
33}
34
35pub trait Store {
41 fn repository(&self, index: usize) -> Option<&str>;
42 fn tag(&self, repository: &str, index: usize) -> Option<&str>;
43 fn contains_repository(&self, repository: &str) -> bool;
44 fn manifest(&self, repository: &str, reference: &str) -> Option<Content<'_>>;
45 fn blob(&self, repository: &str, digest: Digest) -> Option<Content<'_>>;
46}
47
48#[derive(Clone, Copy, Debug)]
50pub struct Tag<'a> {
51 pub name: &'a str,
52 pub digest: Digest,
53}
54
55#[derive(Clone, Copy, Debug)]
57pub struct Repository<'a> {
58 pub name: &'a str,
59 pub tags: &'a [Tag<'a>],
60 pub manifests: &'a [Content<'a>],
61 pub blobs: &'a [Content<'a>],
62}
63
64#[derive(Clone, Copy, Debug)]
67pub struct MemoryStore<'a>(pub &'a [Repository<'a>]);
68
69impl MemoryStore<'_> {
70 fn find(&self, name: &str) -> Option<&Repository<'_>> {
71 self.0.iter().find(|repo| repo.name == name)
72 }
73}
74
75impl Store for MemoryStore<'_> {
76 fn repository(&self, index: usize) -> Option<&str> {
77 self.0.get(index).map(|repo| repo.name)
78 }
79
80 fn tag(&self, repository: &str, index: usize) -> Option<&str> {
81 self.find(repository)?.tags.get(index).map(|tag| tag.name)
82 }
83
84 fn contains_repository(&self, repository: &str) -> bool {
85 self.find(repository).is_some()
86 }
87
88 fn manifest(&self, repository: &str, reference: &str) -> Option<Content<'_>> {
89 let repo = self.find(repository)?;
90 let digest = if let Ok(digest) = Digest::parse(reference) {
91 digest
92 } else {
93 repo.tags.iter().find(|tag| tag.name == reference)?.digest
94 };
95 repo.manifests
96 .iter()
97 .find(|item| item.digest == digest)
98 .copied()
99 }
100
101 fn blob(&self, repository: &str, digest: Digest) -> Option<Content<'_>> {
102 self.find(repository)?
103 .blobs
104 .iter()
105 .find(|item| item.digest == digest)
106 .copied()
107 }
108}
109
110#[derive(Debug)]
112pub struct Response<'a> {
113 pub status: u16,
114 pub media_type: &'a str,
115 pub body: &'a [u8],
116 pub content_length: usize,
118 pub digest: Option<Digest>,
119 pub next: Option<NextLink<'a>>,
120}
121
122#[derive(Clone, Copy, Debug)]
124pub struct NextLink<'a> {
125 pub path: &'a str,
126 pub last: &'a str,
127 pub n: usize,
128}
129
130impl fmt::Display for NextLink<'_> {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(f, "<{}?n={}&last=", self.path, self.n)?;
133 for byte in self.last.bytes() {
134 if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) {
135 write!(f, "{}", byte as char)?;
136 } else {
137 write!(f, "%{byte:02X}")?;
138 }
139 }
140 f.write_str(">; rel=\"next\"")
141 }
142}
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq, derive_more::Display)]
147#[display("registry listing buffer too small")]
148pub struct BufferTooSmall;
149
150impl From<fmt::Error> for BufferTooSmall {
151 fn from(_: fmt::Error) -> Self {
152 Self
153 }
154}
155
156pub fn serve<'a>(
162 store: &'a impl Store,
163 method: &str,
164 target: &'a str,
165 scratch: &'a mut [u8],
166) -> Result<Response<'a>, BufferTooSmall> {
167 let head = method == "HEAD";
168 if method == "OPTIONS" {
169 return Ok(response(204, b"", "application/json", None, false));
170 }
171 if method != "GET" && !head {
172 return Ok(error(405, "UNSUPPORTED", head));
173 }
174 let (path, query) = target.split_once('?').unwrap_or((target, ""));
175 if path == "/v2/" || path == "/v2" {
176 return Ok(response(200, b"{}", "application/json", None, head));
177 }
178 let Some(rest) = path.strip_prefix("/v2/") else {
179 return Ok(error(404, "NAME_UNKNOWN", head));
180 };
181 let listing = if rest == "_catalog" {
182 Some(None)
183 } else {
184 rest.strip_suffix("/tags/list").map(Some)
185 };
186 if let Some(repository) = listing {
187 return serve_listing(store, repository, path, query, scratch, head);
188 }
189 let Some((prefix, reference)) = rest.rsplit_once('/') else {
190 return Ok(error(404, "NAME_UNKNOWN", head));
191 };
192 let Some((repo, operation)) = prefix.rsplit_once('/') else {
193 return Ok(error(404, "NAME_UNKNOWN", head));
194 };
195 if !store.contains_repository(repo) {
196 return Ok(error(404, "NAME_UNKNOWN", head));
197 }
198 let (content, missing) = match operation {
199 "manifests" => (store.manifest(repo, reference), "MANIFEST_UNKNOWN"),
200 "blobs" => {
201 let Ok(digest) = Digest::parse(reference) else {
202 return Ok(error(400, "DIGEST_INVALID", head));
203 };
204 (
205 store.blob(repo, digest).map(|content| Content {
206 media_type: "application/octet-stream",
207 ..content
208 }),
209 "BLOB_UNKNOWN",
210 )
211 }
212 _ => return Ok(error(404, "NAME_UNKNOWN", head)),
213 };
214 Ok(match content {
215 Some(content) => response(
216 200,
217 content.bytes,
218 content.media_type,
219 Some(content.digest),
220 head,
221 ),
222 None => error(404, missing, head),
223 })
224}
225
226fn serve_listing<'a>(
227 store: &'a impl Store,
228 repository: Option<&'a str>,
229 path: &'a str,
230 query: &str,
231 scratch: &'a mut [u8],
232 head: bool,
233) -> Result<Response<'a>, BufferTooSmall> {
234 if repository.is_some_and(|repo| !store.contains_repository(repo)) {
235 return Ok(error(404, "NAME_UNKNOWN", head));
236 }
237 let mut cursor = [0u8; 256];
238 let (n, last) = match pagination(query, &mut cursor) {
239 Some(value) => value,
240 None => return Ok(error(400, "UNSUPPORTED", head)),
241 };
242 let name_at = |index| match repository {
243 Some(repo) => store.tag(repo, index),
244 None => store.repository(index),
245 };
246 let mut output = BufferWriter::new(scratch);
247 if let Some(repo) = repository {
248 write!(output, r#"{{"name":"{repo}","tags":["#)?;
249 } else {
250 output.write_str(r#"{"repositories":["#)?;
251 }
252 let mut previous = last;
253 let mut count = 0;
254 let mut emitted = None;
255 let mut next = None;
256 loop {
257 if n == 0 {
258 break;
259 }
260 let Some(name) = (0..)
263 .map_while(&name_at)
264 .filter(|name| *name > previous)
265 .min()
266 else {
267 break;
268 };
269 if count == n {
270 next = Some(NextLink {
271 path,
272 last: emitted.unwrap(),
273 n,
274 });
275 break;
276 }
277 if count != 0 {
278 output.write_str(",")?;
279 }
280 write!(output, "\"{name}\"")?;
282 previous = name;
283 emitted = Some(name);
284 count += 1;
285 }
286 output.write_str("]}")?;
287 let length = output.len();
288 let mut result = response(200, &scratch[..length], "application/json", None, head);
289 result.next = next;
290 Ok(result)
291}
292
293fn response<'a>(
294 status: u16,
295 bytes: &'a [u8],
296 media_type: &'a str,
297 digest: Option<Digest>,
298 head: bool,
299) -> Response<'a> {
300 Response {
301 status,
302 media_type,
303 body: if head { b"" } else { bytes },
304 content_length: bytes.len(),
305 digest,
306 next: None,
307 }
308}
309
310fn error(status: u16, code: &str, head: bool) -> Response<'static> {
311 let bytes: &[u8] = match code {
312 "MANIFEST_UNKNOWN" => {
313 br#"{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown"}]}"#
314 }
315 "BLOB_UNKNOWN" => br#"{"errors":[{"code":"BLOB_UNKNOWN","message":"blob unknown"}]}"#,
316 "DIGEST_INVALID" => br#"{"errors":[{"code":"DIGEST_INVALID","message":"invalid digest"}]}"#,
317 "UNSUPPORTED" => br#"{"errors":[{"code":"UNSUPPORTED","message":"unsupported request"}]}"#,
318 _ => br#"{"errors":[{"code":"NAME_UNKNOWN","message":"repository unknown"}]}"#,
319 };
320 response(status, bytes, "application/json", None, head)
321}
322
323fn pagination<'a>(query: &str, cursor: &'a mut [u8]) -> Option<(usize, &'a str)> {
324 let mut n = None;
325 let mut last = None;
326 for parameter in query.split('&').filter(|item| !item.is_empty()) {
327 let (key, value) = parameter.split_once('=').unwrap_or((parameter, ""));
328 match key {
329 "n" => {
330 if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
331 return None;
332 }
333 if n.replace(value.parse().ok()?).is_some() {
334 return None;
335 }
336 }
337 "last" if last.replace(value).is_some() => return None,
338 _ => {}
339 }
340 }
341 let mut bytes = last.unwrap_or("").bytes();
342 let mut len = 0;
343 while let Some(byte) = bytes.next() {
344 let decoded = if byte == b'%' {
345 let high = (bytes.next()? as char).to_digit(16)?;
346 let low = (bytes.next()? as char).to_digit(16)?;
347 (high * 16 + low) as u8
348 } else if byte == b'+' {
349 b' '
350 } else {
351 byte
352 };
353 *cursor.get_mut(len)? = decoded;
354 len += 1;
355 }
356 Some((
357 n.unwrap_or(usize::MAX),
358 core::str::from_utf8(&cursor[..len]).ok()?,
359 ))
360}