hackgt-nfc 0.4.0

A portable Rust library for working with HackGT's NFC badges
Documentation
use std::str;

#[derive(Debug, PartialEq)]
enum ParserState {
	None,
	NDEFInitial,
	NDEFTypeLength,
	NDEFPayloadLength,
	NDEFRecordType,
	NDEFData
}
#[derive(Debug, PartialEq)]
pub enum WellKnownType {
	Unknown,
	Text,
	URI
}

/// A very simple (and probably buggy) NDEF message parser based on TypeScript code I wrote for HackGT 5: https://github.com/HackGT/checkin-labels/blob/master/index.ts
pub struct NDEF {
	pub ndef_type: WellKnownType,
	pub data: Vec<u8>,
}

impl NDEF {
	pub fn parse(buffer: &[u8]) -> Result<Self, &'static str> {
		let mut state = ParserState::None;
		let mut data = Vec::with_capacity(0);
		let mut data_index: usize = 0;
		let mut ndef_type = WellKnownType::Unknown;

		let mut i: usize = 0;
		while i < buffer.len() {
			let byte = buffer[i];
			match state {
				ParserState::None => {
					if byte == 0x00 {
						// NULL block, skip
						i += 1;
					}
					else if byte == 0x03 && buffer.len() > i + 2 && buffer[i + 2] == 0xD1 {
						// NDEF message
						// Skip length field for now
						i += 1;
						state = ParserState::NDEFInitial;
					}
				},
				ParserState::NDEFInitial => {
					if (byte & 1 << 0) != 1 {
						return Err("Only NFC Well Known Records are supported");
					}
					if (byte & 1 << 4) == 0 {
						return Err("Only short records supported currently");
					}
					if (byte & 1 << 6) == 0 {
						return Err("Message must be end message currently");
					}
					if (byte & 1 << 7) == 0 {
						return Err("Message must be beginning message currently");
					}
					state = ParserState::NDEFTypeLength;
				},
				ParserState::NDEFTypeLength => {
					state = ParserState::NDEFPayloadLength;
				},
				ParserState::NDEFPayloadLength => {
					data = Vec::with_capacity(byte as usize);
					data_index = 0;
					state = ParserState::NDEFRecordType;
				},
				ParserState::NDEFRecordType => {
					ndef_type = match byte {
						0x54 => WellKnownType::Text,
						0x55 => WellKnownType::URI,
						_ => WellKnownType::Unknown,
					};
					state = ParserState::NDEFData;
				},
				ParserState::NDEFData => {
					// 0xFE terminates an NDEF message
					if byte == 0xFE {
						state = ParserState::None;
					}
					else {
						data.insert(data_index, byte);
						data_index += 1;
					}
				},
			}
			i += 1;
		}

		Ok(Self {
			ndef_type,
			data
		})
	}

	fn get_uri(&self) -> Option<String> {
		if self.data.len() < 2 || self.ndef_type != WellKnownType::URI {
			return None;
		}
		let url = str::from_utf8(&self.data[1..]).ok();
		url.map(|value| NDEF::get_protocol(self.data[0]).to_owned() + value)
	}

	fn get_text(&self) -> Option<String> {
		if self.data.len() < 4 || self.ndef_type != WellKnownType::Text {
			return None;
		}
		let language_code_length = self.data[0] as usize;
		str::from_utf8(&self.data[1 + language_code_length..]).ok().map(|value| value.to_owned())
	}

	pub fn get_content(&self) -> Option<String> {
		match self.ndef_type {
			WellKnownType::Text => self.get_text(),
			WellKnownType::URI => self.get_uri(),
			_ => None
		}
	}

	fn get_protocol(identifier: u8) -> &'static str {
		match identifier {
			0x00 => "",
			0x01 => "http://www.",
			0x02 => "https://www.",
			0x03 => "http://",
			0x04 => "https://",
			0x05 => "tel:",
			0x06 => "mailto:",
			0x07 => "ftp://anonymous:anonymous@",
			0x08 => "ftp://ftp.",
			0x09 => "ftps://",
			0x0A => "sftp://",
			0x0B => "smb://",
			0x0C => "nfs://",
			0x0D => "ftp://",
			0x0E => "dav://",
			0x0F => "news:",
			0x10 => "telnet://",
			0x11 => "imap:",
			0x12 => "rtsp://",
			0x13 => "urn:",
			0x14 => "pop:",
			0x15 => "sip:",
			0x16 => "sips:",
			0x17 => "tftp:",
			0x18 => "btspp://",
			0x19 => "btl2cap://",
			0x1A => "btgoep://",
			0x1B => "tcpobex://",
			0x1C => "irdaobex://",
			0x1D => "file://",
			0x1E => "urn: epc: id:",
			0x1F => "urn: epc: tag:",
			0x20 => "urn: epc: pat:",
			0x21 => "urn: epc: raw:",
			0x22 => "urn: epc:",
			0x23 => "urn: nfc:",
			_ => "",
		}
	}
}

#[cfg(test)]
mod tests {
	use super::NDEF;
	fn compare_data(data: &[u8], answer: &str) {
		let parsed = NDEF::parse(&data).unwrap();
		assert_eq!(parsed.get_content().unwrap(), answer);
	}
	#[test]
	fn parse_uri() {
		let data = [0x1, 0x3, 0xa0, 0xc, 0x34, 0x3, 0x3b, 0xd1, 0x1, 0x37, 0x55, 0x4, 0x6c, 0x69, 0x76, 0x65, 0x2e, 0x68, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x74, 0x3f, 0x75, 0x73, 0x65, 0x72, 0x3d, 0x37, 0x64, 0x64, 0x30, 0x30, 0x30, 0x32, 0x31, 0x2d, 0x38, 0x39, 0x66, 0x64, 0x2d, 0x34, 0x39, 0x66, 0x31, 0x2d, 0x39, 0x63, 0x31, 0x37, 0x2d, 0x62, 0x64, 0x30, 0x62, 0x61, 0x37, 0x64, 0x63, 0x66, 0x39, 0x37, 0x65, 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];
		compare_data(&data, "https://live.hack.gt?user=7dd00021-89fd-49f1-9c17-bd0ba7dcf97e");
		let data = [0x0, 0x0, 0x1, 0x3, 0xa0, 0xc, 0x34, 0x3, 0x3c, 0xd1, 0x1, 0x38, 0x55, 0x4, 0x6c, 0x69, 0x76, 0x65, 0x2e, 0x68, 0x61, 0x63, 0x6b, 0x2e, 0x67, 0x74, 0x2f, 0x3f, 0x75, 0x73, 0x65, 0x72, 0x3d, 0x63, 0x65, 0x65, 0x32, 0x30, 0x35, 0x32, 0x30, 0x2d, 0x61, 0x65, 0x66, 0x30, 0x2d, 0x34, 0x36, 0x32, 0x31, 0x2d, 0x61, 0x66, 0x39, 0x37, 0x2d, 0x30, 0x62, 0x35, 0x31, 0x63, 0x38, 0x30, 0x63, 0x30, 0x64, 0x39, 0x63, 0xfe];
		compare_data(&data, "https://live.hack.gt/?user=cee20520-aef0-4621-af97-0b51c80c0d9c");
	}
}