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;
12use crate::proto::build_update::build_failure;
13
14tonic::include_proto!("snix.build.v1");
15
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
64fn is_clean_path<P: AsRef<Path>>(p: P) -> bool {
67 let p = p.as_ref();
68
69 let mut cleaned_p = PathBuf::new();
75 for component in p.components() {
76 match component {
77 std::path::Component::Prefix(_) => {}
78 std::path::Component::RootDir => {}
79 std::path::Component::CurDir => return false,
80 std::path::Component::ParentDir => return false,
81 std::path::Component::Normal(a) => {
82 if a.is_empty() {
83 return false;
84 }
85 }
86 }
87 cleaned_p.push(component);
88 }
89
90 if cleaned_p.as_os_str() != p.as_os_str() {
92 return false;
93 }
94
95 true
96}
97
98fn is_clean_relative_path<P: AsRef<Path>>(p: P) -> bool {
99 if p.as_ref().is_absolute() {
100 return false;
101 }
102
103 is_clean_path(p)
104}
105
106fn is_clean_absolute_path<P: AsRef<Path>>(p: P) -> bool {
107 if !p.as_ref().is_absolute() {
108 return false;
109 }
110
111 is_clean_path(p)
112}
113
114fn is_sorted<I>(data: I) -> bool
116where
117 I: Iterator,
118 I::Item: Ord + Clone,
119{
120 data.tuple_windows().all(|(a, b)| a <= b)
121}
122
123impl From<crate::buildservice::BuildRequest> for BuildRequest {
124 fn from(value: crate::buildservice::BuildRequest) -> Self {
125 let constraints = if value.constraints.is_empty() {
126 None
127 } else {
128 let mut constraints = build_request::BuildConstraints::default();
129 for constraint in value.constraints {
130 use crate::buildservice::BuildConstraints;
131 match constraint {
132 BuildConstraints::System(system) => constraints.system = system,
133 BuildConstraints::MinMemory(min_memory) => constraints.min_memory = min_memory,
134 BuildConstraints::AvailableReadOnlyPath(path) => {
135 constraints.available_ro_paths.push(path)
136 }
137 BuildConstraints::ProvideBinSh => constraints.provide_bin_sh = true,
138 BuildConstraints::NetworkAccess => constraints.network_access = true,
139 }
140 }
141 Some(constraints)
142 };
143 Self {
144 inputs: value
145 .inputs
146 .into_iter()
147 .map(|(name, node)| {
148 snix_castore::proto::Entry::from_name_and_node(name.into(), node)
149 })
150 .collect(),
151 command_args: value.command_args,
152 working_dir: value.working_dir,
153 scratch_paths: value.scratch_paths,
154 inputs_dir: value.inputs_dir,
155 outputs: value.outputs,
156 environment_vars: value.environment_vars.into_iter().map(Into::into).collect(),
157 constraints,
158 additional_files: value.additional_files.into_iter().map(Into::into).collect(),
159 refscan_needles: value.refscan_needles,
160 }
161 }
162}
163
164impl TryFrom<BuildRequest> for crate::buildservice::BuildRequest {
165 type Error = ValidateBuildRequestError;
166 fn try_from(value: BuildRequest) -> Result<Self, Self::Error> {
167 let mut last_name: bytes::Bytes = "".into();
170 let mut inputs: BTreeMap<PathComponent, Node> = BTreeMap::new();
171 for (i, node) in value.inputs.iter().enumerate() {
172 let (name, node) = node
173 .clone()
174 .try_into_name_and_node()
175 .map_err(|e| ValidateBuildRequestError::InvalidInputNode(i, e))?;
176
177 if name.as_ref() <= last_name.as_ref() {
178 return Err(ValidateBuildRequestError::InputNodesNotSorted);
179 } else {
180 inputs.insert(name.clone(), node);
181 last_name = name.into();
182 }
183 }
184
185 if !is_clean_relative_path(&value.working_dir) {
187 Err(ValidateBuildRequestError::InvalidWorkingDir)?;
188 }
189
190 for (i, p) in value.scratch_paths.iter().enumerate() {
192 if !is_clean_relative_path(p) {
193 Err(ValidateBuildRequestError::InvalidScratchPath(i))?
194 }
195 }
196 if !is_sorted(value.scratch_paths.iter().map(|e| e.as_bytes())) {
197 Err(ValidateBuildRequestError::ScratchPathsNotSorted)?;
198 }
199
200 if !is_clean_relative_path(&value.inputs_dir) {
202 Err(ValidateBuildRequestError::InvalidInputsDir)?;
203 }
204
205 for (i, p) in value.outputs.iter().enumerate() {
207 if !is_clean_relative_path(p) {
208 Err(ValidateBuildRequestError::InvalidOutputPath(i))?
209 }
210 }
211 if !is_sorted(value.outputs.iter().map(|e| e.as_bytes())) {
212 Err(ValidateBuildRequestError::OutputsNotSorted)?;
213 }
214
215 for (i, e) in value.environment_vars.iter().enumerate() {
217 if e.key.is_empty() || e.key.contains('=') || e.key.contains('\0') {
218 Err(ValidateBuildRequestError::InvalidEnvVar(i))?
219 }
220 if e.value.contains(&0) {
221 Err(ValidateBuildRequestError::InvalidEnvVar(i))?
222 }
223 }
224 if !is_sorted(value.environment_vars.iter().map(|e| e.key.as_bytes())) {
225 Err(ValidateBuildRequestError::EnvVarNotSorted)?;
226 }
227
228 let constraints = value
230 .constraints
231 .map_or(Ok(HashSet::new()), |constraints| {
232 constraints
233 .try_into()
234 .map_err(ValidateBuildRequestError::InvalidBuildConstraints)
235 })?;
236
237 for (i, additional_file) in value.additional_files.iter().enumerate() {
239 if !is_clean_relative_path(&additional_file.path) {
240 Err(ValidateBuildRequestError::InvalidAdditionalFilePath(i))?
241 }
242 }
243 if !is_sorted(value.additional_files.iter().map(|e| e.path.as_bytes())) {
244 Err(ValidateBuildRequestError::AdditionalFilesNotSorted)?;
245 }
246
247 Ok(Self {
248 inputs,
249 command_args: value.command_args,
250 working_dir: value.working_dir,
251 scratch_paths: value.scratch_paths,
252 inputs_dir: value.inputs_dir,
253 outputs: value.outputs,
254 environment_vars: value.environment_vars.into_iter().map(Into::into).collect(),
255 constraints,
256 additional_files: value.additional_files.into_iter().map(Into::into).collect(),
257 refscan_needles: value.refscan_needles,
258 })
259 }
260}
261
262#[derive(Debug, thiserror::Error)]
265pub enum ValidateBuildConstraintsError {
266 #[error("invalid system")]
267 InvalidSystem,
268
269 #[error("invalid available_ro_paths at position {0}")]
270 InvalidAvailableRoPaths(usize),
271
272 #[error("available_ro_paths not sorted")]
273 AvailableRoPathsNotSorted,
274}
275
276impl From<build_request::EnvVar> for crate::buildservice::EnvVar {
277 fn from(value: build_request::EnvVar) -> Self {
278 Self {
279 key: value.key,
280 value: value.value,
281 }
282 }
283}
284
285impl From<crate::buildservice::EnvVar> for build_request::EnvVar {
286 fn from(value: crate::buildservice::EnvVar) -> Self {
287 Self {
288 key: value.key,
289 value: value.value,
290 }
291 }
292}
293
294impl From<build_request::AdditionalFile> for crate::buildservice::AdditionalFile {
295 fn from(value: build_request::AdditionalFile) -> Self {
296 Self {
297 path: PathBuf::from(value.path),
298 contents: value.contents,
299 }
300 }
301}
302
303impl From<crate::buildservice::AdditionalFile> for build_request::AdditionalFile {
304 fn from(value: crate::buildservice::AdditionalFile) -> Self {
305 Self {
306 path: value
307 .path
308 .to_str()
309 .expect("Snix bug: expected a valid path")
310 .to_string(),
311 contents: value.contents,
312 }
313 }
314}
315
316impl TryFrom<build_request::BuildConstraints> for HashSet<crate::buildservice::BuildConstraints> {
317 type Error = ValidateBuildConstraintsError;
318 fn try_from(value: build_request::BuildConstraints) -> Result<Self, Self::Error> {
319 use crate::buildservice::BuildConstraints;
320
321 if value.system.is_empty() {
323 Err(ValidateBuildConstraintsError::InvalidSystem)?;
324 }
325
326 let mut build_constraints = HashSet::from([
327 BuildConstraints::System(value.system),
328 BuildConstraints::MinMemory(value.min_memory),
329 ]);
330
331 if !is_sorted(value.available_ro_paths.iter()) {
333 Err(ValidateBuildConstraintsError::AvailableRoPathsNotSorted)?;
334 }
335 for (i, p) in value.available_ro_paths.into_iter().enumerate() {
336 if !is_clean_absolute_path(&p) {
337 Err(ValidateBuildConstraintsError::InvalidAvailableRoPaths(i))?
338 } else {
339 build_constraints.insert(BuildConstraints::AvailableReadOnlyPath(p));
340 }
341 }
342
343 if value.network_access {
344 build_constraints.insert(BuildConstraints::NetworkAccess);
345 }
346 if value.provide_bin_sh {
347 build_constraints.insert(BuildConstraints::ProvideBinSh);
348 }
349
350 Ok(build_constraints)
351 }
352}
353
354impl From<buildservice::BuildUpdate> for BuildUpdate {
355 fn from(value: buildservice::BuildUpdate) -> Self {
356 Self {
357 kind: Some(match value {
358 buildservice::BuildUpdate::ProducedOutput {
359 node,
360 idx,
361 refscan_needles,
362 } => build_update::Kind::ProducedOutput(build_update::ProducedOutput {
363 entry: Some(snix_castore::proto::Entry::from_name_and_node(
364 "".into(),
365 node,
366 )),
367 idx,
368 needles: refscan_needles.into_iter().collect(),
369 }),
370 buildservice::BuildUpdate::ProducedStdout(items) => {
371 build_update::Kind::ProducedStdout(build_update::StdioChunk {
372 chunk: items.into(),
373 })
374 }
375 buildservice::BuildUpdate::ProducedStderr(items) => {
376 build_update::Kind::ProducedStderr(build_update::StdioChunk {
377 chunk: items.into(),
378 })
379 }
380 buildservice::BuildUpdate::BuildFailure(build_failure) => {
381 build_update::Kind::BuildFailure(build_update::BuildFailure {
382 kind: Some(match build_failure {
383 buildservice::BuildFailure::NonzeroExitCode => {
384 build_failure::Kind::NonzeroExitCode(
385 build_failure::NonzeroExitCode {},
386 )
387 }
388 buildservice::BuildFailure::MissingOutput { output } => {
389 build_failure::Kind::MissingOutput(build_failure::MissingOutput {
390 output,
391 })
392 }
393 buildservice::BuildFailure::Other { message } => {
394 build_failure::Kind::Other(build_failure::Other { message })
395 }
396 }),
397 })
398 }
399 }),
400 }
401 }
402}
403
404impl TryFrom<BuildUpdate> for buildservice::BuildUpdate {
405 type Error = ValidateBuildUpdateError;
406
407 fn try_from(value: BuildUpdate) -> Result<Self, Self::Error> {
408 let kind = value
409 .kind
410 .ok_or(ValidateBuildUpdateError::UnexpectedBuildUpdateKind)?;
411
412 Ok(match kind {
413 build_update::Kind::ProducedOutput(build_update::ProducedOutput {
414 entry: None,
415 ..
416 }) => {
417 return Err(ValidateBuildUpdateError::InvalidProducedOutputEntry);
418 }
419 build_update::Kind::ProducedOutput(build_update::ProducedOutput {
420 entry: Some(entry),
421 idx,
422 needles,
423 }) => {
424 let node = entry
425 .try_into_anonymous_node()
426 .or(Err(ValidateBuildUpdateError::InvalidProducedOutputEntry))?;
427 Self::ProducedOutput {
428 node,
429 idx,
430 refscan_needles: BTreeSet::from_iter(needles),
431 }
432 }
433 build_update::Kind::ProducedStdout(produced_stdout) => {
434 Self::ProducedStdout(produced_stdout.chunk.into())
435 }
436 build_update::Kind::ProducedStderr(produced_stderr) => {
437 Self::ProducedStderr(produced_stderr.chunk.into())
438 }
439 build_update::Kind::BuildFailure(build_failure) => {
440 let kind = build_failure
441 .kind
442 .ok_or(ValidateBuildUpdateError::UnexpectedBuildFailureKind)?;
443 Self::BuildFailure(match kind {
444 build_failure::Kind::NonzeroExitCode(_) => {
445 buildservice::BuildFailure::NonzeroExitCode
446 }
447 build_failure::Kind::MissingOutput(build_failure::MissingOutput { output }) => {
448 buildservice::BuildFailure::MissingOutput { output }
449 }
450 build_failure::Kind::Other(other) => buildservice::BuildFailure::Other {
451 message: other.message,
452 },
453 })
454 }
455 })
456 }
457}
458
459#[derive(thiserror::Error, Debug)]
460pub enum ValidateBuildUpdateError {
461 #[error("unexpected BuildUpdate kind")]
462 UnexpectedBuildUpdateKind,
463 #[error("unexpected BuildFailure kind")]
464 UnexpectedBuildFailureKind,
465 #[error("invalid entry in ProducedOutput")]
466 InvalidProducedOutputEntry,
467}
468
469impl From<ValidateBuildUpdateError> for crate::buildservice::BuildFailure {
470 fn from(value: ValidateBuildUpdateError) -> Self {
471 Self::Other {
472 message: format!("failed to validate BuildUpdate: {value}"),
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
484 use super::{is_clean_path, is_clean_relative_path};
485 use rstest::rstest;
486
487 #[rstest]
488 #[case::fail_trailing_slash("foo/bar/", false)]
489 #[case::fail_dotdot("foo/../bar", false)]
490 #[case::fail_singledot("foo/./bar", false)]
491 #[case::fail_unnecessary_slashes("foo//bar", false)]
492 #[case::fail_absolute_unnecessary_slashes("//foo/bar", false)]
493 #[case::ok_empty("", true)]
494 #[case::ok_relative("foo/bar", true)]
495 #[case::ok_absolute("/", true)]
496 #[case::ok_absolute2("/foo/bar", true)]
497 fn test_is_clean_path(#[case] s: &str, #[case] expected: bool) {
498 assert_eq!(is_clean_path(s), expected);
499 }
500
501 #[rstest]
502 #[case::fail_absolute("/", false)]
503 #[case::ok_relative("foo/bar", true)]
504 fn test_is_clean_relative_path(#[case] s: &str, #[case] expected: bool) {
505 assert_eq!(is_clean_relative_path(s), expected);
506 }
507
508 }