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 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}