pub use kcode_k1_access_profile_values::{
AuthorizationProfile, GroupId, ModelId, ProfileId, ProfileOwner, ProfileViewer, TxId, UserId,
};
const WIRE_VERSION: u8 = 1;
const CREATE_TAG: u8 = 1;
const REPLACE_TAG: u8 = 2;
const DELETE_TAG: u8 = 3;
const HEADER_BYTES: usize = 2;
const OPERATION_ID_BYTES: usize = 16;
const TX_ID_BYTES: usize = 12;
const CREATE_PREFIX_BYTES: usize = HEADER_BYTES + OPERATION_ID_BYTES + TX_ID_BYTES;
const REPLACE_PREFIX_BYTES: usize = CREATE_PREFIX_BYTES + TX_ID_BYTES;
pub type OperationId = [u8; OPERATION_ID_BYTES];
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProfileMutation {
Create {
owner: UserId,
profile: AuthorizationProfile,
},
Replace {
profile_id: ProfileId,
actor: UserId,
profile: AuthorizationProfile,
},
Delete {
profile_id: ProfileId,
actor: UserId,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProfileOperation {
operation_id: OperationId,
mutation: ProfileMutation,
}
impl ProfileOperation {
pub const fn new(operation_id: OperationId, mutation: ProfileMutation) -> Self {
Self {
operation_id,
mutation,
}
}
pub const fn operation_id(&self) -> OperationId {
self.operation_id
}
pub const fn mutation(&self) -> &ProfileMutation {
&self.mutation
}
pub fn into_mutation(self) -> ProfileMutation {
self.mutation
}
pub fn into_parts(self) -> (OperationId, ProfileMutation) {
(self.operation_id, self.mutation)
}
}
pub fn encode_profile(profile: &AuthorizationProfile) -> Result<Vec<u8>, String> {
let owner_count = u32::try_from(profile.owners().len())
.map_err(|_| "owner count exceeds canonical encoding".to_owned())?;
let viewer_count = u32::try_from(profile.viewers().len())
.map_err(|_| "viewer count exceeds canonical encoding".to_owned())?;
let mut bytes = Vec::new();
bytes.push(1);
bytes.extend_from_slice(&owner_count.to_be_bytes());
for owner in profile.owners() {
match owner {
ProfileOwner::RequestUser => bytes.push(0),
ProfileOwner::User(user) => {
bytes.push(1);
bytes.extend_from_slice(user.as_tx_id().as_bytes());
}
ProfileOwner::Group(group) => {
bytes.push(2);
bytes.extend_from_slice(group.txid().as_bytes());
}
}
}
bytes.extend_from_slice(&viewer_count.to_be_bytes());
for viewer in profile.viewers() {
match viewer {
ProfileViewer::RequestUser => bytes.push(0),
ProfileViewer::RequestModel => bytes.push(1),
ProfileViewer::User(user) => {
bytes.push(2);
bytes.extend_from_slice(user.as_tx_id().as_bytes());
}
ProfileViewer::Group(group) => {
bytes.push(3);
bytes.extend_from_slice(group.txid().as_bytes());
}
ProfileViewer::Model(model) => {
bytes.push(4);
bytes.extend_from_slice(model.as_bytes());
}
}
}
Ok(bytes)
}
pub fn decode_profile(bytes: &[u8]) -> Result<AuthorizationProfile, String> {
let mut reader = Reader::new(bytes);
if reader.u8()? != 1 {
return Err("unknown profile encoding version".to_owned());
}
let owner_count = usize::try_from(reader.u32()?)
.map_err(|_| "owner count does not fit this platform".to_owned())?;
let mut owners = Vec::new();
for _ in 0..owner_count {
owners.push(match reader.u8()? {
0 => ProfileOwner::RequestUser,
1 => ProfileOwner::User(UserId::from_tx_id(TxId::from_bytes(reader.take()?))),
2 => ProfileOwner::Group(GroupId::new(TxId::from_bytes(reader.take()?))),
_ => return Err("unknown profile owner tag".to_owned()),
});
}
let viewer_count = usize::try_from(reader.u32()?)
.map_err(|_| "viewer count does not fit this platform".to_owned())?;
let mut viewers = Vec::new();
for _ in 0..viewer_count {
viewers.push(match reader.u8()? {
0 => ProfileViewer::RequestUser,
1 => ProfileViewer::RequestModel,
2 => ProfileViewer::User(UserId::from_tx_id(TxId::from_bytes(reader.take()?))),
3 => ProfileViewer::Group(GroupId::new(TxId::from_bytes(reader.take()?))),
4 => ProfileViewer::Model(ModelId::from_bytes(reader.take()?)),
_ => return Err("unknown profile viewer tag".to_owned()),
});
}
if !reader.finished() {
return Err("trailing bytes in profile encoding".to_owned());
}
let profile = AuthorizationProfile::new(owners, viewers)?;
if encode_profile(&profile)?.as_slice() != bytes {
return Err("profile encoding is not canonical".to_owned());
}
Ok(profile)
}
struct Reader<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Reader<'a> {
const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
fn u8(&mut self) -> Result<u8, String> {
Ok(self.take::<1>()?[0])
}
fn u32(&mut self) -> Result<u32, String> {
Ok(u32::from_be_bytes(self.take()?))
}
fn take<const N: usize>(&mut self) -> Result<[u8; N], String> {
let end = self
.offset
.checked_add(N)
.ok_or_else(|| "truncated profile encoding".to_owned())?;
let source = self
.bytes
.get(self.offset..end)
.ok_or_else(|| "truncated profile encoding".to_owned())?;
let mut output = [0; N];
output.copy_from_slice(source);
self.offset = end;
Ok(output)
}
fn finished(&self) -> bool {
self.offset == self.bytes.len()
}
}
pub fn encode_operation(operation: &ProfileOperation) -> Result<Vec<u8>, String> {
let operation_id = operation.operation_id();
match operation.mutation() {
ProfileMutation::Create { owner, profile } => {
let profile_bytes = encode_profile(profile)?;
let capacity = CREATE_PREFIX_BYTES
.checked_add(profile_bytes.len())
.ok_or_else(|| "profile payload length overflow".to_owned())?;
let mut payload = Vec::with_capacity(capacity);
payload.extend_from_slice(&[WIRE_VERSION, CREATE_TAG]);
payload.extend_from_slice(&operation_id);
payload.extend_from_slice(owner.as_tx_id().as_bytes());
payload.extend_from_slice(&profile_bytes);
Ok(payload)
}
ProfileMutation::Replace {
profile_id,
actor,
profile,
} => {
let profile_bytes = encode_profile(profile)?;
let capacity = REPLACE_PREFIX_BYTES
.checked_add(profile_bytes.len())
.ok_or_else(|| "profile payload length overflow".to_owned())?;
let mut payload = Vec::with_capacity(capacity);
payload.extend_from_slice(&[WIRE_VERSION, REPLACE_TAG]);
payload.extend_from_slice(&operation_id);
payload.extend_from_slice(profile_id.txid().as_bytes());
payload.extend_from_slice(actor.as_tx_id().as_bytes());
payload.extend_from_slice(&profile_bytes);
Ok(payload)
}
ProfileMutation::Delete { profile_id, actor } => {
let mut payload = Vec::with_capacity(REPLACE_PREFIX_BYTES);
payload.extend_from_slice(&[WIRE_VERSION, DELETE_TAG]);
payload.extend_from_slice(&operation_id);
payload.extend_from_slice(profile_id.txid().as_bytes());
payload.extend_from_slice(actor.as_tx_id().as_bytes());
Ok(payload)
}
}
}
pub fn parse_operation(payload: &[u8]) -> Result<ProfileOperation, String> {
if payload.len() < HEADER_BYTES {
return Err("profile payload header is truncated".to_owned());
}
if payload[0] != WIRE_VERSION {
return Err("unsupported profile payload version".to_owned());
}
match payload[1] {
CREATE_TAG => parse_create(payload),
REPLACE_TAG => parse_replace(payload),
DELETE_TAG => parse_delete(payload),
_ => Err("unsupported profile payload action".to_owned()),
}
}
fn parse_create(payload: &[u8]) -> Result<ProfileOperation, String> {
if payload.len() <= CREATE_PREFIX_BYTES {
return Err("create profile payload is truncated".to_owned());
}
Ok(ProfileOperation::new(
read_operation_id(payload),
ProfileMutation::Create {
owner: UserId::from_tx_id(read_txid(&payload[18..30])),
profile: decode_profile(&payload[CREATE_PREFIX_BYTES..])?,
},
))
}
fn parse_replace(payload: &[u8]) -> Result<ProfileOperation, String> {
if payload.len() <= REPLACE_PREFIX_BYTES {
return Err("replace profile payload is truncated".to_owned());
}
Ok(ProfileOperation::new(
read_operation_id(payload),
ProfileMutation::Replace {
profile_id: ProfileId::new(read_txid(&payload[18..30])),
actor: UserId::from_tx_id(read_txid(&payload[30..42])),
profile: decode_profile(&payload[REPLACE_PREFIX_BYTES..])?,
},
))
}
fn parse_delete(payload: &[u8]) -> Result<ProfileOperation, String> {
if payload.len() != REPLACE_PREFIX_BYTES {
return Err("delete profile payload must be exactly 42 bytes".to_owned());
}
Ok(ProfileOperation::new(
read_operation_id(payload),
ProfileMutation::Delete {
profile_id: ProfileId::new(read_txid(&payload[18..30])),
actor: UserId::from_tx_id(read_txid(&payload[30..42])),
},
))
}
fn read_operation_id(payload: &[u8]) -> OperationId {
let mut operation_id = [0; OPERATION_ID_BYTES];
operation_id.copy_from_slice(&payload[2..18]);
operation_id
}
fn read_txid(bytes: &[u8]) -> TxId {
let mut txid = [0; TX_ID_BYTES];
txid.copy_from_slice(bytes);
TxId::from_bytes(txid)
}
#[cfg(test)]
mod tests {
use super::*;
fn tx(byte: u8) -> TxId {
TxId::from_bytes([byte; 12])
}
fn user(byte: u8) -> UserId {
UserId::from_tx_id(tx(byte))
}
fn group(byte: u8) -> GroupId {
GroupId::new(tx(byte))
}
fn model(byte: u8) -> ModelId {
ModelId::from_bytes([byte; 32])
}
fn profile() -> AuthorizationProfile {
AuthorizationProfile::new(
vec![
ProfileOwner::Group(group(3)),
ProfileOwner::RequestUser,
ProfileOwner::User(user(2)),
],
vec![
ProfileViewer::Model(model(5)),
ProfileViewer::Group(group(4)),
ProfileViewer::RequestUser,
ProfileViewer::User(user(3)),
ProfileViewer::RequestModel,
],
)
.expect("profile")
}
#[test]
fn canonical_codec_has_exact_version_one_bytes_and_round_trips() {
let mut expected = vec![1];
expected.extend_from_slice(&3_u32.to_be_bytes());
expected.push(0);
expected.push(1);
expected.extend_from_slice(tx(2).as_bytes());
expected.push(2);
expected.extend_from_slice(tx(3).as_bytes());
expected.extend_from_slice(&5_u32.to_be_bytes());
expected.extend_from_slice(&[0, 1, 2]);
expected.extend_from_slice(tx(3).as_bytes());
expected.push(3);
expected.extend_from_slice(tx(4).as_bytes());
expected.push(4);
expected.extend_from_slice(model(5).as_bytes());
assert_eq!(encode_profile(&profile()).expect("encoding"), expected);
assert_eq!(decode_profile(&expected).expect("decoding"), profile());
}
#[test]
fn decoder_rejects_malformed_and_noncanonical_encodings_with_exact_errors() {
assert_eq!(
decode_profile(&[]),
Err("truncated profile encoding".to_owned())
);
assert_eq!(
decode_profile(&[2]),
Err("unknown profile encoding version".to_owned())
);
assert_eq!(
decode_profile(&[1]),
Err("truncated profile encoding".to_owned())
);
assert_eq!(
decode_profile(&[1, 0, 0, 0, 1, 9]),
Err("unknown profile owner tag".to_owned())
);
assert_eq!(
decode_profile(&[1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 9]),
Err("unknown profile viewer tag".to_owned())
);
assert_eq!(
decode_profile(&[1, 0, 0, 0, 0, 0, 0, 0, 0]),
Err("authorization profile requires at least one owner".to_owned())
);
let mut noncanonical = vec![1];
noncanonical.extend_from_slice(&2_u32.to_be_bytes());
noncanonical.push(2);
noncanonical.extend_from_slice(tx(1).as_bytes());
noncanonical.push(0);
noncanonical.extend_from_slice(&0_u32.to_be_bytes());
assert_eq!(
decode_profile(&noncanonical),
Err("profile encoding is not canonical".to_owned())
);
let mut duplicate = vec![1];
duplicate.extend_from_slice(&2_u32.to_be_bytes());
duplicate.extend_from_slice(&[0, 0]);
duplicate.extend_from_slice(&0_u32.to_be_bytes());
assert_eq!(
decode_profile(&duplicate),
Err("profile encoding is not canonical".to_owned())
);
let mut trailing = encode_profile(
&AuthorizationProfile::new(vec![ProfileOwner::RequestUser], vec![]).expect("profile"),
)
.expect("encoding");
trailing.push(0);
assert_eq!(
decode_profile(&trailing),
Err("trailing bytes in profile encoding".to_owned())
);
}
#[test]
fn mutation_wire_has_exact_bytes_and_round_trips_all_actions() {
let id = [7; 16];
let cases = [
ProfileMutation::Create {
owner: user(8),
profile: profile(),
},
ProfileMutation::Replace {
profile_id: ProfileId::new(tx(9)),
actor: user(8),
profile: profile(),
},
ProfileMutation::Delete {
profile_id: ProfileId::new(tx(9)),
actor: user(8),
},
];
for (index, mutation) in cases.into_iter().enumerate() {
let operation = ProfileOperation::new(id, mutation);
let encoded = encode_operation(&operation).expect("encoding");
let mut expected = vec![1, (index + 1) as u8];
expected.extend_from_slice(&id);
match operation.mutation() {
ProfileMutation::Create { owner, profile } => {
expected.extend_from_slice(owner.as_tx_id().as_bytes());
expected.extend_from_slice(&encode_profile(profile).expect("profile encoding"));
}
ProfileMutation::Replace {
profile_id,
actor,
profile,
} => {
expected.extend_from_slice(profile_id.txid().as_bytes());
expected.extend_from_slice(actor.as_tx_id().as_bytes());
expected.extend_from_slice(&encode_profile(profile).expect("profile encoding"));
}
ProfileMutation::Delete { profile_id, actor } => {
expected.extend_from_slice(profile_id.txid().as_bytes());
expected.extend_from_slice(actor.as_tx_id().as_bytes());
assert_eq!(encoded.len(), 42);
}
}
assert_eq!(encoded, expected);
assert_eq!(parse_operation(&encoded).expect("parsing"), operation);
}
}
#[test]
fn mutation_parser_preserves_validation_order_and_exact_errors() {
assert_eq!(
parse_operation(&[]),
Err("profile payload header is truncated".to_owned())
);
assert_eq!(
parse_operation(&[1]),
Err("profile payload header is truncated".to_owned())
);
assert_eq!(
parse_operation(&[2, 9]),
Err("unsupported profile payload version".to_owned())
);
assert_eq!(
parse_operation(&[1, 9]),
Err("unsupported profile payload action".to_owned())
);
assert_eq!(
parse_operation(&[1, 1]),
Err("create profile payload is truncated".to_owned())
);
assert_eq!(
parse_operation(&[1, 2]),
Err("replace profile payload is truncated".to_owned())
);
assert_eq!(
parse_operation(&[1, 3]),
Err("delete profile payload must be exactly 42 bytes".to_owned())
);
let mut create = vec![1, 1];
create.extend_from_slice(&[0; 16]);
create.extend_from_slice(tx(1).as_bytes());
create.push(2);
assert_eq!(
parse_operation(&create),
Err("unknown profile encoding version".to_owned())
);
let mut delete = vec![1, 3];
delete.extend_from_slice(&[0; 41]);
assert_eq!(
parse_operation(&delete),
Err("delete profile payload must be exactly 42 bytes".to_owned())
);
}
}