1use crate::canonical::{settings_identity, validate_clip_settings};
2use crate::{
3 DefaultStatus, EngineDeclaration, EngineDeclarationV2, EngineProfile, EngineProfileV2,
4 InvalidSettingReason, ProfileSelection, ResolutionError, ResolvedSettingOriginV2,
5 ResolvedSettingV2, SettingApplicability, SettingDefaultV2, SettingDomain, SettingDomainV2,
6 SettingId, SettingIdV2, SettingLocation, SettingMap, SettingMapV2, SettingScope, SettingValue,
7 SettingValueV2, profiles_v1, profiles_v2,
8};
9use animsmith_core::{
10 ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS, EngineContractError, InputIdentity, SourceFormatV1,
11};
12use std::collections::BTreeMap;
13
14pub const SOURCE_TRANSFORM_PATH_MAX_BYTES: usize = 4_096;
16
17pub const RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS: usize = ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS;
22
23pub fn lookup_profile(
29 selection: &ProfileSelection,
30) -> Result<&'static EngineProfile, ResolutionError> {
31 profiles_v1()
32 .iter()
33 .find(|profile| profile.selection() == selection)
34 .ok_or_else(|| ResolutionError::UnknownProfile(selection.clone()))
35}
36
37pub fn lookup_profile_v2(
44 selection: &ProfileSelection,
45) -> Result<&'static EngineProfileV2, ResolutionErrorV2> {
46 profiles_v2()
47 .iter()
48 .find(|profile| profile.selection() == selection)
49 .ok_or_else(|| ResolutionErrorV2::UnknownProfile(selection.clone()))
50}
51
52pub fn resolve_static_v2(
64 declaration: EngineDeclarationV2,
65) -> Result<Option<StaticResolutionV2>, ResolutionErrorV2> {
66 let EngineDeclarationV2 {
67 selection,
68 document_settings,
69 clip_settings,
70 } = declaration;
71 let Some(selection) = selection else {
72 if document_settings.is_some() || !clip_settings.is_empty() {
73 return Err(ResolutionErrorV2::SettingsWithoutSelection);
74 }
75 return Ok(None);
76 };
77 let profile = lookup_profile_v2(&selection)?;
78
79 let explicit_document = validate_map_v2(
80 profile,
81 document_settings.unwrap_or_default(),
82 SettingScope::Document,
83 SettingLocation::Document,
84 )?;
85 let mut document_settings = BTreeMap::new();
86 for descriptor in profile.setting_descriptors() {
87 if descriptor.scope() != SettingScope::Document {
88 continue;
89 }
90 if let Some(value) = explicit_document.get(&descriptor.id()) {
91 document_settings.insert(
92 descriptor.id(),
93 ResolvedSettingV2 {
94 value: value.clone(),
95 origin: ResolvedSettingOriginV2::ExplicitConfig,
96 },
97 );
98 } else {
99 match descriptor.default() {
100 SettingDefaultV2::RequiredExplicit => {
101 return Err(ResolutionErrorV2::MissingRequiredSetting {
102 setting: descriptor.id(),
103 location: SettingLocation::Document,
104 });
105 }
106 SettingDefaultV2::Verified(value) => {
107 document_settings.insert(
108 descriptor.id(),
109 ResolvedSettingV2 {
110 value: value.clone(),
111 origin: ResolvedSettingOriginV2::ProfileDefault,
112 },
113 );
114 }
115 }
116 }
117 }
118
119 let mut clip_overlays = BTreeMap::new();
120 for (selector, settings) in clip_settings {
121 let values = validate_map_v2(
122 profile,
123 settings,
124 SettingScope::Clip,
125 SettingLocation::ClipSelector(selector.clone()),
126 )?;
127 clip_overlays.insert(selector, values);
128 }
129
130 Ok(Some(StaticResolutionV2 {
131 profile,
132 document_settings,
133 clip_overlays,
134 }))
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct StaticResolutionV2 {
140 profile: &'static EngineProfileV2,
141 document_settings: BTreeMap<SettingIdV2, ResolvedSettingV2>,
142 clip_overlays: BTreeMap<String, BTreeMap<SettingIdV2, SettingValueV2>>,
143}
144
145impl StaticResolutionV2 {
146 pub const fn profile(&self) -> &'static EngineProfileV2 {
148 self.profile
149 }
150
151 pub const fn document_settings(&self) -> &BTreeMap<SettingIdV2, ResolvedSettingV2> {
153 &self.document_settings
154 }
155
156 pub const fn clip_overlays(&self) -> &BTreeMap<String, BTreeMap<SettingIdV2, SettingValueV2>> {
158 &self.clip_overlays
159 }
160
161 pub fn resolve_input(
168 &self,
169 source_format: SourceFormatV1,
170 ) -> Result<ResolvedProfileSettingsV2, ResolutionErrorV2> {
171 if !self.profile.accepted_inputs().contains(&source_format) {
172 return Err(ResolutionErrorV2::UnacceptedInputFormat {
173 selection: self.profile.selection().clone(),
174 format: source_format,
175 });
176 }
177 Ok(ResolvedProfileSettingsV2 {
178 profile: self.profile,
179 source_format,
180 document_settings: self.document_settings.clone(),
181 clips: Vec::new(),
182 clip_coverage: ResolvedClipCoverageV2::Complete,
183 work: ResolvedEngineSettingsWorkV2 {
184 actual_clip_rows_inspected: 0,
185 materialized_clip_rows: 0,
186 retained_clip_rows: 0,
187 },
188 })
189 }
190
191 pub fn resolve_input_with_clips(
201 &self,
202 source_format: SourceFormatV1,
203 clip_names: &[String],
204 ) -> Result<ResolvedProfileSettingsV2, ResolutionErrorV2> {
205 self.resolve_input_with_clips_iter(source_format, clip_names.iter().map(String::as_str))
206 }
207
208 pub fn resolve_input_with_clips_iter<'a>(
214 &self,
215 source_format: SourceFormatV1,
216 clip_names: impl ExactSizeIterator<Item = &'a str>,
217 ) -> Result<ResolvedProfileSettingsV2, ResolutionErrorV2> {
218 if !self.profile.accepted_inputs().contains(&source_format) {
219 return Err(ResolutionErrorV2::UnacceptedInputFormat {
220 selection: self.profile.selection().clone(),
221 format: source_format,
222 });
223 }
224 if !self
229 .profile
230 .setting_descriptors()
231 .iter()
232 .any(|descriptor| descriptor.scope() == SettingScope::Clip)
233 {
234 return self.resolve_input(source_format);
235 }
236 let actual_clip_rows = clip_names.len();
237 let retained_clip_rows = actual_clip_rows.min(RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS);
238 let overflowed = actual_clip_rows > RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS;
239 let mut clips = Vec::with_capacity(retained_clip_rows);
240 for (clip_ordinal, clip_name) in (0_u64..).zip(clip_names.take(retained_clip_rows)) {
241 let mut values = BTreeMap::new();
242 for (selector, settings) in &self.clip_overlays {
243 if selector != clip_name && animsmith_core::config::glob_match(selector, clip_name)
244 {
245 values.extend(settings.iter().map(|(id, value)| (*id, value.clone())));
246 }
247 }
248 if let Some(exact) = self.clip_overlays.get(clip_name) {
249 values.extend(exact.iter().map(|(id, value)| (*id, value.clone())));
250 }
251 let mut resolved = BTreeMap::new();
252 for descriptor in self.profile.setting_descriptors() {
253 if descriptor.scope() != SettingScope::Clip {
254 continue;
255 }
256 if let Some(value) = values.get(&descriptor.id()) {
257 resolved.insert(
258 descriptor.id(),
259 ResolvedSettingV2 {
260 value: value.clone(),
261 origin: ResolvedSettingOriginV2::ExplicitConfig,
262 },
263 );
264 } else {
265 match descriptor.default() {
266 SettingDefaultV2::RequiredExplicit => {
267 return Err(ResolutionErrorV2::MissingRequiredSetting {
268 setting: descriptor.id(),
269 location: SettingLocation::ClipSelector(clip_name.to_owned()),
270 });
271 }
272 SettingDefaultV2::Verified(value) => {
273 resolved.insert(
274 descriptor.id(),
275 ResolvedSettingV2 {
276 value: value.clone(),
277 origin: ResolvedSettingOriginV2::ProfileDefault,
278 },
279 );
280 }
281 }
282 }
283 }
284 clips.push(ResolvedClipSettingsV2 {
285 clip_ordinal,
286 clip_name: clip_name.to_owned(),
287 settings: resolved,
288 });
289 }
290 clips.sort_by(|left, right| {
291 (left.clip_name.as_str(), left.clip_ordinal)
292 .cmp(&(right.clip_name.as_str(), right.clip_ordinal))
293 });
294 Ok(ResolvedProfileSettingsV2 {
295 profile: self.profile,
296 source_format,
297 document_settings: self.document_settings.clone(),
298 clips,
299 clip_coverage: if overflowed {
300 ResolvedClipCoverageV2::Partial {
301 reason: ResolvedClipCoverageReasonV2::ActualClipRowsExceeded,
302 }
303 } else {
304 ResolvedClipCoverageV2::Complete
305 },
306 work: ResolvedEngineSettingsWorkV2 {
307 actual_clip_rows_inspected: actual_clip_rows
308 .min(RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS.saturating_add(1)),
309 materialized_clip_rows: retained_clip_rows,
310 retained_clip_rows,
311 },
312 })
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct ResolvedProfileSettingsV2 {
319 profile: &'static EngineProfileV2,
320 source_format: SourceFormatV1,
321 document_settings: BTreeMap<SettingIdV2, ResolvedSettingV2>,
322 clips: Vec<ResolvedClipSettingsV2>,
323 clip_coverage: ResolvedClipCoverageV2,
324 work: ResolvedEngineSettingsWorkV2,
325}
326
327impl ResolvedProfileSettingsV2 {
328 pub const fn profile(&self) -> &'static EngineProfileV2 {
330 self.profile
331 }
332
333 pub const fn source_format(&self) -> SourceFormatV1 {
335 self.source_format
336 }
337
338 pub const fn document_settings(&self) -> &BTreeMap<SettingIdV2, ResolvedSettingV2> {
340 &self.document_settings
341 }
342
343 pub fn clip_settings(&self) -> &[ResolvedClipSettingsV2] {
348 &self.clips
349 }
350
351 pub fn clip_setting(&self, ordinal: u64, name: &str) -> Option<&ResolvedClipSettingsV2> {
353 self.clips
354 .iter()
355 .find(|clip| clip.clip_ordinal == ordinal && clip.clip_name == name)
356 }
357
358 pub const fn clip_coverage(&self) -> ResolvedClipCoverageV2 {
360 self.clip_coverage
361 }
362
363 pub const fn work(&self) -> ResolvedEngineSettingsWorkV2 {
365 self.work
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct ResolvedClipSettingsV2 {
372 clip_ordinal: u64,
373 clip_name: String,
374 settings: BTreeMap<SettingIdV2, ResolvedSettingV2>,
375}
376
377impl ResolvedClipSettingsV2 {
378 pub const fn clip_ordinal(&self) -> u64 {
380 self.clip_ordinal
381 }
382
383 pub fn clip_name(&self) -> &str {
385 &self.clip_name
386 }
387
388 pub const fn settings(&self) -> &BTreeMap<SettingIdV2, ResolvedSettingV2> {
390 &self.settings
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
396#[non_exhaustive]
397pub enum ResolutionErrorV2 {
398 #[error("V2 engine settings were declared without an engine profile selection")]
400 SettingsWithoutSelection,
401 #[error("unknown V2 engine profile selection {0:?}")]
403 UnknownProfile(ProfileSelection),
404 #[error("unknown V2 engine setting {key:?} in {location}")]
406 UnknownSetting {
407 key: String,
409 location: SettingLocation,
411 },
412 #[error(
414 "V2 engine setting {setting} has {expected:?} scope but was declared in {found:?} scope"
415 )]
416 WrongScope {
417 setting: SettingIdV2,
419 expected: SettingScope,
421 found: SettingScope,
423 location: SettingLocation,
425 },
426 #[error("invalid value domain for V2 engine setting {setting} in {location}")]
428 InvalidSettingValue {
429 setting: SettingIdV2,
431 expected: SettingDomainV2,
433 location: SettingLocation,
435 },
436 #[error("missing required V2 engine setting {setting} in {location}")]
438 MissingRequiredSetting {
439 setting: SettingIdV2,
441 location: SettingLocation,
443 },
444 #[error("input format {format:?} is not accepted by V2 engine profile {selection:?}")]
446 UnacceptedInputFormat {
447 selection: ProfileSelection,
449 format: SourceFormatV1,
451 },
452}
453
454fn validate_map_v2(
455 profile: &EngineProfileV2,
456 settings: SettingMapV2,
457 supplied_scope: SettingScope,
458 location: SettingLocation,
459) -> Result<BTreeMap<SettingIdV2, SettingValueV2>, ResolutionErrorV2> {
460 let mut validated = BTreeMap::new();
461 for (key, value) in settings {
462 let Some(id) = SettingIdV2::from_str_v2(&key) else {
463 return Err(ResolutionErrorV2::UnknownSetting {
464 key,
465 location: location.clone(),
466 });
467 };
468 let Some(descriptor) = profile.setting_descriptor(id) else {
469 return Err(ResolutionErrorV2::UnknownSetting {
470 key,
471 location: location.clone(),
472 });
473 };
474 if descriptor.scope() != supplied_scope {
475 return Err(ResolutionErrorV2::WrongScope {
476 setting: id,
477 expected: descriptor.scope(),
478 found: supplied_scope,
479 location: location.clone(),
480 });
481 }
482 if !valid_setting_value_v2(profile, id, &value, descriptor.domain()) {
483 return Err(ResolutionErrorV2::InvalidSettingValue {
484 setting: id,
485 expected: descriptor.domain(),
486 location: location.clone(),
487 });
488 }
489 validated.insert(id, value);
490 }
491 Ok(validated)
492}
493
494fn valid_setting_value_v2(
495 profile: &EngineProfileV2,
496 id: SettingIdV2,
497 value: &SettingValueV2,
498 domain: SettingDomainV2,
499) -> bool {
500 if !value.matches_domain(domain) {
501 return false;
502 }
503 if id == SettingIdV2::RootMotionSource && !valid_source_transform_path_v2(value) {
504 return false;
505 }
506 if profile.selection().family() == "unity-generic"
507 && profile.selection().profile_revision() == 2
508 {
509 match (id, value) {
510 (
511 SettingIdV2::AnimationType,
512 SettingValueV2::AnimationType(crate::UnityAnimationTypeV2::Generic),
513 )
514 | (
515 SettingIdV2::AvatarSetup,
516 SettingValueV2::AvatarSetup(crate::UnityAvatarSetupV2::CreateFromThisModel),
517 )
518 | (SettingIdV2::ImportAnimation, SettingValueV2::Boolean(true)) => {}
519 (
520 SettingIdV2::AnimationType
521 | SettingIdV2::AvatarSetup
522 | SettingIdV2::ImportAnimation,
523 _,
524 ) => return false,
525 _ => {}
526 }
527 }
528 true
529}
530
531fn valid_source_transform_path_v2(value: &SettingValueV2) -> bool {
532 let SettingValueV2::SourceTransformPath(path) = value else {
533 return false;
534 };
535 animsmith_core::RawTransformPathV1::parse(path).is_ok()
536}
537
538pub fn resolve_static(
550 declaration: EngineDeclaration,
551) -> Result<Option<StaticResolution>, ResolutionError> {
552 let EngineDeclaration {
553 selection,
554 document_settings,
555 clip_settings,
556 } = declaration;
557 let Some(selection) = selection else {
558 if document_settings.is_some() || !clip_settings.is_empty() {
559 return Err(ResolutionError::SettingsWithoutSelection);
560 }
561 return Ok(None);
562 };
563 let profile = lookup_profile(&selection)?;
564
565 let document_settings = validate_map(
566 profile,
567 document_settings.unwrap_or_default(),
568 SettingScope::Document,
569 SettingLocation::Document,
570 )?;
571 for descriptor in profile.setting_descriptors() {
572 if descriptor.applicability() == SettingApplicability::Applicable
573 && descriptor.scope() == SettingScope::Document
574 && descriptor.default_status() == DefaultStatus::RequiredWithoutDefault
575 && !document_settings.contains_key(&descriptor.id())
576 {
577 return Err(ResolutionError::MissingRequiredSetting {
578 setting: descriptor.id(),
579 location: SettingLocation::Document,
580 });
581 }
582 }
583
584 let mut validated_clips = BTreeMap::new();
585 for (selector, settings) in clip_settings {
586 let values = validate_map(
587 profile,
588 settings,
589 SettingScope::Clip,
590 SettingLocation::ClipSelector(selector.clone()),
591 )?;
592 validated_clips.insert(selector, values);
593 }
594
595 Ok(Some(StaticResolution {
596 profile,
597 document_settings,
598 clip_overlays: validated_clips,
599 }))
600}
601
602#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct StaticResolution {
605 profile: &'static EngineProfile,
606 document_settings: BTreeMap<SettingId, SettingValue>,
607 clip_overlays: BTreeMap<String, BTreeMap<SettingId, SettingValue>>,
608}
609
610impl StaticResolution {
611 pub const fn profile(&self) -> &'static EngineProfile {
613 self.profile
614 }
615
616 pub const fn document_settings(&self) -> &BTreeMap<SettingId, SettingValue> {
618 &self.document_settings
619 }
620
621 pub fn resolve_input(
635 &self,
636 source_format: SourceFormatV1,
637 clip_names: &[String],
638 ) -> Result<ResolvedProfile, ResolutionError> {
639 self.resolve_input_iter(source_format, clip_names.iter().map(String::as_str))
640 }
641
642 pub fn resolve_input_iter<'a>(
653 &self,
654 source_format: SourceFormatV1,
655 clip_names: impl ExactSizeIterator<Item = &'a str>,
656 ) -> Result<ResolvedProfile, ResolutionError> {
657 if !self.profile.accepted_inputs().contains(&source_format) {
658 return Err(ResolutionError::UnacceptedInputFormat {
659 selection: self.profile.selection().clone(),
660 format: source_format,
661 });
662 }
663
664 let clip_count = clip_names.len();
665 if clip_count > ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS {
666 return Err(ResolutionError::ResolvedSettingsContract(
667 EngineContractError::TooManyRows {
668 field: "settings.clips",
669 found: clip_count,
670 max: ENGINE_CONTRACT_V1_MAX_COLLECTION_ROWS,
671 },
672 ));
673 }
674
675 let mut clips = Vec::with_capacity(clip_count);
676 for clip_name in clip_names {
677 let mut materialized = BTreeMap::new();
678 for (selector, settings) in &self.clip_overlays {
679 if selector != clip_name && animsmith_core::config::glob_match(selector, clip_name)
680 {
681 materialized.extend(settings.iter().map(|(id, value)| (*id, value.clone())));
682 }
683 }
684 if let Some(exact) = self.clip_overlays.get(clip_name) {
685 materialized.extend(exact.iter().map(|(id, value)| (*id, value.clone())));
686 }
687 for descriptor in self.profile.setting_descriptors() {
688 if descriptor.applicability() == SettingApplicability::Applicable
689 && descriptor.scope() == SettingScope::Clip
690 && descriptor.default_status() == DefaultStatus::RequiredWithoutDefault
691 && !materialized.contains_key(&descriptor.id())
692 {
693 return Err(ResolutionError::MissingRequiredSetting {
694 setting: descriptor.id(),
695 location: SettingLocation::ClipSelector(clip_name.to_owned()),
696 });
697 }
698 }
699 clips.push(ResolvedClipSettings {
700 clip_name: clip_name.to_owned(),
701 settings: materialized,
702 });
703 }
704 clips.sort_by(|left, right| left.clip_name.cmp(&right.clip_name));
705 let identity = settings_identity(
706 self.profile,
707 &self.document_settings,
708 clips
709 .iter()
710 .map(|clip| (clip.clip_name.as_str(), &clip.settings)),
711 )?;
712 Ok(ResolvedProfile {
713 profile: self.profile,
714 source_format,
715 document_settings: self.document_settings.clone(),
716 clips,
717 settings_identity: identity,
718 })
719 }
720
721 pub fn resolve_input_v2_iter<'a>(
738 &self,
739 source_format: SourceFormatV1,
740 clip_names: impl ExactSizeIterator<Item = &'a str>,
741 ) -> Result<ResolvedProfileV2, ResolutionError> {
742 if !self.profile.accepted_inputs().contains(&source_format) {
743 return Err(ResolutionError::UnacceptedInputFormat {
744 selection: self.profile.selection().clone(),
745 format: source_format,
746 });
747 }
748
749 let actual_clip_rows = clip_names.len();
750 let retained_clip_rows = actual_clip_rows.min(RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS);
751 let overflowed = actual_clip_rows > RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS;
752 let mut clips = Vec::with_capacity(retained_clip_rows);
753 for clip_name in clip_names.take(retained_clip_rows) {
754 clips.push(self.materialize_clip(clip_name)?);
755 }
756 clips.sort_by(|left, right| left.clip_name.cmp(&right.clip_name));
757
758 let settings_identity = settings_identity(
762 self.profile,
763 &self.document_settings,
764 clips
765 .iter()
766 .map(|clip| (clip.clip_name.as_str(), &clip.settings)),
767 )?;
768 Ok(ResolvedProfileV2 {
769 profile: self.profile,
770 source_format,
771 document_settings: self.document_settings.clone(),
772 clips,
773 settings_identity,
774 clip_coverage: if overflowed {
775 ResolvedClipCoverageV2::Partial {
776 reason: ResolvedClipCoverageReasonV2::ActualClipRowsExceeded,
777 }
778 } else {
779 ResolvedClipCoverageV2::Complete
780 },
781 work: ResolvedEngineSettingsWorkV2 {
782 actual_clip_rows_inspected: actual_clip_rows
783 .min(RESOLVED_ENGINE_SETTINGS_V2_MAX_CLIPS.saturating_add(1)),
784 materialized_clip_rows: retained_clip_rows,
785 retained_clip_rows,
786 },
787 })
788 }
789
790 fn materialize_clip(&self, clip_name: &str) -> Result<ResolvedClipSettings, ResolutionError> {
791 let mut materialized = BTreeMap::new();
792 for (selector, settings) in &self.clip_overlays {
793 if selector != clip_name && animsmith_core::config::glob_match(selector, clip_name) {
794 materialized.extend(settings.iter().map(|(id, value)| (*id, value.clone())));
795 }
796 }
797 if let Some(exact) = self.clip_overlays.get(clip_name) {
798 materialized.extend(exact.iter().map(|(id, value)| (*id, value.clone())));
799 }
800 for descriptor in self.profile.setting_descriptors() {
801 if descriptor.applicability() == SettingApplicability::Applicable
802 && descriptor.scope() == SettingScope::Clip
803 && descriptor.default_status() == DefaultStatus::RequiredWithoutDefault
804 && !materialized.contains_key(&descriptor.id())
805 {
806 return Err(ResolutionError::MissingRequiredSetting {
807 setting: descriptor.id(),
808 location: SettingLocation::ClipSelector(clip_name.to_owned()),
809 });
810 }
811 }
812 validate_clip_settings(clip_name, &materialized)?;
813 Ok(ResolvedClipSettings {
814 clip_name: clip_name.to_owned(),
815 settings: materialized,
816 })
817 }
818}
819
820#[derive(Debug, Clone, PartialEq, Eq)]
822pub struct ResolvedClipSettings {
823 clip_name: String,
824 settings: BTreeMap<SettingId, SettingValue>,
825}
826
827impl ResolvedClipSettings {
828 pub fn clip_name(&self) -> &str {
830 &self.clip_name
831 }
832
833 pub const fn settings(&self) -> &BTreeMap<SettingId, SettingValue> {
835 &self.settings
836 }
837}
838
839#[derive(Debug, Clone, PartialEq, Eq)]
841pub struct ResolvedProfile {
842 profile: &'static EngineProfile,
843 source_format: SourceFormatV1,
844 document_settings: BTreeMap<SettingId, SettingValue>,
845 clips: Vec<ResolvedClipSettings>,
846 settings_identity: InputIdentity,
847}
848
849#[derive(Debug, Clone, Copy, PartialEq, Eq)]
851pub enum ResolvedClipCoverageV2 {
852 Complete,
854 Partial {
856 reason: ResolvedClipCoverageReasonV2,
858 },
859}
860
861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
863pub enum ResolvedClipCoverageReasonV2 {
864 ActualClipRowsExceeded,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq)]
870pub struct ResolvedEngineSettingsWorkV2 {
871 actual_clip_rows_inspected: usize,
872 materialized_clip_rows: usize,
873 retained_clip_rows: usize,
874}
875
876impl ResolvedEngineSettingsWorkV2 {
877 pub const fn actual_clip_rows_inspected(&self) -> usize {
879 self.actual_clip_rows_inspected
880 }
881
882 pub const fn materialized_clip_rows(&self) -> usize {
884 self.materialized_clip_rows
885 }
886
887 pub const fn retained_clip_rows(&self) -> usize {
889 self.retained_clip_rows
890 }
891}
892
893#[derive(Debug, Clone, PartialEq, Eq)]
895pub struct ResolvedProfileV2 {
896 profile: &'static EngineProfile,
897 source_format: SourceFormatV1,
898 document_settings: BTreeMap<SettingId, SettingValue>,
899 clips: Vec<ResolvedClipSettings>,
900 settings_identity: InputIdentity,
901 clip_coverage: ResolvedClipCoverageV2,
902 work: ResolvedEngineSettingsWorkV2,
903}
904
905impl ResolvedProfileV2 {
906 pub const fn profile(&self) -> &'static EngineProfile {
908 self.profile
909 }
910
911 pub const fn source_format(&self) -> SourceFormatV1 {
913 self.source_format
914 }
915
916 pub const fn document_settings(&self) -> &BTreeMap<SettingId, SettingValue> {
918 &self.document_settings
919 }
920
921 pub fn clip_settings(&self) -> &[ResolvedClipSettings] {
923 &self.clips
924 }
925
926 pub const fn settings_identity(&self) -> &InputIdentity {
928 &self.settings_identity
929 }
930
931 pub const fn clip_coverage(&self) -> ResolvedClipCoverageV2 {
933 self.clip_coverage
934 }
935
936 pub const fn work(&self) -> ResolvedEngineSettingsWorkV2 {
938 self.work
939 }
940}
941
942impl ResolvedProfile {
943 pub const fn profile(&self) -> &'static EngineProfile {
945 self.profile
946 }
947
948 pub const fn source_format(&self) -> SourceFormatV1 {
950 self.source_format
951 }
952
953 pub const fn document_settings(&self) -> &BTreeMap<SettingId, SettingValue> {
955 &self.document_settings
956 }
957
958 pub fn clip_settings(&self) -> &[ResolvedClipSettings] {
960 &self.clips
961 }
962
963 pub const fn settings_identity(&self) -> &InputIdentity {
965 &self.settings_identity
966 }
967}
968
969fn validate_map(
970 profile: &EngineProfile,
971 settings: SettingMap,
972 supplied_scope: SettingScope,
973 location: SettingLocation,
974) -> Result<BTreeMap<SettingId, SettingValue>, ResolutionError> {
975 let mut validated = BTreeMap::new();
976 for (key, value) in settings {
977 let Some(id) = SettingId::from_str(&key) else {
978 return Err(ResolutionError::UnknownSetting {
979 key,
980 location: location.clone(),
981 });
982 };
983 let Some(descriptor) = profile.setting_descriptor(id) else {
984 return Err(ResolutionError::UnknownSetting {
985 key,
986 location: location.clone(),
987 });
988 };
989 if descriptor.applicability() == SettingApplicability::NotApplicable {
990 return Err(ResolutionError::NotApplicable {
991 setting: id,
992 location: location.clone(),
993 });
994 }
995 if descriptor.scope() != supplied_scope {
996 return Err(ResolutionError::WrongScope {
997 setting: id,
998 expected: descriptor.scope(),
999 found: supplied_scope,
1000 location: location.clone(),
1001 });
1002 }
1003 validate_value(id, descriptor.domain(), &value, location.clone())?;
1004 validated.insert(id, value);
1005 }
1006 Ok(validated)
1007}
1008
1009fn validate_value(
1010 setting: SettingId,
1011 domain: SettingDomain,
1012 value: &SettingValue,
1013 location: SettingLocation,
1014) -> Result<(), ResolutionError> {
1015 let domain_matches = matches!(
1016 (domain, value),
1017 (SettingDomain::Boolean, SettingValue::Boolean(_))
1018 | (SettingDomain::BakeOrExtract, SettingValue::BakeOrExtract(_))
1019 | (
1020 SettingDomain::SourceTransformPath,
1021 SettingValue::SourceTransformPath(_)
1022 )
1023 );
1024 if !domain_matches {
1025 return Err(ResolutionError::InvalidSettingValue {
1026 setting,
1027 location,
1028 reason: InvalidSettingReason::WrongDomain {
1029 expected: domain,
1030 found: value.kind(),
1031 },
1032 });
1033 }
1034 if let SettingValue::SourceTransformPath(path) = value {
1035 let reason = if path.is_empty() {
1036 Some(InvalidSettingReason::EmptyPath)
1037 } else if path.len() > SOURCE_TRANSFORM_PATH_MAX_BYTES {
1038 Some(InvalidSettingReason::PathTooLong {
1039 bytes: path.len(),
1040 limit: SOURCE_TRANSFORM_PATH_MAX_BYTES,
1041 })
1042 } else if path.starts_with('/') {
1043 Some(InvalidSettingReason::AbsolutePath)
1044 } else if path.chars().any(char::is_control) {
1045 Some(InvalidSettingReason::ControlCharacter)
1046 } else if path.split('/').any(str::is_empty) {
1047 Some(InvalidSettingReason::EmptyPathSegment)
1048 } else if path.split('/').any(|segment| matches!(segment, "." | "..")) {
1049 Some(InvalidSettingReason::DotPathSegment)
1050 } else {
1051 None
1052 };
1053 if let Some(reason) = reason {
1054 return Err(ResolutionError::InvalidSettingValue {
1055 setting,
1056 location,
1057 reason,
1058 });
1059 }
1060 }
1061 Ok(())
1062}