1use include_dir::{Dir, include_dir};
10
11pub use crate::payload_roots::PAYLOAD_ROOTS;
12
13pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
15
16pub static BINDINGS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/bindings");
18
19pub static RUNBOOKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/runbooks");
21
22pub static FORGES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/forges");
24
25pub static SETUP: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/setup");
28
29pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/snippets");
32
33pub static BLOCKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/blocks");
40
41pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
43
44pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
46
47pub static VERSIONS: &str = include_str!("../versions.toml");
49
50pub static LICENSE: &str = include_str!("../LICENSE");
52
53pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
55
56pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
58
59pub const SENTINEL: &str = "TODO(release-kit)";
62
63pub(crate) fn walk<'a>(dir: &Dir<'a>) -> Vec<(String, &'a [u8])> {
66 let mut out = Vec::new();
67 for file in dir.files() {
68 out.push((file.path().to_string_lossy().into_owned(), file.contents()));
69 }
70 for sub in dir.dirs() {
71 out.extend(walk(sub));
72 }
73 out.sort_by(|a, b| a.0.cmp(&b.0));
74 out
75}
76
77#[must_use]
81pub fn root_files(root: &str) -> Option<Vec<(String, &'static [u8])>> {
82 let dir = match root {
83 "method" => &METHOD,
84 "bindings" => &BINDINGS,
85 "runbooks" => &RUNBOOKS,
86 "forges" => &FORGES,
87 "snippets" => &SNIPPETS,
88 "blocks" => &BLOCKS,
89 "setup" => &SETUP,
90 "skills" => &SKILLS,
91 "skill-shared" => &SKILL_SHARED,
92 "versions.toml" => return Some(vec![(root.to_owned(), VERSIONS.as_bytes())]),
93 _ => return None,
94 };
95 Some(
96 walk(dir)
97 .into_iter()
98 .map(|(path, bytes)| (format!("{root}/{path}"), bytes))
99 .collect(),
100 )
101}
102
103#[must_use]
109pub fn artifacts() -> Vec<(String, &'static [u8])> {
110 PAYLOAD_ROOTS
111 .iter()
112 .filter_map(|root| root_files(root))
113 .flatten()
114 .collect()
115}
116
117#[cfg(test)]
118mod tests {
119 #![allow(clippy::expect_used)]
120
121 use super::{PAYLOAD_ROOTS, artifacts, root_files};
122
123 #[test]
129 fn the_inventory_and_the_embed_declare_the_same_roots() {
130 let source = include_str!("embedded.rs");
131 let mut embedded: Vec<String> = source
132 .lines()
133 .filter_map(|line| {
134 let (_, rest) = line.split_once("include_dir!(\"$CARGO_MANIFEST_DIR/")?;
135 let (root, _) = rest.split_once('"')?;
136 Some(root.to_owned())
137 })
138 .collect();
139 embedded.extend(source.lines().filter_map(|line| {
140 let (_, rest) = line.split_once("include_str!(\"../")?;
141 let (name, _) = rest.split_once('"')?;
142 (!name.starts_with("LICENSE")).then(|| name.to_owned())
143 }));
144 embedded.sort();
145 let mut declared: Vec<String> = PAYLOAD_ROOTS.iter().map(ToString::to_string).collect();
146 declared.sort();
147 assert_eq!(
148 embedded, declared,
149 "src/embedded.rs and src/payload_roots.rs disagree on the payload roots"
150 );
151 }
152
153 #[test]
154 fn every_declared_root_serves_at_least_one_file() {
155 for root in PAYLOAD_ROOTS {
156 let files = root_files(root).expect("a declared root resolves");
157 assert!(!files.is_empty(), "{root}: the root carries no file");
158 for (path, _) in &files {
159 assert!(
160 path == root || path.starts_with(&format!("{root}/")),
161 "{path}: an artifact path must carry its root"
162 );
163 }
164 }
165 assert!(root_files("no-such-root").is_none());
166 }
167
168 #[test]
173 fn every_block_is_authored_with_one_final_newline() {
174 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("blocks");
175 for file in super::BLOCKS.files() {
176 let name = file.path().to_string_lossy().into_owned();
177 let disk = std::fs::read(root.join(&name)).expect("an embedded block exists on disk");
178 assert_eq!(disk, file.contents(), "{name}: embed and disk disagree");
179 let text = std::str::from_utf8(file.contents()).expect("a block is UTF-8");
180 assert!(text.ends_with('\n'), "{name}: a block ends in a newline");
181 assert!(
182 !text.ends_with("\n\n"),
183 "{name}: a block ends in exactly one newline"
184 );
185 }
186 }
187
188 #[test]
199 fn no_artifact_body_lives_as_a_source_literal() {
200 let needles = [
201 "## Releases",
202 "Installed by rk setup step branch-reminder",
203 "This project works in worktrees:",
204 "Branches are worked in the main checkout",
205 "stages: [commit-msg]",
206 "ROUTING_BLOCK",
207 "ROUTING_WORKTREE_LINE",
208 "ROUTING_BRANCHES_LINE",
209 "HOOKS_BLOCK",
210 "WORKTREE_GUARD_ENTRY",
211 "HOOK_BODY",
212 ];
213 let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
214 let mut offenders = Vec::new();
215 scan(&src, &needles, &mut offenders);
216 assert!(
217 offenders.is_empty(),
218 "an artifact body belongs under blocks/, not in the sources: {offenders:?}"
219 );
220 }
221
222 fn scan(dir: &std::path::Path, needles: &[&str], offenders: &mut Vec<String>) {
223 for entry in std::fs::read_dir(dir).expect("the source tree is readable") {
224 let entry = entry.expect("a directory entry is readable");
225 let path = entry.path();
226 if path.is_dir() {
227 scan(&path, needles, offenders);
228 continue;
229 }
230 if path.extension().is_none_or(|ext| ext != "rs") {
231 continue;
232 }
233 let text = std::fs::read_to_string(&path).expect("a source file is UTF-8");
234 let production = text.split("#[cfg(test)]").next().unwrap_or("");
235 for (index, line) in production.lines().enumerate() {
236 if line.trim_start().starts_with("//") {
237 continue;
238 }
239 for needle in needles {
240 if line.contains(needle) {
241 offenders.push(format!("{}:{}: {needle}", path.display(), index + 1));
242 }
243 }
244 }
245 for (line, span) in multiline_literals(production) {
246 offenders.push(format!(
247 "{}:{line}: a string literal spanning {span} lines",
248 path.display()
249 ));
250 }
251 }
252 }
253
254 const GLUE: [&str; 3] = [
260 "{}\\n\\n{block}\\n",
261 "{HOOK_TYPES_LINE}\\n\\nrepos:\\n{block}\\n",
262 concat!(
263 "Authorization: Bearer {jwt}\\nAccept: application/vnd.github+json\\n",
264 "X-GitHub-Api-Version: 2022-11-28\\n"
265 ),
266 ];
267
268 fn multiline_literals(text: &str) -> Vec<(usize, usize)> {
276 let bytes = text.as_bytes();
277 let mut spans = Vec::new();
278 let mut line = 1;
279 let mut i = 0;
280 while i < bytes.len() {
281 match bytes[i] {
282 b'\n' => {
283 line += 1;
284 i += 1;
285 }
286 b'/' if bytes.get(i + 1) == Some(&b'/') => {
287 while i < bytes.len() && bytes[i] != b'\n' {
288 i += 1;
289 }
290 }
291 b'r' if matches!(bytes.get(i + 1), Some(&b'#' | &b'"')) => {
292 let hashes = bytes[i + 1..]
293 .iter()
294 .take_while(|byte| **byte == b'#')
295 .count();
296 if bytes.get(i + 1 + hashes) != Some(&b'"') {
297 i += 1;
298 continue;
299 }
300 let body = i + hashes + 2;
301 let close = format!("\"{}", "#".repeat(hashes));
302 let end = text[body..]
303 .find(&close)
304 .map_or(bytes.len(), |at| body + at);
305 let physical = text[i..end].matches('\n').count();
306 if physical >= 2 {
307 spans.push((line, physical + 1));
308 }
309 line += physical;
310 i = (end + close.len()).min(bytes.len());
311 }
312 b'"' => {
313 let mut j = i + 1;
314 while j < bytes.len() && bytes[j] != b'"' {
315 j += if bytes[j] == b'\\' { 2 } else { 1 };
316 }
317 let segment = &text[i + 1..j.min(bytes.len())];
318 let physical = segment.matches('\n').count();
319 let decoded = physical + segment.matches("\\n").count();
320 if physical >= 2 || (decoded >= 2 && !GLUE.contains(&segment)) {
327 spans.push((line, decoded + 1));
328 }
329 line += physical;
330 i = j + 1;
331 }
332 _ => i += 1,
333 }
334 }
335 spans
336 }
337
338 #[test]
339 fn the_artifact_list_is_stable_and_complete() {
340 let listed = artifacts();
341 let total: usize = PAYLOAD_ROOTS
342 .iter()
343 .map(|root| root_files(root).expect("a declared root resolves").len())
344 .sum();
345 assert_eq!(listed.len(), total);
346 assert!(
347 listed.iter().any(|(path, _)| path == "versions.toml"),
348 "the single-file root must appear as itself"
349 );
350 }
351}