1use std::collections::BTreeSet;
4
5use crate::{Error, ErrorKind, RationalRate, ResourceId, Result};
6
7pub const MAX_RESOURCE_ROLE_BYTES: usize = 128;
9pub const MAX_SEQUENCE_PATTERN_BYTES: usize = 1_024;
11pub const MAX_FRAME_PADDING: u8 = 32;
13pub const MAX_SEQUENCE_EXCEPTIONS: usize = 100_000;
15pub const MAX_CONTENT_MEMBERS: usize = 100_000;
17
18#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
20pub struct FrameRange {
21 start: i64,
22 end: i64,
23 step: u32,
24}
25
26impl FrameRange {
27 pub fn new(start: i64, end: i64, step: u32) -> Result<Self> {
34 if step == 0 || end < start {
35 return Err(Error::new(
36 ErrorKind::InvalidArgument,
37 "frame range must be ascending with a non-zero step",
38 ));
39 }
40 let distance = i128::from(end) - i128::from(start);
41 if distance % i128::from(step) != 0 {
42 return Err(Error::new(
43 ErrorKind::InvalidArgument,
44 "frame range end must be aligned to its step",
45 ));
46 }
47 Ok(Self { start, end, step })
48 }
49
50 #[must_use]
52 pub const fn start(self) -> i64 {
53 self.start
54 }
55
56 #[must_use]
58 pub const fn end(self) -> i64 {
59 self.end
60 }
61
62 #[must_use]
64 pub const fn step(self) -> u32 {
65 self.step
66 }
67
68 #[must_use]
70 pub fn frame_count(self) -> u128 {
71 let distance = i128::from(self.end) - i128::from(self.start);
72 distance.unsigned_abs() / u128::from(self.step) + 1
73 }
74
75 #[must_use]
77 pub fn contains(self, frame: i64) -> bool {
78 if frame < self.start || frame > self.end {
79 return false;
80 }
81 let distance = i128::from(frame) - i128::from(self.start);
82 distance % i128::from(self.step) == 0
83 }
84}
85
86#[derive(Clone, Debug, Eq, Hash, PartialEq)]
88pub struct ImageSequencePattern {
89 prefix: String,
90 suffix: String,
91 padding: u8,
92}
93
94impl ImageSequencePattern {
95 pub fn new(prefix: impl Into<String>, suffix: impl Into<String>, padding: u8) -> Result<Self> {
102 let prefix = prefix.into();
103 let suffix = suffix.into();
104 let invalid_character = |character| matches!(character, '/' | '\\' | '\0');
105 if (prefix.is_empty() && suffix.is_empty())
106 || prefix.len().saturating_add(suffix.len()) > MAX_SEQUENCE_PATTERN_BYTES
107 || prefix.chars().any(invalid_character)
108 || suffix.chars().any(invalid_character)
109 || padding > MAX_FRAME_PADDING
110 {
111 return Err(Error::new(
112 ErrorKind::InvalidArgument,
113 "invalid image-sequence filename pattern",
114 ));
115 }
116 Ok(Self {
117 prefix,
118 suffix,
119 padding,
120 })
121 }
122
123 #[must_use]
125 pub fn prefix(&self) -> &str {
126 &self.prefix
127 }
128
129 #[must_use]
131 pub fn suffix(&self) -> &str {
132 &self.suffix
133 }
134
135 #[must_use]
137 pub const fn padding(&self) -> u8 {
138 self.padding
139 }
140
141 #[must_use]
143 pub fn filename(&self, frame: i64) -> String {
144 let frame = format!("{frame:0width$}", width = usize::from(self.padding));
145 format!("{}{frame}{}", self.prefix, self.suffix)
146 }
147}
148
149#[derive(Clone, Debug, Eq, PartialEq)]
151pub struct ImageSequenceDescriptor {
152 resource_id: ResourceId,
153 pattern: ImageSequencePattern,
154 frames: FrameRange,
155 rate: RationalRate,
156 known_missing_frames: Vec<i64>,
157}
158
159impl ImageSequenceDescriptor {
160 pub fn new(
167 resource_id: ResourceId,
168 pattern: ImageSequencePattern,
169 frames: FrameRange,
170 rate: RationalRate,
171 mut known_missing_frames: Vec<i64>,
172 ) -> Result<Self> {
173 if known_missing_frames.len() > MAX_SEQUENCE_EXCEPTIONS {
174 return Err(Error::new(
175 ErrorKind::InvalidArgument,
176 format!("image sequence has more than {MAX_SEQUENCE_EXCEPTIONS} sparse exceptions"),
177 ));
178 }
179 if known_missing_frames
180 .iter()
181 .any(|frame| !frames.contains(*frame))
182 {
183 return Err(Error::new(
184 ErrorKind::InvalidArgument,
185 "missing image-sequence frame is outside the regular frame domain",
186 ));
187 }
188 known_missing_frames.sort_unstable();
189 known_missing_frames.dedup();
190 Ok(Self {
191 resource_id,
192 pattern,
193 frames,
194 rate,
195 known_missing_frames,
196 })
197 }
198
199 #[must_use]
201 pub const fn resource_id(&self) -> ResourceId {
202 self.resource_id
203 }
204
205 #[must_use]
207 pub const fn pattern(&self) -> &ImageSequencePattern {
208 &self.pattern
209 }
210
211 #[must_use]
213 pub const fn frames(&self) -> FrameRange {
214 self.frames
215 }
216
217 #[must_use]
219 pub const fn rate(&self) -> RationalRate {
220 self.rate
221 }
222
223 #[must_use]
225 pub fn known_missing_frames(&self) -> &[i64] {
226 &self.known_missing_frames
227 }
228
229 #[must_use]
231 pub fn is_known_missing(&self, frame: i64) -> bool {
232 self.known_missing_frames.binary_search(&frame).is_ok()
233 }
234}
235
236#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
238pub struct ResourceRole(String);
239
240impl ResourceRole {
241 pub fn new(value: impl Into<String>) -> Result<Self> {
248 let value = value.into();
249 let valid_bytes = value
250 .bytes()
251 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':'));
252 let valid_namespace = value
253 .split_once(':')
254 .is_some_and(|(namespace, local)| !namespace.is_empty() && !local.is_empty());
255 if value.len() > MAX_RESOURCE_ROLE_BYTES || !valid_bytes || !valid_namespace {
256 return Err(Error::new(
257 ErrorKind::InvalidArgument,
258 format!(
259 "resource role must be a namespaced identifier of at most {MAX_RESOURCE_ROLE_BYTES} ASCII bytes"
260 ),
261 ));
262 }
263 Ok(Self(value))
264 }
265
266 #[must_use]
268 pub fn as_str(&self) -> &str {
269 &self.0
270 }
271}
272
273#[derive(Clone, Debug, Eq, PartialEq)]
275pub struct ResourceMember {
276 resource_id: ResourceId,
277 role: ResourceRole,
278 required: bool,
279}
280
281impl ResourceMember {
282 #[must_use]
284 pub const fn new(resource_id: ResourceId, role: ResourceRole, required: bool) -> Self {
285 Self {
286 resource_id,
287 role,
288 required,
289 }
290 }
291
292 #[must_use]
294 pub const fn resource_id(&self) -> ResourceId {
295 self.resource_id
296 }
297
298 #[must_use]
300 pub const fn role(&self) -> &ResourceRole {
301 &self.role
302 }
303
304 #[must_use]
306 pub const fn is_required(&self) -> bool {
307 self.required
308 }
309}
310
311#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
313#[non_exhaustive]
314pub enum ContentStructureKind {
315 SingleResource,
317 ImageSequence,
319 OrderedParts,
321 Package,
323}
324
325#[derive(Clone, Debug, Eq, PartialEq)]
326enum ContentStructureData {
327 SingleResource(ResourceId),
328 ImageSequence(ImageSequenceDescriptor),
329 OrderedParts(Vec<ResourceMember>),
330 Package(Vec<ResourceMember>),
331}
332
333#[derive(Clone, Debug, Eq, PartialEq)]
335pub struct ContentStructure(ContentStructureData);
336
337impl ContentStructure {
338 #[must_use]
340 pub const fn single_resource(resource_id: ResourceId) -> Self {
341 Self(ContentStructureData::SingleResource(resource_id))
342 }
343
344 #[must_use]
346 pub const fn image_sequence(descriptor: ImageSequenceDescriptor) -> Self {
347 Self(ContentStructureData::ImageSequence(descriptor))
348 }
349
350 pub fn ordered_parts(members: Vec<ResourceMember>) -> Result<Self> {
357 validate_members(&members)?;
358 if members.iter().any(|member| !member.is_required()) {
359 return Err(Error::new(
360 ErrorKind::InvalidArgument,
361 "ordered content parts must all be required",
362 ));
363 }
364 Ok(Self(ContentStructureData::OrderedParts(members)))
365 }
366
367 pub fn package(members: Vec<ResourceMember>) -> Result<Self> {
374 validate_members(&members)?;
375 if !members.iter().any(ResourceMember::is_required) {
376 return Err(Error::new(
377 ErrorKind::InvalidArgument,
378 "a content package must have at least one required member",
379 ));
380 }
381 Ok(Self(ContentStructureData::Package(members)))
382 }
383
384 #[must_use]
386 pub const fn kind(&self) -> ContentStructureKind {
387 match self.0 {
388 ContentStructureData::SingleResource(_) => ContentStructureKind::SingleResource,
389 ContentStructureData::ImageSequence(_) => ContentStructureKind::ImageSequence,
390 ContentStructureData::OrderedParts(_) => ContentStructureKind::OrderedParts,
391 ContentStructureData::Package(_) => ContentStructureKind::Package,
392 }
393 }
394
395 #[must_use]
397 pub const fn single_resource_id(&self) -> Option<ResourceId> {
398 match self.0 {
399 ContentStructureData::SingleResource(resource_id) => Some(resource_id),
400 _ => None,
401 }
402 }
403
404 #[must_use]
406 pub const fn image_sequence_descriptor(&self) -> Option<&ImageSequenceDescriptor> {
407 match &self.0 {
408 ContentStructureData::ImageSequence(descriptor) => Some(descriptor),
409 _ => None,
410 }
411 }
412
413 #[must_use]
415 pub fn members(&self) -> Option<&[ResourceMember]> {
416 match &self.0 {
417 ContentStructureData::OrderedParts(members)
418 | ContentStructureData::Package(members) => Some(members),
419 _ => None,
420 }
421 }
422
423 #[must_use]
425 pub fn resource_ids(&self) -> Vec<ResourceId> {
426 match &self.0 {
427 ContentStructureData::SingleResource(resource_id) => vec![*resource_id],
428 ContentStructureData::ImageSequence(descriptor) => vec![descriptor.resource_id()],
429 ContentStructureData::OrderedParts(members)
430 | ContentStructureData::Package(members) => {
431 members.iter().map(ResourceMember::resource_id).collect()
432 }
433 }
434 }
435}
436
437fn validate_members(members: &[ResourceMember]) -> Result<()> {
438 if members.is_empty() || members.len() > MAX_CONTENT_MEMBERS {
439 return Err(Error::new(
440 ErrorKind::InvalidArgument,
441 format!("content structure must have 1-{MAX_CONTENT_MEMBERS} members"),
442 ));
443 }
444 let unique: BTreeSet<_> = members.iter().map(ResourceMember::resource_id).collect();
445 if unique.len() != members.len() {
446 return Err(Error::new(
447 ErrorKind::InvalidArgument,
448 "content structure contains a duplicate resource",
449 ));
450 }
451 Ok(())
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn frame_ranges_are_inclusive_and_stepped() {
460 let frames = FrameRange::new(-2, 4, 2).expect("valid range");
461
462 assert_eq!(frames.frame_count(), 4);
463 assert!(frames.contains(-2));
464 assert!(frames.contains(4));
465 assert!(!frames.contains(1));
466 assert!(FrameRange::new(1, 0, 1).is_err());
467 assert!(FrameRange::new(0, 5, 2).is_err());
468 }
469
470 #[test]
471 fn sequence_patterns_format_frames_without_paths() {
472 let pattern = ImageSequencePattern::new("shot.", ".exr", 4).expect("valid pattern");
473
474 assert_eq!(pattern.filename(12), "shot.0012.exr");
475 assert_eq!(pattern.filename(-2), "shot.-002.exr");
476 assert!(ImageSequencePattern::new("directory/shot.", ".exr", 4).is_err());
477 assert!(ImageSequencePattern::new("", "", 0).is_err());
478 }
479
480 #[test]
481 fn sequence_descriptor_is_compact_and_canonical() {
482 let frames = FrameRange::new(1_001, 1_010, 1).expect("valid range");
483 let pattern = ImageSequencePattern::new("render.", ".exr", 4).expect("valid pattern");
484 let rate = RationalRate::new(24_000, 1_001).expect("valid rate");
485 let descriptor = ImageSequenceDescriptor::new(
486 ResourceId::new(),
487 pattern,
488 frames,
489 rate,
490 vec![1_007, 1_003, 1_007],
491 )
492 .expect("valid sequence");
493
494 assert_eq!(descriptor.known_missing_frames(), [1_003, 1_007]);
495 assert!(descriptor.is_known_missing(1_003));
496 assert!(!descriptor.is_known_missing(1_004));
497 assert!(
498 ImageSequenceDescriptor::new(
499 ResourceId::new(),
500 descriptor.pattern().clone(),
501 frames,
502 rate,
503 vec![999],
504 )
505 .is_err()
506 );
507 }
508
509 #[test]
510 fn resource_roles_are_namespaced_and_open_world() {
511 let standard = ResourceRole::new("org.postproject:essence").expect("valid role");
512 let vendor = ResourceRole::new("example.camera:playlist-v2").expect("valid role");
513
514 assert_eq!(standard.as_str(), "org.postproject:essence");
515 assert_eq!(vendor.as_str(), "example.camera:playlist-v2");
516 assert!(ResourceRole::new("essence").is_err());
517 assert!(ResourceRole::new("vendor:").is_err());
518 assert!(ResourceRole::new("vendor:side car").is_err());
519 }
520
521 #[test]
522 fn membership_preserves_role_and_requiredness() {
523 let resource_id = ResourceId::new();
524 let role = ResourceRole::new("org.postproject:thumbnail").expect("valid role");
525 let member = ResourceMember::new(resource_id, role, false);
526
527 assert_eq!(member.resource_id(), resource_id);
528 assert_eq!(member.role().as_str(), "org.postproject:thumbnail");
529 assert!(!member.is_required());
530 }
531
532 #[test]
533 fn compound_structures_enforce_membership_invariants() {
534 let essence = ResourceRole::new("org.postproject:essence").expect("valid role");
535 let thumbnail = ResourceRole::new("org.postproject:thumbnail").expect("valid role");
536 let required = ResourceMember::new(ResourceId::new(), essence, true);
537 let optional = ResourceMember::new(ResourceId::new(), thumbnail, false);
538
539 let ordered =
540 ContentStructure::ordered_parts(vec![required.clone()]).expect("valid ordered parts");
541 let package = ContentStructure::package(vec![required.clone(), optional.clone()])
542 .expect("valid package");
543
544 assert_eq!(ordered.kind(), ContentStructureKind::OrderedParts);
545 assert_eq!(package.members().expect("package members").len(), 2);
546 assert_eq!(
547 package.resource_ids(),
548 vec![required.resource_id(), optional.resource_id()]
549 );
550 assert!(ContentStructure::ordered_parts(vec![optional.clone()]).is_err());
551 assert!(ContentStructure::package(vec![optional]).is_err());
552 assert!(ContentStructure::package(vec![required.clone(), required]).is_err());
553 }
554}