Skip to main content

kcode_k1_access_profile_wire/
lib.rs

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