use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use serde::{Deserialize, Serialize};
use crate::compiler::native_input_protocol::{
MAX_NATIVE_LIBRARY_PROBES, MAX_NATIVE_LIBRARY_SEARCHES, NativeLibraryForm, NativeLibrarySearch,
native_library_candidate_valid, native_library_name_valid,
};
use crate::error::{RailError, RailResult};
use super::{LINK_CAPTURE_LIMITS, LinkFileWitness, NativeCaptureBudget, capture_link_file, validate_link_file};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct NativeLibraryCandidate {
pub(crate) directory: u32,
pub(crate) file: String,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct NativeLibrarySelector {
pub(crate) name: String,
pub(crate) form: NativeLibraryForm,
pub(crate) verbatim: bool,
pub(crate) candidates: Vec<NativeLibraryCandidate>,
}
impl NativeLibrarySelector {
fn validate(&self) -> RailResult<()> {
let mut candidates = std::collections::BTreeSet::new();
let valid = native_library_name_valid(&self.name)
&& self.candidates.len() <= MAX_NATIVE_LIBRARY_PROBES
&& self
.candidates
.windows(2)
.all(|pair| pair[0].directory <= pair[1].directory)
&& self.candidates.iter().all(|candidate| {
native_library_candidate_valid(&self.name, self.verbatim, &candidate.file)
&& candidates.insert(candidate)
});
if !valid {
return Err(RailError::message(
"native library selector is not a bounded rustc search",
));
}
Ok(())
}
}
pub(crate) fn validate_selectors(selectors: &[NativeLibrarySelector]) -> RailResult<()> {
if selectors.len() > MAX_NATIVE_LIBRARY_SEARCHES
|| selectors.windows(2).any(|pair| {
(&pair[0].name, pair[0].form, pair[0].verbatim) >= (&pair[1].name, pair[1].form, pair[1].verbatim)
})
{
return Err(RailError::message(
"native library selectors are not bounded, sorted, and unique",
));
}
selectors.iter().try_for_each(NativeLibrarySelector::validate)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", content = "file", rename_all = "snake_case", deny_unknown_fields)]
pub(super) enum NativeLibraryCandidateState {
Absent,
Present(LinkFileWitness),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct NativeLibraryInput {
selector: NativeLibrarySelector,
states: Vec<NativeLibraryCandidateState>,
}
impl NativeLibraryInput {
pub(super) fn selector(&self) -> &NativeLibrarySelector {
&self.selector
}
}
pub(crate) fn validate_inputs(inputs: &[NativeLibraryInput]) -> RailResult<()> {
let selectors = inputs.iter().map(|input| input.selector.clone()).collect::<Vec<_>>();
validate_selectors(&selectors)?;
for input in inputs {
let last = input.states.len().checked_sub(1);
if input.states.len() > input.selector.candidates.len()
|| input.states.len() < input.selector.candidates.len()
&& !matches!(input.states.last(), Some(NativeLibraryCandidateState::Present(_)))
|| input.states.iter().enumerate().any(|(index, state)| match state {
NativeLibraryCandidateState::Absent => false,
NativeLibraryCandidateState::Present(file) => {
Some(index) != last
|| validate_link_file(file).is_err()
|| !Path::new(&file.path).ends_with(&input.selector.candidates[index].file)
}
})
{
return Err(RailError::message("native library input state is invalid"));
}
}
Ok(())
}
pub(super) fn selectors(
searches: &[NativeLibrarySearch],
directories: &[PathBuf],
) -> RailResult<Vec<NativeLibrarySelector>> {
let canonical = directories
.iter()
.map(|directory| crate::utils::canonicalize_allow_missing(directory).map_err(RailError::from))
.collect::<RailResult<Vec<_>>>()?;
let selectors = searches
.iter()
.map(|search| {
search.validate().map_err(RailError::message)?;
let mut position = 0usize;
let mut candidates = Vec::with_capacity(search.probes.len());
for probe in &search.probes {
let spelling = Path::new(&probe.directory);
let resolved = crate::utils::canonicalize_allow_missing(spelling)?;
position = (position..directories.len())
.find(|index| directories[*index] == spelling)
.or_else(|| (position..directories.len()).find(|index| canonical[*index] == resolved))
.ok_or_else(|| {
RailError::message(format!(
"compiler searched a native library in '{}', which is not a native search directory of the action",
spelling.display()
))
})?;
candidates.push(NativeLibraryCandidate {
directory: u32::try_from(position)
.map_err(|_| RailError::message("native search directory position overflowed"))?,
file: probe.file.clone(),
});
}
Ok(NativeLibrarySelector {
name: search.name.clone(),
form: search.form,
verbatim: search.verbatim,
candidates,
})
})
.collect::<RailResult<Vec<_>>>()?;
validate_selectors(&selectors)?;
Ok(selectors)
}
pub(super) fn capture(
selectors: &[NativeLibrarySelector],
directories: &[PathBuf],
) -> RailResult<(Vec<NativeLibraryInput>, u64)> {
validate_selectors(selectors)?;
let started = Instant::now();
let mut budget = NativeCaptureBudget::new(LINK_CAPTURE_LIMITS);
let inputs = selectors
.iter()
.map(|selector| {
let mut states = Vec::new();
for candidate in &selector.candidates {
let directory = usize::try_from(candidate.directory)
.ok()
.and_then(|index| directories.get(index))
.ok_or_else(|| {
RailError::message("native library selector names a search directory the action lacks")
})?;
let state = capture_candidate(&directory.join(&candidate.file), started, &mut budget)?;
let selected = matches!(state, NativeLibraryCandidateState::Present(_));
states.push(state);
if selected {
break;
}
}
Ok(NativeLibraryInput {
selector: selector.clone(),
states,
})
})
.collect::<RailResult<Vec<_>>>()?;
validate_inputs(&inputs)?;
Ok((inputs, budget.bytes_hashed))
}
fn capture_candidate(
path: &Path,
started: Instant,
budget: &mut NativeCaptureBudget,
) -> RailResult<NativeLibraryCandidateState> {
match fs::metadata(path) {
Ok(_) => {
let (file, _) = capture_link_file(path, started, budget)?;
Ok(NativeLibraryCandidateState::Present(file))
}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
) =>
{
budget.account_entry(&path.to_string_lossy())?;
Ok(NativeLibraryCandidateState::Absent)
}
Err(error) => Err(RailError::message(format!(
"native library candidate '{}' cannot be observed: {error}",
path.display()
))),
}
}
pub(super) fn selected_files(inputs: &[NativeLibraryInput]) -> impl Iterator<Item = &str> {
inputs.iter().filter_map(|input| match input.states.last() {
Some(NativeLibraryCandidateState::Present(file)) => Some(file.canonical_path.as_str()),
_ => None,
})
}
pub(super) fn post_execution_paths(
inputs: &[NativeLibraryInput],
directories: &[PathBuf],
) -> RailResult<Vec<super::PostExecutionPath>> {
let mut paths = Vec::new();
for input in inputs {
for (candidate, state) in input.selector.candidates.iter().zip(&input.states) {
let directory = usize::try_from(candidate.directory)
.ok()
.and_then(|index| directories.get(index))
.ok_or_else(|| {
RailError::message("native library selector names a search directory the action lacks")
})?;
let path = directory.join(&candidate.file);
paths.push(match state {
NativeLibraryCandidateState::Absent => super::PostExecutionPath::Absent(path),
NativeLibraryCandidateState::Present(_) => super::PostExecutionPath::Present(path),
});
}
}
Ok(paths)
}
pub(super) fn verify_observed(inputs: &[NativeLibraryInput], searches: &[NativeLibrarySearch]) -> RailResult<()> {
let observed = inputs.len() == searches.len()
&& inputs.iter().zip(searches).all(|(input, search)| {
let selected = search.probes.iter().position(|probe| probe.present);
input.states.len() == selected.map_or(search.probes.len(), |index| index + 1)
&& input
.states
.iter()
.zip(&search.probes)
.all(|(state, probe)| matches!(state, NativeLibraryCandidateState::Present(_)) == probe.present)
});
if !observed {
return Err(RailError::message(
"native library candidates changed while the compiler searched them",
));
}
Ok(())
}
pub(super) fn revalidate(inputs: &[NativeLibraryInput], directories: &[PathBuf]) -> RailResult<u64> {
let selectors = inputs.iter().map(|input| input.selector.clone()).collect::<Vec<_>>();
let (current, bytes_hashed) = capture(&selectors, directories)?;
if current != inputs {
return Err(RailError::message(
"a native library candidate changed before the restore commit",
));
}
Ok(bytes_hashed)
}