stackforge_core/anonymize/
timestamp.rs1use std::time::Duration;
8
9use rand::Rng;
10use rand::SeedableRng;
11use rand::rngs::StdRng;
12
13#[derive(Debug)]
15pub struct TimestampAnonymizer {
16 epoch_offset: Duration,
18 jitter_ms: u32,
20 rng: StdRng,
22}
23
24impl TimestampAnonymizer {
25 pub fn epoch_shift_only(rng: &mut StdRng) -> Self {
27 let offset_secs: u64 = rng.random_range(30 * 86400..365 * 86400);
29 Self {
30 epoch_offset: Duration::from_secs(offset_secs),
31 jitter_ms: 0,
32 rng: StdRng::from_os_rng(),
33 }
34 }
35
36 pub fn with_jitter(jitter_ms: u32, rng: &mut StdRng) -> Self {
38 let offset_secs: u64 = rng.random_range(30 * 86400..365 * 86400);
39 Self {
40 epoch_offset: Duration::from_secs(offset_secs),
41 jitter_ms,
42 rng: StdRng::from_os_rng(),
43 }
44 }
45
46 pub fn anonymize(&mut self, ts: Duration) -> Duration {
50 let shifted = ts + self.epoch_offset;
51 if self.jitter_ms == 0 {
52 return shifted;
53 }
54 let jitter = Duration::from_millis(
55 self.rng.random_range(0..=u64::from(self.jitter_ms)),
56 );
57 shifted + jitter
58 }
59
60 #[must_use]
62 pub fn epoch_offset(&self) -> Duration {
63 self.epoch_offset
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use rand::SeedableRng;
71
72 #[test]
73 fn test_epoch_shift_preserves_ordering() {
74 let mut rng = StdRng::seed_from_u64(42);
75 let mut anon = TimestampAnonymizer::epoch_shift_only(&mut rng);
76
77 let t1 = Duration::from_secs(100);
78 let t2 = Duration::from_secs(200);
79 let t3 = Duration::from_secs(300);
80
81 let a1 = anon.anonymize(t1);
82 let a2 = anon.anonymize(t2);
83 let a3 = anon.anonymize(t3);
84
85 assert!(a1 < a2);
86 assert!(a2 < a3);
87 }
88
89 #[test]
90 fn test_epoch_shift_preserves_duration() {
91 let mut rng = StdRng::seed_from_u64(42);
92 let mut anon = TimestampAnonymizer::epoch_shift_only(&mut rng);
93
94 let t1 = Duration::from_secs(100);
95 let t2 = Duration::from_secs(200);
96
97 let a1 = anon.anonymize(t1);
98 let a2 = anon.anonymize(t2);
99
100 assert_eq!(a2 - a1, t2 - t1);
102 }
103
104 #[test]
105 fn test_offset_is_positive() {
106 let mut rng = StdRng::seed_from_u64(42);
107 let anon = TimestampAnonymizer::epoch_shift_only(&mut rng);
108 assert!(anon.epoch_offset() >= Duration::from_secs(30 * 86400));
110 }
111
112 #[test]
113 fn test_jitter_adds_noise() {
114 let mut rng = StdRng::seed_from_u64(42);
115 let mut anon = TimestampAnonymizer::with_jitter(10, &mut rng);
116 let ts = Duration::from_secs(100);
117
118 let results: Vec<Duration> = (0..100).map(|_| anon.anonymize(ts)).collect();
120
121 let first = results[0];
123 assert!(results.iter().any(|&r| r != first));
124 }
125}