kcode_k1_launch_node_codec/
lib.rs1use 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}