ic_host_tools/candid/extraction/
mod.rs1use super::text::{NormalizationError, normalize};
5use ic_host_artifacts::artifact::{ArtifactError, ArtifactIdentity};
6use ic_host_fs::read::hash_file;
7use ic_host_process::tool::{
8 AdmittedTool, CompletedExecution, ExecutionContext, OutputLimits, ToolError,
9};
10use std::{fmt, path::Path};
11
12#[cfg(test)]
13mod tests;
14
15#[derive(Debug)]
17pub enum ExtractionError {
18 SourcePath,
20 Input(ArtifactError),
22 Tool(ToolError),
24 SourceInspection {
26 source: ArtifactError,
28 evidence: Box<CompletedExecution>,
30 },
31 SourceChanged {
33 before: ArtifactIdentity,
35 after: ArtifactIdentity,
37 evidence: Box<CompletedExecution>,
39 },
40 Normalize {
42 source: NormalizationError,
44 evidence: Box<CompletedExecution>,
46 },
47}
48
49impl fmt::Display for ExtractionError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::SourcePath => f.write_str("Candid source path must be absolute"),
53 Self::Input(source) => write!(f, "Candid source inspection failed: {source}"),
54 Self::Tool(source) => source.fmt(f),
55 Self::SourceInspection { .. } => f.write_str("Candid source re-inspection failed"),
56 Self::SourceChanged { .. } => f.write_str("Candid source changed during extraction"),
57 Self::Normalize { source, .. } => source.fmt(f),
58 }
59 }
60}
61impl std::error::Error for ExtractionError {
62 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63 match self {
64 Self::Input(source) | Self::SourceInspection { source, .. } => Some(source),
65 Self::Tool(source) => Some(source),
66 Self::Normalize { source, .. } => Some(source),
67 _ => None,
68 }
69 }
70}
71
72pub struct ExtractedCandid {
74 pub text: String,
76 pub source_identity: ArtifactIdentity,
78 pub tool_identity: ArtifactIdentity,
80 pub evidence: CompletedExecution,
82}
83
84impl fmt::Debug for ExtractedCandid {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("ExtractedCandid")
87 .field("text_bytes", &self.text.len())
88 .field("source_identity", &self.source_identity)
89 .field("tool_identity", &self.tool_identity)
90 .field("evidence", &self.evidence)
91 .finish()
92 }
93}
94
95pub fn extract(
107 tool: &AdmittedTool,
108 source: &Path,
109 context: &ExecutionContext<'_>,
110 source_bytes: u64,
111 output: OutputLimits,
112) -> Result<ExtractedCandid, ExtractionError> {
113 if !source.is_absolute() {
114 return Err(ExtractionError::SourcePath);
115 }
116 let before = hash_file(source, source_bytes).map_err(ExtractionError::Input)?;
117 let evidence = tool
118 .run(&[source.as_os_str().to_owned()], context, output)
119 .and_then(CompletedExecution::require_success)
120 .and_then(CompletedExecution::require_complete)
121 .map_err(ExtractionError::Tool)?;
122 let after = match hash_file(source, source_bytes) {
123 Ok(identity) => identity,
124 Err(source) => {
125 return Err(ExtractionError::SourceInspection {
126 source,
127 evidence: Box::new(evidence),
128 });
129 }
130 };
131 if before != after {
132 return Err(ExtractionError::SourceChanged {
133 before,
134 after,
135 evidence: Box::new(evidence),
136 });
137 }
138 let text = match normalize(&evidence.stdout, output.stdout.retained_bytes()) {
139 Ok(text) => text,
140 Err(source) => {
141 return Err(ExtractionError::Normalize {
142 source,
143 evidence: Box::new(evidence),
144 });
145 }
146 };
147 Ok(ExtractedCandid {
148 text,
149 source_identity: before,
150 tool_identity: tool.identity(),
151 evidence,
152 })
153}