macroonz_compiler/identity/type_guard.rs
1//! The identity home's invariant nucleus: every road that reaches a private field.
2//!
3//! This file is declared inside `types.rs` as its own child, so it sees the fields the declarations keep private and nothing else in the crate does.
4//! That is what makes the walls structural rather than reviewed: a road around one of them would have to be written here, and none is.
5
6use super::{
7 Anchoring, GENERATOR, GeneratorIdentity, HumanProjection, Identity, OwnerIdentity, Profile,
8 Provenance, Role, ShapeVersion, Subject, Transcript, Version,
9};
10use crate::bounded::{Bounded, Overflow};
11use core::fmt;
12use core::hash::{Hash, Hasher};
13use core::marker::PhantomData;
14
15impl<S: Subject> Identity<S> {
16 /// Derive one identity from its complete transcript.
17 /// Deterministic and total: every transcript names an identity.
18 #[must_use]
19 pub fn derived(transcript: Transcript<'_>) -> Self {
20 let context = transcript.profile().context_for::<S>(transcript.role());
21 Self(
22 blake3::derive_key(&context, &transcript.encoded::<S>()),
23 PhantomData,
24 )
25 }
26
27 /// Derive one identity and the record of how it was derived.
28 ///
29 /// The record is for a value that is going to keep it — one whose identity a reader may be handed on its own and asked to account for.
30 /// A caller with nowhere to put one takes [`Identity::derived`], and the record is simply not made rather than made and carried by everything.
31 #[must_use]
32 pub fn derived_with_provenance(transcript: Transcript<'_>) -> (Self, Provenance) {
33 (Self::derived(transcript), transcript.provenance::<S>())
34 }
35
36 /// The identity's thirty-two bytes, borrowed for comparison and for rendering.
37 #[must_use]
38 pub const fn as_bytes(&self) -> &[u8; 32] {
39 &self.0
40 }
41}
42
43impl<S: Subject> PartialEq for Identity<S> {
44 fn eq(&self, other: &Self) -> bool {
45 self.0 == other.0
46 }
47}
48
49impl<S: Subject> Eq for Identity<S> {}
50
51impl<S: Subject> Hash for Identity<S> {
52 fn hash<H: Hasher>(&self, into: &mut H) {
53 self.0.hash(into);
54 }
55}
56
57impl<S: Subject> fmt::Debug for Identity<S> {
58 fn fmt(&self, into: &mut fmt::Formatter<'_>) -> fmt::Result {
59 into.debug_tuple(S::NAME).field(&self.0).finish()
60 }
61}
62
63impl Version {
64 /// The position the grammar's owner assigned.
65 #[must_use]
66 pub const fn declared(position: u32) -> Self {
67 Self(position)
68 }
69
70 /// The assigned position.
71 #[must_use]
72 pub const fn position(self) -> u32 {
73 self.0
74 }
75}
76
77impl Profile {
78 /// One grammar, under the stem of whoever owns it, at one version.
79 #[must_use]
80 pub const fn declared(stem: &'static str, name: &'static str, version: Version) -> Self {
81 Self {
82 stem,
83 name,
84 version,
85 }
86 }
87
88 /// The stem of whoever owns this grammar.
89 #[must_use]
90 pub const fn stem(self) -> &'static str {
91 self.stem
92 }
93
94 /// The grammar's declared name.
95 #[must_use]
96 pub const fn name(self) -> &'static str {
97 self.name
98 }
99
100 /// The declared version — this grammar's own position, and no other grammar's.
101 #[must_use]
102 pub const fn version(self) -> Version {
103 self.version
104 }
105
106 /// The derive-key context one SUBJECT derives under, at one role.
107 ///
108 /// # Authority
109 ///
110 /// **The subject is the TYPE's and never an argument.** A road that took the subject as text would let a caller derive under any key space it could spell — including one another subject already occupies — and the typed identity above it would be a promise the encoder never had to keep.
111 #[must_use]
112 pub fn context_for<S: Subject>(self, role: Role) -> String {
113 self.context_over(S::STEM, S::NAME, role)
114 }
115
116 /// The same context, over a subject a derivation record already carries.
117 ///
118 /// Crate-internal, with one caller: [`Provenance::context`], which renders what a derivation recorded rather than performing one.
119 pub(crate) fn context_over(self, subject_stem: &str, subject: &str, role: Role) -> String {
120 let Self {
121 stem,
122 name,
123 version,
124 } = self;
125 let position = version.position();
126 let seat = role.name();
127 format!("{stem}/{name}/v{position}/{subject_stem}/{subject}/{seat}")
128 }
129}
130
131impl ShapeVersion {
132 /// The shape position the generator's owner assigned.
133 #[must_use]
134 pub const fn declared(position: u32) -> Self {
135 Self(position)
136 }
137
138 /// The assigned position.
139 #[must_use]
140 pub const fn position(self) -> u32 {
141 self.0
142 }
143}
144
145impl GeneratorIdentity {
146 /// The generator under its declared name, rendered shape, and recorded package version.
147 #[must_use]
148 pub const fn declared(name: &'static str, shape: ShapeVersion, package: &'static str) -> Self {
149 Self {
150 name,
151 shape,
152 package,
153 }
154 }
155
156 /// The generator's stable name. One of the two facts a staleness comparison reads.
157 #[must_use]
158 pub const fn name(self) -> &'static str {
159 self.name
160 }
161
162 /// The rendered shape's version. The other fact a staleness comparison reads.
163 #[must_use]
164 pub const fn shape(self) -> ShapeVersion {
165 self.shape
166 }
167
168 /// The package version, recorded for a reader and compared by nothing.
169 #[must_use]
170 pub const fn package_version(self) -> &'static str {
171 self.package
172 }
173
174 /// Whether two generator identities name the same generator rendering the same shape.
175 ///
176 /// The comparison a staleness reading wants, and the reason it is a named road rather than `==`: equality compares the package version too, and the package version moves for reasons no output noticed.
177 #[must_use]
178 pub fn same_shape(self, other: Self) -> bool {
179 self.name == other.name && self.shape == other.shape
180 }
181}
182
183impl<'material> Transcript<'material> {
184 /// Write a transcript under a grammar the caller names.
185 ///
186 /// The road for a preimage whose grammar is the caller's own; the four roads below name a role and take this compiler's grammar for it.
187 #[must_use]
188 pub const fn under_profile(
189 profile: Profile,
190 role: Role,
191 anchoring: Anchoring,
192 material: &'material [u8],
193 position: u32,
194 ) -> Self {
195 Self {
196 profile,
197 generator: GENERATOR,
198 role,
199 anchoring,
200 material,
201 position,
202 }
203 }
204
205 /// Write a transcript under an anchoring the caller already decided.
206 ///
207 /// The road for a mint site whose anchor depends on a typed posture rather than on which family of identity it holds — a plan hangs off whatever caused it, and what caused it is a sum type.
208 #[must_use]
209 pub const fn under(
210 role: Role,
211 anchoring: Anchoring,
212 material: &'material [u8],
213 position: u32,
214 ) -> Self {
215 Self::under_profile(role.profile(), role, anchoring, material, position)
216 }
217
218 /// Derive under no anchor at all — the root of one derivation chain.
219 #[must_use]
220 pub const fn rooted(role: Role, material: &'material [u8], position: u32) -> Self {
221 Self::under(role, Anchoring::Rooted, material, position)
222 }
223
224 /// Derive under an identity a CONSUMER minted.
225 #[must_use]
226 pub const fn under_owner(
227 role: Role,
228 anchor: &OwnerIdentity,
229 material: &'material [u8],
230 position: u32,
231 ) -> Self {
232 Self::under(
233 role,
234 Anchoring::UnderOwner(anchor.bytes),
235 material,
236 position,
237 )
238 }
239
240 /// Derive under another identity this compiler derived.
241 #[must_use]
242 pub fn under_projection<S: Subject>(
243 role: Role,
244 anchor: &Identity<S>,
245 material: &'material [u8],
246 position: u32,
247 ) -> Self {
248 Self::under(
249 role,
250 Anchoring::UnderProjection(*anchor.as_bytes()),
251 material,
252 position,
253 )
254 }
255
256 /// The grammar this transcript is written under.
257 #[must_use]
258 pub const fn profile(&self) -> Profile {
259 self.profile
260 }
261
262 /// The generator this transcript records.
263 ///
264 /// Recorded and never written: the encoding carries no member for it, and this road exists so the derivation record can.
265 #[must_use]
266 pub const fn generator(&self) -> GeneratorIdentity {
267 self.generator
268 }
269
270 /// The seat this transcript stands in.
271 #[must_use]
272 pub const fn role(&self) -> Role {
273 self.role
274 }
275
276 /// What this transcript hangs off.
277 #[must_use]
278 pub const fn anchoring(&self) -> Anchoring {
279 self.anchoring
280 }
281
282 /// The varying material, at full length.
283 #[must_use]
284 pub const fn material(&self) -> &'material [u8] {
285 self.material
286 }
287
288 /// The position inside the anchor's declared sequence.
289 #[must_use]
290 pub const fn position(&self) -> u32 {
291 self.position
292 }
293
294 /// The derivation record this transcript leaves for one identity subject.
295 #[must_use]
296 pub fn provenance<S: Subject>(&self) -> Provenance {
297 Provenance {
298 subject_stem: S::STEM,
299 subject: S::NAME,
300 role: self.role,
301 profile: self.profile,
302 generator: self.generator,
303 anchoring: self.anchoring,
304 material_length: u64::try_from(self.material.len()).unwrap_or(u64::MAX),
305 position: self.position,
306 }
307 }
308}
309
310impl Provenance {
311 /// The stem of whoever owns the subject this derivation named.
312 #[must_use]
313 pub const fn subject_stem(&self) -> &'static str {
314 self.subject_stem
315 }
316
317 /// The identity subject this derivation named.
318 #[must_use]
319 pub const fn subject(&self) -> &'static str {
320 self.subject
321 }
322
323 /// The seat it stood in.
324 #[must_use]
325 pub const fn role(&self) -> Role {
326 self.role
327 }
328
329 /// The grammar and version it was derived under.
330 #[must_use]
331 pub const fn profile(&self) -> Profile {
332 self.profile
333 }
334
335 /// The generator that derived it.
336 #[must_use]
337 pub const fn generator(&self) -> GeneratorIdentity {
338 self.generator
339 }
340
341 /// What it hung off, anchor included.
342 #[must_use]
343 pub const fn anchoring(&self) -> Anchoring {
344 self.anchoring
345 }
346
347 /// How many bytes of material went into the transcript.
348 #[must_use]
349 pub const fn material_length(&self) -> u64 {
350 self.material_length
351 }
352
353 /// The position inside the anchor's declared sequence.
354 #[must_use]
355 pub const fn position(&self) -> u32 {
356 self.position
357 }
358
359 /// The derive-key context this derivation ran under.
360 #[must_use]
361 pub fn context(&self) -> String {
362 self.profile
363 .context_over(self.subject_stem, self.subject, self.role)
364 }
365
366 /// Whether this derivation was recorded under the generator and rendered shape this crate declares today.
367 ///
368 /// # Nonclaims
369 ///
370 /// A `false` says a different generator shape produced the record.
371 /// It says nothing about whether the material moved, whether the identity would re-derive the same, or whether anything needs redoing.
372 #[must_use]
373 pub fn under_current_shape(&self) -> bool {
374 self.generator.same_shape(GENERATOR)
375 }
376}
377
378impl HumanProjection {
379 /// Render one bounded human projection.
380 ///
381 /// # Errors
382 ///
383 /// Returns [`Overflow`] when the rendering runs past [`HUMAN_TEXT_LIMIT`](super::HUMAN_TEXT_LIMIT).
384 /// A projection that does not fit refuses rather than truncating: a silently cut explanation is a false one.
385 pub fn projected(text: &str) -> Result<Self, Overflow> {
386 Bounded::new(text.as_bytes().to_vec()).map(Self)
387 }
388
389 /// The seam behind [`human_projection!`], which is the only road to it.
390 ///
391 /// The rendering arrives as a fixed-width array, and the width is the array's own TYPE, so this road carries no runtime count, returns no refusal, and has no branch where a rendering that did not fit becomes an empty one.
392 #[must_use]
393 pub(crate) fn proven<const N: usize>(rendered: [u8; N]) -> Self {
394 Self(Bounded::from_array(rendered))
395 }
396
397 /// The empty rendering.
398 /// Total: a caller with nothing to say for a person still owes a value rather than a hole.
399 #[must_use]
400 pub fn empty() -> Self {
401 Self(Bounded::empty())
402 }
403
404 /// The rendering's byte length.
405 #[must_use]
406 pub fn len(&self) -> usize {
407 self.0.len()
408 }
409
410 /// Whether the rendering carries no bytes.
411 #[must_use]
412 pub fn is_empty(&self) -> bool {
413 self.0.is_empty()
414 }
415
416 /// The rendering, for a caller to SHOW a person.
417 ///
418 /// The one lawful use of the bytes and a one-way road out: a frontend that must put a sentence in front of somebody calls this, and nothing inside the compiler calls it at all.
419 #[must_use]
420 pub fn shown(&self) -> String {
421 String::from_utf8_lossy(self.0.as_slice()).into_owned()
422 }
423}
424
425/// One static rendering's bytes, at the fixed width the caller declared.
426///
427/// Written for the `const` item [`human_projection!`] builds: a width other than the rendering's own length stops the compiler rather than handing a reader a padded or cut projection.
428#[must_use]
429pub(crate) const fn static_bytes<const N: usize>(text: &str) -> [u8; N] {
430 assert!(
431 text.len() == N,
432 "a declared width that is not the rendering's own length"
433 );
434 let mut rendered = [0u8; N];
435 let mut source = text.as_bytes();
436 let mut sink: &mut [u8] = &mut rendered;
437 while let Some((seat, open)) = sink.split_first_mut() {
438 let Some((byte, remaining)) = source.split_first() else {
439 break;
440 };
441 *seat = *byte;
442 sink = open;
443 source = remaining;
444 }
445 rendered
446}
447
448/// Projects one STATIC rendering, proving at COMPILE TIME that it fits.
449///
450/// [`HumanProjection::projected`] reads a runtime length and may refuse, and a caller that swallowed that refusal with an empty fallback would be silently deleting an explanation.
451/// Where the material is static the length is a compile-time fact instead, and no refusal road appears anywhere between the literal and the projection.
452macro_rules! human_projection {
453 ($text:literal) => {{
454 const RENDERED: [u8; $text.len()] = $crate::identity::static_bytes($text);
455 $crate::identity::HumanProjection::proven(RENDERED)
456 }};
457}
458
459pub(crate) use human_projection;
460
461/// Whether a roster of declared names really separates: every name inside the context grammar, and no name declared twice.
462///
463/// Written for the `const` block the `subjects!` roster stamp emits, and evaluated at compile time — a name that would collapse two key spaces is a compile error rather than a defect a reader has to notice.
464///
465/// The grammar is the closed one [`Subject::NAME`] declares: lowercase ASCII letters and digits in `-`-joined segments, with no leading, trailing, or doubled separator.
466#[must_use]
467pub const fn names_are_separating(names: &[&str]) -> bool {
468 match names.split_first() {
469 None => true,
470 Some((first, rest)) => {
471 name_is_grammatical(first) && !names_contain(rest, first) && names_are_separating(rest)
472 }
473 }
474}
475
476/// Where in a `-`-joined segment the grammar walk stands.
477#[derive(Clone, Copy)]
478enum Segment {
479 /// No character of the current segment has been read yet.
480 Opening,
481 /// At least one character of the current segment has been read.
482 Inside,
483}
484
485/// Whether one declared name stands inside the closed context grammar.
486const fn name_is_grammatical(name: &str) -> bool {
487 grammatical(name.as_bytes(), Segment::Opening)
488}
489
490/// The grammar walk, carrying where in a segment the reader stands.
491///
492/// It opens at [`Segment::Opening`], so an empty name and a leading separator both refuse; it ends well only from [`Segment::Inside`], so a trailing separator refuses too.
493const fn grammatical(bytes: &[u8], at: Segment) -> bool {
494 match bytes.split_first() {
495 None => matches!(at, Segment::Inside),
496 Some((byte, rest)) => {
497 if *byte == b'-' {
498 matches!(at, Segment::Inside) && grammatical(rest, Segment::Opening)
499 } else if byte.is_ascii_lowercase() || byte.is_ascii_digit() {
500 grammatical(rest, Segment::Inside)
501 } else {
502 false
503 }
504 }
505 }
506}
507
508/// Whether a roster already carries one declared name.
509const fn names_contain(names: &[&str], name: &str) -> bool {
510 match names.split_first() {
511 None => false,
512 Some((first, rest)) => {
513 same_bytes(first.as_bytes(), name.as_bytes()) || names_contain(rest, name)
514 }
515 }
516}
517
518/// Whether two declared names are the same bytes.
519const fn same_bytes(left: &[u8], right: &[u8]) -> bool {
520 match (left.split_first(), right.split_first()) {
521 (None, None) => true,
522 (None, Some(_)) | (Some(_), None) => false,
523 (Some((here, left_rest)), Some((there, right_rest))) => {
524 *here == *there && same_bytes(left_rest, right_rest)
525 }
526 }
527}