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

[webnfc] Support read/write external type records

Note: not support sub-records yet.

The corresponding spec changes:
https://github.com/w3c/web-nfc/pull/278
https://github.com/w3c/web-nfc/pull/326
and the relevant issue:
https://github.com/w3c/web-nfc/issues/331

BUG=520391,995234

Change-Id: I5b99543bddf505975567ecad22c0d5390842337f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1791533Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Commit-Queue: Leon Han <leon.han@intel.com>
Cr-Commit-Position: refs/heads/master@{#705377}
parent 24db98e7
......@@ -17,8 +17,8 @@ import java.util.Arrays;
import java.util.List;
/**
* Utility class that provides convesion between Android NdefMessage
* and mojo NdefMessage data structures.
* Utility class that provides conversion between Android NdefMessage and Mojo NdefMessage data
* structures.
*/
public final class NdefMessageUtils {
private static final String TAG = "NdefMessageUtils";
......@@ -33,6 +33,17 @@ public final class NdefMessageUtils {
public static final String RECORD_TYPE_URL = "url";
public static final String RECORD_TYPE_JSON = "json";
public static final String RECORD_TYPE_OPAQUE = "opaque";
public static final String RECORD_TYPE_SMART_POSTER = "smart-poster";
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
......@@ -111,10 +122,16 @@ public final class NdefMessageUtils {
case RECORD_TYPE_EMPTY:
return new android.nfc.NdefRecord(
android.nfc.NdefRecord.TNF_EMPTY, null, null, null);
// TODO(https://crbug.com/520391): Support external type records.
default:
case RECORD_TYPE_SMART_POSTER:
// TODO(https://crbug.com/520391): Support 'smart-poster' type records.
throw new InvalidNdefMessageException();
}
PairOfDomainAndType pair = parseDomainAndType(record.recordType);
if (pair != null) {
return android.nfc.NdefRecord.createExternal(pair.mDomain, pair.mType, record.data);
}
throw new InvalidNdefMessageException();
}
/**
......@@ -134,6 +151,9 @@ public final class NdefMessageUtils {
return createWellKnownRecord(ndefRecord);
case android.nfc.NdefRecord.TNF_UNKNOWN:
return createUnKnownRecord(ndefRecord.getPayload());
case android.nfc.NdefRecord.TNF_EXTERNAL_TYPE:
return createExternalTypeRecord(
new String(ndefRecord.getType(), "UTF-8"), ndefRecord.getPayload());
}
return null;
}
......@@ -222,6 +242,8 @@ public final class NdefMessageUtils {
return createTextRecord(record.getPayload());
}
// TODO(https://crbug.com/520391): Support RTD_SMART_POSTER type records.
return null;
}
......@@ -235,4 +257,40 @@ public final class NdefMessageUtils {
nfcRecord.data = payload;
return nfcRecord;
}
/**
* Constructs External type NdefRecord
*/
private static NdefRecord createExternalTypeRecord(String customType, byte[] payload) {
NdefRecord nfcRecord = new NdefRecord();
nfcRecord.recordType = customType;
nfcRecord.mediaType = OCTET_STREAM_MIME;
nfcRecord.data = payload;
return nfcRecord;
}
/**
* Parses the input custom type to get its domain and type.
* e.g. returns a pair ('w3.org', 'xyz') for the input 'w3.org:xyz'.
* Returns null for invalid input.
* https://w3c.github.io/web-nfc/#the-ndefrecordtype-string
*
* 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) {
int colonIndex = customType.indexOf(':');
if (colonIndex == -1) return null;
// TODO(ThisCL): verify |domain| is a valid FQDN, asking help at
// https://groups.google.com/a/chromium.org/forum/#!topic/chromium-dev/QN2mHt_WgHo.
String domain = customType.substring(0, colonIndex);
if (domain.isEmpty()) return null;
String type = customType.substring(colonIndex + 1);
if (type.isEmpty()) return null;
if (!type.matches("[a-zA-Z0-9()+,\\-:=@;$_!*'.]+")) return null;
return new PairOfDomainAndType(domain, type);
}
}
......@@ -87,6 +87,8 @@ public class NFCTest {
private ArgumentCaptor<int[]> mOnWatchCallbackCaptor;
// Constants used for the test.
private static final String DUMMY_EXTERNAL_RECORD_DOMAIN = "abc.com";
private static final String DUMMY_EXTERNAL_RECORD_TYPE = "xyz";
private static final String TEST_TEXT = "test";
private static final String TEST_URL = "https://google.com";
private static final String TEST_JSON = "{\"key1\":\"value1\",\"key2\":2}";
......@@ -275,7 +277,19 @@ public class NFCTest {
assertEquals(OCTET_STREAM_MIME, unknownMojoNdefMessage.data[0].mediaType);
assertEquals(TEST_TEXT, new String(unknownMojoNdefMessage.data[0].data));
// Test NdefMessage with WebNFC external type.
// Test external record conversion.
android.nfc.NdefMessage extNdefMessage = new android.nfc.NdefMessage(
android.nfc.NdefRecord.createExternal(DUMMY_EXTERNAL_RECORD_DOMAIN,
DUMMY_EXTERNAL_RECORD_TYPE, ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT)));
NdefMessage extMojoNdefMessage = NdefMessageUtils.toNdefMessage(extNdefMessage);
assertNull(extMojoNdefMessage.url);
assertEquals(1, extMojoNdefMessage.data.length);
assertEquals(DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE,
extMojoNdefMessage.data[0].recordType);
assertEquals(OCTET_STREAM_MIME, extMojoNdefMessage.data[0].mediaType);
assertEquals(TEST_TEXT, new String(extMojoNdefMessage.data[0].data));
// Test NdefMessage with an additional WebNFC author record.
android.nfc.NdefRecord jsonNdefRecord = android.nfc.NdefRecord.createMime(
JSON_MIME, ApiCompatibilityUtils.getBytesUtf8(TEST_JSON));
android.nfc.NdefRecord extNdefRecord =
......@@ -357,6 +371,22 @@ public class NFCTest {
assertEquals(
android.nfc.NdefRecord.TNF_EXTERNAL_TYPE, jsonNdefMessage.getRecords()[1].getTnf());
// Test external record conversion.
NdefRecord extMojoNdefRecord = new NdefRecord();
extMojoNdefRecord.recordType =
DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE;
extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
NdefMessage extMojoNdefMessage = createMojoNdefMessage(TEST_URL, extMojoNdefRecord);
android.nfc.NdefMessage extNdefMessage = NdefMessageUtils.toNdefMessage(extMojoNdefMessage);
assertEquals(2, extNdefMessage.getRecords().length);
assertEquals(
android.nfc.NdefRecord.TNF_EXTERNAL_TYPE, extNdefMessage.getRecords()[0].getTnf());
assertEquals(DUMMY_EXTERNAL_RECORD_DOMAIN + ':' + DUMMY_EXTERNAL_RECORD_TYPE,
new String(extNdefMessage.getRecords()[0].getType()));
assertEquals(TEST_TEXT, new String(extNdefMessage.getRecords()[0].getPayload()));
assertEquals(
android.nfc.NdefRecord.TNF_EXTERNAL_TYPE, extNdefMessage.getRecords()[1].getTnf());
// Test EMPTY record conversion.
NdefRecord emptyMojoNdefRecord = new NdefRecord();
emptyMojoNdefRecord.recordType = NdefMessageUtils.RECORD_TYPE_EMPTY;
......@@ -369,6 +399,21 @@ public class NFCTest {
emptyNdefMessage.getRecords()[1].getTnf());
}
/**
* Test external record conversion with invalid custom type.
*/
@Test(expected = InvalidNdefMessageException.class)
@Feature({"NFCTest"})
public void testInvalidExternalRecordType() throws InvalidNdefMessageException {
NdefRecord extMojoNdefRecord = new NdefRecord();
// '/' is not allowed.
extMojoNdefRecord.recordType = "abc.com:xyz/";
extMojoNdefRecord.data = ApiCompatibilityUtils.getBytesUtf8(TEST_TEXT);
NdefMessage extMojoNdefMessage = createMojoNdefMessage(TEST_URL, extMojoNdefRecord);
android.nfc.NdefMessage extNdefMessage = NdefMessageUtils.toNdefMessage(extMojoNdefMessage);
assertEquals(null, extNdefMessage);
}
/**
* Test that invalid NdefMessage is rejected with INVALID_MESSAGE error code.
*/
......
......@@ -13,6 +13,8 @@
#include "third_party/blink/renderer/platform/bindings/script_state.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/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
namespace blink {
......@@ -26,6 +28,45 @@ WTF::Vector<uint8_t> GetUTF8DataFromString(const String& string) {
return data;
}
// https://w3c.github.io/web-nfc/#the-ndefrecordtype-string
// Derives a formatted custom type for the external type record from |input|.
// Returns a null string for an invalid |input|.
//
// 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.
String ValidateCustomRecordType(const String& input) {
static const String kOtherCharsForCustomType("()+,-:=@;$_!*'.");
if (input.IsEmpty())
return String();
// Finds the separator ':'.
wtf_size_t colon_index = input.find(':');
if (colon_index == kNotFound)
return String();
// Derives the domain (FQDN) from the part before ':'.
String left = input.Left(colon_index);
bool success = false;
String domain = SecurityOrigin::CanonicalizeHost(left, &success);
if (!success || domain.IsEmpty())
return String();
// Validates the part after ':'.
String right = input.Substring(colon_index + 1);
if (right.length() == 0)
return String();
for (wtf_size_t i = 0; i < right.length(); i++) {
if (!IsASCIIAlphanumeric(right[i]) &&
!kOtherCharsForCustomType.Contains(right[i])) {
return String();
}
}
return domain + ':' + right;
}
static NDEFRecord* CreateTextRecord(const String& media_type,
const ScriptValue& data,
ExceptionState& exception_state) {
......@@ -141,6 +182,25 @@ static NDEFRecord* CreateOpaqueRecord(const String& media_type,
std::move(bytes));
}
static NDEFRecord* CreateExternalRecord(const String& custom_type,
const ScriptValue& data,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#dfn-map-external-data-to-ndef
if (data.IsEmpty() || !data.V8Value()->IsArrayBuffer()) {
exception_state.ThrowTypeError(
"The data for external type NDEFRecord must be an ArrayBuffer.");
return nullptr;
}
DOMArrayBuffer* array_buffer =
V8ArrayBuffer::ToImpl(data.V8Value().As<v8::Object>());
WTF::Vector<uint8_t> bytes;
bytes.Append(static_cast<uint8_t*>(array_buffer->Data()),
array_buffer->ByteLength());
return MakeGarbageCollected<NDEFRecord>(
custom_type, "application/octet-stream", std::move(bytes));
}
} // namespace
// static
......@@ -178,12 +238,18 @@ NDEFRecord* NDEFRecord::Create(const NDEFRecordInit* init,
return CreateJsonRecord(init->mediaType(), init->data(), exception_state);
} else if (record_type == "opaque") {
return CreateOpaqueRecord(init->mediaType(), init->data(), exception_state);
} else if (record_type == "smart-poster") {
// TODO(https://crbug.com/520391): Support creating smart-poster records.
exception_state.ThrowTypeError("smart-poster type is not supported yet");
return nullptr;
} else {
// TODO(https://crbug.com/520391): Support creating smart-poster and
// external type records.
String formated_type = ValidateCustomRecordType(record_type);
if (!formated_type.IsNull())
return CreateExternalRecord(formated_type, init->data(), exception_state);
}
exception_state.ThrowTypeError("Unknown NDEFRecord type.");
return nullptr;
}
}
NDEFRecord::NDEFRecord(const String& record_type,
......@@ -228,18 +294,24 @@ String NDEFRecord::toText() const {
}
DOMArrayBuffer* NDEFRecord::toArrayBuffer() const {
if (record_type_ != "json" && record_type_ != "opaque") {
if (record_type_ == "empty" || record_type_ == "text" ||
record_type_ == "url") {
return nullptr;
}
DCHECK(record_type_ == "json" || record_type_ == "opaque" ||
!ValidateCustomRecordType(record_type_).IsNull());
return DOMArrayBuffer::Create(data_.data(), data_.size());
}
ScriptValue NDEFRecord::toJSON(ScriptState* script_state,
ExceptionState& exception_state) const {
if (record_type_ != "json" && record_type_ != "opaque") {
if (record_type_ == "empty" || record_type_ == "text" ||
record_type_ == "url") {
return ScriptValue::CreateNull(script_state->GetIsolate());
}
DCHECK(record_type_ == "json" || record_type_ == "opaque" ||
!ValidateCustomRecordType(record_type_).IsNull());
ScriptState::Scope scope(script_state);
v8::Local<v8::Value> json_object = FromJSONString(
......
......@@ -63,7 +63,7 @@
'Modifying the original buffer does not affect toArrayBuffer() content');
assert_array_equals(new Uint8Array(data_3), original_data,
'Modifying the original buffer does not affect toArrayBuffer() content');
}, 'NDEFRecord constructor with opaque data');
}, 'NDEFRecord constructor with opaque record type');
test(() => {
const record = new NDEFRecord(createJsonRecord(test_json_data));
......@@ -82,7 +82,40 @@
'toJSON() again returns another new object');
assert_object_equals(data_2, test_json_data,
'toJSON() has the same content with the original dictionary');
}, 'NDEFRecord constructor with json data');
}, 'NDEFRecord constructor with JSON record type');
test(() => {
let buffer = new ArrayBuffer(4);
let buffer_view = new Uint8Array(buffer);
let original_data = new Uint8Array([1, 2, 3, 4]);
buffer_view.set(original_data);
const record = new NDEFRecord(createRecord('foo.eXamPle.coM:bAr*-', undefined, buffer));
assert_equals(record.recordType, 'foo.example.com:bAr*-', 'recordType');
assert_equals(record.mediaType, 'application/octet-stream', 'mediaType');
const data_1 = record.toArrayBuffer();
assert_true(data_1 instanceof ArrayBuffer);
assert_not_equals(data_1, buffer, 'toArrayBuffer() returns a new object');
assert_array_equals(new Uint8Array(data_1), original_data,
'toArrayBuffer() has the same content with the original buffer');
const data_2 = record.toArrayBuffer();
assert_true(data_2 instanceof ArrayBuffer);
assert_not_equals(data_2, data_1,
'toArrayBuffer() again returns another new object');
assert_array_equals(new Uint8Array(data_2), original_data,
'toArrayBuffer() has the same content with the original buffer');
buffer_view.set([4, 3, 2, 1]);
const data_3 = record.toArrayBuffer();
assert_true(data_3 instanceof ArrayBuffer);
assert_array_equals(new Uint8Array(data_1), original_data,
'Modifying the original buffer does not affect toArrayBuffer() content');
assert_array_equals(new Uint8Array(data_2), original_data,
'Modifying the original buffer does not affect toArrayBuffer() content');
assert_array_equals(new Uint8Array(data_3), original_data,
'Modifying the original buffer does not affect toArrayBuffer() content');
}, 'NDEFRecord constructor with external record type');
test(() => {
assert_throws(new TypeError, () => new NDEFRecord(createRecord('EMptY')),
......@@ -99,4 +132,17 @@
'Unknown record type.');
}, 'NDEFRecord constructor with record type string being treated as case sensitive');
test(() => {
assert_throws(new TypeError, () => new NDEFRecord(createRecord(
':xyz', '', test_buffer_data)), 'The domain should not be empty.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord(
'[:xyz', '', test_buffer_data)), '"[" is not a valid FQDN.');
assert_throws(new TypeError, () => new NDEFRecord(createRecord(
'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(
'example.com:xyz/', '', test_buffer_data)), 'The type should not contain \'/\'.');
}, 'NDEFRecord constructor with invalid external record type');
</script>
......@@ -47,6 +47,14 @@ const NFCReaderOptionTests =
unmatchedScanOptions: {recordType: "json"},
message: createMessage([createUrlRecord(test_url_data)])
},
{
desc: "Test that reading data succeed when NFCScanOptions'" +
" recordType is set to a custom type for external type records.",
scanOptions: {recordType: "w3.org:xyz"},
unmatchedScanOptions: {recordType: "opaque"},
message: createMessage([createRecord('w3.org:xyz', 'application/octet-stream',
test_buffer_data)])
},
{
desc: "Test that the url of NFCScanOptions filters relevant data" +
" sources correctly.",
......@@ -101,6 +109,14 @@ const ReadMultiMessagesTests =
message: createMessage([createUrlRecord(test_url_data)]),
unmatchedMessage: createMessage([createTextRecord(test_text_data)])
},
{
desc: "Test that filtering external record from different messages" +
" correctly with NFCScanOptions' recordType is set to the custom type.",
scanOptions: {recordType: "w3.org:xyz"},
message: createMessage([createRecord('w3.org:xyz', 'application/octet-stream',
test_buffer_data)]),
unmatchedMessage: createMessage([createTextRecord(test_text_data)])
},
{
desc: "Test that filtering 'text' record from different messages" +
" correctly with NFCScanOptions' url set.",
......
......@@ -48,7 +48,21 @@ const invalid_type_messages =
// NDEFRecord.data for 'opaque' record must be ArrayBuffer.
createMessage([createOpaqueRecord(test_text_data)]),
createMessage([createOpaqueRecord(test_number_data)]),
createMessage([createOpaqueRecord(test_json_data)])
createMessage([createOpaqueRecord(test_json_data)]),
// https://w3c.github.io/web-nfc/#dfn-map-external-data-to-ndef
// NDEFRecord must have data.
createMessage([createRecord('w3.org:xyz', '', undefined)]),
// NDEFRecord.data for external record must be ArrayBuffer.
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/#the-ndefrecordtype-string
// The record type is neither a known type ('text', 'json' etc.) nor a
// valid custom type for an external type record.
createMessage([createRecord('unmatched_type', '', test_buffer_data)])
];
const invalid_syntax_messages =
......@@ -266,11 +280,12 @@ nfc_test(async (t, mockNFC) => {
createJsonRecord(test_json_data),
createJsonRecord(test_number_data),
createOpaqueRecord(test_buffer_data),
createUrlRecord(test_url_data)],
createUrlRecord(test_url_data),
createRecord('w3.org:xyz', '', test_buffer_data)],
test_message_origin);
await writer.push(message);
assertNDEFMessagesEqual(message, mockNFC.pushedMessage());
}, "NFCWriter.push NDEFMessage containing text, json, opaque and url records \
}, "NFCWriter.push NDEFMessage containing text, json, opaque, url and external records \
with default NFCPushOptions.");
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