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}