Skip to main content

kcode_k1_access_profile_wire/
lib.rs

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}