@@ -203,6 +203,8 @@ mod test_breeding_genetics;
203203#[ cfg( test) ]
204204mod test_pet_birthday_validation;
205205#[ cfg( test) ]
206+ mod test_microchip_normalization;
207+ #[ cfg( test) ]
206208mod test_verify_claim_document;
207209#[ cfg( test) ]
208210mod test_vet_pagination;
@@ -325,6 +327,7 @@ const MAX_SUPPORTED_LANGUAGES: u32 = 50;
325327
326328/// Maximum byte length of a `color` field (pet registration).
327329const MAX_COLOR_LEN : u32 = 50 ;
330+ const MAX_MICROCHIP_ID_LEN : usize = 64 ;
328331
329332/// Maximum byte length of a `BehaviorRecord::description`.
330333const 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 ,
0 commit comments