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
...@@ -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