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::{Elective, Extension, Mandatory, 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: Option<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
144pub trait SearchEnv: PathEnv {
149 #[must_use]
151 fn builtin(&self, name: &str) -> Option<Builtin>;
152
153 #[must_use]
155 fn function(&self, name: &str) -> Option<&Rc<Function>>;
156}
157
158impl<E: SearchEnv> ClassifyEnv for E {
159 #[inline]
160 fn builtin(&self, name: &str) -> Option<Builtin> {
161 SearchEnv::builtin(self, name)
162 }
163
164 #[inline]
165 fn function(&self, name: &str) -> Option<&Rc<Function>> {
166 SearchEnv::function(self, name)
167 }
168}
169
170impl PathEnv for Env {
171 fn path(&self) -> Expansion<'_> {
176 self.variables
177 .get(PATH)
178 .and_then(|var| {
179 assert_eq!(var.quirk, None, "PATH does not support quirks");
180 var.value.as_ref()
181 })
182 .into()
183 }
184
185 fn is_executable_file(&self, path: &CStr) -> bool {
186 self.system.is_executable_file(path)
187 }
188}
189
190impl SearchEnv for Env {
191 fn builtin(&self, name: &str) -> Option<Builtin> {
192 self.builtins.get(name).copied()
193 }
194
195 #[inline]
196 fn function(&self, name: &str) -> Option<&Rc<Function>> {
197 self.functions.get(name)
198 }
199}
200
201pub fn search<E: SearchEnv>(env: &mut E, name: &str) -> Option<Target> {
215 let mut target = classify(env, name);
216
217 match &mut target {
218 Target::Builtin {
219 builtin,
220 path: Some(path),
221 } => {
222 assert_eq!(builtin.r#type, Substitutive);
223 if let Some(real_path) = search_path(env, name) {
225 *path = real_path;
226 } else {
227 return None;
228 }
229 }
230
231 Target::External { path } => {
232 let real_path = if name.contains('/') {
233 CString::new(name).ok()
235 } else {
236 search_path(env, name)
238 };
239 if let Some(real_path) = real_path {
240 *path = real_path;
241 } else {
242 return None;
243 }
244 }
245 Target::Builtin { .. } | Target::Function(_) => {
247 }
249 }
250
251 Some(target)
252}
253
254pub fn classify<E: ClassifyEnv>(env: &E, name: &str) -> Target {
265 if name.contains('/') {
266 return Target::External {
267 path: CString::default(),
268 };
269 }
270
271 let builtin = env.builtin(name);
272 if let Some(builtin) = builtin {
273 if builtin.r#type == Special {
274 let path = None;
275 return Target::Builtin { builtin, path };
276 }
277 }
278
279 if let Some(function) = env.function(name) {
280 return Rc::clone(function).into();
281 }
282
283 if let Some(builtin) = builtin {
284 let path = match builtin.r#type {
285 Special => unreachable!(),
286 Mandatory | Elective | Extension => None,
287 Substitutive => Some(CString::default()),
288 };
289 return Target::Builtin { builtin, path };
290 }
291
292 Target::External {
293 path: CString::default(),
294 }
295}
296
297pub fn search_path<E: PathEnv>(env: &mut E, name: &str) -> Option<CString> {
307 env.path()
308 .split()
309 .filter_map(|dir| {
310 let candidate = PathBuf::from_iter([dir, name])
311 .into_unix_string()
312 .into_vec();
313 CString::new(candidate).ok()
314 })
315 .find(|path| env.is_executable_file(path))
316}
317
318#[allow(clippy::field_reassign_with_default)]
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use assert_matches::assert_matches;
323 use std::collections::HashMap;
324 use std::collections::HashSet;
325 use yash_env::function::FunctionSet;
326 use yash_env::variable::Value;
327 use yash_syntax::source::Location;
328 use yash_syntax::syntax::CompoundCommand;
329 use yash_syntax::syntax::FullCompoundCommand;
330
331 #[derive(Default)]
332 struct DummyEnv {
333 builtins: HashMap<&'static str, Builtin>,
334 functions: FunctionSet,
335 path: Expansion<'static>,
336 executables: HashSet<String>,
337 }
338
339 impl PathEnv for DummyEnv {
340 fn path(&self) -> Expansion<'_> {
341 self.path.as_ref()
342 }
343 fn is_executable_file(&self, path: &CStr) -> bool {
344 if let Ok(path) = path.to_str() {
345 self.executables.contains(path)
346 } else {
347 false
348 }
349 }
350 }
351
352 impl SearchEnv for DummyEnv {
353 fn builtin(&self, name: &str) -> Option<Builtin> {
354 self.builtins.get(name).copied()
355 }
356 fn function(&self, name: &str) -> Option<&Rc<Function>> {
357 self.functions.get(name)
358 }
359 }
360
361 fn full_compound_command(s: &str) -> FullCompoundCommand {
362 FullCompoundCommand {
363 command: CompoundCommand::Grouping(s.parse().unwrap()),
364 redirs: vec![],
365 }
366 }
367
368 #[test]
369 fn nothing_is_found_in_empty_env() {
370 let mut env = DummyEnv::default();
371 let target = search(&mut env, "foo");
372 assert!(target.is_none(), "target = {target:?}");
373 }
374
375 #[test]
376 fn nothing_is_found_with_name_unmatched() {
377 let mut env = DummyEnv::default();
378 env.builtins
379 .insert("foo", Builtin::new(Special, |_, _| unreachable!()));
380 let function = Function::new("foo", full_compound_command(""), Location::dummy(""));
381 env.functions.define(function).unwrap();
382
383 let target = search(&mut env, "bar");
384 assert!(target.is_none(), "target = {target:?}");
385 }
386
387 #[test]
388 fn classify_defaults_to_external() {
389 let env = DummyEnv::default();
392 let target = classify(&env, "foo");
393 assert_eq!(
394 target,
395 Target::External {
396 path: CString::default()
397 }
398 );
399 }
400
401 #[test]
402 fn special_builtin_is_found() {
403 let mut env = DummyEnv::default();
404 let builtin = Builtin::new(Special, |_, _| unreachable!());
405 env.builtins.insert("foo", builtin);
406
407 assert_matches!(
408 search(&mut env, "foo"),
409 Some(Target::Builtin { builtin: result, path: None }) => {
410 assert_eq!(result.r#type, builtin.r#type);
411 }
412 );
413 assert_matches!(
414 classify(&env, "foo"),
415 Target::Builtin { builtin: result, path: None } => {
416 assert_eq!(result.r#type, builtin.r#type);
417 }
418 );
419 }
420
421 #[test]
422 fn function_is_found_if_not_hidden_by_special_builtin() {
423 let mut env = DummyEnv::default();
424 let function = Rc::new(Function::new(
425 "foo",
426 full_compound_command("bar"),
427 Location::dummy("location"),
428 ));
429 env.functions.define(function.clone()).unwrap();
430
431 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
432 assert_eq!(result, function);
433 });
434 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
435 assert_eq!(result, function);
436 });
437 }
438
439 #[test]
440 fn special_builtin_takes_priority_over_function() {
441 let mut env = DummyEnv::default();
442 let builtin = Builtin::new(Special, |_, _| unreachable!());
443 env.builtins.insert("foo", builtin);
444 let function = Function::new(
445 "foo",
446 full_compound_command("bar"),
447 Location::dummy("location"),
448 );
449 env.functions.define(function).unwrap();
450
451 assert_matches!(
452 search(&mut env, "foo"),
453 Some(Target::Builtin { builtin: result, path: None }) => {
454 assert_eq!(result.r#type, builtin.r#type);
455 }
456 );
457 assert_matches!(
458 classify(&env, "foo"),
459 Target::Builtin { builtin: result, path: None } => {
460 assert_eq!(result.r#type, builtin.r#type);
461 }
462 );
463 }
464
465 #[test]
466 fn mandatory_builtin_is_found_if_not_hidden_by_function() {
467 let mut env = DummyEnv::default();
468 let builtin = Builtin::new(Mandatory, |_, _| unreachable!());
469 env.builtins.insert("foo", builtin);
470
471 assert_matches!(
472 search(&mut env, "foo"),
473 Some(Target::Builtin { builtin: result, path: None }) => {
474 assert_eq!(result.r#type, builtin.r#type);
475 }
476 );
477 assert_matches!(
478 classify(&env, "foo"),
479 Target::Builtin { builtin: result, path: None } => {
480 assert_eq!(result.r#type, builtin.r#type);
481 }
482 );
483 }
484
485 #[test]
486 fn elective_builtin_is_found_if_not_hidden_by_function() {
487 let mut env = DummyEnv::default();
488 let builtin = Builtin::new(Elective, |_, _| unreachable!());
489 env.builtins.insert("foo", builtin);
490
491 assert_matches!(
492 search(&mut env, "foo"),
493 Some(Target::Builtin { builtin: result, path: None }) => {
494 assert_eq!(result.r#type, builtin.r#type);
495 }
496 );
497 assert_matches!(
498 classify(&env, "foo"),
499 Target::Builtin { builtin: result, path: None } => {
500 assert_eq!(result.r#type, builtin.r#type);
501 }
502 );
503 }
504
505 #[test]
506 fn extension_builtin_is_found_if_not_hidden_by_function_or_option() {
507 let mut env = DummyEnv::default();
508 let builtin = Builtin::new(Extension, |_, _| unreachable!());
509 env.builtins.insert("foo", builtin);
510
511 assert_matches!(
512 search(&mut env, "foo"),
513 Some(Target::Builtin { builtin: result, path: None }) => {
514 assert_eq!(result.r#type, builtin.r#type);
515 }
516 );
517 assert_matches!(
518 classify(&env, "foo"),
519 Target::Builtin { builtin: result, path: None } => {
520 assert_eq!(result.r#type, builtin.r#type);
521 }
522 );
523 }
524
525 #[test]
526 fn function_takes_priority_over_mandatory_builtin() {
527 let mut env = DummyEnv::default();
528 env.builtins
529 .insert("foo", Builtin::new(Mandatory, |_, _| unreachable!()));
530
531 let function = Rc::new(Function::new(
532 "foo",
533 full_compound_command("bar"),
534 Location::dummy("location"),
535 ));
536 env.functions.define(function.clone()).unwrap();
537
538 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
539 assert_eq!(result, function);
540 });
541 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
542 assert_eq!(result, function);
543 });
544 }
545
546 #[test]
547 fn function_takes_priority_over_elective_builtin() {
548 let mut env = DummyEnv::default();
549 env.builtins
550 .insert("foo", Builtin::new(Elective, |_, _| unreachable!()));
551
552 let function = Rc::new(Function::new(
553 "foo",
554 full_compound_command("bar"),
555 Location::dummy("location"),
556 ));
557 env.functions.define(function.clone()).unwrap();
558
559 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
560 assert_eq!(result, function);
561 });
562 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
563 assert_eq!(result, function);
564 });
565 }
566
567 #[test]
568 fn function_takes_priority_over_extension_builtin() {
569 let mut env = DummyEnv::default();
570 env.builtins
571 .insert("foo", Builtin::new(Extension, |_, _| unreachable!()));
572
573 let function = Rc::new(Function::new(
574 "foo",
575 full_compound_command("bar"),
576 Location::dummy("location"),
577 ));
578 env.functions.define(function.clone()).unwrap();
579
580 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
581 assert_eq!(result, function);
582 });
583 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
584 assert_eq!(result, function);
585 });
586 }
587
588 #[test]
589 fn substitutive_builtin_is_found_if_external_executable_exists() {
590 let mut env = DummyEnv::default();
591 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
592 env.builtins.insert("foo", builtin);
593 env.path = Expansion::from("/bin");
594 env.executables.insert("/bin/foo".to_string());
595
596 assert_matches!(
597 search(&mut env, "foo"),
598 Some(Target::Builtin { builtin: result, path: Some(path) }) => {
599 assert_eq!(result.r#type, builtin.r#type);
600 assert_eq!(path.to_bytes(), b"/bin/foo");
601 }
602 );
603 assert_matches!(
604 classify(&env, "foo"),
605 Target::Builtin { builtin: result, path: Some(path) } => {
606 assert_eq!(result.r#type, builtin.r#type);
607 assert_eq!(path.to_bytes(), b"");
608 }
609 );
610 }
611
612 #[test]
613 fn substitutive_builtin_is_not_found_without_external_executable() {
614 let mut env = DummyEnv::default();
615 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
616 env.builtins.insert("foo", builtin);
617
618 let target = search(&mut env, "foo");
619 assert!(target.is_none(), "target = {target:?}");
620 }
621
622 #[test]
623 fn substitutive_builtin_is_classified_even_without_external_executable() {
624 let mut env = DummyEnv::default();
625 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
626 env.builtins.insert("foo", builtin);
627
628 assert_matches!(
629 classify(&env, "foo"),
630 Target::Builtin { builtin: result, path: Some(path) } => {
631 assert_eq!(result.r#type, builtin.r#type);
632 assert_eq!(path.to_bytes(), b"");
633 }
634 );
635 }
636
637 #[test]
638 fn function_takes_priority_over_substitutive_builtin() {
639 let mut env = DummyEnv::default();
640 let builtin = Builtin::new(Substitutive, |_, _| unreachable!());
641 env.builtins.insert("foo", builtin);
642 env.path = Expansion::from("/bin");
643 env.executables.insert("/bin/foo".to_string());
644
645 let function = Rc::new(Function::new(
646 "foo",
647 full_compound_command("bar"),
648 Location::dummy("location"),
649 ));
650 env.functions.define(function.clone()).unwrap();
651
652 assert_matches!(search(&mut env, "foo"), Some(Target::Function(result)) => {
653 assert_eq!(result, function);
654 });
655 assert_matches!(classify(&env, "foo"), Target::Function(result) => {
656 assert_eq!(result, function);
657 });
658 }
659
660 #[test]
661 fn external_utility_is_found_if_external_executable_exists() {
662 let mut env = DummyEnv::default();
663 env.path = Expansion::from("/bin");
664 env.executables.insert("/bin/foo".to_string());
665
666 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
667 assert_eq!(path.to_bytes(), "/bin/foo".as_bytes());
668 });
669 assert_matches!(classify(&env, "foo"), Target::External { path } => {
670 assert_eq!(path.to_bytes(), b"");
671 });
672 }
673
674 #[test]
675 fn returns_external_utility_if_name_contains_slash() {
676 let mut env = DummyEnv::default();
678 let builtin = Builtin::new(Special, |_, _| unreachable!());
681 env.builtins.insert("bar/baz", builtin);
682
683 assert_matches!(search(&mut env, "bar/baz"), Some(Target::External { path }) => {
684 assert_eq!(path.to_bytes(), "bar/baz".as_bytes());
685 });
686 assert_matches!(classify(&env, "bar/baz"), Target::External { path } => {
687 assert_eq!(path.to_bytes(), b"");
688 });
689 }
690
691 #[test]
692 fn external_target_is_first_executable_found_in_path_scalar() {
693 let mut env = DummyEnv::default();
694 env.path = Expansion::from("/usr/local/bin:/usr/bin:/bin");
695 env.executables.insert("/usr/bin/foo".to_string());
696 env.executables.insert("/bin/foo".to_string());
697
698 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
699 assert_eq!(path.to_bytes(), "/usr/bin/foo".as_bytes());
700 });
701
702 env.executables.insert("/usr/local/bin/foo".to_string());
703
704 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
705 assert_eq!(path.to_bytes(), "/usr/local/bin/foo".as_bytes());
706 });
707 }
708
709 #[test]
710 fn external_target_is_first_executable_found_in_path_array() {
711 let mut env = DummyEnv::default();
712 env.path = Expansion::from(Value::array(["/usr/local/bin", "/usr/bin", "/bin"]));
713 env.executables.insert("/usr/bin/foo".to_string());
714 env.executables.insert("/bin/foo".to_string());
715
716 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
717 assert_eq!(path.to_bytes(), "/usr/bin/foo".as_bytes());
718 });
719
720 env.executables.insert("/usr/local/bin/foo".to_string());
721
722 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
723 assert_eq!(path.to_bytes(), "/usr/local/bin/foo".as_bytes());
724 });
725 }
726
727 #[test]
728 fn empty_string_in_path_names_current_directory() {
729 let mut env = DummyEnv::default();
730 env.path = Expansion::from("/x::/y");
731 env.executables.insert("foo".to_string());
732
733 assert_matches!(search(&mut env, "foo"), Some(Target::External { path }) => {
734 assert_eq!(path.to_bytes(), "foo".as_bytes());
735 });
736 }
737}