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 _ => None,
92 }
93 }
94}
95
96impl From<PathNormalizationError> for BypassReason {
97 fn from(reason: PathNormalizationError) -> Self {
98 match reason {
99 PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
100 PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
101 }
102 }
103}
104
105const SUPPORTED_CODEGEN_OPTIONS: &[&str] = &[
106 "codegen-units",
107 "control-flow-guard",
108 "debug-assertions",
109 "debuginfo",
110 "default-linker-libraries",
111 "embed-bitcode",
112 "extra-filename",
113 "force-frame-pointers",
114 "force-unwind-tables",
115 "instrument-coverage",
116 "link-arg",
117 "link-args",
118 "link-dead-code",
119 "link-self-contained",
120 "linker-plugin-lto",
121 "lto",
122 "metadata",
123 "no-prepopulate-passes",
124 "opt-level",
125 "overflow-checks",
126 "panic",
127 "prefer-dynamic",
128 "relocation-model",
129 "rpath",
130 "save-temps",
131 "soft-float",
132 "split-debuginfo",
133 "split-dwarf-kind",
134 "strip",
135 "symbol-mangling-version",
136 "target-cpu",
137 "target-feature",
138 "tls-model",
139];
140
141const NATIVE_DIRECTORY_PREDICTION_PREFIX: &str = "@native-directory:";
142const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
143const MAX_DECODED_PREDICTION_BYTES: usize = 16 * 1024 * 1024;
147const MAX_NATIVE_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
148
149const COMPILER_BUNDLED_WASM_TARGETS: &[&str] = &[
152 "wasm32-unknown-unknown",
153 "wasm32-wasip1",
154 "wasm32-wasip1-threads",
155 "wasm32-wasip2",
156 "wasm32v1-none",
157 "wasm64-unknown-unknown",
158];
159
160#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
161#[strum(serialize_all = "kebab-case")]
162#[non_exhaustive]
172pub enum BypassReason {
173 #[error("rustc argument {index} is not valid UTF-8")]
175 NonUtf8Argument {
176 index: usize,
178 },
179 #[error("could not model rustc response file: {0}")]
181 ResponseFile(String),
182 #[error("rustc flag is not modeled by the cache adapter: {0}")]
184 UnknownFlag(String),
185 #[error("rustc codegen option is not modeled by the cache adapter: {0}")]
187 UnknownCodegenOption(String),
188 #[error("rustc flag requires a value: {0}")]
190 MissingValue(String),
191 #[error("rustc invocation is a compiler query, not a compilation")]
193 CompilerQuery,
194 #[error("rustc invocation reads source from standard input")]
196 StandardInput,
197 #[error("rustc invocation has no source input")]
199 MissingInput,
200 #[error("rustc invocation has multiple source inputs")]
202 MultipleInputs,
203 #[error("incremental compilation cannot be combined with action caching")]
205 Incremental,
206 #[error("rustc crate type is not cacheable yet: {0}")]
208 UnsupportedCrateType(String),
209 #[error("rustc output type is not cacheable yet: {0}")]
211 UnsupportedEmit(String),
212 #[error("rustc invocation does not emit a cacheable artifact")]
214 NoCacheableOutput,
215 #[error("rustc invocation does not emit dependency information")]
217 NoDepInfo,
218 #[error("rustc output paths do not share one directory")]
220 SplitOutputDirectories,
221 #[error("rustc output path has no file name: {0}")]
223 InvalidOutputPath(PathBuf),
224 #[error("rustc -o with an emit that has no explicit path is not modeled: {0}")]
226 ImplicitEmitWithOutputFile(PathBuf),
227 #[error("native library on a linked output is not cacheable yet: {0}")]
230 NativeLibrary(String),
231 #[error("native static library was not found on any search path: {0}")]
234 MissingNativeLibrary(String),
235 #[error("static library file names are not known for a custom target: {0}")]
238 CustomTargetNativeLibrary(String),
239 #[error("rustc output name does not distinguish a program from a library: {0}")]
241 AmbiguousOutputName(PathBuf),
242 #[error("native link is not reproducible across checkouts: {0}")]
244 UnportableNativeLink(String),
245 #[error("rustc link argument is not modeled by the cache adapter: {0}")]
247 UnmodeledLinkArgument(String),
248 #[error("rustc search path kind is not cacheable yet: {0}")]
250 UnsupportedSearchPath(String),
251 #[error("rustc extern does not identify an input artifact: {0}")]
253 UnresolvedExtern(String),
254 #[error("absolute path has no stable cache mapping: {0}")]
256 UnmappedAbsolutePath(PathBuf),
257 #[error("cache key paths must be valid UTF-8: {0}")]
259 NonUtf8Path(PathBuf),
260 #[error("cache action working directory must be absolute: {0}")]
262 RelativeWorkingDirectory(PathBuf),
263 #[error("cache path mapping must use an absolute root: {0}")]
265 RelativePathMapping(PathBuf),
266 #[error("cache path mapping placeholder is invalid: {0}")]
268 InvalidPathPlaceholder(String),
269 #[error("required compiler input was not provided: {0}")]
271 MissingRequiredInput(String),
272 #[error("compiler input has an invalid digest: {0}")]
274 InvalidInputDigest(String),
275 #[error("compiler input appears more than once with different content: {0}")]
277 ConflictingInput(String),
278 #[error("rustc dep-info is malformed: {0}")]
280 MalformedDepInfo(String),
281 #[error("failed to read rustc dep-info {path}: {message}")]
283 DepInfoRead {
284 path: PathBuf,
286 message: String,
288 },
289 #[error("rustc dep-info output path must be absolute: {0}")]
291 RelativeDepInfoPath(PathBuf),
292 #[error("rustc dep-info output path cannot contain a comma: {0}")]
294 UnsafeDepInfoPath(PathBuf),
295 #[error("failed to read compiler input {path}: {message}")]
297 InputRead {
298 path: PathBuf,
300 message: String,
302 },
303 #[error("compiler input changed after discovery: {0}")]
305 InputChanged(PathBuf),
306 #[error("compiler input was modified during compilation: {0}")]
308 InputModifiedDuringCompilation(PathBuf),
309 #[error("discovered inputs were collected from a different working directory")]
311 DiscoveryWorkingDirectory,
312 #[error("compiler environment input has conflicting values: {0}")]
314 ConflictingEnvironment(String),
315 #[error("failed to serialize the rustc action: {0}")]
317 Serialization(String),
318 #[error("rustc action prediction is unsupported")]
320 UnsupportedPrediction,
321 #[error("rustc action prediction contains an invalid input path: {0}")]
323 InvalidPredictedInput(String),
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327enum Argument {
328 Plain(String),
329 Path {
330 flag: String,
331 path: PathBuf,
332 },
333 SearchPath {
334 kind: String,
335 path: PathBuf,
336 },
337 Extern {
338 name: String,
339 path: Option<PathBuf>,
340 },
341 Emit(Vec<Emit>),
342 RemapPath {
343 from: PathBuf,
344 to: String,
345 },
346 InstallName(String),
348 OsoPrefix {
351 path: PathBuf,
352 trailing_slash: bool,
353 },
354 FuseLd(String),
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361struct Emit {
362 kind: String,
363 path: Option<PathBuf>,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct RustcInvocation {
372 arguments: Vec<Argument>,
373 source: PathBuf,
374 required_inputs: Vec<PathBuf>,
375 crate_name: String,
376 extra_filename: String,
377 out_dir: Option<PathBuf>,
378 explicit_output: Option<PathBuf>,
379 emits: Vec<Emit>,
380 target: Option<String>,
381 link_output: LinkOutput,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385enum LinkOutput {
386 Library,
387 WasmExecutable,
388 NativeExecutable,
389 NativeProcMacro,
390 NativeSharedLibrary,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
403#[non_exhaustive]
404pub struct ParseOptions {
405 pub cache_native_links: bool,
408 pub custom_target_search: bool,
414}
415
416impl ParseOptions {
417 pub fn caching_native_links(enabled: bool) -> Self {
419 Self {
420 cache_native_links: enabled,
421 ..Self::default()
422 }
423 }
424
425 pub fn with_custom_target_search(mut self, enabled: bool) -> Self {
428 self.custom_target_search = enabled;
429 self
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct RustcOutputs {
436 pub directory: PathBuf,
438 pub files: Vec<PathBuf>,
440 pub dep_info: PathBuf,
442}
443
444impl RustcInvocation {
445 pub fn parse(arguments: &[OsString]) -> Result<Self, BypassReason> {
454 Self::parse_with(arguments, ParseOptions::default())
455 }
456
457 pub fn expand_arguments(arguments: &[OsString]) -> Result<Vec<OsString>, BypassReason> {
462 Ok(expand_response_files(arguments)?.arguments)
463 }
464
465 pub fn parse_with(arguments: &[OsString], options: ParseOptions) -> Result<Self, BypassReason> {
468 let expanded = expand_response_files(arguments)?;
469 Parser::new(&expanded.arguments, options).parse()
470 }
471
472 pub fn links_natively(&self) -> bool {
475 matches!(
476 self.link_output,
477 LinkOutput::NativeExecutable
478 | LinkOutput::NativeProcMacro
479 | LinkOutput::NativeSharedLibrary
480 )
481 }
482
483 pub fn portable_install_name(&self, outputs: &RustcOutputs) -> Option<String> {
487 if !cfg!(target_os = "macos")
488 || self.link_output != LinkOutput::NativeProcMacro
489 || self
490 .arguments
491 .iter()
492 .any(|arg| matches!(arg, Argument::InstallName(_)))
493 {
494 return None;
495 }
496 let file = outputs
497 .files
498 .iter()
499 .find(|path| path.extension().is_some_and(|ext| ext == "dylib"))?;
500 let name = file.file_name()?.to_str()?;
501 if !safe_install_name(name) {
502 return None;
503 }
504 Some(format!("-Clink-arg=-Wl,-install_name,@rpath/{name}"))
505 }
506
507 pub fn linker_override(&self) -> Option<&Path> {
510 self.arguments.iter().find_map(|argument| {
511 let Argument::Plain(value) = argument else {
512 return None;
513 };
514 value.strip_prefix("--codegen=linker=").map(Path::new)
515 })
516 }
517
518 pub fn fuse_ld(&self) -> Option<&str> {
521 self.arguments.iter().find_map(|argument| {
522 let Argument::FuseLd(selection) = argument else {
523 return None;
524 };
525 Some(selection.as_str())
526 })
527 }
528
529 fn emits_windows_pdb(&self) -> bool {
530 if !cfg!(windows) || !self.links_natively() {
531 return false;
532 }
533 let mut enabled = false;
534 for argument in &self.arguments {
535 let Argument::Plain(value) = argument else {
536 continue;
537 };
538 if value == "-g" {
539 enabled = true;
540 } else if let Some(value) = value.strip_prefix("--codegen=debuginfo=") {
541 enabled = !matches!(value, "0" | "none");
542 }
543 }
544 enabled
545 }
546
547 pub fn native_search_paths(&self) -> impl Iterator<Item = &Path> {
551 self.arguments.iter().filter_map(|argument| match argument {
552 Argument::SearchPath { kind, path } if kind == "native" => Some(path.as_path()),
553 _ => None,
554 })
555 }
556
557 pub fn source(&self) -> &Path {
559 &self.source
560 }
561
562 pub fn required_inputs_in(&self, working_dir: &Path) -> Vec<PathBuf> {
568 self.required_inputs
569 .iter()
570 .map(|path| absolute_path(path, working_dir))
571 .collect()
572 }
573
574 pub fn target(&self) -> Option<&str> {
576 self.target.as_deref()
577 }
578
579 pub fn crate_name(&self) -> &str {
581 &self.crate_name
582 }
583
584 pub fn source_fingerprint(&self, discovered: &DiscoveredInputs) -> CacheDigest {
597 let linked = self
598 .arguments
599 .iter()
600 .filter_map(|argument| match argument {
601 Argument::Extern {
602 path: Some(path), ..
603 } => Some(path.as_path()),
604 _ => None,
605 })
606 .collect::<BTreeSet<_>>();
607 let owned = discovered
608 .inputs
609 .iter()
610 .filter(|input| {
611 !linked.contains(input.path.as_path()) && !discovered.is_native_only(&input.path)
612 })
613 .map(|input| (input.path.as_path(), &input.digest))
614 .collect::<BTreeMap<_, _>>();
615 let mut bytes = Vec::new();
616 for (path, digest) in owned {
617 bytes.extend_from_slice(path.as_os_str().as_encoded_bytes());
618 bytes.push(0);
619 bytes.extend_from_slice(digest.key().as_bytes());
620 bytes.push(0);
621 }
622 CacheDigest::blake3(&bytes)
623 }
624
625 pub fn outputs(&self, working_dir: &Path) -> Result<RustcOutputs, BypassReason> {
630 if !working_dir.is_absolute() {
631 return Err(BypassReason::RelativeWorkingDirectory(
632 working_dir.to_path_buf(),
633 ));
634 }
635 let explicit_output = self
636 .explicit_output
637 .as_deref()
638 .map(|path| absolute_path(path, working_dir));
639 let output_directory = explicit_output
640 .as_deref()
641 .and_then(Path::parent)
642 .map(Path::to_path_buf)
643 .or_else(|| {
644 self.out_dir
645 .as_deref()
646 .map(|path| absolute_path(path, working_dir))
647 })
648 .unwrap_or_else(|| normalize_components(working_dir));
649 if let Some(output) = &explicit_output
653 && self.emits.iter().any(|emit| {
654 emit.path.is_none()
655 && matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata")
656 })
657 {
658 return Err(BypassReason::ImplicitEmitWithOutputFile(output.clone()));
659 }
660 let mut files = BTreeSet::new();
661 let mut dep_info = None;
662 for emit in &self.emits {
663 if emit.kind == "dep-info" {
664 let path = emit.path.as_ref().map_or_else(
665 || {
666 explicit_output.clone().map_or_else(
667 || {
668 output_directory
669 .join(format!("{}{}.d", self.crate_name, self.extra_filename))
670 },
671 |path| path.with_extension("d"),
672 )
673 },
674 |path| absolute_path(path, working_dir),
675 );
676 if path.file_name().is_none() {
677 return Err(BypassReason::InvalidOutputPath(path));
678 }
679 dep_info = Some(path);
680 continue;
681 }
682 let (prefix, extension) = match emit.kind.as_str() {
683 "link" => match self.link_output {
684 LinkOutput::Library => ("lib", "rlib"),
685 LinkOutput::WasmExecutable => ("", "wasm"),
686 LinkOutput::NativeExecutable => ("", std::env::consts::EXE_EXTENSION),
687 LinkOutput::NativeProcMacro | LinkOutput::NativeSharedLibrary => (
688 std::env::consts::DLL_PREFIX,
689 std::env::consts::DLL_SUFFIX.trim_start_matches('.'),
690 ),
691 },
692 "metadata" => ("lib", "rmeta"),
693 _ => continue,
694 };
695 let path = if let Some(path) = &emit.path {
696 absolute_path(path, working_dir)
697 } else {
698 let name = format!("{prefix}{}{}", self.crate_name, self.extra_filename);
699 output_directory.join(if extension.is_empty() {
700 name
701 } else {
702 format!("{name}.{extension}")
703 })
704 };
705 if path.file_name().is_none() {
706 return Err(BypassReason::InvalidOutputPath(path));
707 }
708 if path.parent() != Some(output_directory.as_path()) {
709 return Err(BypassReason::SplitOutputDirectories);
710 }
711 let has_library_extension = path
716 .extension()
717 .and_then(|extension| extension.to_str())
718 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta"))
719 || (cfg!(windows)
720 && path
721 .file_stem()
722 .and_then(|stem| Path::new(stem).extension())
723 .and_then(|extension| extension.to_str())
724 .is_some_and(|extension| matches!(extension, "rlib" | "rmeta")));
725 if emit.kind == "link"
726 && !matches!(self.link_output, LinkOutput::Library)
727 && has_library_extension
728 {
729 return Err(BypassReason::AmbiguousOutputName(path));
730 }
731 if emit.kind == "link" && self.emits_windows_pdb() {
732 files.insert(path.with_extension("pdb"));
733 }
734 files.insert(path);
735 }
736 let dep_info = dep_info.ok_or(BypassReason::NoDepInfo)?;
737 if dep_info.parent() != Some(output_directory.as_path()) {
738 return Err(BypassReason::SplitOutputDirectories);
739 }
740 Ok(RustcOutputs {
741 directory: output_directory,
742 files: files.into_iter().collect(),
743 dep_info,
744 })
745 }
746
747 pub fn action(&self, context: ActionContext) -> Result<RustcAction, BypassReason> {
753 self.action_linked_by(context, None)
754 }
755
756 pub fn action_linked_by(
765 &self,
766 context: ActionContext,
767 linker: Option<LinkerIdentity>,
768 ) -> Result<RustcAction, BypassReason> {
769 ActionBuilder::new(self, context).linked_by(linker).build()
770 }
771
772 pub fn invocation_digest(&self, context: &ActionContext) -> Result<CacheDigest, BypassReason> {
774 let descriptor = ActionBuilder::new(self, context.clone()).invocation_descriptor()?;
775 let bytes = canonical_json(&descriptor)
776 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
777 Ok(CacheDigest::blake3(&bytes))
778 }
779
780 pub fn prediction(
783 &self,
784 context: &ActionContext,
785 discovered: &DiscoveredInputs,
786 ) -> Result<RustcInputPrediction, BypassReason> {
787 let builder = ActionBuilder::new(self, context.clone());
788 builder.validate_mappings()?;
789 let mut native_directories = BTreeSet::new();
790 for argument in &self.arguments {
791 if let Argument::SearchPath { kind, path } = argument
792 && kind == "native"
793 {
794 native_directories.insert(builder.normalize_path(path)?);
795 }
796 }
797 let mut inputs = BTreeSet::new();
809 for input in &discovered.inputs {
810 let normalized = builder.normalize_path(&input.path)?;
811 if !under_any_directory(&normalized, &native_directories)
812 || !discovered.is_native_only(&input.path)
813 {
814 inputs.insert(normalized);
815 }
816 }
817 inputs.extend(
818 native_directories
819 .into_iter()
820 .map(|directory| format!("{NATIVE_DIRECTORY_PREDICTION_PREFIX}{directory}")),
821 );
822 Ok(RustcInputPrediction {
823 version: 4,
824 inputs: inputs.into_iter().collect(),
825 environment: discovered.environment.keys().cloned().collect(),
826 compiler_duration_ns: 0,
827 crate_name: String::new(),
828 })
829 }
830}
831
832pub fn is_build_script_crate_name(crate_name: &str) -> bool {
836 crate_name
837 .strip_prefix("build_script_")
838 .is_some_and(|stem| !stem.is_empty())
839}
840
841impl RustcOutputs {
842 pub fn build_script_executable(&self, crate_name: &str) -> Option<&Path> {
852 is_build_script_crate_name(crate_name)
853 .then(|| self.files.iter().find(|path| self.is_executable(path)))
854 .flatten()
855 .map(PathBuf::as_path)
856 }
857
858 pub fn is_executable(&self, path: &Path) -> bool {
866 self.files.iter().any(|output| output == path)
867 && !matches!(
868 path.extension().and_then(|extension| extension.to_str()),
869 Some("rlib" | "rmeta")
870 )
871 }
872}
873
874#[derive(Debug, Clone, PartialEq, Eq)]
875pub struct PathMapping {
877 pub root: PathBuf,
879 pub placeholder: String,
881}
882
883impl PathMapping {
884 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
886 Self {
887 root: root.into(),
888 placeholder: placeholder.into(),
889 }
890 }
891
892 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
895 let mut ordered = mappings.to_vec();
896 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
897 ordered
898 }
899}
900
901fn shared_path_mappings(mappings: &[PathMapping]) -> Vec<SharedPathMapping> {
902 mappings
903 .iter()
904 .map(|mapping| SharedPathMapping::new(&mapping.root, &mapping.placeholder))
905 .collect()
906}
907
908pub fn normalize_mapped_path(
915 path: &Path,
916 working_dir: &Path,
917 mappings: &[PathMapping],
918) -> Result<String, BypassReason> {
919 normalize_shared_path(path, working_dir, &shared_path_mappings(mappings)).map_err(Into::into)
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
923pub struct CompilerIdentity {
925 pub toolchain: String,
927 pub rustc_version: String,
929 pub host: String,
931 pub driver: Option<String>,
933}
934
935#[derive(Debug, Clone, PartialEq, Eq)]
937pub struct ActionInput {
938 pub path: PathBuf,
940 pub digest: CacheDigest,
942}
943
944#[derive(Debug, Clone, PartialEq, Eq)]
946pub struct ActionContext {
947 pub compiler: CompilerIdentity,
949 pub working_dir: PathBuf,
951 pub path_mappings: Vec<PathMapping>,
953 pub environment: BTreeMap<String, Option<String>>,
955 pub portable_environment: BTreeSet<String>,
962 pub inputs: Vec<ActionInput>,
964}
965
966#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
976#[serde(deny_unknown_fields)]
977pub struct LinkerIdentity {
978 pub driver: String,
980 pub driver_version: String,
982 pub linker_version: String,
984 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
986 pub crt_objects: BTreeMap<String, CacheDigest>,
987 #[serde(skip_serializing_if = "Option::is_none", default)]
989 pub sdk: Option<String>,
990 #[serde(skip_serializing_if = "Option::is_none", default)]
992 pub deployment_target: Option<String>,
993}
994
995#[derive(Debug, Clone, PartialEq, Eq)]
997pub struct RustcAction {
998 pub digest: CacheDigest,
1000 pub bytes: Vec<u8>,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq)]
1007pub struct RustcInputPrediction {
1008 pub version: u8,
1010 pub inputs: Vec<String>,
1012 pub environment: Vec<String>,
1014 pub compiler_duration_ns: u64,
1017 pub crate_name: String,
1019}
1020
1021#[derive(Serialize, Deserialize)]
1022#[serde(deny_unknown_fields)]
1023struct RustcInputPredictionWire {
1024 version: u8,
1025 inputs: Vec<String>,
1026 environment: Vec<String>,
1027 #[serde(default, skip_serializing_if = "is_zero")]
1028 compiler_duration_ns: u64,
1029 #[serde(default, skip_serializing_if = "String::is_empty")]
1030 crate_name: String,
1031}
1032
1033impl Serialize for RustcInputPrediction {
1034 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1035 where
1036 S: Serializer,
1037 {
1038 RustcInputPredictionWire {
1039 version: self.version,
1040 inputs: if self.version == 4 {
1041 compact_prediction_inputs(&self.inputs)
1042 } else {
1043 self.inputs.clone()
1044 },
1045 environment: self.environment.clone(),
1046 compiler_duration_ns: self.compiler_duration_ns,
1047 crate_name: self.crate_name.clone(),
1048 }
1049 .serialize(serializer)
1050 }
1051}
1052
1053impl<'de> Deserialize<'de> for RustcInputPrediction {
1054 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1055 where
1056 D: Deserializer<'de>,
1057 {
1058 let wire = RustcInputPredictionWire::deserialize(deserializer)?;
1059 let inputs = if wire.version == 4 {
1060 expand_prediction_inputs(&wire.inputs).map_err(serde::de::Error::custom)?
1061 } else {
1062 wire.inputs
1063 };
1064 Ok(Self {
1065 version: wire.version,
1066 inputs,
1067 environment: wire.environment,
1068 compiler_duration_ns: wire.compiler_duration_ns,
1069 crate_name: wire.crate_name,
1070 })
1071 }
1072}
1073
1074fn compact_prediction_inputs(inputs: &[String]) -> Vec<String> {
1075 let mut previous = "";
1076 inputs
1077 .iter()
1078 .map(|input| {
1079 let mut shared = previous
1080 .bytes()
1081 .zip(input.bytes())
1082 .take_while(|(left, right)| left == right)
1083 .count();
1084 while !input.is_char_boundary(shared) {
1085 shared -= 1;
1086 }
1087 let compact = format!("{shared}:{}", &input[shared..]);
1088 previous = input;
1089 compact
1090 })
1091 .collect()
1092}
1093
1094fn expand_prediction_inputs(inputs: &[String]) -> Result<Vec<String>, &'static str> {
1095 let mut expanded: Vec<String> = Vec::with_capacity(inputs.len());
1096 let mut decoded_bytes = 0_usize;
1097 for input in inputs {
1098 let (shared_text, suffix) = input
1099 .split_once(':')
1100 .ok_or("compact rustc prediction input has no prefix length")?;
1101 let shared: usize = shared_text
1102 .parse()
1103 .map_err(|_| "compact rustc prediction prefix length is invalid")?;
1104 if shared.to_string() != shared_text
1105 || shared > expanded.last().map_or(0, String::len)
1106 || expanded
1107 .last()
1108 .is_some_and(|previous| !previous.is_char_boundary(shared))
1109 {
1110 return Err("compact rustc prediction prefix is not canonical");
1111 }
1112 let mut path = expanded
1113 .last()
1114 .map_or_else(String::new, |previous| previous[..shared].to_string());
1115 path.push_str(suffix);
1116 decoded_bytes = decoded_bytes
1117 .checked_add(path.len())
1118 .ok_or("compact rustc prediction is too large")?;
1119 if decoded_bytes > MAX_DECODED_PREDICTION_BYTES {
1120 return Err("compact rustc prediction is too large");
1121 }
1122 expanded.push(path);
1123 }
1124 Ok(expanded)
1125}
1126
1127fn is_zero(value: &u64) -> bool {
1128 *value == 0
1129}
1130
1131impl RustcInputPrediction {
1132 pub fn discover(
1135 &self,
1136 working_dir: &Path,
1137 path_mappings: &[PathMapping],
1138 digests: &dyn FileDigestCache,
1139 ) -> Result<DiscoveredInputs, BypassReason> {
1140 if !matches!(self.version, 2..=4) {
1141 return Err(BypassReason::UnsupportedPrediction);
1142 }
1143 if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
1144 return Err(BypassReason::UnsupportedPrediction);
1145 }
1146 let mut paths = BTreeSet::new();
1147 let mut named = BTreeSet::new();
1148 let admitted_roots = dep_info::native_input_roots(working_dir, path_mappings);
1149 let mut native_bytes = 0_u64;
1150 for path in &self.inputs {
1151 if self.version >= 3
1152 && let Some(path) = path.strip_prefix(NATIVE_DIRECTORY_PREDICTION_PREFIX)
1153 {
1154 let directory = denormalize_path(path, path_mappings)?;
1155 dep_info::collect_native_directory(
1156 &directory,
1157 &admitted_roots,
1158 &mut paths,
1159 &mut native_bytes,
1160 )?;
1161 } else {
1162 let path = denormalize_path(path, path_mappings)?;
1163 named.insert(path.clone());
1164 paths.insert(path);
1165 }
1166 }
1167 let native_only = paths.difference(&named).cloned().collect();
1168 let environment = self
1169 .environment
1170 .iter()
1171 .map(|name| {
1172 if name.is_empty() || name.contains(['=', '\0']) {
1173 return Err(BypassReason::UnsupportedPrediction);
1174 }
1175 let value = std::env::var_os(name)
1176 .map(|value| {
1177 value
1178 .into_string()
1179 .map_err(|_| BypassReason::UnsupportedPrediction)
1180 })
1181 .transpose()?;
1182 Ok((name.clone(), value))
1183 })
1184 .collect::<Result<BTreeMap<_, _>, _>>()?;
1185 Ok(
1186 DiscoveredInputs::from_paths(working_dir, paths, environment, digests)?
1187 .with_native_only(native_only),
1188 )
1189 }
1190}
1191
1192#[derive(Serialize)]
1193struct ActionDescriptor {
1194 version: u8,
1195 kind: &'static str,
1196 adapter_version: u8,
1197 compiler: CompilerDescriptor,
1198 arguments: Vec<String>,
1199 environment: BTreeMap<String, Option<String>>,
1200 inputs: Vec<InputDescriptor>,
1201 #[serde(skip_serializing_if = "Option::is_none")]
1204 linker: Option<LinkerIdentity>,
1205}
1206
1207#[derive(Serialize)]
1208struct InvocationDescriptor {
1209 version: u8,
1210 kind: &'static str,
1211 adapter_version: u8,
1212 compiler: CompilerDescriptor,
1213 arguments: Vec<String>,
1214 required_inputs: Vec<String>,
1215}
1216
1217#[derive(Serialize)]
1218struct CompilerDescriptor {
1219 toolchain: String,
1220 rustc_version: String,
1221 host: String,
1222 #[serde(skip_serializing_if = "Option::is_none")]
1223 driver: Option<String>,
1224}
1225
1226#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
1227struct InputDescriptor {
1228 path: String,
1229 digest: CacheDigest,
1230}
1231
1232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1234enum StaticLibraryNaming {
1235 Msvc,
1237 Unix,
1239}
1240
1241struct Parser<'a> {
1242 arguments: &'a [OsString],
1243 index: usize,
1244 parsed: Vec<Argument>,
1245 source: Option<PathBuf>,
1246 crate_types: Vec<String>,
1247 emits: Vec<Emit>,
1248 required_inputs: Vec<PathBuf>,
1249 native_libraries: Vec<String>,
1250 test: bool,
1251 crate_name: Option<String>,
1252 extra_filename: String,
1253 out_dir: Option<PathBuf>,
1254 explicit_output: Option<PathBuf>,
1255 target: Option<String>,
1256 sysroot_backend: Option<String>,
1257 options: ParseOptions,
1258}
1259
1260struct ExpandedArguments {
1261 arguments: Vec<OsString>,
1262}
1263
1264#[derive(Default)]
1265struct ResponseExpander {
1266 shell_argfiles: bool,
1267 next_is_unstable_option: bool,
1268 arguments: Vec<OsString>,
1269}
1270
1271impl ResponseExpander {
1272 fn push(&mut self, argument: String) {
1273 if self.next_is_unstable_option {
1274 self.shell_argfiles |= argument == "shell-argfiles";
1275 self.next_is_unstable_option = false;
1276 } else if let Some(option) = argument.strip_prefix("-Z") {
1277 if option.is_empty() {
1278 self.next_is_unstable_option = true;
1279 } else {
1280 self.shell_argfiles |= option == "shell-argfiles";
1281 }
1282 }
1283 self.arguments.push(argument.into());
1284 }
1285}
1286
1287fn expand_response_files(arguments: &[OsString]) -> Result<ExpandedArguments, BypassReason> {
1290 let mut expanded = ResponseExpander::default();
1291 for (index, argument) in arguments.iter().enumerate() {
1292 let argument = argument
1293 .to_str()
1294 .ok_or(BypassReason::NonUtf8Argument { index })?;
1295 let Some(argfile) = argument.strip_prefix('@') else {
1296 expanded.push(argument.to_string());
1297 continue;
1298 };
1299 let (path, shell) = match argfile.split_once(':') {
1300 Some(("shell", path)) if expanded.shell_argfiles => (path, true),
1301 _ => (argfile, false),
1302 };
1303 let contents = std::fs::read_to_string(path).map_err(|error| {
1304 BypassReason::ResponseFile(format!("{}: {error}", Path::new(path).display()))
1305 })?;
1306 if shell {
1307 let arguments = shlex::split(&contents).ok_or_else(|| {
1308 BypassReason::ResponseFile(format!(
1309 "invalid shell-style arguments in {}",
1310 Path::new(path).display()
1311 ))
1312 })?;
1313 for argument in arguments {
1314 expanded.push(argument);
1315 }
1316 } else {
1317 for argument in contents.lines() {
1318 expanded.push(argument.to_string());
1319 }
1320 }
1321 }
1322 Ok(ExpandedArguments {
1323 arguments: expanded.arguments,
1324 })
1325}
1326
1327impl<'a> Parser<'a> {
1328 fn new(arguments: &'a [OsString], options: ParseOptions) -> Self {
1329 Self {
1330 arguments,
1331 options,
1332 index: 0,
1333 parsed: Vec::new(),
1334 source: None,
1335 crate_types: Vec::new(),
1336 emits: Vec::new(),
1337 required_inputs: Vec::new(),
1338 native_libraries: Vec::new(),
1339 test: false,
1340 crate_name: None,
1341 extra_filename: String::new(),
1342 out_dir: None,
1343 explicit_output: None,
1344 target: None,
1345 sysroot_backend: None,
1346 }
1347 }
1348
1349 fn parse(mut self) -> Result<RustcInvocation, BypassReason> {
1350 while self.index < self.arguments.len() {
1351 let value = self.current()?.to_string();
1352 self.index += 1;
1353 if let Some(long) = value.strip_prefix("--") {
1354 self.parse_long(long)?;
1355 } else if value.starts_with('-') && value != "-" {
1356 self.parse_short(&value)?;
1357 } else {
1358 self.parse_input(&value)?;
1359 }
1360 }
1361
1362 if let Some(backend) = self.sysroot_backend.take()
1364 && self.parsed.iter().any(
1365 |argument| matches!(argument, Argument::Path { flag, .. } if flag == "--sysroot"),
1366 )
1367 {
1368 return Err(BypassReason::UnknownFlag(backend));
1369 }
1370 let source = self.source.clone().ok_or(BypassReason::MissingInput)?;
1371 let link_output = self.classify()?;
1372 self.require_native_libraries(link_output)?;
1373 let crate_name = self.crate_name.clone().map_or_else(
1374 || {
1375 source
1376 .file_stem()
1377 .and_then(|name| name.to_str())
1378 .map(|name| name.replace('-', "_"))
1379 .ok_or_else(|| BypassReason::NonUtf8Path(source.clone()))
1380 },
1381 Ok,
1382 )?;
1383 self.required_inputs.push(source.clone());
1384 Ok(RustcInvocation {
1385 arguments: self.parsed,
1386 source,
1387 required_inputs: self.required_inputs,
1388 crate_name,
1389 extra_filename: self.extra_filename,
1390 out_dir: self.out_dir,
1391 explicit_output: self.explicit_output,
1392 emits: self.emits,
1393 target: self.target,
1394 link_output,
1395 })
1396 }
1397
1398 fn current(&self) -> Result<&str, BypassReason> {
1399 self.arguments[self.index]
1400 .to_str()
1401 .ok_or(BypassReason::NonUtf8Argument { index: self.index })
1402 }
1403
1404 fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, BypassReason> {
1405 if let Some(value) = inline {
1406 if value.is_empty() {
1407 return Err(BypassReason::MissingValue(flag.into()));
1408 }
1409 return Ok(value.into());
1410 }
1411 if self.index >= self.arguments.len() {
1412 return Err(BypassReason::MissingValue(flag.into()));
1413 }
1414 let value = self.current()?.to_string();
1415 self.index += 1;
1416 Ok(value)
1417 }
1418
1419 fn parse_long(&mut self, value: &str) -> Result<(), BypassReason> {
1420 let (flag, inline) = value
1421 .split_once('=')
1422 .map_or((value, None), |(flag, value)| (flag, Some(value)));
1423 let rendered_flag = format!("--{flag}");
1424 match flag {
1425 "help" | "version" | "explain" | "print" => Err(BypassReason::CompilerQuery),
1426 "test" => {
1427 self.test = true;
1428 self.parsed.push(Argument::Plain(rendered_flag));
1429 Ok(())
1430 }
1431 "verbose" => {
1432 self.parsed.push(Argument::Plain(rendered_flag));
1433 Ok(())
1434 }
1435 "crate-name" => {
1436 let value = self.take_value(&rendered_flag, inline)?;
1437 self.crate_name = Some(value.clone());
1438 self.parsed
1439 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1440 Ok(())
1441 }
1442 "cfg" | "check-cfg" | "edition" | "error-format" | "json" | "color"
1443 | "diagnostic-width" | "remap-path-scope" | "allow" | "warn" | "force-warn"
1444 | "deny" | "forbid" | "cap-lints" => {
1445 let value = self.take_value(&rendered_flag, inline)?;
1446 self.parsed
1447 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1448 Ok(())
1449 }
1450 "target" => {
1451 let value = self.take_value(&rendered_flag, inline)?;
1452 let effective = self.target.is_none();
1456 if effective {
1457 self.target = Some(value.clone());
1458 }
1459 if value.ends_with(".json") || value.contains(['/', '\\']) {
1460 let path = PathBuf::from(value);
1461 if effective {
1462 self.required_inputs.push(path.clone());
1463 }
1464 self.parsed.push(Argument::Path {
1465 flag: rendered_flag,
1466 path,
1467 });
1468 } else {
1469 self.parsed
1470 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1471 }
1472 Ok(())
1473 }
1474 "crate-type" => {
1475 let value = self.take_value(&rendered_flag, inline)?;
1476 self.crate_types
1477 .extend(value.split(',').map(ToOwned::to_owned));
1478 self.parsed
1479 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1480 Ok(())
1481 }
1482 "emit" => {
1483 let value = self.take_value(&rendered_flag, inline)?;
1484 let emits = parse_emits(&value);
1485 self.emits.extend(emits.clone());
1486 self.parsed.push(Argument::Emit(emits));
1487 Ok(())
1488 }
1489 "out-dir" => {
1490 let path = PathBuf::from(self.take_value(&rendered_flag, inline)?);
1491 self.out_dir = Some(path.clone());
1492 self.parsed.push(Argument::Path {
1493 flag: rendered_flag,
1494 path,
1495 });
1496 Ok(())
1497 }
1498 "sysroot" => {
1499 let path = self.take_value(&rendered_flag, inline)?;
1500 self.parsed.push(Argument::Path {
1501 flag: rendered_flag,
1502 path: path.into(),
1503 });
1504 Ok(())
1505 }
1506 "extern" => {
1507 let value = self.take_value(&rendered_flag, inline)?;
1508 let (name, path) = value
1509 .split_once('=')
1510 .map_or((value.as_str(), None), |(name, path)| {
1511 (name, Some(PathBuf::from(path)))
1512 });
1513 if let Some(path) = &path {
1514 self.required_inputs.push(path.clone());
1515 }
1516 self.parsed.push(Argument::Extern {
1517 name: name.into(),
1518 path,
1519 });
1520 Ok(())
1521 }
1522 "remap-path-prefix" => {
1523 let value = self.take_value(&rendered_flag, inline)?;
1524 let Some((from, to)) = value.split_once('=') else {
1525 return Err(BypassReason::MissingValue(rendered_flag));
1526 };
1527 self.parsed.push(Argument::RemapPath {
1528 from: from.into(),
1529 to: to.into(),
1530 });
1531 Ok(())
1532 }
1533 "codegen" => {
1534 let value = self.take_value(&rendered_flag, inline)?;
1535 self.parse_codegen(&value)
1536 }
1537 "jobs-frontend" => {
1538 let value = self.take_value(&rendered_flag, inline)?;
1539 self.parsed
1540 .push(Argument::Plain(format!("{rendered_flag}={value}")));
1541 Ok(())
1542 }
1543 _ => Err(BypassReason::UnknownFlag(rendered_flag)),
1544 }
1545 }
1546
1547 fn parse_short(&mut self, value: &str) -> Result<(), BypassReason> {
1548 if let Some(attached) = value.strip_prefix("-Z") {
1549 let option = self.take_value("-Z", (!attached.is_empty()).then_some(attached))?;
1550 match option.as_str() {
1551 "embed-metadata=no"
1555 | "embed-metadata=yes"
1556 | "force-unstable-if-unmarked"
1557 | "shell-argfiles"
1558 | "unstable-options" => {
1559 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1560 return Ok(());
1561 }
1562 "threads" | "threads=" => {
1563 return Err(BypassReason::MissingValue("-Zthreads".into()));
1564 }
1565 _ => {}
1566 }
1567 if option.starts_with("threads=") {
1568 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1569 return Ok(());
1570 }
1571 if let Some(backend) = option.strip_prefix("codegen-backend=")
1573 && !backend.is_empty()
1574 && !backend.contains(['.', '/', '\\'])
1575 {
1576 self.sysroot_backend = Some(format!("-Z{option}"));
1577 self.parsed.push(Argument::Plain(format!("-Z{option}")));
1578 return Ok(());
1579 }
1580 return Err(BypassReason::UnknownFlag(format!("-Z{option}")));
1581 }
1582 match value {
1583 "-h" | "-V" | "-vV" => return Err(BypassReason::CompilerQuery),
1586 "-g" | "-O" | "-v" => {
1587 self.parsed.push(Argument::Plain(value.into()));
1588 return Ok(());
1589 }
1590 _ => {}
1591 }
1592 for (short, long) in [
1593 ("-A", "--allow"),
1594 ("-W", "--warn"),
1595 ("-D", "--deny"),
1596 ("-F", "--forbid"),
1597 ] {
1598 if let Some(attached) = value.strip_prefix(short) {
1599 let lint = self.take_value(short, (!attached.is_empty()).then_some(attached))?;
1600 self.parsed.push(Argument::Plain(format!("{long}={lint}")));
1601 return Ok(());
1602 }
1603 }
1604 if let Some(attached) = value.strip_prefix("-C") {
1605 let option = self.take_value("-C", (!attached.is_empty()).then_some(attached))?;
1606 return self.parse_codegen(&option);
1607 }
1608 if let Some(attached) = value.strip_prefix("-L") {
1609 let search = self.take_value("-L", (!attached.is_empty()).then_some(attached))?;
1610 let (kind, path) = search
1611 .split_once('=')
1612 .map_or(("all", search.as_str()), |(kind, path)| (kind, path));
1613 if !matches!(kind, "dependency" | "native") {
1614 return Err(BypassReason::UnsupportedSearchPath(kind.into()));
1615 }
1616 self.parsed.push(Argument::SearchPath {
1617 kind: kind.into(),
1618 path: path.into(),
1619 });
1620 return Ok(());
1621 }
1622 if let Some(attached) = value.strip_prefix("-l") {
1623 let library = self.take_value("-l", (!attached.is_empty()).then_some(attached))?;
1624 self.parsed.push(Argument::Plain(format!("-l{library}")));
1625 self.native_libraries.push(library);
1626 return Ok(());
1627 }
1628 if let Some(attached) = value.strip_prefix("-o") {
1629 let path = self.take_value("-o", (!attached.is_empty()).then_some(attached))?;
1630 self.explicit_output = Some(path.clone().into());
1631 self.parsed.push(Argument::Path {
1632 flag: "-o".into(),
1633 path: path.into(),
1634 });
1635 return Ok(());
1636 }
1637 Err(BypassReason::UnknownFlag(value.into()))
1638 }
1639
1640 fn parse_codegen(&mut self, value: &str) -> Result<(), BypassReason> {
1641 let name = value.split_once('=').map_or(value, |(name, _)| name);
1642 if name == "incremental" {
1643 return Err(BypassReason::Incremental);
1644 }
1645 if SUPPORTED_CODEGEN_OPTIONS.binary_search(&name).is_err() && name != "linker" {
1646 return Err(BypassReason::UnknownCodegenOption(name.into()));
1647 }
1648 if name == "linker-plugin-lto"
1654 && let Some((_, selection)) = value.split_once('=')
1655 && !matches!(
1656 selection,
1657 "" | "y" | "yes" | "on" | "true" | "n" | "no" | "off" | "false"
1658 )
1659 {
1660 return Err(BypassReason::UnknownCodegenOption(format!(
1661 "linker-plugin-lto={selection}"
1662 )));
1663 }
1664 if name == "link-arg"
1671 && let Some((_, option)) = value.split_once('=')
1672 && let Some(selection) = option.strip_prefix("-fuse-ld=")
1673 && !selection.is_empty()
1674 && !selection.contains(',')
1675 && !self.parsed.iter().any(
1676 |argument| matches!(argument, Argument::FuseLd(existing) if existing != selection),
1677 )
1678 {
1679 if !self.parsed.iter().any(
1680 |argument| matches!(argument, Argument::FuseLd(existing) if existing == selection),
1681 ) {
1682 self.parsed.push(Argument::FuseLd(selection.to_owned()));
1683 }
1684 return Ok(());
1685 }
1686 if name == "link-arg"
1689 && let Some(option) = value.strip_prefix("link-arg=-Wl,-install_name,@rpath/")
1690 && safe_install_name(option)
1691 {
1692 self.parsed.push(Argument::InstallName(option.to_owned()));
1693 return Ok(());
1694 }
1695 if matches!(name, "link-arg" | "link-args")
1700 && let Some((_, option)) = value.split_once('=')
1701 && let Some(prefix) = option.strip_prefix("-Wl,-oso_prefix,")
1702 && !prefix.trim_end_matches('/').is_empty()
1703 && !prefix.contains(',')
1704 {
1705 let trailing_slash = prefix.ends_with('/');
1706 self.parsed.push(Argument::OsoPrefix {
1707 path: PathBuf::from(prefix.trim_end_matches('/')),
1708 trailing_slash,
1709 });
1710 return Ok(());
1711 }
1712 self.parsed
1713 .push(Argument::Plain(format!("--codegen={value}")));
1714 if name == "extra-filename" {
1715 self.extra_filename = value
1716 .split_once('=')
1717 .map_or(String::new(), |(_, value)| value.to_string());
1718 }
1719 Ok(())
1720 }
1721
1722 fn require_native_libraries(&mut self, link_output: LinkOutput) -> Result<(), BypassReason> {
1737 for library in std::mem::take(&mut self.native_libraries) {
1738 if link_output != LinkOutput::Library {
1739 return Err(BypassReason::NativeLibrary(library));
1740 }
1741 let Some((kind, name)) = library.split_once('=') else {
1749 continue;
1750 };
1751 let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
1752 match kind {
1753 "static" => {}
1754 "dylib" | "framework" | "link-arg" => continue,
1755 _ => return Err(BypassReason::UnknownFlag(format!("-l{library}"))),
1756 }
1757 let name = name.split_once(':').map_or(name, |(_, renamed)| renamed);
1760 let (mut verbatim, mut bundle) = (false, true);
1761 for modifier in modifiers.split(',') {
1762 match modifier {
1763 "+verbatim" => verbatim = true,
1764 "-verbatim" => verbatim = false,
1765 "+bundle" => bundle = true,
1766 "-bundle" => bundle = false,
1767 _ => {}
1768 }
1769 }
1770 if !bundle {
1771 continue;
1772 }
1773 let archive = self
1774 .find_native_static_library(name, verbatim)
1775 .map_err(|()| BypassReason::CustomTargetNativeLibrary(library.clone()))?
1776 .ok_or_else(|| BypassReason::MissingNativeLibrary(library.clone()))?;
1777 self.required_inputs.push(archive);
1778 }
1779 Ok(())
1780 }
1781
1782 fn find_native_static_library(
1794 &self,
1795 name: &str,
1796 verbatim: bool,
1797 ) -> Result<Option<PathBuf>, ()> {
1798 let file_names = if verbatim {
1799 vec![name.to_owned()]
1800 } else {
1801 match self.static_library_naming() {
1802 Some(StaticLibraryNaming::Msvc) => {
1803 vec![format!("{name}.lib"), format!("lib{name}.a")]
1804 }
1805 Some(StaticLibraryNaming::Unix) => vec![format!("lib{name}.a")],
1806 None => return Err(()),
1807 }
1808 };
1809 Ok(self
1810 .parsed
1811 .iter()
1812 .filter_map(|argument| match argument {
1813 Argument::SearchPath { kind, path } if kind == "native" => Some(path),
1814 _ => None,
1815 })
1816 .flat_map(|directory| {
1817 file_names
1818 .iter()
1819 .map(move |file_name| directory.join(file_name))
1820 })
1821 .find(|candidate| candidate.is_file()))
1822 }
1823
1824 fn static_library_naming(&self) -> Option<StaticLibraryNaming> {
1832 let Some(target) = self.target.as_deref() else {
1833 return Some(if cfg!(any(target_env = "msvc", target_os = "uefi")) {
1834 StaticLibraryNaming::Msvc
1835 } else {
1836 StaticLibraryNaming::Unix
1837 });
1838 };
1839 if target.ends_with(".json")
1840 || target.contains(['/', '\\'])
1841 || self.options.custom_target_search
1842 {
1843 return None;
1844 }
1845 Some(if target.contains("msvc") || target.ends_with("-uefi") {
1848 StaticLibraryNaming::Msvc
1849 } else {
1850 StaticLibraryNaming::Unix
1851 })
1852 }
1853
1854 fn parse_input(&mut self, value: &str) -> Result<(), BypassReason> {
1855 if value == "-" {
1856 return Err(BypassReason::StandardInput);
1857 }
1858 if self.source.replace(value.into()).is_some() {
1859 return Err(BypassReason::MultipleInputs);
1860 }
1861 Ok(())
1862 }
1863
1864 fn classify(&self) -> Result<LinkOutput, BypassReason> {
1865 let never_links = !self.emits.iter().any(|emit| emit.kind == "link");
1874 let builds_a_library = !self.test
1875 && !self.crate_types.is_empty()
1876 && self
1877 .crate_types
1878 .iter()
1879 .all(|crate_type| matches!(crate_type.as_str(), "lib" | "rlib"));
1880 let link_output = if never_links || builds_a_library {
1881 LinkOutput::Library
1882 } else if self
1883 .target
1884 .as_deref()
1885 .is_some_and(compiler_bundled_wasm_target)
1886 && ((self.test && self.crate_types.is_empty())
1887 || matches!(self.crate_types.as_slice(), [kind] if kind == "bin" || kind == "cdylib"))
1888 {
1889 if self.parsed.iter().any(|argument| match argument {
1890 Argument::Plain(value) if value == "--codegen=link-self-contained" => false,
1891 Argument::Plain(value) if value.starts_with("--codegen=link-self-contained=") => {
1892 !matches!(
1893 value.rsplit_once('=').map(|(_, value)| value),
1894 Some("y" | "yes" | "on" | "true")
1895 )
1896 }
1897 _ => false,
1898 }) {
1899 return Err(BypassReason::UnknownCodegenOption(
1900 "link-self-contained".into(),
1901 ));
1902 }
1903 if self.target.as_deref().is_some_and(|target| target.contains("wasi"))
1904 && self.parsed.iter().any(|argument| {
1905 matches!(argument, Argument::Plain(value) if value.strip_prefix("--codegen=target-feature=").is_some_and(|features| features.split(',').any(|feature| feature == "-crt-static")))
1906 })
1907 {
1908 return Err(BypassReason::UnknownCodegenOption(
1909 "target-feature=-crt-static".into(),
1910 ));
1911 }
1912 LinkOutput::WasmExecutable
1916 } else if self.options.cache_native_links && self.links_a_native_artifact() {
1917 self.check_native_link_is_portable()?;
1918 if matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro") {
1919 LinkOutput::NativeProcMacro
1920 } else if matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib") {
1921 LinkOutput::NativeSharedLibrary
1922 } else {
1923 LinkOutput::NativeExecutable
1924 }
1925 } else if self.test {
1926 return Err(BypassReason::UnsupportedCrateType("test".into()));
1927 } else {
1928 return Err(BypassReason::UnsupportedCrateType(
1929 self.crate_types
1930 .iter()
1931 .find(|crate_type| !matches!(crate_type.as_str(), "lib" | "rlib"))
1932 .cloned()
1933 .unwrap_or_else(|| "bin".into()),
1934 ));
1935 };
1936 if link_output != LinkOutput::Library
1945 && let Some(option) = self.first_link_argument(link_output)
1946 {
1947 return Err(BypassReason::UnmodeledLinkArgument(option.to_owned()));
1948 }
1949 if self
1950 .parsed
1951 .iter()
1952 .any(|arg| matches!(arg, Argument::InstallName(_)))
1953 && !(cfg!(target_os = "macos") && link_output == LinkOutput::NativeProcMacro)
1954 {
1955 return Err(BypassReason::UnmodeledLinkArgument(
1956 "link-arg=-Wl,-install_name".into(),
1957 ));
1958 }
1959 if let Some(selection) = self.parsed.iter().find_map(|argument| {
1966 let Argument::FuseLd(selection) = argument else {
1967 return None;
1968 };
1969 Some(selection.as_str())
1970 }) && !matches!(
1971 link_output,
1972 LinkOutput::Library
1973 | LinkOutput::NativeExecutable
1974 | LinkOutput::NativeProcMacro
1975 | LinkOutput::NativeSharedLibrary
1976 ) {
1977 return Err(BypassReason::UnmodeledLinkArgument(format!(
1978 "link-arg=-fuse-ld={selection}"
1979 )));
1980 }
1981 if self.parsed.iter().any(|argument| {
1982 matches!(argument, Argument::Plain(value) if value.starts_with("--codegen=linker="))
1983 }) && !matches!(
1984 link_output,
1985 LinkOutput::NativeExecutable
1986 | LinkOutput::NativeProcMacro
1987 | LinkOutput::NativeSharedLibrary
1988 ) {
1989 return Err(BypassReason::UnknownCodegenOption("linker".into()));
1990 }
1991 if !matches!(
1995 link_output,
1996 LinkOutput::Library
1997 | LinkOutput::NativeExecutable
1998 | LinkOutput::NativeProcMacro
1999 | LinkOutput::NativeSharedLibrary
2000 ) && self
2001 .parsed
2002 .iter()
2003 .any(|argument| matches!(argument, Argument::OsoPrefix { .. }))
2004 {
2005 return Err(BypassReason::UnmodeledLinkArgument(
2006 "link-arg=-Wl,-oso_prefix".into(),
2007 ));
2008 }
2009 if let Some(name) = self.parsed.iter().find_map(|argument| match argument {
2010 Argument::Extern { name, path: None } if name != "proc_macro" => Some(name),
2011 _ => None,
2012 }) {
2013 return Err(BypassReason::UnresolvedExtern(name.clone()));
2014 }
2015 if let Some(emit) = self
2016 .emits
2017 .iter()
2018 .find(|emit| !matches!(emit.kind.as_str(), "dep-info" | "link" | "metadata"))
2019 {
2020 return Err(BypassReason::UnsupportedEmit(emit.kind.clone()));
2021 }
2022 if !self
2023 .emits
2024 .iter()
2025 .any(|emit| matches!(emit.kind.as_str(), "link" | "metadata"))
2026 {
2027 return Err(BypassReason::NoCacheableOutput);
2028 }
2029 Ok(link_output)
2030 }
2031}
2032
2033impl Parser<'_> {
2034 fn first_link_argument(&self, link_output: LinkOutput) -> Option<&str> {
2037 self.parsed.iter().find_map(|argument| {
2038 let Argument::Plain(value) = argument else {
2039 return None;
2040 };
2041 let option = value.strip_prefix("--codegen=")?;
2042 let (name, value) = option.split_once('=').unwrap_or((option, ""));
2043 (matches!(name, "link-arg" | "link-args")
2044 && !(name == "link-arg" && elf_keyword_is_modeled(link_output, value))
2045 && !msvc_options_are_modeled(link_output, name, value))
2046 .then_some(option)
2047 })
2048 }
2049
2050 fn oso_prefix_covers_outputs(&self) -> bool {
2059 let Some(directory) = self
2060 .out_dir
2061 .as_deref()
2062 .or_else(|| self.explicit_output.as_deref().and_then(Path::parent))
2063 else {
2064 return false;
2065 };
2066 if !directory.is_absolute() {
2067 return false;
2068 }
2069 let directory = normalize_components(directory);
2070 self.parsed.iter().any(|argument| {
2071 let Argument::OsoPrefix { path, .. } = argument else {
2072 return false;
2073 };
2074 path.is_absolute()
2075 && (directory.starts_with(normalize_components(path))
2076 || std::fs::canonicalize(&directory)
2077 .ok()
2078 .zip(std::fs::canonicalize(path).ok())
2079 .is_some_and(|(directory, prefix)| directory.starts_with(prefix)))
2080 })
2081 }
2082
2083 fn links_a_native_artifact(&self) -> bool {
2090 self.target.is_none()
2091 && self.emits.iter().any(|emit| emit.kind == "link")
2096 && ((self.test && self.crate_types.is_empty())
2097 || matches!(self.crate_types.as_slice(), [kind] if matches!(kind.as_str(), "bin" | "proc-macro"))
2098 || (cfg!(target_os = "linux")
2099 && matches!(self.crate_types.as_slice(), [kind] if kind == "cdylib")))
2100 }
2101
2102 fn check_native_link_is_portable(&self) -> Result<(), BypassReason> {
2109 for argument in &self.parsed {
2110 let Argument::Plain(value) = argument else {
2111 continue;
2112 };
2113 let (name, value) = if value == "-g" {
2116 ("debuginfo", Some("2"))
2117 } else if let Some(option) = value.strip_prefix("--codegen=") {
2118 match option.split_once('=') {
2119 Some((name, value)) => (name, Some(value)),
2120 None => (option, None),
2124 }
2125 } else {
2126 continue;
2127 };
2128 let unportable = match name {
2129 "split-debuginfo" => match value {
2135 Some("off") => false,
2136 Some("unpacked") if cfg!(target_os = "macos") => {
2137 !self.oso_prefix_covers_outputs()
2138 }
2139 _ => true,
2140 },
2141 "debuginfo" if cfg!(target_os = "macos") => {
2150 !matches!(value, Some("0" | "none")) && !self.oso_prefix_covers_outputs()
2151 }
2152 "rpath" => is_enabled(value),
2160 "prefer-dynamic" => {
2161 is_enabled(value)
2162 && !matches!(self.crate_types.as_slice(), [kind] if kind == "proc-macro")
2163 }
2164 "link-self-contained" => true,
2167 _ => false,
2168 };
2169 if unportable {
2170 return Err(BypassReason::UnportableNativeLink(match value {
2171 Some(value) => format!("{name}={value}"),
2172 None => name.to_owned(),
2173 }));
2174 }
2175 }
2176 Ok(())
2177 }
2178}
2179
2180fn safe_install_name(name: &str) -> bool {
2181 !name.is_empty()
2182 && name
2183 .bytes()
2184 .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
2185}
2186
2187fn is_enabled(value: Option<&str>) -> bool {
2190 matches!(value, None | Some("y" | "yes" | "on" | "true"))
2191}
2192
2193fn elf_keyword_is_modeled(link_output: LinkOutput, value: &str) -> bool {
2203 cfg!(target_os = "linux")
2204 && matches!(
2205 link_output,
2206 LinkOutput::NativeExecutable
2207 | LinkOutput::NativeProcMacro
2208 | LinkOutput::NativeSharedLibrary
2209 )
2210 && value.strip_prefix("-Wl,-z,").is_some_and(|keyword| {
2211 matches!(
2212 keyword,
2213 "defs"
2214 | "lazy"
2215 | "nodelete"
2216 | "nodlopen"
2217 | "noexecstack"
2218 | "norelro"
2219 | "now"
2220 | "origin"
2221 | "relro"
2222 )
2223 })
2224}
2225
2226fn msvc_options_are_modeled(link_output: LinkOutput, name: &str, value: &str) -> bool {
2242 cfg!(target_env = "msvc")
2243 && matches!(
2244 link_output,
2245 LinkOutput::NativeExecutable
2246 | LinkOutput::NativeProcMacro
2247 | LinkOutput::NativeSharedLibrary
2248 )
2249 && match name {
2250 "link-arg" => msvc_option_reads_no_file(value),
2251 "link-args" => {
2254 let mut options = value.split_ascii_whitespace().peekable();
2255 options.peek().is_some() && options.all(msvc_option_reads_no_file)
2256 }
2257 _ => false,
2258 }
2259}
2260
2261fn msvc_option_reads_no_file(option: &str) -> bool {
2265 let Some(option) = option.strip_prefix(['/', '-']) else {
2266 return false;
2267 };
2268 if option.eq_ignore_ascii_case("brepro") {
2269 return true;
2270 }
2271 let Some((name, sizes)) = option.split_once(':') else {
2272 return false;
2273 };
2274 let is_size = |size: &str| match size.strip_prefix("0x").or_else(|| size.strip_prefix("0X")) {
2277 Some(hex) => !hex.is_empty() && hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
2278 None => !size.is_empty() && size.bytes().all(|byte| byte.is_ascii_digit()),
2279 };
2280 name.eq_ignore_ascii_case("stack")
2281 && sizes.split(',').count() <= 2
2282 && sizes.split(',').all(is_size)
2283}
2284
2285fn compiler_bundled_wasm_target(target: &str) -> bool {
2286 COMPILER_BUNDLED_WASM_TARGETS.binary_search(&target).is_ok()
2287}
2288
2289fn parse_emits(value: &str) -> Vec<Emit> {
2290 value
2291 .split(',')
2292 .map(|emit| {
2293 let (kind, path) = emit
2294 .split_once('=')
2295 .map_or((emit, None), |(kind, path)| (kind, Some(path.into())));
2296 Emit {
2297 kind: kind.into(),
2298 path,
2299 }
2300 })
2301 .collect()
2302}
2303
2304struct ActionBuilder<'a> {
2305 invocation: &'a RustcInvocation,
2306 context: ActionContext,
2307 mappings: Vec<SharedPathMapping>,
2308 aliases: PathAliases,
2312 linker: Option<LinkerIdentity>,
2313}
2314
2315impl<'a> ActionBuilder<'a> {
2316 fn new(invocation: &'a RustcInvocation, mut context: ActionContext) -> Self {
2317 context.path_mappings = PathMapping::ordered(&context.path_mappings);
2318 let mappings = resolve_path_mappings(&shared_path_mappings(&context.path_mappings));
2319 Self {
2320 linker: None,
2321 invocation,
2322 mappings,
2323 aliases: PathAliases::default(),
2324 context,
2325 }
2326 }
2327
2328 fn linked_by(mut self, linker: Option<LinkerIdentity>) -> Self {
2329 self.linker = linker;
2330 self
2331 }
2332
2333 fn build(self) -> Result<RustcAction, BypassReason> {
2334 self.validate_mappings()?;
2335 let invocation = self.invocation_descriptor()?;
2336 let environment = self.environment_descriptor()?;
2337
2338 let mut inputs = BTreeMap::<String, CacheDigest>::new();
2339 for input in &self.context.inputs {
2340 input
2341 .digest
2342 .validate()
2343 .map_err(|_| BypassReason::InvalidInputDigest(input.path.display().to_string()))?;
2344 let path = self.normalize_input_path(&input.path)?;
2345 if inputs
2346 .insert(path.clone(), input.digest.clone())
2347 .is_some_and(|existing| existing != input.digest)
2348 {
2349 return Err(BypassReason::ConflictingInput(path));
2350 }
2351 }
2352 let required = self
2353 .invocation
2354 .required_inputs
2355 .iter()
2356 .map(|path| self.normalize_input_path(path))
2357 .collect::<Result<BTreeSet<_>, _>>()?;
2358 if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
2359 return Err(BypassReason::MissingRequiredInput(missing.clone()));
2360 }
2361 let inputs = inputs
2362 .into_iter()
2363 .map(|(path, digest)| InputDescriptor { path, digest })
2364 .collect();
2365 if self.invocation.links_natively() && self.linker.is_none() {
2368 return Err(BypassReason::UnportableNativeLink(
2369 "linker identity is unknown".into(),
2370 ));
2371 }
2372 let descriptor = ActionDescriptor {
2373 version: ACTION_SCHEMA_VERSION,
2374 kind: "rustc",
2375 adapter_version: ADAPTER_VERSION,
2376 compiler: invocation.compiler,
2377 arguments: invocation.arguments,
2378 environment,
2379 inputs,
2380 linker: self.linker.clone(),
2381 };
2382 let bytes = canonical_json(&descriptor)
2383 .map_err(|error| BypassReason::Serialization(error.to_string()))?;
2384 let digest = CacheDigest::blake3(&bytes);
2385 Ok(RustcAction { digest, bytes })
2386 }
2387
2388 fn invocation_descriptor(&self) -> Result<InvocationDescriptor, BypassReason> {
2389 self.validate_mappings()?;
2390 let arguments = self
2391 .invocation
2392 .arguments
2393 .iter()
2394 .map(|argument| self.normalize_argument(argument))
2395 .collect::<Result<Vec<_>, _>>()?;
2396 let required_inputs = self
2397 .invocation
2398 .required_inputs
2399 .iter()
2400 .map(|path| self.normalize_path(path))
2401 .collect::<Result<BTreeSet<_>, _>>()?
2402 .into_iter()
2403 .collect();
2404 Ok(InvocationDescriptor {
2405 version: ACTION_SCHEMA_VERSION,
2406 kind: "rustc",
2407 adapter_version: ADAPTER_VERSION,
2408 compiler: CompilerDescriptor {
2409 toolchain: self.context.compiler.toolchain.clone(),
2410 rustc_version: self.context.compiler.rustc_version.clone(),
2411 host: self.context.compiler.host.clone(),
2412 driver: self.context.compiler.driver.clone(),
2413 },
2414 arguments,
2415 required_inputs,
2416 })
2417 }
2418
2419 fn validate_mappings(&self) -> Result<(), BypassReason> {
2420 if !self.context.working_dir.is_absolute() {
2421 return Err(BypassReason::RelativeWorkingDirectory(
2422 self.context.working_dir.clone(),
2423 ));
2424 }
2425 let mut roots = BTreeSet::new();
2426 let mut placeholders = BTreeSet::new();
2427 for mapping in &self.mappings {
2428 if !mapping.root.is_absolute() {
2429 return Err(BypassReason::RelativePathMapping(mapping.root.clone()));
2430 }
2431 if mapping.placeholder.is_empty()
2432 || !mapping
2433 .placeholder
2434 .bytes()
2435 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
2436 || !roots.insert(normalize_components(&mapping.root))
2437 || !placeholders.insert(&mapping.placeholder)
2438 {
2439 return Err(BypassReason::InvalidPathPlaceholder(
2440 mapping.placeholder.clone(),
2441 ));
2442 }
2443 }
2444 Ok(())
2445 }
2446
2447 fn normalize_argument(&self, argument: &Argument) -> Result<String, BypassReason> {
2448 match argument {
2449 Argument::Plain(value) => Ok(value.clone()),
2450 Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
2451 Argument::SearchPath { kind, path } => {
2452 let text = self.normalize_path(path)?;
2453 Ok(format!("-L{kind}={text}"))
2454 }
2455 Argument::Extern { name, path } => match path {
2456 Some(path) => Ok(format!("--extern={name}={}", self.normalize_path(path)?)),
2457 None => Ok(format!("--extern={name}")),
2458 },
2459 Argument::Emit(emits) => Ok(format!(
2460 "--emit={}",
2461 emits
2462 .iter()
2463 .map(|emit| match &emit.path {
2464 Some(path) => self
2465 .normalize_path(path)
2466 .map(|path| format!("{}={path}", emit.kind)),
2467 None => Ok(emit.kind.clone()),
2468 })
2469 .collect::<Result<Vec<_>, _>>()?
2470 .join(",")
2471 )),
2472 Argument::RemapPath { from, to } => Ok(format!(
2473 "--remap-path-prefix={}={}",
2474 self.normalize_path(from)?,
2475 to
2476 )),
2477 Argument::InstallName(name) => Ok(format!(
2478 "--codegen=link-arg=-Wl,-install_name,@rpath/{name}"
2479 )),
2480 Argument::OsoPrefix {
2481 path,
2482 trailing_slash,
2483 } => Ok(format!(
2484 "--codegen=link-arg=-Wl,-oso_prefix,{}{}",
2485 self.normalize_path(path)?,
2486 if *trailing_slash { "/" } else { "" }
2487 )),
2488 Argument::FuseLd(selection) => Ok(format!("--codegen=link-arg=-fuse-ld={selection}")),
2489 }
2490 }
2491
2492 fn environment_descriptor(&self) -> Result<BTreeMap<String, Option<String>>, BypassReason> {
2499 self.context
2500 .environment
2501 .iter()
2502 .map(|(name, value)| {
2503 let value = match value {
2504 Some(value) if self.context.portable_environment.contains(name) => {
2505 Some(self.normalize_path(Path::new(value))?)
2506 }
2507 value => value.clone(),
2508 };
2509 Ok((name.clone(), value))
2510 })
2511 .collect()
2512 }
2513
2514 fn normalize_input_path(&self, path: &Path) -> Result<String, BypassReason> {
2515 let absolute = normalize_components(&self.context.working_dir.join(path));
2516 for directory in self.invocation.native_search_paths() {
2517 let directory = normalize_components(&self.context.working_dir.join(directory));
2518 if let Ok(relative) = absolute.strip_prefix(&directory) {
2519 let suffix = relative
2525 .components()
2526 .map(|component| {
2527 component
2528 .as_os_str()
2529 .to_str()
2530 .ok_or_else(|| BypassReason::NonUtf8Path(path.to_path_buf()))
2531 })
2532 .collect::<Result<Vec<_>, _>>()?
2533 .join("/");
2534 if !suffix.is_empty() {
2535 return Ok(format!("{}/{}", self.normalize_path(&directory)?, suffix));
2536 }
2537 }
2538 }
2539 self.normalize_path(path)
2540 }
2541
2542 fn normalize_path(&self, path: &Path) -> Result<String, BypassReason> {
2543 normalize_resolved_shared_path_with(
2544 &self.aliases,
2545 path,
2546 &self.context.working_dir,
2547 &self.mappings,
2548 )
2549 .map_err(Into::into)
2550 }
2551}
2552
2553fn under_any_directory(path: &str, directories: &BTreeSet<String>) -> bool {
2557 directories.iter().any(|directory| {
2558 path.len() > directory.len()
2559 && path.as_bytes()[directory.len()] == b'/'
2560 && path.starts_with(directory)
2561 })
2562}
2563
2564fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, BypassReason> {
2565 for mapping in mappings {
2566 let prefix = format!("${{{}}}", mapping.placeholder);
2567 let suffix = if value == prefix {
2568 ""
2569 } else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
2570 suffix
2571 } else {
2572 continue;
2573 };
2574 if !mapping.root.is_absolute()
2575 || (!suffix.is_empty()
2576 && suffix.split('/').any(|component| {
2577 component.is_empty()
2578 || matches!(component, "." | "..")
2579 || component.contains('\\')
2580 }))
2581 {
2582 return Err(BypassReason::InvalidPredictedInput(value.into()));
2583 }
2584 let mut path = normalize_components(&mapping.root);
2585 path.extend(suffix.split('/').filter(|component| !component.is_empty()));
2586 return Ok(path);
2587 }
2588 Err(BypassReason::InvalidPredictedInput(value.into()))
2589}
2590
2591fn normalize_components(path: &Path) -> PathBuf {
2592 let mut normalized = PathBuf::new();
2593 for component in path.components() {
2594 match component {
2595 Component::CurDir => {}
2596 Component::ParentDir => {
2597 normalized.pop();
2598 }
2599 component => normalized.push(component.as_os_str()),
2600 }
2601 }
2602 normalized
2603}
2604
2605fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
2606 if path.is_absolute() {
2607 normalize_components(path)
2608 } else {
2609 normalize_components(&working_dir.join(path))
2610 }
2611}
2612
2613#[cfg(test)]
2614#[path = "rustc_cache_tests.rs"]
2615mod tests;