1use std::ffi::CStr;
38use std::ffi::CString;
39use std::rc::Rc;
40use yash_env::Env;
41use yash_env::System;
42use yash_env::builtin::Builtin;
43use yash_env::builtin::Type::{Special, Substitutive};
44use yash_env::function::Function;
45use yash_env::path::PathBuf;
46use yash_env::variable::Expansion;
47use yash_env::variable::PATH;
48
49#[derive(Clone, Debug, Eq, PartialEq)]
62pub enum Target {
63 Builtin {
65 builtin: Builtin,
67
68 path: CString,
78 },
79
80 Function(Rc<Function>),
82
83 External {
85 path: CString,
96 },
97}
98
99impl From<Rc<Function>> for Target {
100 #[inline]
101 fn from(function: Rc<Function>) -> Target {
102 Target::Function(function)
103 }
104}
105
106impl From<Function> for Target {
107 #[inline]
108 fn from(function: Function) -> Target {
109 Target::Function(function.into())
110 }
111}
112
113pub trait ClassifyEnv {
119 #[must_use]
121 fn builtin(&self, name: &str) -> Option<Builtin>;
122
123 #[must_use]
125 fn function(&self, name: &str) -> Option<&Rc<Function>>;
126}
127
128pub trait PathEnv {
130 #[must_use]
136 fn path(&self) -> Expansion<'_>;
137
138 #[must_use]
140 fn is_executable_file(&self, path: &CStr) -> bool;
141 }
143
144impl PathEnv for Env {
145 fn path(&self) -> Expansion<'_> {
150 self.variables
151 .get(PATH)
152 .and_then(|var| {
153 assert_eq!(var.quirk, None, "PATH does not support quirks");
154 var.value.as_ref()
155 })
156 .into()
157 }
158
159 fn is_executable_file(&self, path: &CStr) -> bool {
160 self.system.is_executable_file(path)
161 }
162}
163
164impl ClassifyEnv for Env {
165 fn builtin(&self, name: &str) -> Option<Builtin> {
166 self.builtins.get(name).copied()
167 }
168
169 #[inline]
170 fn function(&self, name: &str) -> Option<&Rc<Function>> {
171 self.functions.get(name)
172 }
173}
174
175#[must_use]
186pub fn search<E: ClassifyEnv + PathEnv>(env: &mut E, name: &str) -> Option<Target> {
187 let mut target = classify(env, name);
188
189 'fill_path: {
190 let path = match &mut target {
191 Target::Builtin { builtin, path } if builtin.r#type == Substitutive => {
192 path
194 }
195
196 Target::External { path } => {
197 if name.contains('/') {
198 *path = CString::new(name).ok()?;
200 break 'fill_path;
201 } else {
202 path
204 }
205 }
206
207 Target::Builtin { .. } | Target::Function(_) => {
208 break 'fill_path;
210 }
211 };
212
213 if let Some(real_path) = search_path(env, name) {
214 *path = real_path;
215 } else {
216 return None;
217 }
218 }
219
220 Some(target)
221}
222
223#[must_use]
234pub fn classify<E: ClassifyEnv>(env: &E, name: &str) -> Target {
235 if name.contains('/') {
236 return Target::External {
237 path: CString::default(),
238 };
239 }
240
241 let builtin = env.builtin(name);
242 if let Some(builtin) = builtin {
243 if builtin.r#type == Special {
244 let path = CString::default();
245 return Target::Builtin { builtin, path };
246 }
247 }
248
249 if let Some(function) = env.function(name) {
250 return Rc::clone(function).into();
251 }
252
253 if let Some(builtin) = builtin {
254 let path = CString::default();
255 return Target::Builtin { builtin, path };
256 }
257
258 Target::External {
259 path: CString::default(),
260 }
261}
262
263#[must_use]
273pub fn search_path<E: PathEnv>(env: &mut E, name: &str) -> Option<CString> {
274 env.path()
275 .split()
276 .filter_map(|dir| {
277 let candidate = PathBuf::from_iter([dir, name])
278 .into_unix_string()
279 .into_vec();
280 CString::new(candidate).ok()
281 })
282 .find(|path| env.is_executable_file(path))
283}
284
285#[allow(clippy::field_reassign_with_default)]
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use assert_matches::assert_matches;
290 use std::collections::HashMap;
291 use std::collections::HashSet;
292 use yash_env::builtin::Type::{Elective, Extension, Mandatory};
293 use yash_env::function::FunctionSet;
294 use yash_env::variable::Value;
295 use yash_syntax::source::Location;
296 use yash_syntax::syntax::CompoundCommand;
297 use yash_syntax::syntax::FullCompoundCommand;
298
299 #[derive(Default)]
300 struct DummyEnv {
301 builtins: HashMap<&'static str, Builtin>,
302 functions: FunctionSet,
303 path: Expansion<'static>,
304 executables: HashSet<String>,
305 }
306
307 impl PathEnv for DummyEnv {
308 fn path(&self) -> Expansion<'_> {
309 self.path.as_ref()
310 }
311 fn is_executable_file(&self, path: &CStr) -> bool {
312 if let Ok(path) = path.to_str() {
313 self.executables.contains(path)
314 } else {
315 false
316 }
317 }
318 }
319
320 impl ClassifyEnv for DummyEnv {
321 fn builtin(&self, name: &str) -> Option<Builtin> {
322 self.builtins.get(name).copied()
323 }
324 fn function(&self, name: &str) -> Option<&Rc<Function>> {
325 self.functions.get(name)
326 }
327 }
328
329 fn full_compound_command(s: &str) -> FullCompoundCommand {
330 FullCompoundCommand {
331 command: CompoundCommand::Grouping(s.parse().unwrap()),
332 redirs: vec![],
333 }
334 }
335
336 #[test]
337 fn nothing_is_found_in_empty_env() {
338 let mut env = DummyEnv::default();
339 let target = search(&mut env, "foo");
340 assert!(target.is_none(), "target = {target:?}");
341 }
342
343 #[test]
344 fn nothing_is_found_with_name_unmatched() {
345 let mut env = DummyEnv::default();
346 env.builtins
347 .insert("foo", Builtin::new(Special, |_, _| unreachable!()));
348 let function = Function::new("foo", full_compound_command(""), Location::dummy(""));
349 env.functions.define(function).unwrap();
350
351 let target = search(&mut env, "bar");
352 assert!(target.is_none(), "target = {target:?}");
353 }
354
355 #[test]
356 fn classify_defaults_to_external() {
357 let env = DummyEnv::default();
360 let target = classify(&env, "foo");
361 assert_eq!(
362 target,
363 Target::External {
364 path: CString::default()
365 }
366 );
367 }
368
369 #[test]
370 fn special_builtin_is_found() {
371 let mut env = DummyEnv::default();
372 let builtin = Builtin::new(Special, |_, _| unreachable!());
373 env.builtins.insert("foo", builtin);
374
375 assert_matches!(
376 search(&mut env, "foo"),
377 Some(Target::Builtin { builtin: result, path }) => {
378 assert_eq!(result.r#type, builtin.r#type);
379 assert_eq!(*path, *c"");
380 }
381 );
382 assert_matches!(
383 classify(&env, "foo"),
384 Target::Builtin { builtin: result, path } => {
385 assert_eq!(result.r#type, builtin.r#type);
386 assert_eq!(*path, *c"");
387 }
388 );
389 }
390
391 #[test]
392 fn function_is_found_if_not_hidden_by_special_builtin() {
393 let mut env = DummyEnv::default();
394 let function = Rc::new(Function::new(
395 "foo",
396 full_compound_command("bar"),
397 Location::dummy("location"),
398 ));
399 env.functions.define(function.clone()).unwrap();
400
401 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
402 assert_eq!(result, function);
403 });
404 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
405 assert_eq!(result, function);
406 });
407 }
408
409 #[test]
410 fn special_builtin_takes_priority_over_function() {
411 let mut env = DummyEnv::default();
412 let builtin = Builtin::new(Special, |_, _| unreachable!());
413 env.builtins.insert("foo", builtin);
414 let function = Function::new(
415 "foo",
416 full_compound_command("bar"),
417 Location::dummy("location"),
418 );
419 env.functions.define(function).unwrap();
420
421 assert_matches!(
422 search(&mut env, "foo"),
423 Some(Target::Builtin { builtin: result, path }) => {
424 assert_eq!(result.r#type, builtin.r#type);
425 assert_eq!(*path, *c"");
426 }
427 );
428 assert_matches!(
429 classify(&env, "foo"),
430 Target::Builtin { builtin: result, path } => {
431 assert_eq!(result.r#type, builtin.r#type);
432 assert_eq!(*path, *c"");
433 }
434 );
435 }
436
437 #[test]
438 fn mandatory_builtin_is_found_if_not_hidden_by_function() {
439 let mut env = DummyEnv::default();
440 let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
441 env.builtins.insert("foo", builtin);
442
443 assert_matches!(
444 search(&mut env, "foo"),
445 Some(Target::Builtin { builtin: result, path }) => {
446 assert_eq!(result.r#type, builtin.r#type);
447 assert_eq!(*path, *c"");
448 }
449 );
450 assert_matches!(
451 classify(&env, "foo"),
452 Target::Builtin { builtin: result, path } => {
453 assert_eq!(result.r#type, builtin.r#type);
454 assert_eq!(*path, *c"");
455 }
456 );
457 }
458
459 #[test]
460 fn elective_builtin_is_found_if_not_hidden_by_function() {
461 let mut env = DummyEnv::default();
462 let builtin = Builtin::new(Elective, |_, _| unreachable!());
463 env.builtins.insert("foo", builtin);
464
465 assert_matches!(
466 search(&mut env, "foo"),
467 Some(Target::Builtin { builtin: result, path }) => {
468 assert_eq!(result.r#type, builtin.r#type);
469 assert_eq!(*path, *c"");
470 }
471 );
472 assert_matches!(
473 classify(&env, "foo"),
474 Target::Builtin { builtin: result, path } => {
475 assert_eq!(result.r#type, builtin.r#type);
476 assert_eq!(*path, *c"");
477 }
478 );
479 }
480
481 #[test]
482 fn extension_builtin_is_found_if_not_hidden_by_function_or_option() {
483 let mut env = DummyEnv::default();
484 let builtin = Builtin::new(Extension, |_, _| unreachable!());
485 env.builtins.insert("foo", builtin);
486
487 assert_matches!(
488 search(&mut env, "foo"),
489 Some(Target::Builtin { builtin: result, path }) => {
490 assert_eq!(result.r#type, builtin.r#type);
491 assert_eq!(*path, *c"");
492 }
493 );
494 assert_matches!(
495 classify(&env, "foo"),
496 Target::Builtin { builtin: result, path } => {
497 assert_eq!(result.r#type, builtin.r#type);
498 assert_eq!(*path, *c"");
499 }
500 );
501 }
502
503 #[test]
504 fn function_takes_priority_over_mandatory_builtin() {
505 let mut env = DummyEnv::default();
506 env.builtins
507 .insert("foo", Builtin::new(Mandatory, |_, _| unreachable!()));
508
509 let function = Rc::new(Function::new(
510 "foo",
511 full_compound_command("bar"),
512 Location::dummy("location"),
513 ));
514 env.functions.define(function.clone()).unwrap();
515
516 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
517 assert_eq!(result, function);
518 });
519 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
520 assert_eq!(result, function);
521 });
522 }
523
524 #[test]
525 fn function_takes_priority_over_elective_builtin() {
526 let mut env = DummyEnv::default();
527 env.builtins
528 .insert("foo", Builtin::new(Elective, |_, _| unreachable!()));
529
530 let function = Rc::new(Function::new(
531 "foo",
532 full_compound_command("bar"),
533 Location::dummy("location"),
534 ));
535 env.functions.define(function.clone()).unwrap();
536
537 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
538 assert_eq!(result, function);
539 });
540 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
541 assert_eq!(result, function);
542 });
543 }
544
545 #[test]
546 fn function_takes_priority_over_extension_builtin() {
547 let mut env = DummyEnv::default();
548 env.builtins
549 .insert("foo", Builtin::new(Extension, |_, _| unreachable!()));
550
551 let function = Rc::new(Function::new(
552 "foo",
553 full_compound_command("bar"),
554 Location::dummy("location"),
555 ));
556 env.functions.define(function.clone()).unwrap();
557
558 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
559 assert_eq!(result, function);
560 });
561 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
562 assert_eq!(result, function);
563 });
564 }
565
566 #[test]
567 fn substitutive_builtin_is_found_if_external_executable_exists() {
568 let mut env = DummyEnv::default();
569 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
570 env.builtins.insert("foo", builtin);
571 env.path = Expansion::from("/bin");
572 env.executables.insert("/bin/foo".to_string());
573
574 assert_matches!(
575 search(&mut env, "foo"),
576 Some(Target::Builtin { builtin: result, path }) => {
577 assert_eq!(result.r#type, builtin.r#type);
578 assert_eq!(*path, *c"/bin/foo");
579 }
580 );
581 assert_matches!(
582 classify(&env, "foo"),
583 Target::Builtin { builtin: result, path } => {
584 assert_eq!(result.r#type, builtin.r#type);
585 assert_eq!(*path, *c"");
586 }
587 );
588 }
589
590 #[test]
591 fn substitutive_builtin_is_not_found_without_external_executable() {
592 let mut env = DummyEnv::default();
593 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
594 env.builtins.insert("foo", builtin);
595
596 let target = search(&mut env, "foo");
597 assert!(target.is_none(), "target = {target:?}");
598 }
599
600 #[test]
601 fn substitutive_builtin_is_classified_even_without_external_executable() {
602 let mut env = DummyEnv::default();
603 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
604 env.builtins.insert("foo", builtin);
605
606 assert_matches!(
607 classify(&env, "foo"),
608 Target::Builtin { builtin: result, path } => {
609 assert_eq!(result.r#type, builtin.r#type);
610 assert_eq!(*path, *c"");
611 }
612 );
613 }
614
615 #[test]
616 fn function_takes_priority_over_substitutive_builtin() {
617 let mut env = DummyEnv::default();
618 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
619 env.builtins.insert("foo", builtin);
620 env.path = Expansion::from("/bin");
621 env.executables.insert("/bin/foo".to_string());
622
623 let function = Rc::new(Function::new(
624 "foo",
625 full_compound_command("bar"),
626 Location::dummy("location"),
627 ));
628 env.functions.define(function.clone()).unwrap();
629
630 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
631 assert_eq!(result, function);
632 });
633 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
634 assert_eq!(result, function);
635 });
636 }
637
638 #[test]
639 fn external_utility_is_found_if_external_executable_exists() {
640 let mut env = DummyEnv::default();
641 env.path = Expansion::from("/bin");
642 env.executables.insert("/bin/foo".to_string());
643
644 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
645 assert_eq!(*path, *c"/bin/foo");
646 });
647 assert_matches!(classify(&env, "foo"), Target::External { path } => {
648 assert_eq!(*path, *c"");
649 });
650 }
651
652 #[test]
653 fn returns_external_utility_if_name_contains_slash() {
654 let mut env = DummyEnv::default();
656 let builtin = Builtin::new(Special, |_, _| unreachable!());
659 env.builtins.insert("bar/baz", builtin);
660
661 assert_matches!(search(&mut env, "bar/baz"), Some(Target::External { path }) => {
662 assert_eq!(*path, *c"bar/baz");
663 });
664 assert_matches!(classify(&env, "bar/baz"), Target::External { path } => {
665 assert_eq!(*path, *c"");
666 });
667 }
668
669 #[test]
670 fn external_target_is_first_executable_found_in_path_scalar() {
671 let mut env = DummyEnv::default();
672 env.path = Expansion::from("/usr/local/bin:/usr/bin:/bin");
673 env.executables.insert("/usr/bin/foo".to_string());
674 env.executables.insert("/bin/foo".to_string());
675
676 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
677 assert_eq!(*path, *c"/usr/bin/foo");
678 });
679
680 env.executables.insert("/usr/local/bin/foo".to_string());
681
682 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
683 assert_eq!(*path, *c"/usr/local/bin/foo");
684 });
685 }
686
687 #[test]
688 fn external_target_is_first_executable_found_in_path_array() {
689 let mut env = DummyEnv::default();
690 env.path = Expansion::from(Value::array(["/usr/local/bin", "/usr/bin", "/bin"]));
691 env.executables.insert("/usr/bin/foo".to_string());
692 env.executables.insert("/bin/foo".to_string());
693
694 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
695 assert_eq!(*path, *c"/usr/bin/foo");
696 });
697
698 env.executables.insert("/usr/local/bin/foo".to_string());
699
700 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
701 assert_eq!(*path, *c"/usr/local/bin/foo");
702 });
703 }
704
705 #[test]
706 fn empty_string_in_path_names_current_directory() {
707 let mut env = DummyEnv::default();
708 env.path = Expansion::from("/x::/y");
709 env.executables.insert("foo".to_string());
710
711 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
712 assert_eq!(*path, *c"foo");
713 });
714 }
715}