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