Skip to content

Commit 89d5729

Browse files
damilare0813zeemscriptllinsss
authored
feat: canonicalize pet identifiers (#1268)
Co-authored-by: Sakariyah Abdulhazeem <sakariyahabdulhazeem@gmail.com> Co-authored-by: llins <llinsomoudu@gmail.com>
1 parent 439a492 commit 89d5729

2 files changed

Lines changed: 144 additions & 3 deletions

File tree

stellar-contracts/src/lib.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ mod test_breeding_genetics;
203203
#[cfg(test)]
204204
mod test_pet_birthday_validation;
205205
#[cfg(test)]
206+
mod test_microchip_normalization;
207+
#[cfg(test)]
206208
mod test_verify_claim_document;
207209
#[cfg(test)]
208210
mod test_vet_pagination;
@@ -325,6 +327,7 @@ const MAX_SUPPORTED_LANGUAGES: u32 = 50;
325327

326328
/// Maximum byte length of a `color` field (pet registration).
327329
const MAX_COLOR_LEN: u32 = 50;
330+
const MAX_MICROCHIP_ID_LEN: usize = 64;
328331

329332
/// Maximum byte length of a `BehaviorRecord::description`.
330333
const MAX_BEHAVIOR_DESC_LEN: u32 = 500;
@@ -1280,7 +1283,8 @@ pub enum DataKey {
12801283
NonceUsage((u64, String, Bytes)),
12811284
RetentionPeriod,
12821285
MaxSubscriptionsPerAddress,
1283-
VetCredentialsExpiry(Address), // optional u64 expiry; 0/absent = no expiry
1286+
/// Canonical microchip identifier -> pet id.
1287+
MicrochipIndex(String),
12841288
}
12851289

12861290
#[contracttype]
@@ -4822,6 +4826,14 @@ impl PetChainContract {
48224826
}
48234827
Self::validate_pet_name(&env, &name);
48244828
Self::validate_breed(&env, &species, &breed);
4829+
let canonical_microchip = microchip_id
4830+
.as_ref()
4831+
.map(|value| Self::canonicalize_microchip_id(&env, value));
4832+
if let Some(ref identifier) = canonical_microchip {
4833+
if env.storage().instance().has(&DataKey::MicrochipIndex(identifier.clone())) {
4834+
panic_with_error!(&env, ContractError::InvalidInput);
4835+
}
4836+
}
48254837
// Bound color field to prevent unbounded ledger entries. (#1152)
48264838
if color.len() > MAX_COLOR_LEN {
48274839
panic_with_error!(&env, ContractError::InputStringTooLong);
@@ -4919,12 +4931,17 @@ impl PetChainContract {
49194931
gender,
49204932
color,
49214933
weight,
4922-
microchip_id,
4934+
microchip_id: canonical_microchip,
49234935
photo_hashes: Vec::new(&env),
49244936
};
49254937

49264938
env.storage().instance().set(&DataKey::Pet(pet_id), &pet);
49274939
env.storage().instance().set(&DataKey::PetCount, &pet_id);
4940+
if let Some(ref identifier) = pet.microchip_id {
4941+
env.storage()
4942+
.instance()
4943+
.set(&DataKey::MicrochipIndex(identifier.clone()), &pet_id);
4944+
}
49284945

49294946
PetChainContract::log_ownership_change(
49304947
&env,
@@ -5047,7 +5064,29 @@ impl PetChainContract {
50475064
pet.privacy_level = privacy_level;
50485065
pet.color = color;
50495066
pet.weight = weight;
5050-
pet.microchip_id = microchip_id;
5067+
let canonical_microchip = microchip_id
5068+
.as_ref()
5069+
.map(|value| Self::canonicalize_microchip_id(&env, value));
5070+
if canonical_microchip.as_ref() != pet.microchip_id.as_ref() {
5071+
if let Some(ref identifier) = canonical_microchip {
5072+
if let Some(existing_id) = env
5073+
.storage()
5074+
.instance()
5075+
.get::<DataKey, u64>(&DataKey::MicrochipIndex(identifier.clone()))
5076+
{
5077+
if existing_id != id {
5078+
panic_with_error!(&env, ContractError::InvalidInput);
5079+
}
5080+
}
5081+
}
5082+
if let Some(ref previous) = pet.microchip_id {
5083+
env.storage().instance().remove(&DataKey::MicrochipIndex(previous.clone()));
5084+
}
5085+
if let Some(ref identifier) = canonical_microchip {
5086+
env.storage().instance().set(&DataKey::MicrochipIndex(identifier.clone()), &id);
5087+
}
5088+
}
5089+
pet.microchip_id = canonical_microchip;
50515090
pet.updated_at = env.ledger().timestamp();
50525091

50535092
env.storage().instance().set(&DataKey::Pet(id), &pet);
@@ -9173,6 +9212,36 @@ impl PetChainContract {
91739212
}
91749213
}
91759214

9215+
/// Canonical form is trimmed, ASCII upper-case, and separator-free. Only
9216+
/// ASCII letters and digits are accepted after separators are removed;
9217+
/// this deliberately rejects Unicode lookalikes and ambiguous encodings.
9218+
fn canonicalize_microchip_id(env: &Env, value: &String) -> String {
9219+
let len = value.len() as usize;
9220+
if len == 0 || len > MAX_MICROCHIP_ID_LEN {
9221+
panic_with_error!(env, ContractError::InvalidInput);
9222+
}
9223+
let mut input = [0u8; MAX_MICROCHIP_ID_LEN];
9224+
value.copy_into_slice(&mut input[..len]);
9225+
let mut output = [0u8; MAX_MICROCHIP_ID_LEN];
9226+
let mut out_len = 0usize;
9227+
for byte in input.iter().take(len) {
9228+
if matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | b'-' | b':' | b'.') {
9229+
continue;
9230+
}
9231+
let canonical = match byte {
9232+
b'a'..=b'z' => byte.to_ascii_uppercase(),
9233+
b'A'..=b'Z' | b'0'..=b'9' => *byte,
9234+
_ => panic_with_error!(env, ContractError::InvalidInput),
9235+
};
9236+
output[out_len] = canonical;
9237+
out_len += 1;
9238+
}
9239+
if out_len == 0 {
9240+
panic_with_error!(env, ContractError::InvalidInput);
9241+
}
9242+
String::from_bytes(env, &output[..out_len])
9243+
}
9244+
91769245
/// Validate breed against the species-specific whitelist stored on-chain.
91779246
/// If no whitelist has been set for the species, any non-empty breed is accepted.
91789247
fn validate_breed(env: &Env, species: &Species, breed: &String) {
@@ -12948,6 +13017,36 @@ impl PetChainContract {
1294813017
.set(&SystemKey::StorageSchemaVersion, &target_version);
1294913018
}
1295013019

13020+
/// Rebuild the canonical microchip index for existing records. The work
13021+
/// is bounded so large deployments can retry in batches. A collision or
13022+
/// invalid legacy value aborts the batch with InvalidInput.
13023+
pub fn migrate_microchip_index(env: Env, admin: Address, start: u64, limit: u64) -> u64 {
13024+
Self::require_admin_auth(&env, &admin);
13025+
let total: u64 = env.storage().instance().get(&DataKey::PetCount).unwrap_or(0);
13026+
let end = start.saturating_add(limit).min(total);
13027+
let mut cursor = start;
13028+
while cursor < end {
13029+
let pet_id = cursor + 1;
13030+
if let Some(mut pet) = env.storage().instance().get::<DataKey, Pet>(&DataKey::Pet(pet_id)) {
13031+
if let Some(ref legacy) = pet.microchip_id {
13032+
let canonical = Self::canonicalize_microchip_id(&env, legacy);
13033+
if let Some(existing) = env.storage().instance().get::<DataKey, u64>(&DataKey::MicrochipIndex(canonical.clone())) {
13034+
if existing != pet_id {
13035+
panic_with_error!(&env, ContractError::InvalidInput);
13036+
}
13037+
}
13038+
if *legacy != canonical {
13039+
pet.microchip_id = Some(canonical.clone());
13040+
env.storage().instance().set(&DataKey::Pet(pet_id), &pet);
13041+
}
13042+
env.storage().instance().set(&DataKey::MicrochipIndex(canonical), &pet_id);
13043+
}
13044+
}
13045+
cursor += 1;
13046+
}
13047+
end
13048+
}
13049+
1295113050
pub fn migrate_storage(
1295213051
env: Env,
1295313052
admin: Address,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use crate::{DataKey, Gender, Pet, PetChainContract, PetChainContractClient, PrivacyLevel, Species};
2+
use soroban_sdk::{testutils::Address as _, Address, Env, String};
3+
4+
fn setup(env: &Env) -> (PetChainContractClient<'_>, Address) {
5+
let contract_id = env.register_contract(None, PetChainContract);
6+
let client = PetChainContractClient::new(env, &contract_id);
7+
let owner = Address::generate(env);
8+
(client, owner)
9+
}
10+
11+
fn register(client: &PetChainContractClient, env: &Env, owner: &Address, chip: &str) -> u64 {
12+
client.register_pet(owner, &String::from_str(env, "Buddy"), &String::from_str(env, "2020-01-01"), &Gender::Male, &Species::Dog, &String::from_str(env, "Labrador"), &String::from_str(env, "Brown"), &10, &Some(String::from_str(env, chip)), &PrivacyLevel::Public)
13+
}
14+
15+
#[test]
16+
fn canonicalizes_case_whitespace_and_separators() {
17+
let env = Env::default();
18+
env.mock_all_auths();
19+
let (client, owner) = setup(&env);
20+
let id = register(&client, &env, &owner, " ab-12:cd.34 ");
21+
let pet: Pet = env.storage().instance().get(&DataKey::Pet(id)).unwrap();
22+
assert_eq!(pet.microchip_id, Some(String::from_str(&env, "AB12CD34")));
23+
}
24+
25+
#[test]
26+
#[should_panic]
27+
fn canonical_collisions_are_rejected() {
28+
let env = Env::default();
29+
env.mock_all_auths();
30+
let (client, owner) = setup(&env);
31+
register(&client, &env, &owner, "AB-12");
32+
register(&client, &env, &owner, " ab12 ");
33+
}
34+
35+
#[test]
36+
#[should_panic]
37+
fn unicode_lookalikes_are_rejected() {
38+
let env = Env::default();
39+
env.mock_all_auths();
40+
let (client, owner) = setup(&env);
41+
register(&client, &env, &owner, "AB12");
42+
}

0 commit comments

Comments
 (0)