use ic_host_artifacts::artifact::{ArtifactError, ArtifactIdentity};
use ic_host_fs::read::hash_file;
use ic_host_process::tool::{
AdmittedTool, ExecutionContext, ExecutionEvidence, OutputLimits, ToolError,
};
use std::{fmt, path::Path};
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub enum NormalizationError {
InputLimit {
actual: usize,
limit: usize,
},
Utf8(std::str::Utf8Error),
OutputLimit {
limit: usize,
},
Allocation(std::collections::TryReserveError),
}
impl fmt::Display for NormalizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InputLimit { actual, limit } => {
write!(f, "Candid output has {actual} bytes, exceeding {limit}")
}
Self::Utf8(_) => f.write_str("Candid extractor output is not UTF-8"),
Self::OutputLimit { limit } => write!(f, "normalized Candid exceeds {limit} bytes"),
Self::Allocation(_) => f.write_str("Candid normalization allocation failed"),
}
}
}
impl std::error::Error for NormalizationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Utf8(source) => Some(source),
Self::Allocation(source) => Some(source),
_ => None,
}
}
}
pub fn normalize(bytes: &[u8], max_bytes: usize) -> Result<String, NormalizationError> {
if bytes.len() > max_bytes {
return Err(NormalizationError::InputLimit {
actual: bytes.len(),
limit: max_bytes,
});
}
let text = std::str::from_utf8(bytes).map_err(NormalizationError::Utf8)?;
let mut normalized = String::new();
for line in text.lines() {
let line = line.trim_end();
let length = normalized
.len()
.checked_add(line.len())
.and_then(|length| length.checked_add(1));
if length.is_none_or(|length| length > max_bytes) {
return Err(NormalizationError::OutputLimit { limit: max_bytes });
}
normalized
.try_reserve_exact(line.len() + 1)
.map_err(NormalizationError::Allocation)?;
normalized.push_str(line);
normalized.push('\n');
}
Ok(normalized)
}
#[derive(Debug)]
pub enum ExtractionError {
SourcePath,
Input(ArtifactError),
Tool(ToolError),
SourceInspection {
source: ArtifactError,
evidence: Box<ExecutionEvidence>,
},
SourceChanged {
before: ArtifactIdentity,
after: ArtifactIdentity,
evidence: Box<ExecutionEvidence>,
},
Normalize {
source: NormalizationError,
evidence: Box<ExecutionEvidence>,
},
}
impl fmt::Display for ExtractionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SourcePath => f.write_str("Candid source path must be absolute"),
Self::Input(source) => write!(f, "Candid source inspection failed: {source}"),
Self::Tool(source) => source.fmt(f),
Self::SourceInspection { .. } => f.write_str("Candid source re-inspection failed"),
Self::SourceChanged { .. } => f.write_str("Candid source changed during extraction"),
Self::Normalize { source, .. } => source.fmt(f),
}
}
}
impl std::error::Error for ExtractionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Input(source) | Self::SourceInspection { source, .. } => Some(source),
Self::Tool(source) => Some(source),
Self::Normalize { source, .. } => Some(source),
_ => None,
}
}
}
pub struct ExtractedCandid {
pub text: String,
pub source_identity: ArtifactIdentity,
pub tool_identity: ArtifactIdentity,
pub evidence: ExecutionEvidence,
}
impl fmt::Debug for ExtractedCandid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExtractedCandid")
.field("text_bytes", &self.text.len())
.field("source_identity", &self.source_identity)
.field("tool_identity", &self.tool_identity)
.field("evidence", &self.evidence)
.finish()
}
}
pub fn extract(
tool: &AdmittedTool,
source: &Path,
context: &ExecutionContext<'_>,
source_bytes: u64,
output: OutputLimits,
) -> Result<ExtractedCandid, ExtractionError> {
if !source.is_absolute() {
return Err(ExtractionError::SourcePath);
}
let before = hash_file(source, source_bytes).map_err(ExtractionError::Input)?;
let evidence = tool
.run(&[source.as_os_str().to_owned()], context, output)
.map_err(ExtractionError::Tool)?;
let after = match hash_file(source, source_bytes) {
Ok(identity) => identity,
Err(source) => {
return Err(ExtractionError::SourceInspection {
source,
evidence: Box::new(evidence),
});
}
};
if before != after {
return Err(ExtractionError::SourceChanged {
before,
after,
evidence: Box::new(evidence),
});
}
let text = match normalize(&evidence.stdout, output.stdout_bytes) {
Ok(text) => text,
Err(source) => {
return Err(ExtractionError::Normalize {
source,
evidence: Box::new(evidence),
});
}
};
Ok(ExtractedCandid {
text,
source_identity: before,
tool_identity: tool.identity(),
evidence,
})
}