1#![deny(missing_docs)]
30
31use mbx_cache_core::{CacheDigest, canonical_json};
32use serde::{Deserialize, Serialize};
33use std::collections::{BTreeMap, BTreeSet};
34use std::ffi::OsString;
35use std::path::{Component, Path, PathBuf};
36use thiserror::Error;
37
38mod dep_info;
39
40pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
41
42pub const ACTION_SCHEMA_VERSION: u8 = 1;
44pub const ADAPTER_VERSION: u8 = 2;
51
52impl BypassReason {
53 pub fn kind(&self) -> &'static str {
58 self.into()
59 }
60}
61
62const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
63 "codegen-units",
64 "control-flow-guard",
65 "debug-assertions",
66 "debuginfo",
67 "default-linker-libraries",
68 "embed-bitcode",
69 "extra-filename",
70 "force-frame-pointers",
71 "force-unwind-tables",
72 "instrument-coverage",
73 "link-dead-code",
74 "link-self-contained",
75 "lto",
76 "metadata",
77 "no-prepopulate-passes",
78 "opt-level",
79 "overflow-checks",
80 "panic",
81 "prefer-dynamic",
82 "relocation-model",
83 "rpath",
84 "save-temps",
85 "soft-float",
86 "split-debuginfo",
87 "split-dwarf-kind",
88 "strip",
89 "symbol-mangling-version",
90 "target-cpu",
91 "target-feature",
92 "tls-model",
93];
94
95const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
96const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
97const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
98
99const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
102 "wasm32-unknown-unknown",
103 "wasm32-wasip1",
104 "wasm32-wasip1-threads",
105 "wasm32-wasip2",
106 "wasm32v1-none",
107 "wasm64-unknown-unknown",
108];
109
110#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
111#[strum(serialize_all = "kebab-case")]
112#[non_exhaustive]
122pub enum BypassReason {
123 #[error("rustc argument {index} is not valid UTF-8")]
125 NonUtf8Argument {
126 index: usize,
128 },
129 #[error("could not model rustc response file: {0}")]
131 ResponseFile(String),
132 #[error("rustc flag is not modeled by the cache adapter: {0}")]
134 UnknownFlag(String),
135 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
137 UnknownCodegenOption(String),
138 #[error("rustc flag requires a value: {0}")]
140 MissingValue(String),
141 #[error("rustc invocation is a compiler query, not a compilation")]
143 CompilerQuery,
144 #[error("rustc invocation reads source from standard input")]
146 StandardInput,
147 #[error("rustc invocation has no source input")]
149 MissingInput,
150 #[error("rustc invocation has multiple source inputs")]
152 MultipleInputs,
153 #[error("incremental compilation cannot be combined with action caching")]
155 Incremental,
156 #[error("rustc crate type is not cacheable yet: {0}")]
158 UnsupportedCrateType(String),
159 #[error("rustc output type is not cacheable yet: {0}")]
161 UnsupportedEmit(String),
162 #[error("rustc invocation does not emit a cacheable artifact")]
164 NoCacheableOutput,
165 #[error("rustc invocation does not emit dependency information")]
167 NoDepInfo,
168 #[error("rustc output paths do not share one directory")]
170 SplitOutputDirectories,
171 #[error("rustc output path has no file name: {0}")]
173 InvalidOutputPath(PathBuf),
174 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
176 ImplicitEmitWithOutputFile(PathBuf),
177 #[error("native library lookup is not cacheable yet")]
179 NativeLibrary,
180 #[error("rustc output name does not distinguish a program from a library: {0}")]
182 AmbiguousOutputName(PathBuf),
183 #[error("native link is not reproducible across checkouts: {0}")]
185 UnportableNativeLink(String),
186 #[error("rustc search path kind is not cacheable yet: {0}")]
188 UnsupportedSearchPath(String),
189 #[error("rustc extern does not identify an input artifact: {0}")]
191 UnresolvedExtern(String),
192 #[error("absolute path has no stable cache mapping: {0}")]
194 UnmappedAbsolutePath(PathBuf),
195 #[error("cache key paths must be valid UTF-8: {0}")]
197 NonUtf8Path(PathBuf),
198 #[error("cache action working directory must be absolute: {0}")]
200 RelativeWorkingDirectory(PathBuf),
201 #[error("cache path mapping must use an absolute root: {0}")]
203 RelativePathMapping(PathBuf),
204 #[error("cache path mapping placeholder is invalid: {0}")]
206 InvalidPathPlaceholder(String),
207 #[error("required compiler input was not provided: {0}")]
209 MissingRequiredInput(String),
210 #[error("compiler input has an invalid digest: {0}")]
212 InvalidInputDigest(String),
213 #[error("compiler input appears more than once with different content: {0}")]
215 ConflictingInput(String),
216 #[error("rustc dep-info is malformed: {0}")]
218 MalformedDepInfo(String),
219 #[error("failed to read rustc dep-info {path}: {message}")]
221 DepInfoRead {
222 path: PathBuf,
224 message: String,
226 },
227 #[error("rustc dep-info output path must be absolute: {0}")]
229 RelativeDepInfoPath(PathBuf),
230 #[error("rustc dep-info output path cannot contain a comma: {0}")]
232 UnsafeDepInfoPath(PathBuf),
233 #[error("failed to read compiler input {path}: {message}")]
235 InputRead {
236 path: PathBuf,
238 message: String,
240 },
241 #[error("compiler input changed after discovery: {0}")]
243 InputChanged(PathBuf),
244 #[error("compiler input was modified during compilation: {0}")]
246 InputModifiedDuringCompilation(PathBuf),
247 #[error("discovered inputs were collected from a different working directory")]
249 DiscoveryWorkingDirectory,
250 #[error("compiler environment input has conflicting values: {0}")]
252 ConflictingEnvironment(String),
253 #[error("failed to serialize the rustc action: {0}")]
255 Serialization(String),
256 #[error("rustc action prediction is unsupported")]
258 UnsupportedPrediction,
259 #[error("rustc action prediction contains an invalid input path: {0}")]
261 InvalidPredictedInput(String),
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
265enum Argument {
266 Plain(String),
267 Path { flag: String, path: PathBuf },
268 SearchPath { kind: String, path: PathBuf },
269 Extern { name: String, path: Option<PathBuf> },
270 Emit(Vec<Emit>),
271 RemapPath { from: PathBuf, to: String },
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
275struct Emit {
276 kind: String,
277 path: Option<PathBuf>,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct RustcInvocation {
286 arguments: Vec<Argument>,
287 source: PathBuf,
288 required_inputs: Vec<PathBuf>,
289 crate_name: String,
290 extra_filename: String,
291 out_dir: Option<PathBuf>,
292 explicit_output: Option<PathBuf>,
293 emits: Vec<Emit>,
294 target: Option<String>,
295 link_output: LinkOutput,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299enum LinkOutput {
300 Library,
301 WasmExecutable,
302 NativeExecutable,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
311#[non_exhaustive]
312pub struct ParseOptions {
313 pub cache_native_links: bool,
316}
317
318impl ParseOptions {
319 pub fn caching_native_links(enabled: bool) -> Self {
321 Self {
322 cache_native_links: enabled,
323 }
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
329pub struct RustcOutputs {
330 pub directory: PathBuf,
332 pub files: Vec<PathBuf>,
334 pub dep_info: PathBuf,
336}
337
338impl RustcInvocation {
339 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
347 Self::parse_with(arguments, ParseOptions::default())
348 }
349
350 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
353 let expanded = expand_response_files(arguments)?;
354 Parser::new(&expanded.arguments, options).parse()
355 }
356
357 pub fn links_natively(&self) -> bool {
360 self.link_output == LinkOutput::NativeExecutable
361 }
362
363 fn native_search_is_inert(&self) -> bool {
376 self.link_output == LinkOutput::Library
377 }
378
379 pub fn source(&self) -> &Path {
381 &self.source
382 }
383
384 pub fn target(&self) -> Option<&str> {
386 self.target.as_deref()
387 }
388
389 pub fn crate_name(&self) -> &str {
391 &self.crate_name
392 }
393
394 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
407 let linked = self
408 .arguments
409 .iter()
410 .filter_map(|argument| match argument {
411 Argument::Extern {
412 path: Some(path), ..
413 } => Some(path.as_path()),
414 _ => None,
415 })
416 .collect::<BTreeSet<_>>();
417 let owned = discovered
418 .inputs
419 .iter()
420 .filter(|input| !linked.contains(input.path.as_path()))
421 .map(|input| (input.path.as_path(), &input.digest))
422 .collect::<BTreeMap<_, _>>();
423 let mut bytes = Vec::new();
424 for (path, digest) in owned {
425 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
426 bytes.push(0);
427 bytes.extend_from_slice(digest.key().as_bytes());
428 bytes.push(0);
429 }
430 CacheDigest::blake3(&bytes)
431 }
432
433 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
438 if !working_dir.is_absolute() {
439 return Err(BypassReason::RelativeWorkingDirectory(
440 working_dir.to_path_buf(),
441 ));
442 }
443 let explicit_output = self
444 .explicit_output
445 .as_deref()
446 .map(|path| absolute_path(path, working_dir));
447 let output_directory = explicit_output
448 .as_deref()
449 .and_then(Path::parent)
450 .map(Path::to_path_buf)
451 .or_else(|| {
452 self.out_dir
453 .as_deref()
454 .map(|path| absolute_path(path, working_dir))
455 })
456 .unwrap_or_else(|| normalize_components(working_dir));
457 if let Some(output) = &explicit_output
461 && self.emits.iter().any(|emit| {
462 emit.path.is_none()
463 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
464 })
465 {
466 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
467 }
468 let mut files = BTreeSet::new();
469 let mut dep_info = None;
470 for emit in &self.emits {
471 if emit.kind == "dep-info" {
472 let path = emit.path.as_ref().map_or_else(
473 || {
474 explicit_output.clone().map_or_else(
475 || {
476 output_directory
477 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
478 },
479 |path| path.with_extension("d"),
480 )
481 },
482 |path| absolute_path(path, working_dir),
483 );
484 if path.file_name().is_none() {
485 return Err(BypassReason::InvalidOutputPath(path));
486 }
487 dep_info = Some(path);
488 continue;
489 }
490 let (prefix, extension) = match emit.kind.as_str() {
491 "link" => match self.link_output {
492 LinkOutput::Library => ("lib", "rlib"),
493 LinkOutput::WasmExecutable => ("", "wasm"),
494 LinkOutput::NativeExecutable => ("", ""),
497 },
498 "metadata" => ("lib", "rmeta"),
499 _ => continue,
500 };
501 let path = if let Some(path) = &emit.path {
502 absolute_path(path, working_dir)
503 } else {
504 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
505 output_directory.join(if extension.is_empty() {
506 name
507 } else {
508 format!("{name}.{extension}")
509 })
510 };
511 if path.file_name().is_none() {
512 return Err(BypassReason::InvalidOutputPath(path));
513 }
514 if path.parent() != Some(output_directory.as_path()) {
515 return Err(BypassReason::SplitOutputDirectories);
516 }
517 if emit.kind == "link"
522 && !matches!(self.link_output, LinkOutput::Library)
523 && matches!(
524 path.extension().and_then(|extension| extension.to_str()),
525 Some("rlib" | "rmeta")
526 )
527 {
528 return Err(BypassReason::AmbiguousOutputName(path));
529 }
530 files.insert(path);
531 }
532 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
533 if dep_info.parent() != Some(output_directory.as_path()) {
534 return Err(BypassReason::SplitOutputDirectories);
535 }
536 Ok(RustcOutputs {
537 directory: output_directory,
538 files: files.into_iter().collect(),
539 dep_info,
540 })
541 }
542
543 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
549 self.action_linked_by(context, None)
550 }
551
552 pub fn action_linked_by(
561 &self,
562 context: ActionContext,
563 linker: Option<LinkerIdentity>,
564 ) -> Result<RustcAction, BypassReason> {
565 ActionBuilder::new(self, context).linked_by(linker).build()
566 }
567
568 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
570 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
571 let bytes = canonical_json(&descriptor)
572 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
573 Ok(CacheDigest::blake3(&bytes))
574 }
575
576 pub fn prediction(
579 &self,
580 context: &ActionContext,
581 discovered: &DiscoveredInputs,
582 ) -> Result<RustcInputPrediction, BypassReason> {
583 let builder = ActionBuilder::new(self, context.clone());
584 builder.validate_mappings()?;
585 let mut inputs = discovered
586 .inputs
587 .iter()
588 .map(|input| builder.normalize_path(&input.path))
589 .collect::<Result<BTreeSet<_>, _>>()?;
590 let mut has_native_directory = false;
591 for argument in &self.arguments {
592 if let Argument::SearchPath { kind, path } = argument
593 && kind == "native"
594 {
595 match builder.normalize_path(path) {
596 Ok(normalized) => {
597 has_native_directory = true;
598 inputs.insert(format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{normalized}"));
599 }
600 Err(BypassReason::UnmappedAbsolutePath(_)) if self.native_search_is_inert() => {
606 }
607 Err(error) => return Err(error),
608 }
609 }
610 }
611 Ok(RustcInputPrediction {
612 version: if has_native_directory { 3 } else { 1 },
613 inputs: inputs.into_iter().collect(),
614 environment: discovered.environment.keys().cloned().collect(),
615 compiler_duration_ns: 0,
616 crate_name: String::new(),
617 })
618 }
619}
620
621impl RustcOutputs {
622 pub fn is_executable(&self, path: &Path) -> bool {
630 self.files.iter().any(|output| output == path)
631 && !matches!(
632 path.extension().and_then(|extension| extension.to_str()),
633 Some("rlib" | "rmeta")
634 )
635 }
636}
637
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct PathMapping {
641 pub root: PathBuf,
643 pub placeholder: String,
645}
646
647impl PathMapping {
648 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
650 Self {
651 root: root.into(),
652 placeholder: placeholder.into(),
653 }
654 }
655
656 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
659 let mut ordered = mappings.to_vec();
660 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
661 ordered
662 }
663}
664
665pub fn normalize_mapped_path(
672 path: &Path,
673 working_dir: &Path,
674 mappings: &[PathMapping],
675) -> Result<String, BypassReason> {
676 let mappings = mappings
677 .iter()
678 .map(|mapping| PathMapping {
679 root: resolve_mapping_root(&mapping.root),
680 placeholder: mapping.placeholder.clone(),
681 })
682 .collect::<Vec<_>>();
683 normalize_resolved_mapped_path(path, working_dir, &mappings)
684}
685
686fn normalize_resolved_mapped_path(
687 path: &Path,
688 working_dir: &Path,
689 mappings: &[PathMapping],
690) -> Result<String, BypassReason> {
691 let absolute = if path.is_absolute() {
692 normalize_components(path)
693 } else {
694 normalize_components(&working_dir.join(path))
695 };
696 let resolved = if absolute.is_absolute() {
697 resolve_path_aliases(&absolute)
698 } else {
699 absolute.clone()
700 };
701 for mapping in mappings {
702 if let Ok(relative) = resolved.strip_prefix(&mapping.root) {
703 let suffix = slash_path(relative)?;
704 return Ok(if suffix.is_empty() {
705 format!("${{{}}}", mapping.placeholder)
706 } else {
707 format!("${{{}}}/{suffix}", mapping.placeholder)
708 });
709 }
710 }
711 Err(BypassReason::UnmappedAbsolutePath(absolute))
712}
713
714#[cfg(unix)]
719fn resolve_path_aliases(path: &Path) -> PathBuf {
720 let mut existing = path;
721 let mut missing = Vec::new();
722 loop {
723 match std::fs::canonicalize(existing) {
724 Ok(mut resolved) => {
725 for component in missing.iter().rev() {
726 resolved.push(component);
727 }
728 return normalize_components(&resolved);
729 }
730 Err(_) => {
731 let Some(name) = existing.file_name() else {
732 return path.to_path_buf();
733 };
734 missing.push(name.to_os_string());
735 let Some(parent) = existing.parent() else {
736 return path.to_path_buf();
737 };
738 existing = parent;
739 }
740 }
741 }
742}
743
744#[cfg(not(unix))]
745fn resolve_path_aliases(path: &Path) -> PathBuf {
746 path.to_path_buf()
747}
748
749fn resolve_mapping_root(root: &Path) -> PathBuf {
750 let root = normalize_components(root);
751 if root.is_absolute() {
752 resolve_path_aliases(&root)
753 } else {
754 root
755 }
756}
757
758#[derive(Debug, Clone, PartialEq, Eq)]
759pub struct CompilerIdentity {
761 pub toolchain: String,
763 pub rustc_version: String,
765 pub host: String,
767}
768
769#[derive(Debug, Clone, PartialEq, Eq)]
771pub struct ActionInput {
772 pub path: PathBuf,
774 pub digest: CacheDigest,
776}
777
778#[derive(Debug, Clone, PartialEq, Eq)]
780pub struct ActionContext {
781 pub compiler: CompilerIdentity,
783 pub working_dir: PathBuf,
785 pub path_mappings: Vec<PathMapping>,
787 pub environment: BTreeMap<String, Option<String>>,
789 pub portable_environment: BTreeSet<String>,
796 pub inputs: Vec<ActionInput>,
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
810#[serde(deny_unknown_fields)]
811pub struct LinkerIdentity {
812 pub driver: String,
814 pub driver_version: String,
816 pub linker_version: String,
818 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
820 pub crt_objects: BTreeMap<String, CacheDigest>,
821 #[serde(skip_serializing_if = "Option::is_none", default)]
823 pub sdk: Option<String>,
824 #[serde(skip_serializing_if = "Option::is_none", default)]
826 pub deployment_target: Option<String>,
827}
828
829#[derive(Debug, Clone, PartialEq, Eq)]
831pub struct RustcAction {
832 pub digest: CacheDigest,
834 pub bytes: Vec<u8>,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
841#[serde(deny_unknown_fields)]
842pub struct RustcInputPrediction {
843 pub version: u8,
845 pub inputs: Vec<String>,
847 pub environment: Vec<String>,
849 #[serde(default, skip_serializing_if = "is_zero")]
852 pub compiler_duration_ns: u64,
853 #[serde(default, skip_serializing_if = "String::is_empty")]
855 pub crate_name: String,
856}
857
858fn is_zero(value: &u64) -> bool {
859 *value == 0
860}
861
862impl RustcInputPrediction {
863 pub fn discover(
866 &self,
867 working_dir: &Path,
868 path_mappings: &[PathMapping],
869 ) -> Result<DiscoveredInputs, BypassReason> {
870 if !matches!(self.version, 1..=3) {
871 return Err(BypassReason::UnsupportedPrediction);
872 }
873 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
874 return Err(BypassReason::UnsupportedPrediction);
875 }
876 let mut paths = BTreeSet::new();
877 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
878 let mut native_bytes = 0_u64;
879 for path in &self.inputs {
880 if self.version >= 3
881 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
882 {
883 let directory = denormalize_path(path, path_mappings)?;
884 dep_info::collect_native_directory(
885 &directory,
886 &admitted_roots,
887 &mut paths,
888 &mut native_bytes,
889 )?;
890 } else {
891 paths.insert(denormalize_path(path, path_mappings)?);
892 }
893 }
894 let environment = self
895 .environment
896 .iter()
897 .map(|name| {
898 if name.is_empty() || name.contains(['=', '\0']) {
899 return Err(BypassReason::UnsupportedPrediction);
900 }
901 let value = std::env::var_os(name)
902 .map(|value| {
903 value
904 .into_string()
905 .map_err(|_| BypassReason::UnsupportedPrediction)
906 })
907 .transpose()?;
908 Ok((name.clone(), value))
909 })
910 .collect::<Result<BTreeMap<_, _>, _>>()?;
911 DiscoveredInputs::from_paths(working_dir, paths, environment)
912 }
913}
914
915#[derive(Serialize)]
916struct ActionDescriptor {
917 version: u8,
918 kind: &'static str,
919 adapter_version: u8,
920 compiler: CompilerDescriptor,
921 arguments: Vec<String>,
922 environment: BTreeMap<String, Option<String>>,
923 inputs: Vec<InputDescriptor>,
924 #[serde(skip_serializing_if = "Option::is_none")]
927 linker: Option<LinkerIdentity>,
928}
929
930#[derive(Serialize)]
931struct InvocationDescriptor {
932 version: u8,
933 kind: &'static str,
934 adapter_version: u8,
935 compiler: CompilerDescriptor,
936 arguments: Vec<String>,
937 required_inputs: Vec<String>,
938}
939
940#[derive(Serialize)]
941struct CompilerDescriptor {
942 toolchain: String,
943 rustc_version: String,
944 host: String,
945}
946
947#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
948struct InputDescriptor {
949 path: String,
950 digest: CacheDigest,
951}
952
953struct Parser<'a> {
954 arguments: &'a [OsString],
955 index: usize,
956 parsed: Vec<Argument>,
957 source: Option<PathBuf>,
958 crate_types: Vec<String>,
959 emits: Vec<Emit>,
960 required_inputs: Vec<PathBuf>,
961 test: bool,
962 crate_name: Option<String>,
963 extra_filename: String,
964 out_dir: Option<PathBuf>,
965 explicit_output: Option<PathBuf>,
966 target: Option<String>,
967 options: ParseOptions,
968}
969
970struct ExpandedArguments {
971 arguments: Vec<OsString>,
972}
973
974#[derive(Default)]
975struct ResponseExpander {
976 shell_argfiles: bool,
977 next_is_unstable_option: bool,
978 arguments: Vec<OsString>,
979}
980
981impl ResponseExpander {
982 fn push(&mut self, argument: String) {
983 if self.next_is_unstable_option {
984 self.shell_argfiles |= argument == "shell-argfiles";
985 self.next_is_unstable_option = false;
986 } else if let Some(option) = argument.strip_prefix("-Z") {
987 if option.is_empty() {
988 self.next_is_unstable_option = true;
989 } else {
990 self.shell_argfiles |= option == "shell-argfiles";
991 }
992 }
993 self.arguments.push(argument.into());
994 }
995}
996
997fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1000 let mut expanded = ResponseExpander::default();
1001 for (index, argument) in arguments.iter().enumerate() {
1002 let argument = argument
1003 .to_str()
1004 .ok_or(BypassReason::NonUtf8Argument { index })?;
1005 let Some(argfile) = argument.strip_prefix('@') else {
1006 expanded.push(argument.to_string());
1007 continue;
1008 };
1009 let (path, shell) = match argfile.split_once(':') {
1010 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1011 _ => (argfile, false),
1012 };
1013 let contents = std::fs::read_to_string(path).map_err(|error| {
1014 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1015 })?;
1016 if shell {
1017 let arguments = shlex::split(&contents).ok_or_else(|| {
1018 BypassReason::ResponseFile(format!(
1019 "invalid shell-style arguments in {}",
1020 Path::new(path).display()
1021 ))
1022 })?;
1023 for argument in arguments {
1024 expanded.push(argument);
1025 }
1026 } else {
1027 for argument in contents.lines() {
1028 expanded.push(argument.to_string());
1029 }
1030 }
1031 }
1032 Ok(ExpandedArguments {
1033 arguments: expanded.arguments,
1034 })
1035}
1036
1037impl<'a> Parser<'a> {
1038 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1039 Self {
1040 arguments,
1041 options,
1042 index: 0,
1043 parsed: Vec::new(),
1044 source: None,
1045 crate_types: Vec::new(),
1046 emits: Vec::new(),
1047 required_inputs: Vec::new(),
1048 test: false,
1049 crate_name: None,
1050 extra_filename: String::new(),
1051 out_dir: None,
1052 explicit_output: None,
1053 target: None,
1054 }
1055 }
1056
1057 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1058 while self.index < self.arguments.len() {
1059 let value = self.current()?.to_string();
1060 self.index += 1;
1061 if let Some(long) = value.strip_prefix("--") {
1062 self.parse_long(long)?;
1063 } else if value.starts_with('-') && value != "-" {
1064 self.parse_short(&value)?;
1065 } else {
1066 self.parse_input(&value)?;
1067 }
1068 }
1069
1070 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1071 let link_output = self.classify()?;
1072 let crate_name = self.crate_name.clone().map_or_else(
1073 || {
1074 source
1075 .file_stem()
1076 .and_then(|name| name.to_str())
1077 .map(|name| name.replace('-', "_"))
1078 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1079 },
1080 Ok,
1081 )?;
1082 self.required_inputs.push(source.clone());
1083 Ok(RustcInvocation {
1084 arguments: self.parsed,
1085 source,
1086 required_inputs: self.required_inputs,
1087 crate_name,
1088 extra_filename: self.extra_filename,
1089 out_dir: self.out_dir,
1090 explicit_output: self.explicit_output,
1091 emits: self.emits,
1092 target: self.target,
1093 link_output,
1094 })
1095 }
1096
1097 fn current(&self) -> Result<&str, BypassReason> {
1098 self.arguments[self.index]
1099 .to_str()
1100 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1101 }
1102
1103 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1104 if let Some(value) = inline {
1105 if value.is_empty() {
1106 return Err(BypassReason::MissingValue(flag.into()));
1107 }
1108 return Ok(value.into());
1109 }
1110 if self.index >= self.arguments.len() {
1111 return Err(BypassReason::MissingValue(flag.into()));
1112 }
1113 let value = self.current()?.to_string();
1114 self.index += 1;
1115 Ok(value)
1116 }
1117
1118 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1119 let (flag, inline) = value
1120 .split_once('=')
1121 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1122 let rendered_flag = format!("--{flag}");
1123 match flag {
1124 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1125 "test" => {
1126 self.test = true;
1127 self.parsed.push(Argument::Plain(rendered_flag));
1128 Ok(())
1129 }
1130 "verbose" => {
1131 self.parsed.push(Argument::Plain(rendered_flag));
1132 Ok(())
1133 }
1134 "crate-name" => {
1135 let value = self.take_value(&rendered_flag, inline)?;
1136 self.crate_name = Some(value.clone());
1137 self.parsed
1138 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1139 Ok(())
1140 }
1141 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1142 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1143 | "deny" | "forbid" | "cap-lints" => {
1144 let value = self.take_value(&rendered_flag, inline)?;
1145 self.parsed
1146 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1147 Ok(())
1148 }
1149 "target" => {
1150 let value = self.take_value(&rendered_flag, inline)?;
1151 self.target = Some(value.clone());
1152 if value.ends_with(".json") || value.contains(['/', '\\']) {
1153 let path = PathBuf::from(value);
1154 self.required_inputs.push(path.clone());
1155 self.parsed.push(Argument::Path {
1156 flag: rendered_flag,
1157 path,
1158 });
1159 } else {
1160 self.target = Some(value.clone());
1161 self.parsed
1162 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1163 }
1164 Ok(())
1165 }
1166 "crate-type" => {
1167 let value = self.take_value(&rendered_flag, inline)?;
1168 self.crate_types
1169 .extend(value.split(',').map(ToOwned::to_owned));
1170 self.parsed
1171 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1172 Ok(())
1173 }
1174 "emit" => {
1175 let value = self.take_value(&rendered_flag, inline)?;
1176 let emits = parse_emits(&value);
1177 self.emits.extend(emits.clone());
1178 self.parsed.push(Argument::Emit(emits));
1179 Ok(())
1180 }
1181 "out-dir" => {
1182 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1183 self.out_dir = Some(path.clone());
1184 self.parsed.push(Argument::Path {
1185 flag: rendered_flag,
1186 path,
1187 });
1188 Ok(())
1189 }
1190 "sysroot" => {
1191 let path = self.take_value(&rendered_flag, inline)?;
1192 self.parsed.push(Argument::Path {
1193 flag: rendered_flag,
1194 path: path.into(),
1195 });
1196 Ok(())
1197 }
1198 "extern" => {
1199 let value = self.take_value(&rendered_flag, inline)?;
1200 let (name, path) = value
1201 .split_once('=')
1202 .map_or((value.as_str(), None), |(name, path)| {
1203 (name, Some(PathBuf::from(path)))
1204 });
1205 if let Some(path) = &path {
1206 self.required_inputs.push(path.clone());
1207 }
1208 self.parsed.push(Argument::Extern {
1209 name: name.into(),
1210 path,
1211 });
1212 Ok(())
1213 }
1214 "remap-path-prefix" => {
1215 let value = self.take_value(&rendered_flag, inline)?;
1216 let Some((from, to)) = value.split_once('=') else {
1217 return Err(BypassReason::MissingValue(rendered_flag));
1218 };
1219 self.parsed.push(Argument::RemapPath {
1220 from: from.into(),
1221 to: to.into(),
1222 });
1223 Ok(())
1224 }
1225 "codegen" => {
1226 let value = self.take_value(&rendered_flag, inline)?;
1227 self.parse_codegen(&value)
1228 }
1229 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1230 }
1231 }
1232
1233 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1234 if let Some(attached) = value.strip_prefix("-Z") {
1235 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1236 if option == "shell-argfiles" {
1237 self.parsed.push(Argument::Plain("-Zshell-argfiles".into()));
1238 return Ok(());
1239 }
1240 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1241 }
1242 match value {
1243 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1246 "-g" | "-O" | "-v" => {
1247 self.parsed.push(Argument::Plain(value.into()));
1248 return Ok(());
1249 }
1250 _ => {}
1251 }
1252 for (short, long) in [
1253 ("-A", "--allow"),
1254 ("-W", "--warn"),
1255 ("-D", "--deny"),
1256 ("-F", "--forbid"),
1257 ] {
1258 if let Some(attached) = value.strip_prefix(short) {
1259 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1260 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1261 return Ok(());
1262 }
1263 }
1264 if let Some(attached) = value.strip_prefix("-C") {
1265 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1266 return self.parse_codegen(&option);
1267 }
1268 if let Some(attached) = value.strip_prefix("-L") {
1269 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1270 let (kind, path) = search
1271 .split_once('=')
1272 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1273 if !matches!(kind, "dependency" | "native") {
1274 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1275 }
1276 self.parsed.push(Argument::SearchPath {
1277 kind: kind.into(),
1278 path: path.into(),
1279 });
1280 return Ok(());
1281 }
1282 if value == "-l" || value.starts_with("-l") {
1283 return Err(BypassReason::NativeLibrary);
1284 }
1285 if let Some(attached) = value.strip_prefix("-o") {
1286 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1287 self.explicit_output = Some(path.clone().into());
1288 self.parsed.push(Argument::Path {
1289 flag: "-o".into(),
1290 path: path.into(),
1291 });
1292 return Ok(());
1293 }
1294 Err(BypassReason::UnknownFlag(value.into()))
1295 }
1296
1297 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1298 let name = value.split_once('=').map_or(value, |(name, _)| name);
1299 if name == "incremental" {
1300 return Err(BypassReason::Incremental);
1301 }
1302 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() {
1303 return Err(BypassReason::UnknownCodegenOption(name.into()));
1304 }
1305 self.parsed
1306 .push(Argument::Plain(format!("--codegen={value}")));
1307 if name == "extra-filename" {
1308 self.extra_filename = value
1309 .split_once('=')
1310 .map_or(String::new(), |(_, value)| value.to_string());
1311 }
1312 Ok(())
1313 }
1314
1315 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1316 if value == "-" {
1317 return Err(BypassReason::StandardInput);
1318 }
1319 if self.source.replace(value.into()).is_some() {
1320 return Err(BypassReason::MultipleInputs);
1321 }
1322 Ok(())
1323 }
1324
1325 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1326 let link_output = if !self.test
1327 && !self.crate_types.is_empty()
1328 && self
1329 .crate_types
1330 .iter()
1331 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"))
1332 {
1333 LinkOutput::Library
1334 } else if self
1335 .target
1336 .as_deref()
1337 .is_some_and(compiler_bundled_wasm_target)
1338 && ((self.test && self.crate_types.is_empty())
1339 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1340 {
1341 if self.parsed.iter().any(|argument| match argument {
1342 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1343 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1344 !matches!(
1345 value.rsplit_once('=').map(|(_, value)| value),
1346 Some("y" | "yes" | "on" | "true")
1347 )
1348 }
1349 _ => false,
1350 }) {
1351 return Err(BypassReason::UnknownCodegenOption(
1352 "link-self-contained".into(),
1353 ));
1354 }
1355 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1356 && self.parsed.iter().any(|argument| {
1357 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1358 })
1359 {
1360 return Err(BypassReason::UnknownCodegenOption(
1361 "target-feature=-crt-static".into(),
1362 ));
1363 }
1364 LinkOutput::WasmExecutable
1368 } else if self.options.cache_native_links && self.links_a_native_program() {
1369 self.check_native_link_is_portable()?;
1370 LinkOutput::NativeExecutable
1371 } else if self.test {
1372 return Err(BypassReason::UnsupportedCrateType("test".into()));
1373 } else {
1374 return Err(BypassReason::UnsupportedCrateType(
1375 self.crate_types
1376 .iter()
1377 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1378 .cloned()
1379 .unwrap_or_else(|| "bin".into()),
1380 ));
1381 };
1382 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
1383 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
1384 _ => None,
1385 }) {
1386 return Err(BypassReason::UnresolvedExtern(name.clone()));
1387 }
1388 if let Some(emit) = self
1389 .emits
1390 .iter()
1391 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
1392 {
1393 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
1394 }
1395 if !self
1396 .emits
1397 .iter()
1398 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
1399 {
1400 return Err(BypassReason::NoCacheableOutput);
1401 }
1402 Ok(link_output)
1403 }
1404}
1405
1406impl Parser<'_> {
1407 fn links_a_native_program(&self) -> bool {
1414 self.target.is_none()
1415 && self.emits.iter().any(|emit| emit.kind == "link")
1420 && ((self.test && self.crate_types.is_empty())
1421 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin"))
1422 }
1423
1424 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
1431 for argument in &self.parsed {
1432 let Argument::Plain(value) = argument else {
1433 continue;
1434 };
1435 let (name, value) = if value == "-g" {
1438 ("debuginfo", Some("2"))
1439 } else if let Some(option) = value.strip_prefix("--codegen=") {
1440 match option.split_once('=') {
1441 Some((name, value)) => (name, Some(value)),
1442 None => (option, None),
1446 }
1447 } else {
1448 continue;
1449 };
1450 let unportable = match name {
1451 "split-debuginfo" => value != Some("off"),
1454 "debuginfo" if cfg!(target_os = "macos") => !matches!(value, Some("0" | "none")),
1459 "rpath" | "prefer-dynamic" => is_enabled(value),
1461 "link-self-contained" => true,
1464 _ => false,
1465 };
1466 if unportable {
1467 return Err(BypassReason::UnportableNativeLink(match value {
1468 Some(value) => format!("{name}={value}"),
1469 None => name.to_owned(),
1470 }));
1471 }
1472 }
1473 Ok(())
1474 }
1475}
1476
1477fn is_enabled(value: Option<&str>) -> bool {
1480 matches!(value, None | Some("y" | "yes" | "on" | "true"))
1481}
1482
1483fn compiler_bundled_wasm_target(target: &str) -> bool {
1484 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
1485}
1486
1487fn parse_emits(value: &str) -> Vec<Emit> {
1488 value
1489 .split(',')
1490 .map(|emit| {
1491 let (kind, path) = emit
1492 .split_once('=')
1493 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
1494 Emit {
1495 kind: kind.into(),
1496 path,
1497 }
1498 })
1499 .collect()
1500}
1501
1502struct ActionBuilder<'a> {
1503 invocation: &'a RustcInvocation,
1504 context: ActionContext,
1505 mappings: Vec<PathMapping>,
1506 linker: Option<LinkerIdentity>,
1507}
1508
1509impl<'a> ActionBuilder<'a> {
1510 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
1511 context.path_mappings = PathMapping::ordered(&context.path_mappings);
1512 let mappings = context
1513 .path_mappings
1514 .iter()
1515 .map(|mapping| PathMapping {
1516 root: resolve_mapping_root(&mapping.root),
1517 placeholder: mapping.placeholder.clone(),
1518 })
1519 .collect();
1520 Self {
1521 linker: None,
1522 invocation,
1523 mappings,
1524 context,
1525 }
1526 }
1527
1528 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
1529 self.linker = linker;
1530 self
1531 }
1532
1533 fn build(self) -> Result<RustcAction, BypassReason> {
1534 self.validate_mappings()?;
1535 let invocation = self.invocation_descriptor()?;
1536 let environment = self.environment_descriptor()?;
1537
1538 let mut inputs = BTreeMap::<String, CacheDigest>::new();
1539 for input in &self.context.inputs {
1540 input
1541 .digest
1542 .validate()
1543 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
1544 let path = self.normalize_path(&input.path)?;
1545 if inputs
1546 .insert(path.clone(), input.digest.clone())
1547 .is_some_and(|existing| existing != input.digest)
1548 {
1549 return Err(BypassReason::ConflictingInput(path));
1550 }
1551 }
1552 let required = self
1553 .invocation
1554 .required_inputs
1555 .iter()
1556 .map(|path| self.normalize_path(path))
1557 .collect::<Result<BTreeSet<_>, _>>()?;
1558 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
1559 return Err(BypassReason::MissingRequiredInput(missing.clone()));
1560 }
1561 let inputs = inputs
1562 .into_iter()
1563 .map(|(path, digest)| InputDescriptor { path, digest })
1564 .collect();
1565 if self.invocation.links_natively() && self.linker.is_none() {
1568 return Err(BypassReason::UnportableNativeLink(
1569 "linker identity is unknown".into(),
1570 ));
1571 }
1572 let descriptor = ActionDescriptor {
1573 version: ACTION_SCHEMA_VERSION,
1574 kind: "rustc",
1575 adapter_version: ADAPTER_VERSION,
1576 compiler: invocation.compiler,
1577 arguments: invocation.arguments,
1578 environment,
1579 inputs,
1580 linker: self.linker.clone(),
1581 };
1582 let bytes = canonical_json(&descriptor)
1583 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
1584 let digest = CacheDigest::blake3(&bytes);
1585 Ok(RustcAction { digest, bytes })
1586 }
1587
1588 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
1589 self.validate_mappings()?;
1590 let arguments = self
1591 .invocation
1592 .arguments
1593 .iter()
1594 .map(|argument| self.normalize_argument(argument))
1595 .collect::<Result<Vec<_>, _>>()?;
1596 let required_inputs = self
1597 .invocation
1598 .required_inputs
1599 .iter()
1600 .map(|path| self.normalize_path(path))
1601 .collect::<Result<BTreeSet<_>, _>>()?
1602 .into_iter()
1603 .collect();
1604 Ok(InvocationDescriptor {
1605 version: ACTION_SCHEMA_VERSION,
1606 kind: "rustc",
1607 adapter_version: ADAPTER_VERSION,
1608 compiler: CompilerDescriptor {
1609 toolchain: self.context.compiler.toolchain.clone(),
1610 rustc_version: self.context.compiler.rustc_version.clone(),
1611 host: self.context.compiler.host.clone(),
1612 },
1613 arguments,
1614 required_inputs,
1615 })
1616 }
1617
1618 fn validate_mappings(&self) -> Result<(), BypassReason> {
1619 if !self.context.working_dir.is_absolute() {
1620 return Err(BypassReason::RelativeWorkingDirectory(
1621 self.context.working_dir.clone(),
1622 ));
1623 }
1624 let mut roots = BTreeSet::new();
1625 let mut placeholders = BTreeSet::new();
1626 for mapping in &self.mappings {
1627 if !mapping.root.is_absolute() {
1628 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
1629 }
1630 if mapping.placeholder.is_empty()
1631 || !mapping
1632 .placeholder
1633 .bytes()
1634 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
1635 || !roots.insert(normalize_components(&mapping.root))
1636 || !placeholders.insert(&mapping.placeholder)
1637 {
1638 return Err(BypassReason::InvalidPathPlaceholder(
1639 mapping.placeholder.clone(),
1640 ));
1641 }
1642 }
1643 Ok(())
1644 }
1645
1646 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
1647 match argument {
1648 Argument::Plain(value) => Ok(value.clone()),
1649 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
1650 Argument::SearchPath { kind, path } => {
1651 let text = match self.normalize_path(path) {
1652 Ok(text) => text,
1653 Err(BypassReason::UnmappedAbsolutePath(absolute))
1661 if kind == "native" && self.invocation.native_search_is_inert() =>
1662 {
1663 absolute
1664 .to_str()
1665 .ok_or(BypassReason::NonUtf8Path(absolute.clone()))?
1666 .to_string()
1667 }
1668 Err(error) => return Err(error),
1669 };
1670 Ok(format!("-L{kind}={text}"))
1671 }
1672 Argument::Extern { name, path } => match path {
1673 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
1674 None => Ok(format!("--extern={name}")),
1675 },
1676 Argument::Emit(emits) => Ok(format!(
1677 "--emit={}",
1678 emits
1679 .iter()
1680 .map(|emit| match &emit.path {
1681 Some(path) => self
1682 .normalize_path(path)
1683 .map(|path| format!("{}={path}", emit.kind)),
1684 None => Ok(emit.kind.clone()),
1685 })
1686 .collect::<Result<Vec<_>, _>>()?
1687 .join(",")
1688 )),
1689 Argument::RemapPath { from, to } => Ok(format!(
1690 "--remap-path-prefix={}={}",
1691 self.normalize_path(from)?,
1692 to
1693 )),
1694 }
1695 }
1696
1697 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
1704 self.context
1705 .environment
1706 .iter()
1707 .map(|(name, value)| {
1708 let value = match value {
1709 Some(value) if self.context.portable_environment.contains(name) => {
1710 Some(self.normalize_path(Path::new(value))?)
1711 }
1712 value => value.clone(),
1713 };
1714 Ok((name.clone(), value))
1715 })
1716 .collect()
1717 }
1718
1719 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
1720 normalize_resolved_mapped_path(path, &self.context.working_dir, &self.mappings)
1721 }
1722}
1723
1724fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
1725 for mapping in mappings {
1726 let prefix = format!("${{{}}}", mapping.placeholder);
1727 let suffix = if value == prefix {
1728 ""
1729 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
1730 suffix
1731 } else {
1732 continue;
1733 };
1734 if !mapping.root.is_absolute()
1735 || (!suffix.is_empty()
1736 && suffix.split('/').any(|component| {
1737 component.is_empty()
1738 || matches!(component, "." | "..")
1739 || component.contains('\\')
1740 }))
1741 {
1742 return Err(BypassReason::InvalidPredictedInput(value.into()));
1743 }
1744 let mut path = normalize_components(&mapping.root);
1745 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
1746 return Ok(path);
1747 }
1748 Err(BypassReason::InvalidPredictedInput(value.into()))
1749}
1750
1751fn normalize_components(path: &Path) -> PathBuf {
1752 let mut normalized = PathBuf::new();
1753 for component in path.components() {
1754 match component {
1755 Component::CurDir => {}
1756 Component::ParentDir => {
1757 normalized.pop();
1758 }
1759 component => normalized.push(component.as_os_str()),
1760 }
1761 }
1762 normalized
1763}
1764
1765fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
1766 if path.is_absolute() {
1767 normalize_components(path)
1768 } else {
1769 normalize_components(&working_dir.join(path))
1770 }
1771}
1772
1773fn slash_path(path: &Path) -> Result<String, BypassReason> {
1774 path.components()
1775 .filter_map(|component| match component {
1776 Component::Normal(value) => Some(
1777 value
1778 .to_str()
1779 .map(ToOwned::to_owned)
1780 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf())),
1781 ),
1782 _ => None,
1783 })
1784 .collect::<Result<Vec<_>, _>>()
1785 .map(|components| components.join("/"))
1786}
1787
1788#[cfg(test)]
1789#[path = "rustc_cache_tests.rs"]
1790mod tests;