1use axum::{http::StatusCode, response::IntoResponse};
2use bytes::Bytes;
3use nix_compat::{
4 narinfo::{NarInfo, Signature},
5 nix_http, nixbase32,
6 store_path::StorePath,
7};
8use snix_castore::proto::write_infused_nar_path;
9use snix_store::pathinfoservice::PathInfo;
10use tracing::{Span, instrument, warn};
11
12use crate::AppState;
13
14const NARINFO_LIMIT: usize = 2 * 1024 * 1024;
16
17#[instrument(skip_all, fields(path_info.name=%narinfo_str))]
18pub async fn head(
19 axum::extract::Path(narinfo_str): axum::extract::Path<String>,
20 axum::extract::State(AppState {
21 path_info_service, ..
22 }): axum::extract::State<AppState>,
23) -> Result<impl IntoResponse, StatusCode> {
24 let digest = nix_http::parse_narinfo_str(&narinfo_str).ok_or(StatusCode::NOT_FOUND)?;
25 Span::current().record("path_info.digest", &narinfo_str[0..32]);
26
27 if path_info_service.has(digest).await.map_err(|e| {
28 warn!(err=%e, "failed to get PathInfo");
29 StatusCode::INTERNAL_SERVER_ERROR
30 })? {
31 Ok(([("content-type", nix_http::MIME_TYPE_NARINFO)], ""))
32 } else {
33 warn!("PathInfo not found");
34 Err(StatusCode::NOT_FOUND)
35 }
36}
37
38#[instrument(skip_all, fields(path_info.name=%narinfo_str))]
39pub async fn get(
40 axum::extract::Path(narinfo_str): axum::extract::Path<String>,
41 axum::extract::State(AppState {
42 path_info_service, ..
43 }): axum::extract::State<AppState>,
44) -> Result<impl IntoResponse, StatusCode> {
45 let digest = nix_http::parse_narinfo_str(&narinfo_str).ok_or(StatusCode::NOT_FOUND)?;
46 Span::current().record("path_info.digest", &narinfo_str[0..32]);
47
48 let path_info = path_info_service
50 .get(digest)
51 .await
52 .map_err(|e| {
53 warn!(err=%e, "failed to get PathInfo");
54 StatusCode::INTERNAL_SERVER_ERROR
55 })?
56 .ok_or(StatusCode::NOT_FOUND)?;
57
58 Ok((
59 [("content-type", nix_http::MIME_TYPE_NARINFO)],
60 gen_narinfo_str(&path_info),
61 ))
62}
63
64#[instrument(skip_all, fields(path_info.name=%narinfo_str))]
65pub async fn put(
66 axum::extract::Path(narinfo_str): axum::extract::Path<String>,
67 axum::extract::State(AppState {
68 path_info_service,
69 root_nodes,
70 ..
71 }): axum::extract::State<AppState>,
72 request: axum::extract::Request,
73) -> Result<&'static str, StatusCode> {
74 let _narinfo_digest = nix_http::parse_narinfo_str(&narinfo_str).ok_or(StatusCode::UNAUTHORIZED);
75 Span::current().record("path_info.digest", &narinfo_str[0..32]);
76
77 let narinfo_bytes: Bytes = axum::body::to_bytes(request.into_body(), NARINFO_LIMIT)
78 .await
79 .map_err(|e| {
80 warn!(err=%e, "unable to fetch body");
81 StatusCode::BAD_REQUEST
82 })?;
83
84 let narinfo_str = std::str::from_utf8(narinfo_bytes.as_ref()).map_err(|e| {
86 warn!(err=%e, "unable decode body as string");
87 StatusCode::BAD_REQUEST
88 })?;
89
90 let narinfo = NarInfo::parse(narinfo_str).map_err(|e| {
91 warn!(err=%e, "unable to parse narinfo");
92 StatusCode::BAD_REQUEST
93 })?;
94
95 Span::current().record("path_info.nar_info", nixbase32::encode(&narinfo.nar_hash));
97
98 let maybe_root_node: Option<snix_castore::Node> =
101 root_nodes.read().peek(&narinfo.nar_hash).cloned();
102
103 match maybe_root_node {
104 Some(root_node) => {
105 path_info_service
107 .put(PathInfo {
108 store_path: narinfo.store_path.to_owned(),
109 node: root_node,
110 references: narinfo.references.iter().map(StorePath::to_owned).collect(),
111 nar_sha256: narinfo.nar_hash,
112 nar_size: narinfo.nar_size,
113 signatures: narinfo
114 .signatures
115 .into_iter()
116 .map(|s| {
117 Signature::<String>::new(s.name().to_string(), s.bytes().to_owned())
118 })
119 .collect(),
120 deriver: narinfo.deriver.as_ref().map(StorePath::to_owned),
121 ca: narinfo.ca,
122 })
123 .await
124 .map_err(|e| {
125 warn!(err=%e, "failed to persist the PathInfo");
126 StatusCode::INTERNAL_SERVER_ERROR
127 })?;
128
129 Ok("")
130 }
131 None => {
132 warn!("received narinfo with unknown NARHash");
133 Err(StatusCode::BAD_REQUEST)
134 }
135 }
136}
137
138fn gen_narinfo_str(path_info: &PathInfo) -> String {
140 let mut narinfo = path_info.to_narinfo();
141 let mut url = String::new();
142 write_infused_nar_path(&mut url, path_info.node.clone(), narinfo.nar_size)
143 .expect("write into string");
144 narinfo.url = &url;
145
146 narinfo.file_size = Some(narinfo.nar_size);
148
149 narinfo.to_string()
150}
151
152#[cfg(test)]
153mod tests {
154 use std::{num::NonZero, sync::Arc};
155
156 use axum::http::Method;
157 use nix_compat::nixbase32;
158 use snix_castore::{
159 blobservice::{BlobService, MemoryBlobService},
160 directoryservice::DirectoryService,
161 utils::gen_test_directory_service,
162 };
163 use snix_store::{
164 fixtures::{DUMMY_PATH_DIGEST, NAR_CONTENTS_SYMLINK, PATH_INFO, PATH_INFO_SYMLINK},
165 path_info::PathInfo,
166 pathinfoservice::PathInfoService,
167 utils::gen_test_pathinfo_service,
168 };
169 use tracing_test::traced_test;
170
171 use crate::AppState;
172
173 fn gen_server(
176 router: axum::Router<AppState>,
177 ) -> (
178 axum_test::TestServer,
179 impl BlobService,
180 impl DirectoryService,
181 impl PathInfoService,
182 ) {
183 let blob_service = Arc::new(MemoryBlobService::default());
184 let directory_service = Arc::new(gen_test_directory_service());
185 let path_info_service = Arc::new(gen_test_pathinfo_service());
186
187 let app = router.with_state(AppState::new(
188 blob_service.clone(),
189 directory_service.clone(),
190 path_info_service.clone(),
191 NonZero::new(100).unwrap(),
192 ));
193
194 (
195 axum_test::TestServer::new(app).unwrap(),
196 blob_service,
197 directory_service,
198 path_info_service,
199 )
200 }
201
202 fn gen_nix_like_narinfo(path_info: &PathInfo) -> String {
203 let mut narinfo = path_info.to_narinfo();
204
205 let url = format!("nar/{}.nar", nixbase32::encode(&path_info.nar_sha256));
206 narinfo.url = &url;
207 narinfo.to_string()
208 }
209
210 #[traced_test]
212 #[tokio::test]
213 async fn test_get_head_not_found() {
214 let (server, _blob_service, _directory_service, _path_info_service) =
215 gen_server(crate::gen_router(100));
216
217 let url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST));
218
219 server
221 .method(Method::HEAD, url)
222 .expect_failure()
223 .await
224 .assert_status_not_found();
225
226 server
228 .get(url)
229 .expect_failure()
230 .await
231 .assert_status_not_found();
232 }
233
234 #[traced_test]
236 #[tokio::test]
237 async fn test_get_head_found() {
238 let (server, _blob_service, _directory_service, path_info_service) =
239 gen_server(crate::gen_router(100));
240
241 let url = &format!("{}.narinfo", nixbase32::encode(&DUMMY_PATH_DIGEST));
242
243 path_info_service
244 .put(PATH_INFO.clone())
245 .await
246 .expect("put pathinfo");
247
248 server
249 .method(Method::HEAD, url)
250 .expect_success()
251 .await
252 .assert_status_ok();
253
254 let narinfo_bytes = server.get(url).expect_success().await.into_bytes();
256
257 assert_eq!(crate::narinfo::gen_narinfo_str(&PATH_INFO), narinfo_bytes);
258 }
259
260 #[traced_test]
262 #[tokio::test]
263 async fn test_put_without_prev_nar_fail() {
264 let (server, _blob_service, _directory_service, _path_info_service) =
265 gen_server(crate::gen_router(100));
266
267 let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK);
271
272 server
273 .put(&format!(
274 "{}.narinfo",
275 nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256)
276 ))
277 .text(narinfo_str)
278 .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO)
279 .expect_failure()
280 .await;
281 }
282
283 #[traced_test]
285 #[tokio::test]
286 async fn test_upload_nar_then_narinfo() {
287 let (server, _blob_service, _directory_service, _path_info_service) =
288 gen_server(crate::gen_router(100));
289
290 server
292 .put(&format!(
293 "/nar/{}.nar",
294 nixbase32::encode(&PATH_INFO_SYMLINK.nar_sha256)
295 ))
296 .bytes(NAR_CONTENTS_SYMLINK[..].into())
297 .expect_success()
298 .await;
299
300 let narinfo_str = gen_nix_like_narinfo(&PATH_INFO_SYMLINK);
301
302 server
304 .put(&format!(
305 "/{}.narinfo",
306 nixbase32::encode(PATH_INFO_SYMLINK.store_path.digest())
307 ))
308 .text(narinfo_str)
309 .content_type(nix_compat::nix_http::MIME_TYPE_NARINFO)
310 .expect_success()
311 .await;
312 }
313}