Skip to content

Commit

Permalink
[webnfc] Support writing/reading local type records
Browse files Browse the repository at this point in the history
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:
w3c/web-nfc#491
w3c/web-nfc#493
w3c/web-nfc#495
w3c/web-nfc#502
w3c/web-nfc#506

BUG=520391

Change-Id: Ic2890c031109aa583437ac93a8901ff71992af78
  • Loading branch information
Leon Han authored and chromium-wpt-export-bot committed Jan 14, 2020
1 parent b122602 commit ed2f8d7
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 36 deletions.
10 changes: 6 additions & 4 deletions resources/chromium/nfc-mock.js
Expand Up @@ -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;
Expand Down
33 changes: 21 additions & 12 deletions web-nfc/NDEFReader_scan.https.html
Expand Up @@ -146,23 +146,32 @@
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.");
Expand Down
136 changes: 125 additions & 11 deletions web-nfc/NDEFRecord_constructor.https.html
Expand Up @@ -83,7 +83,7 @@
assert_equals(record.lang, null, 'lang');
assert_equals(record.data, null, 'data');
assert_throws('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(() => {
Expand All @@ -97,7 +97,7 @@
assert_equals(decoder.decode(record.data), test_text_data,
'data has the same content with the original dictionary');
assert_throws('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(() => {
Expand All @@ -113,7 +113,7 @@
assert_equals(decoder.decode(record.data), test_text_data,
'data has the same content with the original dictionary');
assert_throws('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(() => {
Expand All @@ -129,7 +129,7 @@
assert_equals(decoder.decode(record.data), test_text_data,
'data has the same content with the original dictionary');
assert_throws('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(() => {
Expand Down Expand Up @@ -175,7 +175,7 @@
assert_equals(decoder.decode(record.data), test_url_data,
'data has the same content with the original dictionary');
assert_throws('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(() => {
Expand All @@ -187,7 +187,7 @@
assert_equals(decoder.decode(record.data), test_url_data,
'data has the same content with the original dictionary');
assert_throws('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(() => {
Expand All @@ -206,7 +206,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [1, 2, 3, 4],
'data has the same content with the original buffer');
assert_throws('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.
{
Expand All @@ -217,7 +217,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [2, 3, 4],
'data has the same content with the original buffer view');
assert_throws('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');

Expand All @@ -230,7 +230,7 @@
test_json_data,
'data has the same content with the original json');
assert_throws('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(() => {
Expand All @@ -248,7 +248,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [1, 2, 3, 4],
'data has the same content with the original buffer');
assert_throws('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.
{
Expand All @@ -259,7 +259,7 @@
assert_array_equals(new Uint8Array(record.data.buffer), [2, 3, 4],
'data has the same content with the original buffer view');
assert_throws('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');

Expand Down Expand Up @@ -314,6 +314,77 @@
}
}, 'NDEFRecord constructor with external record type');

test(() => {
assert_throws(new 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(new 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(new TypeError, () => new NDEFRecord(createRecord('EMptY')),
'Unknown record type.');
Expand Down Expand Up @@ -354,4 +425,47 @@
'example.com:xyz/', test_buffer_data)), 'The type should not contain \'/\'.');
}, 'NDEFRecord constructor with invalid external record type');

test(() => {
assert_throws(new 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(new 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(new 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(new TypeError, () => new NDEFRecord(external_record_embedding_local_record),
'The local type must start with a \':\'.');

local_record.recordType = ':Xyz';
assert_throws(new 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(new 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>
39 changes: 30 additions & 9 deletions web-nfc/NDEFWriter_push.https.html
Expand Up @@ -72,14 +72,23 @@
// 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)])
];

Expand Down Expand Up @@ -283,22 +292,34 @@
and external records with default NDEFPushOptions.");

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.push(message);
const pushed_message = mockNFC.pushedMessage();

// 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.push NDEFMessage containing embedded records.");

nfc_test(async (t, mockNFC) => {
Expand Down

0 comments on commit ed2f8d7

Please sign in to comment.