use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{error::schema, model::Contract, GenerateError};
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Lock {
pub format: u32,
pub schema_hash: String,
pub contract: Contract,
pub retired_ids: BTreeSet<u32>,
}
pub(crate) fn update(
contract: &Contract,
hash: &str,
previous: Option<&[u8]>,
) -> Result<Lock, GenerateError> {
let mut retired_ids = BTreeSet::new();
let mut history = contract.clone();
if let Some(bytes) = previous {
let old: Lock = serde_json::from_slice(bytes).map_err(|e| {
schema(
"ipc-lock",
"compatibility lock",
e.to_string(),
"restore a valid checked-in lock; do not discard compatibility history",
)
})?;
if old.format != 1
|| old.contract.name != contract.name
|| contract.major < old.contract.major
{
return Err(schema(
"ipc-lock-contract",
&contract.name,
"lock format, contract name, or major is incompatible",
"retain the contract identity and use a nondecreasing major version",
));
}
retired_ids = old.retired_ids;
for method in &contract.methods {
if old
.contract
.methods
.iter()
.any(|m| m.name == method.name && m.id != method.id)
{
return Err(schema(
"ipc-method-id-change",
&method.name,
"existing method ID changed",
"retain the original ID; introduce a newly named method for a new identity",
));
}
if retired_ids.contains(&method.id) {
return Err(schema(
"ipc-retired-id",
&method.name,
"method ID has been retired",
"assign a new ID; retired IDs must never be reused",
));
}
if let Some(previous) = old.contract.methods.iter().find(|m| m.id == method.id) {
if previous.name != method.name {
return Err(schema("ipc-method-rename", &method.name, "method ID belongs to a different name", "for an intentional rename, explicitly update the checked-in lock name while retaining its ID"));
}
if (previous.receiver != method.receiver
|| previous.kind != method.kind
|| previous.input != method.input
|| previous.output != method.output
|| previous.development_only != method.development_only)
&& contract.major == old.contract.major
{
return Err(schema(
"ipc-breaking-method",
&method.name,
"method contract changed without a major version increment",
"increment contract_major and deploy both endpoints together",
));
}
}
}
for method in &old.contract.methods {
if !contract.methods.iter().any(|m| m.id == method.id) {
retired_ids.insert(method.id);
}
}
check_messages(contract, &old.contract)?;
check_enums(contract, &old.contract)?;
for message in old.contract.messages {
if !history.messages.iter().any(|m| m.name == message.name) {
history.messages.push(message);
}
}
for enumeration in old.contract.enums {
if !history.enums.iter().any(|e| e.name == enumeration.name) {
history.enums.push(enumeration);
}
}
}
history.messages.sort_by(|a, b| a.name.cmp(&b.name));
history.enums.sort_by(|a, b| a.name.cmp(&b.name));
Ok(Lock {
format: 1,
schema_hash: hash.into(),
contract: history,
retired_ids,
})
}
fn check_messages(new: &Contract, old: &Contract) -> Result<(), GenerateError> {
for previous in &old.messages {
let Some(message) = new.messages.iter().find(|m| m.name == previous.name) else {
continue;
};
for field in &previous.fields {
if let Some(current) = message.fields.iter().find(|f| f.number == field.number) {
if current.kind != field.kind
|| current.target != field.target
|| current.repeated != field.repeated
|| current.map != field.map
{
return Err(schema("ipc-field-type", &message.name, format!("field {} changes its protobuf type", field.number), "never change an existing field type; reserve the old number and add a new field"));
}
if (current.optional != field.optional
|| current.oneof != field.oneof
|| current.name != field.name)
&& new.major == old.major
{
return Err(schema(
"ipc-breaking-field",
&message.name,
format!("field {} changes its application API", field.number),
"increment contract_major for presence, oneof, or naming changes",
));
}
} else {
let reserved = message.reserved.iter().any(|(start, end)| {
i64::from(*start) <= i64::from(field.number)
&& i64::from(field.number) < i64::from(*end)
});
if !reserved || !message.reserved_names.contains(&field.name) {
return Err(schema(
"ipc-field-retirement",
&message.name,
format!(
"removed field {} is not reserved by number and name",
field.number
),
"add reserved declarations for the removed field number and name",
));
}
}
}
for field in &message.fields {
if previous.reserved.iter().any(|(a, b)| {
i64::from(*a) <= i64::from(field.number) && i64::from(field.number) < i64::from(*b)
}) || previous.reserved_names.contains(&field.name)
{
return Err(schema(
"ipc-retired-field",
&message.name,
"reserved field number or name was reused",
"choose a fresh field number and name",
));
}
}
for (a, b) in &previous.reserved {
if !message.reserved.iter().any(|(c, d)| c <= a && d >= b) {
return Err(schema(
"ipc-reservation-history",
&message.name,
"field reservation was removed",
"retain all reserved field ranges and names",
));
}
}
if previous
.reserved_names
.iter()
.any(|n| !message.reserved_names.contains(n))
{
return Err(schema(
"ipc-reservation-history",
&message.name,
"field name reservation was removed",
"retain all reserved names",
));
}
}
Ok(())
}
fn check_enums(new: &Contract, old: &Contract) -> Result<(), GenerateError> {
for previous in &old.enums {
let Some(current) = new.enums.iter().find(|e| e.name == previous.name) else {
continue;
};
for (name, number) in &previous.values {
match current.values.get(name) {
Some(value) if value == number => {}
Some(_) => {
return Err(schema(
"ipc-enum-number",
¤t.name,
"enum value changed number",
"retain the original numeric value",
))
}
None => {
if !current.reserved_names.contains(name)
|| !current
.reserved
.iter()
.any(|(a, b)| a <= number && number <= b)
{
return Err(schema(
"ipc-enum-retirement",
¤t.name,
"removed enum value is not reserved",
"reserve the removed enum name and number",
));
}
}
}
}
for (name, number) in ¤t.values {
if previous.reserved_names.contains(name)
|| previous
.reserved
.iter()
.any(|(a, b)| a <= number && number <= b)
{
return Err(schema(
"ipc-retired-enum",
¤t.name,
"retired enum value was reused",
"use a fresh enum name and number",
));
}
}
if previous
.reserved_names
.iter()
.any(|n| !current.reserved_names.contains(n))
|| previous
.reserved
.iter()
.any(|(a, b)| !current.reserved.iter().any(|(c, d)| c <= a && b <= d))
{
return Err(schema(
"ipc-reservation-history",
¤t.name,
"enum reservation was removed",
"retain all reserved enum numbers and names",
));
}
}
Ok(())
}