1use crate::{
4 CcActionContext, CcActionInput, CcBypassReason, CcCompilerFamily, MAX_INPUT_BYTES,
5 MAX_MANIFEST_ENTRIES, MAX_PREDICTED_INPUTS, normalize_components,
6};
7use mbx_cache_core::{
8 CacheDigest, FileDigestCache, FileDigestResolution, FileDigestScope, FileIdentity,
9 FileSnapshot, RecordedFileDigest, digest_file, digest_file_validated,
10};
11use std::collections::{BTreeMap, BTreeSet};
12use std::io::Read;
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16pub const INCLUDE_MANIFEST_PREFIX: &str = "@include-manifest:";
18
19const ASSEMBLER_INPUT_DIRECTIVES: &[&[u8]] = &[b".include", b".incbin", b".sinclude"];
22
23const SCAN_CHUNK_BYTES: usize = 64 * 1024;
24
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
27pub struct CcDepfile {
28 pub files: Vec<PathBuf>,
30}
31
32impl CcDepfile {
33 pub fn read(path: &Path) -> Result<Self, CcBypassReason> {
35 let contents =
36 std::fs::read_to_string(path).map_err(|error| CcBypassReason::DepfileRead {
37 path: path.to_path_buf(),
38 message: error.to_string(),
39 })?;
40 Self::parse(&contents)
41 }
42
43 pub fn read_for(path: &Path, family: CcCompilerFamily) -> Result<Self, CcBypassReason> {
45 if family.is_msvc() {
46 Self::read_msvc(path)
47 } else {
48 Self::read(path)
49 }
50 }
51
52 pub fn read_msvc(path: &Path) -> Result<Self, CcBypassReason> {
54 let contents = std::fs::read(path).map_err(|error| CcBypassReason::DepfileRead {
55 path: path.to_path_buf(),
56 message: error.to_string(),
57 })?;
58 let value: serde_json::Value = serde_json::from_slice(&contents)
59 .map_err(|error| CcBypassReason::MalformedDepfile(error.to_string()))?;
60 let data = value
61 .get("Data")
62 .and_then(serde_json::Value::as_object)
63 .ok_or_else(|| CcBypassReason::MalformedDepfile("missing Data object".into()))?;
64 if data
65 .get("ImportedModules")
66 .and_then(serde_json::Value::as_array)
67 .is_some_and(|modules| !modules.is_empty())
68 || data.get("ProvidedModule").is_some_and(|module| {
69 !module.is_null() && module.as_str().is_none_or(|s| !s.is_empty())
70 })
71 {
72 return Err(CcBypassReason::MalformedDepfile(
73 "C++ module dependencies are not modeled".into(),
74 ));
75 }
76 let includes = data
77 .get("Includes")
78 .and_then(serde_json::Value::as_array)
79 .ok_or_else(|| CcBypassReason::MalformedDepfile("missing Includes array".into()))?;
80 let files = includes
81 .iter()
82 .map(|entry| {
83 entry.as_str().map(PathBuf::from).ok_or_else(|| {
84 CcBypassReason::MalformedDepfile("non-string include path".into())
85 })
86 })
87 .collect::<Result<Vec<_>, _>>()?;
88 Ok(Self { files })
89 }
90
91 pub fn parse(contents: &str) -> Result<Self, CcBypassReason> {
97 let joined = join_continuations(contents)?;
98 let (_, prerequisites) = joined
99 .lines()
100 .find_map(|line| line.split_once(RULE_SEPARATOR))
101 .ok_or_else(|| CcBypassReason::MalformedDepfile("no dependency rule".into()))?;
102 let files = split_prerequisites(prerequisites)?;
103 Ok(Self { files })
104 }
105}
106
107const RULE_SEPARATOR: &str = ": ";
108
109impl CcDepfile {
110 pub fn render(
120 targets: &[crate::DepfileTarget],
121 files: &[PathBuf],
122 source: &Path,
123 phony_targets: bool,
124 ) -> String {
125 let mut rendered = String::new();
126 for (index, target) in targets.iter().enumerate() {
127 if index > 0 {
128 rendered.push(' ');
129 }
130 if target.quoted {
131 rendered.push_str(&escape_make_word(&target.name));
132 } else {
133 rendered.push_str(&target.name);
134 }
135 }
136 rendered.push(':');
137 for file in files {
138 rendered.push_str(" \\\n ");
139 rendered.push_str(&escape_make_word(&file.to_string_lossy()));
140 }
141 rendered.push('\n');
142 if phony_targets {
143 for file in files.iter().filter(|file| file.as_path() != source) {
144 rendered.push_str(&escape_make_word(&file.to_string_lossy()));
145 rendered.push_str(":\n");
146 }
147 }
148 rendered
149 }
150}
151
152fn escape_make_word(word: &str) -> String {
154 let mut escaped = String::with_capacity(word.len());
155 for character in word.chars() {
156 match character {
157 ' ' => escaped.push_str("\\ "),
158 '#' => escaped.push_str("\\#"),
159 '$' => escaped.push_str("$$"),
160 other => escaped.push(other),
161 }
162 }
163 escaped
164}
165
166fn join_continuations(contents: &str) -> Result<String, CcBypassReason> {
168 let mut joined = String::with_capacity(contents.len());
169 let mut continued = false;
170 for line in contents.lines() {
171 let trimmed = line.strip_suffix('\r').unwrap_or(line);
172 let (text, continues) = match trimmed.strip_suffix('\\') {
173 Some(text) => (text, true),
174 None => (trimmed, false),
175 };
176 if continued {
177 joined.push(' ');
178 }
179 joined.push_str(text.trim_end_matches(['\t']));
180 if !continues {
181 joined.push('\n');
182 }
183 continued = continues;
184 }
185 if continued {
186 return Err(CcBypassReason::MalformedDepfile(
187 "unterminated line continuation".into(),
188 ));
189 }
190 Ok(joined)
191}
192
193fn split_prerequisites(value: &str) -> Result<Vec<PathBuf>, CcBypassReason> {
198 let mut files = Vec::new();
199 let mut current = String::new();
200 let mut characters = value.chars().peekable();
201 while let Some(character) = characters.next() {
202 match character {
203 ' ' | '\t' => {
204 if !current.is_empty() {
205 files.push(PathBuf::from(std::mem::take(&mut current)));
206 }
207 }
208 '\\' => match characters.next() {
209 Some(' ') => current.push(' '),
210 Some('#') => current.push('#'),
211 Some(other) => {
212 return Err(CcBypassReason::MalformedDepfile(format!(
213 "unmodeled escape \\{other}"
214 )));
215 }
216 None => {
217 return Err(CcBypassReason::MalformedDepfile(
218 "trailing escape character".into(),
219 ));
220 }
221 },
222 '$' => match characters.next() {
223 Some('$') => current.push('$'),
224 Some(other) => {
225 return Err(CcBypassReason::MalformedDepfile(format!(
226 "unmodeled variable reference ${other}"
227 )));
228 }
229 None => {
230 return Err(CcBypassReason::MalformedDepfile(
231 "trailing variable reference".into(),
232 ));
233 }
234 },
235 other => current.push(other),
236 }
237 }
238 if !current.is_empty() {
239 files.push(PathBuf::from(current));
240 }
241 Ok(files)
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct CcDiscoveredInputs {
247 working_dir: PathBuf,
248 pub inputs: Vec<CcActionInput>,
250 identities: Vec<Option<FileIdentity>>,
254}
255
256impl CcDiscoveredInputs {
257 pub fn collect(
264 working_dir: &Path,
265 files: BTreeSet<PathBuf>,
266 directories: BTreeSet<PathBuf>,
267 digests: &dyn FileDigestCache,
268 ) -> Result<Self, CcBypassReason> {
269 if !working_dir.is_absolute() {
270 return Err(CcBypassReason::RelativeWorkingDirectory(
271 working_dir.to_path_buf(),
272 ));
273 }
274 let directories = minimal_manifest_directories(directories);
275 if files.len() + directories.len() > MAX_PREDICTED_INPUTS {
276 return Err(CcBypassReason::TooManyInputs);
277 }
278 let working_dir = normalize_components(working_dir);
279 let mut inputs = Vec::with_capacity(files.len() + directories.len());
280 let mut total_bytes = 0_u64;
281 let mut identified = Vec::with_capacity(files.len());
288 for path in files {
289 let metadata = std::fs::metadata(&path).map_err(|error| CcBypassReason::InputRead {
290 path: path.clone(),
291 message: error.to_string(),
292 })?;
293 if !metadata.is_file() {
294 return Err(CcBypassReason::InputRead {
295 path,
296 message: "input is not a regular file".into(),
297 });
298 }
299 total_bytes = total_bytes.saturating_add(metadata.len());
300 if total_bytes > MAX_INPUT_BYTES {
301 return Err(CcBypassReason::TooManyInputs);
302 }
303 let identity = FileIdentity::for_digest_cache(&path, &metadata).map_err(|error| {
304 CcBypassReason::InputRead {
305 path: path.clone(),
306 message: error.to_string(),
307 }
308 })?;
309 identified.push((path, identity));
310 }
311 let queries = identified
312 .iter()
313 .filter_map(|(_, identity)| identity.clone())
314 .collect::<Vec<_>>();
315 let mut recorded = digests
316 .resolve(FileDigestScope::CcInput, &queries)
317 .into_iter();
318 let mut identities = Vec::with_capacity(inputs.capacity());
319 let mut fresh = Vec::new();
320 for (path, identity) in identified {
321 let resolution = identity
322 .as_ref()
323 .and_then(|_| recorded.next())
324 .unwrap_or(FileDigestResolution::Unresolved);
325 let cached = match resolution {
326 FileDigestResolution::Digest(digest)
327 if identity.as_ref().is_some_and(|identity| {
328 identity.len == digest.size && identity.still_describes().unwrap_or(false)
329 }) =>
330 {
331 Some(Ok(digest))
332 }
333 FileDigestResolution::EmbeddedTimestampMacro
334 if identity
335 .as_ref()
336 .is_some_and(|identity| identity.still_describes().unwrap_or(false)) =>
337 {
338 Some(Err(CcBypassReason::EmbeddedTimestampMacro(path.clone())))
339 }
340 FileDigestResolution::Digest(_)
341 | FileDigestResolution::EmbeddedTimestampMacro
342 | FileDigestResolution::Unresolved => None,
343 };
344 let digest = if let Some(cached) = cached {
345 identities.push(identity);
346 cached?
347 } else {
348 let observed =
349 digest_file_validated(FileDigestScope::CcInput, &path).map_err(|error| {
350 CcBypassReason::InputRead {
351 path: path.clone(),
352 message: error.to_string(),
353 }
354 })?;
355 let (resolution, observed_identity) = if let Some(observed) = observed {
356 let identity = observed.cache_identity.clone();
357 if let FileDigestResolution::Digest(digest) = &observed.resolution
358 && let Some(file) = observed.cache_identity
359 && file.len == digest.size
360 {
361 fresh.push(RecordedFileDigest {
362 file,
363 digest: digest.clone(),
364 });
365 }
366 (observed.resolution, identity)
367 } else {
368 let resolution =
369 digest_file(FileDigestScope::CcInput, &path).map_err(|error| {
370 CcBypassReason::InputRead {
371 path: path.clone(),
372 message: error.to_string(),
373 }
374 })?;
375 (resolution, None)
376 };
377 let FileDigestResolution::Digest(digest) = resolution else {
378 return Err(CcBypassReason::EmbeddedTimestampMacro(path));
379 };
380 identities.push(observed_identity);
381 digest
382 };
383 inputs.push(CcActionInput { path, digest });
384 }
385 if !fresh.is_empty() {
386 digests.record(FileDigestScope::CcInput, fresh);
387 }
388 let mut manifest_entries = 0_usize;
389 for directory in directories {
390 let digest = include_manifest(&directory, &mut manifest_entries)?;
391 inputs.push(CcActionInput {
392 path: PathBuf::from(format!("{INCLUDE_MANIFEST_PREFIX}{}", directory.display())),
393 digest,
394 });
395 identities.push(None);
396 }
397 Ok(Self {
398 working_dir,
399 inputs,
400 identities,
401 })
402 }
403
404 pub fn files(&self) -> impl Iterator<Item = &CcActionInput> {
406 self.inputs
407 .iter()
408 .filter(|input| !is_manifest_input(&input.path))
409 }
410
411 pub fn verify_not_modified_since(&self, started_at: SystemTime) -> Result<(), CcBypassReason> {
418 self.verify_not_modified_since_with_snapshots(started_at, &BTreeMap::new())
419 }
420
421 pub fn verify_not_modified_since_with_snapshots(
424 &self,
425 started_at: SystemTime,
426 before: &BTreeMap<PathBuf, FileSnapshot>,
427 ) -> Result<(), CcBypassReason> {
428 for input in self.files() {
429 if let Some(previous) = before.get(&input.path)
430 && previous.proves_content_change()
431 {
432 let metadata =
433 std::fs::metadata(&input.path).map_err(|error| CcBypassReason::InputRead {
434 path: input.path.clone(),
435 message: error.to_string(),
436 })?;
437 let identity = FileIdentity::for_digest_cache(&input.path, &metadata)
438 .map_err(|error| CcBypassReason::InputRead {
439 path: input.path.clone(),
440 message: error.to_string(),
441 })?
442 .or_else(|| FileIdentity::describe(&input.path, &metadata));
443 if previous.matches(identity.as_ref(), &input.digest) {
444 continue;
445 }
446 return Err(CcBypassReason::InputModifiedDuringCompilation(
447 input.path.clone(),
448 ));
449 }
450 let metadata =
451 std::fs::metadata(&input.path).map_err(|error| CcBypassReason::InputRead {
452 path: input.path.clone(),
453 message: error.to_string(),
454 })?;
455 let modified = metadata
456 .modified()
457 .map_err(|error| CcBypassReason::InputRead {
458 path: input.path.clone(),
459 message: error.to_string(),
460 })?;
461 if modified >= started_at {
462 return Err(CcBypassReason::InputModifiedDuringCompilation(
463 input.path.clone(),
464 ));
465 }
466 }
467 Ok(())
468 }
469
470 pub fn verify_not_modified_since_with_identities(
472 &self,
473 started_at: SystemTime,
474 before: &BTreeMap<PathBuf, FileIdentity>,
475 ) -> Result<(), CcBypassReason> {
476 let snapshots = before
477 .iter()
478 .map(|(path, identity)| (path.clone(), identity.clone().into()))
479 .collect();
480 self.verify_not_modified_since_with_snapshots(started_at, &snapshots)
481 }
482
483 pub fn verify(&self) -> Result<(), CcBypassReason> {
492 for (index, input) in self.inputs.iter().enumerate() {
493 if is_manifest_input(&input.path) {
494 continue;
495 }
496 let read_error = |error: std::io::Error| CcBypassReason::InputRead {
497 path: input.path.clone(),
498 message: error.to_string(),
499 };
500 if let Some(Some(identity)) = self.identities.get(index)
501 && identity.can_skip_content_verification()
502 && identity.still_describes().map_err(read_error)?
503 {
504 continue;
505 }
506 let matches = input.digest.matches_file(&input.path).map_err(|error| {
507 CcBypassReason::InputRead {
508 path: input.path.clone(),
509 message: error.to_string(),
510 }
511 })?;
512 if !matches {
513 return Err(CcBypassReason::InputChanged(input.path.clone()));
514 }
515 }
516 Ok(())
517 }
518
519 pub fn apply_to(self, context: &mut CcActionContext) -> Result<(), CcBypassReason> {
522 if normalize_components(&context.working_dir) != self.working_dir {
523 return Err(CcBypassReason::DiscoveryWorkingDirectory);
524 }
525 context.inputs.extend(self.inputs);
526 Ok(())
527 }
528}
529
530fn minimal_manifest_directories(directories: BTreeSet<PathBuf>) -> Vec<PathBuf> {
538 let mut directories = directories
539 .into_iter()
540 .map(|directory| {
541 let normalized = manifest_directory_identity(&directory);
542 (directory, normalized)
543 })
544 .collect::<Vec<_>>();
545 directories.sort_by(|(left, left_normalized), (right, right_normalized)| {
546 left_normalized
547 .as_deref()
548 .unwrap_or(left)
549 .components()
550 .count()
551 .cmp(
552 &right_normalized
553 .as_deref()
554 .unwrap_or(right)
555 .components()
556 .count(),
557 )
558 .then_with(|| left_normalized.cmp(right_normalized))
559 .then_with(|| left.cmp(right))
560 });
561
562 let mut minimal = Vec::<(PathBuf, Option<PathBuf>)>::new();
563 for (directory, normalized) in directories {
564 if !minimal
565 .iter()
566 .any(|(_, ancestor)| match (ancestor, &normalized) {
567 (Some(ancestor), Some(normalized)) => manifest_covers(ancestor, normalized),
568 _ => false,
569 })
570 {
571 minimal.push((directory, normalized));
572 }
573 }
574 minimal
575 .into_iter()
576 .map(|(directory, _)| directory)
577 .collect()
578}
579
580fn manifest_directory_identity(directory: &Path) -> Option<PathBuf> {
584 let mut normalized = PathBuf::new();
585 for component in directory.components() {
586 match component {
587 std::path::Component::CurDir => {}
588 std::path::Component::ParentDir => {
589 let metadata = std::fs::symlink_metadata(&normalized).ok()?;
590 if !metadata.is_dir() || metadata.file_type().is_symlink() {
591 return None;
592 }
593 normalized.pop();
594 }
595 component => normalized.push(component.as_os_str()),
596 }
597 }
598 Some(normalized)
599}
600
601fn manifest_covers(ancestor: &Path, descendant: &Path) -> bool {
608 let Ok(relative) = descendant.strip_prefix(ancestor) else {
609 return false;
610 };
611 if relative.as_os_str().is_empty() {
612 return true;
613 }
614 let mut current = ancestor.to_path_buf();
615 for component in relative.components() {
616 current.push(component);
617 let Ok(metadata) = std::fs::symlink_metadata(¤t) else {
618 return false;
619 };
620 if !metadata.is_dir() || metadata.file_type().is_symlink() {
621 return false;
622 }
623 }
624 true
625}
626
627fn is_manifest_input(path: &Path) -> bool {
628 path.to_str()
629 .is_some_and(|path| path.starts_with(INCLUDE_MANIFEST_PREFIX))
630}
631
632pub fn manifest_snapshot(
640 directories: &BTreeSet<PathBuf>,
641) -> Result<BTreeMap<PathBuf, CacheDigest>, CcBypassReason> {
642 let mut budget = 0_usize;
643 minimal_manifest_directories(directories.iter().cloned().collect())
644 .into_iter()
645 .map(|directory| {
646 include_manifest(&directory, &mut budget).map(|digest| (directory, digest))
647 })
648 .collect()
649}
650
651const INCLUDABLE_EXTENSIONS: &[&str] = &[
662 "c", "c++", "cc", "cpp", "cxx", "def", "gch", "h", "h++", "hh", "hpp", "hxx", "inc", "inl",
663 "ipp", "pch", "s", "tcc",
664];
665
666fn is_includable(name: &str) -> bool {
675 match name.rsplit_once('.') {
676 Some((stem, extension)) if !stem.is_empty() => INCLUDABLE_EXTENSIONS
677 .binary_search(&extension.to_ascii_lowercase().as_str())
678 .is_ok(),
679 _ => !name.starts_with('.'),
681 }
682}
683
684fn include_manifest(directory: &Path, budget: &mut usize) -> Result<CacheDigest, CcBypassReason> {
691 include_manifest_with(directory, budget, &|path| std::fs::read_dir(path))
692}
693
694fn include_manifest_with(
695 directory: &Path,
696 budget: &mut usize,
697 read_dir: &impl Fn(&Path) -> std::io::Result<std::fs::ReadDir>,
698) -> Result<CacheDigest, CcBypassReason> {
699 #[cfg(unix)]
700 if let Some(memo) = manifest_memo::find(directory) {
701 *budget = budget.saturating_add(memo.entries);
702 if *budget > MAX_MANIFEST_ENTRIES {
703 return Err(CcBypassReason::TooManyInputs);
704 }
705 return Ok(memo.digest);
706 }
707 #[cfg(unix)]
708 let mut directories = Vec::new();
709 #[cfg(unix)]
710 let initial_budget = *budget;
711 let mut names = Vec::new();
712 let mut pending = vec![(directory.to_path_buf(), String::new())];
713 while let Some((current, prefix)) = pending.pop() {
714 #[cfg(unix)]
717 directories.push((current.clone(), manifest_memo::Identity::read(¤t)));
718 let entries = match read_dir(¤t) {
719 Ok(entries) => entries,
720 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
721 Err(error) => {
722 return Err(CcBypassReason::InputRead {
723 path: current,
724 message: error.to_string(),
725 });
726 }
727 };
728 for entry in entries {
729 let entry = entry.map_err(|error| CcBypassReason::InputRead {
730 path: current.clone(),
731 message: error.to_string(),
732 })?;
733 let name = entry.file_name();
734 let Some(name) = name.to_str() else {
735 return Err(CcBypassReason::NonUtf8Path(entry.path()));
736 };
737 let relative = if prefix.is_empty() {
738 name.to_string()
739 } else {
740 format!("{prefix}/{name}")
741 };
742 let file_type = entry
743 .file_type()
744 .map_err(|error| CcBypassReason::InputRead {
745 path: entry.path(),
746 message: error.to_string(),
747 })?;
748 if file_type.is_dir() {
749 pending.push((entry.path(), relative));
750 continue;
751 }
752 if !is_includable(name) {
753 continue;
754 }
755 *budget += 1;
756 if *budget > MAX_MANIFEST_ENTRIES {
757 return Err(CcBypassReason::TooManyInputs);
758 }
759 names.push(relative);
760 }
761 }
762 names.sort();
763 let digest = CacheDigest::blake3(names.join("\n").as_bytes());
764 #[cfg(unix)]
765 manifest_memo::record(directory, &digest, *budget - initial_budget, directories);
766 Ok(digest)
767}
768
769#[cfg(unix)]
773mod manifest_memo {
774 use super::*;
775 use std::os::unix::fs::MetadataExt;
776 use std::sync::{Mutex, OnceLock};
777
778 const MAX_ROOTS: usize = 32;
779 const MAX_DIRECTORIES: usize = 4096;
780 static MEMOS: OnceLock<Mutex<BTreeMap<PathBuf, Memo>>> = OnceLock::new();
781
782 #[derive(Clone, PartialEq, Eq)]
783 pub(super) struct Identity {
784 device: u64,
785 inode: u64,
786 modified: (i64, i64),
787 changed: (i64, i64),
788 mode: u32,
789 }
790 impl Identity {
791 pub(super) fn read(path: &Path) -> Option<Self> {
792 let metadata = std::fs::metadata(path).ok()?;
793 if !metadata.is_dir() || metadata.mtime_nsec() == 0 || metadata.ctime_nsec() == 0 {
796 return None;
797 }
798 let changed = FileIdentity::for_digest_cache(path, &metadata)
801 .ok()??
802 .changed?;
803 Some(Self {
804 device: metadata.dev(),
805 inode: metadata.ino(),
806 modified: (metadata.mtime(), metadata.mtime_nsec()),
807 changed,
808 mode: metadata.mode(),
809 })
810 }
811 }
812 #[derive(Clone)]
813 pub(super) struct Memo {
814 pub digest: CacheDigest,
815 pub entries: usize,
816 directories: Vec<(PathBuf, Identity)>,
817 }
818 impl Memo {
819 fn valid(&self) -> bool {
820 self.directories
821 .iter()
822 .all(|(path, identity)| Identity::read(path).as_ref() == Some(identity))
823 }
824 }
825 pub(super) fn find(directory: &Path) -> Option<Memo> {
826 let memo = MEMOS.get()?.lock().ok()?.get(directory)?.clone();
827 memo.valid().then_some(memo)
828 }
829 pub(super) fn record(
830 directory: &Path,
831 digest: &CacheDigest,
832 entries: usize,
833 directories: Vec<(PathBuf, Option<Identity>)>,
834 ) {
835 if directories.is_empty() || directories.len() > MAX_DIRECTORIES {
836 return;
837 }
838 let Some(directories) = directories
839 .into_iter()
840 .map(|(path, identity)| Some((path, identity?)))
841 .collect::<Option<Vec<_>>>()
842 else {
843 return;
844 };
845 let memo = Memo {
846 digest: digest.clone(),
847 entries,
848 directories,
849 };
850 if !memo.valid() {
851 return;
852 }
853 let Ok(mut memos) = MEMOS.get_or_init(Default::default).lock() else {
854 return;
855 };
856 if memos.len() >= MAX_ROOTS {
857 memos.clear();
858 }
859 memos.insert(directory.to_path_buf(), memo);
860 }
861}
862
863pub(crate) fn contains_assembler_input_directive(path: &Path) -> Result<bool, CcBypassReason> {
870 contains_any(path, ASSEMBLER_INPUT_DIRECTIVES)
871}
872
873fn contains_any(path: &Path, needles: &[&[u8]]) -> Result<bool, CcBypassReason> {
874 let file = std::fs::File::open(path).map_err(|error| CcBypassReason::InputRead {
875 path: path.to_path_buf(),
876 message: error.to_string(),
877 })?;
878 let longest = needles
879 .iter()
880 .map(|needle| needle.len())
881 .max()
882 .unwrap_or_default();
883 let mut reader = std::io::BufReader::new(file);
884 let mut window = Vec::with_capacity(SCAN_CHUNK_BYTES + longest);
885 let mut chunk = vec![0_u8; SCAN_CHUNK_BYTES];
886 loop {
887 let read = reader
888 .read(&mut chunk)
889 .map_err(|error| CcBypassReason::InputRead {
890 path: path.to_path_buf(),
891 message: error.to_string(),
892 })?;
893 if read == 0 {
894 return Ok(false);
895 }
896 window.extend_from_slice(&chunk[..read]);
897 if needles
898 .iter()
899 .any(|needle| contains_subslice_ascii_case_insensitive(&window, needle))
900 {
901 return Ok(true);
902 }
903 let keep = window.len().saturating_sub(longest.saturating_sub(1));
904 window.drain(..keep);
905 }
906}
907
908fn contains_subslice_ascii_case_insensitive(haystack: &[u8], needle: &[u8]) -> bool {
909 if needle.is_empty() || haystack.len() < needle.len() {
910 return false;
911 }
912 haystack.windows(needle.len()).any(|window| {
913 window
914 .iter()
915 .zip(needle)
916 .all(|(left, right)| left.eq_ignore_ascii_case(right))
917 })
918}
919
920#[cfg(test)]
921#[path = "depfile_tests.rs"]
922mod tests;