ndef/
lib.rs

1// SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#![no_std]
5
6mod error;
7
8pub use error::{Error, Result};
9
10#[cfg(feature = "alloc")]
11extern crate alloc;
12#[cfg(feature = "alloc")]
13use alloc::{format, string::String, vec::Vec};
14#[cfg(not(feature = "alloc"))]
15use heapless::Vec;
16
17#[derive(Clone, Debug, PartialEq)]
18pub enum TypeNameFormat {
19    Empty,
20    NfcWellKnown,
21    Media,
22    AbsoluteUri,
23    NfcExternal,
24    Unknown,
25    Unchanged,
26    Reserved,
27}
28
29#[derive(Clone, Debug, Default, PartialEq)]
30struct Header(u8);
31
32impl Header {
33    // fn message_begin(&self) -> bool {
34    //     self.0 & 0x80 == 0x80
35    // }
36    fn set_message_begin(&mut self) {
37        self.0 |= 0x80;
38    }
39
40    // fn message_end(&self) -> bool {
41    //     self.0 & 0x40 == 0x40
42    // }
43    fn set_message_end(&mut self) {
44        self.0 |= 0x40;
45    }
46    fn clr_message_end(&mut self) {
47        self.0 &= !0x40;
48    }
49
50    // fn message_chunk(&self) -> bool {
51    //     self.0 & 0x20 == 0x20
52    // }
53
54    fn short_record(&self) -> bool {
55        self.0 & 0x10 == 0x10
56    }
57    fn set_short_record(&mut self) {
58        self.0 |= 0x10;
59    }
60
61    fn id_length(&self) -> bool {
62        self.0 & 0x08 == 0x08
63    }
64    fn set_id_length(&mut self) {
65        self.0 |= 0x08;
66    }
67
68    fn type_name_format(&self) -> TypeNameFormat {
69        match self.0 & 0x07 {
70            0 => TypeNameFormat::Empty,
71            1 => TypeNameFormat::NfcWellKnown,
72            2 => TypeNameFormat::Media,
73            3 => TypeNameFormat::AbsoluteUri,
74            4 => TypeNameFormat::NfcExternal,
75            5 => TypeNameFormat::Unknown,
76            6 => TypeNameFormat::Unchanged,
77            7 => TypeNameFormat::Reserved,
78            _ => unreachable!(),
79        }
80    }
81    fn set_type_name_format(&mut self, tnf: TypeNameFormat) {
82        self.0 &= !0x70;
83        self.0 |= match tnf {
84            TypeNameFormat::Empty => 0x00,
85            TypeNameFormat::NfcWellKnown => 0x01,
86            TypeNameFormat::Media => 0x02,
87            TypeNameFormat::AbsoluteUri => 0x03,
88            TypeNameFormat::NfcExternal => 0x04,
89            TypeNameFormat::Unknown => 0x05,
90            TypeNameFormat::Unchanged => 0x06,
91            TypeNameFormat::Reserved => 0x07,
92        };
93    }
94}
95
96#[derive(Clone, Debug, PartialEq)]
97pub enum RecordType<'a> {
98    #[cfg(not(feature = "alloc"))]
99    Text { enc: &'a str, txt: &'a str },
100    #[cfg(feature = "alloc")]
101    Text { enc: &'a str, txt: String },
102    External {
103        domain: &'a str,
104        type_: &'a str,
105        data: &'a [u8],
106    },
107    #[cfg(all(feature = "cbor", not(feature = "alloc")))]
108    Cbor(&'a [u8]),
109    #[cfg(all(feature = "cbor", feature = "alloc"))]
110    Cbor(Vec<u8>),
111}
112
113impl<'a> RecordType<'a> {
114    fn len(&self) -> usize {
115        match self {
116            RecordType::Text { enc, txt } => 1 + enc.len() + txt.len(),
117            RecordType::External { data, .. } => data.len(),
118            #[cfg(feature = "cbor")]
119            RecordType::Cbor(data) => data.len(),
120        }
121    }
122
123    #[cfg(feature = "alloc")]
124    fn to_vec(&self) -> Vec<u8> {
125        match self {
126            RecordType::Text { enc, txt } => {
127                let mut data = Vec::new();
128                // force utf-8 encoding here
129                data.push(enc.len() as u8);
130                data.extend_from_slice(enc.as_bytes());
131                data.extend_from_slice(txt.as_bytes());
132                data
133            }
134            RecordType::External { data, .. } => data.to_vec(),
135            #[cfg(feature = "cbor")]
136            RecordType::Cbor(data) => data.clone(),
137        }
138    }
139    #[cfg(not(feature = "alloc"))]
140    fn to_vec(&self) -> Result<Vec<u8, 256>> {
141        match self {
142            RecordType::Text { enc, txt } => {
143                let mut data = Vec::new();
144                // force utf-8 encoding here
145                data.push(enc.len() as u8)
146                    .map_err(|_| Error::BufferTooSmall)?;
147                data.extend_from_slice(enc.as_bytes())
148                    .map_err(|_| Error::BufferTooSmall)?;
149                data.extend_from_slice(txt.as_bytes())
150                    .map_err(|_| Error::BufferTooSmall)?;
151                Ok(data)
152            }
153            RecordType::External { data, .. } => {
154                Vec::from_slice(data).map_err(|_| Error::BufferTooSmall)
155            }
156            #[cfg(feature = "cbor")]
157            RecordType::Cbor(data) => Vec::from_slice(data).map_err(|_| Error::BufferTooSmall),
158        }
159    }
160}
161
162#[derive(Clone, Debug, PartialEq)]
163pub enum Payload<'a> {
164    RTD(RecordType<'a>),
165}
166
167impl<'a> From<&Payload<'a>> for TypeNameFormat {
168    fn from(pl: &Payload<'a>) -> TypeNameFormat {
169        match pl {
170            Payload::RTD(RecordType::External { .. }) => TypeNameFormat::NfcExternal,
171            #[cfg(feature = "cbor")]
172            Payload::RTD(RecordType::Cbor(_)) => TypeNameFormat::NfcExternal,
173            Payload::RTD(_) => TypeNameFormat::NfcWellKnown,
174        }
175    }
176}
177
178impl<'a> Payload<'a> {
179    fn len(&self) -> usize {
180        match self {
181            Payload::RTD(rtd) => rtd.len(),
182        }
183    }
184
185    #[cfg(feature = "alloc")]
186    fn to_vec(&self) -> Vec<u8> {
187        match self {
188            Payload::RTD(rtd) => rtd.to_vec(),
189        }
190    }
191    #[cfg(not(feature = "alloc"))]
192    fn to_vec(&self) -> Result<Vec<u8, 256>> {
193        match self {
194            Payload::RTD(rtd) => rtd.to_vec(),
195        }
196    }
197    #[cfg(all(feature = "alloc", feature = "cbor"))]
198    pub fn from_cbor_encodable<T>(x: &T) -> Self
199    where
200        T: minicbor::Encode<()>,
201    {
202        Payload::RTD(RecordType::Cbor(minicbor::to_vec(x).unwrap()))
203    }
204}
205
206#[derive(Clone, Debug, PartialEq)]
207pub struct Record<'a> {
208    header: Header,
209    id: Option<&'a [u8]>,
210    pub payload: Payload<'a>,
211}
212
213impl<'a> Record<'a> {
214    pub fn new(id: Option<&'a [u8]>, payload: Payload<'a>) -> Self {
215        let mut header = Header::default();
216        header.set_type_name_format(TypeNameFormat::from(&payload));
217        if id.is_some() {
218            header.set_id_length();
219        }
220        if payload.len() < 256 {
221            header.set_short_record();
222        }
223        Self {
224            header,
225            id,
226            payload,
227        }
228    }
229
230    #[cfg(feature = "cbor")]
231    pub fn is_type_cbor(&self) -> bool {
232        matches!(&self.payload, Payload::RTD(RecordType::Cbor(_)))
233    }
234
235    #[cfg(feature = "alloc")]
236    pub fn get_type(&self) -> String {
237        use alloc::string::ToString;
238
239        match &self.payload {
240            Payload::RTD(rtd) => match rtd {
241                RecordType::Text { .. } => "T".to_string(),
242                RecordType::External { domain, type_, .. } => format!("{domain}:{type_}"),
243                #[cfg(feature = "cbor")]
244                RecordType::Cbor(_) => "cbor.io:cbor".to_string(),
245            },
246        }
247    }
248    #[cfg(not(feature = "alloc"))]
249    pub fn get_type(&self) -> &'a str {
250        match &self.payload {
251            Payload::RTD(rtd) => match rtd {
252                RecordType::Text { .. } => "T",
253                RecordType::External {
254                    domain: _,
255                    type_: _,
256                    ..
257                } => unimplemented!("can't concat without alloc"),
258                #[cfg(feature = "cbor")]
259                RecordType::Cbor(_) => "cbor.io:cbor",
260            },
261        }
262    }
263
264    #[cfg(feature = "alloc")]
265    pub fn payload(&self) -> Vec<u8> {
266        self.payload.to_vec()
267    }
268    #[cfg(not(feature = "alloc"))]
269    pub fn payload(&self) -> Result<Vec<u8, 256>> {
270        self.payload.to_vec()
271    }
272}
273
274#[derive(Clone, Debug, Default, PartialEq)]
275pub struct Message<'a> {
276    #[cfg(feature = "alloc")]
277    pub records: Vec<Record<'a>>,
278    #[cfg(not(feature = "alloc"))]
279    pub records: Vec<Record<'a>, 8>,
280}
281
282impl<'a> Message<'a> {
283    #[cfg(feature = "alloc")]
284    pub fn append_record(&mut self, record: &mut Record<'a>) {
285        if self.records.is_empty() {
286            record.header.set_message_begin();
287        } else {
288            self.records.last_mut().unwrap().header.clr_message_end();
289        }
290        record.header.set_message_end();
291        self.records.push(record.clone());
292    }
293
294    #[cfg(not(feature = "alloc"))]
295    pub fn append_record(&mut self, record: &mut Record<'a>) -> Result<()> {
296        if self.records.is_empty() {
297            record.header.set_message_begin();
298        } else {
299            self.records.last_mut().unwrap().header.clr_message_end();
300        }
301        record.header.set_message_end();
302        self.records
303            .push(record.clone())
304            .map_err(|_| Error::BufferTooSmall)
305    }
306
307    #[cfg(feature = "alloc")]
308    pub fn to_vec(&self) -> Vec<u8> {
309        let mut buf = Vec::new();
310        for record in &self.records {
311            let type_ = record.get_type();
312            let payload_data = record.payload();
313            // Header
314            buf.push(record.header.0);
315            // Type Length
316            buf.push(type_.len() as u8);
317            // Payload Length
318            buf.push(payload_data.len() as u8);
319            // ID Length
320            if let Some(id) = &record.id {
321                buf.push(id.len() as u8);
322            }
323            // Type
324            buf.extend_from_slice(type_.as_bytes());
325            // ID
326            if let Some(id) = &record.id {
327                buf.extend_from_slice(id);
328            }
329            // Payload
330            buf.extend_from_slice(payload_data.as_slice());
331        }
332        buf
333    }
334
335    #[cfg(not(feature = "alloc"))]
336    pub fn to_vec(&self) -> Result<Vec<u8, 256>> {
337        let mut buf = Vec::new();
338        for record in &self.records {
339            let type_ = record.get_type();
340            let payload_data = record.payload()?;
341            // Header
342            buf.push(record.header.0)
343                .map_err(|_| Error::BufferTooSmall)?;
344            // Type Length
345            buf.push(type_.len() as u8)
346                .map_err(|_| Error::BufferTooSmall)?;
347            // Payload Length
348            buf.push(payload_data.len() as u8)
349                .map_err(|_| Error::BufferTooSmall)?;
350            // ID Length
351            if let Some(id) = &record.id {
352                buf.push(id.len() as u8)
353                    .map_err(|_| Error::BufferTooSmall)?;
354            }
355            // Type
356            buf.extend_from_slice(type_.as_bytes())
357                .map_err(|_| Error::BufferTooSmall)?;
358            // ID
359            if let Some(id) = &record.id {
360                buf.extend_from_slice(id)
361                    .map_err(|_| Error::BufferTooSmall)?;
362            }
363            // Payload
364            buf.extend_from_slice(payload_data.as_slice())
365                .map_err(|_| Error::BufferTooSmall)?;
366        }
367        Ok(buf)
368    }
369}
370
371impl<'a> TryFrom<&'a [u8]> for Message<'a> {
372    type Error = Error<'a>;
373
374    fn try_from(slice: &'a [u8]) -> Result<Self> {
375        if slice.is_empty() {
376            return Err(Error::SliceTooShort);
377        }
378        let mut records = Vec::new();
379        let mut offset = 0;
380        macro_rules! checked_add_offset {
381            ($inc:expr) => {{
382                if offset + $inc > slice.len() {
383                    return Err(Error::SliceTooShort);
384                }
385                offset += $inc;
386            }};
387        }
388        while offset < slice.len() {
389            // Header
390            checked_add_offset!(1);
391            let header = Header(slice[offset - 1]);
392            // Type Length
393            checked_add_offset!(1);
394            let type_length = slice[offset - 1] as usize;
395            // Payload Length
396            let payload_length = if header.short_record() {
397                checked_add_offset!(1);
398                slice[offset - 1] as usize
399            } else {
400                checked_add_offset!(4);
401                u32::from_be_bytes(slice[offset - 4..offset].try_into().unwrap()) as usize
402            };
403            // ID Length
404            let id_length = if header.id_length() {
405                checked_add_offset!(1);
406                slice[offset - 1] as usize
407            } else {
408                0
409            };
410            // Type
411            checked_add_offset!(type_length);
412            let type_ = core::str::from_utf8(&slice[offset - type_length..offset])?;
413            // ID
414            let id = if header.id_length() {
415                checked_add_offset!(id_length);
416                Some(&slice[offset - id_length..offset])
417            } else {
418                None
419            };
420            // Payload
421            checked_add_offset!(payload_length);
422            let payload_data = &slice[offset - payload_length..offset];
423            let payload = match header.type_name_format() {
424                TypeNameFormat::NfcWellKnown => Payload::RTD(match type_ {
425                    "T" => {
426                        if payload_data.is_empty() {
427                            return Err(Error::SliceTooShort);
428                        }
429                        let enc_len = (payload_data[0] & 0x1f) as usize;
430                        let is_utf16 = (payload_data[0] & 0x80) != 0;
431                        if payload_data.len() < enc_len + 1 {
432                            return Err(Error::SliceTooShort);
433                        }
434                        let enc = core::str::from_utf8(&payload_data[1..enc_len + 1])?;
435                        let txt = if is_utf16 {
436                            #[cfg(not(feature = "alloc"))]
437                            unimplemented!("UTF-16 decoding is not supported yet");
438                            #[cfg(feature = "alloc")]
439                            {
440                                let utf16_bytes = &payload_data[enc_len + 1..];
441                                // Ensure the byte slice has an even length (UTF-16 is 2 bytes per unit)
442                                if utf16_bytes.len() % 2 != 0 {
443                                    return Err(Error::UTF16OddLength(utf16_bytes.len()));
444                                }
445                                // Convert the byte slice into u16 units
446                                let utf16_units: Vec<u16> = utf16_bytes
447                                    .chunks(2)
448                                    .map(|chunk| u16::from_be_bytes(chunk.try_into().unwrap()))
449                                    .collect();
450                                String::from_utf16(&utf16_units).map_err(|_| Error::UTF16Decode)?
451                            }
452                        } else {
453                            #[cfg(not(feature = "alloc"))]
454                            {
455                                core::str::from_utf8(&payload_data[enc_len + 1..])?
456                            }
457                            #[cfg(feature = "alloc")]
458                            String::from_utf8(payload_data[enc_len + 1..].to_vec())?
459                        };
460                        RecordType::Text { enc, txt }
461                    }
462                    t => return Err(Error::UnsupportedRecordType(t)),
463                }),
464                TypeNameFormat::NfcExternal => match type_ {
465                    #[cfg(all(feature = "cbor", not(feature = "alloc")))]
466                    "cbor.io:cbor" => Payload::RTD(RecordType::Cbor(payload_data)),
467                    #[cfg(all(feature = "cbor", feature = "alloc"))]
468                    "cbor.io:cbor" => Payload::RTD(RecordType::Cbor(payload_data.to_vec())),
469                    _ => {
470                        if let Some(index) = type_.find(':') {
471                            let domain = &type_[..index];
472                            let type_ = &type_[index + 1..];
473                            Payload::RTD(RecordType::External {
474                                domain,
475                                type_,
476                                data: payload_data,
477                            })
478                        } else {
479                            return Err(Error::InvalidExternalType(type_));
480                        }
481                    }
482                },
483                tnf => return Err(Error::UnsupportedTypeNameFormat(tnf)),
484            };
485            #[cfg(feature = "alloc")]
486            records.push(Record {
487                header,
488                id,
489                payload,
490            });
491            #[cfg(not(feature = "alloc"))]
492            records
493                .push(Record {
494                    header,
495                    id,
496                    payload,
497                })
498                .map_err(|_| Error::SliceTooShort)?;
499        }
500        Ok(Message { records })
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    #[cfg(feature = "alloc")]
507    use alloc::string::ToString;
508
509    use super::*;
510
511    #[test]
512    fn test_rtd_text_utf8() {
513        let raw = [
514            0xD1, 0x01, 0x12, 0x54, 0x02, 0x66, 0x72, 0x55, 0x54, 0x46, 0x2D, 0x38, 0x20, 0x74,
515            0x65, 0x78, 0x74, 0x20, 0xf0, 0x9f, 0xa6, 0x80,
516        ];
517        let mut msg = Message::default();
518        let txt = "UTF-8 text 🦀";
519        #[cfg(feature = "alloc")]
520        let txt = txt.to_string();
521        let mut rec1 = Record::new(None, Payload::RTD(RecordType::Text { enc: "fr", txt }));
522        #[cfg(feature = "alloc")]
523        msg.append_record(&mut rec1);
524        #[cfg(not(feature = "alloc"))]
525        msg.append_record(&mut rec1).unwrap();
526        assert_eq!(msg, Message::try_from(raw.as_slice()).unwrap());
527        #[cfg(feature = "alloc")]
528        assert_eq!(&raw, msg.to_vec().as_slice());
529        #[cfg(not(feature = "alloc"))]
530        assert_eq!(&raw, msg.to_vec().unwrap().as_slice());
531    }
532    #[test]
533    #[cfg(feature = "alloc")]
534    fn test_rtd_text_utf16() {
535        let raw = [
536            0xD1, 0x01, 0x1F, 0x54, 0x82, 0x66, 0x72, 0x00, 0x55, 0x00, 0x54, 0x00, 0x46, 0x00,
537            0x2D, 0x00, 0x31, 0x00, 0x36, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, 0x00, 0x78, 0x00,
538            0x74, 0x00, 0x20, 0xd8, 0x3e, 0xdd, 0x80,
539        ];
540        let mut msg = Message::default();
541        let mut rec1 = Record::new(
542            None,
543            Payload::RTD(RecordType::Text {
544                enc: "fr",
545                txt: "UTF-16 text 🦀".to_string(),
546            }),
547        );
548        msg.append_record(&mut rec1);
549        assert_eq!(msg, Message::try_from(raw.as_slice()).unwrap());
550    }
551    #[test]
552    fn test_rtd_external() {
553        let raw = [
554            0xD4, 0x08, 0x01, 0x65, 0x78, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x74, 0x61,
555        ];
556        let mut msg = Message::default();
557        let mut rec1 = Record::new(
558            None,
559            Payload::RTD(RecordType::External {
560                domain: "ex.com",
561                type_: "t",
562                data: &[0x61],
563            }),
564        );
565        #[cfg(feature = "alloc")]
566        msg.append_record(&mut rec1);
567        #[cfg(not(feature = "alloc"))]
568        msg.append_record(&mut rec1).unwrap();
569        assert_eq!(msg, Message::try_from(raw.as_slice()).unwrap());
570        #[cfg(feature = "alloc")]
571        assert_eq!(&raw, msg.to_vec().as_slice());
572    }
573    #[test]
574    #[cfg(feature = "cbor")]
575    fn test_cbor() {
576        let raw = [
577            0xD4, 0x0c, 0x01, 0x63, 0x62, 0x6f, 0x72, 0x2e, 0x69, 0x6f, 0x3a, 0x63, 0x62, 0x6f,
578            0x72, 0x61,
579        ];
580        let mut msg = Message::default();
581        #[cfg(feature = "alloc")]
582        let mut rec1 = Record::new(None, Payload::RTD(RecordType::Cbor(alloc::vec![0x61])));
583        #[cfg(not(feature = "alloc"))]
584        let mut rec1 = Record::new(None, Payload::RTD(RecordType::Cbor(&[0x61])));
585        #[cfg(feature = "alloc")]
586        msg.append_record(&mut rec1);
587        #[cfg(not(feature = "alloc"))]
588        msg.append_record(&mut rec1).unwrap();
589        assert_eq!(msg, Message::try_from(raw.as_slice()).unwrap());
590        #[cfg(feature = "alloc")]
591        assert_eq!(&raw, msg.to_vec().as_slice());
592        #[cfg(not(feature = "alloc"))]
593        assert_eq!(&raw, msg.to_vec().unwrap().as_slice());
594    }
595}