Commit 5ab5d1c1 authored by Leon Han's avatar Leon Han Committed by Commit Bot

[webnfc] Implement the algo for validating external types

The spec change:
https://github.com/w3c/web-nfc/pull/474/

BUG=520391

Change-Id: If1e4a6513410f3705bec4da7b2cc514c209a3cd9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1978622
Commit-Queue: Leon Han <leon.han@intel.com>
Reviewed-by: default avatarRijubrata Bhaumik <rijubrata.bhaumik@intel.com>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarFrançois Beaufort <beaufort.francois@gmail.com>
Cr-Commit-Position: refs/heads/master@{#727853}
parent 4f8b5432
...@@ -11,6 +11,7 @@ import android.nfc.FormatException; ...@@ -11,6 +11,7 @@ import android.nfc.FormatException;
import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.device.mojom.NdefMessage; import org.chromium.device.mojom.NdefMessage;
import org.chromium.device.mojom.NdefRecord; import org.chromium.device.mojom.NdefRecord;
import org.chromium.device.mojom.NdefRecordTypeCategory;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -80,16 +81,6 @@ public final class NdefMessageUtils { ...@@ -80,16 +81,6 @@ public final class NdefMessageUtils {
"urn:nfc:", // 0x23 "urn:nfc:", // 0x23
}; };
private static class PairOfDomainAndType {
private String mDomain;
private String mType;
private PairOfDomainAndType(String domain, String type) {
mDomain = domain;
mType = type;
}
}
/** /**
* Converts mojo NdefMessage to android.nfc.NdefMessage * Converts mojo NdefMessage to android.nfc.NdefMessage
*/ */
...@@ -136,33 +127,43 @@ public final class NdefMessageUtils { ...@@ -136,33 +127,43 @@ public final class NdefMessageUtils {
private static android.nfc.NdefRecord toNdefRecord(NdefRecord record) private static android.nfc.NdefRecord toNdefRecord(NdefRecord record)
throws InvalidNdefMessageException, IllegalArgumentException, throws InvalidNdefMessageException, IllegalArgumentException,
UnsupportedEncodingException { UnsupportedEncodingException {
switch (record.recordType) { if (record.category == NdefRecordTypeCategory.STANDARDIZED) {
case RECORD_TYPE_URL: switch (record.recordType) {
return createPlatformUrlRecord(record.data, record.id, false /* isAbsUrl */); case RECORD_TYPE_URL:
case RECORD_TYPE_ABSOLUTE_URL: return createPlatformUrlRecord(record.data, record.id, false /* isAbsUrl */);
return createPlatformUrlRecord(record.data, record.id, true /* isAbsUrl */); case RECORD_TYPE_ABSOLUTE_URL:
case RECORD_TYPE_TEXT: return createPlatformUrlRecord(record.data, record.id, true /* isAbsUrl */);
return createPlatformTextRecord( case RECORD_TYPE_TEXT:
record.id, record.lang, record.encoding, record.data); return createPlatformTextRecord(
case RECORD_TYPE_MIME: record.id, record.lang, record.encoding, record.data);
return createPlatformMimeRecord(record.mediaType, record.id, record.data); case RECORD_TYPE_MIME:
case RECORD_TYPE_UNKNOWN: return createPlatformMimeRecord(record.mediaType, record.id, record.data);
return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_UNKNOWN, case RECORD_TYPE_UNKNOWN:
null /* type */, return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_UNKNOWN,
record.id == null ? null : ApiCompatibilityUtils.getBytesUtf8(record.id), null /* type */,
record.data); record.id == null ? null
case RECORD_TYPE_EMPTY: : ApiCompatibilityUtils.getBytesUtf8(record.id),
return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_EMPTY, null /* type */, record.data);
null /* id */, null /* payload */); case RECORD_TYPE_EMPTY:
case RECORD_TYPE_SMART_POSTER: return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_EMPTY,
// TODO(https://crbug.com/520391): Support 'smart-poster' type records. null /* type */, null /* id */, null /* payload */);
throw new InvalidNdefMessageException(); case RECORD_TYPE_SMART_POSTER:
// TODO(https://crbug.com/520391): Support 'smart-poster' type records.
throw new InvalidNdefMessageException();
}
throw new InvalidNdefMessageException();
} }
// TODO(https://crbug.com/520391): Need to create an external record for either a custom
// type name or a local type name (for an embedded record). if (record.category == NdefRecordTypeCategory.EXTERNAL) {
PairOfDomainAndType pair = parseDomainAndType(record.recordType); if (isValidExternalType(record.recordType)) {
if (pair != null) { return createPlatformExternalRecord(record.recordType, record.id, record.data);
return createPlatformExternalRecord(pair.mDomain, pair.mType, record.id, record.data); }
throw new InvalidNdefMessageException();
}
if (record.category == NdefRecordTypeCategory.LOCAL) {
// TODO(https://crbug.com/520391): Support local type records.
throw new InvalidNdefMessageException();
} }
throw new InvalidNdefMessageException(); throw new InvalidNdefMessageException();
...@@ -215,6 +216,7 @@ public final class NdefMessageUtils { ...@@ -215,6 +216,7 @@ public final class NdefMessageUtils {
*/ */
private static NdefRecord createEmptyRecord() { private static NdefRecord createEmptyRecord() {
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.STANDARDIZED;
nfcRecord.recordType = RECORD_TYPE_EMPTY; nfcRecord.recordType = RECORD_TYPE_EMPTY;
nfcRecord.data = new byte[0]; nfcRecord.data = new byte[0];
return nfcRecord; return nfcRecord;
...@@ -226,6 +228,7 @@ public final class NdefMessageUtils { ...@@ -226,6 +228,7 @@ public final class NdefMessageUtils {
private static NdefRecord createURLRecord(Uri uri, boolean isAbsUrl) { private static NdefRecord createURLRecord(Uri uri, boolean isAbsUrl) {
if (uri == null) return null; if (uri == null) return null;
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.STANDARDIZED;
if (isAbsUrl) { if (isAbsUrl) {
nfcRecord.recordType = RECORD_TYPE_ABSOLUTE_URL; nfcRecord.recordType = RECORD_TYPE_ABSOLUTE_URL;
} else { } else {
...@@ -241,6 +244,7 @@ public final class NdefMessageUtils { ...@@ -241,6 +244,7 @@ public final class NdefMessageUtils {
*/ */
private static NdefRecord createMIMERecord(String mediaType, byte[] payload) { private static NdefRecord createMIMERecord(String mediaType, byte[] payload) {
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.STANDARDIZED;
nfcRecord.recordType = RECORD_TYPE_MIME; nfcRecord.recordType = RECORD_TYPE_MIME;
nfcRecord.mediaType = mediaType; nfcRecord.mediaType = mediaType;
nfcRecord.data = payload; nfcRecord.data = payload;
...@@ -257,6 +261,7 @@ public final class NdefMessageUtils { ...@@ -257,6 +261,7 @@ public final class NdefMessageUtils {
} }
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.STANDARDIZED;
nfcRecord.recordType = RECORD_TYPE_TEXT; nfcRecord.recordType = RECORD_TYPE_TEXT;
// According to NFCForum-TS-RTD_Text_1.0 specification, section 3.2.1 Syntax. // According to NFCForum-TS-RTD_Text_1.0 specification, section 3.2.1 Syntax.
// First byte of the payload is status byte, defined in Table 3: Status Byte Encodings. // First byte of the payload is status byte, defined in Table 3: Status Byte Encodings.
...@@ -289,6 +294,8 @@ public final class NdefMessageUtils { ...@@ -289,6 +294,8 @@ public final class NdefMessageUtils {
// TODO(https://crbug.com/520391): Support RTD_SMART_POSTER type records. // TODO(https://crbug.com/520391): Support RTD_SMART_POSTER type records.
// TODO(https://crbug.com/520391): Support local type records.
return null; return null;
} }
...@@ -297,6 +304,7 @@ public final class NdefMessageUtils { ...@@ -297,6 +304,7 @@ public final class NdefMessageUtils {
*/ */
private static NdefRecord createUnknownRecord(byte[] payload) { private static NdefRecord createUnknownRecord(byte[] payload) {
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.STANDARDIZED;
nfcRecord.recordType = RECORD_TYPE_UNKNOWN; nfcRecord.recordType = RECORD_TYPE_UNKNOWN;
nfcRecord.data = payload; nfcRecord.data = payload;
return nfcRecord; return nfcRecord;
...@@ -306,8 +314,8 @@ public final class NdefMessageUtils { ...@@ -306,8 +314,8 @@ public final class NdefMessageUtils {
* Constructs External type NdefRecord * Constructs External type NdefRecord
*/ */
private static NdefRecord createExternalTypeRecord(String type, byte[] payload) { private static NdefRecord createExternalTypeRecord(String type, byte[] payload) {
// |type| may be a custom type name or a local type name (for an embedded record).
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.EXTERNAL;
nfcRecord.recordType = type; nfcRecord.recordType = type;
nfcRecord.data = payload; nfcRecord.data = payload;
nfcRecord.payloadMessage = getNdefMessageFromPayload(payload); nfcRecord.payloadMessage = getNdefMessageFromPayload(payload);
...@@ -411,45 +419,39 @@ public final class NdefMessageUtils { ...@@ -411,45 +419,39 @@ public final class NdefMessageUtils {
* Creates a TNF_EXTERNAL_TYPE android.nfc.NdefRecord. * Creates a TNF_EXTERNAL_TYPE android.nfc.NdefRecord.
*/ */
public static android.nfc.NdefRecord createPlatformExternalRecord( public static android.nfc.NdefRecord createPlatformExternalRecord(
String domain, String type, String id, byte[] payload) { String recordType, String id, byte[] payload) {
// Already guaranteed by parseDomainAndType(). // Already guaranteed by the caller.
assert domain != null && !domain.isEmpty(); assert recordType != null && !recordType.isEmpty();
assert type != null && !type.isEmpty();
// NFC Forum requires that the domain and type used in an external record are treated as // NFC Forum requires that the domain and type used in an external record are treated as
// case insensitive, however Android intent filtering is always case sensitive. So we force // case insensitive, however Android intent filtering is always case sensitive. So we force
// the domain and type to lower-case here and later we will compare in a case insensitive // the domain and type to lower-case here and later we will compare in a case insensitive
// way when filtering by them. // way when filtering by them.
String record_type = domain.toLowerCase(Locale.ROOT) + ':' + type.toLowerCase(Locale.ROOT);
return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_EXTERNAL_TYPE, return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_EXTERNAL_TYPE,
ApiCompatibilityUtils.getBytesUtf8(record_type), ApiCompatibilityUtils.getBytesUtf8(recordType.toLowerCase(Locale.ROOT)),
id == null ? null : ApiCompatibilityUtils.getBytesUtf8(id), payload); id == null ? null : ApiCompatibilityUtils.getBytesUtf8(id), payload);
} }
/** /**
* Parses the input custom type to get its domain and type. * Validates external types.
* e.g. returns a pair ('w3.org', 'xyz') for the input 'w3.org:xyz'. * https://w3c.github.io/web-nfc/#dfn-validate-external-type
* Returns null for invalid input.
* https://w3c.github.io/web-nfc/#ndef-record-types
*
* TODO(https://crbug.com/520391): Refine the validation algorithm here accordingly once there
* is a conclusion on some case-sensitive things at https://github.com/w3c/web-nfc/issues/331.
*/ */
private static PairOfDomainAndType parseDomainAndType(String customType) { private static boolean isValidExternalType(String input) {
int colonIndex = customType.indexOf(':'); if (input.isEmpty() || input.length() > 255) return false;
if (colonIndex == -1) return null;
int colonIndex = input.lastIndexOf(':');
if (colonIndex == -1) return false;
// TODO(ThisCL): verify |domain| is a valid FQDN, asking help at String domain = input.substring(0, colonIndex).trim();
// https://groups.google.com/a/chromium.org/forum/#!topic/chromium-dev/QN2mHt_WgHo. if (domain.isEmpty()) return false;
String domain = customType.substring(0, colonIndex).trim(); // TODO(https://crbug.com/520391): Make sure |domain| can be converted successfully to ASCII
if (domain.isEmpty()) return null; // using IDN rules and does not contain any forbidden host code point.
String type = customType.substring(colonIndex + 1).trim(); String type = input.substring(colonIndex + 1).trim();
if (type.isEmpty()) return null; if (type.isEmpty()) return false;
if (!type.matches("[a-zA-Z0-9()+,\\-:=@;$_!*'.]+")) return null; if (!type.matches("[a-zA-Z0-9()+,\\-=@;$_*'.]+")) return false;
return new PairOfDomainAndType(domain, type); return true;
} }
/** /**
......
...@@ -48,6 +48,7 @@ import org.chromium.device.mojom.NdefMessage; ...@@ -48,6 +48,7 @@ import org.chromium.device.mojom.NdefMessage;
import org.chromium.device.mojom.NdefPushOptions; import org.chromium.device.mojom.NdefPushOptions;
import org.chromium.device.mojom.NdefPushTarget; import org.chromium.device.mojom.NdefPushTarget;
import org.chromium.device.mojom.NdefRecord; import org.chromium.device.mojom.NdefRecord;
import org.chromium.device.mojom.NdefRecordTypeCategory;
import org.chromium.device.mojom.NdefScanOptions; import org.chromium.device.mojom.NdefScanOptions;
import org.chromium.device.mojom.Nfc.CancelAllWatchesResponse; import org.chromium.device.mojom.Nfc.CancelAllWatchesResponse;
import org.chromium.device.mojom.Nfc.CancelPushResponse; import org.chromium.device.mojom.Nfc.CancelPushResponse;
...@@ -62,6 +63,7 @@ import java.io.UnsupportedEncodingException; ...@@ -62,6 +63,7 @@ import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/** /**
* Unit tests for NfcImpl and NdefMessageUtils classes. * Unit tests for NfcImpl and NdefMessageUtils classes.
...@@ -89,8 +91,7 @@ public class NFCTest { ...@@ -89,8 +91,7 @@ public class NFCTest {
private ArgumentCaptor<int[]> mOnWatchCallbackCaptor; private ArgumentCaptor<int[]> mOnWatchCallbackCaptor;
// Constants used for the test. // Constants used for the test.
private static final String DUMMY_EXTERNAL_RECORD_DOMAIN = "abc.com"; private static final String DUMMY_EXTERNAL_TYPE = "abc.com:xyz";
private static final String DUMMY_EXTERNAL_RECORD_TYPE = "xyz";
private static final String DUMMY_RECORD_ID = "https://www.example.com/ids/1"; private static final String DUMMY_RECORD_ID = "https://www.example.com/ids/1";
private static final String TEST_TEXT = "test"; private static final String TEST_TEXT = "test";
private static final String TEST_URL = "https://google.com"; private static final String TEST_URL = "https://google.com";
...@@ -219,6 +220,7 @@ public class NFCTest { ...@@ -219,6 +220,7 @@ public class NFCTest {
new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_EMPTY, null, null, null)); new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_EMPTY, null, null, null));
NdefMessage emptyMojoNdefMessage = NdefMessageUtils.toNdefMessage(emptyNdefMessage); NdefMessage emptyMojoNdefMessage = NdefMessageUtils.toNdefMessage(emptyNdefMessage);
assertEquals(1, emptyMojoNdefMessage.data.length); assertEquals(1, emptyMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, emptyMojoNdefMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_EMPTY, emptyMojoNdefMessage.data[0].recordType); assertEquals(NdefMessageUtils.RECORD_TYPE_EMPTY, emptyMojoNdefMessage.data[0].recordType);
assertEquals(null, emptyMojoNdefMessage.data[0].mediaType); assertEquals(null, emptyMojoNdefMessage.data[0].mediaType);
assertEquals(null, emptyMojoNdefMessage.data[0].id); assertEquals(null, emptyMojoNdefMessage.data[0].id);
...@@ -233,6 +235,7 @@ public class NFCTest { ...@@ -233,6 +235,7 @@ public class NFCTest {
false /* isAbsUrl */)); false /* isAbsUrl */));
NdefMessage urlMojoNdefMessage = NdefMessageUtils.toNdefMessage(urlNdefMessage); NdefMessage urlMojoNdefMessage = NdefMessageUtils.toNdefMessage(urlNdefMessage);
assertEquals(1, urlMojoNdefMessage.data.length); assertEquals(1, urlMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, urlMojoNdefMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_URL, urlMojoNdefMessage.data[0].recordType); assertEquals(NdefMessageUtils.RECORD_TYPE_URL, urlMojoNdefMessage.data[0].recordType);
assertEquals(null, urlMojoNdefMessage.data[0].mediaType); assertEquals(null, urlMojoNdefMessage.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, urlMojoNdefMessage.data[0].id); assertEquals(DUMMY_RECORD_ID, urlMojoNdefMessage.data[0].id);
...@@ -247,6 +250,7 @@ public class NFCTest { ...@@ -247,6 +250,7 @@ public class NFCTest {
true /* isAbsUrl */)); true /* isAbsUrl */));
NdefMessage absUrlMojoNdefMessage = NdefMessageUtils.toNdefMessage(absUrlNdefMessage); NdefMessage absUrlMojoNdefMessage = NdefMessageUtils.toNdefMessage(absUrlNdefMessage);
assertEquals(1, absUrlMojoNdefMessage.data.length); assertEquals(1, absUrlMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, absUrlMojoNdefMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_ABSOLUTE_URL, assertEquals(NdefMessageUtils.RECORD_TYPE_ABSOLUTE_URL,
absUrlMojoNdefMessage.data[0].recordType); absUrlMojoNdefMessage.data[0].recordType);
assertEquals(null, absUrlMojoNdefMessage.data[0].mediaType); assertEquals(null, absUrlMojoNdefMessage.data[0].mediaType);
...@@ -259,6 +263,7 @@ public class NFCTest { ...@@ -259,6 +263,7 @@ public class NFCTest {
ENCODING_UTF8, ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT))); ENCODING_UTF8, ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT)));
NdefMessage utf8TextMojoNdefMessage = NdefMessageUtils.toNdefMessage(utf8TextNdefMessage); NdefMessage utf8TextMojoNdefMessage = NdefMessageUtils.toNdefMessage(utf8TextNdefMessage);
assertEquals(1, utf8TextMojoNdefMessage.data.length); assertEquals(1, utf8TextMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, utf8TextMojoNdefMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_TEXT, utf8TextMojoNdefMessage.data[0].recordType); assertEquals(NdefMessageUtils.RECORD_TYPE_TEXT, utf8TextMojoNdefMessage.data[0].recordType);
assertEquals(null, utf8TextMojoNdefMessage.data[0].mediaType); assertEquals(null, utf8TextMojoNdefMessage.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, utf8TextMojoNdefMessage.data[0].id); assertEquals(DUMMY_RECORD_ID, utf8TextMojoNdefMessage.data[0].id);
...@@ -274,6 +279,8 @@ public class NFCTest { ...@@ -274,6 +279,8 @@ public class NFCTest {
DUMMY_RECORD_ID, LANG_EN_US, ENCODING_UTF16, textBytes)); DUMMY_RECORD_ID, LANG_EN_US, ENCODING_UTF16, textBytes));
NdefMessage utf16TextMojoNdefMessage = NdefMessageUtils.toNdefMessage(utf16TextNdefMessage); NdefMessage utf16TextMojoNdefMessage = NdefMessageUtils.toNdefMessage(utf16TextNdefMessage);
assertEquals(1, utf16TextMojoNdefMessage.data.length); assertEquals(1, utf16TextMojoNdefMessage.data.length);
assertEquals(
NdefRecordTypeCategory.STANDARDIZED, utf16TextMojoNdefMessage.data[0].category);
assertEquals( assertEquals(
NdefMessageUtils.RECORD_TYPE_TEXT, utf16TextMojoNdefMessage.data[0].recordType); NdefMessageUtils.RECORD_TYPE_TEXT, utf16TextMojoNdefMessage.data[0].recordType);
assertEquals(null, utf16TextMojoNdefMessage.data[0].mediaType); assertEquals(null, utf16TextMojoNdefMessage.data[0].mediaType);
...@@ -288,6 +295,7 @@ public class NFCTest { ...@@ -288,6 +295,7 @@ public class NFCTest {
TEXT_MIME, DUMMY_RECORD_ID, ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT))); TEXT_MIME, DUMMY_RECORD_ID, ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT)));
NdefMessage mimeMojoNdefMessage = NdefMessageUtils.toNdefMessage(mimeNdefMessage); NdefMessage mimeMojoNdefMessage = NdefMessageUtils.toNdefMessage(mimeNdefMessage);
assertEquals(1, mimeMojoNdefMessage.data.length); assertEquals(1, mimeMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, mimeMojoNdefMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_MIME, mimeMojoNdefMessage.data[0].recordType); assertEquals(NdefMessageUtils.RECORD_TYPE_MIME, mimeMojoNdefMessage.data[0].recordType);
assertEquals(TEXT_MIME, mimeMojoNdefMessage.data[0].mediaType); assertEquals(TEXT_MIME, mimeMojoNdefMessage.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, mimeMojoNdefMessage.data[0].id); assertEquals(DUMMY_RECORD_ID, mimeMojoNdefMessage.data[0].id);
...@@ -301,6 +309,7 @@ public class NFCTest { ...@@ -301,6 +309,7 @@ public class NFCTest {
JSON_MIME, DUMMY_RECORD_ID, ApiCompatibilityUtils.getBytesUtf8(TEST_JSON))); JSON_MIME, DUMMY_RECORD_ID, ApiCompatibilityUtils.getBytesUtf8(TEST_JSON)));
NdefMessage jsonMojoNdefMessage = NdefMessageUtils.toNdefMessage(jsonNdefMessage); NdefMessage jsonMojoNdefMessage = NdefMessageUtils.toNdefMessage(jsonNdefMessage);
assertEquals(1, jsonMojoNdefMessage.data.length); assertEquals(1, jsonMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, jsonMojoNdefMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_MIME, jsonMojoNdefMessage.data[0].recordType); assertEquals(NdefMessageUtils.RECORD_TYPE_MIME, jsonMojoNdefMessage.data[0].recordType);
assertEquals(JSON_MIME, jsonMojoNdefMessage.data[0].mediaType); assertEquals(JSON_MIME, jsonMojoNdefMessage.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, jsonMojoNdefMessage.data[0].id); assertEquals(DUMMY_RECORD_ID, jsonMojoNdefMessage.data[0].id);
...@@ -315,6 +324,7 @@ public class NFCTest { ...@@ -315,6 +324,7 @@ public class NFCTest {
ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT))); ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT)));
NdefMessage unknownMojoNdefMessage = NdefMessageUtils.toNdefMessage(unknownNdefMessage); NdefMessage unknownMojoNdefMessage = NdefMessageUtils.toNdefMessage(unknownNdefMessage);
assertEquals(1, unknownMojoNdefMessage.data.length); assertEquals(1, unknownMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, unknownMojoNdefMessage.data[0].category);
assertEquals( assertEquals(
NdefMessageUtils.RECORD_TYPE_UNKNOWN, unknownMojoNdefMessage.data[0].recordType); NdefMessageUtils.RECORD_TYPE_UNKNOWN, unknownMojoNdefMessage.data[0].recordType);
assertEquals(DUMMY_RECORD_ID, unknownMojoNdefMessage.data[0].id); assertEquals(DUMMY_RECORD_ID, unknownMojoNdefMessage.data[0].id);
...@@ -323,14 +333,13 @@ public class NFCTest { ...@@ -323,14 +333,13 @@ public class NFCTest {
assertEquals(TEST_TEXT, new String(unknownMojoNdefMessage.data[0].data)); assertEquals(TEST_TEXT, new String(unknownMojoNdefMessage.data[0].data));
// Test external record conversion. // Test external record conversion.
android.nfc.NdefMessage extNdefMessage = android.nfc.NdefMessage extNdefMessage = new android.nfc.NdefMessage(
new android.nfc.NdefMessage(NdefMessageUtils.createPlatformExternalRecord( NdefMessageUtils.createPlatformExternalRecord(DUMMY_EXTERNAL_TYPE, DUMMY_RECORD_ID,
DUMMY_EXTERNAL_RECORD_DOMAIN, DUMMY_EXTERNAL_RECORD_TYPE, DUMMY_RECORD_ID,
ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT))); ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT)));
NdefMessage extMojoNdefMessage = NdefMessageUtils.toNdefMessage(extNdefMessage); NdefMessage extMojoNdefMessage = NdefMessageUtils.toNdefMessage(extNdefMessage);
assertEquals(1, extMojoNdefMessage.data.length); assertEquals(1, extMojoNdefMessage.data.length);
assertEquals(DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE, assertEquals(NdefRecordTypeCategory.EXTERNAL, extMojoNdefMessage.data[0].category);
extMojoNdefMessage.data[0].recordType); assertEquals(DUMMY_EXTERNAL_TYPE, extMojoNdefMessage.data[0].recordType);
assertEquals(null, extMojoNdefMessage.data[0].mediaType); assertEquals(null, extMojoNdefMessage.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, extMojoNdefMessage.data[0].id); assertEquals(DUMMY_RECORD_ID, extMojoNdefMessage.data[0].id);
assertNull(extMojoNdefMessage.data[0].encoding); assertNull(extMojoNdefMessage.data[0].encoding);
...@@ -342,19 +351,20 @@ public class NFCTest { ...@@ -342,19 +351,20 @@ public class NFCTest {
android.nfc.NdefRecord.createTextRecord(LANG_EN_US, TEST_TEXT)); android.nfc.NdefRecord.createTextRecord(LANG_EN_US, TEST_TEXT));
byte[] payloadBytes = payloadMessage.toByteArray(); byte[] payloadBytes = payloadMessage.toByteArray();
// Put |payloadBytes| as payload of an external record. // Put |payloadBytes| as payload of an external record.
android.nfc.NdefMessage extNdefMessage1 = new android.nfc.NdefMessage( android.nfc.NdefMessage extNdefMessage1 =
NdefMessageUtils.createPlatformExternalRecord(DUMMY_EXTERNAL_RECORD_DOMAIN, new android.nfc.NdefMessage(NdefMessageUtils.createPlatformExternalRecord(
DUMMY_EXTERNAL_RECORD_TYPE, DUMMY_RECORD_ID, payloadBytes)); DUMMY_EXTERNAL_TYPE, DUMMY_RECORD_ID, payloadBytes));
NdefMessage extMojoNdefMessage1 = NdefMessageUtils.toNdefMessage(extNdefMessage1); NdefMessage extMojoNdefMessage1 = NdefMessageUtils.toNdefMessage(extNdefMessage1);
assertEquals(1, extMojoNdefMessage1.data.length); assertEquals(1, extMojoNdefMessage1.data.length);
assertEquals(DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE, assertEquals(NdefRecordTypeCategory.EXTERNAL, extMojoNdefMessage1.data[0].category);
extMojoNdefMessage1.data[0].recordType); assertEquals(DUMMY_EXTERNAL_TYPE, extMojoNdefMessage1.data[0].recordType);
assertEquals(null, extMojoNdefMessage1.data[0].mediaType); assertEquals(null, extMojoNdefMessage1.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, extMojoNdefMessage1.data[0].id); assertEquals(DUMMY_RECORD_ID, extMojoNdefMessage1.data[0].id);
// The embedded ndef message should have content corresponding with the original // The embedded ndef message should have content corresponding with the original
// |payloadMessage|. // |payloadMessage|.
NdefMessage payloadMojoMessage = extMojoNdefMessage1.data[0].payloadMessage; NdefMessage payloadMojoMessage = extMojoNdefMessage1.data[0].payloadMessage;
assertEquals(1, payloadMojoMessage.data.length); assertEquals(1, payloadMojoMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, payloadMojoMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_TEXT, payloadMojoMessage.data[0].recordType); assertEquals(NdefMessageUtils.RECORD_TYPE_TEXT, payloadMojoMessage.data[0].recordType);
assertEquals(null, payloadMojoMessage.data[0].mediaType); assertEquals(null, payloadMojoMessage.data[0].mediaType);
assertEquals(TEST_TEXT, new String(payloadMojoMessage.data[0].data)); assertEquals(TEST_TEXT, new String(payloadMojoMessage.data[0].data));
...@@ -368,6 +378,7 @@ public class NFCTest { ...@@ -368,6 +378,7 @@ public class NFCTest {
public void testMojoToNdefConversion() throws InvalidNdefMessageException { public void testMojoToNdefConversion() throws InvalidNdefMessageException {
// Test url record conversion. // Test url record conversion.
NdefRecord urlMojoNdefRecord = new NdefRecord(); NdefRecord urlMojoNdefRecord = new NdefRecord();
urlMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
urlMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_URL; urlMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_URL;
urlMojoNdefRecord.id = DUMMY_RECORD_ID; urlMojoNdefRecord.id = DUMMY_RECORD_ID;
urlMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_URL); urlMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_URL);
...@@ -383,6 +394,7 @@ public class NFCTest { ...@@ -383,6 +394,7 @@ public class NFCTest {
// Test absolute-url record conversion. // Test absolute-url record conversion.
NdefRecord absUrlMojoNdefRecord = new NdefRecord(); NdefRecord absUrlMojoNdefRecord = new NdefRecord();
absUrlMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
absUrlMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_ABSOLUTE_URL; absUrlMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_ABSOLUTE_URL;
absUrlMojoNdefRecord.id = DUMMY_RECORD_ID; absUrlMojoNdefRecord.id = DUMMY_RECORD_ID;
absUrlMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_URL); absUrlMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_URL);
...@@ -397,6 +409,7 @@ public class NFCTest { ...@@ -397,6 +409,7 @@ public class NFCTest {
// Test text record conversion for UTF-8 content. // Test text record conversion for UTF-8 content.
NdefRecord utf8TextMojoNdefRecord = new NdefRecord(); NdefRecord utf8TextMojoNdefRecord = new NdefRecord();
utf8TextMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
utf8TextMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_TEXT; utf8TextMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_TEXT;
utf8TextMojoNdefRecord.id = DUMMY_RECORD_ID; utf8TextMojoNdefRecord.id = DUMMY_RECORD_ID;
utf8TextMojoNdefRecord.encoding = ENCODING_UTF8; utf8TextMojoNdefRecord.encoding = ENCODING_UTF8;
...@@ -423,6 +436,7 @@ public class NFCTest { ...@@ -423,6 +436,7 @@ public class NFCTest {
// Test text record conversion for UTF-16 content. // Test text record conversion for UTF-16 content.
NdefRecord utf16TextMojoNdefRecord = new NdefRecord(); NdefRecord utf16TextMojoNdefRecord = new NdefRecord();
utf16TextMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
utf16TextMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_TEXT; utf16TextMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_TEXT;
utf16TextMojoNdefRecord.id = DUMMY_RECORD_ID; utf16TextMojoNdefRecord.id = DUMMY_RECORD_ID;
utf16TextMojoNdefRecord.encoding = ENCODING_UTF16; utf16TextMojoNdefRecord.encoding = ENCODING_UTF16;
...@@ -450,6 +464,7 @@ public class NFCTest { ...@@ -450,6 +464,7 @@ public class NFCTest {
// Test mime record conversion with "text/plain" mime type. // Test mime record conversion with "text/plain" mime type.
NdefRecord mimeMojoNdefRecord = new NdefRecord(); NdefRecord mimeMojoNdefRecord = new NdefRecord();
mimeMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
mimeMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_MIME; mimeMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_MIME;
mimeMojoNdefRecord.mediaType = TEXT_MIME; mimeMojoNdefRecord.mediaType = TEXT_MIME;
mimeMojoNdefRecord.id = DUMMY_RECORD_ID; mimeMojoNdefRecord.id = DUMMY_RECORD_ID;
...@@ -466,6 +481,7 @@ public class NFCTest { ...@@ -466,6 +481,7 @@ public class NFCTest {
// Test mime record conversion with "application/json" mime type. // Test mime record conversion with "application/json" mime type.
NdefRecord jsonMojoNdefRecord = new NdefRecord(); NdefRecord jsonMojoNdefRecord = new NdefRecord();
jsonMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
jsonMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_MIME; jsonMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_MIME;
jsonMojoNdefRecord.mediaType = JSON_MIME; jsonMojoNdefRecord.mediaType = JSON_MIME;
jsonMojoNdefRecord.id = DUMMY_RECORD_ID; jsonMojoNdefRecord.id = DUMMY_RECORD_ID;
...@@ -482,6 +498,7 @@ public class NFCTest { ...@@ -482,6 +498,7 @@ public class NFCTest {
// Test unknown record conversion. // Test unknown record conversion.
NdefRecord unknownMojoNdefRecord = new NdefRecord(); NdefRecord unknownMojoNdefRecord = new NdefRecord();
unknownMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
unknownMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_UNKNOWN; unknownMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_UNKNOWN;
unknownMojoNdefRecord.id = DUMMY_RECORD_ID; unknownMojoNdefRecord.id = DUMMY_RECORD_ID;
unknownMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT); unknownMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
...@@ -496,8 +513,8 @@ public class NFCTest { ...@@ -496,8 +513,8 @@ public class NFCTest {
// Test external record conversion. // Test external record conversion.
NdefRecord extMojoNdefRecord = new NdefRecord(); NdefRecord extMojoNdefRecord = new NdefRecord();
extMojoNdefRecord.recordType = extMojoNdefRecord.category = NdefRecordTypeCategory.EXTERNAL;
DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE; extMojoNdefRecord.recordType = DUMMY_EXTERNAL_TYPE;
extMojoNdefRecord.id = DUMMY_RECORD_ID; extMojoNdefRecord.id = DUMMY_RECORD_ID;
extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT); extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
NdefMessage extMojoNdefMessage = createMojoNdefMessage(extMojoNdefRecord); NdefMessage extMojoNdefMessage = createMojoNdefMessage(extMojoNdefRecord);
...@@ -505,13 +522,13 @@ public class NFCTest { ...@@ -505,13 +522,13 @@ public class NFCTest {
assertEquals(1, extNdefMessage.getRecords().length); assertEquals(1, extNdefMessage.getRecords().length);
assertEquals( assertEquals(
android.nfc.NdefRecord.TNF_EXTERNAL_TYPE, extNdefMessage.getRecords()[0].getTnf()); android.nfc.NdefRecord.TNF_EXTERNAL_TYPE, extNdefMessage.getRecords()[0].getTnf());
assertEquals(DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE, assertEquals(DUMMY_EXTERNAL_TYPE, new String(extNdefMessage.getRecords()[0].getType()));
new String(extNdefMessage.getRecords()[0].getType()));
assertEquals(DUMMY_RECORD_ID, new String(extNdefMessage.getRecords()[0].getId())); assertEquals(DUMMY_RECORD_ID, new String(extNdefMessage.getRecords()[0].getId()));
assertEquals(TEST_TEXT, new String(extNdefMessage.getRecords()[0].getPayload())); assertEquals(TEST_TEXT, new String(extNdefMessage.getRecords()[0].getPayload()));
// Test EMPTY record conversion. // Test EMPTY record conversion.
NdefRecord emptyMojoNdefRecord = new NdefRecord(); NdefRecord emptyMojoNdefRecord = new NdefRecord();
emptyMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
emptyMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_EMPTY; emptyMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_EMPTY;
NdefMessage emptyMojoNdefMessage = createMojoNdefMessage(emptyMojoNdefRecord); NdefMessage emptyMojoNdefMessage = createMojoNdefMessage(emptyMojoNdefRecord);
android.nfc.NdefMessage emptyNdefMessage = android.nfc.NdefMessage emptyNdefMessage =
...@@ -526,13 +543,49 @@ public class NFCTest { ...@@ -526,13 +543,49 @@ public class NFCTest {
@Test(expected = InvalidNdefMessageException.class) @Test(expected = InvalidNdefMessageException.class)
@Feature({"NFCTest"}) @Feature({"NFCTest"})
public void testInvalidExternalRecordType() throws InvalidNdefMessageException { public void testInvalidExternalRecordType() throws InvalidNdefMessageException {
NdefRecord extMojoNdefRecord = new NdefRecord(); {
// '/' is not allowed. NdefRecord extMojoNdefRecord = new NdefRecord();
extMojoNdefRecord.recordType = "abc.com:xyz/"; extMojoNdefRecord.category = NdefRecordTypeCategory.EXTERNAL;
extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT); // Must have a ':'.
NdefMessage extMojoNdefMessage = createMojoNdefMessage(extMojoNdefRecord); extMojoNdefRecord.recordType = "abc.com";
android.nfc.NdefMessage extNdefMessage = NdefMessageUtils.toNdefMessage(extMojoNdefMessage); extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
assertEquals(null, extNdefMessage); NdefMessage extMojoNdefMessage = createMojoNdefMessage(extMojoNdefRecord);
android.nfc.NdefMessage extNdefMessage =
NdefMessageUtils.toNdefMessage(extMojoNdefMessage);
assertNull(extNdefMessage);
}
{
NdefRecord extMojoNdefRecord = new NdefRecord();
extMojoNdefRecord.category = NdefRecordTypeCategory.EXTERNAL;
extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
char[] chars = new char[251];
Arrays.fill(chars, 'a');
String domain = new String(chars);
// |recordType|'s length is 255, OK.
extMojoNdefRecord.recordType = domain + ":xyz";
android.nfc.NdefMessage extNdefMessage_255 =
NdefMessageUtils.toNdefMessage(createMojoNdefMessage(extMojoNdefRecord));
assertNotNull(extNdefMessage_255);
// Exceeding the maximum length 255, FAIL.
extMojoNdefRecord.recordType = domain + ":xyze";
android.nfc.NdefMessage extNdefMessage_256 =
NdefMessageUtils.toNdefMessage(createMojoNdefMessage(extMojoNdefRecord));
assertNull(extNdefMessage_256);
}
{
NdefRecord extMojoNdefRecord = new NdefRecord();
extMojoNdefRecord.category = NdefRecordTypeCategory.EXTERNAL;
// '/' is not allowed.
extMojoNdefRecord.recordType = "abc.com:xyz/";
extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
NdefMessage extMojoNdefMessage = createMojoNdefMessage(extMojoNdefRecord);
android.nfc.NdefMessage extNdefMessage =
NdefMessageUtils.toNdefMessage(extMojoNdefMessage);
assertNull(extNdefMessage);
}
} }
/** /**
...@@ -1093,6 +1146,7 @@ public class NFCTest { ...@@ -1093,6 +1146,7 @@ public class NFCTest {
// Create message with empty record. // Create message with empty record.
NdefRecord emptyNdefRecord = new NdefRecord(); NdefRecord emptyNdefRecord = new NdefRecord();
emptyNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
emptyNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_EMPTY; emptyNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_EMPTY;
NdefMessage ndefMessage = createMojoNdefMessage(emptyNdefRecord); NdefMessage ndefMessage = createMojoNdefMessage(emptyNdefRecord);
...@@ -1123,6 +1177,7 @@ public class NFCTest { ...@@ -1123,6 +1177,7 @@ public class NFCTest {
message.data = new NdefRecord[1]; message.data = new NdefRecord[1];
NdefRecord nfcRecord = new NdefRecord(); NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.STANDARDIZED;
nfcRecord.recordType = NdefMessageUtils.RECORD_TYPE_TEXT; nfcRecord.recordType = NdefMessageUtils.RECORD_TYPE_TEXT;
nfcRecord.encoding = ENCODING_UTF8; nfcRecord.encoding = ENCODING_UTF8;
nfcRecord.lang = LANG_EN_US; nfcRecord.lang = LANG_EN_US;
......
...@@ -29,13 +29,36 @@ enum NDEFPushTarget { ...@@ -29,13 +29,36 @@ enum NDEFPushTarget {
ANY ANY
}; };
// https://w3c.github.io/web-nfc/#the-record-type-string
enum NDEFRecordTypeCategory {
// Standardized well known types, including "empty", "text", "url",
// "smart-poster", "absolute-url", "mime", and "unknown", etc.
// https://w3c.github.io/web-nfc/#dfn-well-known-type-name
kStandardized,
// External types that follow the rule defined by
// https://w3c.github.io/web-nfc/#dfn-external-type-name.
kExternal,
// Local types that follow the rule defined by
// https://w3c.github.io/web-nfc/#dfn-local-type-name.
kLocal
};
struct NDEFError { struct NDEFError {
NDEFErrorType error_type; NDEFErrorType error_type;
}; };
// https://w3c.github.io/web-nfc/#dom-ndefrecord // https://w3c.github.io/web-nfc/#dom-ndefrecord
struct NDEFRecord { struct NDEFRecord {
// The category |record_type| belongs to. This field is not exposed to JS, but
// is used to transfer information internally and make code more readable,
// given that there're some complex logic around external types and local
// types (will be supported in the future).
NDEFRecordTypeCategory category;
// The type of NDEFRecord. // The type of NDEFRecord.
// https://w3c.github.io/web-nfc/#the-record-type-string
string record_type; string record_type;
// Represents the IANA media type of the NDEFRecord data field. // Represents the IANA media type of the NDEFRecord data field.
......
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
#include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/network/http_parsers.h" #include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h" #include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
...@@ -70,43 +69,39 @@ bool GetBytesOfBufferSource(const NDEFRecordDataSource& buffer_source, ...@@ -70,43 +69,39 @@ bool GetBytesOfBufferSource(const NDEFRecordDataSource& buffer_source,
return true; return true;
} }
// https://w3c.github.io/web-nfc/#ndef-record-types // https://w3c.github.io/web-nfc/#dfn-validate-external-type
// Derives a formatted custom type for the external type record from |input|. // Validates |input| as an external type.
// Returns a null string for an invalid |input|. bool IsValidExternalType(const String& input) {
// static const String kOtherCharsForCustomType("()+,-=@;$_*'.");
// TODO(https://crbug.com/520391): Refine the validation algorithm here
// accordingly once there is a conclusion on some case-sensitive things at if (input.IsEmpty() || input.length() > 255)
// https://github.com/w3c/web-nfc/issues/331. return false;
String ValidateCustomRecordType(const String& input) {
static const String kOtherCharsForCustomType("()+,-:=@;$_!*'."); // Finds the last occurrence of ':'.
wtf_size_t colon_index = input.ReverseFind(':');
if (input.IsEmpty())
return String();
// Finds the separator ':'.
wtf_size_t colon_index = input.find(':');
if (colon_index == kNotFound) if (colon_index == kNotFound)
return String(); return false;
// Derives the domain (FQDN) from the part before ':'. // Validates the domain (the part before ':').
String left = input.Left(colon_index); String domain = input.Left(colon_index);
bool success = false; if (domain.IsEmpty())
String domain = SecurityOrigin::CanonicalizeHost(left, &success); return false;
if (!success || domain.IsEmpty()) // TODO(https://crbug.com/520391): Make sure |domain| can be converted
return String(); // successfully to ASCII using IDN rules and does not contain any forbidden
// host code point.
// Validates the part after ':'.
String right = input.Substring(colon_index + 1); // Validates the type (the part after ':').
if (right.length() == 0) String type = input.Substring(colon_index + 1);
return String(); if (type.IsEmpty())
for (wtf_size_t i = 0; i < right.length(); i++) { return false;
if (!IsASCIIAlphanumeric(right[i]) && for (wtf_size_t i = 0; i < type.length(); i++) {
!kOtherCharsForCustomType.Contains(right[i])) { if (!IsASCIIAlphanumeric(type[i]) &&
return String(); !kOtherCharsForCustomType.Contains(type[i])) {
return false;
} }
} }
return domain + ':' + right; return true;
} }
String getDocumentLanguage(const ExecutionContext* execution_context) { String getDocumentLanguage(const ExecutionContext* execution_context) {
...@@ -245,7 +240,9 @@ static NDEFRecord* CreateExternalRecord(const String& custom_type, ...@@ -245,7 +240,9 @@ static NDEFRecord* CreateExternalRecord(const String& custom_type,
if (!GetBytesOfBufferSource(data, &bytes, exception_state)) { if (!GetBytesOfBufferSource(data, &bytes, exception_state)) {
return nullptr; return nullptr;
} }
return MakeGarbageCollected<NDEFRecord>(custom_type, bytes); NDEFRecord* record = MakeGarbageCollected<NDEFRecord>(
device::mojom::NDEFRecordTypeCategory::kExternal, custom_type, bytes);
return record;
} }
} // namespace } // namespace
...@@ -303,19 +300,12 @@ NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context, ...@@ -303,19 +300,12 @@ NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context,
// TODO(https://crbug.com/520391): Support creating smart-poster records. // TODO(https://crbug.com/520391): Support creating smart-poster records.
exception_state.ThrowTypeError("smart-poster type is not supported yet"); exception_state.ThrowTypeError("smart-poster type is not supported yet");
return nullptr; return nullptr;
} else if (IsValidExternalType(record_type)) {
instance = CreateExternalRecord(record_type, init->data(), exception_state);
} else { } else {
// TODO(https://crbug.com/520391): Here |record_type| may be a custom type // TODO(https://crbug.com/520391): Support local type records.
// name for an external type record, or a local type name for an external exception_state.ThrowTypeError("Invalid NDEFRecord type.");
// type record embedded within a ndef message as payload of a parent record, return nullptr;
// in either case we should try to create an external type record from
// |data|.
String formated_type = ValidateCustomRecordType(record_type);
if (formated_type.IsNull()) {
exception_state.ThrowTypeError("Invalid NDEFRecord type.");
return nullptr;
}
instance =
CreateExternalRecord(formated_type, init->data(), exception_state);
} }
if (instance && init->hasId()) { if (instance && init->hasId()) {
...@@ -324,30 +314,44 @@ NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context, ...@@ -324,30 +314,44 @@ NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context,
return instance; return instance;
} }
NDEFRecord::NDEFRecord(const String& record_type, NDEFRecord::NDEFRecord(const String& record_type, WTF::Vector<uint8_t> data)
WTF::Vector<uint8_t> data) : category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
: record_type_(record_type), record_type_(record_type),
payload_data_(std::move(data)) {} payload_data_(std::move(data)) {}
NDEFRecord::NDEFRecord(const String& record_type, NDEFRecord::NDEFRecord(const String& record_type,
const String& encoding, const String& encoding,
const String& lang, const String& lang,
WTF::Vector<uint8_t> data) WTF::Vector<uint8_t> data)
: record_type_(record_type), : category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
record_type_(record_type),
encoding_(encoding), encoding_(encoding),
lang_(lang), lang_(lang),
payload_data_(std::move(data)) {} payload_data_(std::move(data)) {}
NDEFRecord::NDEFRecord(device::mojom::NDEFRecordTypeCategory category,
const String& record_type,
WTF::Vector<uint8_t> data)
: category_(category),
record_type_(record_type),
payload_data_(std::move(data)) {
DCHECK_EQ(category_ == device::mojom::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
}
NDEFRecord::NDEFRecord(const ExecutionContext* execution_context, NDEFRecord::NDEFRecord(const ExecutionContext* execution_context,
const String& text) const String& text)
: record_type_("text"), : category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
record_type_("text"),
encoding_("utf-8"), encoding_("utf-8"),
lang_(getDocumentLanguage(execution_context)), lang_(getDocumentLanguage(execution_context)),
payload_data_(GetUTF8DataFromString(text)) {} payload_data_(GetUTF8DataFromString(text)) {}
NDEFRecord::NDEFRecord(WTF::Vector<uint8_t> payload_data, NDEFRecord::NDEFRecord(WTF::Vector<uint8_t> payload_data,
const String& media_type) const String& media_type)
: record_type_("mime"), payload_data_(std::move(payload_data)) { : category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
record_type_("mime"),
payload_data_(std::move(payload_data)) {
// ExtractMIMETypeFromMediaType() ignores parameters of the MIME type. // ExtractMIMETypeFromMediaType() ignores parameters of the MIME type.
media_type_ = ExtractMIMETypeFromMediaType(AtomicString(media_type)); media_type_ = ExtractMIMETypeFromMediaType(AtomicString(media_type));
if (media_type_.IsEmpty()) { if (media_type_.IsEmpty()) {
...@@ -356,40 +360,27 @@ NDEFRecord::NDEFRecord(WTF::Vector<uint8_t> payload_data, ...@@ -356,40 +360,27 @@ NDEFRecord::NDEFRecord(WTF::Vector<uint8_t> payload_data,
} }
NDEFRecord::NDEFRecord(const device::mojom::blink::NDEFRecord& record) NDEFRecord::NDEFRecord(const device::mojom::blink::NDEFRecord& record)
: record_type_(record.record_type), : category_(record.category),
record_type_(record.record_type),
media_type_(record.media_type), media_type_(record.media_type),
id_(record.id), id_(record.id),
encoding_(record.encoding), encoding_(record.encoding),
lang_(record.lang), lang_(record.lang),
payload_data_(record.data) { payload_data_(record.data) {
DCHECK_NE(record_type_ == "mime", media_type_.IsNull()); DCHECK_NE(record_type_ == "mime", media_type_.IsNull());
DCHECK_EQ(category_ == device::mojom::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
if (record.payload_message) { if (record.payload_message) {
payload_message_ = payload_message_ =
MakeGarbageCollected<NDEFMessage>(*record.payload_message); MakeGarbageCollected<NDEFMessage>(*record.payload_message);
} }
} }
const String& NDEFRecord::recordType() const {
return record_type_;
}
const String& NDEFRecord::mediaType() const { const String& NDEFRecord::mediaType() const {
DCHECK_NE(record_type_ == "mime", media_type_.IsNull()); DCHECK_NE(record_type_ == "mime", media_type_.IsNull());
return media_type_; return media_type_;
} }
const String& NDEFRecord::id() const {
return id_;
}
const String& NDEFRecord::encoding() const {
return encoding_;
}
const String& NDEFRecord::lang() const {
return lang_;
}
DOMDataView* NDEFRecord::data() const { DOMDataView* NDEFRecord::data() const {
// Step 4 in https://w3c.github.io/web-nfc/#dfn-parse-an-ndef-record // Step 4 in https://w3c.github.io/web-nfc/#dfn-parse-an-ndef-record
if (record_type_ == "empty") { if (record_type_ == "empty") {
...@@ -401,15 +392,11 @@ DOMDataView* NDEFRecord::data() const { ...@@ -401,15 +392,11 @@ DOMDataView* NDEFRecord::data() const {
return DOMDataView::Create(dom_buffer, 0, payload_data_.size()); return DOMDataView::Create(dom_buffer, 0, payload_data_.size());
} }
const WTF::Vector<uint8_t>& NDEFRecord::payloadData() const {
return payload_data_;
}
// https://w3c.github.io/web-nfc/#dfn-convert-ndefrecord-payloaddata-bytes // https://w3c.github.io/web-nfc/#dfn-convert-ndefrecord-payloaddata-bytes
base::Optional<HeapVector<Member<NDEFRecord>>> NDEFRecord::toRecords( base::Optional<HeapVector<Member<NDEFRecord>>> NDEFRecord::toRecords(
ExceptionState& exception_state) const { ExceptionState& exception_state) const {
if (record_type_ != "smart-poster" && if (record_type_ != "smart-poster" &&
ValidateCustomRecordType(record_type_).IsNull()) { category_ != device::mojom::NDEFRecordTypeCategory::kExternal) {
exception_state.ThrowDOMException( exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError, DOMExceptionCode::kNotSupportedError,
"Only smart-poster records and external type records could have a ndef " "Only smart-poster records and external type records could have a ndef "
...@@ -423,10 +410,6 @@ base::Optional<HeapVector<Member<NDEFRecord>>> NDEFRecord::toRecords( ...@@ -423,10 +410,6 @@ base::Optional<HeapVector<Member<NDEFRecord>>> NDEFRecord::toRecords(
return payload_message_->records(); return payload_message_->records();
} }
const NDEFMessage* NDEFRecord::payload_message() const {
return payload_message_.Get();
}
void NDEFRecord::Trace(blink::Visitor* visitor) { void NDEFRecord::Trace(blink::Visitor* visitor) {
visitor->Trace(payload_message_); visitor->Trace(payload_message_);
ScriptWrappable::Trace(visitor); ScriptWrappable::Trace(visitor);
......
...@@ -35,29 +35,39 @@ class MODULES_EXPORT NDEFRecord final : public ScriptWrappable { ...@@ -35,29 +35,39 @@ class MODULES_EXPORT NDEFRecord final : public ScriptWrappable {
explicit NDEFRecord(WTF::Vector<uint8_t> /* payload_data */, explicit NDEFRecord(WTF::Vector<uint8_t> /* payload_data */,
const String& /* media_type */); const String& /* media_type */);
NDEFRecord(const String& /* record_type */, WTF::Vector<uint8_t> /* data */); explicit NDEFRecord(const String& /* record_type */,
NDEFRecord(const String& /* record_type */, WTF::Vector<uint8_t> /* data */);
const String& /* encoding */, explicit NDEFRecord(const String& /* record_type */,
const String& /* lang */, const String& /* encoding */,
WTF::Vector<uint8_t> /* data */); const String& /* lang */,
WTF::Vector<uint8_t> /* data */);
// All above Ctors by default set the type category to
// device::mojom::NDEFRecordTypeCategory::kStandardized.
explicit NDEFRecord(device::mojom::NDEFRecordTypeCategory /* category */,
const String& /* record_type */,
WTF::Vector<uint8_t> /* data */);
explicit NDEFRecord(const device::mojom::blink::NDEFRecord&); explicit NDEFRecord(const device::mojom::blink::NDEFRecord&);
const String& recordType() const; const String& recordType() const { return record_type_; }
const String& mediaType() const; const String& mediaType() const;
const String& id() const; const String& id() const { return id_; }
const String& encoding() const; const String& encoding() const { return encoding_; }
const String& lang() const; const String& lang() const { return lang_; }
DOMDataView* data() const; DOMDataView* data() const;
base::Optional<HeapVector<Member<NDEFRecord>>> toRecords( base::Optional<HeapVector<Member<NDEFRecord>>> toRecords(
ExceptionState& exception_state) const; ExceptionState& exception_state) const;
const WTF::Vector<uint8_t>& payloadData() const; device::mojom::NDEFRecordTypeCategory category() const { return category_; }
const NDEFMessage* payload_message() const; const WTF::Vector<uint8_t>& payloadData() const { return payload_data_; }
const NDEFMessage* payload_message() const { return payload_message_.Get(); }
void Trace(blink::Visitor*) override; void Trace(blink::Visitor*) override;
private: private:
String record_type_; const device::mojom::NDEFRecordTypeCategory category_;
const String record_type_;
String media_type_; String media_type_;
String id_; String id_;
String encoding_; String encoding_;
......
...@@ -30,8 +30,8 @@ namespace mojo { ...@@ -30,8 +30,8 @@ namespace mojo {
NDEFRecordPtr TypeConverter<NDEFRecordPtr, blink::NDEFRecord*>::Convert( NDEFRecordPtr TypeConverter<NDEFRecordPtr, blink::NDEFRecord*>::Convert(
const blink::NDEFRecord* record) { const blink::NDEFRecord* record) {
return NDEFRecord::New( return NDEFRecord::New(
record->recordType(), record->mediaType(), record->id(), record->category(), record->recordType(), record->mediaType(),
record->encoding(), record->lang(), record->payloadData(), record->id(), record->encoding(), record->lang(), record->payloadData(),
TypeConverter<NDEFMessagePtr, blink::NDEFMessage*>::Convert( TypeConverter<NDEFMessagePtr, blink::NDEFMessage*>::Convert(
record->payload_message())); record->payload_message()));
} }
......
...@@ -24,6 +24,14 @@ function toMojoNDEFMessage(message) { ...@@ -24,6 +24,14 @@ function toMojoNDEFMessage(message) {
function toMojoNDEFRecord(record) { function toMojoNDEFRecord(record) {
let nfcRecord = new device.mojom.NDEFRecord(); let nfcRecord = new device.mojom.NDEFRecord();
if (record.recordType.search(':') != -1) {
// Simply checks the existence of ':' to decide whether it's an external
// type. As a mock, no need to really implement the validation algo at
// https://w3c.github.io/web-nfc/#dfn-validate-external-type.
nfcRecord.category = device.mojom.NDEFRecordTypeCategory.kExternal;
} else {
nfcRecord.category = device.mojom.NDEFRecordTypeCategory.kStandardized;
}
nfcRecord.recordType = record.recordType; nfcRecord.recordType = record.recordType;
nfcRecord.mediaType = record.mediaType; nfcRecord.mediaType = record.mediaType;
nfcRecord.id = record.id; nfcRecord.id = record.id;
......
...@@ -260,15 +260,15 @@ ...@@ -260,15 +260,15 @@
test(() => { test(() => {
assert_throws(new TypeError, () => new NDEFRecord( assert_throws(new TypeError, () => new NDEFRecord(
createRecord('foo.eXamPle.coM:bAr*-', "A string is not a BufferSource or NDEFMessage")), createRecord('foo.eXamPle.com:bAr*-', "A string is not a BufferSource or NDEFMessage")),
'Only BufferSource and NDEFMessage are allowed to be the record data.'); 'Only BufferSource and NDEFMessage are allowed to be the record data.');
let buffer = new ArrayBuffer(4); let buffer = new ArrayBuffer(4);
new Uint8Array(buffer).set([1, 2, 3, 4]); new Uint8Array(buffer).set([1, 2, 3, 4]);
// Feed ArrayBuffer. // Feed ArrayBuffer.
{ {
const record = new NDEFRecord(createRecord('foo.eXamPle.coM:bAr*-', buffer, test_record_id)); const record = new NDEFRecord(createRecord('foo.eXamPle.com:bAr*-', buffer, test_record_id));
assert_equals(record.recordType, 'foo.example.com:bAr*-', 'recordType'); assert_equals(record.recordType, 'foo.eXamPle.com:bAr*-', 'recordType');
assert_equals(record.mediaType, null, 'mediaType'); assert_equals(record.mediaType, null, 'mediaType');
assert_equals(record.id, test_record_id, 'id'); assert_equals(record.id, test_record_id, 'id');
assert_array_equals(new Uint8Array(record.data.buffer), [1, 2, 3, 4], assert_array_equals(new Uint8Array(record.data.buffer), [1, 2, 3, 4],
...@@ -280,8 +280,8 @@ ...@@ -280,8 +280,8 @@
{ {
let buffer_view = new Uint8Array(buffer, 1); let buffer_view = new Uint8Array(buffer, 1);
const record = new NDEFRecord(createRecord( const record = new NDEFRecord(createRecord(
'foo.eXamPle.coM:bAr*-', buffer_view, test_record_id)); 'foo.eXamPle.com:bAr*-', buffer_view, test_record_id));
assert_equals(record.recordType, 'foo.example.com:bAr*-', 'recordType'); assert_equals(record.recordType, 'foo.eXamPle.com:bAr*-', 'recordType');
assert_equals(record.mediaType, null, 'mediaType'); assert_equals(record.mediaType, null, 'mediaType');
assert_equals(record.id, test_record_id, 'id'); assert_equals(record.id, test_record_id, 'id');
assert_array_equals(new Uint8Array(record.data.buffer), [2, 3, 4], assert_array_equals(new Uint8Array(record.data.buffer), [2, 3, 4],
...@@ -305,12 +305,22 @@ ...@@ -305,12 +305,22 @@
}, 'NDEFRecord constructor with record type string being treated as case sensitive'); }, 'NDEFRecord constructor with record type string being treated as case sensitive');
test(() => { test(() => {
// Length of the external type is 255, OK.
const record = new NDEFRecord(createRecord(
[...Array(251)].map(_ => 'a').join('') + ':xyz', test_buffer_data));
// Exceeding 255, Throws.
assert_throws(new TypeError, () => new NDEFRecord(createRecord( assert_throws(new TypeError, () => new NDEFRecord(createRecord(
':xyz', test_buffer_data)), 'The domain should not be empty.'); [...Array(252)].map(_ => 'a').join('') + ':xyz', test_buffer_data)),
'The external type should not be longer than 255.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord( assert_throws(new TypeError, () => new NDEFRecord(createRecord(
'[:xyz', test_buffer_data)), '"[" is not a valid FQDN.'); 'xyz', test_buffer_data)), 'The external type must have a \':\'.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord(
':xyz', test_buffer_data)), 'The domain should not be empty.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord( assert_throws(new TypeError, () => new NDEFRecord(createRecord(
'example.com:', test_buffer_data)), 'The type should not be empty.'); 'example.com:', test_buffer_data)), 'The type should not be empty.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord(
'example.com:xyz[', test_buffer_data)), 'The type should not contain \'[\'.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord( assert_throws(new TypeError, () => new NDEFRecord(createRecord(
'example.com:xyz~', test_buffer_data)), 'The type should not contain \'~\'.'); 'example.com:xyz~', test_buffer_data)), 'The type should not contain \'~\'.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord( assert_throws(new TypeError, () => new NDEFRecord(createRecord(
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment