1#![deny(missing_docs)]
30
31use mbx_cache_core::{
32 CacheDigest, FileDigestCache, PathAliases, PathMapping as SharedPathMapping,
33 PathNormalizationError, canonical_json, normalize_mapped_path as normalize_shared_path,
34 normalize_resolved_mapped_path_with as normalize_resolved_shared_path_with,
35 resolve_path_mappings,
36};
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use std::collections::{BTreeMap, BTreeSet};
39use std::ffi::OsString;
40use std::path::{Component, Path, PathBuf};
41use thiserror::Error;
42
43mod dep_info;
44
45pub use dep_info::{DepInfoCommand, DiscoveredInputs, RustcDepInfo};
46
47pub const ACTION_SCHEMA_VERSION: u8 = 1;
49pub const ADAPTER_VERSION: u8 = 2;
56
57impl BypassReason {
58 pub fn kind(&self) -> &'static str {
63 self.into()
64 }
65
66 pub fn remediation(&self) -> Option<&'static str> {
72 match self {
73 Self::Incremental => Some(
74 "Set `MBX_INCREMENTAL=0`; mbx will then disable Cargo incremental state and cache the compilation.",
75 ),
76 Self::UnportableNativeLink(detail) if detail.contains("linker") => Some(
77 "Make the native linker resolvable on `PATH`, or configure a linker mbx can identify for this target.",
78 ),
79 Self::UnportableNativeLink(_) => Some(
80 "Remove the reported `-C` option from the active Cargo profile or `RUSTFLAGS` to make these links cacheable.",
81 ),
82 Self::UnknownFlag(_) | Self::UnknownCodegenOption(_) => Some(
83 "Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
84 ),
85 Self::UnmodeledLinkArgument(_) => Some(
86 "Remove the reported linker argument, or keep these links uncached if the argument is required.",
87 ),
88 Self::UnmappedAbsolutePath(_) => Some(
89 "Move the input under the workspace, target, Cargo, toolchain, or home roots so mbx can give it a portable cache name.",
90 ),
91 Self::BuildScriptInputOverlapsOutputs(_) => Some(
92 "Keep compiler output directories outside watched trees, or narrow `rerun-if-changed` to source and asset paths.",
93 ),
94 _ => None,
95 }
96 }
97}
98
99impl From<PathNormalizationError> for BypassReason {
100 fn from(reason: PathNormalizationError) -> Self {
101 match reason {
102 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
103 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
104 }
105 }
106}
107
108const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
109 "codegen-units",
110 "control-flow-guard",
111 "debug-assertions",
112 "debuginfo",
113 "default-linker-libraries",
114 "embed-bitcode",
115 "extra-filename",
116 "force-frame-pointers",
117 "force-unwind-tables",
118 "instrument-coverage",
119 "link-arg",
120 "link-args",
121 "link-dead-code",
122 "link-self-contained",
123 "linker-plugin-lto",
124 "lto",
125 "metadata",
126 "no-prepopulate-passes",
127 "opt-level",
128 "overflow-checks",
129 "panic",
130 "prefer-dynamic",
131 "relocation-model",
132 "rpath",
133 "save-temps",
134 "soft-float",
135 "split-debuginfo",
136 "split-dwarf-kind",
137 "strip",
138 "symbol-mangling-version",
139 "target-cpu",
140 "target-feature",
141 "tls-model",
142];
143
144const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
145const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
146const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
150const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
151
152const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
155 "wasm32-unknown-unknown",
156 "wasm32-wasip1",
157 "wasm32-wasip1-threads",
158 "wasm32-wasip2",
159 "wasm32v1-none",
160 "wasm64-unknown-unknown",
161];
162
163#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
164#[strum(serialize_all = "kebab-case")]
165#[non_exhaustive]
175pub enum BypassReason {
176 #[error("rustc argument {index} is not valid UTF-8")]
178 NonUtf8Argument {
179 index: usize,
181 },
182 #[error("could not model rustc response file: {0}")]
184 ResponseFile(String),
185 #[error("rustc flag is not modeled by the cache adapter: {0}")]
187 UnknownFlag(String),
188 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
190 UnknownCodegenOption(String),
191 #[error("rustc flag requires a value: {0}")]
193 MissingValue(String),
194 #[error("rustc invocation is a compiler query, not a compilation")]
196 CompilerQuery,
197 #[error("rustc invocation reads source from standard input")]
199 StandardInput,
200 #[error("rustc invocation has no source input")]
202 MissingInput,
203 #[error("rustc invocation has multiple source inputs")]
205 MultipleInputs,
206 #[error("incremental compilation cannot be combined with action caching")]
208 Incremental,
209 #[error("rustc crate type is not cacheable yet: {0}")]
211 UnsupportedCrateType(String),
212 #[error("rustc output type is not cacheable yet: {0}")]
214 UnsupportedEmit(String),
215 #[error("rustc invocation does not emit a cacheable artifact")]
217 NoCacheableOutput,
218 #[error("rustc invocation does not emit dependency information")]
220 NoDepInfo,
221 #[error("rustc output paths do not share one directory")]
223 SplitOutputDirectories,
224 #[error("rustc output path has no file name: {0}")]
226 InvalidOutputPath(PathBuf),
227 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
229 ImplicitEmitWithOutputFile(PathBuf),
230 #[error("native library on a linked output is not cacheable yet: {0}")]
233 NativeLibrary(String),
234 #[error("native static library was not found on any search path: {0}")]
237 MissingNativeLibrary(String),
238 #[error("static library file names are not known for a custom target: {0}")]
241 CustomTargetNativeLibrary(String),
242 #[error("rustc output name does not distinguish a program from a library: {0}")]
244 AmbiguousOutputName(PathBuf),
245 #[error("native link is not reproducible across checkouts: {0}")]
247 UnportableNativeLink(String),
248 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
250 UnmodeledLinkArgument(String),
251 #[error("rustc search path kind is not cacheable yet: {0}")]
253 UnsupportedSearchPath(String),
254 #[error("rustc extern does not identify an input artifact: {0}")]
256 UnresolvedExtern(String),
257 #[error("absolute path has no stable cache mapping: {0}")]
259 UnmappedAbsolutePath(PathBuf),
260 #[error("cache key paths must be valid UTF-8: {0}")]
262 NonUtf8Path(PathBuf),
263 #[error("cache action working directory must be absolute: {0}")]
265 RelativeWorkingDirectory(PathBuf),
266 #[error("cache path mapping must use an absolute root: {0}")]
268 RelativePathMapping(PathBuf),
269 #[error("cache path mapping placeholder is invalid: {0}")]
271 InvalidPathPlaceholder(String),
272 #[error("required compiler input was not provided: {0}")]
274 MissingRequiredInput(String),
275 #[error("compiler input has an invalid digest: {0}")]
277 InvalidInputDigest(String),
278 #[error("compiler input appears more than once with different content: {0}")]
280 ConflictingInput(String),
281 #[error("rustc dep-info is malformed: {0}")]
283 MalformedDepInfo(String),
284 #[error("failed to read rustc dep-info {path}: {message}")]
286 DepInfoRead {
287 path: PathBuf,
289 message: String,
291 },
292 #[error("rustc dep-info output path must be absolute: {0}")]
294 RelativeDepInfoPath(PathBuf),
295 #[error("rustc dep-info output path cannot contain a comma: {0}")]
297 UnsafeDepInfoPath(PathBuf),
298 #[error("failed to read compiler input {path}: {message}")]
300 InputRead {
301 path: PathBuf,
303 message: String,
305 },
306 #[error("declared build-script input overlaps compiler outputs: {0}")]
308 BuildScriptInputOverlapsOutputs(PathBuf),
309 #[error("build-script inputs changed during compilation (declarations: {0})")]
312 BuildScriptInputsChanged(PathBuf),
313 #[error("compiler input changed after discovery: {0}")]
315 InputChanged(PathBuf),
316 #[error("compiler input was modified during compilation: {0}")]
318 InputModifiedDuringCompilation(PathBuf),
319 #[error("discovered inputs were collected from a different working directory")]
321 DiscoveryWorkingDirectory,
322 #[error("compiler environment input has conflicting values: {0}")]
324 ConflictingEnvironment(String),
325 #[error("failed to serialize the rustc action: {0}")]
327 Serialization(String),
328 #[error("rustc action prediction is unsupported")]
330 UnsupportedPrediction,
331 #[error("rustc action prediction contains an invalid input path: {0}")]
333 InvalidPredictedInput(String),
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
337enum Argument {
338 Plain(String),
339 Path {
340 flag: String,
341 path: PathBuf,
342 },
343 SearchPath {
344 kind: String,
345 path: PathBuf,
346 },
347 Extern {
348 name: String,
349 path: Option<PathBuf>,
350 },
351 Emit(Vec<Emit>),
352 RemapPath {
353 from: PathBuf,
354 to: String,
355 },
356 InstallName(String),
358 OsoPrefix {
361 path: PathBuf,
362 trailing_slash: bool,
363 },
364 FuseLd(String),
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
371struct Emit {
372 kind: String,
373 path: Option<PathBuf>,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct RustcInvocation {
382 arguments: Vec<Argument>,
383 source: PathBuf,
384 required_inputs: Vec<PathBuf>,
385 crate_name: String,
386 extra_filename: String,
387 out_dir: Option<PathBuf>,
388 explicit_output: Option<PathBuf>,
389 emits: Vec<Emit>,
390 target: Option<String>,
391 link_output: LinkOutput,
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395enum LinkOutput {
396 Library,
397 WasmExecutable,
398 NativeExecutable,
399 NativeProcMacro,
400 NativeSharedLibrary,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413#[non_exhaustive]
414pub struct ParseOptions {
415 pub cache_native_links: bool,
418 pub custom_target_search: bool,
424}
425
426impl ParseOptions {
427 pub fn caching_native_links(enabled: bool) -> Self {
429 Self {
430 cache_native_links: enabled,
431 ..Self::default()
432 }
433 }
434
435 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
438 self.custom_target_search = enabled;
439 self
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
445pub struct RustcOutputs {
446 pub directory: PathBuf,
448 pub files: Vec<PathBuf>,
450 pub dep_info: PathBuf,
452}
453
454impl RustcInvocation {
455 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
464 Self::parse_with(arguments, ParseOptions::default())
465 }
466
467 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
472 Ok(expand_response_files(arguments)?.arguments)
473 }
474
475 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
478 let expanded = expand_response_files(arguments)?;
479 Parser::new(&expanded.arguments, options).parse()
480 }
481
482 pub fn links_natively(&self) -> bool {
485 matches!(
486 self.link_output,
487 LinkOutput::NativeExecutable
488 | LinkOutput::NativeProcMacro
489 | LinkOutput::NativeSharedLibrary
490 )
491 }
492
493 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
497 if !cfg!(target_os = "macos")
498 || self.link_output != LinkOutput::NativeProcMacro
499 || self
500 .arguments
501 .iter()
502 .any(|arg| matches!(arg, Argument::InstallName(_)))
503 {
504 return None;
505 }
506 let file = outputs
507 .files
508 .iter()
509 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
510 let name = file.file_name()?.to_str()?;
511 if !safe_install_name(name) {
512 return None;
513 }
514 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
515 }
516
517 pub fn linker_override(&self) -> Option<&Path> {
520 self.arguments.iter().find_map(|argument| {
521 let Argument::Plain(value) = argument else {
522 return None;
523 };
524 value.strip_prefix("--codegen=linker=").map(Path::new)
525 })
526 }
527
528 pub fn fuse_ld(&self) -> Option<&str> {
531 self.arguments.iter().find_map(|argument| {
532 let Argument::FuseLd(selection) = argument else {
533 return None;
534 };
535 Some(selection.as_str())
536 })
537 }
538
539 fn emits_windows_pdb(&self) -> bool {
540 if !cfg!(windows) || !self.links_natively() {
541 return false;
542 }
543 let mut enabled = false;
544 for argument in &self.arguments {
545 let Argument::Plain(value) = argument else {
546 continue;
547 };
548 if value == "-g" {
549 enabled = true;
550 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
551 enabled = !matches!(value, "0" | "none");
552 }
553 }
554 enabled
555 }
556
557 pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
561 self.arguments.iter().filter_map(|argument| match argument {
562 Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
563 _ => None,
564 })
565 }
566
567 pub fn source(&self) -> &Path {
569 &self.source
570 }
571
572 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
578 self.required_inputs
579 .iter()
580 .map(|path| absolute_path(path, working_dir))
581 .collect()
582 }
583
584 pub fn target(&self) -> Option<&str> {
586 self.target.as_deref()
587 }
588
589 pub fn crate_name(&self) -> &str {
591 &self.crate_name
592 }
593
594 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
607 let linked = self
608 .arguments
609 .iter()
610 .filter_map(|argument| match argument {
611 Argument::Extern {
612 path: Some(path), ..
613 } => Some(path.as_path()),
614 _ => None,
615 })
616 .collect::<BTreeSet<_>>();
617 let owned = discovered
618 .inputs
619 .iter()
620 .filter(|input| {
621 !linked.contains(input.path.as_path()) && !discovered.is_native_only(&input.path)
622 })
623 .map(|input| (input.path.as_path(), &input.digest))
624 .collect::<BTreeMap<_, _>>();
625 let mut bytes = Vec::new();
626 for (path, digest) in owned {
627 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
628 bytes.push(0);
629 bytes.extend_from_slice(digest.key().as_bytes());
630 bytes.push(0);
631 }
632 CacheDigest::blake3(&bytes)
633 }
634
635 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
640 if !working_dir.is_absolute() {
641 return Err(BypassReason::RelativeWorkingDirectory(
642 working_dir.to_path_buf(),
643 ));
644 }
645 let explicit_output = self
646 .explicit_output
647 .as_deref()
648 .map(|path| absolute_path(path, working_dir));
649 let output_directory = explicit_output
650 .as_deref()
651 .and_then(Path::parent)
652 .map(Path::to_path_buf)
653 .or_else(|| {
654 self.out_dir
655 .as_deref()
656 .map(|path| absolute_path(path, working_dir))
657 })
658 .unwrap_or_else(|| normalize_components(working_dir));
659 if let Some(output) = &explicit_output
663 && self.emits.iter().any(|emit| {
664 emit.path.is_none()
665 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
666 })
667 {
668 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
669 }
670 let mut files = BTreeSet::new();
671 let mut dep_info = None;
672 for emit in &self.emits {
673 if emit.kind == "dep-info" {
674 let path = emit.path.as_ref().map_or_else(
675 || {
676 explicit_output.clone().map_or_else(
677 || {
678 output_directory
679 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
680 },
681 |path| path.with_extension("d"),
682 )
683 },
684 |path| absolute_path(path, working_dir),
685 );
686 if path.file_name().is_none() {
687 return Err(BypassReason::InvalidOutputPath(path));
688 }
689 dep_info = Some(path);
690 continue;
691 }
692 let (prefix, extension) = match emit.kind.as_str() {
693 "link" => match self.link_output {
694 LinkOutput::Library => ("lib", "rlib"),
695 LinkOutput::WasmExecutable => ("", "wasm"),
696 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
697 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
698 std::env::consts::DLL_PREFIX,
699 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
700 ),
701 },
702 "metadata" => ("lib", "rmeta"),
703 _ => continue,
704 };
705 let path = if let Some(path) = &emit.path {
706 absolute_path(path, working_dir)
707 } else {
708 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
709 output_directory.join(if extension.is_empty() {
710 name
711 } else {
712 format!("{name}.{extension}")
713 })
714 };
715 if path.file_name().is_none() {
716 return Err(BypassReason::InvalidOutputPath(path));
717 }
718 if path.parent() != Some(output_directory.as_path()) {
719 return Err(BypassReason::SplitOutputDirectories);
720 }
721 let has_library_extension = path
726 .extension()
727 .and_then(|extension| extension.to_str())
728 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
729 || (cfg!(windows)
730 && path
731 .file_stem()
732 .and_then(|stem| Path::new(stem).extension())
733 .and_then(|extension| extension.to_str())
734 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
735 if emit.kind == "link"
736 && !matches!(self.link_output, LinkOutput::Library)
737 && has_library_extension
738 {
739 return Err(BypassReason::AmbiguousOutputName(path));
740 }
741 if emit.kind == "link" && self.emits_windows_pdb() {
742 files.insert(path.with_extension("pdb"));
743 }
744 files.insert(path);
745 }
746 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
747 if dep_info.parent() != Some(output_directory.as_path()) {
748 return Err(BypassReason::SplitOutputDirectories);
749 }
750 Ok(RustcOutputs {
751 directory: output_directory,
752 files: files.into_iter().collect(),
753 dep_info,
754 })
755 }
756
757 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
763 self.action_linked_by(context, None)
764 }
765
766 pub fn action_linked_by(
775 &self,
776 context: ActionContext,
777 linker: Option<LinkerIdentity>,
778 ) -> Result<RustcAction, BypassReason> {
779 ActionBuilder::new(self, context).linked_by(linker).build()
780 }
781
782 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
784 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
785 let bytes = canonical_json(&descriptor)
786 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
787 Ok(CacheDigest::blake3(&bytes))
788 }
789
790 pub fn prediction(
793 &self,
794 context: &ActionContext,
795 discovered: &DiscoveredInputs,
796 ) -> Result<RustcInputPrediction, BypassReason> {
797 let builder = ActionBuilder::new(self, context.clone());
798 builder.validate_mappings()?;
799 let mut native_directories = BTreeSet::new();
800 for argument in &self.arguments {
801 if let Argument::SearchPath { kind, path } = argument
802 && kind == "native"
803 {
804 native_directories.insert(builder.normalize_path(path)?);
805 }
806 }
807 let mut inputs = BTreeSet::new();
819 for input in &discovered.inputs {
820 let normalized = builder.normalize_path(&input.path)?;
821 if !under_any_directory(&normalized, &native_directories)
822 || !discovered.is_native_only(&input.path)
823 {
824 inputs.insert(normalized);
825 }
826 }
827 inputs.extend(
828 native_directories
829 .into_iter()
830 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
831 );
832 Ok(RustcInputPrediction {
833 version: 4,
834 inputs: inputs.into_iter().collect(),
835 environment: discovered.environment.keys().cloned().collect(),
836 compiler_duration_ns: 0,
837 crate_name: String::new(),
838 })
839 }
840}
841
842pub fn is_build_script_crate_name(crate_name: &str) -> bool {
846 crate_name
847 .strip_prefix("build_script_")
848 .is_some_and(|stem| !stem.is_empty())
849}
850
851impl RustcOutputs {
852 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
862 is_build_script_crate_name(crate_name)
863 .then(|| self.files.iter().find(|path| self.is_executable(path)))
864 .flatten()
865 .map(PathBuf::as_path)
866 }
867
868 pub fn is_executable(&self, path: &Path) -> bool {
876 self.files.iter().any(|output| output == path)
877 && !matches!(
878 path.extension().and_then(|extension| extension.to_str()),
879 Some("rlib" | "rmeta")
880 )
881 }
882}
883
884#[derive(Debug, Clone, PartialEq, Eq)]
885pub struct PathMapping {
887 pub root: PathBuf,
889 pub placeholder: String,
891}
892
893impl PathMapping {
894 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
896 Self {
897 root: root.into(),
898 placeholder: placeholder.into(),
899 }
900 }
901
902 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
905 let mut ordered = mappings.to_vec();
906 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
907 ordered
908 }
909}
910
911fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
912 mappings
913 .iter()
914 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
915 .collect()
916}
917
918pub fn normalize_mapped_path(
925 path: &Path,
926 working_dir: &Path,
927 mappings: &[PathMapping],
928) -> Result<String, BypassReason> {
929 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
930}
931
932#[derive(Debug, Clone, PartialEq, Eq)]
933pub struct CompilerIdentity {
935 pub toolchain: String,
937 pub rustc_version: String,
939 pub host: String,
941 pub driver: Option<String>,
943}
944
945#[derive(Debug, Clone, PartialEq, Eq)]
947pub struct ActionInput {
948 pub path: PathBuf,
950 pub digest: CacheDigest,
952}
953
954#[derive(Debug, Clone, PartialEq, Eq)]
956pub struct ActionContext {
957 pub compiler: CompilerIdentity,
959 pub working_dir: PathBuf,
961 pub path_mappings: Vec<PathMapping>,
963 pub environment: BTreeMap<String, Option<String>>,
965 pub portable_environment: BTreeSet<String>,
972 pub inputs: Vec<ActionInput>,
974}
975
976#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
986#[serde(deny_unknown_fields)]
987pub struct LinkerIdentity {
988 pub driver: String,
990 pub driver_version: String,
992 pub linker_version: String,
994 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
996 pub crt_objects: BTreeMap<String, CacheDigest>,
997 #[serde(skip_serializing_if = "Option::is_none", default)]
999 pub sdk: Option<String>,
1000 #[serde(skip_serializing_if = "Option::is_none", default)]
1002 pub deployment_target: Option<String>,
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Eq)]
1007pub struct RustcAction {
1008 pub digest: CacheDigest,
1010 pub bytes: Vec<u8>,
1012}
1013
1014#[derive(Debug, Clone, PartialEq, Eq)]
1017pub struct RustcInputPrediction {
1018 pub version: u8,
1020 pub inputs: Vec<String>,
1022 pub environment: Vec<String>,
1024 pub compiler_duration_ns: u64,
1027 pub crate_name: String,
1029}
1030
1031#[derive(Serialize, Deserialize)]
1032#[serde(deny_unknown_fields)]
1033struct RustcInputPredictionWire {
1034 version: u8,
1035 inputs: Vec<String>,
1036 environment: Vec<String>,
1037 #[serde(default, skip_serializing_if = "is_zero")]
1038 compiler_duration_ns: u64,
1039 #[serde(default, skip_serializing_if = "String::is_empty")]
1040 crate_name: String,
1041}
1042
1043impl Serialize for RustcInputPrediction {
1044 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1045 where
1046 S: Serializer,
1047 {
1048 RustcInputPredictionWire {
1049 version: self.version,
1050 inputs: if self.version == 4 {
1051 compact_prediction_inputs(&self.inputs)
1052 } else {
1053 self.inputs.clone()
1054 },
1055 environment: self.environment.clone(),
1056 compiler_duration_ns: self.compiler_duration_ns,
1057 crate_name: self.crate_name.clone(),
1058 }
1059 .serialize(serializer)
1060 }
1061}
1062
1063impl<'de> Deserialize<'de> for RustcInputPrediction {
1064 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1065 where
1066 D: Deserializer<'de>,
1067 {
1068 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1069 let inputs = if wire.version == 4 {
1070 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1071 } else {
1072 wire.inputs
1073 };
1074 Ok(Self {
1075 version: wire.version,
1076 inputs,
1077 environment: wire.environment,
1078 compiler_duration_ns: wire.compiler_duration_ns,
1079 crate_name: wire.crate_name,
1080 })
1081 }
1082}
1083
1084fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1085 let mut previous = "";
1086 inputs
1087 .iter()
1088 .map(|input| {
1089 let mut shared = previous
1090 .bytes()
1091 .zip(input.bytes())
1092 .take_while(|(left, right)| left == right)
1093 .count();
1094 while !input.is_char_boundary(shared) {
1095 shared -= 1;
1096 }
1097 let compact = format!("{shared}:{}", &input[shared..]);
1098 previous = input;
1099 compact
1100 })
1101 .collect()
1102}
1103
1104fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1105 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1106 let mut decoded_bytes = 0_usize;
1107 for input in inputs {
1108 let (shared_text, suffix) = input
1109 .split_once(':')
1110 .ok_or("compact rustc prediction input has no prefix length")?;
1111 let shared: usize = shared_text
1112 .parse()
1113 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1114 if shared.to_string() != shared_text
1115 || shared > expanded.last().map_or(0, String::len)
1116 || expanded
1117 .last()
1118 .is_some_and(|previous| !previous.is_char_boundary(shared))
1119 {
1120 return Err("compact rustc prediction prefix is not canonical");
1121 }
1122 let mut path = expanded
1123 .last()
1124 .map_or_else(String::new, |previous| previous[..shared].to_string());
1125 path.push_str(suffix);
1126 decoded_bytes = decoded_bytes
1127 .checked_add(path.len())
1128 .ok_or("compact rustc prediction is too large")?;
1129 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1130 return Err("compact rustc prediction is too large");
1131 }
1132 expanded.push(path);
1133 }
1134 Ok(expanded)
1135}
1136
1137fn is_zero(value: &u64) -> bool {
1138 *value == 0
1139}
1140
1141impl RustcInputPrediction {
1142 pub fn discover(
1145 &self,
1146 working_dir: &Path,
1147 path_mappings: &[PathMapping],
1148 digests: &dyn FileDigestCache,
1149 ) -> Result<DiscoveredInputs, BypassReason> {
1150 if !matches!(self.version, 2..=4) {
1151 return Err(BypassReason::UnsupportedPrediction);
1152 }
1153 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1154 return Err(BypassReason::UnsupportedPrediction);
1155 }
1156 let mut paths = BTreeSet::new();
1157 let mut named = BTreeSet::new();
1158 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1159 let mut native_bytes = 0_u64;
1160 for path in &self.inputs {
1161 if self.version >= 3
1162 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1163 {
1164 let directory = denormalize_path(path, path_mappings)?;
1165 dep_info::collect_native_directory(
1166 &directory,
1167 &admitted_roots,
1168 &mut paths,
1169 &mut native_bytes,
1170 )?;
1171 } else {
1172 let path = denormalize_path(path, path_mappings)?;
1173 named.insert(path.clone());
1174 paths.insert(path);
1175 }
1176 }
1177 let native_only = paths.difference(&named).cloned().collect();
1178 let environment = self
1179 .environment
1180 .iter()
1181 .map(|name| {
1182 if name.is_empty() || name.contains(['=', '\0']) {
1183 return Err(BypassReason::UnsupportedPrediction);
1184 }
1185 let value = std::env::var_os(name)
1186 .map(|value| {
1187 value
1188 .into_string()
1189 .map_err(|_| BypassReason::UnsupportedPrediction)
1190 })
1191 .transpose()?;
1192 Ok((name.clone(), value))
1193 })
1194 .collect::<Result<BTreeMap<_, _>, _>>()?;
1195 Ok(
1196 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1197 .with_native_only(native_only),
1198 )
1199 }
1200}
1201
1202#[derive(Serialize)]
1203struct ActionDescriptor {
1204 version: u8,
1205 kind: &'static str,
1206 adapter_version: u8,
1207 compiler: CompilerDescriptor,
1208 arguments: Vec<String>,
1209 environment: BTreeMap<String, Option<String>>,
1210 inputs: Vec<InputDescriptor>,
1211 #[serde(skip_serializing_if = "Option::is_none")]
1214 linker: Option<LinkerIdentity>,
1215}
1216
1217#[derive(Serialize)]
1218struct InvocationDescriptor {
1219 version: u8,
1220 kind: &'static str,
1221 adapter_version: u8,
1222 compiler: CompilerDescriptor,
1223 arguments: Vec<String>,
1224 required_inputs: Vec<String>,
1225}
1226
1227#[derive(Serialize)]
1228struct CompilerDescriptor {
1229 toolchain: String,
1230 rustc_version: String,
1231 host: String,
1232 #[serde(skip_serializing_if = "Option::is_none")]
1233 driver: Option<String>,
1234}
1235
1236#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1237struct InputDescriptor {
1238 path: String,
1239 digest: CacheDigest,
1240}
1241
1242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1244enum StaticLibraryNaming {
1245 Msvc,
1247 Unix,
1249}
1250
1251struct Parser<'a> {
1252 arguments: &'a [OsString],
1253 index: usize,
1254 parsed: Vec<Argument>,
1255 source: Option<PathBuf>,
1256 crate_types: Vec<String>,
1257 emits: Vec<Emit>,
1258 required_inputs: Vec<PathBuf>,
1259 native_libraries: Vec<String>,
1260 test: bool,
1261 crate_name: Option<String>,
1262 extra_filename: String,
1263 out_dir: Option<PathBuf>,
1264 explicit_output: Option<PathBuf>,
1265 target: Option<String>,
1266 sysroot_backend: Option<String>,
1267 options: ParseOptions,
1268}
1269
1270struct ExpandedArguments {
1271 arguments: Vec<OsString>,
1272}
1273
1274#[derive(Default)]
1275struct ResponseExpander {
1276 shell_argfiles: bool,
1277 next_is_unstable_option: bool,
1278 arguments: Vec<OsString>,
1279}
1280
1281impl ResponseExpander {
1282 fn push(&mut self, argument: String) {
1283 if self.next_is_unstable_option {
1284 self.shell_argfiles |= argument == "shell-argfiles";
1285 self.next_is_unstable_option = false;
1286 } else if let Some(option) = argument.strip_prefix("-Z") {
1287 if option.is_empty() {
1288 self.next_is_unstable_option = true;
1289 } else {
1290 self.shell_argfiles |= option == "shell-argfiles";
1291 }
1292 }
1293 self.arguments.push(argument.into());
1294 }
1295}
1296
1297fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1300 let mut expanded = ResponseExpander::default();
1301 for (index, argument) in arguments.iter().enumerate() {
1302 let argument = argument
1303 .to_str()
1304 .ok_or(BypassReason::NonUtf8Argument { index })?;
1305 let Some(argfile) = argument.strip_prefix('@') else {
1306 expanded.push(argument.to_string());
1307 continue;
1308 };
1309 let (path, shell) = match argfile.split_once(':') {
1310 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1311 _ => (argfile, false),
1312 };
1313 let contents = std::fs::read_to_string(path).map_err(|error| {
1314 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1315 })?;
1316 if shell {
1317 let arguments = shlex::split(&contents).ok_or_else(|| {
1318 BypassReason::ResponseFile(format!(
1319 "invalid shell-style arguments in {}",
1320 Path::new(path).display()
1321 ))
1322 })?;
1323 for argument in arguments {
1324 expanded.push(argument);
1325 }
1326 } else {
1327 for argument in contents.lines() {
1328 expanded.push(argument.to_string());
1329 }
1330 }
1331 }
1332 Ok(ExpandedArguments {
1333 arguments: expanded.arguments,
1334 })
1335}
1336
1337impl<'a> Parser<'a> {
1338 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1339 Self {
1340 arguments,
1341 options,
1342 index: 0,
1343 parsed: Vec::new(),
1344 source: None,
1345 crate_types: Vec::new(),
1346 emits: Vec::new(),
1347 required_inputs: Vec::new(),
1348 native_libraries: Vec::new(),
1349 test: false,
1350 crate_name: None,
1351 extra_filename: String::new(),
1352 out_dir: None,
1353 explicit_output: None,
1354 target: None,
1355 sysroot_backend: None,
1356 }
1357 }
1358
1359 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1360 while self.index < self.arguments.len() {
1361 let value = self.current()?.to_string();
1362 self.index += 1;
1363 if let Some(long) = value.strip_prefix("--") {
1364 self.parse_long(long)?;
1365 } else if value.starts_with('-') && value != "-" {
1366 self.parse_short(&value)?;
1367 } else {
1368 self.parse_input(&value)?;
1369 }
1370 }
1371
1372 if let Some(backend) = self.sysroot_backend.take()
1374 && self.parsed.iter().any(
1375 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1376 )
1377 {
1378 return Err(BypassReason::UnknownFlag(backend));
1379 }
1380 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1381 let link_output = self.classify()?;
1382 self.require_native_libraries(link_output)?;
1383 let crate_name = self.crate_name.clone().map_or_else(
1384 || {
1385 source
1386 .file_stem()
1387 .and_then(|name| name.to_str())
1388 .map(|name| name.replace('-', "_"))
1389 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1390 },
1391 Ok,
1392 )?;
1393 self.required_inputs.push(source.clone());
1394 Ok(RustcInvocation {
1395 arguments: self.parsed,
1396 source,
1397 required_inputs: self.required_inputs,
1398 crate_name,
1399 extra_filename: self.extra_filename,
1400 out_dir: self.out_dir,
1401 explicit_output: self.explicit_output,
1402 emits: self.emits,
1403 target: self.target,
1404 link_output,
1405 })
1406 }
1407
1408 fn current(&self) -> Result<&str, BypassReason> {
1409 self.arguments[self.index]
1410 .to_str()
1411 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1412 }
1413
1414 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1415 if let Some(value) = inline {
1416 if value.is_empty() {
1417 return Err(BypassReason::MissingValue(flag.into()));
1418 }
1419 return Ok(value.into());
1420 }
1421 if self.index >= self.arguments.len() {
1422 return Err(BypassReason::MissingValue(flag.into()));
1423 }
1424 let value = self.current()?.to_string();
1425 self.index += 1;
1426 Ok(value)
1427 }
1428
1429 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1430 let (flag, inline) = value
1431 .split_once('=')
1432 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1433 let rendered_flag = format!("--{flag}");
1434 match flag {
1435 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1436 "test" => {
1437 self.test = true;
1438 self.parsed.push(Argument::Plain(rendered_flag));
1439 Ok(())
1440 }
1441 "verbose" => {
1442 self.parsed.push(Argument::Plain(rendered_flag));
1443 Ok(())
1444 }
1445 "crate-name" => {
1446 let value = self.take_value(&rendered_flag, inline)?;
1447 self.crate_name = Some(value.clone());
1448 self.parsed
1449 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1450 Ok(())
1451 }
1452 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1453 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1454 | "deny" | "forbid" | "cap-lints" => {
1455 let value = self.take_value(&rendered_flag, inline)?;
1456 self.parsed
1457 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1458 Ok(())
1459 }
1460 "target" => {
1461 let value = self.take_value(&rendered_flag, inline)?;
1462 let effective = self.target.is_none();
1466 if effective {
1467 self.target = Some(value.clone());
1468 }
1469 if value.ends_with(".json") || value.contains(['/', '\\']) {
1470 let path = PathBuf::from(value);
1471 if effective {
1472 self.required_inputs.push(path.clone());
1473 }
1474 self.parsed.push(Argument::Path {
1475 flag: rendered_flag,
1476 path,
1477 });
1478 } else {
1479 self.parsed
1480 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1481 }
1482 Ok(())
1483 }
1484 "crate-type" => {
1485 let value = self.take_value(&rendered_flag, inline)?;
1486 self.crate_types
1487 .extend(value.split(',').map(ToOwned::to_owned));
1488 self.parsed
1489 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1490 Ok(())
1491 }
1492 "emit" => {
1493 let value = self.take_value(&rendered_flag, inline)?;
1494 let emits = parse_emits(&value);
1495 self.emits.extend(emits.clone());
1496 self.parsed.push(Argument::Emit(emits));
1497 Ok(())
1498 }
1499 "out-dir" => {
1500 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1501 self.out_dir = Some(path.clone());
1502 self.parsed.push(Argument::Path {
1503 flag: rendered_flag,
1504 path,
1505 });
1506 Ok(())
1507 }
1508 "sysroot" => {
1509 let path = self.take_value(&rendered_flag, inline)?;
1510 self.parsed.push(Argument::Path {
1511 flag: rendered_flag,
1512 path: path.into(),
1513 });
1514 Ok(())
1515 }
1516 "extern" => {
1517 let value = self.take_value(&rendered_flag, inline)?;
1518 let (name, path) = value
1519 .split_once('=')
1520 .map_or((value.as_str(), None), |(name, path)| {
1521 (name, Some(PathBuf::from(path)))
1522 });
1523 if let Some(path) = &path {
1524 self.required_inputs.push(path.clone());
1525 }
1526 self.parsed.push(Argument::Extern {
1527 name: name.into(),
1528 path,
1529 });
1530 Ok(())
1531 }
1532 "remap-path-prefix" => {
1533 let value = self.take_value(&rendered_flag, inline)?;
1534 let Some((from, to)) = value.split_once('=') else {
1535 return Err(BypassReason::MissingValue(rendered_flag));
1536 };
1537 self.parsed.push(Argument::RemapPath {
1538 from: from.into(),
1539 to: to.into(),
1540 });
1541 Ok(())
1542 }
1543 "codegen" => {
1544 let value = self.take_value(&rendered_flag, inline)?;
1545 self.parse_codegen(&value)
1546 }
1547 "jobs-frontend" => {
1548 let value = self.take_value(&rendered_flag, inline)?;
1549 self.parsed
1550 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1551 Ok(())
1552 }
1553 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1554 }
1555 }
1556
1557 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1558 if let Some(attached) = value.strip_prefix("-Z") {
1559 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1560 match option.as_str() {
1561 "embed-metadata=no"
1565 | "embed-metadata=yes"
1566 | "force-unstable-if-unmarked"
1567 | "shell-argfiles"
1568 | "unstable-options" => {
1569 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1570 return Ok(());
1571 }
1572 "use-sync-unwind=yes" | "use-sync-unwind=no" => {
1575 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1576 return Ok(());
1577 }
1578 "threads" | "threads=" => {
1579 return Err(BypassReason::MissingValue("-Zthreads".into()));
1580 }
1581 _ => {}
1582 }
1583 if option.starts_with("threads=") {
1584 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1585 return Ok(());
1586 }
1587 if let Some(backend) = option.strip_prefix("codegen-backend=")
1589 && !backend.is_empty()
1590 && !backend.contains(['.', '/', '\\'])
1591 {
1592 self.sysroot_backend = Some(format!("-Z{option}"));
1593 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1594 return Ok(());
1595 }
1596 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1597 }
1598 match value {
1599 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1602 "-g" | "-O" | "-v" => {
1603 self.parsed.push(Argument::Plain(value.into()));
1604 return Ok(());
1605 }
1606 _ => {}
1607 }
1608 for (short, long) in [
1609 ("-A", "--allow"),
1610 ("-W", "--warn"),
1611 ("-D", "--deny"),
1612 ("-F", "--forbid"),
1613 ] {
1614 if let Some(attached) = value.strip_prefix(short) {
1615 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1616 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1617 return Ok(());
1618 }
1619 }
1620 if let Some(attached) = value.strip_prefix("-C") {
1621 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1622 return self.parse_codegen(&option);
1623 }
1624 if let Some(attached) = value.strip_prefix("-L") {
1625 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1626 let (kind, path) = search
1627 .split_once('=')
1628 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1629 if !matches!(kind, "dependency" | "native") {
1630 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1631 }
1632 self.parsed.push(Argument::SearchPath {
1633 kind: kind.into(),
1634 path: path.into(),
1635 });
1636 return Ok(());
1637 }
1638 if let Some(attached) = value.strip_prefix("-l") {
1639 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1640 self.parsed.push(Argument::Plain(format!("-l{library}")));
1641 self.native_libraries.push(library);
1642 return Ok(());
1643 }
1644 if let Some(attached) = value.strip_prefix("-o") {
1645 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1646 self.explicit_output = Some(path.clone().into());
1647 self.parsed.push(Argument::Path {
1648 flag: "-o".into(),
1649 path: path.into(),
1650 });
1651 return Ok(());
1652 }
1653 Err(BypassReason::UnknownFlag(value.into()))
1654 }
1655
1656 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1657 let name = value.split_once('=').map_or(value, |(name, _)| name);
1658 if name == "incremental" {
1659 return Err(BypassReason::Incremental);
1660 }
1661 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1662 return Err(BypassReason::UnknownCodegenOption(name.into()));
1663 }
1664 if name == "linker-plugin-lto"
1670 && let Some((_, selection)) = value.split_once('=')
1671 && !matches!(
1672 selection,
1673 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1674 )
1675 {
1676 return Err(BypassReason::UnknownCodegenOption(format!(
1677 "linker-plugin-lto={selection}"
1678 )));
1679 }
1680 if name == "link-arg"
1687 && let Some((_, option)) = value.split_once('=')
1688 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1689 && !selection.is_empty()
1690 && !selection.contains(',')
1691 && !self.parsed.iter().any(
1692 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1693 )
1694 {
1695 if !self.parsed.iter().any(
1696 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1697 ) {
1698 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1699 }
1700 return Ok(());
1701 }
1702 if name == "link-arg"
1705 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1706 && safe_install_name(option)
1707 {
1708 self.parsed.push(Argument::InstallName(option.to_owned()));
1709 return Ok(());
1710 }
1711 if matches!(name, "link-arg" | "link-args")
1716 && let Some((_, option)) = value.split_once('=')
1717 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1718 && !prefix.trim_end_matches('/').is_empty()
1719 && !prefix.contains(',')
1720 {
1721 let trailing_slash = prefix.ends_with('/');
1722 self.parsed.push(Argument::OsoPrefix {
1723 path: PathBuf::from(prefix.trim_end_matches('/')),
1724 trailing_slash,
1725 });
1726 return Ok(());
1727 }
1728 self.parsed
1729 .push(Argument::Plain(format!("--codegen={value}")));
1730 if name == "extra-filename" {
1731 self.extra_filename = value
1732 .split_once('=')
1733 .map_or(String::new(), |(_, value)| value.to_string());
1734 }
1735 Ok(())
1736 }
1737
1738 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1753 for library in std::mem::take(&mut self.native_libraries) {
1754 if link_output != LinkOutput::Library {
1755 return Err(BypassReason::NativeLibrary(library));
1756 }
1757 let Some((kind, name)) = library.split_once('=') else {
1765 continue;
1766 };
1767 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1768 match kind {
1769 "static" => {}
1770 "dylib" | "framework" | "link-arg" => continue,
1771 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1772 }
1773 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1776 let (mut verbatim, mut bundle) = (false, true);
1777 for modifier in modifiers.split(',') {
1778 match modifier {
1779 "+verbatim" => verbatim = true,
1780 "-verbatim" => verbatim = false,
1781 "+bundle" => bundle = true,
1782 "-bundle" => bundle = false,
1783 _ => {}
1784 }
1785 }
1786 if !bundle {
1787 continue;
1788 }
1789 let archive = self
1790 .find_native_static_library(name, verbatim)
1791 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1792 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1793 self.required_inputs.push(archive);
1794 }
1795 Ok(())
1796 }
1797
1798 fn find_native_static_library(
1810 &self,
1811 name: &str,
1812 verbatim: bool,
1813 ) -> Result<Option<PathBuf>, ()> {
1814 let file_names = if verbatim {
1815 vec![name.to_owned()]
1816 } else {
1817 match self.static_library_naming() {
1818 Some(StaticLibraryNaming::Msvc) => {
1819 vec![format!("{name}.lib"), format!("lib{name}.a")]
1820 }
1821 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1822 None => return Err(()),
1823 }
1824 };
1825 Ok(self
1826 .parsed
1827 .iter()
1828 .filter_map(|argument| match argument {
1829 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1830 _ => None,
1831 })
1832 .flat_map(|directory| {
1833 file_names
1834 .iter()
1835 .map(move |file_name| directory.join(file_name))
1836 })
1837 .find(|candidate| candidate.is_file()))
1838 }
1839
1840 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1848 let Some(target) = self.target.as_deref() else {
1849 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1850 StaticLibraryNaming::Msvc
1851 } else {
1852 StaticLibraryNaming::Unix
1853 });
1854 };
1855 if target.ends_with(".json")
1856 || target.contains(['/', '\\'])
1857 || self.options.custom_target_search
1858 {
1859 return None;
1860 }
1861 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1864 StaticLibraryNaming::Msvc
1865 } else {
1866 StaticLibraryNaming::Unix
1867 })
1868 }
1869
1870 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1871 if value == "-" {
1872 return Err(BypassReason::StandardInput);
1873 }
1874 if self.source.replace(value.into()).is_some() {
1875 return Err(BypassReason::MultipleInputs);
1876 }
1877 Ok(())
1878 }
1879
1880 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1881 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1890 let builds_a_library = !self.test
1891 && !self.crate_types.is_empty()
1892 && self
1893 .crate_types
1894 .iter()
1895 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1896 let link_output = if never_links || builds_a_library {
1897 LinkOutput::Library
1898 } else if self
1899 .target
1900 .as_deref()
1901 .is_some_and(compiler_bundled_wasm_target)
1902 && ((self.test && self.crate_types.is_empty())
1903 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1904 {
1905 if self.parsed.iter().any(|argument| match argument {
1906 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1907 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1908 !matches!(
1909 value.rsplit_once('=').map(|(_, value)| value),
1910 Some("y" | "yes" | "on" | "true")
1911 )
1912 }
1913 _ => false,
1914 }) {
1915 return Err(BypassReason::UnknownCodegenOption(
1916 "link-self-contained".into(),
1917 ));
1918 }
1919 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1920 && self.parsed.iter().any(|argument| {
1921 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1922 })
1923 {
1924 return Err(BypassReason::UnknownCodegenOption(
1925 "target-feature=-crt-static".into(),
1926 ));
1927 }
1928 LinkOutput::WasmExecutable
1932 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1933 self.check_native_link_is_portable()?;
1934 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1935 LinkOutput::NativeProcMacro
1936 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1937 LinkOutput::NativeSharedLibrary
1938 } else {
1939 LinkOutput::NativeExecutable
1940 }
1941 } else if self.test {
1942 return Err(BypassReason::UnsupportedCrateType("test".into()));
1943 } else {
1944 return Err(BypassReason::UnsupportedCrateType(
1945 self.crate_types
1946 .iter()
1947 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1948 .cloned()
1949 .unwrap_or_else(|| "bin".into()),
1950 ));
1951 };
1952 if link_output != LinkOutput::Library
1961 && let Some(option) = self.first_link_argument(link_output)
1962 {
1963 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1964 }
1965 if self
1966 .parsed
1967 .iter()
1968 .any(|arg| matches!(arg, Argument::InstallName(_)))
1969 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1970 {
1971 return Err(BypassReason::UnmodeledLinkArgument(
1972 "link-arg=-Wl,-install_name".into(),
1973 ));
1974 }
1975 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1982 let Argument::FuseLd(selection) = argument else {
1983 return None;
1984 };
1985 Some(selection.as_str())
1986 }) && !matches!(
1987 link_output,
1988 LinkOutput::Library
1989 | LinkOutput::NativeExecutable
1990 | LinkOutput::NativeProcMacro
1991 | LinkOutput::NativeSharedLibrary
1992 ) {
1993 return Err(BypassReason::UnmodeledLinkArgument(format!(
1994 "link-arg=-fuse-ld={selection}"
1995 )));
1996 }
1997 if self.parsed.iter().any(|argument| {
1998 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1999 }) && !matches!(
2000 link_output,
2001 LinkOutput::NativeExecutable
2002 | LinkOutput::NativeProcMacro
2003 | LinkOutput::NativeSharedLibrary
2004 ) {
2005 return Err(BypassReason::UnknownCodegenOption("linker".into()));
2006 }
2007 if !matches!(
2011 link_output,
2012 LinkOutput::Library
2013 | LinkOutput::NativeExecutable
2014 | LinkOutput::NativeProcMacro
2015 | LinkOutput::NativeSharedLibrary
2016 ) && self
2017 .parsed
2018 .iter()
2019 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2020 {
2021 return Err(BypassReason::UnmodeledLinkArgument(
2022 "link-arg=-Wl,-oso_prefix".into(),
2023 ));
2024 }
2025 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2026 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2027 _ => None,
2028 }) {
2029 return Err(BypassReason::UnresolvedExtern(name.clone()));
2030 }
2031 if let Some(emit) = self
2032 .emits
2033 .iter()
2034 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2035 {
2036 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2037 }
2038 if !self
2039 .emits
2040 .iter()
2041 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2042 {
2043 return Err(BypassReason::NoCacheableOutput);
2044 }
2045 Ok(link_output)
2046 }
2047}
2048
2049impl Parser<'_> {
2050 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2053 self.parsed.iter().find_map(|argument| {
2054 let Argument::Plain(value) = argument else {
2055 return None;
2056 };
2057 let option = value.strip_prefix("--codegen=")?;
2058 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2059 (matches!(name, "link-arg" | "link-args")
2060 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2061 && !msvc_options_are_modeled(link_output, name, value))
2062 .then_some(option)
2063 })
2064 }
2065
2066 fn oso_prefix_covers_outputs(&self) -> bool {
2075 let Some(directory) = self
2076 .out_dir
2077 .as_deref()
2078 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2079 else {
2080 return false;
2081 };
2082 if !directory.is_absolute() {
2083 return false;
2084 }
2085 let directory = normalize_components(directory);
2086 self.parsed.iter().any(|argument| {
2087 let Argument::OsoPrefix { path, .. } = argument else {
2088 return false;
2089 };
2090 path.is_absolute()
2091 && (directory.starts_with(normalize_components(path))
2092 || std::fs::canonicalize(&directory)
2093 .ok()
2094 .zip(std::fs::canonicalize(path).ok())
2095 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2096 })
2097 }
2098
2099 fn links_a_native_artifact(&self) -> bool {
2106 self.target.is_none()
2107 && self.emits.iter().any(|emit| emit.kind == "link")
2112 && ((self.test && self.crate_types.is_empty())
2113 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2114 || (cfg!(target_os = "linux")
2115 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2116 }
2117
2118 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2125 for argument in &self.parsed {
2126 let Argument::Plain(value) = argument else {
2127 continue;
2128 };
2129 let (name, value) = if value == "-g" {
2132 ("debuginfo", Some("2"))
2133 } else if let Some(option) = value.strip_prefix("--codegen=") {
2134 match option.split_once('=') {
2135 Some((name, value)) => (name, Some(value)),
2136 None => (option, None),
2140 }
2141 } else {
2142 continue;
2143 };
2144 let unportable = match name {
2145 "split-debuginfo" => match value {
2151 Some("off") => false,
2152 Some("unpacked") if cfg!(target_os = "macos") => {
2153 !self.oso_prefix_covers_outputs()
2154 }
2155 _ => true,
2156 },
2157 "debuginfo" if cfg!(target_os = "macos") => {
2166 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2167 }
2168 "rpath" => is_enabled(value),
2176 "prefer-dynamic" => {
2177 is_enabled(value)
2178 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2179 }
2180 "link-self-contained" => true,
2183 _ => false,
2184 };
2185 if unportable {
2186 return Err(BypassReason::UnportableNativeLink(match value {
2187 Some(value) => format!("{name}={value}"),
2188 None => name.to_owned(),
2189 }));
2190 }
2191 }
2192 Ok(())
2193 }
2194}
2195
2196fn safe_install_name(name: &str) -> bool {
2197 !name.is_empty()
2198 && name
2199 .bytes()
2200 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2201}
2202
2203fn is_enabled(value: Option<&str>) -> bool {
2206 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2207}
2208
2209fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2219 cfg!(target_os = "linux")
2220 && matches!(
2221 link_output,
2222 LinkOutput::NativeExecutable
2223 | LinkOutput::NativeProcMacro
2224 | LinkOutput::NativeSharedLibrary
2225 )
2226 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2227 matches!(
2228 keyword,
2229 "defs"
2230 | "lazy"
2231 | "nodelete"
2232 | "nodlopen"
2233 | "noexecstack"
2234 | "norelro"
2235 | "now"
2236 | "origin"
2237 | "relro"
2238 )
2239 })
2240}
2241
2242fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2258 cfg!(target_env = "msvc")
2259 && matches!(
2260 link_output,
2261 LinkOutput::NativeExecutable
2262 | LinkOutput::NativeProcMacro
2263 | LinkOutput::NativeSharedLibrary
2264 )
2265 && match name {
2266 "link-arg" => msvc_option_reads_no_file(value),
2267 "link-args" => {
2270 let mut options = value.split_ascii_whitespace().peekable();
2271 options.peek().is_some() && options.all(msvc_option_reads_no_file)
2272 }
2273 _ => false,
2274 }
2275}
2276
2277fn msvc_option_reads_no_file(option: &str) -> bool {
2281 let Some(option) = option.strip_prefix(['/', '-']) else {
2282 return false;
2283 };
2284 if option.eq_ignore_ascii_case("brepro") {
2285 return true;
2286 }
2287 let Some((name, sizes)) = option.split_once(':') else {
2288 return false;
2289 };
2290 let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2293 Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2294 None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2295 };
2296 name.eq_ignore_ascii_case("stack")
2297 && sizes.split(',').count() <= 2
2298 && sizes.split(',').all(is_size)
2299}
2300
2301fn compiler_bundled_wasm_target(target: &str) -> bool {
2302 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2303}
2304
2305fn parse_emits(value: &str) -> Vec<Emit> {
2306 value
2307 .split(',')
2308 .map(|emit| {
2309 let (kind, path) = emit
2310 .split_once('=')
2311 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2312 Emit {
2313 kind: kind.into(),
2314 path,
2315 }
2316 })
2317 .collect()
2318}
2319
2320struct ActionBuilder<'a> {
2321 invocation: &'a RustcInvocation,
2322 context: ActionContext,
2323 mappings: Vec<SharedPathMapping>,
2324 aliases: PathAliases,
2328 linker: Option<LinkerIdentity>,
2329}
2330
2331impl<'a> ActionBuilder<'a> {
2332 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2333 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2334 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2335 Self {
2336 linker: None,
2337 invocation,
2338 mappings,
2339 aliases: PathAliases::default(),
2340 context,
2341 }
2342 }
2343
2344 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2345 self.linker = linker;
2346 self
2347 }
2348
2349 fn build(self) -> Result<RustcAction, BypassReason> {
2350 self.validate_mappings()?;
2351 let invocation = self.invocation_descriptor()?;
2352 let environment = self.environment_descriptor()?;
2353
2354 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2355 for input in &self.context.inputs {
2356 input
2357 .digest
2358 .validate()
2359 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2360 let path = self.normalize_input_path(&input.path)?;
2361 if inputs
2362 .insert(path.clone(), input.digest.clone())
2363 .is_some_and(|existing| existing != input.digest)
2364 {
2365 return Err(BypassReason::ConflictingInput(path));
2366 }
2367 }
2368 let required = self
2369 .invocation
2370 .required_inputs
2371 .iter()
2372 .map(|path| self.normalize_input_path(path))
2373 .collect::<Result<BTreeSet<_>, _>>()?;
2374 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2375 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2376 }
2377 let inputs = inputs
2378 .into_iter()
2379 .map(|(path, digest)| InputDescriptor { path, digest })
2380 .collect();
2381 if self.invocation.links_natively() && self.linker.is_none() {
2384 return Err(BypassReason::UnportableNativeLink(
2385 "linker identity is unknown".into(),
2386 ));
2387 }
2388 let descriptor = ActionDescriptor {
2389 version: ACTION_SCHEMA_VERSION,
2390 kind: "rustc",
2391 adapter_version: ADAPTER_VERSION,
2392 compiler: invocation.compiler,
2393 arguments: invocation.arguments,
2394 environment,
2395 inputs,
2396 linker: self.linker.clone(),
2397 };
2398 let bytes = canonical_json(&descriptor)
2399 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2400 let digest = CacheDigest::blake3(&bytes);
2401 Ok(RustcAction { digest, bytes })
2402 }
2403
2404 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2405 self.validate_mappings()?;
2406 let arguments = self
2407 .invocation
2408 .arguments
2409 .iter()
2410 .map(|argument| self.normalize_argument(argument))
2411 .collect::<Result<Vec<_>, _>>()?;
2412 let required_inputs = self
2413 .invocation
2414 .required_inputs
2415 .iter()
2416 .map(|path| self.normalize_path(path))
2417 .collect::<Result<BTreeSet<_>, _>>()?
2418 .into_iter()
2419 .collect();
2420 Ok(InvocationDescriptor {
2421 version: ACTION_SCHEMA_VERSION,
2422 kind: "rustc",
2423 adapter_version: ADAPTER_VERSION,
2424 compiler: CompilerDescriptor {
2425 toolchain: self.context.compiler.toolchain.clone(),
2426 rustc_version: self.context.compiler.rustc_version.clone(),
2427 host: self.context.compiler.host.clone(),
2428 driver: self.context.compiler.driver.clone(),
2429 },
2430 arguments,
2431 required_inputs,
2432 })
2433 }
2434
2435 fn validate_mappings(&self) -> Result<(), BypassReason> {
2436 if !self.context.working_dir.is_absolute() {
2437 return Err(BypassReason::RelativeWorkingDirectory(
2438 self.context.working_dir.clone(),
2439 ));
2440 }
2441 let mut roots = BTreeSet::new();
2442 let mut placeholders = BTreeSet::new();
2443 for mapping in &self.mappings {
2444 if !mapping.root.is_absolute() {
2445 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2446 }
2447 if mapping.placeholder.is_empty()
2448 || !mapping
2449 .placeholder
2450 .bytes()
2451 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2452 || !roots.insert(normalize_components(&mapping.root))
2453 || !placeholders.insert(&mapping.placeholder)
2454 {
2455 return Err(BypassReason::InvalidPathPlaceholder(
2456 mapping.placeholder.clone(),
2457 ));
2458 }
2459 }
2460 Ok(())
2461 }
2462
2463 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2464 match argument {
2465 Argument::Plain(value) => Ok(value.clone()),
2466 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2467 Argument::SearchPath { kind, path } => {
2468 let text = self.normalize_path(path)?;
2469 Ok(format!("-L{kind}={text}"))
2470 }
2471 Argument::Extern { name, path } => match path {
2472 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2473 None => Ok(format!("--extern={name}")),
2474 },
2475 Argument::Emit(emits) => Ok(format!(
2476 "--emit={}",
2477 emits
2478 .iter()
2479 .map(|emit| match &emit.path {
2480 Some(path) => self
2481 .normalize_path(path)
2482 .map(|path| format!("{}={path}", emit.kind)),
2483 None => Ok(emit.kind.clone()),
2484 })
2485 .collect::<Result<Vec<_>, _>>()?
2486 .join(",")
2487 )),
2488 Argument::RemapPath { from, to } => Ok(format!(
2489 "--remap-path-prefix={}={}",
2490 self.normalize_path(from)?,
2491 to
2492 )),
2493 Argument::InstallName(name) => Ok(format!(
2494 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2495 )),
2496 Argument::OsoPrefix {
2497 path,
2498 trailing_slash,
2499 } => Ok(format!(
2500 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2501 self.normalize_path(path)?,
2502 if *trailing_slash { "/" } else { "" }
2503 )),
2504 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2505 }
2506 }
2507
2508 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2515 self.context
2516 .environment
2517 .iter()
2518 .map(|(name, value)| {
2519 let value = match value {
2520 Some(value) if self.context.portable_environment.contains(name) => {
2521 Some(self.normalize_path(Path::new(value))?)
2522 }
2523 value => value.clone(),
2524 };
2525 Ok((name.clone(), value))
2526 })
2527 .collect()
2528 }
2529
2530 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2531 let absolute = normalize_components(&self.context.working_dir.join(path));
2532 for directory in self.invocation.native_search_paths() {
2533 let directory = normalize_components(&self.context.working_dir.join(directory));
2534 if let Ok(relative) = absolute.strip_prefix(&directory) {
2535 let suffix = relative
2541 .components()
2542 .map(|component| {
2543 component
2544 .as_os_str()
2545 .to_str()
2546 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2547 })
2548 .collect::<Result<Vec<_>, _>>()?
2549 .join("/");
2550 if !suffix.is_empty() {
2551 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2552 }
2553 }
2554 }
2555 self.normalize_path(path)
2556 }
2557
2558 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2559 normalize_resolved_shared_path_with(
2560 &self.aliases,
2561 path,
2562 &self.context.working_dir,
2563 &self.mappings,
2564 )
2565 .map_err(Into::into)
2566 }
2567}
2568
2569fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2573 directories.iter().any(|directory| {
2574 path.len() > directory.len()
2575 && path.as_bytes()[directory.len()] == b'/'
2576 && path.starts_with(directory)
2577 })
2578}
2579
2580fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2581 for mapping in mappings {
2582 let prefix = format!("${{{}}}", mapping.placeholder);
2583 let suffix = if value == prefix {
2584 ""
2585 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2586 suffix
2587 } else {
2588 continue;
2589 };
2590 if !mapping.root.is_absolute()
2591 || (!suffix.is_empty()
2592 && suffix.split('/').any(|component| {
2593 component.is_empty()
2594 || matches!(component, "." | "..")
2595 || component.contains('\\')
2596 }))
2597 {
2598 return Err(BypassReason::InvalidPredictedInput(value.into()));
2599 }
2600 let mut path = normalize_components(&mapping.root);
2601 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2602 return Ok(path);
2603 }
2604 Err(BypassReason::InvalidPredictedInput(value.into()))
2605}
2606
2607fn normalize_components(path: &Path) -> PathBuf {
2608 let mut normalized = PathBuf::new();
2609 for component in path.components() {
2610 match component {
2611 Component::CurDir => {}
2612 Component::ParentDir => {
2613 normalized.pop();
2614 }
2615 component => normalized.push(component.as_os_str()),
2616 }
2617 }
2618 normalized
2619}
2620
2621fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2622 if path.is_absolute() {
2623 normalize_components(path)
2624 } else {
2625 normalize_components(&working_dir.join(path))
2626 }
2627}
2628
2629#[cfg(test)]
2630#[path = "rustc_cache_tests.rs"]
2631mod tests;