1use matter_cert::MatterCertificate;
48use matter_codec::{Tag, TlvWriter, Value};
49
50use crate::error::Error;
51use crate::state::{
52 CommissionerIdentity, ControllerState, DeviceEntry, FabricEntry, GroupKeySetConfig,
53 IcacIdentity,
54};
55
56pub(crate) const SNAPSHOT_VERSION: u8 = 1;
58
59pub(crate) fn serialize(state: &ControllerState) -> Result<Vec<u8>, Error> {
66 let mut fabrics = Vec::with_capacity(state.fabrics.len());
67 for f in &state.fabrics {
68 fabrics.push(fabric_to_value(f)?);
69 }
70 let root = Value::Structure(vec![
71 (Tag::Context(0), Value::Uint(u64::from(SNAPSHOT_VERSION))),
72 (Tag::Context(1), Value::Array(fabrics)),
73 ]);
74
75 let mut out = Vec::new();
76 let mut w = TlvWriter::new(&mut out);
77 w.write_value(Tag::Anonymous, &root)?;
78 Ok(out)
79}
80
81fn fabric_to_value(f: &FabricEntry) -> Result<Value, Error> {
82 let devices = f.devices.iter().map(device_to_value).collect();
83 let group_keys: Vec<Value> = f.group_keys.iter().map(group_key_to_value).collect();
84 let icd_clients: Vec<Value> = f
85 .icd_clients
86 .iter()
87 .map(icd_registration_to_value)
88 .collect();
89 let mut members = vec![
90 (Tag::Context(0), Value::Uint(f.fabric_id)),
91 (Tag::Context(1), Value::Bytes(f.ipk.to_vec())),
92 (Tag::Context(2), Value::Bytes(f.rcac_cert.to_tlv()?)),
93 (Tag::Context(3), Value::Bytes(f.rcac_pkcs8.clone())),
94 (Tag::Context(4), commissioner_to_value(&f.commissioner)?),
95 (Tag::Context(5), Value::Array(devices)),
96 (Tag::Context(6), Value::Array(group_keys)),
97 (
98 Tag::Context(7),
99 Value::Uint(u64::from(f.outbound_group_counter)),
100 ),
101 (Tag::Context(8), Value::Array(icd_clients)),
102 ];
103 if let Some(icac) = &f.icac {
107 members.push((Tag::Context(9), Value::Bytes(icac.cert.to_tlv()?)));
108 members.push((Tag::Context(10), Value::Bytes(icac.pkcs8.clone())));
109 }
110 Ok(Value::Structure(members))
111}
112
113fn icd_registration_to_value(r: &crate::icd::IcdRegistration) -> Value {
114 Value::Structure(vec![
115 (Tag::Context(0), Value::Uint(r.node_id)),
116 (Tag::Context(1), Value::Uint(r.check_in_node_id)),
117 (Tag::Context(2), Value::Uint(r.monitored_subject)),
118 (Tag::Context(3), Value::Bytes(r.key.to_vec())),
119 (Tag::Context(4), Value::Uint(u64::from(r.start_counter))),
120 ])
121}
122
123fn group_key_to_value(k: &GroupKeySetConfig) -> Value {
124 Value::Structure(vec![
125 (Tag::Context(0), Value::Uint(u64::from(k.key_set_id))),
126 (Tag::Context(1), Value::Bytes(k.epoch_key.to_vec())),
127 (Tag::Context(2), Value::Uint(k.epoch_start_time)),
128 ])
129}
130
131fn commissioner_to_value(c: &CommissionerIdentity) -> Result<Value, Error> {
132 Ok(Value::Structure(vec![
133 (Tag::Context(0), Value::Uint(c.node_id)),
134 (Tag::Context(1), Value::Bytes(c.operational_pkcs8.clone())),
135 (Tag::Context(2), Value::Bytes(c.noc.to_tlv()?)),
136 ]))
137}
138
139fn device_to_value(d: &DeviceEntry) -> Value {
140 let mut members = vec![
141 (Tag::Context(0), Value::Uint(d.node_id)),
142 (
143 Tag::Context(1),
144 Value::Bytes(d.peer_noc_public_key.to_vec()),
145 ),
146 ];
147 if let Some(rr) = &d.resumption_record {
148 members.push((Tag::Context(2), Value::Bytes(rr.clone())));
149 }
150 if let Some(addr) = &d.last_known_addr {
151 members.push((Tag::Context(3), Value::Utf8(addr.clone())));
152 }
153 if let Some(vid) = d.vendor_id {
154 members.push((Tag::Context(4), Value::Uint(u64::from(vid))));
155 }
156 if let Some(pid) = d.product_id {
157 members.push((Tag::Context(5), Value::Uint(u64::from(pid))));
158 }
159 if let Some(label) = &d.label {
160 members.push((Tag::Context(6), Value::Utf8(label.clone())));
161 }
162 Value::Structure(members)
163}
164
165pub(crate) fn deserialize(bytes: &[u8]) -> Result<ControllerState, Error> {
173 use matter_codec::TlvReader;
174
175 let mut r = TlvReader::new(bytes);
176 let (_tag, value) = r.read_value()?;
177 let root = as_struct(&value)?;
178
179 let version = get_uint(root, 0)?;
180 if version != u64::from(SNAPSHOT_VERSION) {
181 return Err(Error::Snapshot(format!(
182 "unsupported snapshot version {version}"
183 )));
184 }
185
186 let fabrics_val =
187 get(root, 1).ok_or_else(|| Error::Snapshot("missing fabrics array".into()))?;
188 let mut fabrics = Vec::new();
189 for fv in as_array(fabrics_val)? {
190 fabrics.push(fabric_from_value(fv)?);
191 }
192 Ok(ControllerState { fabrics })
193}
194
195fn fabric_from_value(v: &Value) -> Result<FabricEntry, Error> {
196 let m = as_struct(v)?;
197 let commissioner_val =
198 get(m, 4).ok_or_else(|| Error::Snapshot("missing commissioner".into()))?;
199 let devices_val = get(m, 5).ok_or_else(|| Error::Snapshot("missing devices array".into()))?;
200 let mut devices = Vec::new();
201 for dv in as_array(devices_val)? {
202 devices.push(device_from_value(dv)?);
203 }
204
205 let group_keys = match get(m, 6) {
209 Some(arr) => {
210 let mut keys = Vec::new();
211 for kv in as_array(arr)? {
212 keys.push(group_key_from_value(kv)?);
213 }
214 keys
215 }
216 None => Vec::new(),
217 };
218 let outbound_group_counter = match get(m, 7) {
219 Some(Value::Uint(n)) => u32::try_from(*n)
220 .map_err(|_| Error::Snapshot("outbound_group_counter exceeds u32 range".into()))?,
221 _ => 0,
222 };
223 let icd_clients = match get(m, 8) {
226 Some(arr) => {
227 let mut regs = Vec::new();
228 for rv in as_array(arr)? {
229 regs.push(icd_registration_from_value(rv)?);
230 }
231 regs
232 }
233 None => Vec::new(),
234 };
235
236 let icac = match (get(m, 9), get(m, 10)) {
243 (Some(Value::Bytes(cert_tlv)), Some(Value::Bytes(pkcs8))) => Some(IcacIdentity {
244 cert: MatterCertificate::from_tlv(cert_tlv)?,
245 pkcs8: pkcs8.clone(),
246 }),
247 _ => None,
248 };
249
250 Ok(FabricEntry {
251 fabric_id: get_uint(m, 0)?,
252 ipk: byte_array::<16>(get_bytes(m, 1)?, "ipk")?,
253 rcac_cert: MatterCertificate::from_tlv(get_bytes(m, 2)?)?,
254 rcac_pkcs8: get_bytes(m, 3)?.to_vec(),
255 commissioner: commissioner_from_value(commissioner_val)?,
256 devices,
257 group_keys,
258 outbound_group_counter,
259 icd_clients,
260 icac,
261 })
262}
263
264fn icd_registration_from_value(v: &Value) -> Result<crate::icd::IcdRegistration, Error> {
265 let m = as_struct(v)?;
266 let start_counter = u32::try_from(get_uint(m, 4)?)
267 .map_err(|_| Error::Snapshot("icd start_counter exceeds u32 range".into()))?;
268 Ok(crate::icd::IcdRegistration::new(
269 get_uint(m, 0)?,
270 get_uint(m, 1)?,
271 get_uint(m, 2)?,
272 byte_array::<16>(get_bytes(m, 3)?, "icd key")?,
273 start_counter,
274 ))
275}
276
277fn group_key_from_value(v: &Value) -> Result<GroupKeySetConfig, Error> {
278 let m = as_struct(v)?;
279 let key_set_id = u16::try_from(get_uint(m, 0)?)
280 .map_err(|_| Error::Snapshot("key_set_id exceeds u16 range".into()))?;
281 let epoch_key = byte_array::<16>(get_bytes(m, 1)?, "epoch_key")?;
282 let epoch_start_time = get_uint(m, 2)?;
283 Ok(GroupKeySetConfig::new(
284 key_set_id,
285 epoch_key,
286 epoch_start_time,
287 ))
288}
289
290fn commissioner_from_value(v: &Value) -> Result<CommissionerIdentity, Error> {
291 let m = as_struct(v)?;
292 Ok(CommissionerIdentity {
293 node_id: get_uint(m, 0)?,
294 operational_pkcs8: get_bytes(m, 1)?.to_vec(),
295 noc: MatterCertificate::from_tlv(get_bytes(m, 2)?)?,
296 })
297}
298
299fn device_from_value(v: &Value) -> Result<DeviceEntry, Error> {
300 let m = as_struct(v)?;
301 let resumption_record = match get(m, 2) {
302 Some(Value::Bytes(b)) => Some(b.clone()),
303 _ => None,
304 };
305 let last_known_addr = match get(m, 3) {
306 Some(Value::Utf8(s)) => Some(s.clone()),
307 _ => None,
308 };
309 let vendor_id = match get(m, 4) {
315 Some(Value::Uint(n)) => u16::try_from(*n).ok(),
316 _ => None,
317 };
318 let product_id = match get(m, 5) {
319 Some(Value::Uint(n)) => u16::try_from(*n).ok(),
320 _ => None,
321 };
322 let label = match get(m, 6) {
323 Some(Value::Utf8(s)) => Some(s.clone()),
324 _ => None,
325 };
326 Ok(DeviceEntry {
327 node_id: get_uint(m, 0)?,
328 peer_noc_public_key: byte_array::<65>(get_bytes(m, 1)?, "peer_noc_public_key")?,
329 resumption_record,
330 last_known_addr,
331 vendor_id,
332 product_id,
333 label,
334 })
335}
336
337fn as_struct(v: &Value) -> Result<&[(Tag, Value)], Error> {
340 match v {
341 Value::Structure(members) => Ok(members),
342 _ => Err(Error::Snapshot("expected structure".into())),
343 }
344}
345
346fn as_array(v: &Value) -> Result<&[Value], Error> {
347 match v {
348 Value::Array(items) => Ok(items),
349 _ => Err(Error::Snapshot("expected array".into())),
350 }
351}
352
353fn get(members: &[(Tag, Value)], ctx: u8) -> Option<&Value> {
354 members
355 .iter()
356 .find(|(t, _)| *t == Tag::Context(ctx))
357 .map(|(_, v)| v)
358}
359
360fn get_uint(members: &[(Tag, Value)], ctx: u8) -> Result<u64, Error> {
361 match get(members, ctx) {
362 Some(Value::Uint(n)) => Ok(*n),
363 _ => Err(Error::Snapshot(format!(
364 "missing or non-uint at context {ctx}"
365 ))),
366 }
367}
368
369fn get_bytes(members: &[(Tag, Value)], ctx: u8) -> Result<&[u8], Error> {
370 match get(members, ctx) {
371 Some(Value::Bytes(b)) => Ok(b),
372 _ => Err(Error::Snapshot(format!(
373 "missing or non-bytes at context {ctx}"
374 ))),
375 }
376}
377
378fn byte_array<const N: usize>(b: &[u8], field: &str) -> Result<[u8; N], Error> {
379 b.try_into()
380 .map_err(|_| Error::Snapshot(format!("{field}: expected {N} bytes, got {}", b.len())))
381}
382
383#[cfg(test)]
384#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
386 use super::*;
387 use crate::fabric::{create_fabric, FabricConfig};
388 use crate::store::{ControllerStore, FileStore};
389 use matter_cert::MatterTime;
390 use matter_commissioning::SystemNocRng;
391 use matter_crypto::Signer as _;
392
393 fn temp_path(name: &str) -> std::path::PathBuf {
396 use std::sync::atomic::{AtomicU32, Ordering};
397 static COUNTER: AtomicU32 = AtomicU32::new(0);
398 let uniq = COUNTER.fetch_add(1, Ordering::Relaxed);
399 let mut p = std::env::temp_dir();
400 p.push(format!(
401 "matter-controller-restart-{name}-{}-{uniq}",
402 std::process::id()
403 ));
404 let _ = std::fs::remove_file(&p);
405 let _ = std::fs::remove_file(p.with_extension("tmp"));
406 p
407 }
408
409 #[test]
414 fn commissioner_identity_is_stable_across_restart() {
415 let cfg = FabricConfig::new(
416 0x0102_0304_0506_0708,
417 1,
418 0x0000_0000_0000_0001,
419 (
420 MatterTime::from_unix_secs(1_700_000_000),
421 MatterTime::NO_EXPIRY,
422 ),
423 );
424 let fabric = create_fabric(&cfg, &SystemNocRng).expect("create_fabric");
425 let original_state = ControllerState::new(vec![fabric]);
426
427 let path = temp_path("identity");
429 let store = FileStore::new(&path);
430 store
431 .save(&serialize(&original_state).expect("serialize"))
432 .expect("save");
433
434 let loaded = store.load().expect("load").expect("snapshot present");
436 let restored = deserialize(&loaded).expect("deserialize");
437
438 let before = &original_state.fabrics[0];
439 let after = &restored.fabrics[0];
440
441 assert_eq!(after.commissioner.node_id, before.commissioner.node_id);
442 assert_eq!(
443 after.commissioner.noc.to_tlv().unwrap(),
444 before.commissioner.noc.to_tlv().unwrap(),
445 "commissioner NOC must survive restart byte-for-byte"
446 );
447
448 let signer = after
450 .commissioner_signer()
451 .expect("reload commissioner signer");
452 assert_eq!(
453 signer.public_key().as_bytes(),
454 after.commissioner.noc.public_key().as_bytes()
455 );
456 let sig_bytes = signer.sign_p256_sha256(b"post-restart").expect("sign");
457 let sig = matter_cert::Signature::new(sig_bytes);
458 signer
459 .public_key()
460 .verify(b"post-restart", &sig)
461 .expect("post-restart signature verifies");
462
463 let record = after.to_fabric_record().expect("to_fabric_record");
465 assert_eq!(record.fabric_id, cfg.fabric_id);
466
467 let _ = std::fs::remove_file(&path);
468 }
469
470 fn sample_state() -> ControllerState {
471 let cfg = FabricConfig {
472 fabric_id: 0x1122_3344_5566_7788,
473 rcac_id: 1,
474 commissioner_node_id: 0x0000_0000_0000_0001,
475 validity: (
476 MatterTime::from_unix_secs(1_700_000_000),
477 MatterTime::NO_EXPIRY,
478 ),
479 issue_icac: false,
480 };
481 let mut fabric = create_fabric(&cfg, &SystemNocRng).expect("create_fabric");
482 fabric.devices.push(DeviceEntry {
483 node_id: 0xABCD,
484 peer_noc_public_key: [0x04; 65],
485 resumption_record: Some(vec![1, 2, 3, 4]),
486 last_known_addr: Some("[fe80::1]:5540".to_string()),
487 vendor_id: None,
488 product_id: None,
489 label: None,
490 });
491 fabric.devices.push(DeviceEntry {
492 node_id: 0xBEEF,
493 peer_noc_public_key: [0x04; 65],
494 resumption_record: None,
495 last_known_addr: None,
496 vendor_id: None,
497 product_id: None,
498 label: None,
499 });
500 ControllerState {
501 fabrics: vec![fabric],
502 }
503 }
504
505 #[test]
506 fn round_trips_a_full_state() {
507 let state = sample_state();
508 let bytes = serialize(&state).expect("serialize");
509 let back = deserialize(&bytes).expect("deserialize");
510
511 assert_eq!(back.fabrics.len(), 1);
512 let (a, b) = (&state.fabrics[0], &back.fabrics[0]);
513 assert_eq!(a.fabric_id, b.fabric_id);
514 assert_eq!(a.ipk, b.ipk);
515 assert_eq!(a.rcac_pkcs8, b.rcac_pkcs8);
516 assert_eq!(a.rcac_cert.to_tlv().unwrap(), b.rcac_cert.to_tlv().unwrap());
517 assert_eq!(a.commissioner.node_id, b.commissioner.node_id);
518 assert_eq!(
519 a.commissioner.operational_pkcs8,
520 b.commissioner.operational_pkcs8
521 );
522 assert_eq!(
523 a.commissioner.noc.to_tlv().unwrap(),
524 b.commissioner.noc.to_tlv().unwrap()
525 );
526 assert_eq!(a.devices.len(), b.devices.len());
527 assert_eq!(a.devices[0].node_id, b.devices[0].node_id);
528 assert_eq!(
529 a.devices[0].resumption_record,
530 b.devices[0].resumption_record
531 );
532 assert_eq!(a.devices[0].last_known_addr, b.devices[0].last_known_addr);
533 assert_eq!(a.devices[1].resumption_record, None);
534 assert_eq!(a.devices[1].last_known_addr, None);
535 }
536
537 #[test]
538 fn empty_state_round_trips() {
539 let bytes = serialize(&ControllerState::default()).expect("serialize");
540 assert!(deserialize(&bytes).expect("deserialize").fabrics.is_empty());
541 }
542
543 #[test]
544 fn rejects_unknown_version() {
545 let root = Value::Structure(vec![
547 (Tag::Context(0), Value::Uint(99)),
548 (Tag::Context(1), Value::Array(vec![])),
549 ]);
550 let mut out = Vec::new();
551 let mut w = TlvWriter::new(&mut out);
552 w.write_value(Tag::Anonymous, &root).unwrap();
553 let err = deserialize(&out).expect_err("must reject");
554 assert!(matches!(err, Error::Snapshot(_)));
555 }
556
557 use proptest::prelude::*;
560 use std::sync::OnceLock;
561
562 fn shared_fabric() -> &'static FabricEntry {
565 static FABRIC: OnceLock<FabricEntry> = OnceLock::new();
566 FABRIC.get_or_init(|| {
567 let cfg = FabricConfig {
568 fabric_id: 0x0102_0304_0506_0708,
569 rcac_id: 1,
570 commissioner_node_id: 0x0000_0000_0000_0001,
571 validity: (
572 MatterTime::from_unix_secs(1_700_000_000),
573 MatterTime::NO_EXPIRY,
574 ),
575 issue_icac: false,
576 };
577 create_fabric(&cfg, &SystemNocRng).expect("mint shared fabric")
578 })
579 }
580
581 prop_compose! {
582 fn arb_device()(
583 node_id in any::<u64>(),
584 pk in prop::collection::vec(any::<u8>(), 65),
585 rr in prop::option::of(prop::collection::vec(any::<u8>(), 0..40)),
586 addr in prop::option::of("[ -~]{0,32}"),
587 ) -> DeviceEntry {
588 let mut peer_noc_public_key = [0u8; 65];
589 peer_noc_public_key.copy_from_slice(&pk);
590 DeviceEntry {
591 node_id,
592 peer_noc_public_key,
593 resumption_record: rr,
594 last_known_addr: addr,
595 vendor_id: None,
596 product_id: None,
597 label: None,
598 }
599 }
600 }
601
602 proptest! {
603 #[test]
605 fn snapshot_round_trips(devices in prop::collection::vec(arb_device(), 0..6)) {
606 let mut fabric = shared_fabric().clone();
607 fabric.devices = devices.clone();
608 let state = ControllerState { fabrics: vec![fabric] };
609
610 let bytes = serialize(&state).expect("serialize");
611 let back = deserialize(&bytes).expect("deserialize");
612
613 prop_assert_eq!(back.fabrics.len(), 1);
614 let dev_back = &back.fabrics[0].devices;
615 prop_assert_eq!(dev_back.len(), devices.len());
616 for (a, b) in devices.iter().zip(dev_back.iter()) {
617 prop_assert_eq!(a.node_id, b.node_id);
618 prop_assert_eq!(a.peer_noc_public_key, b.peer_noc_public_key);
619 prop_assert_eq!(&a.resumption_record, &b.resumption_record);
620 prop_assert_eq!(&a.last_known_addr, &b.last_known_addr);
621 }
622 }
623 }
624
625 #[test]
628 fn group_keys_round_trip() {
629 let mut fabric = shared_fabric().clone();
632 fabric.group_keys = vec![
633 GroupKeySetConfig::new(0x0001, [0xAA; 16], 1_700_000_000),
634 GroupKeySetConfig::new(0x0002, [0xBB; 16], 1_700_100_000),
635 ];
636 fabric.outbound_group_counter = 42;
637 let state = ControllerState {
638 fabrics: vec![fabric.clone()],
639 };
640
641 let bytes = serialize(&state).expect("serialize");
642 let back = deserialize(&bytes).expect("deserialize");
643
644 assert_eq!(back.fabrics.len(), 1);
645 let f = &back.fabrics[0];
646 assert_eq!(f.outbound_group_counter, 42);
647 assert_eq!(f.group_keys.len(), 2);
648 assert_eq!(f.group_keys[0].key_set_id, 0x0001);
649 assert_eq!(f.group_keys[0].epoch_key, [0xAA; 16]);
650 assert_eq!(f.group_keys[0].epoch_start_time, 1_700_000_000);
651 assert_eq!(f.group_keys[1].key_set_id, 0x0002);
652 assert_eq!(f.group_keys[1].epoch_key, [0xBB; 16]);
653 assert_eq!(f.group_keys[1].epoch_start_time, 1_700_100_000);
654 }
655
656 #[test]
657 fn icd_clients_round_trip() {
658 let mut fabric = shared_fabric().clone();
661 fabric.icd_clients = vec![
662 crate::icd::IcdRegistration::new(0x0042, 1, 1, [0xCC; 16], 7),
663 crate::icd::IcdRegistration::new(0x0043, 1, 2, [0xDD; 16], 99),
664 ];
665 let state = ControllerState {
666 fabrics: vec![fabric.clone()],
667 };
668 let bytes = serialize(&state).expect("serialize");
669 let back = deserialize(&bytes).expect("deserialize");
670 assert_eq!(back.fabrics[0].icd_clients, fabric.icd_clients);
671 }
672
673 fn sample_icac(fabric: &FabricEntry) -> crate::state::IcacIdentity {
678 use matter_cert::operational::{icac, sign_with_ring, IcacParams};
679 use matter_cert::PublicKey;
680 use matter_crypto::{RingSigner, Signer};
681
682 let (icac_signer, icac_pkcs8) = RingSigner::generate().expect("generate icac key");
683 let icac_public_key =
684 PublicKey::new(*icac_signer.public_key().as_bytes()).expect("valid P-256 public key");
685 let issuer_skid = fabric
686 .rcac_cert
687 .extensions()
688 .subject_key_identifier
689 .expect("rcac has SKID");
690
691 let unsigned = icac(IcacParams::new(
692 0x0000_0000_0000_0099,
693 fabric.rcac_cert.subject().clone(),
694 issuer_skid,
695 icac_public_key,
696 vec![0x01],
697 MatterTime::from_unix_secs(1_700_000_000),
698 MatterTime::NO_EXPIRY,
699 ))
700 .expect("build unsigned icac");
701 let cert = sign_with_ring(unsigned, &fabric.rcac_pkcs8).expect("sign icac");
702
703 crate::state::IcacIdentity {
704 cert,
705 pkcs8: icac_pkcs8,
706 }
707 }
708
709 #[test]
710 fn snapshot_round_trips_fabric_with_icac() {
711 let mut fabric = shared_fabric().clone();
714 let icac_identity = sample_icac(&fabric);
715 fabric.icac = Some(icac_identity);
716
717 let state = ControllerState {
718 fabrics: vec![fabric.clone()],
719 };
720 let bytes = serialize(&state).expect("serialize");
721 let back = deserialize(&bytes).expect("deserialize");
722
723 let want = fabric.icac.as_ref().expect("icac set on input fabric");
724 let got = back.fabrics[0]
725 .icac
726 .as_ref()
727 .expect("icac must round-trip as Some");
728 assert_eq!(got.cert.to_tlv().unwrap(), want.cert.to_tlv().unwrap());
729 assert_eq!(got.pkcs8, want.pkcs8);
730 }
731
732 #[test]
733 fn snapshot_without_icac_is_backward_compatible() {
734 let fabric = shared_fabric().clone();
738 assert!(fabric.icac.is_none());
739 let state = ControllerState {
740 fabrics: vec![fabric],
741 };
742
743 let bytes = serialize(&state).expect("serialize");
744 let back = deserialize(&bytes).expect("deserialize");
745 assert!(back.fabrics[0].icac.is_none());
746
747 let mut r = matter_codec::TlvReader::new(&bytes);
753 let (_tag, root) = r.read_value().expect("read root");
754 let root_members = as_struct(&root).expect("root struct");
755 let fabrics_arr = get(root_members, 1).expect("fabrics array");
756 let first_fabric = &as_array(fabrics_arr).expect("fabrics array")[0];
757 let fabric_members = as_struct(first_fabric).expect("fabric struct");
758 assert!(
759 get(fabric_members, 9).is_none(),
760 "C9 (icac_cert) must be absent when icac is None"
761 );
762 assert!(
763 get(fabric_members, 10).is_none(),
764 "C10 (icac_pkcs8) must be absent when icac is None"
765 );
766 }
767
768 #[test]
771 fn device_metadata_round_trips_and_defaults_none() {
772 let mut fabric = shared_fabric().clone();
775 fabric.devices = vec![DeviceEntry {
776 node_id: 0x1234,
777 peer_noc_public_key: [0x04; 65],
778 resumption_record: None,
779 last_known_addr: None,
780 vendor_id: Some(0xFFF1),
781 product_id: Some(0x8000),
782 label: Some("kitchen plug".to_string()),
783 }];
784 let state = ControllerState {
785 fabrics: vec![fabric],
786 };
787 let bytes = serialize(&state).expect("serialize");
788 let back = deserialize(&bytes).expect("deserialize");
789 let d = &back.fabrics[0].devices[0];
790 assert_eq!(d.vendor_id, Some(0xFFF1));
791 assert_eq!(d.product_id, Some(0x8000));
792 assert_eq!(d.label.as_deref(), Some("kitchen plug"));
793
794 let old_device_val = Value::Structure(vec![
799 (Tag::Context(0), Value::Uint(0x9999)),
800 (Tag::Context(1), Value::Bytes(vec![0x04; 65])),
801 ]);
802 let old_device = device_from_value(&old_device_val).expect("old device must load");
803 assert_eq!(old_device.vendor_id, None);
804 assert_eq!(old_device.product_id, None);
805 assert_eq!(old_device.label, None);
806 }
807
808 #[test]
809 fn old_snapshot_without_t6_t7_loads_with_defaults() {
810 let fabric = shared_fabric().clone();
814
815 let old_fabric_val = Value::Structure(vec![
817 (Tag::Context(0), Value::Uint(fabric.fabric_id)),
818 (Tag::Context(1), Value::Bytes(fabric.ipk.to_vec())),
819 (
820 Tag::Context(2),
821 Value::Bytes(fabric.rcac_cert.to_tlv().expect("rcac tlv")),
822 ),
823 (Tag::Context(3), Value::Bytes(fabric.rcac_pkcs8.clone())),
824 (
825 Tag::Context(4),
826 commissioner_to_value(&fabric.commissioner).expect("commissioner"),
827 ),
828 (Tag::Context(5), Value::Array(vec![])), ]);
830
831 let root = Value::Structure(vec![
832 (Tag::Context(0), Value::Uint(u64::from(SNAPSHOT_VERSION))),
833 (Tag::Context(1), Value::Array(vec![old_fabric_val])),
834 ]);
835
836 let mut out = Vec::new();
837 let mut w = TlvWriter::new(&mut out);
838 w.write_value(Tag::Anonymous, &root).unwrap();
839
840 let back = deserialize(&out).expect("old snapshot must load without error");
841 assert_eq!(back.fabrics.len(), 1);
842 let f = &back.fabrics[0];
843 assert!(
844 f.group_keys.is_empty(),
845 "group_keys must default to empty for old snapshot"
846 );
847 assert_eq!(
848 f.outbound_group_counter, 0,
849 "outbound_group_counter must default to 0 for old snapshot"
850 );
851 }
852}