Skip to main content

kcode_k1_launch_node_codec/
lib.rs

1use std::fmt;
2
3pub use kcode_k1_access_types::{Authority, GroupId, TxId, UserId};
4pub use kcode_k1_kmap_format::NodeId;
5
6pub const PROJECTION_HEADER: &[u8; 8] = b"K1LNV3\0\0";
7
8#[derive(Clone, Debug, Eq, Hash, PartialEq)]
9pub struct TargetName(String);
10
11impl TargetName {
12    pub fn new(value: String) -> Result<Self, String> {
13        let scalars = value.chars().count();
14        if !(1..=80).contains(&scalars)
15            || value.len() > 320
16            || value.chars().all(char::is_whitespace)
17            || value.chars().any(char::is_control)
18        {
19            return Err("invalid target name".to_owned());
20        }
21        Ok(Self(value))
22    }
23
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27
28    pub fn into_string(self) -> String {
29        self.0
30    }
31}
32
33#[derive(Clone, Debug, Eq, Hash, PartialEq)]
34pub struct TargetId {
35    authority: Authority,
36    target: TargetName,
37}
38
39impl TargetId {
40    pub fn new(authority: Authority, target: TargetName) -> Self {
41        Self { authority, target }
42    }
43
44    pub fn authority(&self) -> Authority {
45        self.authority
46    }
47
48    pub fn target(&self) -> &TargetName {
49        &self.target
50    }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct SetAction {
55    operation_id: [u8; 16],
56    target: TargetId,
57    node: NodeId,
58}
59
60impl SetAction {
61    pub fn new(operation_id: [u8; 16], target: TargetId, node: NodeId) -> Self {
62        Self {
63            operation_id,
64            target,
65            node,
66        }
67    }
68
69    pub fn operation_id(&self) -> &[u8; 16] {
70        &self.operation_id
71    }
72
73    pub fn target(&self) -> &TargetId {
74        &self.target
75    }
76
77    pub fn node(&self) -> &NodeId {
78        &self.node
79    }
80
81    pub fn encode(&self) -> Vec<u8> {
82        let mut bytes = Vec::with_capacity(64);
83        bytes.extend_from_slice(&[1, 1]);
84        bytes.extend_from_slice(&self.operation_id);
85        encode_target(&mut bytes, &self.target);
86        bytes.extend_from_slice(&self.node.0);
87        bytes
88    }
89
90    pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
91        let mut decoder = Decoder::new(bytes);
92        if decoder.byte()? != 1 {
93            return Err(CodecError::UnknownVersion);
94        }
95        if decoder.byte()? != 1 {
96            return Err(CodecError::UnknownAction);
97        }
98        let operation_id = decoder.array()?;
99        let target = decoder.target()?;
100        let node = NodeId(decoder.array()?);
101        if decoder.remaining() != 0 {
102            return Err(CodecError::TrailingBytes);
103        }
104        Ok(Self::new(operation_id, target, node))
105    }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct ProjectionRecord {
110    callback: TxId,
111    target: TargetId,
112    node: NodeId,
113}
114
115impl ProjectionRecord {
116    pub fn new(callback: TxId, target: TargetId, node: NodeId) -> Self {
117        Self {
118            callback,
119            target,
120            node,
121        }
122    }
123
124    pub fn callback(&self) -> &TxId {
125        &self.callback
126    }
127
128    pub fn target(&self) -> &TargetId {
129        &self.target
130    }
131
132    pub fn node(&self) -> &NodeId {
133        &self.node
134    }
135
136    pub fn encode(&self) -> Vec<u8> {
137        let mut bytes = Vec::with_capacity(64);
138        bytes.extend_from_slice(self.callback.as_bytes());
139        encode_target(&mut bytes, &self.target);
140        bytes.extend_from_slice(&self.node.0);
141        let checksum = checksum(&bytes);
142        bytes.extend_from_slice(&checksum.to_le_bytes());
143        bytes
144    }
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
148pub enum ProjectionDecode {
149    Incomplete,
150    Complete {
151        record: ProjectionRecord,
152        consumed: usize,
153    },
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum CodecError {
158    UnknownVersion,
159    UnknownAction,
160    InvalidAuthority,
161    InvalidId,
162    InvalidUtf8,
163    InvalidTargetName,
164    Truncated,
165    TrailingBytes,
166    ChecksumMismatch,
167}
168
169impl fmt::Display for CodecError {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        write!(formatter, "{self:?}")
172    }
173}
174
175impl std::error::Error for CodecError {}
176
177pub fn decode_projection_record(bytes: &[u8]) -> Result<ProjectionDecode, CodecError> {
178    if bytes.len() < 27 {
179        return Ok(ProjectionDecode::Incomplete);
180    }
181    let target_length = u16::from_le_bytes([bytes[25], bytes[26]]) as usize;
182    if target_length > 320 {
183        return Err(CodecError::InvalidTargetName);
184    }
185    let record_length = 43 + target_length;
186    if bytes.len() < record_length {
187        return Ok(ProjectionDecode::Incomplete);
188    }
189    let body_length = record_length - 4;
190    let stored = u32::from_le_bytes([
191        bytes[body_length],
192        bytes[body_length + 1],
193        bytes[body_length + 2],
194        bytes[body_length + 3],
195    ]);
196    if checksum(&bytes[..body_length]) != stored {
197        return Err(CodecError::ChecksumMismatch);
198    }
199    let mut decoder = Decoder::new(&bytes[..body_length]);
200    let callback = decoder.tx_id()?;
201    let target = decoder.target()?;
202    let node = NodeId(decoder.array()?);
203    Ok(ProjectionDecode::Complete {
204        record: ProjectionRecord::new(callback, target, node),
205        consumed: record_length,
206    })
207}
208
209fn encode_target(bytes: &mut Vec<u8>, target: &TargetId) {
210    match target.authority() {
211        Authority::User(id) => {
212            bytes.push(1);
213            bytes.extend_from_slice(id.as_tx_id().as_bytes());
214        }
215        Authority::Group(id) => {
216            bytes.push(2);
217            bytes.extend_from_slice(id.txid().as_bytes());
218        }
219    }
220    let value = target.target().as_str().as_bytes();
221    bytes.extend_from_slice(&(value.len() as u16).to_le_bytes());
222    bytes.extend_from_slice(value);
223}
224
225fn checksum(bytes: &[u8]) -> u32 {
226    bytes.iter().fold(0x811c_9dc5, |value, byte| {
227        (value ^ u32::from(*byte)).wrapping_mul(0x0100_0193)
228    })
229}
230
231struct Decoder<'a> {
232    bytes: &'a [u8],
233    position: usize,
234}
235
236impl<'a> Decoder<'a> {
237    fn new(bytes: &'a [u8]) -> Self {
238        Self { bytes, position: 0 }
239    }
240
241    fn byte(&mut self) -> Result<u8, CodecError> {
242        Ok(self.array::<1>()?[0])
243    }
244
245    fn array<const N: usize>(&mut self) -> Result<[u8; N], CodecError> {
246        if self.remaining() < N {
247            return Err(CodecError::Truncated);
248        }
249        let mut value = [0; N];
250        value.copy_from_slice(&self.bytes[self.position..self.position + N]);
251        self.position += N;
252        Ok(value)
253    }
254
255    fn tx_id(&mut self) -> Result<TxId, CodecError> {
256        Ok(TxId::from_bytes(self.array()?))
257    }
258
259    fn target(&mut self) -> Result<TargetId, CodecError> {
260        let tag = self.byte()?;
261        let authority = match tag {
262            1 => Authority::User(UserId::from_tx_id(self.tx_id()?)),
263            2 => Authority::Group(GroupId::new(self.tx_id()?)),
264            _ => return Err(CodecError::InvalidAuthority),
265        };
266        let length = u16::from_le_bytes(self.array()?) as usize;
267        let value = self.slice(length)?;
268        let text = std::str::from_utf8(value).map_err(|_| CodecError::InvalidUtf8)?;
269        let target = TargetName::new(text.to_owned()).map_err(|_| CodecError::InvalidTargetName)?;
270        Ok(TargetId::new(authority, target))
271    }
272
273    fn slice(&mut self, length: usize) -> Result<&'a [u8], CodecError> {
274        if self.remaining() < length {
275            return Err(CodecError::Truncated);
276        }
277        let value = &self.bytes[self.position..self.position + length];
278        self.position += length;
279        Ok(value)
280    }
281
282    fn remaining(&self) -> usize {
283        self.bytes.len() - self.position
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    fn action_bytes(authority_tag: u8) -> Vec<u8> {
292        let mut bytes = vec![1, 1];
293        bytes.extend_from_slice(&[7; 16]);
294        bytes.push(authority_tag);
295        bytes.extend_from_slice(&[1; 12]);
296        bytes.extend_from_slice(&3_u16.to_le_bytes());
297        bytes.extend_from_slice(b"a/b");
298        bytes.extend_from_slice(&[2; 12]);
299        bytes
300    }
301
302    fn record_bytes() -> Vec<u8> {
303        let mut bytes = vec![3; 12];
304        bytes.push(1);
305        bytes.extend_from_slice(&[1; 12]);
306        bytes.extend_from_slice(&3_u16.to_le_bytes());
307        bytes.extend_from_slice(b"a/b");
308        bytes.extend_from_slice(&[2; 12]);
309        let sum = bytes.iter().fold(0x811c_9dc5_u32, |value, byte| {
310            (value ^ u32::from(*byte)).wrapping_mul(0x0100_0193)
311        });
312        bytes.extend_from_slice(&sum.to_le_bytes());
313        bytes
314    }
315
316    #[test]
317    fn action_has_exact_bytes_and_roundtrips() {
318        let bytes = action_bytes(1);
319        let action = SetAction::decode(&bytes).unwrap();
320        assert_eq!(action.operation_id(), &[7; 16]);
321        assert_eq!(action.target().target().as_str(), "a/b");
322        assert_eq!(action.encode(), bytes);
323    }
324
325    #[test]
326    fn user_and_group_authorities_stay_separate() {
327        let user = SetAction::decode(&action_bytes(1)).unwrap();
328        let group = SetAction::decode(&action_bytes(2)).unwrap();
329        assert!(matches!(user.target().authority(), Authority::User(_)));
330        assert!(matches!(group.target().authority(), Authority::Group(_)));
331        assert_ne!(user.encode(), group.encode());
332    }
333
334    #[test]
335    fn target_validation_preserves_accepted_text() {
336        assert!(TargetName::new(String::new()).is_err());
337        assert!(TargetName::new(" \u{2003}".to_owned()).is_err());
338        assert!(TargetName::new("a\n".to_owned()).is_err());
339        assert!(TargetName::new("a".repeat(81)).is_err());
340        assert!(TargetName::new("🦀".repeat(80)).is_ok());
341        assert_eq!("🦀".repeat(80).len(), 320);
342        assert!(TargetName::new("🦀".repeat(80) + "a").is_err());
343        let exact = " / Case / ".to_owned();
344        assert_eq!(TargetName::new(exact.clone()).unwrap().into_string(), exact);
345    }
346
347    #[test]
348    fn action_rejects_unknown_truncated_and_trailing_input() {
349        let bytes = action_bytes(1);
350        for length in 0..bytes.len() {
351            assert_eq!(
352                SetAction::decode(&bytes[..length]),
353                Err(CodecError::Truncated)
354            );
355        }
356        let mut version = bytes.clone();
357        version[0] = 2;
358        assert_eq!(SetAction::decode(&version), Err(CodecError::UnknownVersion));
359        let mut tag = bytes.clone();
360        tag[1] = 2;
361        assert_eq!(SetAction::decode(&tag), Err(CodecError::UnknownAction));
362        let mut trailing = bytes;
363        trailing.push(0);
364        assert_eq!(SetAction::decode(&trailing), Err(CodecError::TrailingBytes));
365    }
366
367    #[test]
368    fn projection_has_exact_bytes_and_roundtrips() {
369        let bytes = record_bytes();
370        let ProjectionDecode::Complete { record, consumed } =
371            decode_projection_record(&bytes).unwrap()
372        else {
373            panic!("complete record expected");
374        };
375        assert_eq!(consumed, bytes.len());
376        assert_eq!(record.target().target().as_str(), "a/b");
377        assert_eq!(record.encode(), bytes);
378    }
379
380    #[test]
381    fn projection_distinguishes_incomplete_boundaries() {
382        let bytes = record_bytes();
383        for length in [0, 1, 11, 12, 26, 27, bytes.len() - 1] {
384            assert_eq!(
385                decode_projection_record(&bytes[..length]),
386                Ok(ProjectionDecode::Incomplete)
387            );
388        }
389    }
390
391    #[test]
392    fn projection_rejects_checksum_corruption() {
393        let mut bytes = record_bytes();
394        bytes[30] ^= 1;
395        assert_eq!(
396            decode_projection_record(&bytes),
397            Err(CodecError::ChecksumMismatch)
398        );
399    }
400
401    #[test]
402    fn complete_package_stays_below_context_ceiling() {
403        let files = [
404            include_str!("../Cargo.toml"),
405            include_str!("../Documentation.md"),
406            include_str!("lib.rs"),
407        ];
408        let count = files
409            .iter()
410            .flat_map(|file| file.lines())
411            .filter(|line| !line.trim().is_empty())
412            .count();
413        assert!(count < 500, "complete nonblank line count: {count}");
414    }
415}