diff --git a/ndef/.gitignore b/ndef/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/ndef/.gitignore @@ -0,0 +1 @@ +/target diff --git a/ndef/Cargo.toml b/ndef/Cargo.toml new file mode 100644 index 0000000..76d84c5 --- /dev/null +++ b/ndef/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ndef" +version = "0.1.0" +edition = "2021" + +[dependencies] +packed_struct = { version = "0.10.1", default-features = false } +heapless = "0.8" +thiserror = { version = "2.0.8", default-features = false } +defmt = { version = "0.3", optional = true } + +[features] +defmt-03 = ["dep:defmt", "heapless/defmt-03"] diff --git a/ndef/LICENSE-APACHE b/ndef/LICENSE-APACHE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/ndef/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ndef/LICENSE-MIT b/ndef/LICENSE-MIT new file mode 100644 index 0000000..81ac99f --- /dev/null +++ b/ndef/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) rnfc project contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ndef/README.md b/ndef/README.md new file mode 100644 index 0000000..bc9b11b --- /dev/null +++ b/ndef/README.md @@ -0,0 +1,61 @@ +# NDEF `no_std` Parser Crate + +This crate provides a lightweight, `no_std` compatible parser for NFC Data Exchange Format (NDEF) messages, specifically designed for embedded environments with limited memory. +It supports parsing and serializing Capability Containers (CC) and NDEF messages using fixed-size buffers. + +## Features + +- **NFC Forum Type 5 Tag (T5T) Support**: Parse NFC Type 5 tags commonly used in embedded NFC applications. +- **Capability Container (CC) Parsing**: Decode Capability Containers, which store metadata about the NFC tag. +- **NDEF Message Handling**: Parse and handle NDEF messages that use Type-Length-Value (TLV) structures. + +## Usage Example + +### Parsing a Capability Container (CC) + +```rust +use ndef_parser::{CapabilityContainer, NdefError}; + +let cc_bytes = [0xE1, 0x40, 0x40, 0x01]; +match CapabilityContainer::unpack(&cc_bytes) { + Ok(cc) => println!("Parsed Capability Container: {:?}", cc), + Err(e) => eprintln!("Error: {}", e), +} +``` + +### Parsing an NDEF TLV + +```rust +use ndef_parser::{NdefTlv, NdefError}; + +let ndef_tlv_bytes = [0x03, 0x27, /* NDEF record data */]; +// 1024 is the maximum size of an NDEF record payload, adjust as needed +match NdefTlv::<1024>::from_bytes(&ndef_tlv_bytes) { + Ok(ndef_tlv) => println!("Parsed NDEF TLV: {:?}", ndef_tlv), + Err(e) => eprintln!("Error: {}", e), +} +``` + +## NFC Concepts Overview + +- **Capability Container (CC)**: Metadata structure that defines the capabilities of an NFC tag, including supported operations and memory layout. +- **NDEF Record**: Individual data record within an NDEF message, consisting of a header and payload. +- **TLV (Type-Length-Value)**: Encoded structure used for representing data in NDEF messages. + +## Planned Features + +- **Payload Decoding**: Support for decoding specific payload types like URIs, MIME types, and more. +- **Extended Tag Compatibility**: Improve compatibility with a wider range of NFC tags beyond ST25DV. + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT License ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +## Contributions + +Contributions are welcome! Feel free to submit a PR to improve functionality, compatibility, or documentation. diff --git a/ndef/src/capability_container.rs b/ndef/src/capability_container.rs new file mode 100644 index 0000000..06fa7b1 --- /dev/null +++ b/ndef/src/capability_container.rs @@ -0,0 +1,116 @@ +use packed_struct::prelude::*; + +/// Magic number of the Capability Container (CC). +/// +/// Indicates the type of the Capability Container. Typically, values are: +/// - `E1` (0xE1): Standard CC. +/// - `E2` (0xE2): Extended CC for the ST25DV (might be used for other tags?). +#[derive(PrimitiveEnum_u8, Clone, Copy, Debug, PartialEq)] +pub enum MagicNumber { + E1 = 0xE1, + E2 = 0xE2, +} + +/// Write access permissions for memory. +/// +/// Represents the rights associated with writing data to the memory area. +#[derive(PrimitiveEnum_u8, Clone, Copy, Debug, PartialEq)] +pub enum WriteAccess { + Always = 0x00, + RFU = 0x01, + Proprietary = 0x02, + Never = 0x03, +} + +/// Read access permissions for memory. +/// +/// Represents the rights associated with reading data from the memory area. +#[derive(PrimitiveEnum_u8, Clone, Copy, Debug, PartialEq)] +pub enum ReadAccess { + Always = 0x00, + RFU1 = 0x01, + Proprietary = 0x02, + RFU2 = 0x03, +} + +/// Represents the version and access conditions of the Capability Container. +/// +/// This byte encodes the version and read/write permissions. +/// For version 1.0 with all accesses granted, this byte value is 40h (commonly used). +#[derive(PackedStruct)] +#[packed_struct(size_bytes = "1", bit_numbering = "lsb0")] +pub struct VersionAccessCondition { + #[packed_field(bits = "0..2", ty = "enum")] + pub write_access: WriteAccess, + #[packed_field(bits = "2..4", ty = "enum")] + pub read_access: ReadAccess, + #[packed_field(bits = "4..6")] + pub minor_version: u8, + #[packed_field(bits = "6..8")] + pub major_version: u8, +} + +/// Additional feature information byte description +#[derive(PackedStruct)] +#[packed_struct(size_bytes = "1", bit_numbering = "lsb0")] +pub struct AdditionalFeatureInformation { + /// Support Read Multiple Block command + #[packed_field(bits = "0")] + mbread: bool, + /// These bits are reserved for future use and must be set to 0. + #[packed_field(bits = "1..3")] + rfu1: u8, + /// Support Lock Block command + #[packed_field(bits = "3")] + lock_block: bool, + /// Support Special Frame + #[packed_field(bits = "4")] + special_frame: bool, + /// These bits are reserved for future use and must be set to 0. + #[packed_field(bits = "5..8")] + rfu2: u8, +} + +/// Capability Container (CC) for NFC Forum Type 5 Tags. +/// +/// The Capability Container stores metadata about the NFC tag, including versioning, access conditions, +/// and supported features. This structure currently supports the four-byte format. +/// +/// **Note:** Eight-byte CC decoding is planned but not yet implemented. +// TODO: add an eight byte CC decoder +#[derive(PackedStruct)] +#[packed_struct(size_bytes = "4", bit_numbering = "msb0")] +pub struct CapabilityContainer { + #[packed_field(bytes = "0", ty = "enum")] + magic_number: MagicNumber, + #[packed_field(bytes = "1")] + version_access_condition: VersionAccessCondition, + #[packed_field(bytes = "2")] + mlen: u8, + #[packed_field(bytes = "3")] + addional_feature_information: AdditionalFeatureInformation, +} + +#[cfg(test)] +mod tests { + use packed_struct::prelude::*; + + use super::{CapabilityContainer, MagicNumber, ReadAccess, WriteAccess}; + + #[test] + fn test_parse_cc() { + let bytes = [0xE1, 0x40, 0x40, 0x01]; + let cc = CapabilityContainer::unpack(&bytes).unwrap(); + assert_eq!(cc.magic_number, MagicNumber::E1); + assert_eq!(cc.version_access_condition.write_access, WriteAccess::Always); + assert_eq!(cc.version_access_condition.read_access, ReadAccess::Always); + assert_eq!(cc.version_access_condition.minor_version, 0); + assert_eq!(cc.version_access_condition.major_version, 1); + assert_eq!(cc.mlen, 64); + assert!(cc.addional_feature_information.mbread); + assert_eq!(cc.addional_feature_information.rfu1, 0); + assert!(!cc.addional_feature_information.lock_block); + assert!(!cc.addional_feature_information.special_frame); + assert_eq!(cc.addional_feature_information.rfu2, 0); + } +} diff --git a/ndef/src/lib.rs b/ndef/src/lib.rs new file mode 100644 index 0000000..674b806 --- /dev/null +++ b/ndef/src/lib.rs @@ -0,0 +1,55 @@ +#![no_std] +//! # NDEF `no_std` Parser +//! +//! This crate provides a simple, `no_std` compatible parser for NFC Data Exchange Format (NDEF) messages. +//! It supports parsing and serializing Capability Containers (CC) and NDEF records using fixed-size buffers. +//! +//! Designed for embedded environments where memory constraints are critical and allocators are unavailable. +//! +//! ## Features +//! - **NFC Forum Type 5 Tag (T5T)** parsing. +//! - **Capability Container (CC)** decoding. +//! - **NDEF TLV (Type-Length-Value)** handling. +//! +//! ## Example Usage +//! +//! This example demonstrates decoding a Capability Container and an NDEF TLV record: +//! +//! ```ignore +//! use ndef::{CapabilityContainer, NdefTlv, NdefRecord}; +//! +//! let cc_bytes = [0xE1, 0x40, 0x40, 0x01]; +//! match CapabilityContainer::unpack(&cc_bytes) { +//! Ok(cc) => println!("{:?}", cc), +//! Err(e) => println!("Error parsing Capability Container: {:?}", e), +//! } +//! +//! let ndef_tlv_bytes = [0x03, 0x27, /* NDEF record data */]; +//! const MAX_PAYLOAD_SIZE: usize = 1024; +//! +//! match NdefTlv::::from_bytes(&ndef_tlv_bytes) { +//! Ok(ndef_tlv) => println!("{:?}", ndef_tlv), +//! Err(e) => println!("Error parsing NDEF TLV: {:?}", e), +//! } +//! ``` +//! +//! ## NFC Concepts +//! +//! - **Capability Container (CC)**: Stores metadata about the NFC Forum Type 5 Tag (T5T). +//! - **NDEF (NFC Data Exchange Format)**: A standardized format for exchanging data over NFC. +//! - **TLV (Type-Length-Value)**: A flexible encoding structure used within NFC tags. +//! - **NDEF Record**: A structured unit within an NDEF message containing a payload, type information, and metadata. +//! +//! ## Notes +//! +//! - **Work in Progress**: This crate may not be fully functional or stable. +//! - **Compatibility**: Initially built for **ST25DV tags**, but PRs are welcome to improve compatibility with other NFC tags. +//! - **Payload Decoding**: Future support for decoding payloads of known types (e.g., URI, MIME) is planned. +//! +//! ## Contributing +//! +//! Contributions are welcome! To improve compatibility or add features, please submit a PR. + +pub mod capability_container; +pub mod ndef_record; +pub mod tlv; diff --git a/ndef/src/ndef_record/external_type.rs b/ndef/src/ndef_record/external_type.rs new file mode 100644 index 0000000..839cced --- /dev/null +++ b/ndef/src/ndef_record/external_type.rs @@ -0,0 +1,114 @@ +use heapless::Vec; +use thiserror::Error; + +use crate::ndef_record::{NdefRecord, NdefRecordHeader, TypeNameFormat}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Provided domain or type is too empty")] + InvalidFormat, + #[error("Provided type is too long, maximum length is 255 bytes")] + TypeTooLong, + #[error("Provided payload is too long, maximum length is {0} bytes")] + PayloadTooLong(usize), + #[error("Vector operations failed")] + VectorFull, +} + +impl NdefRecord { + /// Creates a new external type NDEF record + /// + /// # Arguments + /// + /// * `domain` - Domain name (e.g., "example.com") + /// * `type_` - Type name within the domain (e.g., "mytype") + /// * `data` - Payload data + /// + /// # Note + /// By default, the record is considered to be the first and last record in the message. + /// If you have multiple records in a message, you should modify the header accordingly. + /// + /// # Returns + /// + /// Returns a Result containing the NdefRecord if successful, or an Error if: + /// - Domain or type is empty + /// - Combined length of domain:type exceeds 255 bytes + /// - Payload exceeds MAX_PAYLOAD_SIZE + /// - Vector operations fail + pub fn new_external(domain: &[u8], type_: &[u8], data: &[u8]) -> Result { + // Domain and type must be non-empty + if domain.is_empty() || type_.is_empty() { + return Err(Error::InvalidFormat); + } + + // Calculate total type length (domain:type) + let total_type_length = domain.len() + 1 + type_.len(); // +1 for ':' + if total_type_length > 255 { + return Err(Error::TypeTooLong); + } + + // Check payload length + if data.len() > MAX_PAYLOAD_SIZE { + return Err(Error::PayloadTooLong(MAX_PAYLOAD_SIZE)); + } + + // Create record type vector (domain:type) + let mut record_type: Vec = Vec::from_slice(domain).map_err(|_| Error::VectorFull)?; + record_type.push(b':').map_err(|_| Error::VectorFull)?; + record_type.extend_from_slice(type_).map_err(|_| Error::VectorFull)?; + + // Create payload vector + let payload: Vec = Vec::from_slice(data).map_err(|_| Error::VectorFull)?; + + Ok(Self { + header: NdefRecordHeader::new(true, true, false, true, false, TypeNameFormat::External), + type_length: total_type_length as u8, + payload_length: data.len() as u32, + id_length: None, + record_type, + id: None, + payload, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_external_type() { + let record = NdefRecord::<1024>::new_external(b"example.com", b"mytype", b"Hello, world!").unwrap(); + + assert_eq!(record.header.type_name_format, TypeNameFormat::External); + assert!(!record.header.id_present); + assert!(record.header.short); + assert!(!record.header.chunk); + assert!(record.header.message_begin); + assert!(record.header.message_end); + + // Check type length + assert_eq!(record.type_length, 18); + + // Check payload length + assert_eq!(record.payload_length, 13); + + // Check ID length + assert_eq!(record.id_length, None); + + // Check record type + assert_eq!( + record.record_type, + [0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x3A, 0x6D, 0x79, 0x74, 0x79, 0x70, 0x65] + ); + + // Check ID + assert_eq!(record.id, None); + + // Check payload + assert_eq!( + record.payload, + [0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21] + ); + } +} diff --git a/ndef/src/ndef_record/mod.rs b/ndef/src/ndef_record/mod.rs new file mode 100644 index 0000000..4b05273 --- /dev/null +++ b/ndef/src/ndef_record/mod.rs @@ -0,0 +1,530 @@ +use heapless::Vec; +use packed_struct::prelude::*; +use thiserror::Error; + +mod external_type; + +#[derive(Error, Debug)] +pub enum NdefRecordError { + #[error("Provided buffer is too small")] + BufferTooSmall, + #[error("Invalid header, could not unpack")] + InvalidHeader, + #[error("Parsed payload length is bigger than provided generic type parameter")] + PayloadLengthMismatch, + #[error("Append elements to Vec failed")] + VecCapacityError, +} + +#[derive(PrimitiveEnum_u8, Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub enum TypeNameFormat { + Empty = 0x00, + WellKnown = 0x01, + MimeMediaType = 0x02, + AbsoluteUri = 0x03, + External = 0x04, + Unknown = 0x05, + Unchanged = 0x06, + Reserved = 0x07, +} + +/// NDEF message record header +#[derive(PackedStruct, PartialEq, Debug, Clone)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[packed_struct(size_bytes = "1", bit_numbering = "lsb0")] +pub struct NdefRecordHeader { + /// Type Name Format (TNF) field that defines how to interpret the type field + #[packed_field(bits = "0..3", ty = "enum")] + pub type_name_format: TypeNameFormat, + /// Indicates whether the record contains an ID field + #[packed_field(bits = "3")] + pub id_present: bool, + /// The Short Record (SR) bit flag determines the length of the payload record. + /// If SR is true, the payload length is one byte, otherwise it’s four bytes. + /// Payload length is required, but may be zero. + #[packed_field(bits = "4")] + pub short: bool, + /// The Chunk Bit Flag indicates whether the payload is a sequence of chunks. + #[packed_field(bits = "5")] + pub chunk: bool, + /// The Message End Bit Flag indicates whether the record is the last one in the message. + #[packed_field(bits = "6")] + pub message_end: bool, + /// The Message Begin Bit Flag indicates whether the record is the first one in the message. + #[packed_field(bits = "7")] + pub message_begin: bool, +} + +impl NdefRecordHeader { + /// Getter for the messaged_end field ot the header + pub fn message_end(&self) -> bool { + self.message_end + } + + /// Creates a new NDEF record header with all options configurable + /// + /// # Arguments + /// + /// * `message_begin` - Whether this is the first record in the message + /// * `message_end` - Whether this is the last record in the message + /// * `chunk` - Whether this record is part of a chunked payload + /// * `short` - Whether the payload length is one byte (true) or four bytes (false) + /// * `id_present` - Whether the record contains an ID field + /// * `type_name_format` - The Type Name Format (TNF) for interpreting the type field + pub fn new( + message_begin: bool, + message_end: bool, + chunk: bool, + short: bool, + id_present: bool, + type_name_format: TypeNameFormat, + ) -> Self { + Self { + message_begin, + message_end, + chunk, + short, + id_present, + type_name_format, + } + } +} + +/// An NDEF (NFC Data Exchange Format) record. +/// +/// Represents a single NDEF record with configurable payload size. Suitable for `no_std` environments. +/// +/// # Type Parameters +/// * `MAX_PAYLOAD_SIZE`: The maximum payload size in bytes. +/// +/// # Fields +/// - `header`: Contains flags and type name format. +/// - `type_length`: Length of the type field. +/// - `payload_length`: Length of the payload field. +/// - `id_length`: Optional length of the ID field. +/// - `record_type`: Type field identifying the record type. +/// - `id`: Optional ID field for linking records. +/// - `payload`: The actual payload data. +/// +/// # Example +/// ```ignore +/// let record = NdefRecord::<256>::from_bytes(&bytes).unwrap(); +/// ``` +#[derive(PartialEq, Debug, Clone)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct NdefRecord { + /// The NDEF record header containing flags and type name format + pub header: NdefRecordHeader, + /// Length of the record type field in bytes + pub type_length: u8, + /// Length of the payload field in bytes + pub payload_length: u32, + /// Length of the ID field in bytes, if present + pub id_length: Option, + /// The record type field identifying the type of the record + pub record_type: Vec, // Record type length is one byte (0-255) + /// The optional ID field used to link NDEF records + pub id: Option>, // ID length is one byte (0-255) + /// The payload data of the record + pub payload: Vec, // Adjust capacity as needed +} + +impl NdefRecord { + /// Calculate the total size needed for the serialized record + pub fn serialized_size(&self) -> usize { + let payload_length_size = if self.header.short { 1 } else { 4 }; + let id_length_size = if self.header.id_present { 1 } else { 0 }; + + 2 + // header + type_length + payload_length_size + + id_length_size + + self.record_type.len() + + self.id.as_ref().map_or(0, |id| id.len()) + + self.payload.len() + } + + /// Parses an NDEF record from a byte slice. + /// + /// # Parameters + /// - `bytes`: A slice of bytes containing the NDEF record data. + /// + /// # Errors + /// Returns a `NdefRecordError` if the input is too small, invalid, or not an NDEF type. + /// + /// # Example + /// ```ignore + /// let bytes = [0x03, 0x10, /* NDEF record bytes */]; + /// let ndef_record = NdefRecord::<1024>::from_bytes(&bytes)?; + /// ``` + pub fn from_bytes(bytes: &[u8]) -> Result<(Self, usize), NdefRecordError> { + // Minimum size check (header + type_length + payload_length) + if bytes.len() < 3 { + return Err(NdefRecordError::BufferTooSmall); + } + + // Parse header first to determine SR flag + let header = NdefRecordHeader::unpack(&[bytes[0]]).map_err(|_| NdefRecordError::InvalidHeader)?; + + let type_length = bytes[1]; + + // Parse payload length based on SR flag + let (payload_length, payload_length_size) = if header.short { + (bytes[2] as u32, 1) + } else { + if bytes.len() < 6 { + return Err(NdefRecordError::BufferTooSmall); + } + let mut payload_len_bytes = [0u8; 4]; + payload_len_bytes.copy_from_slice(&bytes[2..6]); + (u32::from_be_bytes(payload_len_bytes), 4) + }; + + // Check if payload length exceeds Vec capacity + if payload_length as usize > MAX_PAYLOAD_SIZE { + return Err(NdefRecordError::PayloadLengthMismatch); + } + + let mut offset = 2 + payload_length_size; + + // Handle ID length if present + let id_length = if header.id_present { + if bytes.len() < offset + 1 { + return Err(NdefRecordError::BufferTooSmall); + } + let id_len = bytes[offset]; + offset += 1; + Some(id_len) + } else { + None + }; + + // Parse record type + let record_type_end = offset + type_length as usize; + if bytes.len() < record_type_end { + return Err(NdefRecordError::BufferTooSmall); + } + let mut record_type = Vec::new(); + record_type + .extend_from_slice(&bytes[offset..record_type_end]) + .map_err(|_| NdefRecordError::VecCapacityError)?; + offset = record_type_end; + + // Parse ID if present + let id = if let Some(id_len) = id_length { + let id_end = offset + id_len as usize; + if bytes.len() < id_end { + return Err(NdefRecordError::BufferTooSmall); + } + let mut id_buf = Vec::new(); + id_buf + .extend_from_slice(&bytes[offset..id_end]) + .map_err(|_| NdefRecordError::VecCapacityError)?; + offset = id_end; + Some(id_buf) + } else { + None + }; + + // Parse payload + let payload_end = offset + payload_length as usize; + if bytes.len() < payload_end { + return Err(NdefRecordError::BufferTooSmall); + } + let mut payload = Vec::new(); + payload + .extend_from_slice(&bytes[offset..payload_end]) + .map_err(|_| NdefRecordError::VecCapacityError)?; + + let bytes_processed = payload_end; // The total number of bytes processed + + Ok(( + Self { + header, + type_length, + payload_length, + id_length, + record_type, + id, + payload, + }, + bytes_processed, + )) + } + + /// Serializes the NDEF record to bytes, writing to a provided buffer. + /// + /// # Parameters + /// - `buffer`: A mutable byte slice to write the serialized data. + /// + /// # Returns + /// The number of bytes written on success. + /// + /// # Errors + /// Returns `NdefRecordError::BufferTooSmall` if the buffer is too small. + /// Returns `NdefRecordError::InvalidHeader` if the header is invalid. + /// + /// # Example + /// ```ignore + /// let mut buffer = [0u8; 128]; + /// let bytes_written = ndef_record.to_bytes(&mut buffer)?; + /// ``` + pub fn to_bytes(&self, buffer: &mut [u8]) -> Result { + let required_size = self.serialized_size(); + + if buffer.len() < required_size { + return Err(NdefRecordError::BufferTooSmall); + } + + let mut offset = 0; + + // Write header + buffer[offset] = self.header.pack().map_err(|_| NdefRecordError::InvalidHeader)?[0]; + offset += 1; + + // Write type length + buffer[offset] = self.type_length; + offset += 1; + + // Write payload length + if self.header.short { + buffer[offset] = self.payload_length as u8; + offset += 1; + } else { + buffer[offset..offset + 4].copy_from_slice(&self.payload_length.to_be_bytes()); + offset += 4; + } + + // Write ID length if present + if let Some(id_length) = self.id_length { + buffer[offset] = id_length; + offset += 1; + } + + // Write record type + buffer[offset..offset + self.record_type.len()].copy_from_slice(&self.record_type); + offset += self.record_type.len(); + + // Write ID if present + if let Some(id) = &self.id { + buffer[offset..offset + id.len()].copy_from_slice(id); + offset += id.len(); + } + + // Write payload + buffer[offset..offset + self.payload.len()].copy_from_slice(&self.payload); + offset += self.payload.len(); + + // Pad remaining buffer with zeros + if buffer.len() > offset { + buffer[offset..].fill(0); + } + + Ok(offset) + } +} + +#[cfg(test)] +mod tests { + use heapless::Vec; + use packed_struct::prelude::*; + + use super::{NdefRecord, NdefRecordHeader, TypeNameFormat}; + use crate::tlv::{NdefTlv, Tag, TL}; + + #[test] + fn test_parse_ndef_record_header() { + let bytes = [0xD4]; + let ndef_record_header = NdefRecordHeader::unpack(&bytes).unwrap(); + assert_eq!(ndef_record_header.type_name_format, TypeNameFormat::External); + assert!(!ndef_record_header.id_present); + assert!(ndef_record_header.short); + assert!(!ndef_record_header.chunk); + assert!(ndef_record_header.message_begin); + assert!(ndef_record_header.message_end); + } + + #[test] + fn test_record_deserialization() { + const TYPE_LEN: usize = 28; + const PAYLOAD_LEN: usize = 8; + + let buffer = [ + 0xd4, 0x1c, 0x8, 0x74, 0x68, 0x65, 0x72, 0x6d, 0x69, 0x67, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x72, 0x75, 0x73, + 0x74, 0x70, 0x6f, 0x73, 0x74, 0x63, 0x61, 0x72, 0x64, 0x2d, 0x76, 0x31, 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf, + 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + + let (parsed, _) = NdefRecord::<1024>::from_bytes(&buffer).unwrap(); + + // Check header + assert_eq!(parsed.header.type_name_format, TypeNameFormat::External); + assert!(!parsed.header.id_present); + assert!(parsed.header.short); + assert!(!parsed.header.chunk); + assert!(parsed.header.message_begin); + assert!(parsed.header.message_end); + + // Check type length + assert_eq!(parsed.type_length, TYPE_LEN as u8); + + // Check payload length + assert_eq!(parsed.payload_length, PAYLOAD_LEN as u32); + + // Check ID length + assert_eq!(parsed.id_length, None); + + // Check record type + assert_eq!( + parsed.record_type, + [ + 0x74, 0x68, 0x65, 0x72, 0x6d, 0x69, 0x67, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x72, 0x75, 0x73, 0x74, 0x70, + 0x6f, 0x73, 0x74, 0x63, 0x61, 0x72, 0x64, 0x2d, 0x76, 0x31 + ] + ); + + // Check ID + assert_eq!(parsed.id, None); + + // Check payload + assert_eq!(parsed.payload, [0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf]); + } + + #[test] + fn test_parse_ndef_message_with_two_text_records() { + const TYPE_LEN: usize = 1; + const PAYLOAD_LEN: usize = 8; + + // NDEF message with two text records: + // - First 4 bytes: Capability Container (CC) descriptor + // - Followed by TLV blocks containing 2 records: + // 1. Text record with payload "Hello" + // 2. Text record with payload "World" + let buffer = [ + 0xe1, 0x40, 0x40, 0x1, 0x3, 0x18, 0x91, 0x1, 0x8, 0x54, 0x2, 0x65, 0x6e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x51, 0x1, + 0x8, 0x54, 0x2, 0x65, 0x6e, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0xfe, 0x0, 0x72, 0x64, 0x2d, 0x76, 0x31, 0x0, 0x0, 0x0, + 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + + let tlv = NdefTlv::<1024, 2>::from_bytes(&buffer[4..]).unwrap(); + let mut records = tlv.value.unwrap(); + assert_eq!(records.len(), 2); + + // Test second record deserialization + let second_record = records.pop().unwrap(); + // Check header + assert_eq!(second_record.header.type_name_format, TypeNameFormat::WellKnown); + assert!(!second_record.header.id_present); + assert!(second_record.header.short); + assert!(!second_record.header.chunk); + assert!(!second_record.header.message_begin); + assert!(second_record.header.message_end); + // Check type length + assert_eq!(second_record.type_length, TYPE_LEN as u8); + // Check payload length + assert_eq!(second_record.payload_length, PAYLOAD_LEN as u32); + // Check ID length + assert_eq!(second_record.id_length, None); + // Check record type + assert_eq!(second_record.record_type, [0x54]); + // Check ID + assert_eq!(second_record.id, None); + // Check payload + assert_eq!(second_record.payload, [0x2, 0x65, 0x6e, 0x57, 0x6f, 0x72, 0x6c, 0x64]); + + // Test first record deserialization + let first_record = records.pop().unwrap(); + // Check header + assert_eq!(first_record.header.type_name_format, TypeNameFormat::WellKnown); + assert!(!first_record.header.id_present); + assert!(first_record.header.short); + assert!(!first_record.header.chunk); + assert!(first_record.header.message_begin); + assert!(!first_record.header.message_end); + + // Check type length + assert_eq!(first_record.type_length, TYPE_LEN as u8); + // Check payload length + assert_eq!(first_record.payload_length, PAYLOAD_LEN as u32); + // Check ID length + assert_eq!(first_record.id_length, None); + // Check record type + assert_eq!(first_record.record_type, [0x54]); + // Check ID + assert_eq!(first_record.id, None); + // Check payload + assert_eq!(first_record.payload, [0x2, 0x65, 0x6e, 0x48, 0x65, 0x6C, 0x6C, 0x6F]); + } + + #[test] + fn test_serialize_ndef_message_with_two_text_records() { + const TYPE_LEN: usize = 1; + const PAYLOAD_LEN: usize = 8; + + let mut records = Vec::new(); + + // Create two NDEF records + let first_record: NdefRecord<32> = NdefRecord { + header: NdefRecordHeader { + type_name_format: TypeNameFormat::WellKnown, + id_present: false, + short: true, + chunk: false, + message_end: false, + message_begin: true, + }, + type_length: TYPE_LEN as u8, + payload_length: PAYLOAD_LEN as u32, + id_length: None, + record_type: Vec::from_slice(&[0x54]).unwrap(), + id: None, + payload: Vec::from_slice(&[0x2, 0x65, 0x6e, 0x48, 0x65, 0x6C, 0x6C, 0x6F]).unwrap(), // "Hello" + }; + records.push(first_record).unwrap(); + + let second_record: NdefRecord<32> = NdefRecord { + header: NdefRecordHeader { + type_name_format: TypeNameFormat::WellKnown, + id_present: false, + short: true, + chunk: false, + message_end: true, + message_begin: false, + }, + type_length: TYPE_LEN as u8, + payload_length: PAYLOAD_LEN as u32, + id_length: None, + record_type: Vec::from_slice(&[0x54]).unwrap(), + id: None, + payload: Vec::from_slice(&[0x2, 0x65, 0x6e, 0x57, 0x6f, 0x72, 0x6c, 0x64]).unwrap(), // "World" + }; + records.push(second_record).unwrap(); + + let tlv = NdefTlv::<32, 2> { + tl: TL { + tag: Tag::Ndef, + length: Some(24), // Length will be the sum of both records + }, + value: Some(records), + }; + + // Calculate the required buffer size + let mut buffer = [0u8; 256]; + let bytes_written = tlv.to_bytes(&mut buffer).unwrap(); + + // Expected serialized buffer based on the provided buffer in your test case + let expected_buffer = [ + 0x3, 0x18, 0x91, 0x1, 0x8, 0x54, 0x2, 0x65, 0x6e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x51, 0x1, 0x8, 0x54, 0x2, 0x65, + 0x6e, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0xfe, 0x0, 0x72, 0x64, 0x2d, 0x76, 0x31, 0x0, 0x0, 0x0, 0xfe, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + + // Assert that the serialized buffer matches the expected output + assert_eq!(&buffer[..bytes_written], &expected_buffer[..bytes_written]); + } +} diff --git a/ndef/src/tlv.rs b/ndef/src/tlv.rs new file mode 100644 index 0000000..b183be9 --- /dev/null +++ b/ndef/src/tlv.rs @@ -0,0 +1,405 @@ +use heapless::Vec; +use thiserror::Error; + +use crate::ndef_record::{NdefRecord, NdefRecordError}; + +/// TLV error types +#[derive(Error, Debug)] +pub enum TlvError { + #[error("Invalid TLV tag")] + InvalidTag, + #[error("Input buffer is empty")] + EmptyInputBuffer, + #[error("Incomplete input buffer")] + IncompleteInputBuffer, + #[error("Invalid TLV length")] + InvalidLength, + #[error("Maximum number of NDEF records exceeded, provide a larger MAX_RECORDS")] + MaxRecordsExceeded, + #[error("Provided buffer is too small, provided: {provided}, required: {required}")] + BufferTooSmall { provided: usize, required: usize }, + #[error("Unsupported Tag type, only NDEF is supported")] + NotNdefType, + #[error("Invalid NDEF record")] + NdefRecordError(#[from] NdefRecordError), + #[error("Too many NDEF records, maximum is {0}")] + TooManyRecords(usize), + #[error("Vector operations failed")] + VectorFull, +} + +/// Tag type part of NDEF TLV +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub enum Tag { + Null = 0x00, + Ndef = 0x03, + Proprietary = 0xFD, + Terminator = 0xFE, +} + +impl TryFrom for Tag { + type Error = TlvError; + + fn try_from(value: u8) -> Result { + match value { + 0x00 => Ok(Tag::Null), + 0x03 => Ok(Tag::Ndef), + 0xFD => Ok(Tag::Proprietary), + 0xFE => Ok(Tag::Terminator), + _ => Err(TlvError::InvalidTag), + } + } +} + +/// Type-Length (part of TLV) structure +#[derive(Debug)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct TL { + pub tag: Tag, + /// In the case of an NDEF tag, the length field indicates the length of the NDEF record. + pub length: Option, +} + +/// TLV (Type-Length-Value) structure specifically for NDEF messages. +/// +/// This structure represents a Type-Length-Value (TLV) block containing an NDEF record. +/// The TLV format is used to encapsulate NDEF messages in NFC Forum Type 5 Tags. +/// +/// # Type Parameters +/// * `MAX_PAYLOAD_SIZE`: Maximum size in bytes for the NDEF record payload +/// +/// # Fields +/// * `tl`: Type and Length fields of the TLV block +/// * `value`: The NDEF record contained in this TLV block +#[derive(Debug)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub struct NdefTlv { + /// Type and Length bytes of the TLV block + pub tl: TL, + /// The NDEF record value + pub value: Option, MAX_RECORDS>>, +} + +impl NdefTlv { + /// Creates a new NDEF TLV structure containing one or more NDEF records + /// + /// # Arguments + /// + /// * `records` - Slice of NDEF records to include in the TLV + /// + /// # Returns + /// + /// Returns a Result containing the NdefTlv if successful, or an Error if: + /// - The number of records exceeds MAX_RECORDS + /// - Vector operations fail + /// - The total length of all records exceeds the maximum allowed TLV length + pub fn new(records: &[NdefRecord]) -> Result { + if records.is_empty() { + return Ok(Self { + tl: TL { + tag: Tag::Ndef, + length: None, + }, + value: None, + }); + } + + if records.len() > MAX_RECORDS { + return Err(TlvError::TooManyRecords(MAX_RECORDS)); + } + + // Create vector of records, updating message begin/end flags + let mut ndef_records: Vec, MAX_RECORDS> = Vec::new(); + + for (index, record) in records.iter().enumerate() { + let mut record = record.clone(); + + // Update header flags for position in message + let is_first = index == 0; + let is_last = index == records.len() - 1; + + if is_first { + record.header.message_begin = true; + } + + if is_last { + record.header.message_end = true; + } + + ndef_records.push(record).map_err(|_| TlvError::VectorFull)?; + } + + // Calculate total length of all records + let total_length: u32 = ndef_records.iter().map(|record| record.serialized_size() as u32).sum(); + + Ok(Self { + tl: TL { + tag: Tag::Ndef, + length: Some(total_length), + }, + value: Some(ndef_records), + }) + } + + /// Parses a TLV structure from a byte slice. + /// + /// # Parameters + /// - `bytes`: A slice of bytes containing the TLV data. + /// + /// # Errors + /// Returns a `TlvError` if the input is too small, invalid, or not an NDEF type. + /// + /// # Example + /// ```ignore + /// let bytes = [0x03, 0x10, /* NDEF record bytes */]; + /// let ndef_tlv = NdefTlv::<1024>::from_bytes(&bytes)?; + /// ``` + pub fn from_bytes(bytes: &[u8]) -> Result { + // The TLV block must be at least 1 bytes long (Terminator TLV only contains the tag byte) + if bytes.is_empty() { + #[cfg(feature = "defmt-03")] + defmt::trace!("Buffer is empty"); + return Err(TlvError::EmptyInputBuffer); + } + + // Handle terminator TLV + if bytes[0] == Tag::Terminator as u8 { + return Ok(Self { + tl: TL { + tag: Tag::Terminator, + length: None, + }, + value: None, + }); + } + + // Parse TL bytes + // If the length field = 0xFF, we should parsed the extended field length and populate it + let tl = if bytes[1] == 0xFF { + if bytes.len() < 4 { + #[cfg(feature = "defmt-03")] + defmt::trace!("Buffer too small for extended length field"); + return Err(TlvError::IncompleteInputBuffer); + } + let length = ((bytes[1] as u32) << 16) | ((bytes[2] as u32) << 8) | (bytes[3] as u32); + TL { + tag: bytes[0].try_into().map_err(|_| TlvError::InvalidTag)?, + length: Some(length), + } + } else { + TL { + tag: bytes[0].try_into().map_err(|_| TlvError::InvalidTag)?, + length: Some(bytes[1] as u32), + } + }; + + // We only support NDEF tags + if tl.tag != Tag::Ndef { + return Err(TlvError::NotNdefType); + } + + // Parse NDEF record length + let value_length = tl.length.ok_or(TlvError::InvalidLength)? as usize; + if bytes.len() < 2 + value_length { + #[cfg(feature = "defmt-03")] + defmt::trace!("Buffer too small for NDEF record, need {} bytes", 2 + value_length); + return Err(TlvError::BufferTooSmall { + provided: bytes.len(), + required: 2 + value_length, + }); + } + + // Parse NDEF record + #[cfg(feature = "defmt-03")] + defmt::trace!("Attempting to parse NDEF records"); + let mut vec: Vec, MAX_RECORDS> = Vec::new(); + let mut offset = 2; // Start after initial 2 bytes + let mut total_bytes_processed = 0; + + while total_bytes_processed < value_length { + let remaining_bytes = &bytes[offset..]; + let (record, bytes_processed) = NdefRecord::from_bytes(remaining_bytes)?; + + if vec.push(record).is_err() { + return Err(TlvError::MaxRecordsExceeded); + } + + offset += bytes_processed; + total_bytes_processed += bytes_processed; + + if vec.last().unwrap().header.message_end() { + break; + } + } + + #[cfg(feature = "defmt-03")] + defmt::trace!("Successfully parsed {} NDEF records from TLV", vec.len()); + + let value = Some(vec); + + Ok(Self { tl, value }) + } + + /// Get the total size of the TLV structure + fn total_size(&self) -> Result { + let length = self.tl.length.ok_or(TlvError::InvalidLength)?; + Ok(2 + length as usize) + } + + /// Serializes the TLV structure to bytes, writing to a provided buffer. + /// + /// # Parameters + /// - `buffer`: A mutable byte slice to write the serialized data. + /// + /// # Returns + /// The number of bytes written on success. + /// + /// # Errors + /// Returns `TlvError::BufferTooSmall` if the buffer is too small. + /// + /// # Example + /// ```ignore + /// let mut buffer = [0u8; 128]; + /// let bytes_written = ndef_tlv.to_bytes(&mut buffer)?; + /// ``` + pub fn to_bytes(&self, buffer: &mut [u8]) -> Result { + let required_size = self.total_size()?; + + // Check if the buffer is too small + if buffer.len() < required_size { + return Err(TlvError::BufferTooSmall { + provided: buffer.len(), + required: required_size, + }); + } + + let mut offset = 0; + + // Write the tag + buffer[offset] = self.tl.tag as u8; + offset += 1; + + // Handle length field + if let Some(length) = self.tl.length { + if length > 0xFE { + // Extended length (0xFF followed by 2-byte length) + buffer[offset] = 0xFF; + offset += 1; + buffer[offset..offset + 2].copy_from_slice(&length.to_be_bytes()[1..3]); + offset += 2; + } else { + // Regular length + buffer[offset] = length as u8; + offset += 1; + } + + // Write the NDEF record value if present + if let Some(records) = &self.value { + for record in records { + let value_buffer = &mut buffer[offset..]; + + let bytes_written = record.to_bytes(value_buffer)?; + + offset += bytes_written; + } + } + } else { + return Err(TlvError::InvalidLength); + } + + // Insert terminator TLV + buffer[offset] = Tag::Terminator as u8; + offset += 1; + + Ok(offset) + } +} + +#[cfg(test)] +mod tests { + use super::{NdefTlv, Tag, TlvError}; + use crate::ndef_record::{NdefRecord, TypeNameFormat}; + + #[test] + fn test_parse_ndef_tlv() { + let bytes = [ + 0x3, 0x27, 0xd4, 0x1c, 0x8, 0x74, 0x68, 0x65, 0x72, 0x6d, 0x69, 0x67, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x72, + 0x75, 0x73, 0x74, 0x70, 0x6f, 0x73, 0x74, 0x63, 0x61, 0x72, 0x64, 0x2d, 0x76, 0x31, 0xd, 0xe, 0xa, 0xd, 0xb, 0xe, + 0xe, 0xf, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + ]; + let tlv = NdefTlv::<1024, 1>::from_bytes(&bytes).unwrap(); + + assert_eq!(tlv.tl.tag, Tag::Ndef); + assert_eq!(tlv.tl.length, Some(39)); + + let total_tlv_size = tlv.total_size().expect("total size should be present"); + let value = tlv.value.expect("value should be present").pop().unwrap(); + // Check header + assert_eq!(value.header.type_name_format, TypeNameFormat::External); + assert!(!value.header.id_present); + assert!(value.header.short); + assert!(!value.header.chunk); + assert!(value.header.message_begin); + assert!(value.header.message_end); + + // Check type length + assert_eq!(value.type_length, 28); + + // Check payload length + assert_eq!(value.payload_length, 8); + + // Check ID length + assert_eq!(value.id_length, None); + + // Check record type + assert_eq!( + value.record_type, + [ + 0x74, 0x68, 0x65, 0x72, 0x6d, 0x69, 0x67, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x72, 0x75, 0x73, 0x74, 0x70, + 0x6f, 0x73, 0x74, 0x63, 0x61, 0x72, 0x64, 0x2d, 0x76, 0x31 + ] + ); + + // Check ID + assert_eq!(value.id, None); + + assert_eq!(total_tlv_size, 41); + } + + #[test] + fn test_build_ndef_tlv() { + let payload = &[0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf]; + + let record = NdefRecord::new_external(b"thermigo.com", b"rustpostcard-v1", payload).unwrap(); + let message: NdefTlv<1024, 1> = NdefTlv::new(&[record]).unwrap(); + + let mut buffer = [0u8; 60]; + let _ = message.to_bytes(&mut buffer).unwrap(); + assert_eq!( + buffer, + [ + 0x3, 0x27, 0xd4, 0x1c, 0x8, 0x74, 0x68, 0x65, 0x72, 0x6d, 0x69, 0x67, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x3a, 0x72, + 0x75, 0x73, 0x74, 0x70, 0x6f, 0x73, 0x74, 0x63, 0x61, 0x72, 0x64, 0x2d, 0x76, 0x31, 0xd, 0xe, 0xa, 0xd, 0xb, + 0xe, 0xe, 0xf, 0xfe, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 + ] + ); + } + + #[test] + fn test_parse_ndef_tlv_errors() { + // Test buffer too small + let bytes = [0x03, 0x04, 0x01]; + assert!(matches!( + NdefTlv::<1024, 1>::from_bytes(&bytes), + Err(TlvError::BufferTooSmall { + provided: _, + required: _ + }) + )); + + // Test wrong tag type + let bytes = [0x00, 0x04, 0x01, 0x02, 0x03, 0x04]; + assert!(matches!(NdefTlv::<1024, 1>::from_bytes(&bytes), Err(TlvError::NotNdefType))); + } +}