1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::path::{Path, PathBuf};
3
4use itertools::Itertools;
5use snix_castore::{DirectoryError, Node, PathComponent};
6
7mod grpc_buildservice_wrapper;
8
9pub use grpc_buildservice_wrapper::GRPCBuildServiceWrapper;
10
11use crate::buildservice::BuildResult;
12
13tonic::include_proto!("snix.build.v1");
14
15pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("snix.build.v1");
19
20#[derive(Debug, thiserror::Error)]
22pub enum ValidateBuildRequestError {
23 #[error("invalid input node at position {0}: {1}")]
24 InvalidInputNode(usize, DirectoryError),
25
26 #[error("input nodes are not sorted by name")]
27 InputNodesNotSorted,
28
29 #[error("invalid working_dir")]
30 InvalidWorkingDir,
31
32 #[error("scratch_paths not sorted")]
33 ScratchPathsNotSorted,
34
35 #[error("invalid scratch path at position {0}")]
36 InvalidScratchPath(usize),
37
38 #[error("invalid inputs_dir")]
39 InvalidInputsDir,
40
41 #[error("invalid output path at position {0}")]
42 InvalidOutputPath(usize),
43
44 #[error("outputs not sorted")]
45 OutputsNotSorted,
46
47 #[error("invalid environment variable at position {0}")]
48 InvalidEnvVar(usize),
49
50 #[error("EnvVar not sorted by their keys")]
51 EnvVarNotSorted,
52
53 #[error("invalid build constraints: {0}")]
54 InvalidBuildConstraints(ValidateBuildConstraintsError),
55
56 #[error("invalid additional file path at position: {0}")]
57 InvalidAdditionalFilePath(usize),
58
59 #[error("additional_files not sorted")]
60 AdditionalFilesNotSorted,
61}
62
63#[derive(Debug, thiserror::Error)]
65pub enum ValidateBuildResultError {
66 #[error("request field is unpopulated")]
67 MissingRequestField,
68 #[error("request is invalid")]
69 InvalidBuildRequest(ValidateBuildRequestError),
70 #[error("output entry {0} missing")]
71 MissingOutputEntry(usize),
72 #[error("output entry {0} invalid")]
73 InvalidOutputEntry(usize),
74}
75
76fn is_clean_path<P: AsRef<Path>>(p: P) -> bool {
79 let p = p.as_ref();
80
81 let mut cleaned_p = PathBuf::new();
87 for component in p.components() {
88 match component {
89 std::path::Component::Prefix(_) => {}
90 std::path::Component::RootDir => {}
91 std::path::Component::CurDir => return false,
92 std::path::Component::ParentDir => return false,
93 std::path::Component::Normal(a) => {
94 if a.is_empty() {
95 return false;
96 }
97 }
98 }
99 cleaned_p.push(component);
100 }
101
102 if cleaned_p.as_os_str() != p.as_os_str() {
104 return false;
105 }
106
107 true
108}
109
110fn is_clean_relative_path<P: AsRef<Path>>(p: P) -> bool {
111 if p.as_ref().is_absolute() {
112 return false;
113 }
114
115 is_clean_path(p)
116}
117
118fn is_clean_absolute_path<P: AsRef<Path>>(p: P) -> bool {
119 if !p.as_ref().is_absolute() {
120 return false;
121 }
122
123 is_clean_path(p)
124}
125
126fn is_sorted<I>(data: I) -> bool
128where
129 I: Iterator,
130 I::Item: Ord + Clone,
131{
132 data.tuple_windows().all(|(a, b)| a <= b)
133}
134
135fn path_to_string(path: &Path) -> String {
136 path.to_str()
137 .expect("Snix Bug: unable to convert Path to String")
138 .to_string()
139}
140
141impl From<crate::buildservice::BuildRequest> for BuildRequest {
142 fn from(value: crate::buildservice::BuildRequest) -> Self {
143 let constraints = if value.constraints.is_empty() {
144 None
145 } else {
146 let mut constraints = build_request::BuildConstraints::default();
147 for constraint in value.constraints {
148 use crate::buildservice::BuildConstraints;
149 match constraint {
150 BuildConstraints::System(system) => constraints.system = system,
151 BuildConstraints::MinMemory(min_memory) => constraints.min_memory = min_memory,
152 BuildConstraints::AvailableReadOnlyPath(path) => {
153 constraints.available_ro_paths.push(path_to_string(&path))
154 }
155 BuildConstraints::ProvideBinSh => constraints.provide_bin_sh = true,
156 BuildConstraints::NetworkAccess => constraints.network_access = true,
157 }
158 }
159 Some(constraints)
160 };
161 Self {
162 inputs: value
163 .inputs
164 .into_iter()
165 .map(|(name, node)| {
166 snix_castore::proto::Entry::from_name_and_node(name.into(), node)
167 })
168 .collect(),
169 command_args: value.command_args,
170 working_dir: path_to_string(&value.working_dir),
171 scratch_paths: value
172 .scratch_paths
173 .iter()
174 .map(|p| path_to_string(p))
175 .collect(),
176 inputs_dir: path_to_string(&value.inputs_dir),
177 outputs: value.outputs.iter().map(|p| path_to_string(p)).collect(),
178 environment_vars: value.environment_vars.into_iter().map(Into::into).collect(),
179 constraints,
180 additional_files: value.additional_files.into_iter().map(Into::into).collect(),
181 refscan_needles: value.refscan_needles,
182 }
183 }
184}
185
186impl TryFrom<BuildRequest> for crate::buildservice::BuildRequest {
187 type Error = ValidateBuildRequestError;
188 fn try_from(value: BuildRequest) -> Result<Self, Self::Error> {
189 let mut last_name: bytes::Bytes = "".into();
192 let mut inputs: BTreeMap<PathComponent, Node> = BTreeMap::new();
193 for (i, node) in value.inputs.iter().enumerate() {
194 let (name, node) = node
195 .clone()
196 .try_into_name_and_node()
197 .map_err(|e| ValidateBuildRequestError::InvalidInputNode(i, e))?;
198
199 if name.as_ref() <= last_name.as_ref() {
200 return Err(ValidateBuildRequestError::InputNodesNotSorted);
201 } else {
202 inputs.insert(name.clone(), node);
203 last_name = name.into();
204 }
205 }
206
207 if !is_clean_relative_path(&value.working_dir) {
209 Err(ValidateBuildRequestError::InvalidWorkingDir)?;
210 }
211
212 for (i, p) in value.scratch_paths.iter().enumerate() {
214 if !is_clean_relative_path(p) {
215 Err(ValidateBuildRequestError::InvalidScratchPath(i))?
216 }
217 }
218 if !is_sorted(value.scratch_paths.iter().map(|e| e.as_bytes())) {
219 Err(ValidateBuildRequestError::ScratchPathsNotSorted)?;
220 }
221
222 if !is_clean_relative_path(&value.inputs_dir) {
224 Err(ValidateBuildRequestError::InvalidInputsDir)?;
225 }
226
227 for (i, p) in value.outputs.iter().enumerate() {
229 if !is_clean_relative_path(p) {
230 Err(ValidateBuildRequestError::InvalidOutputPath(i))?
231 }
232 }
233 if !is_sorted(value.outputs.iter().map(|e| e.as_bytes())) {
234 Err(ValidateBuildRequestError::OutputsNotSorted)?;
235 }
236
237 for (i, e) in value.environment_vars.iter().enumerate() {
239 if e.key.is_empty() || e.key.contains('=') || e.key.contains('\0') {
240 Err(ValidateBuildRequestError::InvalidEnvVar(i))?
241 }
242 if e.value.contains(&0) {
243 Err(ValidateBuildRequestError::InvalidEnvVar(i))?
244 }
245 }
246 if !is_sorted(value.environment_vars.iter().map(|e| e.key.as_bytes())) {
247 Err(ValidateBuildRequestError::EnvVarNotSorted)?;
248 }
249
250 let constraints = value
252 .constraints
253 .map_or(Ok(HashSet::new()), |constraints| {
254 constraints
255 .try_into()
256 .map_err(ValidateBuildRequestError::InvalidBuildConstraints)
257 })?;
258
259 for (i, additional_file) in value.additional_files.iter().enumerate() {
261 if !is_clean_relative_path(&additional_file.path) {
262 Err(ValidateBuildRequestError::InvalidAdditionalFilePath(i))?
263 }
264 }
265 if !is_sorted(value.additional_files.iter().map(|e| e.path.as_bytes())) {
266 Err(ValidateBuildRequestError::AdditionalFilesNotSorted)?;
267 }
268
269 Ok(Self {
270 inputs,
271 command_args: value.command_args,
272 working_dir: PathBuf::from(value.working_dir),
273 scratch_paths: value.scratch_paths.iter().map(PathBuf::from).collect(),
274 inputs_dir: PathBuf::from(value.inputs_dir),
275 outputs: value.outputs.iter().map(PathBuf::from).collect(),
276 environment_vars: value.environment_vars.into_iter().map(Into::into).collect(),
277 constraints,
278 additional_files: value.additional_files.into_iter().map(Into::into).collect(),
279 refscan_needles: value.refscan_needles,
280 })
281 }
282}
283
284impl From<BuildResult> for BuildResponse {
285 fn from(value: BuildResult) -> Self {
286 Self {
287 outputs: value
288 .outputs
289 .into_iter()
290 .map(|output| build_response::Output {
291 output: Some(snix_castore::proto::Entry::from_name_and_node(
292 "".into(),
293 output.node,
294 )),
295 needles: output.output_needles.into_iter().collect(),
296 })
297 .collect(),
298 }
299 }
300}
301
302impl TryFrom<BuildResponse> for BuildResult {
303 type Error = ValidateBuildResultError;
304
305 fn try_from(value: BuildResponse) -> Result<Self, Self::Error> {
306 Ok(Self {
307 outputs: value
308 .outputs
309 .into_iter()
310 .enumerate()
311 .map(|(i, output)| {
312 let node = output
313 .output
314 .ok_or(ValidateBuildResultError::MissingOutputEntry(i))?
315 .try_into_anonymous_node()
316 .map_err(|_| ValidateBuildResultError::InvalidOutputEntry(i))?;
317
318 Ok::<_, ValidateBuildResultError>(crate::buildservice::BuildOutput {
319 node,
320 output_needles: BTreeSet::from_iter(output.needles),
321 })
322 })
323 .try_collect()?,
324 })
325 }
326}
327
328#[derive(Debug, thiserror::Error)]
331pub enum ValidateBuildConstraintsError {
332 #[error("invalid system")]
333 InvalidSystem,
334
335 #[error("invalid available_ro_paths at position {0}")]
336 InvalidAvailableRoPaths(usize),
337
338 #[error("available_ro_paths not sorted")]
339 AvailableRoPathsNotSorted,
340}
341
342impl From<build_request::EnvVar> for crate::buildservice::EnvVar {
343 fn from(value: build_request::EnvVar) -> Self {
344 Self {
345 key: value.key,
346 value: value.value,
347 }
348 }
349}
350
351impl From<crate::buildservice::EnvVar> for build_request::EnvVar {
352 fn from(value: crate::buildservice::EnvVar) -> Self {
353 Self {
354 key: value.key,
355 value: value.value,
356 }
357 }
358}
359
360impl From<build_request::AdditionalFile> for crate::buildservice::AdditionalFile {
361 fn from(value: build_request::AdditionalFile) -> Self {
362 Self {
363 path: PathBuf::from(value.path),
364 contents: value.contents,
365 }
366 }
367}
368
369impl From<crate::buildservice::AdditionalFile> for build_request::AdditionalFile {
370 fn from(value: crate::buildservice::AdditionalFile) -> Self {
371 Self {
372 path: value
373 .path
374 .to_str()
375 .expect("Snix bug: expected a valid path")
376 .to_string(),
377 contents: value.contents,
378 }
379 }
380}
381
382impl TryFrom<build_request::BuildConstraints> for HashSet<crate::buildservice::BuildConstraints> {
383 type Error = ValidateBuildConstraintsError;
384 fn try_from(value: build_request::BuildConstraints) -> Result<Self, Self::Error> {
385 use crate::buildservice::BuildConstraints;
386
387 if value.system.is_empty() {
389 Err(ValidateBuildConstraintsError::InvalidSystem)?;
390 }
391
392 let mut build_constraints = HashSet::from([
393 BuildConstraints::System(value.system),
394 BuildConstraints::MinMemory(value.min_memory),
395 ]);
396
397 for (i, p) in value.available_ro_paths.iter().enumerate() {
399 if !is_clean_absolute_path(p) {
400 Err(ValidateBuildConstraintsError::InvalidAvailableRoPaths(i))?
401 } else {
402 build_constraints.insert(BuildConstraints::AvailableReadOnlyPath(PathBuf::from(p)));
403 }
404 }
405 if !is_sorted(value.available_ro_paths.iter().map(|e| e.as_bytes())) {
406 Err(ValidateBuildConstraintsError::AvailableRoPathsNotSorted)?;
407 }
408
409 if value.network_access {
410 build_constraints.insert(BuildConstraints::NetworkAccess);
411 }
412 if value.provide_bin_sh {
413 build_constraints.insert(BuildConstraints::ProvideBinSh);
414 }
415
416 Ok(build_constraints)
417 }
418}
419
420#[cfg(test)]
421mod tests {
427 use super::{is_clean_path, is_clean_relative_path};
428 use rstest::rstest;
429
430 #[rstest]
431 #[case::fail_trailing_slash("foo/bar/", false)]
432 #[case::fail_dotdot("foo/../bar", false)]
433 #[case::fail_singledot("foo/./bar", false)]
434 #[case::fail_unnecessary_slashes("foo//bar", false)]
435 #[case::fail_absolute_unnecessary_slashes("//foo/bar", false)]
436 #[case::ok_empty("", true)]
437 #[case::ok_relative("foo/bar", true)]
438 #[case::ok_absolute("/", true)]
439 #[case::ok_absolute2("/foo/bar", true)]
440 fn test_is_clean_path(#[case] s: &str, #[case] expected: bool) {
441 assert_eq!(is_clean_path(s), expected);
442 }
443
444 #[rstest]
445 #[case::fail_absolute("/", false)]
446 #[case::ok_relative("foo/bar", true)]
447 fn test_is_clean_relative_path(#[case] s: &str, #[case] expected: bool) {
448 assert_eq!(is_clean_relative_path(s), expected);
449 }
450
451 }