1use futures::TryStreamExt;
2use nix_compat::nar::listing::{Listing, ListingEntry, ListingVersion};
3use snix_castore::{B3Digest, Directory, Node, directoryservice::DirectoryService};
4
5use snix_castore::directoryservice::order_validator::{self, OrderValidator, RootToLeaves};
6use std::{
7 collections::{BTreeMap, HashMap},
8 sync::atomic::{AtomicU64, Ordering},
9};
10
11struct CountingWriter<'a> {
18 bytes_written: &'a AtomicU64,
19}
20
21impl std::io::Write for CountingWriter<'_> {
22 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
23 self.bytes_written
24 .fetch_add(buf.len() as u64, Ordering::Relaxed);
25 Ok(buf.len())
26 }
27
28 fn flush(&mut self) -> std::io::Result<()> {
29 Ok(())
30 }
31}
32
33pub async fn produce_listing<DS>(root_node: &Node, directory_service: &DS) -> Result<Listing, Error>
37where
38 DS: DirectoryService,
39{
40 let mut directories: HashMap<B3Digest, Directory> = HashMap::new();
41
42 if let Node::Directory { digest, .. } = root_node {
43 let mut directories_stream = directory_service.get_recursive(digest);
44 let mut validator = RootToLeaves::new_with_root_digest(digest.to_owned());
45 while let Some(directory) = directories_stream.try_next().await? {
46 validator.try_accept(&directory)?;
47 directories.insert(directory.digest(), directory);
48 }
49 };
50
51 let bytes_written = AtomicU64::new(0);
52 let mut writer = CountingWriter {
53 bytes_written: &bytes_written,
54 };
55 let nar_node = nix_compat::nar::writer::open(&mut writer).expect("writing");
56
57 Ok(Listing::V1 {
60 root: produce_listing_inner(root_node, &bytes_written, nar_node, &|digest| {
61 directories
63 .get(digest)
64 .expect("Snix bug: lookup of unknown directory")
65 })?,
66 version: ListingVersion,
67 })
68}
69
70fn produce_listing_inner<'d, F>(
71 node: &Node,
72 bytes_written: &AtomicU64,
73 writer_node: nix_compat::nar::writer::Node<'_, impl std::io::Write>,
74 get_directory: &'d F,
75) -> Result<ListingEntry, Error>
76where
77 F: Fn(&B3Digest) -> &'d Directory + 'd,
78{
79 Ok(match node {
80 Node::Directory { digest, .. } => {
81 let mut writer_entries = writer_node.directory().expect("nar writing");
82
83 let nodes_it = get_directory(digest).nodes();
84
85 let mut child_entries: BTreeMap<String, ListingEntry> = BTreeMap::new();
86
87 for (child_name, child_node) in nodes_it {
88 let writer_child_node = writer_entries
89 .entry(child_name.as_ref())
90 .expect("nar writing");
91 child_entries.insert(
92 str::from_utf8(child_name.as_ref())
93 .or(Err(Error::PathComponentIsNoString))?
94 .to_owned(),
95 produce_listing_inner(
96 child_node,
97 bytes_written,
98 writer_child_node,
99 get_directory,
100 )?,
101 );
102 }
103
104 writer_entries.close().expect("nar writing");
105
106 ListingEntry::Directory {
107 entries: child_entries,
108 }
109 }
110 Node::File {
111 size, executable, ..
112 } => {
113 let (w, hdl) = writer_node
114 .file_manual_write(*executable, *size)
115 .expect("nar writing");
116
117 let nar_offset = bytes_written.fetch_add(*size, Ordering::Relaxed);
119
120 hdl.close(w).expect("nar writing");
121
122 ListingEntry::Regular {
123 size: *size,
124 executable: *executable,
125 nar_offset,
126 }
127 }
128 Node::Symlink { target } => {
129 writer_node.symlink(target.as_ref()).expect("nar writing");
130
131 ListingEntry::Symlink {
132 target: str::from_utf8(target.as_ref())
133 .or(Err(Error::SymlinkTargetIsNoString))?
134 .to_owned(),
135 }
136 }
137 })
138}
139#[derive(Debug, thiserror::Error)]
140pub enum Error {
141 #[error("symlink target is not a string")]
142 SymlinkTargetIsNoString,
143 #[error("path component is not a string")]
144 PathComponentIsNoString,
145 #[error("from directoryservice: {0}")]
146 DirectoryService(#[from] snix_castore::directoryservice::Error),
147 #[error("ordering error from directoryservice: {0}")]
148 OrderingError(#[from] order_validator::OrderingError),
149}
150
151#[cfg(test)]
152mod tests {
153 use super::produce_listing;
154 use crate::fixtures::{
155 CASTORE_NODE_COMPLICATED, CASTORE_NODE_HELLOWORLD, CASTORE_NODE_SYMLINK,
156 };
157 use rstest::rstest;
158 use snix_castore::Directory;
159 use snix_castore::fixtures::{DIRECTORY_COMPLICATED, DIRECTORY_WITH_KEEP};
160 use snix_castore::{
161 Node, directoryservice::DirectoryService, utils::gen_test_directory_service,
162 };
163
164 #[tokio::test]
165 #[rstest]
166 #[case::symlink(&CASTORE_NODE_SYMLINK, vec![],
167 r#"{"root":{"target":"/nix/store/somewhereelse","type":"symlink"},"version":1}"#)]
168 #[case::blob(&CASTORE_NODE_HELLOWORLD, vec![],
169 r#"{"root":{"narOffset":96,"size":12,"type":"regular"},"version":1}"#)]
170 #[case::complicated(&CASTORE_NODE_COMPLICATED, vec![&*DIRECTORY_COMPLICATED, &*DIRECTORY_WITH_KEEP],
171 r#"{"root":{"entries":{".keep":{"narOffset":232,"size":0,"type":"regular"},"aa":{"target":"/nix/store/somewhereelse","type":"symlink"},"keep":{"entries":{".keep":{"narOffset":760,"size":0,"type":"regular"}},"type":"directory"}},"type":"directory"},"version":1}"#)]
172 async fn produce_listing_test(
173 #[case] root_node: &Node,
174 #[case] directories: Vec<&Directory>,
175 #[case] exp_json: &str,
176 ) {
177 let svc = gen_test_directory_service();
178 for directory in directories {
179 svc.put(directory.to_owned()).await.expect("must insert");
180 }
181
182 let listing = produce_listing(root_node, &svc)
183 .await
184 .expect("must succeed");
185 let actual_json = serde_json::to_string(&listing).expect("must serialize");
186
187 assert_eq!(exp_json, actual_json);
188 }
189}