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