| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
#ifndef SEKHMET_ULID_H |
|
2
|
|
|
|
|
|
|
#define SEKHMET_ULID_H |
|
3
|
|
|
|
|
|
|
|
|
4
|
|
|
|
|
|
|
/* |
|
5
|
|
|
|
|
|
|
* sekhmet_ulid.h - ULID generation, encoding, decoding, conversion |
|
6
|
|
|
|
|
|
|
* |
|
7
|
|
|
|
|
|
|
* Binary ULID layout (16 bytes, big-endian): |
|
8
|
|
|
|
|
|
|
* Bytes 0-5: 48-bit Unix epoch milliseconds |
|
9
|
|
|
|
|
|
|
* Bytes 6-15: 80-bit cryptographic randomness |
|
10
|
|
|
|
|
|
|
* |
|
11
|
|
|
|
|
|
|
* Crockford base32 encoding (26 chars): |
|
12
|
|
|
|
|
|
|
* Chars 0-9: timestamp (48 bits, first char uses only 3 bits) |
|
13
|
|
|
|
|
|
|
* Chars 10-25: randomness (80 bits) |
|
14
|
|
|
|
|
|
|
* |
|
15
|
|
|
|
|
|
|
* Depends on horus_core.h for: |
|
16
|
|
|
|
|
|
|
* horus_unix_epoch_ms(), horus_random_bytes(), |
|
17
|
|
|
|
|
|
|
* horus_crockford_encode(), horus_crockford_decode() |
|
18
|
|
|
|
|
|
|
*/ |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
#include |
|
21
|
|
|
|
|
|
|
#include |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
/* ── Monotonic state ────────────────────────────────────────────── */ |
|
24
|
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
typedef struct { |
|
26
|
|
|
|
|
|
|
uint64_t last_ms; |
|
27
|
|
|
|
|
|
|
unsigned char last_rand[10]; /* 80 bits of randomness */ |
|
28
|
|
|
|
|
|
|
} sekhmet_monotonic_state_t; |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
/* ── Timestamp helpers ──────────────────────────────────────────── */ |
|
31
|
|
|
|
|
|
|
|
|
32
|
2914
|
|
|
|
|
|
static inline void sekhmet_pack_timestamp(unsigned char *out, uint64_t ms) { |
|
33
|
2914
|
|
|
|
|
|
out[0] = (unsigned char)((ms >> 40) & 0xFF); |
|
34
|
2914
|
|
|
|
|
|
out[1] = (unsigned char)((ms >> 32) & 0xFF); |
|
35
|
2914
|
|
|
|
|
|
out[2] = (unsigned char)((ms >> 24) & 0xFF); |
|
36
|
2914
|
|
|
|
|
|
out[3] = (unsigned char)((ms >> 16) & 0xFF); |
|
37
|
2914
|
|
|
|
|
|
out[4] = (unsigned char)((ms >> 8) & 0xFF); |
|
38
|
2914
|
|
|
|
|
|
out[5] = (unsigned char)(ms & 0xFF); |
|
39
|
2914
|
|
|
|
|
|
} |
|
40
|
|
|
|
|
|
|
|
|
41
|
18
|
|
|
|
|
|
static inline uint64_t sekhmet_unpack_timestamp(const unsigned char *ulid) { |
|
42
|
18
|
|
|
|
|
|
return ((uint64_t)ulid[0] << 40) |
|
43
|
18
|
|
|
|
|
|
| ((uint64_t)ulid[1] << 32) |
|
44
|
18
|
|
|
|
|
|
| ((uint64_t)ulid[2] << 24) |
|
45
|
18
|
|
|
|
|
|
| ((uint64_t)ulid[3] << 16) |
|
46
|
18
|
|
|
|
|
|
| ((uint64_t)ulid[4] << 8) |
|
47
|
18
|
|
|
|
|
|
| (uint64_t)ulid[5]; |
|
48
|
|
|
|
|
|
|
} |
|
49
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
/* ── Generate ULID (random mode) ───────────────────────────────── */ |
|
51
|
|
|
|
|
|
|
|
|
52
|
1132
|
|
|
|
|
|
static inline void sekhmet_ulid_generate(unsigned char *out) { |
|
53
|
1132
|
|
|
|
|
|
uint64_t ms = horus_unix_epoch_ms(); |
|
54
|
1132
|
|
|
|
|
|
sekhmet_pack_timestamp(out, ms); |
|
55
|
1132
|
|
|
|
|
|
horus_random_bytes(out + 6, 10); |
|
56
|
1132
|
|
|
|
|
|
} |
|
57
|
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
/* ── Generate ULID (monotonic mode) ────────────────────────────── */ |
|
59
|
|
|
|
|
|
|
|
|
60
|
1782
|
|
|
|
|
|
static inline void sekhmet_ulid_monotonic(unsigned char *out, |
|
61
|
|
|
|
|
|
|
sekhmet_monotonic_state_t *state) { |
|
62
|
1782
|
|
|
|
|
|
uint64_t ms = horus_unix_epoch_ms(); |
|
63
|
1782
|
|
|
|
|
|
sekhmet_pack_timestamp(out, ms); |
|
64
|
|
|
|
|
|
|
|
|
65
|
1782
|
100
|
|
|
|
|
if (ms == state->last_ms) { |
|
66
|
|
|
|
|
|
|
/* Same millisecond: increment 80-bit random (big-endian) */ |
|
67
|
1770
|
|
|
|
|
|
int i, carry = 1; |
|
68
|
3548
|
50
|
|
|
|
|
for (i = 9; i >= 0 && carry; i--) { |
|
|
|
100
|
|
|
|
|
|
|
69
|
1778
|
|
|
|
|
|
int val = (int)state->last_rand[i] + carry; |
|
70
|
1778
|
|
|
|
|
|
state->last_rand[i] = (unsigned char)(val & 0xFF); |
|
71
|
1778
|
|
|
|
|
|
carry = val >> 8; |
|
72
|
|
|
|
|
|
|
} |
|
73
|
1770
|
50
|
|
|
|
|
if (carry) { |
|
74
|
|
|
|
|
|
|
/* Overflow of 2^80 — reseed (practically impossible) */ |
|
75
|
0
|
|
|
|
|
|
horus_random_bytes(state->last_rand, 10); |
|
76
|
|
|
|
|
|
|
} |
|
77
|
1770
|
|
|
|
|
|
memcpy(out + 6, state->last_rand, 10); |
|
78
|
|
|
|
|
|
|
} else { |
|
79
|
|
|
|
|
|
|
/* New millisecond: fresh random */ |
|
80
|
12
|
|
|
|
|
|
horus_random_bytes(out + 6, 10); |
|
81
|
12
|
|
|
|
|
|
state->last_ms = ms; |
|
82
|
12
|
|
|
|
|
|
memcpy(state->last_rand, out + 6, 10); |
|
83
|
|
|
|
|
|
|
} |
|
84
|
1782
|
|
|
|
|
|
} |
|
85
|
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
/* ── Encode binary ULID to 26-char Crockford string ────────────── */ |
|
87
|
|
|
|
|
|
|
|
|
88
|
2858
|
|
|
|
|
|
static inline void sekhmet_ulid_encode(char *dst, const unsigned char *ulid) { |
|
89
|
2858
|
|
|
|
|
|
horus_crockford_encode(dst, ulid); |
|
90
|
2858
|
|
|
|
|
|
} |
|
91
|
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
/* ── Decode 26-char Crockford string to binary ULID ────────────── */ |
|
93
|
|
|
|
|
|
|
|
|
94
|
107
|
|
|
|
|
|
static inline int sekhmet_ulid_decode(unsigned char *dst, |
|
95
|
|
|
|
|
|
|
const char *src, int len) { |
|
96
|
107
|
50
|
|
|
|
|
if (len != 26) return 0; |
|
97
|
107
|
|
|
|
|
|
return horus_crockford_decode(dst, src, len) == 16; |
|
98
|
|
|
|
|
|
|
} |
|
99
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
/* ── Extract timestamp as epoch seconds (double) ───────────────── */ |
|
101
|
|
|
|
|
|
|
|
|
102
|
4
|
|
|
|
|
|
static inline double sekhmet_ulid_time(const unsigned char *ulid) { |
|
103
|
4
|
|
|
|
|
|
uint64_t ms = sekhmet_unpack_timestamp(ulid); |
|
104
|
4
|
|
|
|
|
|
return (double)ms / 1000.0; |
|
105
|
|
|
|
|
|
|
} |
|
106
|
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
/* ── Extract timestamp as epoch milliseconds ───────────────────── */ |
|
108
|
|
|
|
|
|
|
|
|
109
|
14
|
|
|
|
|
|
static inline uint64_t sekhmet_ulid_time_ms(const unsigned char *ulid) { |
|
110
|
14
|
|
|
|
|
|
return sekhmet_unpack_timestamp(ulid); |
|
111
|
|
|
|
|
|
|
} |
|
112
|
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
/* ── ULID → UUID string conversion ─────────────────────────────── * |
|
114
|
|
|
|
|
|
|
* |
|
115
|
|
|
|
|
|
|
* ULID: [48-bit ts][80-bit random] |
|
116
|
|
|
|
|
|
|
* UUIDv7: [48-bit ts][4-bit ver=7][12-bit rand_a][2-bit var=10][62-bit rand_b] |
|
117
|
|
|
|
|
|
|
* |
|
118
|
|
|
|
|
|
|
* We stamp version=7 and variant=RFC4122 into the binary, then format |
|
119
|
|
|
|
|
|
|
* as a standard UUID hyphenated string. |
|
120
|
|
|
|
|
|
|
*/ |
|
121
|
|
|
|
|
|
|
|
|
122
|
8
|
|
|
|
|
|
static inline void sekhmet_format_uuid(char *dst, const unsigned char *uuid) { |
|
123
|
|
|
|
|
|
|
static const char hex[] = "0123456789abcdef"; |
|
124
|
8
|
|
|
|
|
|
int i, j = 0; |
|
125
|
136
|
100
|
|
|
|
|
for (i = 0; i < 16; i++) { |
|
126
|
128
|
100
|
|
|
|
|
if (i == 4 || i == 6 || i == 8 || i == 10) |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
127
|
32
|
|
|
|
|
|
dst[j++] = '-'; |
|
128
|
128
|
|
|
|
|
|
dst[j++] = hex[(uuid[i] >> 4) & 0x0F]; |
|
129
|
128
|
|
|
|
|
|
dst[j++] = hex[uuid[i] & 0x0F]; |
|
130
|
|
|
|
|
|
|
} |
|
131
|
8
|
|
|
|
|
|
} |
|
132
|
|
|
|
|
|
|
|
|
133
|
8
|
|
|
|
|
|
static inline void sekhmet_ulid_to_uuid_bin(unsigned char *uuid, |
|
134
|
|
|
|
|
|
|
const unsigned char *ulid) { |
|
135
|
8
|
|
|
|
|
|
memcpy(uuid, ulid, 16); |
|
136
|
|
|
|
|
|
|
/* Stamp version 7: byte 6 high nibble = 0x7 */ |
|
137
|
8
|
|
|
|
|
|
uuid[6] = (uuid[6] & 0x0F) | 0x70; |
|
138
|
|
|
|
|
|
|
/* Stamp variant RFC4122: byte 8 high 2 bits = 10 */ |
|
139
|
8
|
|
|
|
|
|
uuid[8] = (uuid[8] & 0x3F) | 0x80; |
|
140
|
8
|
|
|
|
|
|
} |
|
141
|
|
|
|
|
|
|
|
|
142
|
8
|
|
|
|
|
|
static inline void sekhmet_ulid_to_uuid_str(char *dst, |
|
143
|
|
|
|
|
|
|
const unsigned char *ulid) { |
|
144
|
|
|
|
|
|
|
unsigned char uuid[16]; |
|
145
|
8
|
|
|
|
|
|
sekhmet_ulid_to_uuid_bin(uuid, ulid); |
|
146
|
8
|
|
|
|
|
|
sekhmet_format_uuid(dst, uuid); |
|
147
|
8
|
|
|
|
|
|
} |
|
148
|
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
/* ── UUID string → ULID conversion ─────────────────────────────── * |
|
150
|
|
|
|
|
|
|
* |
|
151
|
|
|
|
|
|
|
* Parse UUID hex string, strip version/variant bits, recover ULID binary. |
|
152
|
|
|
|
|
|
|
*/ |
|
153
|
|
|
|
|
|
|
|
|
154
|
7
|
|
|
|
|
|
static inline int sekhmet_parse_uuid_hex(unsigned char *out, |
|
155
|
|
|
|
|
|
|
const char *src, int len) { |
|
156
|
|
|
|
|
|
|
static const unsigned char hv[256] = { |
|
157
|
|
|
|
|
|
|
['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, |
|
158
|
|
|
|
|
|
|
['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7, |
|
159
|
|
|
|
|
|
|
['8'] = 8, ['9'] = 9, |
|
160
|
|
|
|
|
|
|
['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, |
|
161
|
|
|
|
|
|
|
['e'] = 14, ['f'] = 15, |
|
162
|
|
|
|
|
|
|
['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, |
|
163
|
|
|
|
|
|
|
['E'] = 14, ['F'] = 15, |
|
164
|
|
|
|
|
|
|
}; |
|
165
|
7
|
|
|
|
|
|
int i, j = 0; |
|
166
|
147
|
100
|
|
|
|
|
for (i = 0; i < len && j < 16; i++) { |
|
|
|
50
|
|
|
|
|
|
|
167
|
140
|
|
|
|
|
|
char c = src[i]; |
|
168
|
140
|
100
|
|
|
|
|
if (c == '-') continue; |
|
169
|
112
|
50
|
|
|
|
|
if (i + 1 >= len) return 0; |
|
170
|
|
|
|
|
|
|
{ |
|
171
|
112
|
|
|
|
|
|
char c2 = src[++i]; |
|
172
|
|
|
|
|
|
|
/* skip hyphens in second nibble position too */ |
|
173
|
112
|
50
|
|
|
|
|
while (c2 == '-' && i + 1 < len) c2 = src[++i]; |
|
|
|
0
|
|
|
|
|
|
|
174
|
112
|
|
|
|
|
|
out[j++] = (unsigned char)((hv[(unsigned char)c] << 4) |
|
175
|
112
|
|
|
|
|
|
| hv[(unsigned char)c2]); |
|
176
|
|
|
|
|
|
|
} |
|
177
|
|
|
|
|
|
|
} |
|
178
|
7
|
|
|
|
|
|
return j == 16; |
|
179
|
|
|
|
|
|
|
} |
|
180
|
|
|
|
|
|
|
|
|
181
|
7
|
|
|
|
|
|
static inline int sekhmet_uuid_to_ulid_bin(unsigned char *ulid, |
|
182
|
|
|
|
|
|
|
const char *uuid_str, int len) { |
|
183
|
|
|
|
|
|
|
unsigned char uuid[16]; |
|
184
|
7
|
50
|
|
|
|
|
if (!sekhmet_parse_uuid_hex(uuid, uuid_str, len)) |
|
185
|
0
|
|
|
|
|
|
return 0; |
|
186
|
|
|
|
|
|
|
|
|
187
|
7
|
|
|
|
|
|
memcpy(ulid, uuid, 16); |
|
188
|
|
|
|
|
|
|
/* Restore original ULID random bits (clear version/variant stamps) */ |
|
189
|
7
|
|
|
|
|
|
ulid[6] = (ulid[6] & 0x0F) | (uuid[6] & 0xF0); |
|
190
|
7
|
|
|
|
|
|
ulid[8] = (ulid[8] & 0x3F) | (uuid[8] & 0xC0); |
|
191
|
7
|
|
|
|
|
|
return 1; |
|
192
|
|
|
|
|
|
|
} |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
/* ── Compare two binary ULIDs ──────────────────────────────────── */ |
|
195
|
|
|
|
|
|
|
|
|
196
|
42
|
|
|
|
|
|
static inline int sekhmet_ulid_compare(const unsigned char *a, |
|
197
|
|
|
|
|
|
|
const unsigned char *b) { |
|
198
|
42
|
|
|
|
|
|
return memcmp(a, b, 16); |
|
199
|
|
|
|
|
|
|
} |
|
200
|
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
/* ── Validate a ULID string ────────────────────────────────────── */ |
|
202
|
|
|
|
|
|
|
|
|
203
|
128
|
|
|
|
|
|
static inline int sekhmet_ulid_validate(const char *str, int len) { |
|
204
|
|
|
|
|
|
|
static const unsigned char valid[256] = { |
|
205
|
|
|
|
|
|
|
['0'] = 1, ['1'] = 1, ['2'] = 1, ['3'] = 1, ['4'] = 1, |
|
206
|
|
|
|
|
|
|
['5'] = 1, ['6'] = 1, ['7'] = 1, ['8'] = 1, ['9'] = 1, |
|
207
|
|
|
|
|
|
|
['A'] = 1, ['B'] = 1, ['C'] = 1, ['D'] = 1, ['E'] = 1, |
|
208
|
|
|
|
|
|
|
['F'] = 1, ['G'] = 1, ['H'] = 1, ['J'] = 1, ['K'] = 1, |
|
209
|
|
|
|
|
|
|
['M'] = 1, ['N'] = 1, ['P'] = 1, ['Q'] = 1, ['R'] = 1, |
|
210
|
|
|
|
|
|
|
['S'] = 1, ['T'] = 1, ['V'] = 1, ['W'] = 1, ['X'] = 1, |
|
211
|
|
|
|
|
|
|
['Y'] = 1, ['Z'] = 1, |
|
212
|
|
|
|
|
|
|
['a'] = 1, ['b'] = 1, ['c'] = 1, ['d'] = 1, ['e'] = 1, |
|
213
|
|
|
|
|
|
|
['f'] = 1, ['g'] = 1, ['h'] = 1, ['j'] = 1, ['k'] = 1, |
|
214
|
|
|
|
|
|
|
['m'] = 1, ['n'] = 1, ['p'] = 1, ['q'] = 1, ['r'] = 1, |
|
215
|
|
|
|
|
|
|
['s'] = 1, ['t'] = 1, ['v'] = 1, ['w'] = 1, ['x'] = 1, |
|
216
|
|
|
|
|
|
|
['y'] = 1, ['z'] = 1, |
|
217
|
|
|
|
|
|
|
}; |
|
218
|
|
|
|
|
|
|
int i; |
|
219
|
128
|
100
|
|
|
|
|
if (len != 26) return 0; |
|
220
|
|
|
|
|
|
|
/* First char must be <= '7' (max timestamp 2^48-1 fits in 3 bits) */ |
|
221
|
125
|
100
|
|
|
|
|
if ((unsigned char)str[0] > '7') return 0; |
|
222
|
3314
|
100
|
|
|
|
|
for (i = 0; i < 26; i++) { |
|
223
|
3198
|
100
|
|
|
|
|
if (!valid[(unsigned char)str[i]]) return 0; |
|
224
|
|
|
|
|
|
|
} |
|
225
|
116
|
|
|
|
|
|
return 1; |
|
226
|
|
|
|
|
|
|
} |
|
227
|
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
#endif /* SEKHMET_ULID_H */ |