1pub use kcode_k1_access_profile_values::{
2 AuthorizationProfile, GroupId, ModelId, ProfileId, ProfileOwner, ProfileViewer, TxId, UserId,
3};
4
5const WIRE_VERSION: u8 = 1;
6const CREATE_TAG: u8 = 1;
7const REPLACE_TAG: u8 = 2;
8const DELETE_TAG: u8 = 3;
9const HEADER_BYTES: usize = 2;
10const OPERATION_ID_BYTES: usize = 16;
11const TX_ID_BYTES: usize = 12;
12const CREATE_PREFIX_BYTES: usize = HEADER_BYTES + OPERATION_ID_BYTES + TX_ID_BYTES;
13const REPLACE_PREFIX_BYTES: usize = CREATE_PREFIX_BYTES + TX_ID_BYTES;
14pub type OperationId = [u8; OPERATION_ID_BYTES];
15
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub enum ProfileMutation {
18 Create {
19 owner: UserId,
20 profile: AuthorizationProfile,
21 },
22 Replace {
23 profile_id: ProfileId,
24 actor: UserId,
25 profile: AuthorizationProfile,
26 },
27 Delete {
28 profile_id: ProfileId,
29 actor: UserId,
30 },
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct ProfileOperation {
35 operation_id: OperationId,
36 mutation: ProfileMutation,
37}
38
39impl ProfileOperation {
40 pub const fn new(operation_id: OperationId, mutation: ProfileMutation) -> Self {
41 Self {
42 operation_id,
43 mutation,
44 }
45 }
46
47 pub const fn operation_id(&self) -> OperationId {
48 self.operation_id
49 }
50
51 pub const fn mutation(&self) -> &ProfileMutation {
52 &self.mutation
53 }
54
55 pub fn into_mutation(self) -> ProfileMutation {
56 self.mutation
57 }
58
59 pub fn into_parts(self) -> (OperationId, ProfileMutation) {
60 (self.operation_id, self.mutation)
61 }
62}
63
64pub fn encode_profile(profile: &AuthorizationProfile) -> Result<Vec<u8>, String> {
65 let owner_count = u32::try_from(profile.owners().len())
66 .map_err(|_| "owner count exceeds canonical encoding".to_owned())?;
67 let viewer_count = u32::try_from(profile.viewers().len())
68 .map_err(|_| "viewer count exceeds canonical encoding".to_owned())?;
69 let mut bytes = Vec::new();
70 bytes.push(1);
71 bytes.extend_from_slice(&owner_count.to_be_bytes());
72 for owner in profile.owners() {
73 match owner {
74 ProfileOwner::RequestUser => bytes.push(0),
75 ProfileOwner::User(user) => {
76 bytes.push(1);
77 bytes.extend_from_slice(user.as_tx_id().as_bytes());
78 }
79 ProfileOwner::Group(group) => {
80 bytes.push(2);
81 bytes.extend_from_slice(group.txid().as_bytes());
82 }
83 }
84 }
85 bytes.extend_from_slice(&viewer_count.to_be_bytes());
86 for viewer in profile.viewers() {
87 match viewer {
88 ProfileViewer::RequestUser => bytes.push(0),
89 ProfileViewer::RequestModel => bytes.push(1),
90 ProfileViewer::User(user) => {
91 bytes.push(2);
92 bytes.extend_from_slice(user.as_tx_id().as_bytes());
93 }
94 ProfileViewer::Group(group) => {
95 bytes.push(3);
96 bytes.extend_from_slice(group.txid().as_bytes());
97 }
98 ProfileViewer::Model(model) => {
99 bytes.push(4);
100 bytes.extend_from_slice(model.as_bytes());
101 }
102 }
103 }
104 Ok(bytes)
105}
106
107pub fn decode_profile(bytes: &[u8]) -> Result<AuthorizationProfile, String> {
108 let mut reader = Reader::new(bytes);
109 if reader.u8()? != 1 {
110 return Err("unknown profile encoding version".to_owned());
111 }
112 let owner_count = usize::try_from(reader.u32()?)
113 .map_err(|_| "owner count does not fit this platform".to_owned())?;
114 let mut owners = Vec::new();
115 for _ in 0..owner_count {
116 owners.push(match reader.u8()? {
117 0 => ProfileOwner::RequestUser,
118 1 => ProfileOwner::User(UserId::from_tx_id(TxId::from_bytes(reader.take()?))),
119 2 => ProfileOwner::Group(GroupId::new(TxId::from_bytes(reader.take()?))),
120 _ => return Err("unknown profile owner tag".to_owned()),
121 });
122 }
123 let viewer_count = usize::try_from(reader.u32()?)
124 .map_err(|_| "viewer count does not fit this platform".to_owned())?;
125 let mut viewers = Vec::new();
126 for _ in 0..viewer_count {
127 viewers.push(match reader.u8()? {
128 0 => ProfileViewer::RequestUser,
129 1 => ProfileViewer::RequestModel,
130 2 => ProfileViewer::User(UserId::from_tx_id(TxId::from_bytes(reader.take()?))),
131 3 => ProfileViewer::Group(GroupId::new(TxId::from_bytes(reader.take()?))),
132 4 => ProfileViewer::Model(ModelId::from_bytes(reader.take()?)),
133 _ => return Err("unknown profile viewer tag".to_owned()),
134 });
135 }
136 if !reader.finished() {
137 return Err("trailing bytes in profile encoding".to_owned());
138 }
139 let profile = AuthorizationProfile::new(owners, viewers)?;
140 if encode_profile(&profile)?.as_slice() != bytes {
141 return Err("profile encoding is not canonical".to_owned());
142 }
143 Ok(profile)
144}
145
146struct Reader<'a> {
147 bytes: &'a [u8],
148 offset: usize,
149}
150
151impl<'a> Reader<'a> {
152 const fn new(bytes: &'a [u8]) -> Self {
153 Self { bytes, offset: 0 }
154 }
155
156 fn u8(&mut self) -> Result<u8, String> {
157 Ok(self.take::<1>()?[0])
158 }
159
160 fn u32(&mut self) -> Result<u32, String> {
161 Ok(u32::from_be_bytes(self.take()?))
162 }
163
164 fn take<const N: usize>(&mut self) -> Result<[u8; N], String> {
165 let end = self
166 .offset
167 .checked_add(N)
168 .ok_or_else(|| "truncated profile encoding".to_owned())?;
169 let source = self
170 .bytes
171 .get(self.offset..end)
172 .ok_or_else(|| "truncated profile encoding".to_owned())?;
173 let mut output = [0; N];
174 output.copy_from_slice(source);
175 self.offset = end;
176 Ok(output)
177 }
178
179 fn finished(&self) -> bool {
180 self.offset == self.bytes.len()
181 }
182}
183
184pub fn encode_operation(operation: &ProfileOperation) -> Result<Vec<u8>, String> {
185 let operation_id = operation.operation_id();
186 match operation.mutation() {
187 ProfileMutation::Create { owner, profile } => {
188 let profile_bytes = encode_profile(profile)?;
189 let capacity = CREATE_PREFIX_BYTES
190 .checked_add(profile_bytes.len())
191 .ok_or_else(|| "profile payload length overflow".to_owned())?;
192 let mut payload = Vec::with_capacity(capacity);
193 payload.extend_from_slice(&[WIRE_VERSION, CREATE_TAG]);
194 payload.extend_from_slice(&operation_id);
195 payload.extend_from_slice(owner.as_tx_id().as_bytes());
196 payload.extend_from_slice(&profile_bytes);
197 Ok(payload)
198 }
199 ProfileMutation::Replace {
200 profile_id,
201 actor,
202 profile,
203 } => {
204 let profile_bytes = encode_profile(profile)?;
205 let capacity = REPLACE_PREFIX_BYTES
206 .checked_add(profile_bytes.len())
207 .ok_or_else(|| "profile payload length overflow".to_owned())?;
208 let mut payload = Vec::with_capacity(capacity);
209 payload.extend_from_slice(&[WIRE_VERSION, REPLACE_TAG]);
210 payload.extend_from_slice(&operation_id);
211 payload.extend_from_slice(profile_id.txid().as_bytes());
212 payload.extend_from_slice(actor.as_tx_id().as_bytes());
213 payload.extend_from_slice(&profile_bytes);
214 Ok(payload)
215 }
216 ProfileMutation::Delete { profile_id, actor } => {
217 let mut payload = Vec::with_capacity(REPLACE_PREFIX_BYTES);
218 payload.extend_from_slice(&[WIRE_VERSION, DELETE_TAG]);
219 payload.extend_from_slice(&operation_id);
220 payload.extend_from_slice(profile_id.txid().as_bytes());
221 payload.extend_from_slice(actor.as_tx_id().as_bytes());
222 Ok(payload)
223 }
224 }
225}
226
227pub fn parse_operation(payload: &[u8]) -> Result<ProfileOperation, String> {
228 if payload.len() < HEADER_BYTES {
229 return Err("profile payload header is truncated".to_owned());
230 }
231 if payload[0] != WIRE_VERSION {
232 return Err("unsupported profile payload version".to_owned());
233 }
234 match payload[1] {
235 CREATE_TAG => parse_create(payload),
236 REPLACE_TAG => parse_replace(payload),
237 DELETE_TAG => parse_delete(payload),
238 _ => Err("unsupported profile payload action".to_owned()),
239 }
240}
241
242fn parse_create(payload: &[u8]) -> Result<ProfileOperation, String> {
243 if payload.len() <= CREATE_PREFIX_BYTES {
244 return Err("create profile payload is truncated".to_owned());
245 }
246 Ok(ProfileOperation::new(
247 read_operation_id(payload),
248 ProfileMutation::Create {
249 owner: UserId::from_tx_id(read_txid(&payload[18..30])),
250 profile: decode_profile(&payload[CREATE_PREFIX_BYTES..])?,
251 },
252 ))
253}
254
255fn parse_replace(payload: &[u8]) -> Result<ProfileOperation, String> {
256 if payload.len() <= REPLACE_PREFIX_BYTES {
257 return Err("replace profile payload is truncated".to_owned());
258 }
259 Ok(ProfileOperation::new(
260 read_operation_id(payload),
261 ProfileMutation::Replace {
262 profile_id: ProfileId::new(read_txid(&payload[18..30])),
263 actor: UserId::from_tx_id(read_txid(&payload[30..42])),
264 profile: decode_profile(&payload[REPLACE_PREFIX_BYTES..])?,
265 },
266 ))
267}
268
269fn parse_delete(payload: &[u8]) -> Result<ProfileOperation, String> {
270 if payload.len() != REPLACE_PREFIX_BYTES {
271 return Err("delete profile payload must be exactly 42 bytes".to_owned());
272 }
273 Ok(ProfileOperation::new(
274 read_operation_id(payload),
275 ProfileMutation::Delete {
276 profile_id: ProfileId::new(read_txid(&payload[18..30])),
277 actor: UserId::from_tx_id(read_txid(&payload[30..42])),
278 },
279 ))
280}
281
282fn read_operation_id(payload: &[u8]) -> OperationId {
283 let mut operation_id = [0; OPERATION_ID_BYTES];
284 operation_id.copy_from_slice(&payload[2..18]);
285 operation_id
286}
287
288fn read_txid(bytes: &[u8]) -> TxId {
289 let mut txid = [0; TX_ID_BYTES];
290 txid.copy_from_slice(bytes);
291 TxId::from_bytes(txid)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn tx(byte: u8) -> TxId {
299 TxId::from_bytes([byte; 12])
300 }
301 fn user(byte: u8) -> UserId {
302 UserId::from_tx_id(tx(byte))
303 }
304 fn group(byte: u8) -> GroupId {
305 GroupId::new(tx(byte))
306 }
307 fn model(byte: u8) -> ModelId {
308 ModelId::from_bytes([byte; 32])
309 }
310 fn profile() -> AuthorizationProfile {
311 AuthorizationProfile::new(
312 vec![
313 ProfileOwner::Group(group(3)),
314 ProfileOwner::RequestUser,
315 ProfileOwner::User(user(2)),
316 ],
317 vec![
318 ProfileViewer::Model(model(5)),
319 ProfileViewer::Group(group(4)),
320 ProfileViewer::RequestUser,
321 ProfileViewer::User(user(3)),
322 ProfileViewer::RequestModel,
323 ],
324 )
325 .expect("profile")
326 }
327
328 #[test]
329 fn canonical_codec_has_exact_version_one_bytes_and_round_trips() {
330 let mut expected = vec![1];
331 expected.extend_from_slice(&3_u32.to_be_bytes());
332 expected.push(0);
333 expected.push(1);
334 expected.extend_from_slice(tx(2).as_bytes());
335 expected.push(2);
336 expected.extend_from_slice(tx(3).as_bytes());
337 expected.extend_from_slice(&5_u32.to_be_bytes());
338 expected.extend_from_slice(&[0, 1, 2]);
339 expected.extend_from_slice(tx(3).as_bytes());
340 expected.push(3);
341 expected.extend_from_slice(tx(4).as_bytes());
342 expected.push(4);
343 expected.extend_from_slice(model(5).as_bytes());
344 assert_eq!(encode_profile(&profile()).expect("encoding"), expected);
345 assert_eq!(decode_profile(&expected).expect("decoding"), profile());
346 }
347
348 #[test]
349 fn decoder_rejects_malformed_and_noncanonical_encodings_with_exact_errors() {
350 assert_eq!(
351 decode_profile(&[]),
352 Err("truncated profile encoding".to_owned())
353 );
354 assert_eq!(
355 decode_profile(&[2]),
356 Err("unknown profile encoding version".to_owned())
357 );
358 assert_eq!(
359 decode_profile(&[1]),
360 Err("truncated profile encoding".to_owned())
361 );
362 assert_eq!(
363 decode_profile(&[1, 0, 0, 0, 1, 9]),
364 Err("unknown profile owner tag".to_owned())
365 );
366 assert_eq!(
367 decode_profile(&[1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 9]),
368 Err("unknown profile viewer tag".to_owned())
369 );
370 assert_eq!(
371 decode_profile(&[1, 0, 0, 0, 0, 0, 0, 0, 0]),
372 Err("authorization profile requires at least one owner".to_owned())
373 );
374
375 let mut noncanonical = vec![1];
376 noncanonical.extend_from_slice(&2_u32.to_be_bytes());
377 noncanonical.push(2);
378 noncanonical.extend_from_slice(tx(1).as_bytes());
379 noncanonical.push(0);
380 noncanonical.extend_from_slice(&0_u32.to_be_bytes());
381 assert_eq!(
382 decode_profile(&noncanonical),
383 Err("profile encoding is not canonical".to_owned())
384 );
385
386 let mut duplicate = vec![1];
387 duplicate.extend_from_slice(&2_u32.to_be_bytes());
388 duplicate.extend_from_slice(&[0, 0]);
389 duplicate.extend_from_slice(&0_u32.to_be_bytes());
390 assert_eq!(
391 decode_profile(&duplicate),
392 Err("profile encoding is not canonical".to_owned())
393 );
394
395 let mut trailing = encode_profile(
396 &AuthorizationProfile::new(vec![ProfileOwner::RequestUser], vec![]).expect("profile"),
397 )
398 .expect("encoding");
399 trailing.push(0);
400 assert_eq!(
401 decode_profile(&trailing),
402 Err("trailing bytes in profile encoding".to_owned())
403 );
404 }
405
406 #[test]
407 fn mutation_wire_has_exact_bytes_and_round_trips_all_actions() {
408 let id = [7; 16];
409 let cases = [
410 ProfileMutation::Create {
411 owner: user(8),
412 profile: profile(),
413 },
414 ProfileMutation::Replace {
415 profile_id: ProfileId::new(tx(9)),
416 actor: user(8),
417 profile: profile(),
418 },
419 ProfileMutation::Delete {
420 profile_id: ProfileId::new(tx(9)),
421 actor: user(8),
422 },
423 ];
424 for (index, mutation) in cases.into_iter().enumerate() {
425 let operation = ProfileOperation::new(id, mutation);
426 let encoded = encode_operation(&operation).expect("encoding");
427 let mut expected = vec![1, (index + 1) as u8];
428 expected.extend_from_slice(&id);
429 match operation.mutation() {
430 ProfileMutation::Create { owner, profile } => {
431 expected.extend_from_slice(owner.as_tx_id().as_bytes());
432 expected.extend_from_slice(&encode_profile(profile).expect("profile encoding"));
433 }
434 ProfileMutation::Replace {
435 profile_id,
436 actor,
437 profile,
438 } => {
439 expected.extend_from_slice(profile_id.txid().as_bytes());
440 expected.extend_from_slice(actor.as_tx_id().as_bytes());
441 expected.extend_from_slice(&encode_profile(profile).expect("profile encoding"));
442 }
443 ProfileMutation::Delete { profile_id, actor } => {
444 expected.extend_from_slice(profile_id.txid().as_bytes());
445 expected.extend_from_slice(actor.as_tx_id().as_bytes());
446 assert_eq!(encoded.len(), 42);
447 }
448 }
449 assert_eq!(encoded, expected);
450 assert_eq!(parse_operation(&encoded).expect("parsing"), operation);
451 }
452 }
453
454 #[test]
455 fn mutation_parser_preserves_validation_order_and_exact_errors() {
456 assert_eq!(
457 parse_operation(&[]),
458 Err("profile payload header is truncated".to_owned())
459 );
460 assert_eq!(
461 parse_operation(&[1]),
462 Err("profile payload header is truncated".to_owned())
463 );
464 assert_eq!(
465 parse_operation(&[2, 9]),
466 Err("unsupported profile payload version".to_owned())
467 );
468 assert_eq!(
469 parse_operation(&[1, 9]),
470 Err("unsupported profile payload action".to_owned())
471 );
472 assert_eq!(
473 parse_operation(&[1, 1]),
474 Err("create profile payload is truncated".to_owned())
475 );
476 assert_eq!(
477 parse_operation(&[1, 2]),
478 Err("replace profile payload is truncated".to_owned())
479 );
480 assert_eq!(
481 parse_operation(&[1, 3]),
482 Err("delete profile payload must be exactly 42 bytes".to_owned())
483 );
484
485 let mut create = vec![1, 1];
486 create.extend_from_slice(&[0; 16]);
487 create.extend_from_slice(tx(1).as_bytes());
488 create.push(2);
489 assert_eq!(
490 parse_operation(&create),
491 Err("unknown profile encoding version".to_owned())
492 );
493
494 let mut delete = vec![1, 3];
495 delete.extend_from_slice(&[0; 41]);
496 assert_eq!(
497 parse_operation(&delete),
498 Err("delete profile payload must be exactly 42 bytes".to_owned())
499 );
500 }
501}