mojo/syncbase: Implementing naming escape and unescape
in Dart.
encodeAsNameElement makes a string representable as
a name element by escaping slashes.
decodeAsNameElement decodes an encoded name element.
also some reorganization to separate integration and
unit tests now that we have unit tests.
Change-Id: I7d266a8a895e5556096d68c8177202ef237f696c
diff --git a/Makefile b/Makefile
index e19bbef..ff8e70e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
PWD := $(shell pwd)
DART_FILES := $(shell find dart/bin dart/lib dart/test sky_demo/lib -name "*.dart" -not -path "dart/lib/gen/*")
-GO_FILES := $(shell find go/src -name "*.go")
+GO_FILES := $(shell find gen/go/src -name "*.go")
V23_GO_FILES := $(shell find $(V23_ROOT) -name "*.go")
include ../shared/mojo.mk
@@ -131,7 +131,14 @@
$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/sky_demo/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) 'mojo:window_manager https://mojo.v.io/sky_demo/lib/main.dart'
.PHONY: test
-test: dart/packages $(ETHER_BUILD_DIR)/syncbase_server.mojo gen-mojom | syncbase-env-check
+test: test-unit test-integration
+
+.PHONY: test-unit
+test-unit: dart/packages
+ cd dart && pub run test test/unit
+
+.PHONY: test-integration
+test-integration: dart/packages $(ETHER_BUILD_DIR)/syncbase_server.mojo gen-mojom | syncbase-env-check
$(MOJO_DIR)/src/mojo/devtools/common/mojo_test --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) --shell-path $(MOJO_SHELL_PATH) tests
.PHONY: syncbase-env-check
diff --git a/README.md b/README.md
index e21f655..ecaae0e 100644
--- a/README.md
+++ b/README.md
@@ -91,7 +91,7 @@
The following command will run a single test file. This is useful when the
full test suite hangs with no output.
- $(MOJO_DIR)/src/mojo/devtools/common/mojo_run -v --enable-multiprocess --shell-path $(MOJO_DIR)/src/out/Debug/mojo_shell dart/test/<filename>
+ $(MOJO_DIR)/src/mojo/devtools/common/mojo_run -v --enable-multiprocess --shell-path $(MOJO_DIR)/src/out/Debug/mojo_shell dart/test/integration/<filename>
[architecture proposal]: https://docs.google.com/document/d/1TyxPYIhj9VBCtY7eAXu_MEV9y0dtRx7n7UY4jm76Qq4/edit
[depot tools]: http://www.chromium.org/developers/how-tos/install-depot-tools
diff --git a/dart/lib/src/naming/util.dart b/dart/lib/src/naming/util.dart
new file mode 100644
index 0000000..2d14018
--- /dev/null
+++ b/dart/lib/src/naming/util.dart
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:convert' show UTF8;
+
+// TODO(aghassemi): Move these naming utilities outside of Syncbase when we have
+// a Dart Vanadium library
+
+// Makes a string representable as a name element by escaping slashes.
+String encodeAsNameElement(String name) {
+ return escape(name, '/');
+}
+
+// Decodes an encoded name element.
+// It throws exception when encountering wrongly encoded names.
+// Note that this is more than the inverse of EncodeAsNameElement since it
+// can handle more hex encodings than / and %.
+// This is intentional since we'll most likely want to add other letters to
+// the set to be encoded.
+String decodeAsNameElement(String name) {
+ return unescape(name);
+}
+
+// Encodes a string replacing the characters in <special> and % with a
+// %<hex> escape.
+String escape(String text, String special) {
+ const String encodedPecent = '%25';
+ const String percent = '%';
+
+ // Replace % with %25 first.
+ var encodedText = text.replaceAll(percent, encodedPecent);
+
+ // For all characters in special, replace occurrences with their hex encoding.
+ // Note that we want to support any Unicode character in special, single-byte
+ // or not. For example a three-byte 읔 character, becomes %EC%9D%94.
+ for (var i = 0; i < special.length; i++) {
+ var char = special[i];
+ if (char == '%') {
+ // Ignore % in special, we have already escaped %.
+ continue;
+ }
+ var bytes = UTF8.encoder.convert(char);
+ var hex = '';
+ for (var byte in bytes) {
+ hex += percent;
+ hex += _byteToHex(byte);
+ }
+ encodedText = encodedText.replaceAll(char, hex);
+ }
+
+ return encodedText;
+}
+
+// Decodes %<hex> encodings in a string into the relevant character.
+// It throws exception when encountering wrongly encoded text.
+String unescape(String text) {
+ // Note that this function is a slightly modified version of _uriDecode() code
+ // in Dart sdk/lib/core/uri.dart (https://goo.gl/1ppJIj).
+ // The biggest difference is that, unlike _uriDecode(), our code DOES NOT
+ // expect non-ASCII characters to have been percent encoded in text.
+ // Dart's _uriDecode() however expects character above 127 to have been
+ // percent encoded or it will throw an argument exception when it encounters a
+ // character above 127.
+ const int percent = 0x25;
+
+ // First check whether there are any characters which need special handling.
+ bool notEncoded = true;
+ for (int i = 0; i < text.length && notEncoded; i++) {
+ var codeUnit = text.codeUnitAt(i);
+ notEncoded = codeUnit != percent;
+ }
+ if (notEncoded) {
+ return text;
+ }
+ List<int> bytes = new List();
+ for (int i = 0; i < text.length; i++) {
+ var codeUnit = text.codeUnitAt(i);
+ if (codeUnit == percent) {
+ if (i + 3 > text.length) {
+ throw new ArgumentError('Trucated or malformed encoded string');
+ }
+ bytes.add(_hexCharPairToByte(text, i + 1));
+ i += 2;
+ } else {
+ bytes.addAll(UTF8.encoder.convert(text[i]));
+ }
+ }
+ return UTF8.decode(bytes);
+}
+
+// Converts a byte to 0 padded hex string.
+// Note that this function is the same as byteToHex() code
+// in Dart sdk/lib/core/uri.dart (https://goo.gl/1ppJIj)
+String _byteToHex(int byte) {
+ const String hex = '0123456789ABCDEF';
+ var buffer = new StringBuffer();
+ buffer.writeCharCode(hex.codeUnitAt(byte >> 4));
+ buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f));
+ return buffer.toString();
+}
+
+// Converts a hex string to a byte.
+// Note that this function is the same code as _hexCharPairToByte()
+// in Dart sdk/lib/core/uri.dart (https://goo.gl/1ppJIj)
+int _hexCharPairToByte(String s, int pos) {
+ int byte = 0;
+ for (int i = 0; i < 2; i++) {
+ var charCode = s.codeUnitAt(pos + i);
+ if (0x30 <= charCode && charCode <= 0x39) {
+ byte = byte * 16 + charCode - 0x30;
+ } else {
+ // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66).
+ charCode |= 0x20;
+ if (0x61 <= charCode && charCode <= 0x66) {
+ byte = byte * 16 + charCode - 0x57;
+ } else {
+ throw new ArgumentError("Trucated or malformed encoded string");
+ }
+ }
+ }
+ return byte;
+}
diff --git a/dart/test/syncbase_app_test.dart b/dart/test/integration/syncbase_app_test.dart
similarity index 100%
rename from dart/test/syncbase_app_test.dart
rename to dart/test/integration/syncbase_app_test.dart
diff --git a/dart/test/syncbase_database_test.dart b/dart/test/integration/syncbase_database_test.dart
similarity index 93%
rename from dart/test/syncbase_database_test.dart
rename to dart/test/integration/syncbase_database_test.dart
index 8e7b697..279d3fc 100644
--- a/dart/test/syncbase_database_test.dart
+++ b/dart/test/integration/syncbase_database_test.dart
@@ -16,7 +16,7 @@
var dbName = utils.uniqueName('db');
var db = app.noSqlDatabase(dbName);
expect(db.relativeName, equals(dbName));
- expect(db.fullName, equals(app.fullName + '/' + dbName));
+ expect(db.fullName, equals(app.fullName + '/\$/' + dbName));
});
test('creating and destroying a database', () async {
diff --git a/dart/test/syncbase_row_test.dart b/dart/test/integration/syncbase_row_test.dart
similarity index 95%
rename from dart/test/syncbase_row_test.dart
rename to dart/test/integration/syncbase_row_test.dart
index 7397f2c..3b32141 100644
--- a/dart/test/syncbase_row_test.dart
+++ b/dart/test/integration/syncbase_row_test.dart
@@ -22,7 +22,7 @@
var row = table.row(rowName);
expect(row.relativeName, equals(rowName));
- expect(row.fullName, equals(table.fullName + '/' + rowName));
+ expect(row.fullName, equals(table.fullName + '/\$/' + rowName));
});
test('putting, getting and deleting row', () async {
diff --git a/dart/test/syncbase_table_test.dart b/dart/test/integration/syncbase_table_test.dart
similarity index 98%
rename from dart/test/syncbase_table_test.dart
rename to dart/test/integration/syncbase_table_test.dart
index a590885..de8b6d0 100644
--- a/dart/test/syncbase_table_test.dart
+++ b/dart/test/integration/syncbase_table_test.dart
@@ -19,7 +19,7 @@
var tableName = utils.uniqueName('table');
var table = db.table(tableName);
expect(table.relativeName, equals(tableName));
- expect(table.fullName, equals(db.fullName + '/' + tableName));
+ expect(table.fullName, equals(db.fullName + '/\$/' + tableName));
});
test('creating and destroying a table', () async {
diff --git a/dart/test/syncbase_test.dart b/dart/test/integration/syncbase_test.dart
similarity index 100%
rename from dart/test/syncbase_test.dart
rename to dart/test/integration/syncbase_test.dart
diff --git a/dart/test/utils.dart b/dart/test/integration/utils.dart
similarity index 100%
rename from dart/test/utils.dart
rename to dart/test/integration/utils.dart
diff --git a/dart/test/unit/naming_util_test.dart b/dart/test/unit/naming_util_test.dart
new file mode 100644
index 0000000..e946912
--- /dev/null
+++ b/dart/test/unit/naming_util_test.dart
@@ -0,0 +1,46 @@
+library naming_util_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/src/naming/util.dart' as naming;
+
+main() {
+ var tests = [
+ ['', ''],
+ ['/', '%2F'],
+ ['%', '%25'],
+ ['/The % rain in /% Spain', '%2FThe %25 rain in %2F%25 Spain'],
+ ['/%/%', '%2F%25%2F%25'],
+ ['ᚸӲ읔+קAل', 'ᚸӲ읔+קAل'],
+ ['ᚸ/Ӳ%읔/ק%A+ل', 'ᚸ%2FӲ%25읔%2Fק%25A+ل'],
+ ];
+
+ test('encoding name elements', () {
+ for (var t in tests) {
+ expect(naming.encodeAsNameElement(t[0]), equals(t[1]));
+ }
+ });
+
+ test('decoding name elements encoded by our encoder', () {
+ for (var t in tests) {
+ expect(naming.decodeAsNameElement(t[1]), equals(t[0]));
+ }
+ });
+
+ test('decoding name elements encoded by third-party percent encoder', () {
+ for (var t in tests) {
+ var uriEncoded = Uri.encodeComponent(t[0]);
+ expect(naming.decodeAsNameElement(uriEncoded), equals(t[0]));
+ }
+ });
+
+ test('escape and unescape with multiple special characters', () {
+ var test = 'ᚸ/Ӳ%읔/ק%A+ل';
+ // Escape multiple characters, also % in special should be ignored since
+ // % is always escaped regardless.
+ var escaped = naming.escape(test, '/읔%');
+ expect(escaped, isNot(contains('읔')));
+ expect(escaped, contains('Ӳ'));
+ expect(naming.unescape(escaped), equals(test));
+ });
+}
diff --git a/tests b/tests
index 170b9be..02e7a9c 100644
--- a/tests
+++ b/tests
@@ -2,7 +2,7 @@
tests = [
{
- "test": "https://test.mojo.v.io/syncbase_test.dart",
+ "test": "https://test.mojo.v.io/integration/syncbase_test.dart",
"type": "dart",
"timeout": 30
}