use crate::{
durable_io::{RegularFileReadError, read_optional_regular_bytes},
release_set::{GZIP_MAGIC, ReleaseSetEntry, WASM_MAGIC},
};
use std::{
fs,
io::{self, Read},
path::{Component, Path, PathBuf},
};
use canic_core::{CANIC_WASM_CHUNK_BYTES, cdk::utils::hash::wasm_hash_hex};
use flate2::read::GzDecoder;
pub(in crate::release_set) struct MaterializedReleaseArtifact {
pub relative_path: String,
pub bytes: Vec<u8>,
}
pub(in crate::release_set) enum ReleaseArtifactMaterializationError {
InvalidPath,
NonUtf8Path,
OutsideRoot,
Read(io::Error),
UnsafeFile,
}
pub(in crate::release_set) fn build_release_set_entry(
icp_root: &Path,
role_name: &str,
artifact_path: &Path,
) -> Result<ReleaseSetEntry, Box<dyn std::error::Error>> {
let artifact_relative_path = artifact_path
.strip_prefix(icp_root)
.map_err(|_| {
format!(
"artifact {} is not under ICP root {}",
artifact_path.display(),
icp_root.display()
)
})?
.to_string_lossy()
.to_string();
let artifact_path = resolve_release_artifact_path(icp_root, &artifact_relative_path)?;
let artifact = read_release_artifact(&artifact_path)?;
let chunk_hashes = artifact
.chunks(CANIC_WASM_CHUNK_BYTES)
.map(wasm_hash_hex)
.collect::<Vec<_>>();
Ok(ReleaseSetEntry {
role: role_name.to_string(),
template_id: format!("embedded:{role_name}"),
artifact_relative_path,
payload_size_bytes: u64::try_from(artifact.len())?,
payload_sha256_hex: wasm_hash_hex(&artifact),
chunk_size_bytes: u64::try_from(CANIC_WASM_CHUNK_BYTES)?,
chunk_sha256_hex: chunk_hashes,
})
}
pub fn validate_release_artifact_relative_path(
artifact_relative_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let relative_path = Path::new(artifact_relative_path);
if relative_path.as_os_str().is_empty()
|| relative_path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(format!(
"release artifact path must be relative to the ICP root: {artifact_relative_path}"
)
.into());
}
Ok(())
}
pub(in crate::release_set) fn materialize_qualified_release_artifact(
root: &Path,
path: &Path,
) -> Result<MaterializedReleaseArtifact, ReleaseArtifactMaterializationError> {
let relative = path
.strip_prefix(root)
.map_err(|_| ReleaseArtifactMaterializationError::OutsideRoot)?;
let relative = relative
.to_str()
.ok_or(ReleaseArtifactMaterializationError::NonUtf8Path)?;
validate_release_artifact_relative_path(relative)
.map_err(|_| ReleaseArtifactMaterializationError::InvalidPath)?;
let canonical_root =
fs::canonicalize(root).map_err(ReleaseArtifactMaterializationError::Read)?;
let parent = path
.parent()
.ok_or(ReleaseArtifactMaterializationError::InvalidPath)?;
let canonical_parent =
fs::canonicalize(parent).map_err(ReleaseArtifactMaterializationError::Read)?;
if !canonical_parent.starts_with(canonical_root) {
return Err(ReleaseArtifactMaterializationError::OutsideRoot);
}
let bytes = match read_optional_regular_bytes(path) {
Ok(Some(bytes)) => bytes,
Ok(None) => {
return Err(ReleaseArtifactMaterializationError::Read(io::Error::new(
io::ErrorKind::NotFound,
"artifact is missing",
)));
}
Err(RegularFileReadError::NotRegular) => {
return Err(ReleaseArtifactMaterializationError::UnsafeFile);
}
Err(RegularFileReadError::Io(source)) => {
return Err(ReleaseArtifactMaterializationError::Read(source));
}
#[cfg(not(unix))]
Err(RegularFileReadError::UnsupportedPlatform) => {
return Err(ReleaseArtifactMaterializationError::Read(io::Error::new(
io::ErrorKind::Unsupported,
"regular no-follow artifact reads are unsupported",
)));
}
};
Ok(MaterializedReleaseArtifact {
relative_path: relative.to_string(),
bytes,
})
}
pub fn resolve_release_artifact_path(
icp_root: &Path,
artifact_relative_path: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
validate_release_artifact_relative_path(artifact_relative_path)?;
let relative_path = Path::new(artifact_relative_path);
let canonical_root = icp_root.canonicalize()?;
let canonical_artifact = canonical_root.join(relative_path).canonicalize()?;
if !canonical_artifact.starts_with(&canonical_root) {
return Err(format!(
"release artifact path escapes ICP root: {}",
canonical_artifact.display()
)
.into());
}
Ok(canonical_artifact)
}
pub(in crate::release_set) fn read_release_artifact(
path: &Path,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let artifact = fs::read(path)?;
if artifact.is_empty() {
return Err(format!("release artifact is empty: {}", path.display()).into());
}
if !artifact.starts_with(&GZIP_MAGIC) {
return Err(format!(
"release artifact is not gzip-compressed: {}",
path.display()
)
.into());
}
let mut decoder = GzDecoder::new(&artifact[..]);
let mut wasm = Vec::new();
decoder
.read_to_end(&mut wasm)
.map_err(|err| format!("failed to decompress {}: {err}", path.display()))?;
if wasm.is_empty() {
return Err(format!(
"release artifact decompresses to zero bytes: {}",
path.display()
)
.into());
}
if !wasm.starts_with(&WASM_MAGIC) {
return Err(format!(
"release artifact does not decompress to a wasm module: {}",
path.display()
)
.into());
}
Ok(artifact)
}