Skip to main content

ic_build/artifacts/candid/
mod.rs

1//! Admitted extractor execution with source identity and bounded output evidence.
2//! Callers own grammar validation, publication and exclusion of concurrent writers.
3
4use ic_host_artifacts::artifact::{ArtifactError, ArtifactIdentity};
5use ic_host_fs::read::hash_file;
6use ic_host_process::tool::{
7    AdmittedTool, CompletedExecution, ExecutionContext, OutputLimits, ToolError,
8};
9use std::{fmt, path::Path};
10
11#[cfg(test)]
12mod test_support;
13#[cfg(test)]
14mod tests;
15
16/// A Candid extraction failed, retaining process bytes where invocation occurred.
17#[derive(Debug)]
18pub enum ExtractionError {
19    /// The source path was relative; no tool was invoked.
20    SourcePath,
21    /// Source inspection failed before invocation.
22    Input(ArtifactError),
23    /// Tool verification or execution failed; execution failures retain evidence.
24    Tool(ToolError),
25    /// Source could not be re-inspected after successful execution.
26    SourceInspection {
27        /// Inspection failure.
28        source: ArtifactError,
29        /// Bounded successful process output.
30        evidence: Box<CompletedExecution>,
31    },
32    /// Source bytes changed between preflight and post-extraction observation.
33    SourceChanged {
34        /// Identity observed before invocation.
35        before: ArtifactIdentity,
36        /// Identity observed after invocation.
37        after: ArtifactIdentity,
38        /// Bounded process output that must not be published as valid extraction.
39        evidence: Box<CompletedExecution>,
40    },
41}
42
43impl fmt::Display for ExtractionError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::SourcePath => f.write_str("Candid source path must be absolute"),
47            Self::Input(source) => write!(f, "Candid source inspection failed: {source}"),
48            Self::Tool(source) => source.fmt(f),
49            Self::SourceInspection { .. } => f.write_str("Candid source re-inspection failed"),
50            Self::SourceChanged { .. } => f.write_str("Candid source changed during extraction"),
51        }
52    }
53}
54impl std::error::Error for ExtractionError {
55    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
56        match self {
57            Self::Input(source) | Self::SourceInspection { source, .. } => Some(source),
58            Self::Tool(source) => Some(source),
59            _ => None,
60        }
61    }
62}
63
64/// Source/tool identities and exact bounded process evidence, without text conversion.
65pub struct ExtractedCandid {
66    /// Matching source identity observed before and after invocation.
67    pub source_identity: ArtifactIdentity,
68    /// Tool bytes admitted by the consumer.
69    pub tool_identity: ArtifactIdentity,
70    /// Original stdout/stderr and successful direct-child exit status.
71    pub evidence: CompletedExecution,
72}
73
74impl fmt::Debug for ExtractedCandid {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.debug_struct("ExtractedCandid")
77            .field("source_identity", &self.source_identity)
78            .field("tool_identity", &self.tool_identity)
79            .field("evidence", &self.evidence)
80            .finish()
81    }
82}
83
84/// Invoke the admitted extractor once with the absolute source path as one arg.
85///
86/// Input regular-file hashing is bounded by `source_bytes`; process output uses
87/// the selected retention limits and must be complete. No UTF-8 conversion,
88/// normalization or grammar validation occurs. Callers can explicitly apply
89/// their own normalization to retained stdout with a separate normalized-byte
90/// allowance. The source is inspected again on successful completion; observed changes reject
91/// publication. Callers must exclude concurrent writers to source/tool paths:
92/// before/after digests cannot prove that transient changes did not occur.
93///
94/// # Errors
95/// Returns typed path/read, tool-execution or source-change failures.
96/// After invocation, process failure/validation evidence is retained.
97pub fn extract(
98    tool: &AdmittedTool,
99    source: &Path,
100    context: &ExecutionContext<'_>,
101    source_bytes: u64,
102    output: OutputLimits,
103) -> Result<ExtractedCandid, ExtractionError> {
104    if !source.is_absolute() {
105        return Err(ExtractionError::SourcePath);
106    }
107    let before = hash_file(source, source_bytes).map_err(ExtractionError::Input)?;
108    let evidence = tool
109        .run(&[source.as_os_str().to_owned()], context, output)
110        .and_then(CompletedExecution::require_success)
111        .and_then(CompletedExecution::require_complete)
112        .map_err(ExtractionError::Tool)?;
113    let after = match hash_file(source, source_bytes) {
114        Ok(identity) => identity,
115        Err(source) => {
116            return Err(ExtractionError::SourceInspection {
117                source,
118                evidence: Box::new(evidence),
119            });
120        }
121    };
122    if before != after {
123        return Err(ExtractionError::SourceChanged {
124            before,
125            after,
126            evidence: Box::new(evidence),
127        });
128    }
129    Ok(ExtractedCandid {
130        source_identity: before,
131        tool_identity: tool.identity(),
132        evidence,
133    })
134}