1use core::fmt::{self, Write};
11
12use sha2::{Digest as _, Sha256};
13
14use crate::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)]
147pub struct BufferTooSmall;
148
149impl fmt::Display for BufferTooSmall {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 f.write_str("registry listing buffer too small")
152 }
153}
154
155impl From<fmt::Error> for BufferTooSmall {
156 fn from(_: fmt::Error) -> Self {
157 Self
158 }
159}
160
161pub fn serve<'a>(
167 store: &'a impl Store,
168 method: &str,
169 target: &'a str,
170 scratch: &'a mut [u8],
171) -> Result<Response<'a>, BufferTooSmall> {
172 let head = method == "HEAD";
173 if method == "OPTIONS" {
174 return Ok(response(204, b"", "application/json", None, false));
175 }
176 if method != "GET" && !head {
177 return Ok(error(405, "UNSUPPORTED", head));
178 }
179 let (path, query) = target.split_once('?').unwrap_or((target, ""));
180 if path == "/v2/" || path == "/v2" {
181 return Ok(response(200, b"{}", "application/json", None, head));
182 }
183 let Some(rest) = path.strip_prefix("/v2/") else {
184 return Ok(error(404, "NAME_UNKNOWN", head));
185 };
186 let listing = if rest == "_catalog" {
187 Some(None)
188 } else {
189 rest.strip_suffix("/tags/list").map(Some)
190 };
191 if let Some(repository) = listing {
192 if repository.is_some_and(|repo| !store.contains_repository(repo)) {
193 return Ok(error(404, "NAME_UNKNOWN", head));
194 }
195 let mut cursor = [0u8; 256];
196 let (n, last) = match pagination(query, &mut cursor) {
197 Some(value) => value,
198 None => return Ok(error(400, "UNSUPPORTED", head)),
199 };
200 let name_at = |index| match repository {
201 Some(repo) => store.tag(repo, index),
202 None => store.repository(index),
203 };
204 let mut output = Output {
205 bytes: scratch,
206 len: 0,
207 };
208 if let Some(repo) = repository {
209 write!(output, r#"{{"name":"{repo}","tags":["#)?;
210 } else {
211 output.write_str(r#"{"repositories":["#)?;
212 }
213 let mut previous = last;
214 let mut count = 0;
215 let mut emitted = None;
216 let mut next = None;
217 loop {
218 if n == 0 {
219 break;
220 }
221 let Some(name) = (0..)
224 .map_while(&name_at)
225 .filter(|name| *name > previous)
226 .min()
227 else {
228 break;
229 };
230 if count == n {
231 next = Some(NextLink {
232 path,
233 last: emitted.unwrap(),
234 n,
235 });
236 break;
237 }
238 if count != 0 {
239 output.write_str(",")?;
240 }
241 write!(output, "\"{name}\"")?;
243 previous = name;
244 emitted = Some(name);
245 count += 1;
246 }
247 output.write_str("]}")?;
248 let length = output.len;
249 let mut result = response(200, &scratch[..length], "application/json", None, head);
250 result.next = next;
251 return Ok(result);
252 }
253 let Some((prefix, reference)) = rest.rsplit_once('/') else {
254 return Ok(error(404, "NAME_UNKNOWN", head));
255 };
256 let Some((repo, operation)) = prefix.rsplit_once('/') else {
257 return Ok(error(404, "NAME_UNKNOWN", head));
258 };
259 if !store.contains_repository(repo) {
260 return Ok(error(404, "NAME_UNKNOWN", head));
261 }
262 let (content, missing) = match operation {
263 "manifests" => (store.manifest(repo, reference), "MANIFEST_UNKNOWN"),
264 "blobs" => {
265 let Ok(digest) = Digest::parse(reference) else {
266 return Ok(error(400, "DIGEST_INVALID", head));
267 };
268 (
269 store.blob(repo, digest).map(|content| Content {
270 media_type: "application/octet-stream",
271 ..content
272 }),
273 "BLOB_UNKNOWN",
274 )
275 }
276 _ => return Ok(error(404, "NAME_UNKNOWN", head)),
277 };
278 Ok(match content {
279 Some(content) => response(
280 200,
281 content.bytes,
282 content.media_type,
283 Some(content.digest),
284 head,
285 ),
286 None => error(404, missing, head),
287 })
288}
289
290fn response<'a>(
291 status: u16,
292 bytes: &'a [u8],
293 media_type: &'a str,
294 digest: Option<Digest>,
295 head: bool,
296) -> Response<'a> {
297 Response {
298 status,
299 media_type,
300 body: if head { b"" } else { bytes },
301 content_length: bytes.len(),
302 digest,
303 next: None,
304 }
305}
306
307fn error(status: u16, code: &str, head: bool) -> Response<'static> {
308 let bytes: &[u8] = match code {
309 "MANIFEST_UNKNOWN" => {
310 br#"{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown"}]}"#
311 }
312 "BLOB_UNKNOWN" => br#"{"errors":[{"code":"BLOB_UNKNOWN","message":"blob unknown"}]}"#,
313 "DIGEST_INVALID" => br#"{"errors":[{"code":"DIGEST_INVALID","message":"invalid digest"}]}"#,
314 "UNSUPPORTED" => br#"{"errors":[{"code":"UNSUPPORTED","message":"unsupported request"}]}"#,
315 _ => br#"{"errors":[{"code":"NAME_UNKNOWN","message":"repository unknown"}]}"#,
316 };
317 response(status, bytes, "application/json", None, head)
318}
319
320fn pagination<'a>(query: &str, cursor: &'a mut [u8]) -> Option<(usize, &'a str)> {
321 let mut n = None;
322 let mut last = None;
323 for parameter in query.split('&').filter(|item| !item.is_empty()) {
324 let (key, value) = parameter.split_once('=').unwrap_or((parameter, ""));
325 match key {
326 "n" => {
327 if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
328 return None;
329 }
330 if n.replace(value.parse().ok()?).is_some() {
331 return None;
332 }
333 }
334 "last" if last.replace(value).is_some() => return None,
335 _ => {}
336 }
337 }
338 let mut bytes = last.unwrap_or("").bytes();
339 let mut len = 0;
340 while let Some(byte) = bytes.next() {
341 let decoded = if byte == b'%' {
342 let high = (bytes.next()? as char).to_digit(16)?;
343 let low = (bytes.next()? as char).to_digit(16)?;
344 (high * 16 + low) as u8
345 } else if byte == b'+' {
346 b' '
347 } else {
348 byte
349 };
350 *cursor.get_mut(len)? = decoded;
351 len += 1;
352 }
353 Some((
354 n.unwrap_or(usize::MAX),
355 core::str::from_utf8(&cursor[..len]).ok()?,
356 ))
357}
358
359struct Output<'a> {
360 bytes: &'a mut [u8],
361 len: usize,
362}
363
364impl Write for Output<'_> {
365 fn write_str(&mut self, value: &str) -> fmt::Result {
366 let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
367 self.bytes
368 .get_mut(self.len..end)
369 .ok_or(fmt::Error)?
370 .copy_from_slice(value.as_bytes());
371 self.len = end;
372 Ok(())
373 }
374}