VulpesOS/gaia/shared/js/nfc_utils.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

671 lines
19 KiB
JavaScript
Executable file

/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright © 2013, Deutsche Telekom, Inc. */
/* globals MozNDEFRecord, TextEncoder, TextDecoder, StringHelper */
/* exported NDEF, NfcBuffer, NfcUtils */
'use strict';
(function(exports) {
/**
* NDEF (NFC Data Exchange Format) contants and common values.
*/
const NDEF = {
MB: 1 << 7,
ME: 1 << 6,
CF: 1 << 5,
SR: 1 << 4,
IL: 1 << 3,
TNF: 0x07,
TNF_EMPTY: 'empty',
TNF_WELL_KNOWN: 'well-known',
TNF_MIME_MEDIA: 'media-type',
TNF_ABSOLUTE_URI: 'absolute-uri',
TNF_EXTERNAL_TYPE: 'external',
TNF_UNKNOWN: 'unknown',
TNF_UNCHANGED: 'unchanged',
RTD_TEXT: 0,
RTD_URI: 0,
RTD_SMART_POSTER: 0,
RTD_ALTERNATIVE_CARRIER: 0,
RTD_COLLISION_RESOLUTION: 0,
RTD_HANDOVER_CARRIER: 0,
RTD_HANDOVER_REQUEST: 0,
RTD_HANDOVER_SELECT: 0,
RTD_TEXT_IANA_LENGTH: 0x3F,
RTD_TEXT_ENCODING: 0x80,
RTD_TEXT_UTF8: 0,
RTD_TEXT_UTF16: 1,
CPS_INACTIVE: 0,
CPS_ACTIVE: 1,
CPS_ACTIVATING: 2,
CPS_UNKNOWN: 3,
MIME_BLUETOOTH_OOB: 0,
SMARTPOSTER_ACTION: 0,
// Action Record Values:
DO_ACTION: 0x00,
SAVE_FOR_LATER_ACTION: 0x01,
OPEN_FOR_EDITING_ACTION: 0x02,
RFU_ACTION: 0x03, // Reserved from 0x03 to 0xFF
URIS: [],
init: function ndef_init() {
var nfcUtils = new NfcUtils();
this.RTD_TEXT = nfcUtils.fromUTF8('T');
this.RTD_URI = nfcUtils.fromUTF8('U');
this.RTD_SMART_POSTER = nfcUtils.fromUTF8('Sp');
this.RTD_ALTERNATIVE_CARRIER = nfcUtils.fromUTF8('ac');
this.RTD_COLLISION_RESOLUTION = nfcUtils.fromUTF8('cr');
this.RTD_HANDOVER_CARRIER = nfcUtils.fromUTF8('Hc');
this.RTD_HANDOVER_REQUEST = nfcUtils.fromUTF8('Hr');
this.RTD_HANDOVER_SELECT = nfcUtils.fromUTF8('Hs');
this.MIME_BLUETOOTH_OOB =
nfcUtils.fromUTF8('application/vnd.bluetooth.ep.oob');
this.MIME_VCARD_STR_ARR =
['text/vcard', 'text/x-vCard', 'text/x-vcard'];
this.SMARTPOSTER_ACTION = nfcUtils.fromUTF8('act');
this.URIS[0x00] = '';
this.URIS[0x01] = 'http://www.';
this.URIS[0x02] = 'https://www.';
this.URIS[0x03] = 'http://';
this.URIS[0x04] = 'https://';
this.URIS[0x05] = 'tel:';
this.URIS[0x06] = 'mailto:';
this.URIS[0x07] = 'ftp://anonymous:anonymous@';
this.URIS[0x08] = 'ftp://ftp.';
this.URIS[0x09] = 'ftps://';
this.URIS[0x0A] = 'sftp://';
this.URIS[0x0B] = 'smb://';
this.URIS[0x0C] = 'nfs://';
this.URIS[0x0D] = 'ftp://';
this.URIS[0x0E] = 'dav://';
this.URIS[0x0F] = 'news:';
this.URIS[0x10] = 'telnet://';
this.URIS[0x11] = 'imap:';
this.URIS[0x12] = 'rtsp://';
this.URIS[0x13] = 'urn:';
this.URIS[0x14] = 'pop:';
this.URIS[0x15] = 'sip:';
this.URIS[0x16] = 'sips:';
this.URIS[0x17] = 'tftp:';
this.URIS[0x18] = 'btspp://';
this.URIS[0x19] = 'btl2cap://';
this.URIS[0x1A] = 'btgoep://';
this.URIS[0x1B] = 'tcpobex://';
this.URIS[0x1C] = 'irdaobex://';
this.URIS[0x1D] = 'file://';
this.URIS[0x1E] = 'urn:epc:id:';
this.URIS[0x1F] = 'urn:epc:tag:';
this.URIS[0x20] = 'urn:epc:pat:';
this.URIS[0x21] = 'urn:epc:raw:';
this.URIS[0x22] = 'urn:epc:';
this.URIS[0x23] = 'urn:nfc:';
},
payload: {
/**
* Decodes NDEF record payload
* @see NFCForum-TS-NDEF_1.0
* @param String tnf - record TNF
* @param {Uint8Array} type - record type
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decode: function decode(tnf, type, payload) {
var nfcUtils = new NfcUtils();
var decodedPayload = { type: 'empty' };
switch (tnf) {
case NDEF.TNF_WELL_KNOWN:
decodedPayload = this.decodeWellKnown(type, payload);
break;
case NDEF.TNF_MIME_MEDIA:
decodedPayload = this.decodeMIME(type, payload);
break;
case NDEF.TNF_ABSOLUTE_URI:
case NDEF.TNF_EXTERNAL_TYPE:
decodedPayload = { type: nfcUtils.toUTF8(type) };
break;
case NDEF.TNF_UNKNOWN:
decodedPayload = {};
break;
case NDEF.TNF_UNCHANGED:
decodedPayload = null;
break;
}
return decodedPayload;
},
/**
* Decodes TNF Well Know NDEF record payload
* @see NFCForum-TS-NDEF_1.0
* @param {Uint8Array} type - record type
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decodeWellKnown: function decodeWellKnown(type, payload) {
var nfcUtils = new NfcUtils();
if (nfcUtils.equalArrays(type, NDEF.RTD_TEXT)) {
return this.decodeText(payload);
} else if (nfcUtils.equalArrays(type, NDEF.RTD_URI)) {
return this.decodeURI(payload);
} else if (nfcUtils.equalArrays(type, NDEF.RTD_SMART_POSTER)) {
return this.decodeSmartPoster(payload);
}
return null;
},
/**
* Decodes TNF Well Known RTD Text NDEF record payload
* @see NFCForum-TS-RTD_Text_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload
*/
decodeText: function decodeText(payload) {
var nfcUtils = new NfcUtils();
var decoded = { type: 'text' };
var langLen = payload[0] & NDEF.RTD_TEXT_IANA_LENGTH;
decoded.language = nfcUtils.toUTF8(payload.subarray(1, langLen + 1));
var encoding = (payload[0] & NDEF.RTD_TEXT_ENCODING) !== 0 ? 1 : 0;
if (encoding === NDEF.RTD_TEXT_UTF8) {
decoded.text = nfcUtils.toUTF8(payload.subarray(langLen + 1));
decoded.encoding = 'UTF-8';
} else if (encoding === NDEF.RTD_TEXT_UTF16) {
decoded.text = nfcUtils.UTF16BytesToStr(payload.subarray(langLen +
1));
decoded.encoding = 'UTF-16';
}
return decoded;
},
/**
* Decodes TNF Well Known RTD URI NDEF record payload
* @see NFCForum-TS-RTD_URI_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload or null if invalid
*/
decodeURI: function decodeURI(payload) {
var nfcUtils = new NfcUtils();
var prefix = NDEF.URIS[payload[0]];
if (prefix === undefined) {
return null;
}
var suffix = nfcUtils.toUTF8(payload.subarray(1));
return { type: 'uri', uri: prefix + suffix };
},
/**
* Decodes TNF Well Known RTD Smart Poster NDEF record payload
* @see NFCForum-SmartPoster_RTD_1.0
* @param {Uint8Array} payload - record payload
* @returns {Object} data - decoded payload
*/
decodeSmartPoster: function decodeSmartPoster(payload) {
var nfcUtils = new NfcUtils();
var buffer = new NfcBuffer(payload);
var records = nfcUtils.parseNDEF(buffer);
// First, decode URI. It's treated specially, because it's the only
// mandatory record in a smart poster.
var URIRecords = records.filter(function(record) {
return nfcUtils.equalArrays(record.type, NDEF.RTD_URI);
});
if (URIRecords.length !== 1) {
return null;
}
var uriPoster = {
type: 'smartposter',
uri: NDEF.payload.decodeURI(URIRecords[0].payload).uri
};
// Now decode all other records and attach their data to poster.
return records.reduce((poster, record) => {
var typeStr = nfcUtils.toUTF8(record.type);
if (nfcUtils.equalArrays(record.type, NDEF.RTD_TEXT)) {
poster.text = poster.text || {};
var textData = NDEF.payload.decodeText(record.payload);
if (poster.text[textData.language]) {
// According to NFCForum-SmartPoster_RTD_1.0 3.3.2,
// there MUST NOT be two or more records with
// the same language identifier.
return null;
}
poster.text[textData.language] = textData.text;
} else if ('act' === typeStr) {
poster.action = record.payload[0];
} else if (NDEF.TNF_MIME_MEDIA === record.tnf) {
poster.icons = poster.icons || [];
poster.icons.push({
type: nfcUtils.toUTF8(record.type),
bytes: record.payload
});
}
return poster;
}, uriPoster);
},
/**
* Decodes TNF MIME Media NDEF Record payload
* @see NFCForum-TS-NDEF_1.0
* @param {Uint8Array} type - record mime-type
* @returns {Object} data - decoded payload
*/
decodeMIME: function docodeMIME(type, payload) {
var nfcUtils = new NfcUtils();
var typeStr = (typeof type === 'string') ? type : nfcUtils.toUTF8(type);
if (NDEF.MIME_VCARD_STR_ARR.indexOf(typeStr) !== -1) {
return {
type: 'text/vcard',
blob: new Blob([nfcUtils.toUTF8(payload)], {type: 'text/vcard'})
};
}
return { type: typeStr };
}
}
};
/**
* NfcBuffer wraps Uint8Array providing helper methods for accessing
* its elements.
*
* @param {Array} array Either a plain Array or an Uint8Array instance.
* @constructor
*/
function NfcBuffer(array) {
this.uint8array = new Uint8Array(array);
this.offset = 0;
}
NfcBuffer.prototype = {
uint8array: null,
offset: 0,
/**
* Returns a single octet from the buffer advancing position
* by one.
*
* @returns {Number} Single element from the buffer.
* @memberof NfcBuffer
*/
getOctet: function getOctet() {
if (this.offset == this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array[this.offset++];
},
/**
* Returns an Uint8Array containing first len elements from the
* buffer.
*
* @param {Number} len Number of elements to return.
* @returns {Uint8Array} Array of len first elements from the buffer.
* @memberof NfcBuffer
*/
getOctetArray: function getOctetArray(len) {
if (typeof len !== 'number' || len < 0 ||
this.offset + len > this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array.subarray(this.offset, this.offset += len);
},
/**
* Discards len elements from the buffer.
*
* @param {Number} len Number of elements to skip over.
* @memberof NfcBuffer
*/
skip: function skip(len) {
if (typeof len !== 'number' || len < 0 ||
this.offset + len > this.uint8array.length) {
throw Error('NfcBuffer too small');
}
this.offset += len;
},
/**
* Returns a single octet from the buffer, but will not
* advance a position.
*
* @returns {Number} Single element from the buffer.
* @memberof NfcBuffer
*/
peek: function peek() {
if (this.offset === this.uint8array.length) {
throw Error('NfcBuffer too small');
}
return this.uint8array[this.offset];
}
};
/**
* NfcUtils offers a set of utility function to handle NDEF messages according
* to NFCForum-TS-NDEF_1.0.
*
* @class NfcUtils
*/
function NfcUtils() { }
NfcUtils.prototype = {
/**
* Returns an Uint8Array representation of a string.
*
* @param {String} str String to convert.
* @return {Uint8Array}
* @memberof NfcUtils
*/
fromUTF8: function fromUTF8(str) {
if (!str) {
return null;
}
var enc = new TextEncoder('utf-8');
return enc.encode(str);
},
/**
* Returns a string representation of an Uint8Array.
*
* @param {Uint8Array} a Uint8Array instance.
* @return {String}
* @memberof NfcUtils
*/
toUTF8: function toUTF8(a) {
if (!a) {
return null;
}
// BOM removal
if (this.equalArrays(a.subarray(0, 3), [0xEF, 0xBB, 0xBF])) {
a = a.subarray(3);
}
var dec = new TextDecoder('utf-8');
return dec.decode(a);
},
/**
* Decodes UTF-16 bytes array into a String
*
* @param {Uint8Array} array containing UTF-16 encoded bytes
* @return {string}
* @memberof NfcUtils
*/
UTF16BytesToStr: function UTF16BytesToStr(array) {
if (!array) {
return null;
}
// if BOM not present Big-endian should be used
// NFCForum-TS-RTD_Text_1.0
var le = false;
var possibleBom = array.subarray(0, 2);
if (this.equalArrays(possibleBom, [0xFF, 0xFE])) {
array = array.subarray(2);
le = true;
} else if (this.equalArrays(possibleBom, [0xFE, 0xFF])) {
array = array.subarray(2);
}
var encoding = (le) ? 'utf-16le' : 'utf-16be';
var dec = new TextDecoder(encoding);
return dec.decode(array);
},
/**
* Ecodes string into Uint8Array conating UTF-16 BE bytes without BOM.
* @param {string}
* @return {Uint8Array}
* @memberof NfcUtils
*/
strToUTF16Bytes: function strToUTF16Bytes(str) {
if (!str) {
return null;
}
var enc = new TextEncoder('utf-16be');
return enc.encode(str);
},
/**
* Compares arrays, array-like objects and typed arrays for equality.
*
* @param {Array} a1 First array
* @param {Array} a2 Second array
* @return {Boolean} True if arrays are equal, false otherwise.
* @memberof NfcUtils
*/
equalArrays: function equalArrays(a1, a2) {
if (!a1 || !a2) {
return false;
}
if (a1.length !== a2.length) {
return false;
}
for (var i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) {
return false;
}
}
return true;
},
/**
* Takes an array of NDEFRecords and returns an Array of octets
* representing the binary encoding of the NDEF message according
* to the NFC Forum.
* Note: support for message chunking is not implemented.
*
* @param {Array} ndefRecords Array of NDEF records to encode.
* @return {Array} Byte array containing encoded NDEF message.
* @memberof NfcUtils
*/
encodeNDEF: function encodeNDEF(records) {
var result = [];
function getTnfNum(tnfString) {
var tnf = [
NDEF.TNF_EMPTY, NDEF.TNF_WELL_KNOWN, NDEF.TNF_MIME_MEDIA,
NDEF.TNF_ABSOLUTE_URI, NDEF.TNF_EXTERNAL_TYPE, NDEF.TNF_UNKNOWN,
NDEF.TNF_UNCHANGED, NDEF.TNF_RESERVED];
return tnf.indexOf(tnfString);
}
records.forEach((record, recordIndex) => {
record.payload = record.payload || [];
record.id = record.id || [];
record.type = record.type || [];
var payloadLen = record.payload.length;
var idLen = record.id.length;
var typeLen = record.type.length;
var firstOctet = getTnfNum(record.tnf) & 0x07;
firstOctet |= (recordIndex === 0) ? NDEF.MB : 0;
firstOctet |= (recordIndex === records.length - 1) ? NDEF.ME : 0;
firstOctet |= (idLen > 0) ? NDEF.IL : 0;
firstOctet |= (payloadLen <= 0xFF) ? NDEF.SR : 0;
result.push(firstOctet);
result.push(typeLen);
for (var p = (payloadLen > 0xFF ? 3 : 0); p >= 0; p -= 1) {
result.push((payloadLen >>> (8 * p)) & 0xFF);
}
if (idLen > 0) {
result.push(idLen);
}
result.push.apply(result, record.type);
result.push.apply(result, record.id);
result.push.apply(result, record.payload);
});
return result;
},
/**
* Parses a NDEF message contained in a NfcBuffer instance according to
* NFCForum-TS-NDEF_1.0.
*
* Usage:
* var buf = new NfcBuffer(<Uint8Array that contains the raw NDEF
* message>);
* var ndefMessage = NdefCodec.parse(buf);
*
* @param {NfcBuffer} Buffer containing the NDEF message.
* @return {Array} Array of MozNDEFRecord instances or null if message
* couldn't be parsed.
* @memberof NfcUtils
*/
parseNDEF: function parseNDEF(buffer) {
try {
return this._doParseNDEF(buffer);
} catch (err) {
console.error('[NfcUtils]: ' + err);
return null;
}
},
_doParseNDEF: function doParseNDEF(buffer) {
var records = [];
var isFirstRecord = true;
var firstOctet;
do {
firstOctet = buffer.peek();
if (isFirstRecord && !(firstOctet & NDEF.MB)) {
throw Error('MB bit not set in first NDEF record');
}
if (!isFirstRecord && (firstOctet & NDEF.MB)) {
throw Error('MB can only be set for the first record');
}
if (firstOctet & NDEF.CF) {
throw Error('Chunked payloads are not supported');
}
records.push(this._parseNDEFRecord(buffer));
isFirstRecord = false;
} while (!(firstOctet & NDEF.ME));
if (buffer.offset < buffer.uint8array.length) {
throw Error('ME bit set on non-last record');
}
return records;
},
_parseNDEFRecord: function parseNDEFRecord(buffer) {
var tnfArray = [
NDEF.TNF_EMPTY, NDEF.TNF_WELL_KNOWN, NDEF.TNF_MIME_MEDIA,
NDEF.TNF_ABSOLUTE_URI, NDEF.TNF_EXTERNAL_TYPE, NDEF.TNF_UNKNOWN,
NDEF.TNF_UNCHANGED, NDEF.TNF_RESERVED];
var firstOctet = buffer.getOctet();
var typeLen = buffer.getOctet();
var payloadLen = buffer.getOctet();
if (!(firstOctet & NDEF.SR)) {
for (var i = 0; i < 3; i++) {
payloadLen <<= 8;
payloadLen |= buffer.getOctet();
}
}
var idLen = 0;
if (firstOctet & NDEF.IL) {
idLen = buffer.getOctet();
}
var tnf = firstOctet & NDEF.TNF;
var type = buffer.getOctetArray(typeLen);
var id = buffer.getOctetArray(idLen);
var payload = buffer.getOctetArray(payloadLen);
return new MozNDEFRecord({tnf: tnfArray[tnf],
type: type || undefined,
id: id || undefined,
payload: payload || undefined});
},
/**
* Parses a URL string to MozNDEFRecord format.
*
* @param {String} url url string.
* @return {Array} Array of MozNDEFRecord instances.
* @memberof NfcUtils
*/
parseURIString: function parseURIString(url) {
if (!url) {
return;
}
var content = url;
for (var i = 1; i < NDEF.URIS.length; i++) {
var len = NDEF.URIS[i].length;
if (url.substring(0, len) == NDEF.URIS[i]) {
var uriPayload = url.substring(len);
content = String.fromCharCode(i) + uriPayload;
break;
}
}
var payload = StringHelper.fromUTF8(content);
var record = new MozNDEFRecord({tnf: NDEF.TNF_WELL_KNOWN,
type: NDEF.RTD_URI,
payload: payload});
if (!record) {
return null;
}
return [record];
}
};
NDEF.init();
exports.NfcBuffer = NfcBuffer;
exports.NfcUtils = NfcUtils;
exports.NDEF = NDEF;
})(window);