use super::type_graph::{Actor, Declaration, TypeNode, TypeRef};
use super::validation_error::{ContractJsonError, ContractValidationError};
use crate::limits::Limits;
use serde::{Deserialize, Serialize, Serializer};
pub const CONTRACT_FORMAT: &str = "candid-core";
pub const FORMAT_VERSION: u32 = 1;
pub const SEMANTICS_PROFILE: &str = "candid-1";
pub const CANONICALIZATION_PROFILE: &str = "candid-core-canon-1";
const PACKAGE_MANIFEST: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContractIdentities {
pub contract: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProducerInfo {
pub name: String,
pub version: String,
pub candid_version: String,
pub candid_parser_version: String,
}
impl ProducerInfo {
pub fn current() -> Self {
Self {
name: env!("CARGO_PKG_NAME").to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
candid_version: exact_dependency_version(PACKAGE_MANIFEST, "candid"),
candid_parser_version: exact_dependency_version(PACKAGE_MANIFEST, "candid_parser"),
}
}
}
fn exact_dependency_version(manifest: &str, dependency: &str) -> String {
manifest_dependency_version(manifest, dependency).unwrap_or_else(|| {
panic!("{dependency} must be declared as an exact dependency in Cargo.toml")
})
}
fn manifest_dependency_version(manifest: &str, dependency: &str) -> Option<String> {
let inline_prefix = format!("{dependency} = ");
let section_header = format!("[dependencies.{dependency}]");
let mut in_dependencies = false;
let mut in_dependency_section = false;
for line in manifest.lines().map(str::trim) {
if line.starts_with('[') {
in_dependencies = line == "[dependencies]";
in_dependency_section = line == section_header;
} else if in_dependencies {
if let Some(version) = line
.strip_prefix(&inline_prefix)
.and_then(exact_version_literal)
{
return Some(version.to_string());
}
} else if in_dependency_section {
if let Some(value) = line.strip_prefix("version = ") {
return exact_version_literal(value).map(str::to_string);
}
}
}
None
}
fn exact_version_literal(declaration: &str) -> Option<&str> {
let literal = match declaration.strip_prefix('{') {
Some(table) => table.split_once("version = ")?.1,
None => declaration,
};
literal
.strip_prefix('"')
.and_then(|value| value.strip_prefix('='))
.and_then(|value| value.split_once('"').map(|(version, _)| version))
}
#[cfg(test)]
mod manifest_tests {
use super::*;
#[test]
fn the_packages_own_manifest_reports_both_engine_versions() {
assert!(!exact_dependency_version(PACKAGE_MANIFEST, "candid").is_empty());
assert!(!exact_dependency_version(PACKAGE_MANIFEST, "candid_parser").is_empty());
}
#[test]
fn every_spelling_cargo_can_produce_is_read_identically() {
let plain = "[dependencies]\ncandid = \"=0.10.30\"\n";
let inline_table = "[dependencies]\ncandid = { version = \"=0.10.30\", optional = true }\n";
let normalized = "[dependencies.candid]\nversion = \"=0.10.30\"\noptional = true\n";
for manifest in [plain, inline_table, normalized] {
assert_eq!(exact_dependency_version(manifest, "candid"), "0.10.30");
}
}
#[test]
fn a_dev_dependency_is_never_mistaken_for_the_real_one() {
let manifest = "[dependencies.candid_parser]\nversion = \"=0.4.0\"\noptional = true\n\
\n[dev-dependencies.candid_parser]\nversion = \"=0.9.9\"\n";
assert_eq!(exact_dependency_version(manifest, "candid_parser"), "0.4.0");
assert_eq!(
manifest_dependency_version(
"[dev-dependencies.candid_parser]\nversion = \"=0.4.0\"\n",
"candid_parser"
),
None
);
}
#[test]
fn a_floating_requirement_is_not_an_exact_pin() {
for manifest in [
"[dependencies]\ncandid = \"0.10.30\"\n",
"[dependencies]\ncandid = { version = \"^0.10\" }\n",
"[dependencies.candid]\nversion = \"0.10.30\"\n",
] {
assert_eq!(manifest_dependency_version(manifest, "candid"), None);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Contract {
pub(crate) format: String,
pub(crate) format_version: u32,
pub(crate) semantics_profile: String,
pub(crate) canonicalization_profile: String,
pub(crate) identities: ContractIdentities,
pub(crate) producer: ProducerInfo,
pub(crate) types: Vec<TypeNode>,
pub(crate) declarations: Vec<Declaration>,
pub(crate) actor: Option<Actor>,
}
impl Contract {
pub fn format(&self) -> &str {
&self.format
}
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn semantics_profile(&self) -> &str {
&self.semantics_profile
}
pub fn canonicalization_profile(&self) -> &str {
&self.canonicalization_profile
}
pub fn identities(&self) -> &ContractIdentities {
&self.identities
}
pub fn contract_id(&self) -> &str {
&self.identities.contract
}
pub fn interface_id(&self) -> Option<&str> {
self.identities.interface.as_deref()
}
pub fn producer(&self) -> &ProducerInfo {
&self.producer
}
pub fn types(&self) -> &[TypeNode] {
&self.types
}
pub fn declarations(&self) -> &[Declaration] {
&self.declarations
}
pub fn actor(&self) -> Option<&Actor> {
self.actor.as_ref()
}
pub fn validate(&self) -> Result<(), ContractValidationError> {
self.validate_with_limits(&Limits::default())
}
pub fn validate_with_limits(&self, limits: &Limits) -> Result<(), ContractValidationError> {
self.validate_with_context(&crate::RuntimeContext::new(limits.clone()))
}
pub fn validate_with_context(
&self,
context: &crate::RuntimeContext,
) -> Result<(), ContractValidationError> {
let mut budget = context.budget();
crate::validate::validate_contract_with_budget(self, &mut budget)
}
pub fn canonicalize(&self) -> Result<Self, ContractValidationError> {
self.canonicalize_with_limits(&Limits::default())
}
pub fn canonicalize_with_limits(
&self,
limits: &Limits,
) -> Result<Self, ContractValidationError> {
self.canonicalize_with_context(&crate::RuntimeContext::new(limits.clone()))
}
pub fn canonicalize_with_context(
&self,
context: &crate::RuntimeContext,
) -> Result<Self, ContractValidationError> {
let mut budget = context.budget();
crate::canonical::canonicalize_contract_with_budget(self, &mut budget)
}
pub fn to_json_pretty(&self) -> Result<String, ContractValidationError> {
self.to_json_pretty_with_context(&crate::RuntimeContext::default())
}
pub fn to_json_pretty_with_limits(
&self,
limits: &Limits,
) -> Result<String, ContractValidationError> {
self.to_json_pretty_with_context(&crate::RuntimeContext::new(limits.clone()))
}
pub fn to_json_pretty_with_context(
&self,
context: &crate::RuntimeContext,
) -> Result<String, ContractValidationError> {
let mut budget = context.budget();
let canonical =
crate::validate::validate_and_canonicalize_with_budget(self, &mut budget)?.contract;
budget
.checkpoint()
.map_err(crate::budget::BudgetError::into_contract_error)?;
let json = serde_json::to_string_pretty(&canonical).map_err(|error| {
ContractValidationError::single(
"contract_json_serialization_failed",
"$",
error.to_string(),
)
})?;
let max_work = budget.limits().max_canonicalization_work;
budget
.charge("canonicalization_work", max_work, json.len())
.map_err(crate::budget::BudgetError::into_contract_error)?;
budget
.checkpoint()
.map_err(crate::budget::BudgetError::into_contract_error)?;
Ok(json)
}
pub fn from_json(input: &str) -> Result<Self, ContractJsonError> {
Self::from_json_with_limits(input, &Limits::default())
}
pub fn from_json_with_limits(input: &str, limits: &Limits) -> Result<Self, ContractJsonError> {
Self::from_json_with_context(input, &crate::RuntimeContext::new(limits.clone()))
}
pub fn from_json_with_context(
input: &str,
context: &crate::RuntimeContext,
) -> Result<Self, ContractJsonError> {
let mut budget = context.budget();
let raw: RawContract = crate::budget::decode_bounded(&mut budget, input.len(), || {
serde_json::from_str(input)
})?;
Self::from_raw_with_mapping_and_budget(raw, &mut budget)
.map(|(contract, _)| contract)
.map_err(ContractJsonError::InvalidContract)
}
pub fn from_slice_with_limits(
input: &[u8],
limits: &Limits,
) -> Result<Self, ContractJsonError> {
Self::from_slice_with_context(input, &crate::RuntimeContext::new(limits.clone()))
}
pub fn from_slice_with_context(
input: &[u8],
context: &crate::RuntimeContext,
) -> Result<Self, ContractJsonError> {
let mut budget = context.budget();
let raw: RawContract = crate::budget::decode_bounded(&mut budget, input.len(), || {
serde_json::from_slice(input)
})?;
Self::from_raw_with_mapping_and_budget(raw, &mut budget)
.map(|(contract, _)| contract)
.map_err(ContractJsonError::InvalidContract)
}
pub fn try_from_raw(raw: RawContract) -> Result<Self, ContractValidationError> {
Self::from_raw_with_limits(raw, &Limits::default())
}
pub fn try_from_raw_with_limits(
raw: RawContract,
limits: &Limits,
) -> Result<Self, ContractValidationError> {
Self::from_raw_with_limits(raw, limits)
}
pub fn try_from_raw_with_context(
raw: RawContract,
context: &crate::RuntimeContext,
) -> Result<Self, ContractValidationError> {
let mut budget = context.budget();
Ok(Self::from_raw_with_mapping_and_budget(raw, &mut budget)?.0)
}
fn from_raw_with_limits(
raw: RawContract,
limits: &Limits,
) -> Result<Self, ContractValidationError> {
Ok(Self::from_raw_with_mapping(raw, limits)?.0)
}
pub(crate) fn from_raw_with_mapping(
raw: RawContract,
limits: &Limits,
) -> Result<(Self, Vec<TypeRef>), ContractValidationError> {
let mut budget = crate::budget::Budget::from_limits(limits);
Self::from_raw_with_mapping_and_budget(raw, &mut budget)
}
pub(crate) fn from_raw_with_mapping_and_budget(
raw: RawContract,
budget: &mut crate::budget::Budget<'_>,
) -> Result<(Self, Vec<TypeRef>), ContractValidationError> {
let contract = Self {
format: raw.format,
format_version: raw.format_version,
semantics_profile: raw.semantics_profile,
canonicalization_profile: raw.canonicalization_profile,
identities: raw.identities,
producer: raw.producer,
types: raw.types,
declarations: raw.declarations,
actor: raw.actor,
};
let canonicalized =
crate::validate::validate_and_canonicalize_with_budget(&contract, budget)?;
Ok((canonicalized.contract, canonicalized.old_to_new))
}
pub(crate) fn new_unchecked(
types: Vec<TypeNode>,
declarations: Vec<Declaration>,
actor: Option<Actor>,
) -> Self {
Self {
format: CONTRACT_FORMAT.to_string(),
format_version: FORMAT_VERSION,
semantics_profile: SEMANTICS_PROFILE.to_string(),
canonicalization_profile: CANONICALIZATION_PROFILE.to_string(),
identities: ContractIdentities {
contract: format!("candid-core:contract:v1:sha256:{}", "0".repeat(64)),
interface: actor
.as_ref()
.map(|_| format!("candid-core:interface:v1:sha256:{}", "0".repeat(64))),
},
producer: ProducerInfo::current(),
types,
declarations,
actor,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ContractDraft {
pub types: Vec<TypeNode>,
#[serde(default)]
pub declarations: Vec<Declaration>,
#[serde(
default,
deserialize_with = "deserialize_actor_forbidding_null",
skip_serializing_if = "Option::is_none"
)]
pub actor: Option<Actor>,
#[serde(
default,
deserialize_with = "deserialize_producer_forbidding_null",
skip_serializing_if = "Option::is_none"
)]
pub producer: Option<ProducerInfo>,
}
fn deserialize_producer_forbidding_null<'de, D>(
deserializer: D,
) -> Result<Option<ProducerInfo>, D::Error>
where
D: serde::Deserializer<'de>,
{
ProducerInfo::deserialize(deserializer).map(Some)
}
impl ContractDraft {
pub fn new(types: Vec<TypeNode>, declarations: Vec<Declaration>, actor: Option<Actor>) -> Self {
Self {
types,
declarations,
actor,
producer: None,
}
}
#[must_use]
pub fn with_producer(mut self, producer: ProducerInfo) -> Self {
self.producer = Some(producer);
self
}
pub fn build(self) -> Result<Contract, ContractValidationError> {
self.build_with_limits(&Limits::default())
}
pub fn build_with_limits(self, limits: &Limits) -> Result<Contract, ContractValidationError> {
let mut budget = crate::budget::Budget::from_limits(limits);
self.build_with_budget(&mut budget)
}
pub fn build_with_context(
self,
context: &crate::RuntimeContext,
) -> Result<Contract, ContractValidationError> {
let mut budget = context.budget();
self.build_with_budget(&mut budget)
}
fn build_with_budget(
self,
budget: &mut crate::budget::Budget<'_>,
) -> Result<Contract, ContractValidationError> {
let mut contract = Contract::new_unchecked(self.types, self.declarations, self.actor);
if let Some(producer) = self.producer {
contract.producer = producer;
}
crate::validate::validate_structure_with_budget(&contract, budget)?;
Ok(
crate::canonical::canonicalize_with_mapping_unchecked_with_budget(&contract, budget)?
.contract,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RawContract {
pub format: String,
pub format_version: u32,
pub semantics_profile: String,
pub canonicalization_profile: String,
pub identities: ContractIdentities,
pub producer: ProducerInfo,
pub types: Vec<TypeNode>,
#[serde(default)]
pub declarations: Vec<Declaration>,
#[serde(
default,
deserialize_with = "deserialize_actor_forbidding_null",
skip_serializing_if = "Option::is_none"
)]
pub actor: Option<Actor>,
}
fn deserialize_actor_forbidding_null<'de, D>(deserializer: D) -> Result<Option<Actor>, D::Error>
where
D: serde::Deserializer<'de>,
{
Actor::deserialize(deserializer).map(Some)
}
impl From<&Contract> for RawContract {
fn from(contract: &Contract) -> Self {
Self {
format: contract.format.clone(),
format_version: contract.format_version,
semantics_profile: contract.semantics_profile.clone(),
canonicalization_profile: contract.canonicalization_profile.clone(),
identities: contract.identities.clone(),
producer: contract.producer.clone(),
types: contract.types.clone(),
declarations: contract.declarations.clone(),
actor: contract.actor.clone(),
}
}
}
impl Serialize for Contract {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
RawContract::from(self).serialize(serializer)
}
}