Skip to main content

canic_backup/restore/plan/
mod.rs

1use crate::manifest::{
2    FleetBackupManifest, FleetMember, IdentityMode, ManifestDesignConformanceReport,
3    ManifestValidationError, SourceSnapshot, VerificationCheck, VerificationPlan,
4};
5use candid::Principal;
6use serde::{Deserialize, Serialize};
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    str::FromStr,
10};
11use thiserror::Error as ThisError;
12
13///
14/// RestoreMapping
15///
16
17#[derive(Clone, Debug, Default, Deserialize, Serialize)]
18pub struct RestoreMapping {
19    pub members: Vec<RestoreMappingEntry>,
20}
21
22impl RestoreMapping {
23    /// Resolve the target canister for one source member.
24    fn target_for(&self, source_canister: &str) -> Option<&str> {
25        self.members
26            .iter()
27            .find(|entry| entry.source_canister == source_canister)
28            .map(|entry| entry.target_canister.as_str())
29    }
30}
31
32///
33/// RestoreMappingEntry
34///
35
36#[derive(Clone, Debug, Deserialize, Serialize)]
37pub struct RestoreMappingEntry {
38    pub source_canister: String,
39    pub target_canister: String,
40}
41
42///
43/// RestorePlan
44///
45
46#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
47pub struct RestorePlan {
48    pub backup_id: String,
49    pub source_environment: String,
50    pub source_root_canister: String,
51    pub topology_hash: String,
52    pub member_count: usize,
53    pub identity_summary: RestoreIdentitySummary,
54    pub snapshot_summary: RestoreSnapshotSummary,
55    pub verification_summary: RestoreVerificationSummary,
56    pub readiness_summary: RestoreReadinessSummary,
57    pub operation_summary: RestoreOperationSummary,
58    pub ordering_summary: RestoreOrderingSummary,
59    #[serde(default)]
60    pub design_conformance: Option<ManifestDesignConformanceReport>,
61    #[serde(default)]
62    pub fleet_verification_checks: Vec<VerificationCheck>,
63    pub phases: Vec<RestorePhase>,
64}
65
66impl RestorePlan {
67    /// Return all planned members in execution order.
68    #[must_use]
69    pub fn ordered_members(&self) -> Vec<&RestorePlanMember> {
70        self.phases
71            .iter()
72            .flat_map(|phase| phase.members.iter())
73            .collect()
74    }
75}
76
77///
78/// RestoreIdentitySummary
79///
80
81#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
82pub struct RestoreIdentitySummary {
83    pub mapping_supplied: bool,
84    pub all_sources_mapped: bool,
85    pub fixed_members: usize,
86    pub relocatable_members: usize,
87    pub in_place_members: usize,
88    pub mapped_members: usize,
89    pub remapped_members: usize,
90}
91
92///
93/// RestoreSnapshotSummary
94///
95
96#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
97#[expect(
98    clippy::struct_excessive_bools,
99    reason = "restore summaries intentionally expose machine-readable readiness flags"
100)]
101pub struct RestoreSnapshotSummary {
102    pub all_members_have_module_hash: bool,
103    pub all_members_have_wasm_hash: bool,
104    pub all_members_have_code_version: bool,
105    pub all_members_have_checksum: bool,
106    pub members_with_module_hash: usize,
107    pub members_with_wasm_hash: usize,
108    pub members_with_code_version: usize,
109    pub members_with_checksum: usize,
110}
111
112///
113/// RestoreVerificationSummary
114///
115
116#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
117pub struct RestoreVerificationSummary {
118    pub verification_required: bool,
119    pub all_members_have_checks: bool,
120    pub fleet_checks: usize,
121    pub member_check_groups: usize,
122    pub member_checks: usize,
123    pub members_with_checks: usize,
124    pub total_checks: usize,
125}
126
127///
128/// RestoreReadinessSummary
129///
130
131#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
132pub struct RestoreReadinessSummary {
133    pub ready: bool,
134    pub reasons: Vec<String>,
135}
136
137///
138/// RestoreOperationSummary
139///
140
141#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
142pub struct RestoreOperationSummary {
143    #[serde(default)]
144    pub planned_snapshot_uploads: usize,
145    pub planned_snapshot_loads: usize,
146    pub planned_code_reinstalls: usize,
147    pub planned_verification_checks: usize,
148    #[serde(default)]
149    pub planned_operations: usize,
150    pub planned_phases: usize,
151}
152
153impl RestoreOperationSummary {
154    /// Return planned snapshot uploads, deriving the value for older plan JSON.
155    #[must_use]
156    pub const fn effective_planned_snapshot_uploads(&self, member_count: usize) -> usize {
157        if self.planned_snapshot_uploads == 0 && member_count > 0 {
158            return member_count;
159        }
160
161        self.planned_snapshot_uploads
162    }
163
164    /// Return total planned operations, deriving the value for older plan JSON.
165    #[must_use]
166    pub const fn effective_planned_operations(&self, member_count: usize) -> usize {
167        if self.planned_operations == 0 {
168            return self.effective_planned_snapshot_uploads(member_count)
169                + self.planned_snapshot_loads
170                + self.planned_code_reinstalls
171                + self.planned_verification_checks;
172        }
173
174        self.planned_operations
175    }
176}
177
178///
179/// RestoreOrderingSummary
180///
181
182#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
183pub struct RestoreOrderingSummary {
184    pub phase_count: usize,
185    pub dependency_free_members: usize,
186    pub in_group_parent_edges: usize,
187    pub cross_group_parent_edges: usize,
188}
189
190///
191/// RestorePhase
192///
193
194#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
195pub struct RestorePhase {
196    pub restore_group: u16,
197    pub members: Vec<RestorePlanMember>,
198}
199
200///
201/// RestorePlanMember
202///
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
205pub struct RestorePlanMember {
206    pub source_canister: String,
207    pub target_canister: String,
208    pub role: String,
209    pub parent_source_canister: Option<String>,
210    pub parent_target_canister: Option<String>,
211    pub ordering_dependency: Option<RestoreOrderingDependency>,
212    pub phase_order: usize,
213    pub restore_group: u16,
214    pub identity_mode: IdentityMode,
215    pub verification_class: String,
216    pub verification_checks: Vec<VerificationCheck>,
217    pub source_snapshot: SourceSnapshot,
218}
219
220///
221/// RestoreOrderingDependency
222///
223
224#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
225pub struct RestoreOrderingDependency {
226    pub source_canister: String,
227    pub target_canister: String,
228    pub relationship: RestoreOrderingRelationship,
229}
230
231///
232/// RestoreOrderingRelationship
233///
234
235#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236#[serde(rename_all = "kebab-case")]
237pub enum RestoreOrderingRelationship {
238    ParentInSameGroup,
239    ParentInEarlierGroup,
240}
241
242///
243/// RestorePlanner
244///
245
246pub struct RestorePlanner;
247
248impl RestorePlanner {
249    /// Build a no-mutation restore plan from the manifest and optional target mapping.
250    pub fn plan(
251        manifest: &FleetBackupManifest,
252        mapping: Option<&RestoreMapping>,
253    ) -> Result<RestorePlan, RestorePlanError> {
254        manifest.validate()?;
255        if let Some(mapping) = mapping {
256            validate_mapping(mapping)?;
257            validate_mapping_sources(manifest, mapping)?;
258        }
259
260        let members = resolve_members(manifest, mapping)?;
261        let identity_summary = restore_identity_summary(&members, mapping.is_some());
262        let snapshot_summary = restore_snapshot_summary(&members);
263        let verification_summary = restore_verification_summary(manifest, &members);
264        let readiness_summary = restore_readiness_summary(&snapshot_summary, &verification_summary);
265        validate_restore_group_dependencies(&members)?;
266        let phases = group_and_order_members(members)?;
267        let ordering_summary = restore_ordering_summary(&phases);
268        let operation_summary =
269            restore_operation_summary(manifest.fleet.members.len(), &verification_summary, &phases);
270
271        Ok(RestorePlan {
272            backup_id: manifest.backup_id.clone(),
273            source_environment: manifest.source.environment.clone(),
274            source_root_canister: manifest.source.root_canister.clone(),
275            topology_hash: manifest.fleet.topology_hash.clone(),
276            member_count: manifest.fleet.members.len(),
277            identity_summary,
278            snapshot_summary,
279            verification_summary,
280            readiness_summary,
281            operation_summary,
282            ordering_summary,
283            design_conformance: Some(manifest.design_conformance_report()),
284            fleet_verification_checks: manifest.verification.fleet_checks.clone(),
285            phases,
286        })
287    }
288}
289
290///
291/// RestorePlanError
292///
293
294#[derive(Debug, ThisError)]
295pub enum RestorePlanError {
296    #[error(transparent)]
297    InvalidManifest(#[from] ManifestValidationError),
298
299    #[error("field {field} must be a valid principal: {value}")]
300    InvalidPrincipal { field: &'static str, value: String },
301
302    #[error("mapping contains duplicate source canister {0}")]
303    DuplicateMappingSource(String),
304
305    #[error("mapping contains duplicate target canister {0}")]
306    DuplicateMappingTarget(String),
307
308    #[error("mapping references unknown source canister {0}")]
309    UnknownMappingSource(String),
310
311    #[error("mapping is missing source canister {0}")]
312    MissingMappingSource(String),
313
314    #[error("fixed-identity member {source_canister} cannot be mapped to {target_canister}")]
315    FixedIdentityRemap {
316        source_canister: String,
317        target_canister: String,
318    },
319
320    #[error("restore plan contains duplicate target canister {0}")]
321    DuplicatePlanTarget(String),
322
323    #[error("restore group {0} contains a parent cycle or unresolved dependency")]
324    RestoreOrderCycle(u16),
325
326    #[error(
327        "restore plan places parent {parent_source_canister} in group {parent_restore_group} after child {child_source_canister} in group {child_restore_group}"
328    )]
329    ParentRestoreGroupAfterChild {
330        child_source_canister: String,
331        parent_source_canister: String,
332        child_restore_group: u16,
333        parent_restore_group: u16,
334    },
335}
336
337// Validate a user-supplied restore mapping before applying it to the manifest.
338fn validate_mapping(mapping: &RestoreMapping) -> Result<(), RestorePlanError> {
339    let mut sources = BTreeSet::new();
340    let mut targets = BTreeSet::new();
341
342    for entry in &mapping.members {
343        validate_principal("mapping.members[].source_canister", &entry.source_canister)?;
344        validate_principal("mapping.members[].target_canister", &entry.target_canister)?;
345
346        if !sources.insert(entry.source_canister.clone()) {
347            return Err(RestorePlanError::DuplicateMappingSource(
348                entry.source_canister.clone(),
349            ));
350        }
351
352        if !targets.insert(entry.target_canister.clone()) {
353            return Err(RestorePlanError::DuplicateMappingTarget(
354                entry.target_canister.clone(),
355            ));
356        }
357    }
358
359    Ok(())
360}
361
362// Ensure mappings only reference members declared in the manifest.
363fn validate_mapping_sources(
364    manifest: &FleetBackupManifest,
365    mapping: &RestoreMapping,
366) -> Result<(), RestorePlanError> {
367    let sources = manifest
368        .fleet
369        .members
370        .iter()
371        .map(|member| member.canister_id.as_str())
372        .collect::<BTreeSet<_>>();
373
374    for entry in &mapping.members {
375        if !sources.contains(entry.source_canister.as_str()) {
376            return Err(RestorePlanError::UnknownMappingSource(
377                entry.source_canister.clone(),
378            ));
379        }
380    }
381
382    Ok(())
383}
384
385// Resolve source manifest members into target restore members.
386fn resolve_members(
387    manifest: &FleetBackupManifest,
388    mapping: Option<&RestoreMapping>,
389) -> Result<Vec<RestorePlanMember>, RestorePlanError> {
390    let mut plan_members = Vec::with_capacity(manifest.fleet.members.len());
391    let mut targets = BTreeSet::new();
392    let mut source_to_target = BTreeMap::new();
393
394    for member in &manifest.fleet.members {
395        let target = resolve_target(member, mapping)?;
396        if !targets.insert(target.clone()) {
397            return Err(RestorePlanError::DuplicatePlanTarget(target));
398        }
399
400        source_to_target.insert(member.canister_id.clone(), target.clone());
401        plan_members.push(RestorePlanMember {
402            source_canister: member.canister_id.clone(),
403            target_canister: target,
404            role: member.role.clone(),
405            parent_source_canister: member.parent_canister_id.clone(),
406            parent_target_canister: None,
407            ordering_dependency: None,
408            phase_order: 0,
409            restore_group: member.restore_group,
410            identity_mode: member.identity_mode.clone(),
411            verification_class: member.verification_class.clone(),
412            verification_checks: concrete_member_verification_checks(
413                member,
414                &manifest.verification,
415            ),
416            source_snapshot: member.source_snapshot.clone(),
417        });
418    }
419
420    for member in &mut plan_members {
421        member.parent_target_canister = member
422            .parent_source_canister
423            .as_ref()
424            .and_then(|parent| source_to_target.get(parent))
425            .cloned();
426    }
427
428    Ok(plan_members)
429}
430
431// Resolve all concrete verification checks that apply to one restore member role.
432fn concrete_member_verification_checks(
433    member: &FleetMember,
434    verification: &VerificationPlan,
435) -> Vec<VerificationCheck> {
436    let mut checks = member
437        .verification_checks
438        .iter()
439        .filter(|check| verification_check_applies_to_role(check, &member.role))
440        .cloned()
441        .collect::<Vec<_>>();
442
443    for group in &verification.member_checks {
444        if group.role != member.role {
445            continue;
446        }
447
448        checks.extend(
449            group
450                .checks
451                .iter()
452                .filter(|check| verification_check_applies_to_role(check, &member.role))
453                .cloned(),
454        );
455    }
456
457    checks
458}
459
460// Return whether a verification check's role filter includes one member role.
461fn verification_check_applies_to_role(check: &VerificationCheck, role: &str) -> bool {
462    check.roles.is_empty() || check.roles.iter().any(|check_role| check_role == role)
463}
464
465// Resolve one member's target canister, enforcing identity continuity.
466fn resolve_target(
467    member: &FleetMember,
468    mapping: Option<&RestoreMapping>,
469) -> Result<String, RestorePlanError> {
470    let target = match mapping {
471        Some(mapping) => mapping
472            .target_for(&member.canister_id)
473            .ok_or_else(|| RestorePlanError::MissingMappingSource(member.canister_id.clone()))?
474            .to_string(),
475        None => member.canister_id.clone(),
476    };
477
478    if matches!(member.identity_mode, IdentityMode::Fixed) && target != member.canister_id {
479        return Err(RestorePlanError::FixedIdentityRemap {
480            source_canister: member.canister_id.clone(),
481            target_canister: target,
482        });
483    }
484
485    Ok(target)
486}
487
488// Summarize identity and mapping decisions before grouping restore phases.
489fn restore_identity_summary(
490    members: &[RestorePlanMember],
491    mapping_supplied: bool,
492) -> RestoreIdentitySummary {
493    let mut summary = RestoreIdentitySummary {
494        mapping_supplied,
495        all_sources_mapped: false,
496        fixed_members: 0,
497        relocatable_members: 0,
498        in_place_members: 0,
499        mapped_members: 0,
500        remapped_members: 0,
501    };
502
503    for member in members {
504        match member.identity_mode {
505            IdentityMode::Fixed => summary.fixed_members += 1,
506            IdentityMode::Relocatable => summary.relocatable_members += 1,
507        }
508
509        if member.source_canister == member.target_canister {
510            summary.in_place_members += 1;
511        } else {
512            summary.remapped_members += 1;
513        }
514        if mapping_supplied {
515            summary.mapped_members += 1;
516        }
517    }
518
519    summary.all_sources_mapped = mapping_supplied && summary.mapped_members == members.len();
520
521    summary
522}
523
524// Summarize snapshot provenance completeness before grouping restore phases.
525fn restore_snapshot_summary(members: &[RestorePlanMember]) -> RestoreSnapshotSummary {
526    let members_with_module_hash = members
527        .iter()
528        .filter(|member| member.source_snapshot.module_hash.is_some())
529        .count();
530    let members_with_wasm_hash = members
531        .iter()
532        .filter(|member| member.source_snapshot.wasm_hash.is_some())
533        .count();
534    let members_with_code_version = members
535        .iter()
536        .filter(|member| member.source_snapshot.code_version.is_some())
537        .count();
538    let members_with_checksum = members
539        .iter()
540        .filter(|member| member.source_snapshot.checksum.is_some())
541        .count();
542
543    RestoreSnapshotSummary {
544        all_members_have_module_hash: members_with_module_hash == members.len(),
545        all_members_have_wasm_hash: members_with_wasm_hash == members.len(),
546        all_members_have_code_version: members_with_code_version == members.len(),
547        all_members_have_checksum: members_with_checksum == members.len(),
548        members_with_module_hash,
549        members_with_wasm_hash,
550        members_with_code_version,
551        members_with_checksum,
552    }
553}
554
555// Summarize whether restore planning has the metadata required for automation.
556fn restore_readiness_summary(
557    snapshot: &RestoreSnapshotSummary,
558    verification: &RestoreVerificationSummary,
559) -> RestoreReadinessSummary {
560    let mut reasons = Vec::new();
561
562    if !snapshot.all_members_have_module_hash {
563        reasons.push("missing-module-hash".to_string());
564    }
565    if !snapshot.all_members_have_wasm_hash {
566        reasons.push("missing-wasm-hash".to_string());
567    }
568    if !snapshot.all_members_have_code_version {
569        reasons.push("missing-code-version".to_string());
570    }
571    if !snapshot.all_members_have_checksum {
572        reasons.push("missing-snapshot-checksum".to_string());
573    }
574    if !verification.all_members_have_checks {
575        reasons.push("missing-verification-checks".to_string());
576    }
577
578    RestoreReadinessSummary {
579        ready: reasons.is_empty(),
580        reasons,
581    }
582}
583
584// Summarize restore verification work declared by the manifest and members.
585fn restore_verification_summary(
586    manifest: &FleetBackupManifest,
587    members: &[RestorePlanMember],
588) -> RestoreVerificationSummary {
589    let fleet_checks = manifest.verification.fleet_checks.len();
590    let member_check_groups = manifest.verification.member_checks.len();
591    let member_checks = members
592        .iter()
593        .map(|member| member.verification_checks.len())
594        .sum::<usize>();
595    let members_with_checks = members
596        .iter()
597        .filter(|member| !member.verification_checks.is_empty())
598        .count();
599
600    RestoreVerificationSummary {
601        verification_required: true,
602        all_members_have_checks: members_with_checks == members.len(),
603        fleet_checks,
604        member_check_groups,
605        member_checks,
606        members_with_checks,
607        total_checks: fleet_checks + member_checks,
608    }
609}
610
611// Summarize the concrete restore operations implied by a no-mutation plan.
612const fn restore_operation_summary(
613    member_count: usize,
614    verification_summary: &RestoreVerificationSummary,
615    phases: &[RestorePhase],
616) -> RestoreOperationSummary {
617    RestoreOperationSummary {
618        planned_snapshot_uploads: member_count,
619        planned_snapshot_loads: member_count,
620        planned_code_reinstalls: 0,
621        planned_verification_checks: verification_summary.total_checks,
622        planned_operations: member_count + member_count + verification_summary.total_checks,
623        planned_phases: phases.len(),
624    }
625}
626
627// Reject group assignments that would restore a child before its parent.
628fn validate_restore_group_dependencies(
629    members: &[RestorePlanMember],
630) -> Result<(), RestorePlanError> {
631    let groups_by_source = members
632        .iter()
633        .map(|member| (member.source_canister.as_str(), member.restore_group))
634        .collect::<BTreeMap<_, _>>();
635
636    for member in members {
637        let Some(parent) = &member.parent_source_canister else {
638            continue;
639        };
640        let Some(parent_group) = groups_by_source.get(parent.as_str()) else {
641            continue;
642        };
643
644        if *parent_group > member.restore_group {
645            return Err(RestorePlanError::ParentRestoreGroupAfterChild {
646                child_source_canister: member.source_canister.clone(),
647                parent_source_canister: parent.clone(),
648                child_restore_group: member.restore_group,
649                parent_restore_group: *parent_group,
650            });
651        }
652    }
653
654    Ok(())
655}
656
657// Group members and apply parent-before-child ordering inside each group.
658fn group_and_order_members(
659    members: Vec<RestorePlanMember>,
660) -> Result<Vec<RestorePhase>, RestorePlanError> {
661    let mut groups = BTreeMap::<u16, Vec<RestorePlanMember>>::new();
662    for member in members {
663        groups.entry(member.restore_group).or_default().push(member);
664    }
665
666    groups
667        .into_iter()
668        .map(|(restore_group, members)| {
669            let members = order_group(restore_group, members)?;
670            Ok(RestorePhase {
671                restore_group,
672                members,
673            })
674        })
675        .collect()
676}
677
678// Topologically order one group using manifest parent relationships.
679fn order_group(
680    restore_group: u16,
681    members: Vec<RestorePlanMember>,
682) -> Result<Vec<RestorePlanMember>, RestorePlanError> {
683    let mut remaining = members;
684    let group_sources = remaining
685        .iter()
686        .map(|member| member.source_canister.clone())
687        .collect::<BTreeSet<_>>();
688    let mut emitted = BTreeSet::new();
689    let mut ordered = Vec::with_capacity(remaining.len());
690
691    while !remaining.is_empty() {
692        let Some(index) = remaining
693            .iter()
694            .position(|member| parent_satisfied(member, &group_sources, &emitted))
695        else {
696            return Err(RestorePlanError::RestoreOrderCycle(restore_group));
697        };
698
699        let mut member = remaining.remove(index);
700        member.phase_order = ordered.len();
701        member.ordering_dependency = ordering_dependency(&member, &group_sources);
702        emitted.insert(member.source_canister.clone());
703        ordered.push(member);
704    }
705
706    Ok(ordered)
707}
708
709// Describe the topology dependency that controlled a member's restore ordering.
710fn ordering_dependency(
711    member: &RestorePlanMember,
712    group_sources: &BTreeSet<String>,
713) -> Option<RestoreOrderingDependency> {
714    let parent_source = member.parent_source_canister.as_ref()?;
715    let parent_target = member.parent_target_canister.as_ref()?;
716    let relationship = if group_sources.contains(parent_source) {
717        RestoreOrderingRelationship::ParentInSameGroup
718    } else {
719        RestoreOrderingRelationship::ParentInEarlierGroup
720    };
721
722    Some(RestoreOrderingDependency {
723        source_canister: parent_source.clone(),
724        target_canister: parent_target.clone(),
725        relationship,
726    })
727}
728
729// Summarize the dependency ordering metadata exposed in the restore plan.
730fn restore_ordering_summary(phases: &[RestorePhase]) -> RestoreOrderingSummary {
731    let mut summary = RestoreOrderingSummary {
732        phase_count: phases.len(),
733        dependency_free_members: 0,
734        in_group_parent_edges: 0,
735        cross_group_parent_edges: 0,
736    };
737
738    for member in phases.iter().flat_map(|phase| phase.members.iter()) {
739        match &member.ordering_dependency {
740            Some(dependency)
741                if dependency.relationship == RestoreOrderingRelationship::ParentInSameGroup =>
742            {
743                summary.in_group_parent_edges += 1;
744            }
745            Some(dependency)
746                if dependency.relationship == RestoreOrderingRelationship::ParentInEarlierGroup =>
747            {
748                summary.cross_group_parent_edges += 1;
749            }
750            Some(_) => {}
751            None => summary.dependency_free_members += 1,
752        }
753    }
754
755    summary
756}
757
758// Determine whether a member's in-group parent has already been emitted.
759fn parent_satisfied(
760    member: &RestorePlanMember,
761    group_sources: &BTreeSet<String>,
762    emitted: &BTreeSet<String>,
763) -> bool {
764    match &member.parent_source_canister {
765        Some(parent) if group_sources.contains(parent) => emitted.contains(parent),
766        _ => true,
767    }
768}
769
770// Validate textual principal fields used in mappings.
771fn validate_principal(field: &'static str, value: &str) -> Result<(), RestorePlanError> {
772    Principal::from_str(value)
773        .map(|_| ())
774        .map_err(|_| RestorePlanError::InvalidPrincipal {
775            field,
776            value: value.to_string(),
777        })
778}