Skip to main content

mediatime/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(test), no_std)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![cfg_attr(docsrs, allow(unused_attributes))]
5#![deny(missing_docs)]
6#![forbid(unsafe_code)]
7
8// `quickcheck` itself is std-only; the `quickcheck` feature implicitly pulls
9// std in, but the `no_std` attribute up top means `::std::*` paths still need
10// the crate brought into scope explicitly so `quickcheck-richderive`'s
11// generated `::std::boxed::Box<…>` shrink type resolves.
12#[cfg(feature = "quickcheck")]
13#[allow(unused_extern_crates)]
14extern crate std;
15
16use core::{
17  cmp::Ordering,
18  hash::{Hash, Hasher},
19  num::NonZeroU32,
20  time::Duration,
21};
22
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26/// A media timebase represented as a rational number: numerator over non-zero denominator.
27///
28/// Typical values: `1/1000` for millisecond PTS, `1/90000` for MPEG-TS,
29/// `1/48000` for audio samples, `30000/1001` for NTSC video (when used as a
30/// frame rate).
31///
32/// # Equality and ordering
33///
34/// Comparison is **value-based**: `1/2` equals `2/4`, and `1/3 < 2/3 < 1/1`.
35/// [`Hash`] hashes the reduced (lowest-terms) form, so equal rationals hash
36/// the same. Cross-multiplication uses `u64` intermediates — exact for any
37/// `u32` numerator / denominator.
38#[derive(Debug, Clone, Copy, Eq)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40#[cfg_attr(
41  feature = "quickcheck",
42  derive(::quickcheck_richderive::Arbitrary),
43  quickcheck(arbitrary = "crate::quickcheck_impls::timebase")
44)]
45pub struct Timebase {
46  #[cfg_attr(feature = "serde", serde(rename = "numerator"))]
47  num: u32,
48  #[cfg_attr(feature = "serde", serde(rename = "denominator"))]
49  den: NonZeroU32,
50}
51
52impl Default for Timebase {
53  #[cfg_attr(not(tarpaulin), inline(always))]
54  fn default() -> Self {
55    Self::new(1, NonZeroU32::new(1).unwrap())
56  }
57}
58
59impl Timebase {
60  /// Creates a new `Timebase` with the given numerator and non-zero denominator.
61  #[cfg_attr(not(tarpaulin), inline(always))]
62  pub const fn new(num: u32, den: NonZeroU32) -> Self {
63    Self { num, den }
64  }
65
66  /// Returns the numerator.
67  #[cfg_attr(not(tarpaulin), inline(always))]
68  pub const fn num(&self) -> u32 {
69    self.num
70  }
71
72  /// Returns the denominator.
73  #[cfg_attr(not(tarpaulin), inline(always))]
74  pub const fn den(&self) -> NonZeroU32 {
75    self.den
76  }
77
78  /// Set the value of the numerator.
79  #[cfg_attr(not(tarpaulin), inline(always))]
80  pub const fn with_num(mut self, num: u32) -> Self {
81    self.set_num(num);
82    self
83  }
84
85  /// Set the value of the denominator.
86  #[cfg_attr(not(tarpaulin), inline(always))]
87  pub const fn with_den(mut self, den: NonZeroU32) -> Self {
88    self.set_den(den);
89    self
90  }
91
92  /// Set the value of the numerator in place.
93  #[cfg_attr(not(tarpaulin), inline(always))]
94  pub const fn set_num(&mut self, num: u32) -> &mut Self {
95    self.num = num;
96    self
97  }
98
99  /// Set the value of the denominator in place.
100  #[cfg_attr(not(tarpaulin), inline(always))]
101  pub const fn set_den(&mut self, den: NonZeroU32) -> &mut Self {
102    self.den = den;
103    self
104  }
105
106  /// Rescales `pts` from timebase `from` to timebase `to`, rounding toward zero.
107  ///
108  /// Equivalent to FFmpeg's `av_rescale_q`. Uses a 128-bit intermediate to
109  /// avoid overflow for typical video PTS ranges. If the rescaled value
110  /// exceeds `i64`'s range (pathological for real video), the result is
111  /// **saturated** to `i64::MIN` or `i64::MAX` — this matches the behavior
112  /// promised by `duration_to_pts` and avoids silent wraparound.
113  ///
114  /// # Panics
115  ///
116  /// Panics if `to.num() == 0` (division by zero).
117  #[cfg_attr(not(tarpaulin), inline(always))]
118  pub const fn rescale_pts(pts: i64, from: Self, to: Self) -> i64 {
119    assert!(to.num != 0, "target timebase numerator must be non-zero");
120    // pts * (from.num / from.den) / (to.num / to.den)
121    // = pts * from.num * to.den / (from.den * to.num)
122    let numerator = (pts as i128) * (from.num as i128) * (to.den.get() as i128);
123    let denominator = (from.den.get() as i128) * (to.num as i128);
124    let q = numerator / denominator;
125    if q > i64::MAX as i128 {
126      i64::MAX
127    } else if q < i64::MIN as i128 {
128      i64::MIN
129    } else {
130      q as i64
131    }
132  }
133
134  /// Rescales `pts` from this timebase to `to`, rounding toward zero.
135  ///
136  /// Method form of [`Self::rescale_pts`]: `self` is the source timebase.
137  ///
138  /// # Panics
139  ///
140  /// Panics if `to.num() == 0` (division by zero).
141  #[cfg_attr(not(tarpaulin), inline(always))]
142  pub const fn rescale(&self, pts: i64, to: Self) -> i64 {
143    Self::rescale_pts(pts, *self, to)
144  }
145
146  /// Treats `self` as a frame rate (frames per second) and returns the
147  /// [`Duration`] corresponding to `frames` frames.
148  ///
149  /// Examples:
150  /// - 30 fps: `Timebase::new(30, nz(1)).frames_to_duration(15)` → 500 ms
151  /// - NTSC: `Timebase::new(30000, nz(1001)).frames_to_duration(30000)` → 1001 ms
152  ///
153  /// Note that "frame rate" and "PTS timebase" are conceptually *different*
154  /// rationals even though both are represented as [`Timebase`]. A 30 fps
155  /// stream typically has PTS timebase `1/30` (seconds per unit) and frame
156  /// rate `30/1` (frames per second) — they are reciprocals.
157  ///
158  /// # Panics
159  ///
160  /// Panics if `self.num() == 0` (division by zero).
161  #[cfg_attr(not(tarpaulin), inline(always))]
162  pub const fn frames_to_duration(&self, frames: u32) -> Duration {
163    // frames / (num/den) seconds = frames * den / num seconds
164    let num = self.num as u128;
165    let den = self.den.get() as u128;
166    assert!(num != 0, "frame rate numerator must be non-zero");
167    let total_ns = (frames as u128) * den * 1_000_000_000 / num;
168    let secs = (total_ns / 1_000_000_000) as u64;
169    let nanos = (total_ns % 1_000_000_000) as u32;
170    Duration::new(secs, nanos)
171  }
172
173  /// Converts a [`Duration`] into the number of PTS units this timebase
174  /// represents, rounding toward zero.
175  ///
176  /// Inverse of "multiplying a PTS value by this timebase to get seconds".
177  /// Saturates at `i64::MAX` if the duration is absurdly large for this
178  /// timebase. Returns `0` if `self.num() == 0` (a degenerate timebase).
179  #[cfg_attr(not(tarpaulin), inline(always))]
180  pub const fn duration_to_pts(&self, d: Duration) -> i64 {
181    let num = self.num as u128;
182    if num == 0 {
183      return 0;
184    }
185    let den = self.den.get() as u128;
186    // pts_units = duration_ns * den / (num * 1e9)
187    let ns = d.as_nanos();
188    let pts = ns * den / (num * 1_000_000_000);
189    if pts > i64::MAX as u128 {
190      i64::MAX
191    } else {
192      pts as i64
193    }
194  }
195}
196
197impl PartialEq for Timebase {
198  #[cfg_attr(not(tarpaulin), inline(always))]
199  fn eq(&self, other: &Self) -> bool {
200    // a.num * b.den == b.num * a.den (cross-multiply; u32 * u32 fits in u64)
201    (self.num as u64) * (other.den.get() as u64) == (other.num as u64) * (self.den.get() as u64)
202  }
203}
204
205impl Hash for Timebase {
206  #[cfg_attr(not(tarpaulin), inline(always))]
207  fn hash<H: Hasher>(&self, state: &mut H) {
208    let d = self.den.get();
209    // gcd(num, d) ≥ 1 because d ≥ 1 (NonZeroU32).
210    let g = gcd_u32(self.num, d);
211    (self.num / g).hash(state);
212    (d / g).hash(state);
213  }
214}
215
216impl Ord for Timebase {
217  #[cfg_attr(not(tarpaulin), inline(always))]
218  fn cmp(&self, other: &Self) -> Ordering {
219    let lhs = (self.num as u64) * (other.den.get() as u64);
220    let rhs = (other.num as u64) * (self.den.get() as u64);
221    lhs.cmp(&rhs)
222  }
223}
224
225impl PartialOrd for Timebase {
226  #[cfg_attr(not(tarpaulin), inline(always))]
227  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
228    Some(self.cmp(other))
229  }
230}
231
232/// A presentation timestamp, expressed as a PTS value in units of an associated [`Timebase`].
233///
234/// # Equality and ordering
235///
236/// Comparison is **value-based** (same instant compares equal even across
237/// different timebases): `Timestamp(1000, 1/1000)` equals
238/// `Timestamp(90_000, 1/90_000)`. [`Hash`] hashes the reduced-form rational
239/// instant `(pts · num, den)`, so equal timestamps hash the same.
240///
241/// Cross-timebase comparisons use 128-bit cross-multiplication — no division,
242/// no rounding error. Same-timebase comparisons take a fast path on `pts`.
243#[derive(Debug, Default, Clone, Copy)]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245#[cfg_attr(
246  feature = "quickcheck",
247  derive(::quickcheck_richderive::Arbitrary),
248  quickcheck(arbitrary = "crate::quickcheck_impls::timestamp")
249)]
250pub struct Timestamp {
251  pts: i64,
252  timebase: Timebase,
253}
254
255impl Timestamp {
256  /// Creates a new `Timestamp` with the given PTS and timebase.
257  #[cfg_attr(not(tarpaulin), inline(always))]
258  pub const fn new(pts: i64, timebase: Timebase) -> Self {
259    Self { pts, timebase }
260  }
261
262  /// Returns the presentation timestamp, in units of [`Self::timebase`].
263  ///
264  /// To obtain a [`Duration`], use [`Self::duration_since`] against a reference
265  /// timestamp, or rescale via [`Self::rescale_to`].
266  #[cfg_attr(not(tarpaulin), inline(always))]
267  pub const fn pts(&self) -> i64 {
268    self.pts
269  }
270
271  /// Returns the timebase of the timestamp.
272  #[cfg_attr(not(tarpaulin), inline(always))]
273  pub const fn timebase(&self) -> Timebase {
274    self.timebase
275  }
276
277  /// Set the value of the presentation timestamp.
278  #[cfg_attr(not(tarpaulin), inline(always))]
279  pub const fn with_pts(mut self, pts: i64) -> Self {
280    self.set_pts(pts);
281    self
282  }
283
284  /// Set the value of the presentation timestamp in place.
285  #[cfg_attr(not(tarpaulin), inline(always))]
286  pub const fn set_pts(&mut self, pts: i64) -> &mut Self {
287    self.pts = pts;
288    self
289  }
290
291  /// Returns a new `Timestamp` representing the same instant in a different timebase.
292  ///
293  /// Rounds toward zero via [`Timebase::rescale_pts`]; round-tripping through a
294  /// coarser timebase can lose precision.
295  #[cfg_attr(not(tarpaulin), inline(always))]
296  pub const fn rescale_to(self, target: Timebase) -> Self {
297    Self {
298      pts: self.timebase.rescale(self.pts, target),
299      timebase: target,
300    }
301  }
302
303  /// Returns a new [`Timestamp`] representing this instant shifted backward
304  /// by `d`, in the same timebase. Saturates at `i64::MIN` if the subtraction
305  /// would underflow (pathological for real video).
306  ///
307  /// Useful for "virtual past" seeding: e.g., initializing a warmup-filter
308  /// state to `ts - min_duration` so the first detected cut can fire
309  /// immediately.
310  #[cfg_attr(not(tarpaulin), inline(always))]
311  pub const fn saturating_sub_duration(self, d: Duration) -> Self {
312    let units = self.timebase.duration_to_pts(d);
313    Self::new(self.pts.saturating_sub(units), self.timebase)
314  }
315
316  /// `const fn` form of [`Ord::cmp`]. Compares two timestamps by the instant
317  /// they represent, rescaling if timebases differ.
318  ///
319  /// Uses a 128-bit cross-multiply for the mixed-timebase case; no division,
320  /// so no rounding error. Same-timebase comparisons take a direct fast path.
321  #[cfg_attr(not(tarpaulin), inline(always))]
322  pub const fn cmp_semantic(&self, other: &Self) -> Ordering {
323    if self.timebase.num == other.timebase.num
324      && self.timebase.den.get() == other.timebase.den.get()
325    {
326      return if self.pts < other.pts {
327        Ordering::Less
328      } else if self.pts > other.pts {
329        Ordering::Greater
330      } else {
331        Ordering::Equal
332      };
333    }
334    // self.pts * self.num / self.den  vs  other.pts * other.num / other.den
335    //   ⇔ self.pts * self.num * other.den  vs  other.pts * other.num * self.den
336    let lhs = (self.pts as i128) * (self.timebase.num as i128) * (other.timebase.den.get() as i128);
337    let rhs =
338      (other.pts as i128) * (other.timebase.num as i128) * (self.timebase.den.get() as i128);
339    if lhs < rhs {
340      Ordering::Less
341    } else if lhs > rhs {
342      Ordering::Greater
343    } else {
344      Ordering::Equal
345    }
346  }
347
348  /// Returns the [`Duration`] from PTS zero (in this timebase) to `self`, or
349  /// `None` if `self.pts() < 0` (pre-roll / edit-list cases can produce
350  /// negative PTS, which has no [`Duration`] representation).
351  ///
352  /// Equivalent to `self.duration_since(&Timestamp::new(0, self.timebase()))`.
353  #[cfg_attr(not(tarpaulin), inline(always))]
354  pub const fn duration(&self) -> Option<Duration> {
355    self.duration_since(&Self::new(0, self.timebase))
356  }
357
358  /// Returns the elapsed [`Duration`] from `earlier` to `self`, or `None` if
359  /// `earlier` is after `self`.
360  ///
361  /// Works across different timebases. Computes the exact rational difference
362  /// first using a common denominator, then truncates once when converting to
363  /// nanoseconds for the returned [`Duration`].
364  /// If the result would exceed `Duration::MAX` (pathological: seconds don't
365  /// fit in `u64`), saturates to `Duration::MAX` rather than wrapping.
366  #[cfg_attr(not(tarpaulin), inline(always))]
367  pub const fn duration_since(&self, earlier: &Self) -> Option<Duration> {
368    const NS_PER_SEC: i128 = 1_000_000_000;
369
370    // Compute LCM of the two denominators via GCD so we can subtract in a
371    // common timebase without per-endpoint truncation.
372    let self_den = self.timebase.den.get();
373    let earlier_den = earlier.timebase.den.get();
374
375    let mut a = self_den;
376    let mut b = earlier_den;
377    while b != 0 {
378      let r = a % b;
379      a = b;
380      b = r;
381    }
382    let gcd = a as i128;
383
384    let self_scale = (earlier_den as i128) / gcd;
385    let earlier_scale = (self_den as i128) / gcd;
386    let common_den = (self_den as i128) * self_scale; // = lcm(self_den, earlier_den)
387
388    // Exact rational difference in units of 1/common_den seconds.
389    let diff_num = (self.pts as i128) * (self.timebase.num as i128) * self_scale
390      - (earlier.pts as i128) * (earlier.timebase.num as i128) * earlier_scale;
391    if diff_num < 0 {
392      return None;
393    }
394
395    // Single truncation: convert to whole seconds + nanosecond remainder.
396    let secs_i128 = diff_num / common_den;
397    if secs_i128 > u64::MAX as i128 {
398      return Some(Duration::MAX);
399    }
400    let rem = diff_num % common_den;
401    let nanos = (rem * NS_PER_SEC / common_den) as u32;
402    Some(Duration::new(secs_i128 as u64, nanos))
403  }
404}
405
406impl PartialEq for Timestamp {
407  #[cfg_attr(not(tarpaulin), inline(always))]
408  fn eq(&self, other: &Self) -> bool {
409    self.cmp_semantic(other).is_eq()
410  }
411}
412impl Eq for Timestamp {}
413
414impl Hash for Timestamp {
415  #[cfg_attr(not(tarpaulin), inline(always))]
416  fn hash<H: Hasher>(&self, state: &mut H) {
417    // Canonical representation: instant as reduced rational (pts * num, den).
418    let n: i128 = (self.pts as i128) * (self.timebase.num as i128);
419    let d: u128 = self.timebase.den.get() as u128;
420    // gcd operates on magnitudes; denominator stays positive. gcd ≥ 1 since d ≥ 1.
421    let g = gcd_u128(n.unsigned_abs(), d) as i128;
422    let rn = n / g;
423    let rd = (d as i128) / g;
424    rn.hash(state);
425    rd.hash(state);
426  }
427}
428
429impl Ord for Timestamp {
430  #[cfg_attr(not(tarpaulin), inline(always))]
431  fn cmp(&self, other: &Self) -> Ordering {
432    self.cmp_semantic(other)
433  }
434}
435
436impl PartialOrd for Timestamp {
437  #[cfg_attr(not(tarpaulin), inline(always))]
438  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
439    Some(self.cmp(other))
440  }
441}
442
443/// A half-open time range `[start, end)` in a given [`Timebase`].
444///
445/// Represents the extent of a detected event — for example, a fade-out →
446/// fade-in span. When `start == end`, the range is degenerate (an instant);
447/// see [`Self::instant`].
448///
449/// Both endpoints share the same [`Timebase`]. To compare ranges across
450/// different timebases, rescale one of them first (e.g., by calling
451/// [`Timestamp::rescale_to`] on each endpoint).
452#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
453#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
454#[cfg_attr(
455  feature = "quickcheck",
456  derive(::quickcheck_richderive::Arbitrary),
457  quickcheck(arbitrary = "crate::quickcheck_impls::time_range")
458)]
459pub struct TimeRange {
460  start: i64,
461  end: i64,
462  timebase: Timebase,
463}
464
465impl TimeRange {
466  /// Creates a new `TimeRange` with the given start/end PTS and shared timebase.
467  ///
468  /// # Panics
469  ///
470  /// - Panics if `end < start` (negative duration).
471  #[cfg_attr(not(tarpaulin), inline(always))]
472  pub const fn new(start: i64, end: i64, timebase: Timebase) -> Self {
473    assert!(start <= end, "end must not be greater or equal to start");
474
475    Self {
476      start,
477      end,
478      timebase,
479    }
480  }
481
482  /// Bypass-invariant constructor used only by the `buffa` decode path.
483  ///
484  /// During protobuf field-by-field merging, intermediate states may
485  /// temporarily violate `start <= end` (e.g. `start` field arrives before
486  /// `end`, so the partially-decoded struct holds `start=100, end=0`).
487  /// The normal `new()` constructor panics in that case. This constructor
488  /// skips the assertion so decode can proceed; the final decoded value
489  /// is always consistent because the encoder never writes `start > end`.
490  #[cfg(feature = "buffa")]
491  #[inline(always)]
492  pub(crate) const fn new_for_decode(start: i64, end: i64, timebase: Timebase) -> Self {
493    Self {
494      start,
495      end,
496      timebase,
497    }
498  }
499
500  /// Fallible variant of [`Self::new`]: returns `None` if `end < start`
501  /// instead of panicking. Accepts `start == end` (degenerate instant range).
502  #[cfg_attr(not(tarpaulin), inline(always))]
503  pub const fn try_new(start: i64, end: i64, timebase: Timebase) -> Option<Self> {
504    if start <= end {
505      Some(Self {
506        start,
507        end,
508        timebase,
509      })
510    } else {
511      None
512    }
513  }
514
515  /// Creates a degenerate (instant) range where `start == end == ts.pts()`.
516  #[cfg_attr(not(tarpaulin), inline(always))]
517  pub const fn instant(ts: Timestamp) -> Self {
518    Self {
519      start: ts.pts(),
520      end: ts.pts(),
521      timebase: ts.timebase(),
522    }
523  }
524
525  /// Returns the start PTS in the range's timebase units.
526  #[cfg_attr(not(tarpaulin), inline(always))]
527  pub const fn start_pts(&self) -> i64 {
528    self.start
529  }
530
531  /// Returns the end PTS in the range's timebase units.
532  #[cfg_attr(not(tarpaulin), inline(always))]
533  pub const fn end_pts(&self) -> i64 {
534    self.end
535  }
536
537  /// Returns the shared timebase.
538  #[cfg_attr(not(tarpaulin), inline(always))]
539  pub const fn timebase(&self) -> Timebase {
540    self.timebase
541  }
542
543  /// Returns the start as a [`Timestamp`].
544  #[cfg_attr(not(tarpaulin), inline(always))]
545  pub const fn start(&self) -> Timestamp {
546    Timestamp::new(self.start, self.timebase)
547  }
548
549  /// Returns the end as a [`Timestamp`].
550  #[cfg_attr(not(tarpaulin), inline(always))]
551  pub const fn end(&self) -> Timestamp {
552    Timestamp::new(self.end, self.timebase)
553  }
554
555  /// Sets the start PTS.
556  #[cfg_attr(not(tarpaulin), inline(always))]
557  pub const fn with_start(mut self, val: i64) -> Self {
558    self.start = val;
559    self
560  }
561
562  /// Sets the start PTS in place.
563  #[cfg_attr(not(tarpaulin), inline(always))]
564  pub const fn set_start(&mut self, val: i64) -> &mut Self {
565    self.start = val;
566    self
567  }
568
569  /// Sets the end PTS.
570  #[cfg_attr(not(tarpaulin), inline(always))]
571  pub const fn with_end(mut self, val: i64) -> Self {
572    self.end = val;
573    self
574  }
575
576  /// Sets the end PTS in place.
577  #[cfg_attr(not(tarpaulin), inline(always))]
578  pub const fn set_end(&mut self, val: i64) -> &mut Self {
579    self.end = val;
580    self
581  }
582
583  /// Sets the shared timebase.
584  #[cfg_attr(not(tarpaulin), inline(always))]
585  pub const fn with_timebase(mut self, timebase: Timebase) -> Self {
586    self.set_timebase(timebase);
587    self
588  }
589
590  /// Sets the shared timebase in place.
591  #[cfg_attr(not(tarpaulin), inline(always))]
592  pub const fn set_timebase(&mut self, timebase: Timebase) -> &mut Self {
593    self.timebase = timebase;
594    self
595  }
596
597  /// Returns `true` if `start == end` (a degenerate instant range).
598  #[cfg_attr(not(tarpaulin), inline(always))]
599  pub const fn is_instant(&self) -> bool {
600    self.start == self.end
601  }
602
603  /// Returns the span in PTS units (`end - start`) in this timebase.
604  ///
605  /// Always non-negative given the `start <= end` constructor invariant.
606  /// Saturates at `i64::MAX` in the pathological case where `end - start`
607  /// would overflow `i64` (e.g., `start = i64::MIN`, `end = i64::MAX`).
608  #[cfg_attr(not(tarpaulin), inline(always))]
609  pub const fn total_pts(&self) -> i64 {
610    self.end.saturating_sub(self.start)
611  }
612
613  /// Returns the elapsed [`Duration`] from `start` to `end`, or `None` if
614  /// `end` is before `start`.
615  #[cfg_attr(not(tarpaulin), inline(always))]
616  pub const fn duration(&self) -> Duration {
617    self
618      .end()
619      .duration_since(&self.start())
620      .expect("end must greater than or equal to start")
621  }
622
623  /// Returns a new `TimeRange` representing the same span in a different timebase.
624  ///
625  /// Rescales both endpoints via [`Timebase::rescale_pts`] (rounds toward zero);
626  /// round-tripping through a coarser timebase can lose precision. Because
627  /// rescaling is monotonic, the `start <= end` invariant is preserved.
628  #[cfg_attr(not(tarpaulin), inline(always))]
629  pub const fn rescale_to(self, target: Timebase) -> Self {
630    Self {
631      start: self.timebase.rescale(self.start, target),
632      end: self.timebase.rescale(self.end, target),
633      timebase: target,
634    }
635  }
636
637  /// Linearly interpolates between `start` and `end`: `t = 0.0` returns
638  /// `start`, `t = 1.0` returns `end`, `t = 0.5` the midpoint. `t` is
639  /// clamped to `[0.0, 1.0]`. Rounds toward zero.
640  ///
641  /// Use this to map an old-style bias value `b ∈ [-1, 1]` onto the range:
642  /// `range.interpolate((b + 1.0) * 0.5)`.
643  #[cfg_attr(not(tarpaulin), inline(always))]
644  pub const fn interpolate(&self, t: f64) -> Timestamp {
645    let t = t.clamp(0.0, 1.0);
646    let delta = self.end.saturating_sub(self.start);
647    let offset = (delta as f64 * t) as i64;
648    Timestamp::new(self.start.saturating_add(offset), self.timebase)
649  }
650}
651
652#[cfg_attr(not(tarpaulin), inline(always))]
653const fn gcd_u32(mut a: u32, mut b: u32) -> u32 {
654  while b != 0 {
655    let t = b;
656    b = a % b;
657    a = t;
658  }
659  a
660}
661
662#[cfg_attr(not(tarpaulin), inline(always))]
663const fn gcd_u128(mut a: u128, mut b: u128) -> u128 {
664  while b != 0 {
665    let t = b;
666    b = a % b;
667    a = t;
668  }
669  a
670}
671
672/// `fn(&mut quickcheck::Gen) -> T` helpers consumed by the per-type
673/// `#[quickcheck(arbitrary = "…")]` attributes on each type's
674/// `quickcheck-richderive::Arbitrary` derive. The derive emits the actual
675/// `impl quickcheck::Arbitrary` blocks; these helpers own the bodies and
676/// preserve invariants the field-by-field default would otherwise violate
677/// (non-zero denom, non-negative pts, well-formed range).
678#[cfg(feature = "quickcheck")]
679#[cfg_attr(docsrs, doc(cfg(feature = "quickcheck")))]
680pub mod quickcheck_impls {
681  use crate::{TimeRange, Timebase, Timestamp};
682  use core::num::NonZeroU32;
683  use quickcheck::{Arbitrary, Gen};
684
685  /// Non-zero denominator + arbitrary numerator. `NonZeroU32` impls
686  /// `quickcheck::Arbitrary` directly, so the loop in the previous
687  /// hand-written impl is unnecessary.
688  pub fn timebase(g: &mut Gen) -> Timebase {
689    Timebase::new(u32::arbitrary(g), NonZeroU32::arbitrary(g))
690  }
691
692  /// Non-negative `pts` + arbitrary `Timebase`.
693  pub fn timestamp(g: &mut Gen) -> Timestamp {
694    Timestamp::new(non_negative_i64(g), timebase(g))
695  }
696
697  /// `[start, end)` with `start <= end`, both non-negative. The previous
698  /// hand-written impl reused a single `Gen` draw for the timebase; same
699  /// here. When the two endpoints happen to coincide we bump `end` by 1 to
700  /// keep the range non-degenerate (matches the original behavior).
701  pub fn time_range(g: &mut Gen) -> TimeRange {
702    let a = non_negative_i64(g);
703    let b = non_negative_i64(g);
704    let start = a.min(b);
705    let mut end = a.max(b);
706    if start == end {
707      end = end.saturating_add(1);
708    }
709    TimeRange::new(start, end, timebase(g))
710  }
711
712  fn non_negative_i64(g: &mut Gen) -> i64 {
713    loop {
714      let d = i64::arbitrary(g);
715      if d >= 0 {
716        return d;
717      }
718    }
719  }
720}
721
722#[cfg(feature = "arbitrary")]
723#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
724const _: () = {
725  use arbitrary::Arbitrary;
726
727  impl<'a> Arbitrary<'a> for Timebase {
728    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
729      // Generate a random non-zero denominator
730      let d = u.arbitrary::<core::num::NonZeroU32>()?;
731      let num = u.arbitrary::<u32>()?;
732      Ok(Timebase::new(num, d))
733    }
734  }
735
736  impl<'a> Arbitrary<'a> for Timestamp {
737    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
738      non_negative_i64(u).and_then(|i| u.arbitrary().map(|tb| Self::new(i, tb)))
739    }
740  }
741
742  impl<'a> Arbitrary<'a> for TimeRange {
743    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
744      let a = non_negative_i64(u)?;
745      let b = non_negative_i64(u)?;
746      let start = a.min(b);
747      let mut end = a.max(b);
748
749      if start == end {
750        end = end.saturating_add(1);
751      }
752
753      Ok(TimeRange::new(start, end, u.arbitrary()?))
754    }
755  }
756
757  fn non_negative_i64(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<i64> {
758    loop {
759      let val = u.arbitrary::<i64>()?;
760      if val >= 0 {
761        return Ok(val);
762      }
763    }
764  }
765};
766
767#[cfg(test)]
768mod tests {
769  use super::*;
770
771  const fn nz(n: u32) -> NonZeroU32 {
772    match NonZeroU32::new(n) {
773      Some(v) => v,
774      None => panic!("zero"),
775    }
776  }
777
778  fn hash_of<T: Hash>(v: &T) -> u64 {
779    use std::collections::hash_map::DefaultHasher;
780    let mut h = DefaultHasher::new();
781    v.hash(&mut h);
782    h.finish()
783  }
784
785  #[test]
786  fn rescale_identity() {
787    let tb = Timebase::new(1, nz(1000));
788    assert_eq!(Timebase::rescale_pts(42, tb, tb), 42);
789    assert_eq!(tb.rescale(42, tb), 42);
790  }
791
792  #[test]
793  fn rescale_between_timebases() {
794    let ms = Timebase::new(1, nz(1000));
795    let mpeg = Timebase::new(1, nz(90_000));
796    assert_eq!(Timebase::rescale_pts(1000, ms, mpeg), 90_000);
797    assert_eq!(ms.rescale(1000, mpeg), 90_000);
798    assert_eq!(mpeg.rescale(90_000, ms), 1000);
799  }
800
801  #[test]
802  fn rescale_rounds_toward_zero() {
803    let from = Timebase::new(1, nz(1000));
804    let to = Timebase::new(1, nz(3));
805    assert_eq!(from.rescale(1, to), 0);
806    assert_eq!(from.rescale(-1, to), 0);
807  }
808
809  #[test]
810  fn rescale_saturates_on_i64_overflow() {
811    // Rescale from a coarse timebase (u32::MAX seconds per tick) to a fine
812    // one (1/u32::MAX seconds per tick): even a modest pts blows past
813    // i64::MAX in the 128-bit intermediate. `rescale_pts` should saturate
814    // to i64::MAX / i64::MIN rather than wrap via `as i64`.
815    let from = Timebase::new(u32::MAX, nz(1));
816    let to = Timebase::new(1, nz(u32::MAX));
817    assert_eq!(from.rescale(1_000_000, to), i64::MAX);
818    assert_eq!(from.rescale(-1_000_000, to), i64::MIN);
819  }
820
821  #[test]
822  fn timebase_eq_is_semantic() {
823    // 1/2 == 2/4 == 3/6
824    let a = Timebase::new(1, nz(2));
825    let b = Timebase::new(2, nz(4));
826    let c = Timebase::new(3, nz(6));
827    assert_eq!(a, b);
828    assert_eq!(b, c);
829    assert_eq!(a, c);
830    // 1/2 != 1/3
831    let d = Timebase::new(1, nz(3));
832    assert_ne!(a, d);
833  }
834
835  #[test]
836  fn timebase_hash_matches_eq() {
837    let a = Timebase::new(1, nz(2));
838    let b = Timebase::new(2, nz(4));
839    let c = Timebase::new(3, nz(6));
840    assert_eq!(hash_of(&a), hash_of(&b));
841    assert_eq!(hash_of(&b), hash_of(&c));
842  }
843
844  #[test]
845  fn timebase_ord_is_numeric() {
846    let third = Timebase::new(1, nz(3));
847    let half = Timebase::new(1, nz(2));
848    let two_thirds = Timebase::new(2, nz(3));
849    let one = Timebase::new(1, nz(1));
850    assert!(third < half);
851    assert!(half < two_thirds);
852    assert!(two_thirds < one);
853    // Structural lex order would have reported (1, 1) < (1, 3); verify it doesn't.
854    assert!(one > third);
855  }
856
857  #[test]
858  fn timebase_num_zero() {
859    // 0/3 == 0/5, and both compare less than anything positive.
860    let a = Timebase::new(0, nz(3));
861    let b = Timebase::new(0, nz(5));
862    assert_eq!(a, b);
863    assert_eq!(hash_of(&a), hash_of(&b));
864    assert!(a < Timebase::new(1, nz(1_000_000)));
865  }
866
867  #[test]
868  fn timestamp_cmp_same_timebase() {
869    let tb = Timebase::new(1, nz(1000));
870    let a = Timestamp::new(100, tb);
871    let b = Timestamp::new(200, tb);
872    assert!(a < b);
873    assert!(b > a);
874    assert_eq!(a, a);
875    assert_eq!(a.cmp(&b), Ordering::Less);
876  }
877
878  #[test]
879  fn timestamp_cmp_cross_timebase() {
880    let a = Timestamp::new(1000, Timebase::new(1, nz(1000)));
881    let b = Timestamp::new(90_000, Timebase::new(1, nz(90_000)));
882    assert_eq!(a, b);
883    assert_eq!(a.cmp(&b), Ordering::Equal);
884
885    let c = Timestamp::new(500, Timebase::new(1, nz(1000)));
886    assert!(c < a);
887    assert!(a > c);
888  }
889
890  #[test]
891  fn timestamp_hash_matches_semantic_eq() {
892    let a = Timestamp::new(1000, Timebase::new(1, nz(1000)));
893    let b = Timestamp::new(90_000, Timebase::new(1, nz(90_000)));
894    let c = Timestamp::new(2000, Timebase::new(1, nz(2000))); // also 1.0s
895    assert_eq!(a, b);
896    assert_eq!(hash_of(&a), hash_of(&b));
897    assert_eq!(hash_of(&a), hash_of(&c));
898  }
899
900  #[test]
901  fn timestamp_hash_negative_pts() {
902    // Pre-roll / edit list scenarios: -500 ms should equal -45_000 @ 1/90_000.
903    let a = Timestamp::new(-500, Timebase::new(1, nz(1000)));
904    let b = Timestamp::new(-45_000, Timebase::new(1, nz(90_000)));
905    assert_eq!(a, b);
906    assert_eq!(hash_of(&a), hash_of(&b));
907  }
908
909  #[test]
910  fn rescale_to_preserves_instant() {
911    let ms = Timebase::new(1, nz(1000));
912    let mpeg = Timebase::new(1, nz(90_000));
913    let a = Timestamp::new(1000, ms);
914    let b = a.rescale_to(mpeg);
915    assert_eq!(b.pts(), 90_000);
916    assert_eq!(b.timebase(), mpeg);
917    assert_eq!(a, b);
918  }
919
920  #[test]
921  fn timestamp_duration_from_zero() {
922    let ms = Timebase::new(1, nz(1000));
923    let ts = Timestamp::new(1500, ms);
924    assert_eq!(ts.duration(), Some(Duration::from_millis(1500)));
925    assert_eq!(Timestamp::new(0, ms).duration(), Some(Duration::ZERO));
926
927    // Cross-timebase equivalence: same instant, same duration.
928    let mpeg = Timebase::new(1, nz(90_000));
929    assert_eq!(
930      Timestamp::new(90_000, mpeg).duration(),
931      Some(Duration::from_secs(1))
932    );
933
934    // Negative PTS (pre-roll) has no Duration representation.
935    assert_eq!(Timestamp::new(-1, ms).duration(), None);
936  }
937
938  #[test]
939  fn duration_since_same_timebase() {
940    let tb = Timebase::new(1, nz(1000));
941    let a = Timestamp::new(1500, tb);
942    let b = Timestamp::new(500, tb);
943    assert_eq!(a.duration_since(&b), Some(Duration::from_millis(1000)));
944    assert_eq!(b.duration_since(&a), None);
945  }
946
947  #[test]
948  fn duration_since_cross_timebase() {
949    let a = Timestamp::new(1000, Timebase::new(1, nz(1000)));
950    let b = Timestamp::new(45_000, Timebase::new(1, nz(90_000)));
951    assert_eq!(a.duration_since(&b), Some(Duration::from_millis(500)));
952  }
953
954  #[test]
955  fn duration_since_saturates_to_duration_max_on_overflow() {
956    // Use a timebase of `u32::MAX / 1` (each tick ≈ 2^32 seconds). Then
957    // i64::MAX ticks ≈ 2^95 seconds — far more than u64::MAX. Should
958    // saturate to Duration::MAX rather than wrap when casting seconds to u64.
959    let tb = Timebase::new(u32::MAX, nz(1));
960    let huge = Timestamp::new(i64::MAX, tb);
961    let zero = Timestamp::new(0, tb);
962    assert_eq!(huge.duration_since(&zero), Some(Duration::MAX));
963  }
964
965  #[test]
966  fn frames_to_duration_integer_fps() {
967    let fps30 = Timebase::new(30, nz(1));
968    assert_eq!(fps30.frames_to_duration(15), Duration::from_millis(500));
969    assert_eq!(fps30.frames_to_duration(30), Duration::from_secs(1));
970    assert_eq!(fps30.frames_to_duration(0), Duration::ZERO);
971  }
972
973  #[test]
974  fn frames_to_duration_ntsc() {
975    // 30000 frames @ 30000/1001 fps = exactly 1001 seconds.
976    let ntsc = Timebase::new(30_000, nz(1001));
977    assert_eq!(ntsc.frames_to_duration(30_000), Duration::from_secs(1001));
978    // 15 frames at NTSC ≈ 500.5 ms.
979    assert_eq!(
980      ntsc.frames_to_duration(15),
981      Duration::from_nanos(500_500_000),
982    );
983  }
984
985  #[test]
986  fn time_range_basic() {
987    let tb = Timebase::default().with_den(nz(1000)).with_num(1);
988    let r = TimeRange::new(100, 500, tb);
989    assert_eq!(r.start_pts(), 100);
990    assert_eq!(r.end_pts(), 500);
991    assert_eq!(r.timebase(), tb);
992    assert_eq!(r.start(), Timestamp::new(100, tb));
993    assert_eq!(r.end(), Timestamp::new(500, tb));
994    assert!(!r.is_instant());
995    assert_eq!(r.duration(), Duration::from_millis(400));
996    // Interpolate: t=0 → start, t=1 → end, t=0.5 → midpoint.
997    assert_eq!(r.interpolate(0.0).pts(), 100);
998    assert_eq!(r.interpolate(1.0).pts(), 500);
999    assert_eq!(r.interpolate(0.5).pts(), 300);
1000    // Out-of-range t is clamped.
1001    assert_eq!(r.interpolate(-1.0).pts(), 100);
1002    assert_eq!(r.interpolate(2.0).pts(), 500);
1003
1004    let nr = r.with_timebase(Timebase::new(1, nz(2000)));
1005    assert_eq!(nr.timebase().den().get(), 2000);
1006    assert_eq!(nr.timebase().num(), 1);
1007  }
1008
1009  #[test]
1010  fn time_range_instant() {
1011    let tb = Timebase::new(1, nz(1000));
1012    let ts = Timestamp::new(123, tb);
1013    let r = TimeRange::instant(ts);
1014    assert!(r.is_instant());
1015    assert_eq!(r.start_pts(), 123);
1016    assert_eq!(r.end_pts(), 123);
1017    assert_eq!(r.duration(), Duration::ZERO);
1018  }
1019
1020  // -------------------------------------------------------------------------
1021  // Coverage top-ups — every public accessor, builder, and setter on the
1022  // three types gets exercised at least once. Grouped per-type.
1023  // -------------------------------------------------------------------------
1024
1025  #[test]
1026  fn timebase_accessors_and_builders() {
1027    let tb = Timebase::new(30_000, nz(1001));
1028    assert_eq!(tb.num(), 30_000);
1029    assert_eq!(tb.den(), nz(1001));
1030
1031    // with_num / with_den — consuming form.
1032    let tb2 = tb.with_num(48_000).with_den(nz(1));
1033    assert_eq!(tb2.num(), 48_000);
1034    assert_eq!(tb2.den(), nz(1));
1035
1036    // set_num / set_den — in-place form. Returns &mut Self for chaining.
1037    let mut tb3 = Timebase::new(1, nz(1000));
1038    tb3.set_num(25).set_den(nz(2));
1039    assert_eq!(tb3.num(), 25);
1040    assert_eq!(tb3.den(), nz(2));
1041  }
1042
1043  #[test]
1044  fn duration_to_pts_happy_path_and_edge_cases() {
1045    // Integer conversion: 1.5 s @ 1/1000 → 1500 units.
1046    let ms = Timebase::new(1, nz(1000));
1047    assert_eq!(ms.duration_to_pts(Duration::from_millis(1500)), 1500);
1048    assert_eq!(ms.duration_to_pts(Duration::ZERO), 0);
1049
1050    // Non-ms timebase: 2 s @ 1/90_000 → 180_000 units.
1051    let mpegts = Timebase::new(1, nz(90_000));
1052    assert_eq!(mpegts.duration_to_pts(Duration::from_secs(2)), 180_000,);
1053
1054    // Degenerate: zero numerator → returns 0.
1055    let degenerate = Timebase::new(0, nz(1));
1056    assert_eq!(degenerate.duration_to_pts(Duration::from_secs(1)), 0,);
1057
1058    // Saturation at i64::MAX when the math would overflow.
1059    // A frame rate of 1 fps (num=1, den=1 s) with an enormous duration:
1060    // pts = ns * 1 / (1 * 1e9). Use a u64::MAX-ish nanos value via the
1061    // max Duration; Rust's Duration max is ~(2^64 - 1) seconds.
1062    let fps1 = Timebase::new(1, nz(1));
1063    let huge = Duration::new(u64::MAX, 0);
1064    assert_eq!(fps1.duration_to_pts(huge), i64::MAX);
1065  }
1066
1067  #[test]
1068  fn timestamp_accessors_and_builders() {
1069    let tb = Timebase::new(1, nz(1000));
1070    let mut ts = Timestamp::new(42, tb);
1071    assert_eq!(ts.pts(), 42);
1072    assert_eq!(ts.timebase(), tb);
1073
1074    // with_pts — consuming form.
1075    let ts2 = ts.with_pts(777);
1076    assert_eq!(ts2.pts(), 777);
1077
1078    // set_pts — in-place form, chainable.
1079    ts.set_pts(-5).set_pts(-6);
1080    assert_eq!(ts.pts(), -6);
1081  }
1082
1083  #[test]
1084  fn cmp_semantic_exercises_all_branches() {
1085    let tb_a = Timebase::new(1, nz(1000)); // ms
1086    let tb_b = Timebase::new(1, nz(90_000)); // MPEG-TS
1087
1088    // Same-timebase fast path: Less / Greater / Equal.
1089    let a = Timestamp::new(100, tb_a);
1090    let b = Timestamp::new(200, tb_a);
1091    assert_eq!(a.cmp_semantic(&b), Ordering::Less);
1092    assert_eq!(b.cmp_semantic(&a), Ordering::Greater);
1093    assert_eq!(a.cmp_semantic(&a), Ordering::Equal);
1094
1095    // Cross-timebase slow path: Less / Greater / Equal.
1096    let one_second_ms = Timestamp::new(1000, tb_a);
1097    let one_second_mpg = Timestamp::new(90_000, tb_b);
1098    let half_second_ms = Timestamp::new(500, tb_a);
1099    let two_seconds_mpg = Timestamp::new(180_000, tb_b);
1100    assert_eq!(half_second_ms.cmp_semantic(&one_second_mpg), Ordering::Less,);
1101    assert_eq!(
1102      two_seconds_mpg.cmp_semantic(&one_second_ms),
1103      Ordering::Greater,
1104    );
1105    assert_eq!(one_second_ms.cmp_semantic(&one_second_mpg), Ordering::Equal,);
1106  }
1107
1108  #[test]
1109  fn saturating_sub_duration_saturates() {
1110    let tb = Timebase::new(1, nz(1000));
1111    // Subtracting a finite duration from a small pts shouldn't panic —
1112    // it saturates at i64::MIN for pathological inputs.
1113    let near_floor = Timestamp::new(i64::MIN + 10, tb);
1114    let shifted = near_floor.saturating_sub_duration(Duration::from_secs(1));
1115    assert_eq!(shifted.pts(), i64::MIN);
1116
1117    // Normal case: 1500 ms - 500 ms → 1000 ms.
1118    let ts = Timestamp::new(1500, tb);
1119    let shifted = ts.saturating_sub_duration(Duration::from_millis(500));
1120    assert_eq!(shifted.pts(), 1000);
1121  }
1122
1123  #[test]
1124  fn time_range_builders_and_setters() {
1125    let tb = Timebase::new(1, nz(1000));
1126    let r = TimeRange::new(0, 0, tb);
1127
1128    // with_start / with_end — consuming form.
1129    let r2 = r.with_start(100).with_end(500);
1130    assert_eq!(r2.start_pts(), 100);
1131    assert_eq!(r2.end_pts(), 500);
1132
1133    // set_start / set_end — in-place form, chainable.
1134    let mut r3 = TimeRange::new(0, 0, tb);
1135    r3.set_start(10).set_end(20);
1136    assert_eq!(r3.start_pts(), 10);
1137    assert_eq!(r3.end_pts(), 20);
1138  }
1139
1140  #[test]
1141  fn time_range_total_pts() {
1142    let tb = Timebase::new(1, nz(1000));
1143    assert_eq!(TimeRange::new(100, 500, tb).total_pts(), 400);
1144    assert_eq!(TimeRange::new(0, 0, tb).total_pts(), 0);
1145    // Saturating: i64::MIN..i64::MAX would overflow a signed subtract.
1146    assert_eq!(TimeRange::new(i64::MIN, i64::MAX, tb).total_pts(), i64::MAX);
1147  }
1148
1149  #[test]
1150  fn time_range_rescale_to() {
1151    let ms = Timebase::new(1, nz(1000));
1152    let mpeg = Timebase::new(1, nz(90_000));
1153    let r = TimeRange::new(1000, 2000, ms);
1154    let r2 = r.rescale_to(mpeg);
1155    assert_eq!(r2.start_pts(), 90_000);
1156    assert_eq!(r2.end_pts(), 180_000);
1157    assert_eq!(r2.timebase(), mpeg);
1158    // Same span in Duration terms.
1159    assert_eq!(r.duration(), r2.duration());
1160    // Instant range stays instant.
1161    let inst = TimeRange::instant(Timestamp::new(500, ms));
1162    assert!(inst.rescale_to(mpeg).is_instant());
1163  }
1164
1165  #[test]
1166  fn time_range_try_new() {
1167    let tb = Timebase::new(1, nz(1000));
1168    // Forward range: Some.
1169    let r = TimeRange::try_new(100, 500, tb).unwrap();
1170    assert_eq!(r.start_pts(), 100);
1171    assert_eq!(r.end_pts(), 500);
1172    // Degenerate instant: allowed.
1173    assert!(TimeRange::try_new(42, 42, tb).is_some());
1174    // Inverted range: None instead of panic.
1175    assert!(TimeRange::try_new(500, 100, tb).is_none());
1176  }
1177
1178  #[test]
1179  #[should_panic]
1180  fn time_range_new_panics_on_negative_duration() {
1181    let tb = Timebase::new(1, nz(1000));
1182    TimeRange::new(500, 100, tb);
1183  }
1184}
1185
1186#[cfg(all(test, feature = "quickcheck"))]
1187mod quickcheck_arbitrary_tests {
1188  use super::*;
1189  use quickcheck::{Arbitrary, Gen};
1190
1191  const ITERATIONS: usize = 1000;
1192  const SIZE: usize = 512;
1193
1194  #[test]
1195  fn timebase_denominator_is_nonzero() {
1196    let mut g = Gen::new(SIZE);
1197    for _ in 0..ITERATIONS {
1198      let tb = Timebase::arbitrary(&mut g);
1199      assert!(tb.den().get() != 0);
1200    }
1201  }
1202
1203  #[test]
1204  fn timestamp_pts_is_non_negative() {
1205    let mut g = Gen::new(SIZE);
1206    for _ in 0..ITERATIONS {
1207      let ts = Timestamp::arbitrary(&mut g);
1208      assert!(ts.pts() >= 0, "pts was {}", ts.pts());
1209      assert!(ts.timebase().den().get() != 0);
1210    }
1211  }
1212
1213  #[test]
1214  fn timerange_is_well_formed() {
1215    let mut g = Gen::new(SIZE);
1216    for _ in 0..ITERATIONS {
1217      let r = TimeRange::arbitrary(&mut g);
1218      assert!(r.start_pts() >= 0, "start was {}", r.start_pts());
1219      assert!(r.end_pts() >= 0, "end was {}", r.end_pts());
1220      assert!(
1221        r.start_pts() <= r.end_pts(),
1222        "start {} > end {}",
1223        r.start_pts(),
1224        r.end_pts()
1225      );
1226      assert!(r.timebase().den().get() != 0);
1227    }
1228  }
1229}
1230
1231#[cfg(all(test, feature = "arbitrary"))]
1232mod arbitrary_impl_tests {
1233  use super::*;
1234  use arbitrary::{Arbitrary, Unstructured};
1235
1236  fn pseudo_random_bytes(seed: u64) -> [u8; 4096] {
1237    // splitmix64: simple, reproducible, good enough for fuzz-input fodder.
1238    let mut out = [0u8; 4096];
1239    let mut s = seed.wrapping_add(0x9E3779B97F4A7C15);
1240    for chunk in out.chunks_mut(8) {
1241      s = s.wrapping_mul(0xBF58476D1CE4E5B9).wrapping_add(1);
1242      let mut z = s;
1243      z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
1244      z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
1245      z ^= z >> 31;
1246      chunk.copy_from_slice(&z.to_le_bytes()[..chunk.len()]);
1247    }
1248    out
1249  }
1250
1251  #[test]
1252  fn timebase_denominator_is_nonzero() {
1253    for seed in 0..200 {
1254      let data = pseudo_random_bytes(seed);
1255      let mut u = Unstructured::new(&data);
1256      let tb = Timebase::arbitrary(&mut u).expect("enough bytes to build a Timebase");
1257      assert!(tb.den().get() != 0);
1258    }
1259  }
1260
1261  #[test]
1262  fn timestamp_pts_is_non_negative() {
1263    for seed in 0..200 {
1264      let data = pseudo_random_bytes(seed);
1265      let mut u = Unstructured::new(&data);
1266      let ts = Timestamp::arbitrary(&mut u).expect("enough bytes to build a Timestamp");
1267      assert!(ts.pts() >= 0, "pts was {}", ts.pts());
1268      assert!(ts.timebase().den().get() != 0);
1269    }
1270  }
1271
1272  #[test]
1273  fn timerange_is_well_formed() {
1274    for seed in 0..200 {
1275      let data = pseudo_random_bytes(seed);
1276      let mut u = Unstructured::new(&data);
1277      let r = TimeRange::arbitrary(&mut u).expect("enough bytes to build a TimeRange");
1278      assert!(r.start_pts() >= 0);
1279      assert!(r.end_pts() >= 0);
1280      assert!(r.start_pts() <= r.end_pts());
1281      assert!(r.timebase().den().get() != 0);
1282    }
1283  }
1284
1285  #[test]
1286  fn arbitrary_take_rest_produces_valid_values() {
1287    // `arbitrary_take_rest` is the entry point fuzzers use at the tail of
1288    // a corpus entry. Make sure our impl plays well with it.
1289    let data = pseudo_random_bytes(42);
1290    let u = Unstructured::new(&data);
1291    let r = TimeRange::arbitrary_take_rest(u).expect("should consume the buffer");
1292    assert!(r.start_pts() >= 0);
1293    assert!(r.end_pts() >= 0);
1294    assert!(r.start_pts() <= r.end_pts());
1295    assert!(r.timebase().den().get() != 0);
1296  }
1297}
1298
1299#[cfg(feature = "buffa")]
1300mod buffa;
1301
1302/// Ancillary module the buffa code generator looks for when an extern-mapped
1303/// type is used as a message field with view generation enabled. The mediatime
1304/// types contain only scalars, so each view is the owned type itself.
1305#[cfg(feature = "buffa")]
1306#[doc(hidden)]
1307pub mod __buffa {
1308  pub mod view {
1309    // `'a` is required by buffa's extern-view convention; unused here
1310    // because these mediatime types are `Copy`/owned (nothing borrowed).
1311    pub type TimebaseView<'a> = crate::Timebase;
1312    pub type TimeRangeView<'a> = crate::TimeRange;
1313    pub type TimestampView<'a> = crate::Timestamp;
1314  }
1315}