Skip to main content

leodos_protocols/misc/sle/
cltu.rs

1//! CLTU (Forward Command Link Transmission Unit) service PDUs.
2//!
3//! CLTU is the SLE service for sending uplink command frames
4//! through a ground station. The client binds, starts the
5//! service, then sends CLTU data which the ground station
6//! radiates to the spacecraft.
7
8use super::ber::{BerReader, BerWriter, Class, tags};
9use super::isp1::Credentials;
10use super::types::{ServiceType, SleError};
11
12/// Maximum length of an initiator/responder identifier string.
13const MAX_ID_LEN: usize = 64;
14
15/// Maximum CLTU data size.
16const MAX_CLTU_DATA: usize = 2048;
17
18/// CLTU operation tags (context-specific CHOICE tags).
19#[derive(Copy, Clone, Debug, PartialEq, Eq)]
20#[repr(u8)]
21pub enum CltuOp {
22    /// Bind invocation.
23    Bind = 0,
24    /// Bind return.
25    BindReturn = 1,
26    /// Unbind invocation.
27    Unbind = 2,
28    /// Unbind return.
29    UnbindReturn = 3,
30    /// Start invocation.
31    Start = 4,
32    /// Start return.
33    StartReturn = 5,
34    /// Stop invocation.
35    Stop = 6,
36    /// Stop return.
37    StopReturn = 7,
38    /// Transfer data invocation.
39    TransferData = 8,
40    /// Transfer data return.
41    TransferDataReturn = 9,
42}
43
44/// Result of CLTU radiation.
45#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46#[repr(u8)]
47pub enum CltuStatus {
48    /// CLTU was successfully radiated.
49    Radiated = 0,
50    /// CLTU expired before it could be radiated.
51    Expired = 1,
52    /// Radiation was interrupted.
53    Interrupted = 2,
54    /// Production has not been started.
55    ProductionNotStarted = 3,
56}
57
58impl CltuStatus {
59    /// Converts from an integer value.
60    pub fn from_i64(v: i64) -> Result<Self, SleError> {
61        match v {
62            0 => Ok(Self::Radiated),
63            1 => Ok(Self::Expired),
64            2 => Ok(Self::Interrupted),
65            3 => Ok(Self::ProductionNotStarted),
66            _ => Err(SleError::InvalidEnumValue),
67        }
68    }
69}
70
71/// CLTU Bind invocation PDU.
72#[derive(Clone, Debug)]
73pub struct CltuBindInvocation {
74    /// Identifier of the initiator (client).
75    pub initiator_id: [u8; MAX_ID_LEN],
76    /// Length of initiator_id.
77    pub initiator_id_len: usize,
78    /// Identifier of the responder (ground station).
79    pub responder_id: [u8; MAX_ID_LEN],
80    /// Length of responder_id.
81    pub responder_id_len: usize,
82    /// Service type (should be FCltu).
83    pub service_type: ServiceType,
84    /// Protocol version number.
85    pub version: u16,
86    /// Optional authentication credentials.
87    pub credentials: Option<Credentials>,
88}
89
90impl CltuBindInvocation {
91    /// Creates a new CLTU bind invocation.
92    pub fn new(
93        initiator_id: &[u8],
94        responder_id: &[u8],
95        version: u16,
96        credentials: Option<Credentials>,
97    ) -> Result<Self, SleError> {
98        if initiator_id.len() > MAX_ID_LEN
99            || responder_id.len() > MAX_ID_LEN
100        {
101            return Err(SleError::TooLong);
102        }
103        let mut init = [0u8; MAX_ID_LEN];
104        init[..initiator_id.len()]
105            .copy_from_slice(initiator_id);
106        let mut resp = [0u8; MAX_ID_LEN];
107        resp[..responder_id.len()]
108            .copy_from_slice(responder_id);
109        Ok(Self {
110            initiator_id: init,
111            initiator_id_len: initiator_id.len(),
112            responder_id: resp,
113            responder_id_len: responder_id.len(),
114            service_type: ServiceType::FCltu,
115            version,
116            credentials,
117        })
118    }
119
120    /// Returns the initiator identifier as a byte slice.
121    pub fn initiator_id(&self) -> &[u8] {
122        &self.initiator_id[..self.initiator_id_len]
123    }
124
125    /// Returns the responder identifier as a byte slice.
126    pub fn responder_id(&self) -> &[u8] {
127        &self.responder_id[..self.responder_id_len]
128    }
129
130    /// Encodes this PDU into `buf`.
131    pub fn encode(
132        &self,
133        buf: &mut [u8],
134    ) -> Result<usize, SleError> {
135        let mut w = BerWriter::new(buf);
136        let outer = w.begin_context(
137            CltuOp::Bind as u8,
138            true,
139        )?;
140        let seq = w.begin_sequence()?;
141
142        match &self.credentials {
143            None => w.write_null()?,
144            Some(cred) => {
145                let cred_seq = w.begin_sequence()?;
146                w.write_octet_string(&cred.time)?;
147                w.write_integer(cred.random as i64)?;
148                w.write_octet_string(&cred.hash)?;
149                w.end_sequence(cred_seq)?;
150            }
151        }
152
153        w.write_octet_string(self.initiator_id())?;
154        w.write_octet_string(self.responder_id())?;
155        w.write_enum(self.service_type as i64)?;
156        w.write_integer(self.version as i64)?;
157
158        w.end_sequence(seq)?;
159        w.end_sequence(outer)?;
160        Ok(w.len())
161    }
162
163    /// Decodes a CLTU Bind invocation from BER bytes.
164    pub fn decode(buf: &[u8]) -> Result<Self, SleError> {
165        let mut r = BerReader::new(buf);
166
167        let (tag, _len) = r.read_context_tag()?;
168        if tag != CltuOp::Bind as u8 {
169            return Err(SleError::UnexpectedTag);
170        }
171
172        let _seq_len = r.read_sequence()?;
173
174        let (peek_tag, peek_class, _) = r.peek_tag()?;
175        let credentials = if peek_tag == tags::NULL
176            && peek_class == Class::Universal
177        {
178            r.read_null()?;
179            None
180        } else {
181            let _cred_len = r.read_sequence()?;
182            let time_bytes = r.read_octet_string()?;
183            let random = r.read_integer()? as u32;
184            let hash_bytes = r.read_octet_string()?;
185            let mut time = [0u8; 8];
186            time.copy_from_slice(
187                time_bytes.get(..8).ok_or(SleError::Truncated)?,
188            );
189            let mut hash = [0u8; 20];
190            hash.copy_from_slice(
191                hash_bytes
192                    .get(..20)
193                    .ok_or(SleError::Truncated)?,
194            );
195            Some(Credentials { time, random, hash })
196        };
197
198        let initiator = r.read_octet_string()?;
199        let responder = r.read_octet_string()?;
200        let _service_type =
201            ServiceType::from_u8(r.read_enum()? as u8)?;
202        let version = r.read_integer()? as u16;
203
204        Self::new(initiator, responder, version, credentials)
205    }
206}
207
208/// CLTU Start invocation PDU.
209///
210/// Starts CLTU production at the ground station so CLTUs
211/// can be sent for radiation.
212#[derive(Copy, Clone, Debug, PartialEq, Eq)]
213pub struct CltuStartInvocation {
214    /// Optional authentication credentials.
215    pub credentials: Option<Credentials>,
216    /// Invocation identifier (sequence number).
217    pub invoke_id: u16,
218    /// First CLTU identifier to be accepted.
219    pub first_cltu_id: u32,
220}
221
222impl CltuStartInvocation {
223    /// Encodes this PDU into `buf`.
224    pub fn encode(
225        &self,
226        buf: &mut [u8],
227    ) -> Result<usize, SleError> {
228        let mut w = BerWriter::new(buf);
229        let outer = w.begin_context(
230            CltuOp::Start as u8,
231            true,
232        )?;
233        let seq = w.begin_sequence()?;
234
235        match &self.credentials {
236            None => w.write_null()?,
237            Some(cred) => {
238                let cred_seq = w.begin_sequence()?;
239                w.write_octet_string(&cred.time)?;
240                w.write_integer(cred.random as i64)?;
241                w.write_octet_string(&cred.hash)?;
242                w.end_sequence(cred_seq)?;
243            }
244        }
245
246        w.write_integer(self.invoke_id as i64)?;
247        w.write_integer(self.first_cltu_id as i64)?;
248
249        w.end_sequence(seq)?;
250        w.end_sequence(outer)?;
251        Ok(w.len())
252    }
253
254    /// Decodes a CLTU Start invocation from BER bytes.
255    pub fn decode(buf: &[u8]) -> Result<Self, SleError> {
256        let mut r = BerReader::new(buf);
257
258        let (tag, _len) = r.read_context_tag()?;
259        if tag != CltuOp::Start as u8 {
260            return Err(SleError::UnexpectedTag);
261        }
262
263        let _seq_len = r.read_sequence()?;
264
265        let (peek_tag, peek_class, _) = r.peek_tag()?;
266        let credentials = if peek_tag == tags::NULL
267            && peek_class == Class::Universal
268        {
269            r.read_null()?;
270            None
271        } else {
272            let _cred_len = r.read_sequence()?;
273            let time_bytes = r.read_octet_string()?;
274            let random = r.read_integer()? as u32;
275            let hash_bytes = r.read_octet_string()?;
276            let mut time = [0u8; 8];
277            time.copy_from_slice(
278                time_bytes.get(..8).ok_or(SleError::Truncated)?,
279            );
280            let mut hash = [0u8; 20];
281            hash.copy_from_slice(
282                hash_bytes
283                    .get(..20)
284                    .ok_or(SleError::Truncated)?,
285            );
286            Some(Credentials { time, random, hash })
287        };
288
289        let invoke_id = r.read_integer()? as u16;
290        let first_cltu_id = r.read_integer()? as u32;
291
292        Ok(Self {
293            credentials,
294            invoke_id,
295            first_cltu_id,
296        })
297    }
298}
299
300/// CLTU Transfer Data invocation — sends a CLTU for radiation.
301#[derive(Clone, Debug)]
302pub struct CltuTransferDataInvocation {
303    /// CLTU identifier (sequence number).
304    pub cltu_id: u32,
305    /// The CLTU data to be radiated.
306    data_buf: [u8; MAX_CLTU_DATA],
307    /// Actual data length.
308    data_len: usize,
309    /// Optional authentication credentials.
310    pub credentials: Option<Credentials>,
311}
312
313impl CltuTransferDataInvocation {
314    /// Creates a new CLTU transfer data invocation.
315    pub fn new(
316        cltu_id: u32,
317        data: &[u8],
318        credentials: Option<Credentials>,
319    ) -> Result<Self, SleError> {
320        if data.len() > MAX_CLTU_DATA {
321            return Err(SleError::TooLong);
322        }
323        let mut data_buf = [0u8; MAX_CLTU_DATA];
324        data_buf[..data.len()].copy_from_slice(data);
325        Ok(Self {
326            cltu_id,
327            data_buf,
328            data_len: data.len(),
329            credentials,
330        })
331    }
332
333    /// Returns the CLTU data.
334    pub fn data(&self) -> &[u8] {
335        &self.data_buf[..self.data_len]
336    }
337
338    /// Encodes this PDU into `buf`.
339    pub fn encode(
340        &self,
341        buf: &mut [u8],
342    ) -> Result<usize, SleError> {
343        let mut w = BerWriter::new(buf);
344        let outer = w.begin_context(
345            CltuOp::TransferData as u8,
346            true,
347        )?;
348        let seq = w.begin_sequence()?;
349
350        match &self.credentials {
351            None => w.write_null()?,
352            Some(cred) => {
353                let cred_seq = w.begin_sequence()?;
354                w.write_octet_string(&cred.time)?;
355                w.write_integer(cred.random as i64)?;
356                w.write_octet_string(&cred.hash)?;
357                w.end_sequence(cred_seq)?;
358            }
359        }
360
361        w.write_integer(self.cltu_id as i64)?;
362        w.write_octet_string(self.data())?;
363
364        w.end_sequence(seq)?;
365        w.end_sequence(outer)?;
366        Ok(w.len())
367    }
368
369    /// Decodes a CLTU transfer data invocation from BER bytes.
370    pub fn decode(buf: &[u8]) -> Result<Self, SleError> {
371        let mut r = BerReader::new(buf);
372
373        let (tag, _len) = r.read_context_tag()?;
374        if tag != CltuOp::TransferData as u8 {
375            return Err(SleError::UnexpectedTag);
376        }
377
378        let _seq_len = r.read_sequence()?;
379
380        let (peek_tag, peek_class, _) = r.peek_tag()?;
381        let credentials = if peek_tag == tags::NULL
382            && peek_class == Class::Universal
383        {
384            r.read_null()?;
385            None
386        } else {
387            let _cred_len = r.read_sequence()?;
388            let time_bytes = r.read_octet_string()?;
389            let random = r.read_integer()? as u32;
390            let hash_bytes = r.read_octet_string()?;
391            let mut time = [0u8; 8];
392            time.copy_from_slice(
393                time_bytes.get(..8).ok_or(SleError::Truncated)?,
394            );
395            let mut hash = [0u8; 20];
396            hash.copy_from_slice(
397                hash_bytes
398                    .get(..20)
399                    .ok_or(SleError::Truncated)?,
400            );
401            Some(Credentials { time, random, hash })
402        };
403
404        let cltu_id = r.read_integer()? as u32;
405        let data = r.read_octet_string()?;
406
407        Self::new(cltu_id, data, credentials)
408    }
409}
410
411/// CLTU Transfer Data return — acknowledgement from provider.
412#[derive(Copy, Clone, Debug, PartialEq, Eq)]
413pub struct CltuTransferDataReturn {
414    /// CLTU identifier this return refers to.
415    pub cltu_id: u32,
416    /// Status of the CLTU.
417    pub status: CltuStatus,
418    /// Optional authentication credentials.
419    pub credentials: Option<Credentials>,
420}
421
422impl CltuTransferDataReturn {
423    /// Encodes this PDU into `buf`.
424    pub fn encode(
425        &self,
426        buf: &mut [u8],
427    ) -> Result<usize, SleError> {
428        let mut w = BerWriter::new(buf);
429        let outer = w.begin_context(
430            CltuOp::TransferDataReturn as u8,
431            true,
432        )?;
433        let seq = w.begin_sequence()?;
434
435        match &self.credentials {
436            None => w.write_null()?,
437            Some(cred) => {
438                let cred_seq = w.begin_sequence()?;
439                w.write_octet_string(&cred.time)?;
440                w.write_integer(cred.random as i64)?;
441                w.write_octet_string(&cred.hash)?;
442                w.end_sequence(cred_seq)?;
443            }
444        }
445
446        w.write_integer(self.cltu_id as i64)?;
447        w.write_enum(self.status as i64)?;
448
449        w.end_sequence(seq)?;
450        w.end_sequence(outer)?;
451        Ok(w.len())
452    }
453
454    /// Decodes a CLTU transfer data return from BER bytes.
455    pub fn decode(buf: &[u8]) -> Result<Self, SleError> {
456        let mut r = BerReader::new(buf);
457
458        let (tag, _len) = r.read_context_tag()?;
459        if tag != CltuOp::TransferDataReturn as u8 {
460            return Err(SleError::UnexpectedTag);
461        }
462
463        let _seq_len = r.read_sequence()?;
464
465        let (peek_tag, peek_class, _) = r.peek_tag()?;
466        let credentials = if peek_tag == tags::NULL
467            && peek_class == Class::Universal
468        {
469            r.read_null()?;
470            None
471        } else {
472            let _cred_len = r.read_sequence()?;
473            let time_bytes = r.read_octet_string()?;
474            let random = r.read_integer()? as u32;
475            let hash_bytes = r.read_octet_string()?;
476            let mut time = [0u8; 8];
477            time.copy_from_slice(
478                time_bytes.get(..8).ok_or(SleError::Truncated)?,
479            );
480            let mut hash = [0u8; 20];
481            hash.copy_from_slice(
482                hash_bytes
483                    .get(..20)
484                    .ok_or(SleError::Truncated)?,
485            );
486            Some(Credentials { time, random, hash })
487        };
488
489        let cltu_id = r.read_integer()? as u32;
490        let status =
491            CltuStatus::from_i64(r.read_enum()?)?;
492
493        Ok(Self {
494            cltu_id,
495            status,
496            credentials,
497        })
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn cltu_bind_roundtrip() {
507        let bind = CltuBindInvocation::new(
508            b"operator",
509            b"antenna-1",
510            3,
511            None,
512        )
513        .unwrap();
514
515        let mut buf = [0u8; 256];
516        let n = bind.encode(&mut buf).unwrap();
517
518        let decoded =
519            CltuBindInvocation::decode(&buf[..n]).unwrap();
520        assert_eq!(decoded.initiator_id(), b"operator");
521        assert_eq!(decoded.responder_id(), b"antenna-1");
522        assert_eq!(decoded.service_type, ServiceType::FCltu);
523        assert_eq!(decoded.version, 3);
524        assert!(decoded.credentials.is_none());
525    }
526
527    #[test]
528    fn cltu_start_roundtrip() {
529        let start = CltuStartInvocation {
530            credentials: None,
531            invoke_id: 1,
532            first_cltu_id: 100,
533        };
534
535        let mut buf = [0u8; 128];
536        let n = start.encode(&mut buf).unwrap();
537
538        let decoded =
539            CltuStartInvocation::decode(&buf[..n]).unwrap();
540        assert_eq!(decoded.invoke_id, 1);
541        assert_eq!(decoded.first_cltu_id, 100);
542        assert!(decoded.credentials.is_none());
543    }
544
545    #[test]
546    fn cltu_transfer_data_roundtrip() {
547        let cltu_data = [0x01, 0x02, 0x03, 0x04, 0x05];
548        let td = CltuTransferDataInvocation::new(
549            42,
550            &cltu_data,
551            None,
552        )
553        .unwrap();
554
555        let mut buf = [0u8; 128];
556        let n = td.encode(&mut buf).unwrap();
557
558        let decoded =
559            CltuTransferDataInvocation::decode(&buf[..n])
560                .unwrap();
561        assert_eq!(decoded.cltu_id, 42);
562        assert_eq!(decoded.data(), &cltu_data);
563        assert!(decoded.credentials.is_none());
564    }
565
566    #[test]
567    fn cltu_transfer_data_return_roundtrip() {
568        let ret = CltuTransferDataReturn {
569            cltu_id: 42,
570            status: CltuStatus::Radiated,
571            credentials: None,
572        };
573
574        let mut buf = [0u8; 64];
575        let n = ret.encode(&mut buf).unwrap();
576
577        let decoded =
578            CltuTransferDataReturn::decode(&buf[..n]).unwrap();
579        assert_eq!(decoded.cltu_id, 42);
580        assert_eq!(decoded.status, CltuStatus::Radiated);
581        assert!(decoded.credentials.is_none());
582    }
583}