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 let mut counted = BTreeSet::new();
1161 for path in &self.inputs {
1162 if self.version >= 3
1163 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1164 {
1165 let directory = denormalize_path(path, path_mappings)?;
1166 dep_info::collect_native_directory(
1167 &directory,
1168 &admitted_roots,
1169 &mut paths,
1170 &mut native_bytes,
1171 &mut counted,
1172 )?;
1173 } else {
1174 let path = denormalize_path(path, path_mappings)?;
1175 named.insert(path.clone());
1176 paths.insert(path);
1177 }
1178 }
1179 let native_only = paths.difference(&named).cloned().collect();
1180 let environment = self
1181 .environment
1182 .iter()
1183 .map(|name| {
1184 if name.is_empty() || name.contains(['=', '\0']) {
1185 return Err(BypassReason::UnsupportedPrediction);
1186 }
1187 let value = std::env::var_os(name)
1188 .map(|value| {
1189 value
1190 .into_string()
1191 .map_err(|_| BypassReason::UnsupportedPrediction)
1192 })
1193 .transpose()?;
1194 Ok((name.clone(), value))
1195 })
1196 .collect::<Result<BTreeMap<_, _>, _>>()?;
1197 Ok(
1198 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1199 .with_native_only(native_only),
1200 )
1201 }
1202}
1203
1204#[derive(Serialize)]
1205struct ActionDescriptor {
1206 version: u8,
1207 kind: &'static str,
1208 adapter_version: u8,
1209 compiler: CompilerDescriptor,
1210 arguments: Vec<String>,
1211 environment: BTreeMap<String, Option<String>>,
1212 inputs: Vec<InputDescriptor>,
1213 #[serde(skip_serializing_if = "Option::is_none")]
1216 linker: Option<LinkerIdentity>,
1217}
1218
1219#[derive(Serialize)]
1220struct InvocationDescriptor {
1221 version: u8,
1222 kind: &'static str,
1223 adapter_version: u8,
1224 compiler: CompilerDescriptor,
1225 arguments: Vec<String>,
1226 required_inputs: Vec<String>,
1227}
1228
1229#[derive(Serialize)]
1230struct CompilerDescriptor {
1231 toolchain: String,
1232 rustc_version: String,
1233 host: String,
1234 #[serde(skip_serializing_if = "Option::is_none")]
1235 driver: Option<String>,
1236}
1237
1238#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1239struct InputDescriptor {
1240 path: String,
1241 digest: CacheDigest,
1242}
1243
1244#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1246enum StaticLibraryNaming {
1247 Msvc,
1249 Unix,
1251}
1252
1253struct Parser<'a> {
1254 arguments: &'a [OsString],
1255 index: usize,
1256 parsed: Vec<Argument>,
1257 source: Option<PathBuf>,
1258 crate_types: Vec<String>,
1259 emits: Vec<Emit>,
1260 required_inputs: Vec<PathBuf>,
1261 native_libraries: Vec<String>,
1262 test: bool,
1263 crate_name: Option<String>,
1264 extra_filename: String,
1265 out_dir: Option<PathBuf>,
1266 explicit_output: Option<PathBuf>,
1267 target: Option<String>,
1268 sysroot_backend: Option<String>,
1269 options: ParseOptions,
1270}
1271
1272struct ExpandedArguments {
1273 arguments: Vec<OsString>,
1274}
1275
1276#[derive(Default)]
1277struct ResponseExpander {
1278 shell_argfiles: bool,
1279 next_is_unstable_option: bool,
1280 arguments: Vec<OsString>,
1281}
1282
1283impl ResponseExpander {
1284 fn push(&mut self, argument: String) {
1285 if self.next_is_unstable_option {
1286 self.shell_argfiles |= argument == "shell-argfiles";
1287 self.next_is_unstable_option = false;
1288 } else if let Some(option) = argument.strip_prefix("-Z") {
1289 if option.is_empty() {
1290 self.next_is_unstable_option = true;
1291 } else {
1292 self.shell_argfiles |= option == "shell-argfiles";
1293 }
1294 }
1295 self.arguments.push(argument.into());
1296 }
1297}
1298
1299fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1302 let mut expanded = ResponseExpander::default();
1303 for (index, argument) in arguments.iter().enumerate() {
1304 let argument = argument
1305 .to_str()
1306 .ok_or(BypassReason::NonUtf8Argument { index })?;
1307 let Some(argfile) = argument.strip_prefix('@') else {
1308 expanded.push(argument.to_string());
1309 continue;
1310 };
1311 let (path, shell) = match argfile.split_once(':') {
1312 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1313 _ => (argfile, false),
1314 };
1315 let contents = std::fs::read_to_string(path).map_err(|error| {
1316 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1317 })?;
1318 if shell {
1319 let arguments = shlex::split(&contents).ok_or_else(|| {
1320 BypassReason::ResponseFile(format!(
1321 "invalid shell-style arguments in {}",
1322 Path::new(path).display()
1323 ))
1324 })?;
1325 for argument in arguments {
1326 expanded.push(argument);
1327 }
1328 } else {
1329 for argument in contents.lines() {
1330 expanded.push(argument.to_string());
1331 }
1332 }
1333 }
1334 Ok(ExpandedArguments {
1335 arguments: expanded.arguments,
1336 })
1337}
1338
1339impl<'a> Parser<'a> {
1340 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1341 Self {
1342 arguments,
1343 options,
1344 index: 0,
1345 parsed: Vec::new(),
1346 source: None,
1347 crate_types: Vec::new(),
1348 emits: Vec::new(),
1349 required_inputs: Vec::new(),
1350 native_libraries: Vec::new(),
1351 test: false,
1352 crate_name: None,
1353 extra_filename: String::new(),
1354 out_dir: None,
1355 explicit_output: None,
1356 target: None,
1357 sysroot_backend: None,
1358 }
1359 }
1360
1361 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1362 while self.index < self.arguments.len() {
1363 let value = self.current()?.to_string();
1364 self.index += 1;
1365 if let Some(long) = value.strip_prefix("--") {
1366 self.parse_long(long)?;
1367 } else if value.starts_with('-') && value != "-" {
1368 self.parse_short(&value)?;
1369 } else {
1370 self.parse_input(&value)?;
1371 }
1372 }
1373
1374 if let Some(backend) = self.sysroot_backend.take()
1376 && self.parsed.iter().any(
1377 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1378 )
1379 {
1380 return Err(BypassReason::UnknownFlag(backend));
1381 }
1382 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1383 let link_output = self.classify()?;
1384 self.require_native_libraries(link_output)?;
1385 let crate_name = self.crate_name.clone().map_or_else(
1386 || {
1387 source
1388 .file_stem()
1389 .and_then(|name| name.to_str())
1390 .map(|name| name.replace('-', "_"))
1391 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1392 },
1393 Ok,
1394 )?;
1395 self.required_inputs.push(source.clone());
1396 Ok(RustcInvocation {
1397 arguments: self.parsed,
1398 source,
1399 required_inputs: self.required_inputs,
1400 crate_name,
1401 extra_filename: self.extra_filename,
1402 out_dir: self.out_dir,
1403 explicit_output: self.explicit_output,
1404 emits: self.emits,
1405 target: self.target,
1406 link_output,
1407 })
1408 }
1409
1410 fn current(&self) -> Result<&str, BypassReason> {
1411 self.arguments[self.index]
1412 .to_str()
1413 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1414 }
1415
1416 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1417 if let Some(value) = inline {
1418 if value.is_empty() {
1419 return Err(BypassReason::MissingValue(flag.into()));
1420 }
1421 return Ok(value.into());
1422 }
1423 if self.index >= self.arguments.len() {
1424 return Err(BypassReason::MissingValue(flag.into()));
1425 }
1426 let value = self.current()?.to_string();
1427 self.index += 1;
1428 Ok(value)
1429 }
1430
1431 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1432 let (flag, inline) = value
1433 .split_once('=')
1434 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1435 let rendered_flag = format!("--{flag}");
1436 match flag {
1437 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1438 "test" => {
1439 self.test = true;
1440 self.parsed.push(Argument::Plain(rendered_flag));
1441 Ok(())
1442 }
1443 "verbose" => {
1444 self.parsed.push(Argument::Plain(rendered_flag));
1445 Ok(())
1446 }
1447 "crate-name" => {
1448 let value = self.take_value(&rendered_flag, inline)?;
1449 self.crate_name = Some(value.clone());
1450 self.parsed
1451 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1452 Ok(())
1453 }
1454 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1455 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1456 | "deny" | "forbid" | "cap-lints" => {
1457 let value = self.take_value(&rendered_flag, inline)?;
1458 self.parsed
1459 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1460 Ok(())
1461 }
1462 "target" => {
1463 let value = self.take_value(&rendered_flag, inline)?;
1464 let effective = self.target.is_none();
1468 if effective {
1469 self.target = Some(value.clone());
1470 }
1471 if value.ends_with(".json") || value.contains(['/', '\\']) {
1472 let path = PathBuf::from(value);
1473 if effective {
1474 self.required_inputs.push(path.clone());
1475 }
1476 self.parsed.push(Argument::Path {
1477 flag: rendered_flag,
1478 path,
1479 });
1480 } else {
1481 self.parsed
1482 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1483 }
1484 Ok(())
1485 }
1486 "crate-type" => {
1487 let value = self.take_value(&rendered_flag, inline)?;
1488 self.crate_types
1489 .extend(value.split(',').map(ToOwned::to_owned));
1490 self.parsed
1491 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1492 Ok(())
1493 }
1494 "emit" => {
1495 let value = self.take_value(&rendered_flag, inline)?;
1496 let emits = parse_emits(&value);
1497 self.emits.extend(emits.clone());
1498 self.parsed.push(Argument::Emit(emits));
1499 Ok(())
1500 }
1501 "out-dir" => {
1502 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1503 self.out_dir = Some(path.clone());
1504 self.parsed.push(Argument::Path {
1505 flag: rendered_flag,
1506 path,
1507 });
1508 Ok(())
1509 }
1510 "sysroot" => {
1511 let path = self.take_value(&rendered_flag, inline)?;
1512 self.parsed.push(Argument::Path {
1513 flag: rendered_flag,
1514 path: path.into(),
1515 });
1516 Ok(())
1517 }
1518 "extern" => {
1519 let value = self.take_value(&rendered_flag, inline)?;
1520 let (name, path) = value
1521 .split_once('=')
1522 .map_or((value.as_str(), None), |(name, path)| {
1523 (name, Some(PathBuf::from(path)))
1524 });
1525 if let Some(path) = &path {
1526 self.required_inputs.push(path.clone());
1527 }
1528 self.parsed.push(Argument::Extern {
1529 name: name.into(),
1530 path,
1531 });
1532 Ok(())
1533 }
1534 "remap-path-prefix" => {
1535 let value = self.take_value(&rendered_flag, inline)?;
1536 let Some((from, to)) = value.split_once('=') else {
1537 return Err(BypassReason::MissingValue(rendered_flag));
1538 };
1539 self.parsed.push(Argument::RemapPath {
1540 from: from.into(),
1541 to: to.into(),
1542 });
1543 Ok(())
1544 }
1545 "codegen" => {
1546 let value = self.take_value(&rendered_flag, inline)?;
1547 self.parse_codegen(&value)
1548 }
1549 "jobs-frontend" => {
1550 let value = self.take_value(&rendered_flag, inline)?;
1551 self.parsed
1552 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1553 Ok(())
1554 }
1555 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1556 }
1557 }
1558
1559 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1560 if let Some(attached) = value.strip_prefix("-Z") {
1561 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1562 match option.as_str() {
1563 "embed-metadata=no"
1567 | "embed-metadata=yes"
1568 | "force-unstable-if-unmarked"
1569 | "shell-argfiles"
1570 | "unstable-options" => {
1571 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1572 return Ok(());
1573 }
1574 "use-sync-unwind=yes" | "use-sync-unwind=no" => {
1577 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1578 return Ok(());
1579 }
1580 "threads" | "threads=" => {
1581 return Err(BypassReason::MissingValue("-Zthreads".into()));
1582 }
1583 _ => {}
1584 }
1585 if option.starts_with("threads=") {
1586 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1587 return Ok(());
1588 }
1589 if let Some(backend) = option.strip_prefix("codegen-backend=")
1591 && !backend.is_empty()
1592 && !backend.contains(['.', '/', '\\'])
1593 {
1594 self.sysroot_backend = Some(format!("-Z{option}"));
1595 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1596 return Ok(());
1597 }
1598 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1599 }
1600 match value {
1601 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1604 "-g" | "-O" | "-v" => {
1605 self.parsed.push(Argument::Plain(value.into()));
1606 return Ok(());
1607 }
1608 _ => {}
1609 }
1610 for (short, long) in [
1611 ("-A", "--allow"),
1612 ("-W", "--warn"),
1613 ("-D", "--deny"),
1614 ("-F", "--forbid"),
1615 ] {
1616 if let Some(attached) = value.strip_prefix(short) {
1617 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1618 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1619 return Ok(());
1620 }
1621 }
1622 if let Some(attached) = value.strip_prefix("-C") {
1623 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1624 return self.parse_codegen(&option);
1625 }
1626 if let Some(attached) = value.strip_prefix("-L") {
1627 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1628 let (kind, path) = search
1629 .split_once('=')
1630 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1631 if !matches!(kind, "dependency" | "native") {
1632 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1633 }
1634 self.parsed.push(Argument::SearchPath {
1635 kind: kind.into(),
1636 path: path.into(),
1637 });
1638 return Ok(());
1639 }
1640 if let Some(attached) = value.strip_prefix("-l") {
1641 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1642 self.parsed.push(Argument::Plain(format!("-l{library}")));
1643 self.native_libraries.push(library);
1644 return Ok(());
1645 }
1646 if let Some(attached) = value.strip_prefix("-o") {
1647 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1648 self.explicit_output = Some(path.clone().into());
1649 self.parsed.push(Argument::Path {
1650 flag: "-o".into(),
1651 path: path.into(),
1652 });
1653 return Ok(());
1654 }
1655 Err(BypassReason::UnknownFlag(value.into()))
1656 }
1657
1658 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1659 let name = value.split_once('=').map_or(value, |(name, _)| name);
1660 if name == "incremental" {
1661 return Err(BypassReason::Incremental);
1662 }
1663 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1664 return Err(BypassReason::UnknownCodegenOption(name.into()));
1665 }
1666 if name == "linker-plugin-lto"
1672 && let Some((_, selection)) = value.split_once('=')
1673 && !matches!(
1674 selection,
1675 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1676 )
1677 {
1678 return Err(BypassReason::UnknownCodegenOption(format!(
1679 "linker-plugin-lto={selection}"
1680 )));
1681 }
1682 if name == "link-arg"
1689 && let Some((_, option)) = value.split_once('=')
1690 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1691 && !selection.is_empty()
1692 && !selection.contains(',')
1693 && !self.parsed.iter().any(
1694 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1695 )
1696 {
1697 if !self.parsed.iter().any(
1698 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1699 ) {
1700 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1701 }
1702 return Ok(());
1703 }
1704 if name == "link-arg"
1707 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1708 && safe_install_name(option)
1709 {
1710 self.parsed.push(Argument::InstallName(option.to_owned()));
1711 return Ok(());
1712 }
1713 if matches!(name, "link-arg" | "link-args")
1718 && let Some((_, option)) = value.split_once('=')
1719 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1720 && !prefix.trim_end_matches('/').is_empty()
1721 && !prefix.contains(',')
1722 {
1723 let trailing_slash = prefix.ends_with('/');
1724 self.parsed.push(Argument::OsoPrefix {
1725 path: PathBuf::from(prefix.trim_end_matches('/')),
1726 trailing_slash,
1727 });
1728 return Ok(());
1729 }
1730 self.parsed
1731 .push(Argument::Plain(format!("--codegen={value}")));
1732 if name == "extra-filename" {
1733 self.extra_filename = value
1734 .split_once('=')
1735 .map_or(String::new(), |(_, value)| value.to_string());
1736 }
1737 Ok(())
1738 }
1739
1740 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1755 for library in std::mem::take(&mut self.native_libraries) {
1756 if link_output != LinkOutput::Library {
1757 return Err(BypassReason::NativeLibrary(library));
1758 }
1759 let Some((kind, name)) = library.split_once('=') else {
1767 continue;
1768 };
1769 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1770 match kind {
1771 "static" => {}
1772 "dylib" | "framework" | "link-arg" => continue,
1773 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1774 }
1775 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1778 let (mut verbatim, mut bundle) = (false, true);
1779 for modifier in modifiers.split(',') {
1780 match modifier {
1781 "+verbatim" => verbatim = true,
1782 "-verbatim" => verbatim = false,
1783 "+bundle" => bundle = true,
1784 "-bundle" => bundle = false,
1785 _ => {}
1786 }
1787 }
1788 if !bundle {
1789 continue;
1790 }
1791 let archive = self
1792 .find_native_static_library(name, verbatim)
1793 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1794 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1795 self.required_inputs.push(archive);
1796 }
1797 Ok(())
1798 }
1799
1800 fn find_native_static_library(
1812 &self,
1813 name: &str,
1814 verbatim: bool,
1815 ) -> Result<Option<PathBuf>, ()> {
1816 let file_names = if verbatim {
1817 vec![name.to_owned()]
1818 } else {
1819 match self.static_library_naming() {
1820 Some(StaticLibraryNaming::Msvc) => {
1821 vec![format!("{name}.lib"), format!("lib{name}.a")]
1822 }
1823 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1824 None => return Err(()),
1825 }
1826 };
1827 Ok(self
1828 .parsed
1829 .iter()
1830 .filter_map(|argument| match argument {
1831 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1832 _ => None,
1833 })
1834 .flat_map(|directory| {
1835 file_names
1836 .iter()
1837 .map(move |file_name| directory.join(file_name))
1838 })
1839 .find(|candidate| candidate.is_file()))
1840 }
1841
1842 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1850 let Some(target) = self.target.as_deref() else {
1851 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1852 StaticLibraryNaming::Msvc
1853 } else {
1854 StaticLibraryNaming::Unix
1855 });
1856 };
1857 if target.ends_with(".json")
1858 || target.contains(['/', '\\'])
1859 || self.options.custom_target_search
1860 {
1861 return None;
1862 }
1863 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1866 StaticLibraryNaming::Msvc
1867 } else {
1868 StaticLibraryNaming::Unix
1869 })
1870 }
1871
1872 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1873 if value == "-" {
1874 return Err(BypassReason::StandardInput);
1875 }
1876 if self.source.replace(value.into()).is_some() {
1877 return Err(BypassReason::MultipleInputs);
1878 }
1879 Ok(())
1880 }
1881
1882 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1883 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1892 let builds_a_library = !self.test
1893 && !self.crate_types.is_empty()
1894 && self
1895 .crate_types
1896 .iter()
1897 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1898 let link_output = if never_links || builds_a_library {
1899 LinkOutput::Library
1900 } else if self
1901 .target
1902 .as_deref()
1903 .is_some_and(compiler_bundled_wasm_target)
1904 && ((self.test && self.crate_types.is_empty())
1905 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1906 {
1907 if self.parsed.iter().any(|argument| match argument {
1908 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1909 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1910 !matches!(
1911 value.rsplit_once('=').map(|(_, value)| value),
1912 Some("y" | "yes" | "on" | "true")
1913 )
1914 }
1915 _ => false,
1916 }) {
1917 return Err(BypassReason::UnknownCodegenOption(
1918 "link-self-contained".into(),
1919 ));
1920 }
1921 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1922 && self.parsed.iter().any(|argument| {
1923 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1924 })
1925 {
1926 return Err(BypassReason::UnknownCodegenOption(
1927 "target-feature=-crt-static".into(),
1928 ));
1929 }
1930 LinkOutput::WasmExecutable
1934 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1935 self.check_native_link_is_portable()?;
1936 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1937 LinkOutput::NativeProcMacro
1938 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1939 LinkOutput::NativeSharedLibrary
1940 } else {
1941 LinkOutput::NativeExecutable
1942 }
1943 } else if self.test {
1944 return Err(BypassReason::UnsupportedCrateType("test".into()));
1945 } else {
1946 return Err(BypassReason::UnsupportedCrateType(
1947 self.crate_types
1948 .iter()
1949 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1950 .cloned()
1951 .unwrap_or_else(|| "bin".into()),
1952 ));
1953 };
1954 if link_output != LinkOutput::Library
1963 && let Some(option) = self.first_link_argument(link_output)
1964 {
1965 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1966 }
1967 if self
1968 .parsed
1969 .iter()
1970 .any(|arg| matches!(arg, Argument::InstallName(_)))
1971 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1972 {
1973 return Err(BypassReason::UnmodeledLinkArgument(
1974 "link-arg=-Wl,-install_name".into(),
1975 ));
1976 }
1977 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1984 let Argument::FuseLd(selection) = argument else {
1985 return None;
1986 };
1987 Some(selection.as_str())
1988 }) && !matches!(
1989 link_output,
1990 LinkOutput::Library
1991 | LinkOutput::NativeExecutable
1992 | LinkOutput::NativeProcMacro
1993 | LinkOutput::NativeSharedLibrary
1994 ) {
1995 return Err(BypassReason::UnmodeledLinkArgument(format!(
1996 "link-arg=-fuse-ld={selection}"
1997 )));
1998 }
1999 if self.parsed.iter().any(|argument| {
2000 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
2001 }) && !matches!(
2002 link_output,
2003 LinkOutput::Library
2004 | LinkOutput::NativeExecutable
2005 | LinkOutput::NativeProcMacro
2006 | LinkOutput::NativeSharedLibrary
2007 ) {
2008 return Err(BypassReason::UnknownCodegenOption("linker".into()));
2009 }
2010 if !matches!(
2014 link_output,
2015 LinkOutput::Library
2016 | LinkOutput::NativeExecutable
2017 | LinkOutput::NativeProcMacro
2018 | LinkOutput::NativeSharedLibrary
2019 ) && self
2020 .parsed
2021 .iter()
2022 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2023 {
2024 return Err(BypassReason::UnmodeledLinkArgument(
2025 "link-arg=-Wl,-oso_prefix".into(),
2026 ));
2027 }
2028 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2029 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2030 _ => None,
2031 }) {
2032 return Err(BypassReason::UnresolvedExtern(name.clone()));
2033 }
2034 if let Some(emit) = self
2035 .emits
2036 .iter()
2037 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2038 {
2039 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2040 }
2041 if !self
2042 .emits
2043 .iter()
2044 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2045 {
2046 return Err(BypassReason::NoCacheableOutput);
2047 }
2048 Ok(link_output)
2049 }
2050}
2051
2052impl Parser<'_> {
2053 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2056 self.parsed.iter().find_map(|argument| {
2057 let Argument::Plain(value) = argument else {
2058 return None;
2059 };
2060 let option = value.strip_prefix("--codegen=")?;
2061 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2062 (matches!(name, "link-arg" | "link-args")
2063 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2064 && !msvc_options_are_modeled(link_output, name, value))
2065 .then_some(option)
2066 })
2067 }
2068
2069 fn oso_prefix_covers_outputs(&self) -> bool {
2078 let Some(directory) = self
2079 .out_dir
2080 .as_deref()
2081 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2082 else {
2083 return false;
2084 };
2085 if !directory.is_absolute() {
2086 return false;
2087 }
2088 let directory = normalize_components(directory);
2089 self.parsed.iter().any(|argument| {
2090 let Argument::OsoPrefix { path, .. } = argument else {
2091 return false;
2092 };
2093 path.is_absolute()
2094 && (directory.starts_with(normalize_components(path))
2095 || std::fs::canonicalize(&directory)
2096 .ok()
2097 .zip(std::fs::canonicalize(path).ok())
2098 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2099 })
2100 }
2101
2102 fn links_a_native_artifact(&self) -> bool {
2109 self.target.is_none()
2110 && self.emits.iter().any(|emit| emit.kind == "link")
2115 && ((self.test && self.crate_types.is_empty())
2116 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2117 || (cfg!(target_os = "linux")
2118 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2119 }
2120
2121 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2128 for argument in &self.parsed {
2129 let Argument::Plain(value) = argument else {
2130 continue;
2131 };
2132 let (name, value) = if value == "-g" {
2135 ("debuginfo", Some("2"))
2136 } else if let Some(option) = value.strip_prefix("--codegen=") {
2137 match option.split_once('=') {
2138 Some((name, value)) => (name, Some(value)),
2139 None => (option, None),
2143 }
2144 } else {
2145 continue;
2146 };
2147 let unportable = match name {
2148 "split-debuginfo" => match value {
2154 Some("off") => false,
2155 Some("unpacked") if cfg!(target_os = "macos") => {
2156 !self.oso_prefix_covers_outputs()
2157 }
2158 _ => true,
2159 },
2160 "debuginfo" if cfg!(target_os = "macos") => {
2169 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2170 }
2171 "rpath" => is_enabled(value),
2179 "prefer-dynamic" => {
2180 is_enabled(value)
2181 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2182 }
2183 "link-self-contained" => true,
2186 _ => false,
2187 };
2188 if unportable {
2189 return Err(BypassReason::UnportableNativeLink(match value {
2190 Some(value) => format!("{name}={value}"),
2191 None => name.to_owned(),
2192 }));
2193 }
2194 }
2195 Ok(())
2196 }
2197}
2198
2199fn safe_install_name(name: &str) -> bool {
2200 !name.is_empty()
2201 && name
2202 .bytes()
2203 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2204}
2205
2206fn is_enabled(value: Option<&str>) -> bool {
2209 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2210}
2211
2212fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2222 cfg!(target_os = "linux")
2223 && matches!(
2224 link_output,
2225 LinkOutput::NativeExecutable
2226 | LinkOutput::NativeProcMacro
2227 | LinkOutput::NativeSharedLibrary
2228 )
2229 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2230 matches!(
2231 keyword,
2232 "defs"
2233 | "lazy"
2234 | "nodelete"
2235 | "nodlopen"
2236 | "noexecstack"
2237 | "norelro"
2238 | "now"
2239 | "origin"
2240 | "relro"
2241 )
2242 })
2243}
2244
2245fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2261 cfg!(target_env = "msvc")
2262 && matches!(
2263 link_output,
2264 LinkOutput::NativeExecutable
2265 | LinkOutput::NativeProcMacro
2266 | LinkOutput::NativeSharedLibrary
2267 )
2268 && match name {
2269 "link-arg" => msvc_option_reads_no_file(value),
2270 "link-args" => {
2273 let mut options = value.split_ascii_whitespace().peekable();
2274 options.peek().is_some() && options.all(msvc_option_reads_no_file)
2275 }
2276 _ => false,
2277 }
2278}
2279
2280fn msvc_option_reads_no_file(option: &str) -> bool {
2284 let Some(option) = option.strip_prefix(['/', '-']) else {
2285 return false;
2286 };
2287 if option.eq_ignore_ascii_case("brepro") {
2288 return true;
2289 }
2290 let Some((name, sizes)) = option.split_once(':') else {
2291 return false;
2292 };
2293 let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2296 Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2297 None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2298 };
2299 name.eq_ignore_ascii_case("stack")
2300 && sizes.split(',').count() <= 2
2301 && sizes.split(',').all(is_size)
2302}
2303
2304fn compiler_bundled_wasm_target(target: &str) -> bool {
2305 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2306}
2307
2308fn parse_emits(value: &str) -> Vec<Emit> {
2309 value
2310 .split(',')
2311 .map(|emit| {
2312 let (kind, path) = emit
2313 .split_once('=')
2314 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2315 Emit {
2316 kind: kind.into(),
2317 path,
2318 }
2319 })
2320 .collect()
2321}
2322
2323struct ActionBuilder<'a> {
2324 invocation: &'a RustcInvocation,
2325 context: ActionContext,
2326 mappings: Vec<SharedPathMapping>,
2327 aliases: PathAliases,
2331 linker: Option<LinkerIdentity>,
2332}
2333
2334impl<'a> ActionBuilder<'a> {
2335 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2336 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2337 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2338 Self {
2339 linker: None,
2340 invocation,
2341 mappings,
2342 aliases: PathAliases::default(),
2343 context,
2344 }
2345 }
2346
2347 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2348 self.linker = linker;
2349 self
2350 }
2351
2352 fn build(self) -> Result<RustcAction, BypassReason> {
2353 self.validate_mappings()?;
2354 let invocation = self.invocation_descriptor()?;
2355 let environment = self.environment_descriptor()?;
2356
2357 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2358 for input in &self.context.inputs {
2359 input
2360 .digest
2361 .validate()
2362 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2363 let path = self.normalize_input_path(&input.path)?;
2364 if inputs
2365 .insert(path.clone(), input.digest.clone())
2366 .is_some_and(|existing| existing != input.digest)
2367 {
2368 return Err(BypassReason::ConflictingInput(path));
2369 }
2370 }
2371 let required = self
2372 .invocation
2373 .required_inputs
2374 .iter()
2375 .map(|path| self.normalize_input_path(path))
2376 .collect::<Result<BTreeSet<_>, _>>()?;
2377 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2378 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2379 }
2380 let inputs = inputs
2381 .into_iter()
2382 .map(|(path, digest)| InputDescriptor { path, digest })
2383 .collect();
2384 if self.invocation.links_natively() && self.linker.is_none() {
2387 return Err(BypassReason::UnportableNativeLink(
2388 "linker identity is unknown".into(),
2389 ));
2390 }
2391 let descriptor = ActionDescriptor {
2392 version: ACTION_SCHEMA_VERSION,
2393 kind: "rustc",
2394 adapter_version: ADAPTER_VERSION,
2395 compiler: invocation.compiler,
2396 arguments: invocation.arguments,
2397 environment,
2398 inputs,
2399 linker: self.linker.clone(),
2400 };
2401 let bytes = canonical_json(&descriptor)
2402 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2403 let digest = CacheDigest::blake3(&bytes);
2404 Ok(RustcAction { digest, bytes })
2405 }
2406
2407 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2408 self.validate_mappings()?;
2409 let arguments = self
2410 .invocation
2411 .arguments
2412 .iter()
2413 .map(|argument| self.normalize_argument(argument))
2414 .collect::<Result<Vec<_>, _>>()?;
2415 let required_inputs = self
2416 .invocation
2417 .required_inputs
2418 .iter()
2419 .map(|path| self.normalize_path(path))
2420 .collect::<Result<BTreeSet<_>, _>>()?
2421 .into_iter()
2422 .collect();
2423 Ok(InvocationDescriptor {
2424 version: ACTION_SCHEMA_VERSION,
2425 kind: "rustc",
2426 adapter_version: ADAPTER_VERSION,
2427 compiler: CompilerDescriptor {
2428 toolchain: self.context.compiler.toolchain.clone(),
2429 rustc_version: self.context.compiler.rustc_version.clone(),
2430 host: self.context.compiler.host.clone(),
2431 driver: self.context.compiler.driver.clone(),
2432 },
2433 arguments,
2434 required_inputs,
2435 })
2436 }
2437
2438 fn validate_mappings(&self) -> Result<(), BypassReason> {
2439 if !self.context.working_dir.is_absolute() {
2440 return Err(BypassReason::RelativeWorkingDirectory(
2441 self.context.working_dir.clone(),
2442 ));
2443 }
2444 let mut roots = BTreeSet::new();
2445 let mut placeholders = BTreeSet::new();
2446 for mapping in &self.mappings {
2447 if !mapping.root.is_absolute() {
2448 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2449 }
2450 if mapping.placeholder.is_empty()
2451 || !mapping
2452 .placeholder
2453 .bytes()
2454 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2455 || !roots.insert(normalize_components(&mapping.root))
2456 || !placeholders.insert(&mapping.placeholder)
2457 {
2458 return Err(BypassReason::InvalidPathPlaceholder(
2459 mapping.placeholder.clone(),
2460 ));
2461 }
2462 }
2463 Ok(())
2464 }
2465
2466 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2467 match argument {
2468 Argument::Plain(value) => Ok(value.clone()),
2469 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2470 Argument::SearchPath { kind, path } => {
2471 let text = self.normalize_path(path)?;
2472 Ok(format!("-L{kind}={text}"))
2473 }
2474 Argument::Extern { name, path } => match path {
2475 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2476 None => Ok(format!("--extern={name}")),
2477 },
2478 Argument::Emit(emits) => Ok(format!(
2479 "--emit={}",
2480 emits
2481 .iter()
2482 .map(|emit| match &emit.path {
2483 Some(path) => self
2484 .normalize_path(path)
2485 .map(|path| format!("{}={path}", emit.kind)),
2486 None => Ok(emit.kind.clone()),
2487 })
2488 .collect::<Result<Vec<_>, _>>()?
2489 .join(",")
2490 )),
2491 Argument::RemapPath { from, to } => Ok(format!(
2492 "--remap-path-prefix={}={}",
2493 self.normalize_path(from)?,
2494 to
2495 )),
2496 Argument::InstallName(name) => Ok(format!(
2497 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2498 )),
2499 Argument::OsoPrefix {
2500 path,
2501 trailing_slash,
2502 } => Ok(format!(
2503 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2504 self.normalize_path(path)?,
2505 if *trailing_slash { "/" } else { "" }
2506 )),
2507 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2508 }
2509 }
2510
2511 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2518 self.context
2519 .environment
2520 .iter()
2521 .map(|(name, value)| {
2522 let value = match value {
2523 Some(value) if self.context.portable_environment.contains(name) => {
2524 Some(self.normalize_path(Path::new(value))?)
2525 }
2526 value => value.clone(),
2527 };
2528 Ok((name.clone(), value))
2529 })
2530 .collect()
2531 }
2532
2533 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2534 let absolute = normalize_components(&self.context.working_dir.join(path));
2535 for directory in self.invocation.native_search_paths() {
2536 let directory = normalize_components(&self.context.working_dir.join(directory));
2537 if let Ok(relative) = absolute.strip_prefix(&directory) {
2538 let suffix = relative
2544 .components()
2545 .map(|component| {
2546 component
2547 .as_os_str()
2548 .to_str()
2549 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2550 })
2551 .collect::<Result<Vec<_>, _>>()?
2552 .join("/");
2553 if !suffix.is_empty() {
2554 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2555 }
2556 }
2557 }
2558 self.normalize_path(path)
2559 }
2560
2561 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2562 normalize_resolved_shared_path_with(
2563 &self.aliases,
2564 path,
2565 &self.context.working_dir,
2566 &self.mappings,
2567 )
2568 .map_err(Into::into)
2569 }
2570}
2571
2572fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2576 directories.iter().any(|directory| {
2577 path.len() > directory.len()
2578 && path.as_bytes()[directory.len()] == b'/'
2579 && path.starts_with(directory)
2580 })
2581}
2582
2583fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2584 for mapping in mappings {
2585 let prefix = format!("${{{}}}", mapping.placeholder);
2586 let suffix = if value == prefix {
2587 ""
2588 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2589 suffix
2590 } else {
2591 continue;
2592 };
2593 if !mapping.root.is_absolute()
2594 || (!suffix.is_empty()
2595 && suffix.split('/').any(|component| {
2596 component.is_empty()
2597 || matches!(component, "." | "..")
2598 || component.contains('\\')
2599 }))
2600 {
2601 return Err(BypassReason::InvalidPredictedInput(value.into()));
2602 }
2603 let mut path = normalize_components(&mapping.root);
2604 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2605 return Ok(path);
2606 }
2607 Err(BypassReason::InvalidPredictedInput(value.into()))
2608}
2609
2610fn normalize_components(path: &Path) -> PathBuf {
2611 let mut normalized = PathBuf::new();
2612 for component in path.components() {
2613 match component {
2614 Component::CurDir => {}
2615 Component::ParentDir => {
2616 normalized.pop();
2617 }
2618 component => normalized.push(component.as_os_str()),
2619 }
2620 }
2621 normalized
2622}
2623
2624fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2625 if path.is_absolute() {
2626 normalize_components(path)
2627 } else {
2628 normalize_components(&working_dir.join(path))
2629 }
2630}
2631
2632#[cfg(test)]
2633#[path = "rustc_cache_tests.rs"]
2634mod tests;