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