Commit acff81de authored by Leon Han's avatar Leon Han Committed by Commit Bot

[webnfc] Support writing/reading local type records

Some notable points:

1) Local type in WebNFC APIs is always prefixed by ':', but, the ':'
   will be omitted when it's actually written into the nfc tag.
     ":act"  --> "act" to be written as the TYPE field into the nfc tag.
     ":text" --> "text"
   The reading direction is vice versa.
     "act"  --> ":act" to be exposed as NDEFRecord#recordType.
     "text" --> ":text"

2) Only "smart-poster", external, and local type records are supposed to
   be able to carry a ndef message as payload.

3) Local type is only expected to exist inside a ndef message that is
   another ndef record's payload. Top level ndef message is not allowed
   to have a local type record.

The spec changes:
https://github.com/w3c/web-nfc/pull/491
https://github.com/w3c/web-nfc/pull/493
https://github.com/w3c/web-nfc/pull/495
https://github.com/w3c/web-nfc/pull/502
https://github.com/w3c/web-nfc/pull/506

BUG=520391

Change-Id: Ic2890c031109aa583437ac93a8901ff71992af78
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1996946Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Commit-Queue: Leon Han <leon.han@intel.com>
Cr-Commit-Position: refs/heads/master@{#737290}
parent 4a9e20a2
......@@ -167,7 +167,17 @@ public final class NdefMessageUtils {
}
if (record.category == NdefRecordTypeCategory.LOCAL) {
// TODO(https://crbug.com/520391): Support local type records.
// It's impossible for a local type record to have non-empty |data| and non-null
// |payloadMessage| at the same time.
// TODO(https://crbug.com/520391): Validate the containing ndef message is the payload
// of another ndef record.
if (isValidLocalType(record.recordType)
&& (record.data.length == 0 || record.payloadMessage == null)) {
// The prefix ':' in |record.recordType| is only used to differentiate local type
// from other type names, remove it before writing.
return createPlatformLocalRecord(record.recordType.substring(1), record.id,
record.data, record.payloadMessage);
}
throw new InvalidNdefMessageException();
}
......@@ -285,7 +295,19 @@ public final class NdefMessageUtils {
}
/**
* Constructs well known type (TEXT or URI) NdefRecord
* Constructs local type NdefRecord
*/
private static NdefRecord createLocalRecord(String localType, byte[] payload) {
NdefRecord nfcRecord = new NdefRecord();
nfcRecord.category = NdefRecordTypeCategory.LOCAL;
nfcRecord.recordType = localType;
nfcRecord.data = payload;
nfcRecord.payloadMessage = getNdefMessageFromPayloadBytes(payload);
return nfcRecord;
}
/**
* Constructs well known type (TEXT, URI or local type) NdefRecord
*/
private static NdefRecord createWellKnownRecord(android.nfc.NdefRecord record)
throws UnsupportedEncodingException {
......@@ -299,7 +321,15 @@ public final class NdefMessageUtils {
// TODO(https://crbug.com/520391): Support RTD_SMART_POSTER type records.
// TODO(https://crbug.com/520391): Support local type records.
// Prefix the raw local type with ':' to differentiate from other type names in WebNFC APIs,
// e.g. |localType| being "text" will become ":text" to differentiate from the standardized
// "text" record.
String recordType = ':' + new String(record.getType(), "UTF-8");
// We do not validate if we're in the context of a parent record but just expose to JS as is
// what has been read from the nfc tag.
if (isValidLocalType(recordType)) {
return createLocalRecord(recordType, record.getPayload());
}
return null;
}
......@@ -444,6 +474,26 @@ public final class NdefMessageUtils {
id == null ? null : ApiCompatibilityUtils.getBytesUtf8(id), payload);
}
/**
* Creates a TNF_WELL_KNOWN + |recordType| android.nfc.NdefRecord.
*/
public static android.nfc.NdefRecord createPlatformLocalRecord(
String recordType, String id, byte[] payload, NdefMessage payloadMessage) {
// Already guaranteed by the caller.
assert recordType != null && !recordType.isEmpty();
// |payloadMessage| being non-null means this record has an NDEF message as its payload.
if (payloadMessage != null) {
// Should be guaranteed by the caller that |payload| is an empty byte array.
assert payload.length == 0;
payload = getBytesFromPayloadNdefMessage(payloadMessage);
}
return new android.nfc.NdefRecord(android.nfc.NdefRecord.TNF_WELL_KNOWN,
ApiCompatibilityUtils.getBytesUtf8(recordType),
id == null ? null : ApiCompatibilityUtils.getBytesUtf8(id), payload);
}
/**
* Validates external types.
* https://w3c.github.io/web-nfc/#dfn-validate-external-type
......@@ -468,6 +518,29 @@ public final class NdefMessageUtils {
return true;
}
/**
* Validates local types.
* https://w3c.github.io/web-nfc/#dfn-validate-local-type
*/
private static boolean isValidLocalType(String input) {
// Must be an ASCII string first.
if (!Charset.forName("US-ASCII").newEncoder().canEncode(input)) return false;
// The prefix ':' will be omitted when we actually write the record type into the nfc tag.
// We're taking it into consideration for validating the length here.
if (input.length() < 2 || input.length() > 256) return false;
if (input.charAt(0) != ':') return false;
if (!Character.isLowerCase(input.charAt(1)) && !Character.isDigit(input.charAt(1))) {
return false;
}
// TODO(https://crbug.com/520391): Validate |input| is not equal to the record type of any
// NDEF record defined in its containing NDEF message.
return true;
}
/**
* Tries to construct a android.nfc.NdefMessage from the raw bytes |payload| then converts it to
* a Mojo NdefMessage and returns. Returns null for anything wrong.
......
......@@ -367,6 +367,45 @@ public class NFCTest {
assertEquals(NdefMessageUtils.RECORD_TYPE_TEXT, payloadMojoMessage.data[0].recordType);
assertEquals(null, payloadMojoMessage.data[0].mediaType);
assertEquals(TEST_TEXT, new String(payloadMojoMessage.data[0].data));
// Test local record conversion.
android.nfc.NdefMessage localNdefMessage = new android.nfc.NdefMessage(
NdefMessageUtils.createPlatformLocalRecord("xyz", DUMMY_RECORD_ID,
ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT), null /* payloadMessage */));
NdefMessage localMojoNdefMessage = NdefMessageUtils.toNdefMessage(localNdefMessage);
assertEquals(1, localMojoNdefMessage.data.length);
assertEquals(NdefRecordTypeCategory.LOCAL, localMojoNdefMessage.data[0].category);
// Is already prefixed with ':'.
assertEquals(":xyz", localMojoNdefMessage.data[0].recordType);
assertEquals(null, localMojoNdefMessage.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, localMojoNdefMessage.data[0].id);
assertNull(localMojoNdefMessage.data[0].encoding);
assertNull(localMojoNdefMessage.data[0].lang);
assertEquals(TEST_TEXT, new String(localMojoNdefMessage.data[0].data));
// Test conversion for local records with the payload being a ndef message.
payloadMessage = new android.nfc.NdefMessage(
android.nfc.NdefRecord.createTextRecord(LANG_EN_US, TEST_TEXT));
payloadBytes = payloadMessage.toByteArray();
// Put |payloadBytes| as payload of a local record.
android.nfc.NdefMessage localNdefMessage1 =
new android.nfc.NdefMessage(NdefMessageUtils.createPlatformLocalRecord(
"xyz", DUMMY_RECORD_ID, payloadBytes, null /* payloadMessage */));
NdefMessage localMojoNdefMessage1 = NdefMessageUtils.toNdefMessage(localNdefMessage1);
assertEquals(1, localMojoNdefMessage1.data.length);
assertEquals(NdefRecordTypeCategory.LOCAL, localMojoNdefMessage1.data[0].category);
// Is already prefixed with ':'.
assertEquals(":xyz", localMojoNdefMessage1.data[0].recordType);
assertEquals(null, localMojoNdefMessage1.data[0].mediaType);
assertEquals(DUMMY_RECORD_ID, localMojoNdefMessage1.data[0].id);
// The embedded ndef message should have content corresponding with the original
// |payloadMessage|.
payloadMojoMessage = localMojoNdefMessage1.data[0].payloadMessage;
assertEquals(1, payloadMojoMessage.data.length);
assertEquals(NdefRecordTypeCategory.STANDARDIZED, payloadMojoMessage.data[0].category);
assertEquals(NdefMessageUtils.RECORD_TYPE_TEXT, payloadMojoMessage.data[0].recordType);
assertEquals(null, payloadMojoMessage.data[0].mediaType);
assertEquals(TEST_TEXT, new String(payloadMojoMessage.data[0].data));
}
/**
......@@ -561,6 +600,56 @@ public class NFCTest {
assertEquals(DUMMY_RECORD_ID, new String(payloadMessage.getRecords()[0].getId()));
assertEquals(TEST_URL, payloadMessage.getRecords()[0].toUri().toString());
// Test local record conversion.
NdefRecord localMojoNdefRecord = new NdefRecord();
localMojoNdefRecord.category = NdefRecordTypeCategory.LOCAL;
localMojoNdefRecord.recordType = ":xyz";
localMojoNdefRecord.id = DUMMY_RECORD_ID;
localMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
NdefMessage localMojoNdefMessage = createMojoNdefMessage(localMojoNdefRecord);
android.nfc.NdefMessage localNdefMessage =
NdefMessageUtils.toNdefMessage(localMojoNdefMessage);
assertEquals(1, localNdefMessage.getRecords().length);
assertEquals(
android.nfc.NdefRecord.TNF_WELL_KNOWN, localNdefMessage.getRecords()[0].getTnf());
// The ':' prefix is already omitted.
assertEquals("xyz", new String(localNdefMessage.getRecords()[0].getType()));
assertEquals(DUMMY_RECORD_ID, new String(localNdefMessage.getRecords()[0].getId()));
assertEquals(TEST_TEXT, new String(localNdefMessage.getRecords()[0].getPayload()));
// Test conversion for local records with the payload being a ndef message.
//
// Prepare a local record that embeds |payloadMojoRecord| in its payload.
NdefRecord localMojoNdefRecord1 = new NdefRecord();
localMojoNdefRecord1.category = NdefRecordTypeCategory.LOCAL;
localMojoNdefRecord1.recordType = ":xyz";
localMojoNdefRecord1.id = DUMMY_RECORD_ID;
// device.mojom.NDEFRecord.data is not allowed to be null, instead, empty byte array is just
// what's passed from Blink.
localMojoNdefRecord1.data = new byte[0];
localMojoNdefRecord1.payloadMessage = createMojoNdefMessage(payloadMojoRecord);
// Do the conversion.
android.nfc.NdefMessage localNdefMessage1 =
NdefMessageUtils.toNdefMessage(createMojoNdefMessage(localMojoNdefRecord1));
assertEquals(1, localNdefMessage1.getRecords().length);
assertEquals(
android.nfc.NdefRecord.TNF_WELL_KNOWN, localNdefMessage1.getRecords()[0].getTnf());
// The ':' prefix is already omitted.
assertEquals("xyz", new String(localNdefMessage1.getRecords()[0].getType()));
assertEquals(DUMMY_RECORD_ID, new String(localNdefMessage1.getRecords()[0].getId()));
// The payload raw bytes should be able to construct an ndef message containing an ndef
// record that has content corresponding with the original |payloadMojoRecord|.
payloadMessage =
new android.nfc.NdefMessage(localNdefMessage1.getRecords()[0].getPayload());
assertNotNull(payloadMessage);
assertEquals(1, payloadMessage.getRecords().length);
assertEquals(
android.nfc.NdefRecord.TNF_WELL_KNOWN, payloadMessage.getRecords()[0].getTnf());
assertEquals(new String(android.nfc.NdefRecord.RTD_URI),
new String(payloadMessage.getRecords()[0].getType()));
assertEquals(DUMMY_RECORD_ID, new String(payloadMessage.getRecords()[0].getId()));
assertEquals(TEST_URL, payloadMessage.getRecords()[0].toUri().toString());
// Test EMPTY record conversion.
NdefRecord emptyMojoNdefRecord = new NdefRecord();
emptyMojoNdefRecord.category = NdefRecordTypeCategory.STANDARDIZED;
......@@ -655,6 +744,49 @@ public class NFCTest {
}
}
/**
* Test local type record conversion with invalid local type.
*/
@Test(expected = InvalidNdefMessageException.class)
@Feature({"NFCTest"})
public void testInvalidLocalRecordType() throws InvalidNdefMessageException {
NdefRecord localMojoNdefRecord = new NdefRecord();
localMojoNdefRecord.category = NdefRecordTypeCategory.LOCAL;
localMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
{
// Must start with ':'.
localMojoNdefRecord.recordType = "dummyLocalTypeNotStartingwith:";
localMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
NdefMessage localMojoNdefMessage = createMojoNdefMessage(localMojoNdefRecord);
android.nfc.NdefMessage localNdefMessage =
NdefMessageUtils.toNdefMessage(localMojoNdefMessage);
assertNull(localNdefMessage);
}
{
// |recordType| is a string mixed with ASCII/non-ASCII, FAIL.
localMojoNdefRecord.recordType = ":hellö";
android.nfc.NdefMessage localNdefMessage_nonASCII =
NdefMessageUtils.toNdefMessage(createMojoNdefMessage(localMojoNdefRecord));
assertNull(localNdefMessage_nonASCII);
char[] chars = new char[255];
Arrays.fill(chars, 'a');
String chars_255 = new String(chars);
// The length of the real local type is 255, OK.
localMojoNdefRecord.recordType = ":" + chars_255;
android.nfc.NdefMessage localNdefMessage_255 =
NdefMessageUtils.toNdefMessage(createMojoNdefMessage(localMojoNdefRecord));
assertNotNull(localNdefMessage_255);
// Exceeding the maximum length 255, FAIL.
localMojoNdefRecord.recordType = ":a" + chars_255;
android.nfc.NdefMessage localNdefMessage_256 =
NdefMessageUtils.toNdefMessage(createMojoNdefMessage(localMojoNdefRecord));
assertNull(localNdefMessage_256);
}
}
/**
* Test that invalid NdefMessage is rejected with INVALID_MESSAGE error code.
*/
......
......@@ -47,9 +47,7 @@ struct NDEFError {
// https://w3c.github.io/web-nfc/#dom-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).
// is used to transfer information internally and make code more readable.
NDEFRecordTypeCategory category;
// The type of NDEFRecord.
......@@ -73,9 +71,14 @@ struct NDEFRecord {
// Payload of the NDEFRecord.
array<uint8> data;
// |data| parsed as an NDEFMessage. This field may be set for some
// "smart-poster" or external type records. This field may be null even if
// |data| is valid.
// This field may be set for some "smart-poster", external, or local type
// records.
// On the writing direction (passed from inside Blink), if this field is
// non-null, |data| will be empty and the receiver is expected to make out the
// payload raw bytes from this field.
// On the reading direction (passed towards Blink), |data| always carries the
// payload raw bytes, this field is non-null only if |data| can be parsed as
// an NDEFMessage.
NDEFMessage? payload_message;
};
......
......@@ -16,7 +16,8 @@ namespace blink {
// static
NDEFMessage* NDEFMessage::Create(const ExecutionContext* execution_context,
const NDEFMessageInit* init,
ExceptionState& exception_state) {
ExceptionState& exception_state,
bool is_embedded) {
// https://w3c.github.io/web-nfc/#creating-ndef-message
// NDEFMessageInit#records is a required field.
......@@ -29,8 +30,8 @@ NDEFMessage* NDEFMessage::Create(const ExecutionContext* execution_context,
NDEFMessage* message = MakeGarbageCollected<NDEFMessage>();
for (const NDEFRecordInit* record_init : init->records()) {
NDEFRecord* record =
NDEFRecord::Create(execution_context, record_init, exception_state);
NDEFRecord* record = NDEFRecord::Create(execution_context, record_init,
exception_state, is_embedded);
if (exception_state.HadException())
return nullptr;
DCHECK(record);
......
......@@ -24,9 +24,12 @@ class MODULES_EXPORT NDEFMessage final : public ScriptWrappable {
DEFINE_WRAPPERTYPEINFO();
public:
// |is_embedded| indicates if this message serves as payload for a parent
// record.
static NDEFMessage* Create(const ExecutionContext*,
const NDEFMessageInit*,
ExceptionState&);
ExceptionState&,
bool is_embedded = false);
static NDEFMessage* Create(const ExecutionContext*,
const NDEFMessageSource&,
ExceptionState&);
......
......@@ -109,6 +109,29 @@ bool IsValidExternalType(const String& input) {
return true;
}
// https://w3c.github.io/web-nfc/#dfn-validate-local-type
// Validates |input| as an local type.
bool IsValidLocalType(const String& input) {
// Ensure |input| is an ASCII string.
if (!input.ContainsOnlyASCIIOrEmpty())
return false;
// The prefix ':' will be omitted when we actually write the record type into
// the nfc tag. We're taking it into consideration for validating the length
// here.
if (input.length() < 2 || input.length() > 256)
return false;
if (input[0] != ':')
return false;
if (!IsASCIILower(input[1]) && !IsASCIIDigit(input[1]))
return false;
// TODO(https://crbug.com/520391): Validate |input| is not equal to the record
// type of any NDEF record defined in its containing NDEF message.
return true;
}
String getDocumentLanguage(const ExecutionContext* execution_context) {
DCHECK(execution_context);
String document_language;
......@@ -200,8 +223,8 @@ static NDEFRecord* CreateUrlRecord(const String& record_type,
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::NDEFRecordTypeCategory::kStandardized, record_type, id,
GetUTF8DataFromString(url));
device::mojom::blink::NDEFRecordTypeCategory::kStandardized, record_type,
id, GetUTF8DataFromString(url));
}
static NDEFRecord* CreateMimeRecord(const String& id,
......@@ -243,8 +266,8 @@ static NDEFRecord* CreateUnknownRecord(const String& id,
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::NDEFRecordTypeCategory::kStandardized, "unknown", id,
bytes);
device::mojom::blink::NDEFRecordTypeCategory::kStandardized, "unknown",
id, bytes);
}
static NDEFRecord* CreateExternalRecord(
......@@ -260,21 +283,54 @@ static NDEFRecord* CreateExternalRecord(
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::NDEFRecordTypeCategory::kExternal, record_type, id,
device::mojom::blink::NDEFRecordTypeCategory::kExternal, record_type,
id, bytes);
} else if (data.IsNDEFMessageInit()) {
NDEFMessage* payload_message =
NDEFMessage::Create(execution_context, data.GetAsNDEFMessageInit(),
exception_state, /*is_embedded=*/true);
if (exception_state.HadException())
return nullptr;
DCHECK(payload_message);
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kExternal, record_type,
id, payload_message);
}
exception_state.ThrowTypeError(
"The data for external type NDEFRecord must be a BufferSource or an "
"NDEFMessageInit.");
return nullptr;
}
static NDEFRecord* CreateLocalRecord(const ExecutionContext* execution_context,
const String& record_type,
const String& id,
const NDEFRecordDataSource& data,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#dfn-map-local-type-to-ndef
if (IsBufferSource(data)) {
WTF::Vector<uint8_t> bytes;
if (!GetBytesOfBufferSource(data, &bytes, exception_state)) {
return nullptr;
}
return MakeGarbageCollected<NDEFRecord>(
device::mojom::blink::NDEFRecordTypeCategory::kLocal, record_type, id,
bytes);
} else if (data.IsNDEFMessageInit()) {
NDEFMessage* payload_message = NDEFMessage::Create(
execution_context, data.GetAsNDEFMessageInit(), exception_state);
NDEFMessage* payload_message =
NDEFMessage::Create(execution_context, data.GetAsNDEFMessageInit(),
exception_state, /*is_embedded=*/true);
if (exception_state.HadException())
return nullptr;
DCHECK(payload_message);
return MakeGarbageCollected<NDEFRecord>(
device::mojom::NDEFRecordTypeCategory::kExternal, record_type, id,
device::mojom::blink::NDEFRecordTypeCategory::kLocal, record_type, id,
payload_message);
}
exception_state.ThrowTypeError(
"The data for external type NDEFRecord must be a BufferSource or an "
"The data for local type NDEFRecord must be a BufferSource or an "
"NDEFMessageInit.");
return nullptr;
}
......@@ -284,7 +340,8 @@ static NDEFRecord* CreateExternalRecord(
// static
NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context,
const NDEFRecordInit* init,
ExceptionState& exception_state) {
ExceptionState& exception_state,
bool is_embedded) {
// https://w3c.github.io/web-nfc/#creating-ndef-record
// NDEFRecordInit#recordType is a required field.
......@@ -308,8 +365,8 @@ NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context,
if (record_type == "empty") {
// https://w3c.github.io/web-nfc/#mapping-empty-record-to-ndef
return MakeGarbageCollected<NDEFRecord>(
device::mojom::NDEFRecordTypeCategory::kStandardized, record_type,
init->id(), WTF::Vector<uint8_t>());
device::mojom::blink::NDEFRecordTypeCategory::kStandardized,
record_type, init->id(), WTF::Vector<uint8_t>());
} else if (record_type == "text") {
return CreateTextRecord(init->id(), execution_context, init->encoding(),
init->lang(), init->data(), exception_state);
......@@ -328,15 +385,22 @@ NDEFRecord* NDEFRecord::Create(const ExecutionContext* execution_context,
} else if (IsValidExternalType(record_type)) {
return CreateExternalRecord(execution_context, record_type, init->id(),
init->data(), exception_state);
} else {
// TODO(https://crbug.com/520391): Support local type records.
} else if (IsValidLocalType(record_type)) {
if (!is_embedded) {
exception_state.ThrowTypeError(
"Local type records are only supposed to be embedded in the payload "
"of another record (smart-poster, external, or local).");
return nullptr;
}
return CreateLocalRecord(execution_context, record_type, init->id(),
init->data(), exception_state);
}
exception_state.ThrowTypeError("Invalid NDEFRecord type.");
return nullptr;
}
NDEFRecord::NDEFRecord(device::mojom::NDEFRecordTypeCategory category,
NDEFRecord::NDEFRecord(device::mojom::blink::NDEFRecordTypeCategory category,
const String& record_type,
const String& id,
WTF::Vector<uint8_t> data)
......@@ -344,11 +408,14 @@ NDEFRecord::NDEFRecord(device::mojom::NDEFRecordTypeCategory category,
record_type_(record_type),
id_(id),
payload_data_(std::move(data)) {
DCHECK_EQ(category_ == device::mojom::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal,
IsValidLocalType(record_type_));
}
NDEFRecord::NDEFRecord(device::mojom::NDEFRecordTypeCategory category,
NDEFRecord::NDEFRecord(device::mojom::blink::NDEFRecordTypeCategory category,
const String& record_type,
const String& id,
NDEFMessage* payload_message)
......@@ -356,17 +423,21 @@ NDEFRecord::NDEFRecord(device::mojom::NDEFRecordTypeCategory category,
record_type_(record_type),
id_(id),
payload_message_(payload_message) {
DCHECK_EQ(category_ == device::mojom::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK(record_type_ == "smart-poster" ||
category_ == device::mojom::NDEFRecordTypeCategory::kExternal);
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal ||
category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal);
DCHECK_EQ(
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal,
IsValidLocalType(record_type_));
}
NDEFRecord::NDEFRecord(const String& id,
const String& encoding,
const String& lang,
WTF::Vector<uint8_t> data)
: category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
: category_(device::mojom::blink::NDEFRecordTypeCategory::kStandardized),
record_type_("text"),
id_(id),
encoding_(encoding),
......@@ -375,7 +446,7 @@ NDEFRecord::NDEFRecord(const String& id,
NDEFRecord::NDEFRecord(const ExecutionContext* execution_context,
const String& text)
: category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
: category_(device::mojom::blink::NDEFRecordTypeCategory::kStandardized),
record_type_("text"),
encoding_("utf-8"),
lang_(getDocumentLanguage(execution_context)),
......@@ -384,12 +455,14 @@ NDEFRecord::NDEFRecord(const ExecutionContext* execution_context,
NDEFRecord::NDEFRecord(const String& id,
const String& media_type,
WTF::Vector<uint8_t> data)
: category_(device::mojom::NDEFRecordTypeCategory::kStandardized),
: category_(device::mojom::blink::NDEFRecordTypeCategory::kStandardized),
record_type_("mime"),
id_(id),
media_type_(media_type),
payload_data_(std::move(data)) {}
// Even if |record| is for a local type record, here we do not validate if it's
// in the context of a parent record but just expose to JS as is.
NDEFRecord::NDEFRecord(const device::mojom::blink::NDEFRecord& record)
: category_(record.category),
record_type_(record.record_type),
......@@ -403,8 +476,11 @@ NDEFRecord::NDEFRecord(const device::mojom::blink::NDEFRecord& record)
? MakeGarbageCollected<NDEFMessage>(*record.payload_message)
: nullptr) {
DCHECK_NE(record_type_ == "mime", media_type_.IsNull());
DCHECK_EQ(category_ == device::mojom::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(
category_ == device::mojom::blink::NDEFRecordTypeCategory::kExternal,
IsValidExternalType(record_type_));
DCHECK_EQ(category_ == device::mojom::blink::NDEFRecordTypeCategory::kLocal,
IsValidLocalType(record_type_));
}
const String& NDEFRecord::mediaType() const {
......@@ -427,10 +503,11 @@ DOMDataView* NDEFRecord::data() const {
base::Optional<HeapVector<Member<NDEFRecord>>> NDEFRecord::toRecords(
ExceptionState& exception_state) const {
if (record_type_ != "smart-poster" &&
category_ != device::mojom::NDEFRecordTypeCategory::kExternal) {
category_ != device::mojom::blink::NDEFRecordTypeCategory::kExternal &&
category_ != device::mojom::blink::NDEFRecordTypeCategory::kLocal) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"Only smart-poster records and external type records could have a ndef "
"Only {smart-poster, external, local} type records could have a ndef "
"message as payload.");
return base::nullopt;
}
......
......@@ -24,17 +24,20 @@ class MODULES_EXPORT NDEFRecord final : public ScriptWrappable {
DEFINE_WRAPPERTYPEINFO();
public:
// |is_embedded| indicates if this record is within the context of a parent
// record.
static NDEFRecord* Create(const ExecutionContext*,
const NDEFRecordInit*,
ExceptionState&);
ExceptionState&,
bool is_embedded = false);
explicit NDEFRecord(device::mojom::NDEFRecordTypeCategory,
const String& record_type,
const String& id,
WTF::Vector<uint8_t>);
// For constructing an external type record or a "smart-poster" record whose
// payload is an NDEF message.
// For constructing a "smart-poster", an external type or a local type record
// whose payload is an NDEF message.
explicit NDEFRecord(device::mojom::NDEFRecordTypeCategory,
const String& record_type,
const String& id,
......@@ -86,7 +89,7 @@ class MODULES_EXPORT NDEFRecord final : public ScriptWrappable {
// https://w3c.github.io/web-nfc/#the-ndefrecord-interface.
const WTF::Vector<uint8_t> payload_data_;
// |payload_data_| parsed as an NDEFMessage. This field will be set for some
// "smart-poster" and external type records.
// "smart-poster", external, and local type records.
const Member<NDEFMessage> payload_message_;
};
......
......@@ -13,10 +13,12 @@ function toMojoNDEFMessage(message) {
function toMojoNDEFRecord(record) {
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.
// Simply checks the existence of ':' to decide whether it's an external
// type or a local type. As a mock, no need to really implement the validation
// algorithms for them.
if (record.recordType.startsWith(':')) {
nfcRecord.category = device.mojom.NDEFRecordTypeCategory.kLocal;
} else if (record.recordType.search(':') != -1) {
nfcRecord.category = device.mojom.NDEFRecordTypeCategory.kExternal;
} else {
nfcRecord.category = device.mojom.NDEFRecordTypeCategory.kStandardized;
......
......@@ -146,23 +146,32 @@ nfc_test(async (t, mockNFC) => {
const promise = readerWatcher.wait_for("reading").then(event => {
controller.abort();
assert_true(event instanceof NDEFReadingEvent);
// The message contains only an external type record.
// The message in the event contains only the external type record.
assert_equals(event.message.records.length, 1);
assert_equals(event.message.records[0].recordType, 'example.com:payloadIsMessage', 'recordType');
// The external type record's payload is a message, which contains only a text record.
const embeddedRecords = event.message.records[0].toRecords();
assert_equals(embeddedRecords.length, 1);
assert_equals(embeddedRecords[0].recordType, 'text', 'recordType');
assert_equals(embeddedRecords[0].mediaType, null, 'mediaType');
assert_equals(event.message.records[0].recordType, 'example.com:containsLocalRecord',
'recordType');
// The external type record contains only the local type record.
assert_equals(event.message.records[0].toRecords().length, 1);
assert_equals(event.message.records[0].toRecords()[0].recordType, ':containsTextRecord',
'recordType');
// The local type record contains only the text record.
assert_equals(event.message.records[0].toRecords()[0].toRecords()[0].recordType, 'text',
'recordType');
const decoder = new TextDecoder();
assert_equals(decoder.decode(embeddedRecords[0].data), test_text_data,
'data has the same content with the original dictionary');
assert_equals(decoder.decode(event.message.records[0].toRecords()[0].toRecords()[0].data),
test_text_data, 'data has the same content with the original dictionary');
});
await reader.scan({signal : controller.signal});
const payloadMessage = createMessage([createTextRecord(test_text_data)]);
const message = createMessage([createRecord('example.com:payloadIsMessage',
payloadMessage)]);
// An external type record --contains-> a local type record --contains-> a text record.
const messageContainText = createMessage([createTextRecord(test_text_data)]);
const messageContainLocal= createMessage([createRecord(':containsTextRecord',
messageContainText)]);
const message = createMessage([createRecord('example.com:containsLocalRecord',
messageContainLocal)]);
mockNFC.setReadingMessage(message);
await promise;
}, "NDEFRecord.toRecords returns its embedded records correctly.");
......
......@@ -83,7 +83,7 @@
assert_equals(record.lang, null, 'lang');
assert_equals(record.data, null, 'data');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with empty record type');
test(() => {
......@@ -97,7 +97,7 @@
assert_equals(decoder.decode(record.data), test_text_data,
'data has the same content with the original dictionary');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with text record type and string data');
test(() => {
......@@ -114,7 +114,7 @@
assert_equals(decoder.decode(record.data), test_text_data,
'data has the same content with the original dictionary');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with text record type and arrayBuffer data');
test(() => {
......@@ -131,7 +131,7 @@
assert_equals(decoder.decode(record.data), test_text_data,
'data has the same content with the original dictionary');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with text record type and arrayBufferView data');
test(() => {
......@@ -191,7 +191,7 @@
assert_equals(decoder.decode(record.data), test_url_data,
'data has the same content with the original dictionary');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with url record type');
test(() => {
......@@ -203,7 +203,7 @@
assert_equals(decoder.decode(record.data), test_url_data,
'data has the same content with the original dictionary');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with absolute-url record type');
test(() => {
......@@ -222,7 +222,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [1, 2, 3, 4],
'data has the same content with the original buffer');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}
// Feed ArrayBufferView.
{
......@@ -233,7 +233,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [2, 3, 4],
'data has the same content with the original buffer view');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}
}, 'NDEFRecord constructor with mime record type and stream data');
......@@ -246,7 +246,7 @@
test_json_data,
'data has the same content with the original json');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}, 'NDEFRecord constructor with mime record type and json data');
test(() => {
......@@ -264,7 +264,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [1, 2, 3, 4],
'data has the same content with the original buffer');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}
// Feed ArrayBufferView.
{
......@@ -275,7 +275,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [2, 3, 4],
'data has the same content with the original buffer view');
assert_throws_dom('NotSupportedError', () => record.toRecords(),
'Only smart-poster records and external type records could have embedded records.');
'Only smart-poster, external type and local type records could have embedded records.');
}
}, 'NDEFRecord constructor with unknown record type');
......@@ -330,6 +330,77 @@
}
}, 'NDEFRecord constructor with external record type');
test(() => {
assert_throws_js(TypeError, () => new NDEFRecord(createRecord(':xyz', test_buffer_data)),
'The local type record must be embedded in the payload of another record (smart-poster, external, or local)');
// The following test cases use an external type record embedding our target local type record.
const local_record= createRecord(':xyz', undefined /* data */, 'dummy_id_for_local_type');
const payload_message = createMessage([local_record]);
const external_record_embedding_local_record = createRecord('example.com:foo', payload_message);
local_record.data = "A string is not a BufferSource or NDEFMessageInit";
assert_throws_js(TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'Only BufferSource and NDEFMessageInit are allowed to be the record data.');
let buffer = new ArrayBuffer(4);
new Uint8Array(buffer).set([1, 2, 3, 4]);
// Feed ArrayBuffer.
{
local_record.data = buffer;
const record = new NDEFRecord(external_record_embedding_local_record);
const embedded_records = record.toRecords();
assert_equals(embedded_records.length, 1, 'Only the one embedded local record.');
// The embedded local record is actually from |local_record|.
assert_equals(embedded_records[0].recordType, ':xyz', 'recordType');
assert_equals(embedded_records[0].mediaType, null, 'mediaType');
assert_equals(embedded_records[0].id, 'dummy_id_for_local_type', 'id');
assert_array_equals(new Uint8Array(embedded_records[0].data.buffer), [1, 2, 3, 4],
'data has the same content with the original buffer');
assert_equals(embedded_records[0].toRecords(), null,
'toRecords() returns null if the payload is not an NDEF message.');
}
// Feed ArrayBufferView.
{
let buffer_view = new Uint8Array(buffer, 1);
local_record.data = buffer_view;
const record = new NDEFRecord(external_record_embedding_local_record);
const embedded_records = record.toRecords();
assert_equals(embedded_records.length, 1, 'Only the one embedded local record.');
// The embedded local record is actually from |local_record|.
assert_equals(embedded_records[0].recordType, ':xyz', 'recordType');
assert_equals(embedded_records[0].mediaType, null, 'mediaType');
assert_equals(embedded_records[0].id, 'dummy_id_for_local_type', 'id');
assert_array_equals(new Uint8Array(embedded_records[0].data.buffer), [2, 3, 4],
'data has the same content with the original buffer view');
assert_equals(embedded_records[0].toRecords(), null,
'toRecords() returns null if the payload is not an NDEF message.');
}
// Feed NDEFMessageInit.
{
const payload_message = createMessage([createTextRecord(test_text_data)]);
local_record.data = payload_message;
const record = new NDEFRecord(external_record_embedding_local_record);
const embedded_records = record.toRecords();
assert_equals(embedded_records.length, 1, 'Only the one embedded local record.');
// The embedded local record is actually from |local_record|.
assert_equals(embedded_records[0].recordType, ':xyz', 'recordType');
assert_equals(embedded_records[0].mediaType, null, 'mediaType');
assert_equals(embedded_records[0].id, 'dummy_id_for_local_type', 'id');
// The embedded local record embeds another text record that's from |payload_message|.
const embedded_records_in_local_record = embedded_records[0].toRecords();
assert_equals(embedded_records_in_local_record.length, 1, 'Only one embedded record.');
// The only one embedded record has correct content.
assert_equals(embedded_records_in_local_record[0].recordType, 'text', 'recordType');
assert_equals(embedded_records_in_local_record[0].mediaType, null, 'mediaType');
assert_equals(embedded_records_in_local_record[0].id, test_record_id, 'id');
const decoder = new TextDecoder();
assert_equals(decoder.decode(embedded_records_in_local_record[0].data), test_text_data,
'data has the same content with the original dictionary');
}
}, 'NDEFRecord constructor with local record type');
test(() => {
assert_throws_js(TypeError, () => new NDEFRecord(createRecord('EMptY')),
'Unknown record type.');
......@@ -370,4 +441,47 @@
'example.com:xyz/', test_buffer_data)), 'The type should not contain \'/\'.');
}, 'NDEFRecord constructor with invalid external record type');
test(() => {
assert_throws_js(TypeError, () => new NDEFRecord(createRecord(':xyz', test_buffer_data)),
'The local type record must be embedded in the payload of another record (smart-poster, external, or local)');
// The following test cases use an external type record embedding our target local type record.
const local_record= createRecord(':xyz', test_buffer_data);
const payload_message = createMessage([local_record]);
const external_record_embedding_local_record = createRecord('example.com:foo', payload_message);
// OK.
new NDEFRecord(external_record_embedding_local_record);
local_record.recordType = ':xyZ123';
new NDEFRecord(external_record_embedding_local_record);
local_record.recordType = ':123XYz';
new NDEFRecord(external_record_embedding_local_record);
local_record.recordType = ':hellö';
assert_throws_js(TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'The local type must be an ASCII string.');
// Length of the local type excluding the prefix ':' is 255, OK.
local_record.recordType = ':' + [...Array(255)].map(_ => 'a').join('');
const record_255 = new NDEFRecord(external_record_embedding_local_record);
// Exceeding 255, Throws.
local_record.recordType = ':' + [...Array(256)].map(_ => 'a').join('');
assert_throws_js(TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'The local type excluding the prefix \':\' should not be longer than 255.');
local_record.recordType = 'xyz';
assert_throws_js(TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'The local type must start with a \':\'.');
local_record.recordType = ':Xyz';
assert_throws_js(TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'The local type must have a lower case letter or digit following the prefix \':\'.');
local_record.recordType = ':-xyz';
assert_throws_js(TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'The local type must have a lower case letter or digit following the prefix \':\'.');
}, 'NDEFRecord constructor with various local record types');
</script>
......@@ -72,14 +72,23 @@ const invalid_type_messages =
// NDEFRecord must have data.
createMessage([createRecord('w3.org:xyz')]),
// NDEFRecord.data for external record must be ArrayBuffer.
// NDEFRecord.data for external record must be a BufferSource or NDEFMessageInit.
createMessage([createRecord('w3.org:xyz', test_text_data)]),
createMessage([createRecord('w3.org:xyz', test_number_data)]),
createMessage([createRecord('w3.org:xyz', test_json_data)]),
// https://w3c.github.io/web-nfc/#dfn-map-local-type-to-ndef
// NDEFRecord must have data.
createMessage([createRecord(':xyz')]),
// NDEFRecord.data for local type record must be a BufferSource or NDEFMessageInit.
createMessage([createRecord(':xyz', test_text_data)]),
createMessage([createRecord(':xyz', test_number_data)]),
createMessage([createRecord(':xyz', test_json_data)]),
// https://w3c.github.io/web-nfc/#ndef-record-types
// The record type is neither a known type ('text', 'mime' etc.) nor a
// valid custom type for an external type record.
// valid external/local type.
createMessage([createRecord('unmatched_type', test_buffer_data)])
];
......@@ -283,10 +292,13 @@ nfc_test(async (t, mockNFC) => {
and external records with default NDEFWriteOptions.");
nfc_test(async (t, mockNFC) => {
const payloadMessage = createMessage([createTextRecord(test_text_data)]);
// Prepare a message containing an external record that uses |payloadMessage| as its payload.
const message = createMessage([createRecord('example.com:payloadIsMessage',
payloadMessage)]);
const messageContainText = createMessage([createTextRecord(test_text_data)]);
// Prepare a local type record that uses |messageContainText| as its payload.
const messageContainLocal = createMessage([createRecord(':containsTextRecord', messageContainText)]);
// Prepare an external type record that uses |messageContainLocal| as its payload.
const message = createMessage([createRecord('example.com:containsLocalRecord', messageContainLocal)]);
const writer = new NDEFWriter();
await writer.write(message);
......@@ -294,11 +306,20 @@ nfc_test(async (t, mockNFC) => {
// The mojom message received by mock nfc contains only the external type record.
assert_equals(pushed_message.data.length, 1);
assert_equals(pushed_message.data[0].recordType, 'example.com:payloadIsMessage', 'recordType');
// The external type record's payload is from the original |payloadMessage|.
assert_equals(pushed_message.data[0].recordType, 'example.com:containsLocalRecord', 'recordType');
// The external type record's payload is from the original |messageContainLocal|,
// containing only the local type record.
assert_array_equals(pushed_message.data[0].data, new Uint8Array(0),
'payloadMessage is used instead');
assertNDEFMessagesEqual(payloadMessage, pushed_message.data[0].payloadMessage);
assert_equals(pushed_message.data[0].payloadMessage.data.length, 1);
assert_equals(pushed_message.data[0].payloadMessage.data[0].recordType, ':containsTextRecord', 'recordType');
// The local type record's payload is from the original |messageContainText|,
// containing only the text record.
assert_array_equals(pushed_message.data[0].payloadMessage.data[0].data, new Uint8Array(0),
'payloadMessage is used instead');
assertNDEFMessagesEqual(messageContainText, pushed_message.data[0].payloadMessage.data[0].payloadMessage);
}, "NDEFWriter.write NDEFMessage containing embedded records.");
nfc_test(async (t, mockNFC) => {
......
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