1use ic_host_artifacts::artifact::{ArtifactError, ArtifactIdentity};
10use ic_host_fs::read::hash_file;
11use ic_host_process::tool::{
12 AdmittedTool, ExecutionContext, ExecutionEvidence, OutputLimits, ToolError,
13};
14use std::{fmt, path::Path};
15
16#[cfg(test)]
17mod tests;
18
19#[derive(Debug)]
21pub enum NormalizationError {
22 InputLimit {
24 actual: usize,
26 limit: usize,
28 },
29 Utf8(std::str::Utf8Error),
31 OutputLimit {
33 limit: usize,
35 },
36 Allocation(std::collections::TryReserveError),
38}
39
40impl fmt::Display for NormalizationError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::InputLimit { actual, limit } => {
44 write!(f, "Candid output has {actual} bytes, exceeding {limit}")
45 }
46 Self::Utf8(_) => f.write_str("Candid extractor output is not UTF-8"),
47 Self::OutputLimit { limit } => write!(f, "normalized Candid exceeds {limit} bytes"),
48 Self::Allocation(_) => f.write_str("Candid normalization allocation failed"),
49 }
50 }
51}
52impl std::error::Error for NormalizationError {
53 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
54 match self {
55 Self::Utf8(source) => Some(source),
56 Self::Allocation(source) => Some(source),
57 _ => None,
58 }
59 }
60}
61
62pub fn normalize(bytes: &[u8], max_bytes: usize) -> Result<String, NormalizationError> {
73 if bytes.len() > max_bytes {
74 return Err(NormalizationError::InputLimit {
75 actual: bytes.len(),
76 limit: max_bytes,
77 });
78 }
79 let text = std::str::from_utf8(bytes).map_err(NormalizationError::Utf8)?;
80 let mut normalized = String::new();
81 for line in text.lines() {
82 let line = line.trim_end();
83 let length = normalized
84 .len()
85 .checked_add(line.len())
86 .and_then(|length| length.checked_add(1));
87 if length.is_none_or(|length| length > max_bytes) {
88 return Err(NormalizationError::OutputLimit { limit: max_bytes });
89 }
90 normalized
91 .try_reserve_exact(line.len() + 1)
92 .map_err(NormalizationError::Allocation)?;
93 normalized.push_str(line);
94 normalized.push('\n');
95 }
96 Ok(normalized)
97}
98
99#[derive(Debug)]
101pub enum ExtractionError {
102 SourcePath,
104 Input(ArtifactError),
106 Tool(ToolError),
108 SourceInspection {
110 source: ArtifactError,
112 evidence: Box<ExecutionEvidence>,
114 },
115 SourceChanged {
117 before: ArtifactIdentity,
119 after: ArtifactIdentity,
121 evidence: Box<ExecutionEvidence>,
123 },
124 Normalize {
126 source: NormalizationError,
128 evidence: Box<ExecutionEvidence>,
130 },
131}
132
133impl fmt::Display for ExtractionError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::SourcePath => f.write_str("Candid source path must be absolute"),
137 Self::Input(source) => write!(f, "Candid source inspection failed: {source}"),
138 Self::Tool(source) => source.fmt(f),
139 Self::SourceInspection { .. } => f.write_str("Candid source re-inspection failed"),
140 Self::SourceChanged { .. } => f.write_str("Candid source changed during extraction"),
141 Self::Normalize { source, .. } => source.fmt(f),
142 }
143 }
144}
145impl std::error::Error for ExtractionError {
146 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
147 match self {
148 Self::Input(source) | Self::SourceInspection { source, .. } => Some(source),
149 Self::Tool(source) => Some(source),
150 Self::Normalize { source, .. } => Some(source),
151 _ => None,
152 }
153 }
154}
155
156pub struct ExtractedCandid {
158 pub text: String,
160 pub source_identity: ArtifactIdentity,
162 pub tool_identity: ArtifactIdentity,
164 pub evidence: ExecutionEvidence,
166}
167
168impl fmt::Debug for ExtractedCandid {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 f.debug_struct("ExtractedCandid")
171 .field("text_bytes", &self.text.len())
172 .field("source_identity", &self.source_identity)
173 .field("tool_identity", &self.tool_identity)
174 .field("evidence", &self.evidence)
175 .finish()
176 }
177}
178
179pub fn extract(
191 tool: &AdmittedTool,
192 source: &Path,
193 context: &ExecutionContext<'_>,
194 source_bytes: u64,
195 output: OutputLimits,
196) -> Result<ExtractedCandid, ExtractionError> {
197 if !source.is_absolute() {
198 return Err(ExtractionError::SourcePath);
199 }
200 let before = hash_file(source, source_bytes).map_err(ExtractionError::Input)?;
201 let evidence = tool
202 .run(&[source.as_os_str().to_owned()], context, output)
203 .map_err(ExtractionError::Tool)?;
204 let after = match hash_file(source, source_bytes) {
205 Ok(identity) => identity,
206 Err(source) => {
207 return Err(ExtractionError::SourceInspection {
208 source,
209 evidence: Box::new(evidence),
210 });
211 }
212 };
213 if before != after {
214 return Err(ExtractionError::SourceChanged {
215 before,
216 after,
217 evidence: Box::new(evidence),
218 });
219 }
220 let text = match normalize(&evidence.stdout, output.stdout_bytes) {
221 Ok(text) => text,
222 Err(source) => {
223 return Err(ExtractionError::Normalize {
224 source,
225 evidence: Box::new(evidence),
226 });
227 }
228 };
229 Ok(ExtractedCandid {
230 text,
231 source_identity: before,
232 tool_identity: tool.identity(),
233 evidence,
234 })
235}