Skip to main content

kcode_k1_access_profile_codec/
lib.rs

1//! Canonical version 1 binary encoding for K1 authorization profiles.
2//!
3//! This crate is a stateless value codec. See `Documentation.md` for the byte
4//! layout and a usage example.
5
6pub use kcode_k1_access_profile_values::{
7    AuthorizationProfile, GroupId, ModelId, ProfileOwner, ProfileViewer, TxId, UserId,
8};
9
10/// Encodes an authorization profile in the canonical version 1 format.
11///
12/// Owners and viewers are emitted in the normalized order exposed by the
13/// profile. An error is returned if either count cannot be represented by the
14/// format's big-endian `u32` count.
15pub fn encode_profile(profile: &AuthorizationProfile) -> Result<Vec<u8>, String> {
16    let owner_count = u32::try_from(profile.owners().len())
17        .map_err(|_| "owner count exceeds canonical encoding".to_owned())?;
18    let viewer_count = u32::try_from(profile.viewers().len())
19        .map_err(|_| "viewer count exceeds canonical encoding".to_owned())?;
20    let mut bytes = Vec::new();
21    bytes.push(1);
22    bytes.extend_from_slice(&owner_count.to_be_bytes());
23    for owner in profile.owners() {
24        match owner {
25            ProfileOwner::RequestUser => bytes.push(0),
26            ProfileOwner::User(user) => {
27                bytes.push(1);
28                bytes.extend_from_slice(user.as_tx_id().as_bytes());
29            }
30            ProfileOwner::Group(group) => {
31                bytes.push(2);
32                bytes.extend_from_slice(group.txid().as_bytes());
33            }
34        }
35    }
36    bytes.extend_from_slice(&viewer_count.to_be_bytes());
37    for viewer in profile.viewers() {
38        match viewer {
39            ProfileViewer::RequestUser => bytes.push(0),
40            ProfileViewer::RequestModel => bytes.push(1),
41            ProfileViewer::User(user) => {
42                bytes.push(2);
43                bytes.extend_from_slice(user.as_tx_id().as_bytes());
44            }
45            ProfileViewer::Group(group) => {
46                bytes.push(3);
47                bytes.extend_from_slice(group.txid().as_bytes());
48            }
49            ProfileViewer::Model(model) => {
50                bytes.push(4);
51                bytes.extend_from_slice(model.as_bytes());
52            }
53        }
54    }
55    Ok(bytes)
56}
57
58/// Decodes a complete canonical version 1 authorization profile.
59///
60/// Unknown versions or tags, truncation, trailing bytes, invalid profiles, and
61/// encodings that are not in normalized canonical order are rejected.
62pub fn decode_profile(bytes: &[u8]) -> Result<AuthorizationProfile, String> {
63    let mut reader = Reader::new(bytes);
64    if reader.u8()? != 1 {
65        return Err("unknown profile encoding version".to_owned());
66    }
67    let owner_count = usize::try_from(reader.u32()?)
68        .map_err(|_| "owner count does not fit this platform".to_owned())?;
69    let mut owners = Vec::new();
70    for _ in 0..owner_count {
71        owners.push(match reader.u8()? {
72            0 => ProfileOwner::RequestUser,
73            1 => ProfileOwner::User(UserId::from_tx_id(TxId::from_bytes(reader.take()?))),
74            2 => ProfileOwner::Group(GroupId::new(TxId::from_bytes(reader.take()?))),
75            _ => return Err("unknown profile owner tag".to_owned()),
76        });
77    }
78    let viewer_count = usize::try_from(reader.u32()?)
79        .map_err(|_| "viewer count does not fit this platform".to_owned())?;
80    let mut viewers = Vec::new();
81    for _ in 0..viewer_count {
82        viewers.push(match reader.u8()? {
83            0 => ProfileViewer::RequestUser,
84            1 => ProfileViewer::RequestModel,
85            2 => ProfileViewer::User(UserId::from_tx_id(TxId::from_bytes(reader.take()?))),
86            3 => ProfileViewer::Group(GroupId::new(TxId::from_bytes(reader.take()?))),
87            4 => ProfileViewer::Model(ModelId::from_bytes(reader.take()?)),
88            _ => return Err("unknown profile viewer tag".to_owned()),
89        });
90    }
91    if !reader.finished() {
92        return Err("trailing bytes in profile encoding".to_owned());
93    }
94    let profile = AuthorizationProfile::new(owners, viewers)?;
95    if encode_profile(&profile)?.as_slice() != bytes {
96        return Err("profile encoding is not canonical".to_owned());
97    }
98    Ok(profile)
99}
100
101struct Reader<'a> {
102    bytes: &'a [u8],
103    offset: usize,
104}
105
106impl<'a> Reader<'a> {
107    const fn new(bytes: &'a [u8]) -> Self {
108        Self { bytes, offset: 0 }
109    }
110
111    fn u8(&mut self) -> Result<u8, String> {
112        Ok(self.take::<1>()?[0])
113    }
114
115    fn u32(&mut self) -> Result<u32, String> {
116        Ok(u32::from_be_bytes(self.take()?))
117    }
118
119    fn take<const N: usize>(&mut self) -> Result<[u8; N], String> {
120        let end = self
121            .offset
122            .checked_add(N)
123            .ok_or_else(|| "truncated profile encoding".to_owned())?;
124        let source = self
125            .bytes
126            .get(self.offset..end)
127            .ok_or_else(|| "truncated profile encoding".to_owned())?;
128        let mut output = [0; N];
129        output.copy_from_slice(source);
130        self.offset = end;
131        Ok(output)
132    }
133
134    fn finished(&self) -> bool {
135        self.offset == self.bytes.len()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use super::{ProfileOwner as O, ProfileViewer as V};
143
144    fn canonical_case() -> (AuthorizationProfile, Vec<u8>) {
145        let tx = |byte| TxId::from_bytes([byte; 12]);
146        let profile = AuthorizationProfile::new(
147            vec![
148                O::Group(GroupId::new(tx(3))),
149                O::RequestUser,
150                O::User(UserId::from_tx_id(tx(2))),
151            ],
152            vec![
153                V::Model(ModelId::from_bytes([5; 32])),
154                V::Group(GroupId::new(tx(4))),
155                V::RequestUser,
156                V::User(UserId::from_tx_id(tx(3))),
157                V::RequestModel,
158            ],
159        )
160        .expect("profile");
161        let bytes = [
162            &[1, 0, 0, 0, 3, 0, 1][..],
163            &[2; 12],
164            &[2],
165            &[3; 12],
166            &[0, 0, 0, 5, 0, 1, 2],
167            &[3; 12],
168            &[3],
169            &[4; 12],
170            &[4],
171            &[5; 32],
172        ]
173        .concat();
174        (profile, bytes)
175    }
176
177    #[test]
178    fn emits_the_exact_canonical_vector() {
179        let (profile, expected) = canonical_case();
180        assert_eq!(encode_profile(&profile).expect("encoding"), expected);
181    }
182
183    #[test]
184    fn canonical_vector_round_trips() {
185        let (profile, bytes) = canonical_case();
186        assert_eq!(decode_profile(&bytes).expect("decoding"), profile);
187        assert_eq!(
188            encode_profile(&decode_profile(&bytes).unwrap()).unwrap(),
189            bytes
190        );
191    }
192
193    #[test]
194    fn rejects_truncated_input() {
195        assert_eq!(
196            decode_profile(&[]).unwrap_err(),
197            "truncated profile encoding"
198        );
199        assert_eq!(
200            decode_profile(&[1, 0, 0, 0, 1, 1]).unwrap_err(),
201            "truncated profile encoding"
202        );
203    }
204
205    #[test]
206    fn rejects_unknown_version_and_tags() {
207        for (input, expected) in [
208            (vec![2], "unknown profile encoding version"),
209            (vec![1, 0, 0, 0, 1, 9], "unknown profile owner tag"),
210            (
211                vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 9],
212                "unknown profile viewer tag",
213            ),
214        ] {
215            assert_eq!(decode_profile(&input).unwrap_err(), expected);
216        }
217    }
218
219    #[test]
220    fn rejects_empty_owner_set() {
221        assert_eq!(
222            decode_profile(&[1, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap_err(),
223            "authorization profile requires at least one owner"
224        );
225    }
226
227    #[test]
228    fn rejects_noncanonical_order_and_duplicates() {
229        let out_of_order = [&[1, 0, 0, 0, 2, 2][..], &[1; 12], &[0, 0, 0, 0, 0]].concat();
230        let duplicate = vec![1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0];
231        for input in [out_of_order, duplicate] {
232            assert_eq!(
233                decode_profile(&input).unwrap_err(),
234                "profile encoding is not canonical"
235            );
236        }
237    }
238
239    #[test]
240    fn rejects_trailing_bytes() {
241        assert_eq!(
242            decode_profile(&[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]).unwrap_err(),
243            "trailing bytes in profile encoding"
244        );
245    }
246}