Skip to main content

moire_trace_types/
lib.rs

1use facet::Facet;
2use std::error::Error;
3use std::fmt;
4use std::sync::OnceLock;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum InvariantError {
10    ZeroId(&'static str),
11    IdOutOfRange {
12        field: &'static str,
13        max: u64,
14        got: u64,
15    },
16    EmptyField(&'static str),
17    EmptyBacktraceFrames,
18}
19
20impl fmt::Display for InvariantError {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::ZeroId(field) => write!(f, "{field} must be non-zero"),
24            Self::IdOutOfRange { field, max, got } => {
25                write!(f, "{field} must be <= {max}, got {got}")
26            }
27            Self::EmptyField(field) => write!(f, "{field} must be non-empty"),
28            Self::EmptyBacktraceFrames => write!(f, "backtrace frames must be non-empty"),
29        }
30    }
31}
32
33impl Error for InvariantError {}
34
35pub const ID_PREFIX_BITS: u32 = 16;
36pub const ID_COUNTER_BITS: u32 = 37;
37pub const ID_COUNTER_MAX_U64: u64 = (1u64 << ID_COUNTER_BITS) - 1;
38pub const JS_SAFE_INT_MAX_U64: u64 = (1u64 << 53) - 1;
39
40fn process_prefix_u16() -> u16 {
41    static PROCESS_PREFIX: OnceLock<u16> = OnceLock::new();
42    *PROCESS_PREFIX.get_or_init(|| {
43        let pid = std::process::id() as u64;
44        let seed = SystemTime::now()
45            .duration_since(UNIX_EPOCH)
46            .map(|duration| duration.as_nanos() as u64)
47            .unwrap_or(0);
48        ((seed ^ pid) & 0xFFFF) as u16
49    })
50}
51
52macro_rules! define_u64_id {
53    (
54        $(#[$meta:meta])*
55        $name:ident,
56        field = $field:literal
57        , max = $max:expr
58    ) => {
59        #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60        #[facet(transparent)]
61        $(#[$meta])*
62        pub struct $name(u64);
63
64        impl $name {
65            fn from_raw(value: u64) -> Result<Self, InvariantError> {
66                if value == 0 {
67                    return Err(InvariantError::ZeroId($field));
68                }
69                if value > $max {
70                    return Err(InvariantError::IdOutOfRange {
71                        field: $field,
72                        max: $max,
73                        got: value,
74                    });
75                }
76                Ok(Self(value))
77            }
78
79            #[cfg(test)]
80            #[allow(dead_code)]
81            fn from_prefixed_counter(prefix: u16, counter: u64) -> Result<Self, InvariantError> {
82                if counter > ID_COUNTER_MAX_U64 {
83                    return Err(InvariantError::IdOutOfRange {
84                        field: $field,
85                        max: ID_COUNTER_MAX_U64,
86                        got: counter,
87                    });
88                }
89                let raw = ((u64::from(prefix)) << ID_COUNTER_BITS) | counter;
90                Self::from_raw(raw)
91            }
92
93            pub fn next() -> Result<Self, InvariantError> {
94                static NEXT_COUNTER: AtomicU64 = AtomicU64::new(1);
95                let counter = NEXT_COUNTER.fetch_add(1, Ordering::Relaxed);
96                if counter > ID_COUNTER_MAX_U64 {
97                    return Err(InvariantError::IdOutOfRange {
98                        field: $field,
99                        max: ID_COUNTER_MAX_U64,
100                        got: counter,
101                    });
102                }
103                let prefix = process_prefix_u16();
104                let raw = ((u64::from(prefix)) << ID_COUNTER_BITS) | counter;
105                Self::from_raw(raw)
106            }
107        }
108
109        impl $name {
110            pub fn as_u64(self) -> u64 {
111                self.0
112            }
113        }
114
115        impl core::fmt::Display for $name {
116            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117                let kind = stringify!($name)
118                    .trim_end_matches("Id")
119                    .to_ascii_uppercase();
120                write!(f, "{kind}#{:x}", self.0)
121            }
122        }
123
124        #[cfg(feature = "rusqlite")]
125        impl rusqlite::types::ToSql for $name {
126            fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
127                let value = i64::try_from(self.0).map_err(|error| {
128                    rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
129                        std::io::ErrorKind::InvalidData,
130                        format!("{} does not fit i64: {error}", $field),
131                    )))
132                })?;
133                Ok(value.into())
134            }
135        }
136
137        #[cfg(feature = "rusqlite")]
138        impl rusqlite::types::FromSql for $name {
139            fn column_result(
140                value: rusqlite::types::ValueRef<'_>,
141            ) -> rusqlite::types::FromSqlResult<Self> {
142                let value = i64::column_result(value)?;
143                let value = u64::try_from(value).map_err(|error| {
144                    rusqlite::types::FromSqlError::Other(Box::new(std::io::Error::new(
145                        std::io::ErrorKind::InvalidData,
146                        format!("{field} must be non-negative i64: {error}", field = $field),
147                    )))
148                })?;
149                $name::from_raw(value).map_err(|error| {
150                    rusqlite::types::FromSqlError::Other(Box::new(std::io::Error::new(
151                        std::io::ErrorKind::InvalidData,
152                        error.to_string(),
153                    )))
154                })
155            }
156        }
157    };
158}
159
160define_u64_id!(ModuleId, field = "module_id", max = JS_SAFE_INT_MAX_U64);
161define_u64_id!(
162    // r[impl model.backtrace]
163    BacktraceId,
164    field = "backtrace_id",
165    max = JS_SAFE_INT_MAX_U64
166);
167define_u64_id!(FrameId, field = "frame_id", max = JS_SAFE_INT_MAX_U64);
168
169#[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
170#[facet(transparent)]
171pub struct RuntimeBase(u64);
172
173impl RuntimeBase {
174    pub fn new(value: u64) -> Result<Self, InvariantError> {
175        if value == 0 {
176            return Err(InvariantError::ZeroId("runtime_base"));
177        }
178        if value > JS_SAFE_INT_MAX_U64 {
179            return Err(InvariantError::IdOutOfRange {
180                field: "runtime_base",
181                max: JS_SAFE_INT_MAX_U64,
182                got: value,
183            });
184        }
185        Ok(Self(value))
186    }
187
188    pub fn get(self) -> u64 {
189        self.0
190    }
191
192    pub fn checked_add_rel_pc(self, rel_pc: RelPc) -> Option<u64> {
193        self.0.checked_add(rel_pc.0)
194    }
195}
196
197#[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
198#[facet(transparent)]
199pub struct RelPc(u64);
200
201impl RelPc {
202    pub fn new(value: u64) -> Result<Self, InvariantError> {
203        if value > JS_SAFE_INT_MAX_U64 {
204            return Err(InvariantError::IdOutOfRange {
205                field: "rel_pc",
206                max: JS_SAFE_INT_MAX_U64,
207                got: value,
208            });
209        }
210        Ok(Self(value))
211    }
212
213    pub fn get(self) -> u64 {
214        self.0
215    }
216}
217
218#[cfg(feature = "rusqlite")]
219impl rusqlite::types::ToSql for RuntimeBase {
220    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
221        let value = i64::try_from(self.0).map_err(|error| {
222            rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
223                std::io::ErrorKind::InvalidData,
224                format!("runtime_base does not fit i64: {error}"),
225            )))
226        })?;
227        Ok(value.into())
228    }
229}
230
231#[cfg(feature = "rusqlite")]
232impl rusqlite::types::FromSql for RuntimeBase {
233    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
234        let value = i64::column_result(value)?;
235        let value = u64::try_from(value).map_err(|error| {
236            rusqlite::types::FromSqlError::Other(Box::new(std::io::Error::new(
237                std::io::ErrorKind::InvalidData,
238                format!("runtime_base must be non-negative i64: {error}"),
239            )))
240        })?;
241        RuntimeBase::new(value).map_err(|error| {
242            rusqlite::types::FromSqlError::Other(Box::new(std::io::Error::new(
243                std::io::ErrorKind::InvalidData,
244                error.to_string(),
245            )))
246        })
247    }
248}
249
250#[cfg(feature = "rusqlite")]
251impl rusqlite::types::ToSql for RelPc {
252    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
253        let value = i64::try_from(self.0).map_err(|error| {
254            rusqlite::Error::ToSqlConversionFailure(Box::new(std::io::Error::new(
255                std::io::ErrorKind::InvalidData,
256                format!("rel_pc does not fit i64: {error}"),
257            )))
258        })?;
259        Ok(value.into())
260    }
261}
262
263#[cfg(feature = "rusqlite")]
264impl rusqlite::types::FromSql for RelPc {
265    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
266        let value = i64::column_result(value)?;
267        let value = u64::try_from(value).map_err(|error| {
268            rusqlite::types::FromSqlError::Other(Box::new(std::io::Error::new(
269                std::io::ErrorKind::InvalidData,
270                format!("rel_pc must be non-negative i64: {error}"),
271            )))
272        })?;
273        RelPc::new(value).map_err(|error| {
274            rusqlite::types::FromSqlError::Other(Box::new(std::io::Error::new(
275                std::io::ErrorKind::InvalidData,
276                error.to_string(),
277            )))
278        })
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn backtrace_id_next_is_prefixed_and_js_safe() {
288        let first = BacktraceId::next().expect("first id must be valid");
289        let second = BacktraceId::next().expect("second id must be valid");
290
291        assert_ne!(first, second, "ids must be unique");
292        let first_fmt = format!("{first}");
293        let second_fmt = format!("{second}");
294        assert!(first_fmt.starts_with("BACKTRACE#"));
295        assert!(second_fmt.starts_with("BACKTRACE#"));
296    }
297}
298
299#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
300pub struct ModulePath(String);
301
302impl ModulePath {
303    pub fn new(value: impl Into<String>) -> Result<Self, InvariantError> {
304        let value = value.into();
305        if value.is_empty() {
306            return Err(InvariantError::EmptyField("module_path"));
307        }
308        Ok(Self(value))
309    }
310
311    pub fn as_str(&self) -> &str {
312        &self.0
313    }
314}
315
316#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
317pub struct BuildId(String);
318
319impl BuildId {
320    pub fn new(value: impl Into<String>) -> Result<Self, InvariantError> {
321        let value = value.into();
322        if value.is_empty() {
323            return Err(InvariantError::EmptyField("build_id"));
324        }
325        Ok(Self(value))
326    }
327
328    pub fn as_str(&self) -> &str {
329        &self.0
330    }
331}
332
333#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
334pub struct DebugId(String);
335
336impl DebugId {
337    pub fn new(value: impl Into<String>) -> Result<Self, InvariantError> {
338        let value = value.into();
339        if value.is_empty() {
340            return Err(InvariantError::EmptyField("debug_id"));
341        }
342        Ok(Self(value))
343    }
344
345    pub fn as_str(&self) -> &str {
346        &self.0
347    }
348}
349
350#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
351pub struct ModuleArch(String);
352
353impl ModuleArch {
354    pub fn new(value: impl Into<String>) -> Result<Self, InvariantError> {
355        let value = value.into();
356        if value.is_empty() {
357            return Err(InvariantError::EmptyField("arch"));
358        }
359        Ok(Self(value))
360    }
361
362    pub fn as_str(&self) -> &str {
363        &self.0
364    }
365}
366
367#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
368#[repr(u8)]
369#[facet(rename_all = "snake_case")]
370pub enum ModuleIdentity {
371    BuildId(BuildId),
372    DebugId(DebugId),
373}
374
375#[derive(Facet, Debug, Clone, PartialEq, Eq, Hash)]
376pub struct FrameKey {
377    pub module_id: ModuleId,
378    pub rel_pc: RelPc,
379}
380
381#[derive(Facet, Debug, Clone, PartialEq, Eq)]
382pub struct BacktraceRecord {
383    pub id: BacktraceId,
384    pub frames: Vec<FrameKey>,
385}
386
387impl BacktraceRecord {
388    pub fn new(id: BacktraceId, frames: Vec<FrameKey>) -> Result<Self, InvariantError> {
389        if frames.is_empty() {
390            return Err(InvariantError::EmptyBacktraceFrames);
391        }
392        Ok(Self { id, frames })
393    }
394}
395
396#[derive(Facet, Debug, Clone, PartialEq, Eq)]
397pub struct ModuleRecord {
398    pub id: ModuleId,
399    pub path: ModulePath,
400    pub runtime_base: RuntimeBase,
401    pub identity: ModuleIdentity,
402    pub arch: ModuleArch,
403}