1use super::gsm::tbcd_encode;
21use serde_json::{json, Value};
22
23pub const MSG_UDT: u8 = 0x09;
24pub const MSG_UDTS: u8 = 0x0a;
25pub const MSG_XUDT: u8 = 0x11;
26pub const MSG_XUDTS: u8 = 0x12;
27pub const MSG_LUDT: u8 = 0x13;
28pub const MSG_LUDTS: u8 = 0x14;
29
30pub const PARAM_END_OF_OPTIONAL: u8 = 0x00;
32pub const PARAM_SEGMENTATION: u8 = 0x10;
33pub const PARAM_IMPORTANCE: u8 = 0x12;
34
35pub const CAUSE_NO_TRANSLATION_FOR_NATURE: u8 = 0x00;
37pub const CAUSE_NO_TRANSLATION_FOR_ADDRESS: u8 = 0x01;
38pub const CAUSE_SUBSYSTEM_FAILURE: u8 = 0x03;
39pub const CAUSE_UNEQUIPPED_USER: u8 = 0x04;
40pub const CAUSE_UNQUALIFIED: u8 = 0x07;
41pub const CAUSE_ERROR_IN_LOCAL_PROCESSING: u8 = 0x09;
42pub const CAUSE_SEGMENTATION_NOT_SUPPORTED: u8 = 0x0d;
43
44pub const SSN_SCMG: u8 = 1;
46pub const SCMG_SSA: u8 = 0x01;
48pub const SCMG_SSP: u8 = 0x02;
49pub const SCMG_SST: u8 = 0x03;
50
51pub const MAX_DATA_LEN: usize = 255;
53pub const DEFAULT_HOP_COUNTER: u8 = 15;
55
56fn err<T>(msg: impl Into<String>) -> Result<T, String> {
57 Err(msg.into())
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct GlobalTitle {
63 pub gti: u8,
65 pub translation_type: u8,
66 pub numbering_plan: u8,
68 pub nature: u8,
70 pub digits: String,
71}
72
73impl GlobalTitle {
74 pub fn e164(digits: &str) -> Self {
75 GlobalTitle {
76 gti: 4,
77 translation_type: 0,
78 numbering_plan: 1,
79 nature: 4,
80 digits: digits.to_string(),
81 }
82 }
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct SccpAddress {
88 pub route_on_gt: bool,
90 pub point_code: Option<u16>,
91 pub ssn: Option<u8>,
92 pub global_title: Option<GlobalTitle>,
93 pub national: bool,
95}
96
97impl SccpAddress {
98 pub fn decode(o: &[u8]) -> Result<Self, String> {
99 let Some((&ai, mut rest)) = o.split_first() else {
100 return err("empty SCCP address");
101 };
102 let pc_present = ai & 0x01 != 0;
103 let ssn_present = ai & 0x02 != 0;
104 let gti = (ai >> 2) & 0x0f;
105 let route_on_gt = ai & 0x40 == 0;
106 let point_code = if pc_present {
107 if rest.len() < 2 {
108 return err("SCCP address indicates a point code but has no room for it");
109 }
110 let pc = u16::from_le_bytes([rest[0], rest[1]]) & 0x3fff;
111 rest = &rest[2..];
112 Some(pc)
113 } else {
114 None
115 };
116 let ssn = if ssn_present {
117 let Some((&s, r)) = rest.split_first() else {
118 return err("SCCP address indicates an SSN but has no room for it");
119 };
120 rest = r;
121 Some(s)
122 } else {
123 None
124 };
125 let global_title = match gti {
126 0 => None,
127 1 => {
128 let Some((&b, digits)) = rest.split_first() else {
129 return err("GTI 1 global title is empty");
130 };
131 let mut d = bcd_digits(digits);
132 if b & 0x80 != 0 {
133 d.pop();
134 }
135 Some(GlobalTitle {
136 gti,
137 translation_type: 0,
138 numbering_plan: 0,
139 nature: b & 0x7f,
140 digits: d,
141 })
142 }
143 2 => {
144 let Some((&tt, digits)) = rest.split_first() else {
145 return err("GTI 2 global title is empty");
146 };
147 Some(GlobalTitle {
148 gti,
149 translation_type: tt,
150 numbering_plan: 0,
151 nature: 0,
152 digits: bcd_digits(digits),
153 })
154 }
155 3 | 4 => {
156 let need = if gti == 4 { 3 } else { 2 };
157 if rest.len() < need {
158 return err(format!("GTI {gti} global title shorter than its header"));
159 }
160 let tt = rest[0];
161 let np = rest[1] >> 4;
162 let es = rest[1] & 0x0f;
163 let nature = if gti == 4 { rest[2] & 0x7f } else { 0 };
164 let digits = &rest[need..];
165 let mut d = bcd_digits(digits);
166 if es == 1 && !d.is_empty() {
169 d.pop();
170 } else if es != 1 && es != 2 {
171 return err(format!(
172 "GTI {gti} global title uses encoding scheme {es}; only BCD (1, 2) is \
173 implemented"
174 ));
175 }
176 Some(GlobalTitle {
177 gti,
178 translation_type: tt,
179 numbering_plan: np,
180 nature,
181 digits: d,
182 })
183 }
184 other => return err(format!("global title indicator {other} is reserved")),
185 };
186 if route_on_gt && global_title.is_none() {
187 return err("SCCP address routes on global title but carries none");
188 }
189 Ok(SccpAddress {
190 route_on_gt,
191 point_code,
192 ssn,
193 global_title,
194 national: ai & 0x80 != 0,
195 })
196 }
197
198 pub fn encode(&self) -> Result<Vec<u8>, String> {
199 let gti = self.global_title.as_ref().map(|g| g.gti).unwrap_or(0);
200 let mut ai = (gti & 0x0f) << 2;
201 if self.point_code.is_some() {
202 ai |= 0x01;
203 }
204 if self.ssn.is_some() {
205 ai |= 0x02;
206 }
207 if !self.route_on_gt {
208 ai |= 0x40;
209 }
210 if self.national {
211 ai |= 0x80;
212 }
213 if self.route_on_gt && self.global_title.is_none() {
214 return err("an address that routes on global title needs one");
215 }
216 if !self.route_on_gt && self.ssn.is_none() {
217 return err("an address that routes on SSN needs an ssn");
218 }
219 let mut out = vec![ai];
220 if let Some(pc) = self.point_code {
221 if pc > 0x3fff {
222 return err(format!("point code {pc} does not fit 14 bits"));
223 }
224 out.extend(pc.to_le_bytes());
225 }
226 if let Some(ssn) = self.ssn {
227 out.push(ssn);
228 }
229 if let Some(g) = &self.global_title {
230 if g.digits.is_empty() || g.digits.len() > 32 {
231 return err("a global title needs 1..32 digits");
232 }
233 let odd = g.digits.len() % 2 == 1;
234 let digits = tbcd_encode(&g.digits)?;
235 let digits: Vec<u8> = if odd {
237 let mut d = digits;
238 if let Some(last) = d.last_mut() {
239 *last &= 0x0f;
240 }
241 d
242 } else {
243 digits
244 };
245 match g.gti {
246 1 => {
247 out.push((odd as u8) << 7 | (g.nature & 0x7f));
248 }
249 2 => out.push(g.translation_type),
250 3 => {
251 out.push(g.translation_type);
252 out.push(g.numbering_plan << 4 | if odd { 1 } else { 2 });
253 }
254 4 => {
255 out.push(g.translation_type);
256 out.push(g.numbering_plan << 4 | if odd { 1 } else { 2 });
257 out.push(g.nature & 0x7f);
258 }
259 other => return err(format!("global title indicator {other} is not 1..4")),
260 }
261 out.extend(digits);
262 }
263 Ok(out)
264 }
265
266 pub fn to_json(&self) -> Value {
267 let mut v = json!({
268 "route_on": if self.route_on_gt { "gt" } else { "ssn" },
269 "pc": self.point_code,
270 "ssn": self.ssn,
271 });
272 if let Some(g) = &self.global_title {
273 v["gt"] = json!({
274 "digits": g.digits,
275 "gti": g.gti,
276 "tt": g.translation_type,
277 "np": g.numbering_plan,
278 "nai": g.nature,
279 });
280 }
281 v
282 }
283
284 pub fn from_json(v: &Value, field: &str) -> Result<Self, String> {
286 let o = v
287 .as_object()
288 .ok_or_else(|| format!("{field} must be an object {{route_on, pc, ssn, gt}}"))?;
289 let num = |k: &str, max: u64| -> Result<Option<u64>, String> {
290 match o.get(k) {
291 None | Some(Value::Null) => Ok(None),
292 Some(x) => x
293 .as_u64()
294 .filter(|n| *n <= max)
295 .map(Some)
296 .ok_or_else(|| format!("{field}.{k} must be an integer 0..{max}")),
297 }
298 };
299 let global_title = match o.get("gt") {
300 None | Some(Value::Null) => None,
301 Some(Value::String(s)) => Some(GlobalTitle::e164(s.trim_start_matches('+'))),
302 Some(Value::Object(g)) => {
303 let get = |k: &str, d: u8, max: u8| -> Result<u8, String> {
304 match g.get(k) {
305 None => Ok(d),
306 Some(x) => x
307 .as_u64()
308 .filter(|n| *n <= max as u64)
309 .map(|n| n as u8)
310 .ok_or_else(|| format!("{field}.gt.{k} must be 0..{max}")),
311 }
312 };
313 Some(GlobalTitle {
314 gti: get("gti", 4, 4)?.max(1),
315 translation_type: get("tt", 0, 255)?,
316 numbering_plan: get("np", 1, 15)?,
317 nature: get("nai", 4, 127)?,
318 digits: g
319 .get("digits")
320 .and_then(Value::as_str)
321 .ok_or_else(|| format!("{field}.gt.digits is required"))?
322 .trim_start_matches('+')
323 .to_string(),
324 })
325 }
326 Some(_) => return Err(format!("{field}.gt must be digits or an object")),
327 };
328 let route_on_gt = match o.get("route_on").and_then(Value::as_str) {
329 Some("gt") => true,
330 Some("ssn") => false,
331 Some(other) => return Err(format!("{field}.route_on {other:?} is not gt or ssn")),
332 None => global_title.is_some() && o.get("pc").is_none(),
333 };
334 let a = SccpAddress {
335 route_on_gt,
336 point_code: num("pc", 0x3fff)?.map(|n| n as u16),
337 ssn: num("ssn", 255)?.map(|n| n as u8),
338 global_title,
339 national: false,
340 };
341 a.encode()?;
342 Ok(a)
343 }
344}
345
346fn bcd_digits(o: &[u8]) -> String {
348 let mut out = String::new();
349 for b in o {
350 for n in [b & 0x0f, b >> 4] {
351 out.push(match n {
352 0..=9 => (b'0' + n) as char,
353 0xb => '*',
354 0xc => '#',
355 0xa => 'a',
356 0xd => 'b',
357 0xe => 'c',
358 _ => 'f',
359 });
360 }
361 }
362 out
363}
364
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub struct Segmentation {
368 pub first: bool,
369 pub remaining: u8,
370 pub local_reference: u32,
371}
372
373impl Segmentation {
374 pub fn is_whole_message(&self) -> bool {
376 self.first && self.remaining == 0
377 }
378}
379
380#[derive(Clone, Debug, PartialEq, Eq)]
382pub struct Unitdata {
383 pub message_type: u8,
384 pub protocol_class: u8,
386 pub return_on_error: bool,
388 pub return_cause: Option<u8>,
390 pub hop_counter: Option<u8>,
391 pub called: SccpAddress,
392 pub calling: SccpAddress,
393 pub data: Vec<u8>,
394 pub segmentation: Option<Segmentation>,
395 pub importance: Option<u8>,
396}
397
398impl Unitdata {
399 pub fn is_service_message(&self) -> bool {
400 matches!(self.message_type, MSG_UDTS | MSG_XUDTS)
401 }
402
403 pub fn type_name(&self) -> &'static str {
404 message_type_name(self.message_type)
405 }
406}
407
408pub fn message_type_name(t: u8) -> &'static str {
409 match t {
410 MSG_UDT => "UDT",
411 MSG_UDTS => "UDTS",
412 MSG_XUDT => "XUDT",
413 MSG_XUDTS => "XUDTS",
414 MSG_LUDT => "LUDT",
415 MSG_LUDTS => "LUDTS",
416 0x01 => "CR",
417 0x02 => "CC",
418 0x03 => "CREF",
419 0x04 => "RLSD",
420 0x05 => "RLC",
421 0x06 => "DT1",
422 _ => "unknown",
423 }
424}
425
426fn variable(o: &[u8], at: usize, name: &str) -> Result<Vec<u8>, String> {
428 let ptr = *o
429 .get(at)
430 .ok_or_else(|| format!("SCCP message ends before the {name} pointer"))?
431 as usize;
432 if ptr == 0 {
433 return err(format!("SCCP {name} pointer is zero"));
434 }
435 let start = at + ptr;
436 let len = *o
437 .get(start)
438 .ok_or_else(|| format!("SCCP {name} pointer leads past the end"))? as usize;
439 o.get(start + 1..start + 1 + len)
440 .map(|s| s.to_vec())
441 .ok_or_else(|| format!("SCCP {name} of {len} octets runs past the end"))
442}
443
444pub fn decode(o: &[u8]) -> Result<Unitdata, String> {
446 let Some(&mt) = o.first() else {
447 return err("empty SCCP message");
448 };
449 let (class_octet, cause, hop, ptr_base, has_optional) = match mt {
450 MSG_UDT => (
451 Some(*o.get(1).ok_or("UDT without protocol class")?),
452 None,
453 None,
454 2,
455 false,
456 ),
457 MSG_UDTS => (
458 None,
459 Some(*o.get(1).ok_or("UDTS without return cause")?),
460 None,
461 2,
462 false,
463 ),
464 MSG_XUDT => (
465 Some(*o.get(1).ok_or("XUDT without protocol class")?),
466 None,
467 Some(*o.get(2).ok_or("XUDT without hop counter")?),
468 3,
469 true,
470 ),
471 MSG_XUDTS => (
472 None,
473 Some(*o.get(1).ok_or("XUDTS without return cause")?),
474 Some(*o.get(2).ok_or("XUDTS without hop counter")?),
475 3,
476 true,
477 ),
478 other => {
479 return err(format!(
480 "SCCP {} (message type 0x{other:02x}) is not implemented; NetGet handles UDT, \
481 XUDT, UDTS and XUDTS",
482 message_type_name(other)
483 ))
484 }
485 };
486 let called = SccpAddress::decode(&variable(o, ptr_base, "called party address")?)?;
487 let calling = SccpAddress::decode(&variable(o, ptr_base + 1, "calling party address")?)?;
488 let data = variable(o, ptr_base + 2, "data")?;
489 let mut segmentation = None;
490 let mut importance = None;
491 if has_optional {
492 let ptr = *o
493 .get(ptr_base + 3)
494 .ok_or("XUDT ends before its optional pointer")? as usize;
495 if ptr != 0 {
496 let mut pos = ptr_base + 3 + ptr;
497 for _ in 0..16 {
499 let Some(&code) = o.get(pos) else {
500 return err("XUDT optional part ends without end-of-optional-parameters");
501 };
502 if code == PARAM_END_OF_OPTIONAL {
503 break;
504 }
505 let len = *o
506 .get(pos + 1)
507 .ok_or("XUDT optional parameter without length")?
508 as usize;
509 let v = o
510 .get(pos + 2..pos + 2 + len)
511 .ok_or("XUDT optional parameter runs past the end")?;
512 match code {
513 PARAM_SEGMENTATION if len == 4 => {
514 segmentation = Some(Segmentation {
515 first: v[0] & 0x80 != 0,
516 remaining: v[0] & 0x0f,
517 local_reference: u32::from_be_bytes([0, v[1], v[2], v[3]]),
518 });
519 }
520 PARAM_IMPORTANCE if len == 1 => importance = Some(v[0] & 0x07),
521 _ => {}
522 }
523 pos += 2 + len;
524 }
525 }
526 }
527 let class = class_octet.unwrap_or(0);
528 Ok(Unitdata {
529 message_type: mt,
530 protocol_class: class & 0x0f,
531 return_on_error: class & 0x80 != 0,
532 return_cause: cause,
533 hop_counter: hop,
534 called,
535 calling,
536 data,
537 segmentation,
538 importance,
539 })
540}
541
542pub fn encode_udt(
544 protocol_class: u8,
545 return_on_error: bool,
546 called: &SccpAddress,
547 calling: &SccpAddress,
548 data: &[u8],
549) -> Result<Vec<u8>, String> {
550 let class = (protocol_class & 0x01) | if return_on_error { 0x80 } else { 0 };
551 encode_unitdata(MSG_UDT, &[class], called, calling, data, false)
552}
553
554pub fn encode_xudt(
556 protocol_class: u8,
557 return_on_error: bool,
558 hop_counter: u8,
559 called: &SccpAddress,
560 calling: &SccpAddress,
561 data: &[u8],
562) -> Result<Vec<u8>, String> {
563 let class = (protocol_class & 0x01) | if return_on_error { 0x80 } else { 0 };
564 encode_unitdata(
565 MSG_XUDT,
566 &[class, hop_counter.clamp(1, 15)],
567 called,
568 calling,
569 data,
570 true,
571 )
572}
573
574pub fn encode_service(original: &Unitdata, cause: u8) -> Result<Vec<u8>, String> {
577 let mut data = original.data.clone();
578 data.truncate(MAX_DATA_LEN);
579 match original.message_type {
580 MSG_XUDT => encode_unitdata(
581 MSG_XUDTS,
582 &[cause, DEFAULT_HOP_COUNTER],
583 &original.calling,
584 &original.called,
585 &data,
586 true,
587 ),
588 _ => encode_unitdata(
589 MSG_UDTS,
590 &[cause],
591 &original.calling,
592 &original.called,
593 &data,
594 false,
595 ),
596 }
597}
598
599fn encode_unitdata(
600 mt: u8,
601 fixed: &[u8],
602 called: &SccpAddress,
603 calling: &SccpAddress,
604 data: &[u8],
605 optional_pointer: bool,
606) -> Result<Vec<u8>, String> {
607 if data.len() > MAX_DATA_LEN {
608 return err(format!(
609 "{} octets of data do not fit one {} ({MAX_DATA_LEN} at most); NetGet does not \
610 segment",
611 data.len(),
612 message_type_name(mt)
613 ));
614 }
615 let called = called.encode()?;
616 let calling = calling.encode()?;
617 let pointers = if optional_pointer { 4 } else { 3 };
618 let mut out = vec![mt];
619 out.extend_from_slice(fixed);
620 let p_called = pointers;
622 let p_calling = p_called - 1 + 1 + called.len();
623 let p_data = p_calling - 1 + 1 + calling.len();
624 for p in [p_called, p_calling, p_data] {
625 if p > 255 {
626 return err("SCCP addresses too long for one-octet pointers");
627 }
628 }
629 out.push(p_called as u8);
630 out.push(p_calling as u8);
631 out.push(p_data as u8);
632 if optional_pointer {
633 out.push(0);
634 }
635 out.push(called.len() as u8);
636 out.extend(called);
637 out.push(calling.len() as u8);
638 out.extend(calling);
639 out.push(data.len() as u8);
640 out.extend_from_slice(data);
641 Ok(out)
642}
643
644#[derive(Clone, Copy, Debug, PartialEq, Eq)]
646pub struct Scmg {
647 pub format: u8,
648 pub affected_ssn: u8,
649 pub affected_pc: u16,
650 pub multiplicity: u8,
651}
652
653impl Scmg {
654 pub fn decode(o: &[u8]) -> Result<Self, String> {
655 if o.len() < 5 {
656 return err(format!("SCMG message of {} octets (5 expected)", o.len()));
657 }
658 Ok(Scmg {
659 format: o[0],
660 affected_ssn: o[1],
661 affected_pc: u16::from_le_bytes([o[2], o[3]]) & 0x3fff,
662 multiplicity: o[4] & 0x03,
663 })
664 }
665
666 pub fn encode(&self) -> Vec<u8> {
667 let pc = self.affected_pc.to_le_bytes();
668 vec![
669 self.format,
670 self.affected_ssn,
671 pc[0],
672 pc[1],
673 self.multiplicity,
674 ]
675 }
676
677 pub fn name(&self) -> &'static str {
678 match self.format {
679 SCMG_SSA => "SSA",
680 SCMG_SSP => "SSP",
681 SCMG_SST => "SST",
682 0x04 => "SOR",
683 0x05 => "SOG",
684 0x06 => "SSC",
685 _ => "unknown",
686 }
687 }
688}